diff --git a/nexus/components/memory-inspect.js b/nexus/components/memory-inspect.js new file mode 100644 index 00000000..e356166e --- /dev/null +++ b/nexus/components/memory-inspect.js @@ -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 = ` +
+ + + +
+
+
+
+ +
+ + `; + 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 = ` + ${catLabel} + + \u25CF ${vitalityBand} (${Math.round(strength * 100)}%) + + ${created ? `${created}` : ''} + `; + + // 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 = ''; + linksHtml += ''; + 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 = ''; + } + + // Footer + const footerEl = document.getElementById('inspect-footer'); + footerEl.innerHTML = ` + + ${data.id || ''} + `; + 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 };