// ═══════════════════════════════════════════════════════════ // MNEMOSYNE — Memory Connection Panel // ═══════════════════════════════════════════════════════════ // // Interactive panel for browsing, adding, and removing memory // connections. Opens as a sub-panel from MemoryInspect when // a memory crystal is selected. // // Usage from app.js: // MemoryConnections.init({ // onNavigate: fn(memId), // fly to another memory // onConnectionChange: fn(memId, newConnections) // update hooks // }); // MemoryConnections.show(memData, allMemories); // MemoryConnections.hide(); // // Depends on: SpatialMemory (for updateMemory + highlightMemory) // ═══════════════════════════════════════════════════════════ const MemoryConnections = (() => { let _panel = null; let _onNavigate = null; let _onConnectionChange = null; let _currentMemId = null; let _hoveredConnId = null; // ─── INIT ──────────────────────────────────────────────── function init(opts = {}) { _onNavigate = opts.onNavigate || null; _onConnectionChange = opts.onConnectionChange || null; _panel = document.getElementById('memory-connections-panel'); if (!_panel) { console.warn('[MemoryConnections] Panel element #memory-connections-panel not found in DOM'); } } // ─── SHOW ──────────────────────────────────────────────── function show(memData, allMemories) { if (!_panel || !memData) return; _currentMemId = memData.id; const connections = memData.connections || []; const connectedSet = new Set(connections); // Build lookup for connected memories const memLookup = {}; (allMemories || []).forEach(m => { memLookup[m.id] = m; }); // Connected memories list let connectedHtml = ''; if (connections.length > 0) { connectedHtml = connections.map(cid => { const cm = memLookup[cid]; const label = cm ? _truncate(cm.content || cid, 40) : cid; const cat = cm ? cm.category : ''; const strength = cm ? Math.round((cm.strength || 0.7) * 100) : 70; return `
${_esc(label)} ${_esc(cat)} · ${strength}%
`; }).join(''); } else { connectedHtml = '
No connections yet
'; } // Find nearby unconnected memories (same region, then other regions) const suggestions = _findSuggestions(memData, allMemories, connectedSet); let suggestHtml = ''; if (suggestions.length > 0) { suggestHtml = suggestions.map(s => { const label = _truncate(s.content || s.id, 36); const cat = s.category || ''; const proximity = s._proximity || ''; return `
${_esc(label)} ${_esc(cat)} · ${_esc(proximity)}
`; }).join(''); } else { suggestHtml = '
No nearby memories to connect
'; } _panel.innerHTML = `
⬡ Connections
LINKED (${connections.length})
${connectedHtml}
SUGGESTED
${suggestHtml}
`; // Wire close button _panel.querySelector('#mc-close-btn')?.addEventListener('click', hide); // Wire navigation buttons _panel.querySelectorAll('[data-nav]').forEach(btn => { btn.addEventListener('click', () => { if (_onNavigate) _onNavigate(btn.dataset.nav); }); }); // Wire remove buttons _panel.querySelectorAll('[data-remove]').forEach(btn => { btn.addEventListener('click', () => _removeConnection(btn.dataset.remove)); }); // Wire add buttons _panel.querySelectorAll('[data-add]').forEach(btn => { btn.addEventListener('click', () => _addConnection(btn.dataset.add)); }); // Wire hover highlight for connection items _panel.querySelectorAll('.mc-conn-item').forEach(item => { item.addEventListener('mouseenter', () => _highlightConnection(item.dataset.memid)); item.addEventListener('mouseleave', _clearConnectionHighlight); }); _panel.style.display = 'flex'; requestAnimationFrame(() => _panel.classList.add('mc-visible')); } // ─── HIDE ──────────────────────────────────────────────── function hide() { if (!_panel) return; _clearConnectionHighlight(); _panel.classList.remove('mc-visible'); const onEnd = () => { _panel.style.display = 'none'; _panel.removeEventListener('transitionend', onEnd); }; _panel.addEventListener('transitionend', onEnd); setTimeout(() => { if (_panel) _panel.style.display = 'none'; }, 350); _currentMemId = null; } // ─── SUGGESTION ENGINE ────────────────────────────────── function _findSuggestions(memData, allMemories, connectedSet) { if (!allMemories) return []; const suggestions = []; const pos = memData.position || [0, 0, 0]; const sameRegion = memData.category || 'working'; for (const m of allMemories) { if (m.id === memData.id) continue; if (connectedSet.has(m.id)) continue; const mpos = m.position || [0, 0, 0]; const dist = Math.sqrt( (pos[0] - mpos[0]) ** 2 + (pos[1] - mpos[1]) ** 2 + (pos[2] - mpos[2]) ** 2 ); // Categorize proximity let proximity = 'nearby'; if (m.category === sameRegion) { proximity = dist < 5 ? 'same region · close' : 'same region'; } else { proximity = dist < 10 ? 'adjacent' : 'distant'; } suggestions.push({ ...m, _dist: dist, _proximity: proximity }); } // Sort: same region first, then by distance suggestions.sort((a, b) => { const aSame = a.category === sameRegion ? 0 : 1; const bSame = b.category === sameRegion ? 0 : 1; if (aSame !== bSame) return aSame - bSame; return a._dist - b._dist; }); return suggestions.slice(0, 8); // Cap at 8 suggestions } // ─── CONNECTION ACTIONS ───────────────────────────────── function _addConnection(targetId) { if (!_currentMemId) return; // Get current memory data via SpatialMemory const allMems = typeof SpatialMemory !== 'undefined' ? SpatialMemory.getAllMemories() : []; const current = allMems.find(m => m.id === _currentMemId); if (!current) return; const conns = [...(current.connections || [])]; if (conns.includes(targetId)) return; conns.push(targetId); // Update SpatialMemory if (typeof SpatialMemory !== 'undefined') { SpatialMemory.updateMemory(_currentMemId, { connections: conns }); } // Also create reverse connection on target const target = allMems.find(m => m.id === targetId); if (target) { const targetConns = [...(target.connections || [])]; if (!targetConns.includes(_currentMemId)) { targetConns.push(_currentMemId); SpatialMemory.updateMemory(targetId, { connections: targetConns }); } } if (_onConnectionChange) _onConnectionChange(_currentMemId, conns); // Re-render panel const updatedMem = { ...current, connections: conns }; show(updatedMem, allMems); } function _removeConnection(targetId) { if (!_currentMemId) return; const allMems = typeof SpatialMemory !== 'undefined' ? SpatialMemory.getAllMemories() : []; const current = allMems.find(m => m.id === _currentMemId); if (!current) return; const conns = (current.connections || []).filter(c => c !== targetId); if (typeof SpatialMemory !== 'undefined') { SpatialMemory.updateMemory(_currentMemId, { connections: conns }); } // Also remove reverse connection const target = allMems.find(m => m.id === targetId); if (target) { const targetConns = (target.connections || []).filter(c => c !== _currentMemId); SpatialMemory.updateMemory(targetId, { connections: targetConns }); } if (_onConnectionChange) _onConnectionChange(_currentMemId, conns); const updatedMem = { ...current, connections: conns }; show(updatedMem, allMems); } // ─── 3D HIGHLIGHT ─────────────────────────────────────── function _highlightConnection(memId) { _hoveredConnId = memId; if (typeof SpatialMemory !== 'undefined') { SpatialMemory.highlightMemory(memId); } } function _clearConnectionHighlight() { if (_hoveredConnId && typeof SpatialMemory !== 'undefined') { SpatialMemory.clearHighlight(); } _hoveredConnId = null; } // ─── HELPERS ──────────────────────────────────────────── function _esc(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function _truncate(str, n) { return str.length > n ? str.slice(0, n - 1) + '\u2026' : str; } function isOpen() { return _panel != null && _panel.style.display !== 'none'; } return { init, show, hide, isOpen }; })(); export { MemoryConnections };