diff --git a/app.js b/app.js index ed5c348..28e9085 100644 --- a/app.js +++ b/app.js @@ -1909,7 +1909,7 @@ function setupControls() { const memInfo = SpatialMemory.getMemoryFromMesh(hitMesh); if (memInfo) { SpatialMemory.highlightMemory(memInfo.data.id); - showMemoryPanel(memInfo); + showMemoryPanel(memInfo, e.clientX, e.clientY); return; } } @@ -2576,46 +2576,162 @@ let pulseTimer = 0; // MNEMOSYNE — MEMORY CRYSTAL INSPECTION // ═══════════════════════════════════════════ +// ── pin state for memory panel ── +let _memPanelPinned = false; + +/** Convert a packed hex color integer to "r,g,b" string for CSS rgba(). */ +function _hexToRgb(hex) { + return ((hex >> 16) & 255) + ',' + ((hex >> 8) & 255) + ',' + (hex & 255); +} + /** - * Show the memory inspection panel with data from a clicked crystal. + * Position the panel near the screen click coordinates, keeping it on-screen. */ -function showMemoryPanel(memInfo) { +function _positionPanel(panel, clickX, clickY) { + const W = window.innerWidth; + const H = window.innerHeight; + const panelW = 356; // matches CSS width + padding + const panelH = 420; // generous estimate + const margin = 12; + + let left = clickX + 24; + if (left + panelW > W - margin) left = clickX - panelW - 24; + left = Math.max(margin, Math.min(W - panelW - margin, left)); + + let top = clickY - 80; + top = Math.max(margin, Math.min(H - panelH - margin, top)); + + panel.style.right = 'auto'; + panel.style.top = top + 'px'; + panel.style.left = left + 'px'; + panel.style.transform = 'none'; +} + +/** + * Navigate to (highlight + show panel for) a memory crystal by id. + */ +function _navigateToMemory(memId) { + SpatialMemory.highlightMemory(memId); + addChatMessage('system', `Focus: ${memId.replace(/_/g, ' ')}`); + const meshes = SpatialMemory.getCrystalMeshes(); + for (const mesh of meshes) { + if (mesh.userData && mesh.userData.memId === memId) { + const memInfo = SpatialMemory.getMemoryFromMesh(mesh); + if (memInfo) { showMemoryPanel(memInfo); break; } + } + } +} + +/** + * Show the holographic detail panel for a clicked crystal. + * @param {object} memInfo — { data, region } from SpatialMemory.getMemoryFromMesh() + * @param {number} [clickX] — screen X of the click (for panel positioning) + * @param {number} [clickY] — screen Y of the click + */ +function showMemoryPanel(memInfo, clickX, clickY) { const panel = document.getElementById('memory-panel'); if (!panel) return; const { data, region } = memInfo; const regionDef = SpatialMemory.REGIONS[region] || SpatialMemory.REGIONS.working; + const colorHex = regionDef.color.toString(16).padStart(6, '0'); + const colorRgb = _hexToRgb(regionDef.color); + // Header — region dot + label document.getElementById('memory-panel-region').textContent = regionDef.label; - document.getElementById('memory-panel-region-dot').style.background = '#' + regionDef.color.toString(16).padStart(6, '0'); + document.getElementById('memory-panel-region-dot').style.background = '#' + colorHex; + + // Category badge + const badge = document.getElementById('memory-panel-category-badge'); + if (badge) { + badge.textContent = (data.category || region || 'memory').toUpperCase(); + badge.style.background = 'rgba(' + colorRgb + ',0.16)'; + badge.style.color = '#' + colorHex; + badge.style.borderColor = 'rgba(' + colorRgb + ',0.4)'; + } + + // Entity name (humanised id) + const entityEl = document.getElementById('memory-panel-entity-name'); + if (entityEl) entityEl.textContent = (data.id || '\u2014').replace(/_/g, ' '); + + // Fact content document.getElementById('memory-panel-content').textContent = data.content || '(empty)'; + + // Trust score bar + const strength = data.strength != null ? data.strength : 0.7; + const trustFill = document.getElementById('memory-panel-trust-fill'); + const trustVal = document.getElementById('memory-panel-trust-value'); + if (trustFill) { + trustFill.style.width = (strength * 100).toFixed(0) + '%'; + trustFill.style.background = '#' + colorHex; + } + if (trustVal) trustVal.textContent = (strength * 100).toFixed(0) + '%'; + + // Meta rows document.getElementById('memory-panel-id').textContent = data.id || '\u2014'; document.getElementById('memory-panel-source').textContent = data.source || 'unknown'; document.getElementById('memory-panel-time').textContent = data.timestamp ? new Date(data.timestamp).toLocaleString() : '\u2014'; + // Related entities — clickable links const connEl = document.getElementById('memory-panel-connections'); connEl.innerHTML = ''; if (data.connections && data.connections.length > 0) { data.connections.forEach(cid => { - const tag = document.createElement('span'); - tag.className = 'memory-conn-tag'; - tag.textContent = cid; - connEl.appendChild(tag); + const btn = document.createElement('button'); + btn.className = 'memory-conn-tag memory-conn-link'; + btn.textContent = cid.replace(/_/g, ' '); + btn.title = 'Go to: ' + cid; + btn.addEventListener('click', (ev) => { ev.stopPropagation(); _navigateToMemory(cid); }); + connEl.appendChild(btn); }); } else { connEl.innerHTML = 'None'; } + // Pin button — reset on fresh open + _memPanelPinned = false; + const pinBtn = document.getElementById('memory-panel-pin'); + if (pinBtn) { + pinBtn.classList.remove('pinned'); + pinBtn.title = 'Pin panel'; + pinBtn.onclick = () => { + _memPanelPinned = !_memPanelPinned; + pinBtn.classList.toggle('pinned', _memPanelPinned); + pinBtn.title = _memPanelPinned ? 'Unpin panel' : 'Pin panel'; + }; + } + + // Positioning — near click if coords provided + if (clickX != null && clickY != null) { + _positionPanel(panel, clickX, clickY); + } + + // Fade in + panel.classList.remove('memory-panel-fade-out'); panel.style.display = 'flex'; } /** - * Dismiss the memory panel and clear any crystal highlight. + * Dismiss the panel (respects pin). Called on empty-space click. */ function dismissMemoryPanel() { + if (_memPanelPinned) return; + _dismissMemoryPanelForce(); +} + +/** + * Force-dismiss the panel regardless of pin state. Used by the close button. + */ +function _dismissMemoryPanelForce() { + _memPanelPinned = false; SpatialMemory.clearHighlight(); const panel = document.getElementById('memory-panel'); - if (panel) panel.style.display = 'none'; + if (!panel || panel.style.display === 'none') return; + panel.classList.add('memory-panel-fade-out'); + setTimeout(() => { + panel.style.display = 'none'; + panel.classList.remove('memory-panel-fade-out'); + }, 200); } diff --git a/index.html b/index.html index 715ca34..b7e3c0b 100644 --- a/index.html +++ b/index.html @@ -212,16 +212,26 @@
diff --git a/style.css b/style.css index 5c5a39e..4d01e4a 100644 --- a/style.css +++ b/style.css @@ -1235,16 +1235,25 @@ canvas#nexus-canvas { right: 24px; transform: translateY(-50%); z-index: 120; - animation: memoryPanelIn 0.25s ease-out; + animation: memoryPanelIn 0.22s ease-out forwards; +} + +.memory-panel-fade-out { + animation: memoryPanelOut 0.18s ease-in forwards !important; } @keyframes memoryPanelIn { - from { opacity: 0; transform: translateY(-50%) translateX(20px); } - to { opacity: 1; transform: translateY(-50%) translateX(0); } + from { opacity: 0; transform: translateY(-50%) translateX(16px); } + to { opacity: 1; transform: translateY(-50%) translateX(0); } +} + +@keyframes memoryPanelOut { + from { opacity: 1; } + to { opacity: 0; transform: translateY(-50%) translateX(12px); } } .memory-panel-content { - width: 320px; + width: 340px; background: rgba(8, 8, 24, 0.92); backdrop-filter: blur(12px); border: 1px solid rgba(74, 240, 192, 0.25); @@ -1256,8 +1265,8 @@ canvas#nexus-canvas { .memory-panel-header { display: flex; align-items: center; - gap: 8px; - margin-bottom: 12px; + gap: 6px; + margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.06); } @@ -1346,3 +1355,109 @@ canvas#nexus-canvas { margin: 1px 2px; } +.memory-conn-link { + cursor: pointer; + transition: background 0.15s, border-color 0.15s; +} + +.memory-conn-link:hover { + background: rgba(74, 240, 192, 0.22); + border-color: rgba(74, 240, 192, 0.5); + color: #fff; +} + +/* Entity name — large heading inside panel */ +.memory-entity-name { + font-family: var(--font-display, monospace); + font-size: 17px; + font-weight: 700; + color: #fff; + letter-spacing: 0.04em; + margin-bottom: 8px; + text-transform: capitalize; + word-break: break-word; +} + +/* Category badge */ +.memory-category-badge { + font-family: var(--font-display, monospace); + font-size: 9px; + letter-spacing: 0.12em; + font-weight: 700; + padding: 2px 6px; + border-radius: 4px; + border: 1px solid rgba(74, 240, 192, 0.3); + background: rgba(74, 240, 192, 0.12); + color: var(--color-primary, #4af0c0); + flex-shrink: 0; +} + +/* Trust score bar */ +.memory-trust-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + font-size: 11px; +} + +.memory-trust-bar { + flex: 1; + height: 5px; + background: rgba(255, 255, 255, 0.08); + border-radius: 3px; + overflow: hidden; +} + +.memory-trust-fill { + height: 100%; + border-radius: 3px; + background: var(--color-primary, #4af0c0); + transition: width 0.35s ease; +} + +.memory-trust-value { + color: var(--color-text-muted, #888); + min-width: 32px; + text-align: right; +} + +/* Pin button */ +.memory-panel-pin { + background: none; + border: 1px solid rgba(255, 255, 255, 0.1); + color: var(--color-text-muted, #888); + font-size: 11px; + cursor: pointer; + width: 24px; + height: 24px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s; + flex-shrink: 0; +} + +.memory-panel-pin:hover { + background: rgba(255, 255, 255, 0.05); + color: #fff; +} + +.memory-panel-pin.pinned { + background: rgba(74, 240, 192, 0.15); + border-color: rgba(74, 240, 192, 0.4); + color: var(--color-primary, #4af0c0); +} + +/* Related row — allow wrapping */ +.memory-meta-row--related { + align-items: flex-start; +} + +.memory-meta-row--related span:last-child { + flex-wrap: wrap; + display: flex; + gap: 2px; +} +