Compare commits

...

2 Commits

Author SHA1 Message Date
Alexander Whitestone
b505d6d65b feat: wire MemoryInspect into app.js
Some checks failed
CI / test (pull_request) Failing after 9s
CI / validate (pull_request) Failing after 13s
Review Approval Gate / verify-review (pull_request) Failing after 2s
- Import MemoryInspect component
- Initialize after SpatialMemory
- Add crystal raycasting in mousedown handler
- Click crystal to open inspect panel, click empty space to close
Closes #1227
2026-04-11 17:11:13 -04:00
Alexander Whitestone
a1e7af36f2 feat: add MemoryInspect panel component
Click-to-read detail view for Mnemosyne memory crystals.
- Slides in from right on crystal click
- Shows content, region, vitality band, linked memories
- Copy-to-clipboard action
- Navigable link list (click to inspect linked memory)
- Closes #1227
2026-04-11 17:10:42 -04:00
2 changed files with 428 additions and 4 deletions

25
app.js
View File

@@ -6,6 +6,7 @@ import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
import { SpatialMemory } from './nexus/components/spatial-memory.js';
import { MemoryBirth } from './nexus/components/memory-birth.js';
import { MemoryOptimizer } from './nexus/components/memory-optimizer.js';
import { MemoryInspect } from './nexus/components/memory-inspect.js';
// ═══════════════════════════════════════════
// NEXUS v1.1 — Portal System Update
@@ -712,6 +713,7 @@ async function init() {
MemoryBirth.init(scene);
MemoryBirth.wrapSpatialMemory(SpatialMemory);
SpatialMemory.setCamera(camera);
MemoryInspect.init(SpatialMemory);
updateLoad(90);
loadSession();
@@ -1904,7 +1906,7 @@ function setupControls() {
orbitState.lastX = e.clientX;
orbitState.lastY = e.clientY;
// Raycasting for portals
// Raycasting for portals and memory crystals
if (!portalOverlayActive) {
const mouse = new THREE.Vector2(
(e.clientX / window.innerWidth) * 2 - 1,
@@ -1912,11 +1914,26 @@ function setupControls() {
);
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(portals.map(p => p.ring));
if (intersects.length > 0) {
const clickedRing = intersects[0].object;
// Check portals first
const portalHits = raycaster.intersectObjects(portals.map(p => p.ring));
if (portalHits.length > 0) {
const clickedRing = portalHits[0].object;
const portal = portals.find(p => p.ring === clickedRing);
if (portal) activatePortal(portal);
} else {
// Check memory crystals
const crystalMeshes = SpatialMemory.getCrystalMeshes();
const crystalHits = raycaster.intersectObjects(crystalMeshes);
if (crystalHits.length > 0) {
const hitMesh = crystalHits[0].object;
const memInfo = SpatialMemory.getMemoryFromMesh(hitMesh);
if (memInfo && memInfo.data) {
MemoryInspect.inspect(memInfo.data.id);
}
} else if (MemoryInspect.isOpen()) {
MemoryInspect.close();
}
}
}
}

View File

@@ -0,0 +1,407 @@
/**
* Memory Inspect Panel — click-to-read detail view for Mnemosyne crystals.
*
* When a memory crystal is clicked in the Nexus, this panel slides in from
* the right showing the memory's content, links, region, and metadata.
*
* Depends on SpatialMemory (for data access) — wired from app.js.
*/
const MemoryInspect = (() => {
let _panel = null;
let _isOpen = false;
let _currentMemId = null;
let _spatialMemory = null;
// ─── PUBLIC API ──────────────────────────────────────
function init(spatialMemoryRef) {
_spatialMemory = spatialMemoryRef;
_buildPanel();
_injectStyles();
console.info('[Mnemosyne] Memory Inspect panel initialized');
}
function inspect(memId) {
if (!_spatialMemory) return;
const memObj = _getMemoryObject(memId);
if (!memObj) return;
_currentMemId = memId;
_renderPanel(memObj);
_open();
_spatialMemory.highlightMemory(memId);
}
function close() {
if (!_isOpen) return;
if (_currentMemId && _spatialMemory) {
_spatialMemory.clearHighlight();
}
_currentMemId = null;
_close();
}
function isOpen() {
return _isOpen;
}
function getCurrentMemId() {
return _currentMemId;
}
// ─── INTERNALS ───────────────────────────────────────
function _getMemoryObject(memId) {
// Access SpatialMemory's internal _memoryObjects via getAllMemories
const all = _spatialMemory.getAllMemories();
if (!all) return null;
// getAllMemories returns array of { id, ...data } objects
const entry = all.find(m => m.id === memId);
if (!entry) return null;
// Get region info
const regions = _spatialMemory.REGIONS;
const region = regions[entry.category] || regions.working;
return { data: entry, region };
}
function _buildPanel() {
_panel = document.createElement('div');
_panel.id = 'memory-inspect-panel';
_panel.className = 'memory-inspect-panel memory-inspect-hidden';
_panel.innerHTML = `
<div class="memory-inspect-header">
<span class="memory-inspect-glyph" id="inspect-glyph"></span>
<span class="memory-inspect-title" id="inspect-title"></span>
<button class="memory-inspect-close" id="inspect-close" title="Close">&times;</button>
</div>
<div class="memory-inspect-body">
<div class="memory-inspect-meta" id="inspect-meta"></div>
<div class="memory-inspect-content" id="inspect-content"></div>
<div class="memory-inspect-links" id="inspect-links"></div>
</div>
<div class="memory-inspect-footer" id="inspect-footer"></div>
`;
document.body.appendChild(_panel);
document.getElementById('inspect-close').addEventListener('click', (e) => {
e.stopPropagation();
close();
});
}
function _renderPanel(memObj) {
const { data, region } = memObj;
const strength = data.strength != null ? data.strength : 0.7;
const vitalityBand = _getVitalityBand(strength);
const bandColors = {
vibrant: '#00e5ff',
alive: '#4488ff',
fading: '#ffaa00',
dim: '#ff6644',
ghost: '#667788'
};
const bandColor = bandColors[vitalityBand] || bandColors.alive;
// Header
document.getElementById('inspect-glyph').textContent = region.glyph || '\uD83D\uDCCB';
document.getElementById('inspect-title').textContent = data.content
? (data.content.length > 60 ? data.content.slice(0, 60) + '\u2026' : data.content)
: data.id || 'Memory';
// Meta bar
const metaEl = document.getElementById('inspect-meta');
const catLabel = region.label || data.category || 'Unknown';
const created = data.timestamp ? _formatTime(data.timestamp) : '';
metaEl.innerHTML = `
<span class="inspect-badge" style="border-color:${'#' + region.color.toString(16).padStart(6,'0')}">${catLabel}</span>
<span class="inspect-vitality" style="color:${bandColor}">
\u25CF ${vitalityBand} (${Math.round(strength * 100)}%)
</span>
${created ? `<span class="inspect-time">${created}</span>` : ''}
`;
// Content
const contentEl = document.getElementById('inspect-content');
contentEl.textContent = data.content || '(no content)';
// Links
const linksEl = document.getElementById('inspect-links');
const connections = data.connections || [];
if (connections.length > 0) {
let linksHtml = '<div class="inspect-links-header">Linked Memories (' + connections.length + ')</div>';
linksHtml += '<div class="inspect-links-list">';
connections.forEach(cid => {
const linked = _getLinkedPreview(cid);
linksHtml += `<div class="inspect-link-item" data-mem-id="${cid}">
<span class="inspect-link-dot"></span>
<span class="inspect-link-text">${linked}</span>
</div>`;
});
linksHtml += '</div>';
linksEl.innerHTML = linksHtml;
// Wire click handlers for linked memories
linksEl.querySelectorAll('.inspect-link-item').forEach(el => {
el.addEventListener('click', () => {
const targetId = el.dataset.memId;
if (targetId) inspect(targetId);
});
});
} else {
linksEl.innerHTML = '<div class="inspect-no-links">No linked memories</div>';
}
// Footer
const footerEl = document.getElementById('inspect-footer');
footerEl.innerHTML = `
<button class="inspect-action-btn" id="inspect-copy-btn" title="Copy content">\uD83D\uDCCB Copy</button>
<span class="inspect-id" title="Memory ID">${data.id || ''}</span>
`;
document.getElementById('inspect-copy-btn').addEventListener('click', () => {
navigator.clipboard.writeText(data.content || '').then(() => {
const btn = document.getElementById('inspect-copy-btn');
btn.textContent = '\u2713 Copied';
setTimeout(() => { btn.textContent = '\uD83D\uDCCB Copy'; }, 1500);
});
});
}
function _getLinkedPreview(memId) {
if (!_spatialMemory) return memId;
const all = _spatialMemory.getAllMemories();
if (!all) return memId;
const entry = all.find(m => m.id === memId);
if (!entry || !entry.content) return memId;
return entry.content.length > 40 ? entry.content.slice(0, 40) + '\u2026' : entry.content;
}
function _getVitalityBand(strength) {
if (strength >= 0.8) return 'vibrant';
if (strength >= 0.5) return 'alive';
if (strength >= 0.25) return 'fading';
if (strength >= 0.1) return 'dim';
return 'ghost';
}
function _formatTime(isoStr) {
try {
const d = new Date(isoStr);
const now = new Date();
const diffMs = now - d;
const diffMin = Math.floor(diffMs / 60000);
if (diffMin < 1) return 'just now';
if (diffMin < 60) return diffMin + 'm ago';
const diffHr = Math.floor(diffMin / 60);
if (diffHr < 24) return diffHr + 'h ago';
const diffDay = Math.floor(diffHr / 24);
if (diffDay < 30) return diffDay + 'd ago';
return d.toLocaleDateString();
} catch {
return '';
}
}
function _open() {
_isOpen = true;
_panel.classList.remove('memory-inspect-hidden');
_panel.classList.add('memory-inspect-visible');
}
function _close() {
_isOpen = false;
_panel.classList.remove('memory-inspect-visible');
_panel.classList.add('memory-inspect-hidden');
}
function _injectStyles() {
if (document.getElementById('memory-inspect-styles')) return;
const style = document.createElement('style');
style.id = 'memory-inspect-styles';
style.textContent = `
.memory-inspect-panel {
position: fixed;
top: 60px;
right: 0;
width: 340px;
max-height: calc(100vh - 80px);
background: rgba(8, 12, 32, 0.92);
border: 1px solid rgba(74, 240, 192, 0.2);
border-right: none;
border-radius: 8px 0 0 8px;
color: #c8d8e8;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 13px;
z-index: 900;
display: flex;
flex-direction: column;
backdrop-filter: blur(12px);
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.5);
transition: transform 0.25s ease, opacity 0.25s ease;
overflow: hidden;
}
.memory-inspect-hidden {
transform: translateX(100%);
opacity: 0;
pointer-events: none;
}
.memory-inspect-visible {
transform: translateX(0);
opacity: 1;
pointer-events: auto;
}
.memory-inspect-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border-bottom: 1px solid rgba(74, 240, 192, 0.15);
background: rgba(74, 240, 192, 0.05);
}
.memory-inspect-glyph {
font-size: 18px;
flex-shrink: 0;
}
.memory-inspect-title {
flex: 1;
font-weight: 600;
color: #4af0c0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.memory-inspect-close {
background: none;
border: none;
color: #667788;
font-size: 20px;
cursor: pointer;
padding: 0 4px;
line-height: 1;
}
.memory-inspect-close:hover {
color: #ff4466;
}
.memory-inspect-body {
flex: 1;
overflow-y: auto;
padding: 12px;
}
.memory-inspect-meta {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 12px;
font-size: 11px;
}
.inspect-badge {
padding: 2px 8px;
border: 1px solid;
border-radius: 4px;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.inspect-vitality {
font-size: 11px;
}
.inspect-time {
color: #556677;
font-size: 10px;
}
.memory-inspect-content {
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 6px;
padding: 10px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
margin-bottom: 12px;
max-height: 200px;
overflow-y: auto;
}
.inspect-links-header {
font-size: 11px;
color: #7b5cff;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 6px;
}
.inspect-links-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.inspect-link-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
background: rgba(123, 92, 255, 0.08);
border: 1px solid rgba(123, 92, 255, 0.15);
border-radius: 4px;
cursor: pointer;
font-size: 11px;
transition: background 0.15s;
}
.inspect-link-item:hover {
background: rgba(123, 92, 255, 0.18);
}
.inspect-link-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #7b5cff;
flex-shrink: 0;
}
.inspect-link-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.inspect-no-links {
color: #445566;
font-size: 11px;
font-style: italic;
}
.memory-inspect-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-top: 1px solid rgba(74, 240, 192, 0.1);
}
.inspect-action-btn {
background: rgba(74, 240, 192, 0.1);
border: 1px solid rgba(74, 240, 192, 0.25);
color: #4af0c0;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
font-family: inherit;
transition: background 0.15s;
}
.inspect-action-btn:hover {
background: rgba(74, 240, 192, 0.2);
}
.inspect-id {
color: #334455;
font-size: 9px;
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`;
document.head.appendChild(style);
}
return { init, inspect, close, isOpen, getCurrentMemId };
})();
export { MemoryInspect };