Merge pull request '[Mnemosyne] Connection Panel — browse, add, remove memory relationships' (#1247) from feat/mnemosyne-connection-panel into main
Some checks failed
Deploy Nexus / deploy (push) Failing after 3s
Staging Verification Gate / verify-staging (push) Failing after 3s

This commit was merged in pull request #1247.
This commit is contained in:
2026-04-12 03:44:39 +00:00
12 changed files with 455 additions and 0 deletions

View File

@@ -477,6 +477,10 @@ index.html
<div id="memory-inspect-panel" class="memory-inspect-panel" style="display:none;" aria-label="Memory Inspect Panel">
</div>
<!-- Memory Connections Panel (Mnemosyne) -->
<div id="memory-connections-panel" class="memory-connections-panel" style="display:none;" aria-label="Memory Connections Panel">
</div>
<script>
// ─── MNEMOSYNE: Memory Filter Panel ───────────────────
function openMemoryFilter() {

View File

@@ -0,0 +1,291 @@
// ═══════════════════════════════════════════════════════════
// 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 `
<div class="mc-conn-item" data-memid="${_esc(cid)}">
<div class="mc-conn-info">
<span class="mc-conn-label" title="${_esc(cid)}">${_esc(label)}</span>
<span class="mc-conn-meta">${_esc(cat)} · ${strength}%</span>
</div>
<div class="mc-conn-actions">
<button class="mc-btn mc-btn-nav" data-nav="${_esc(cid)}" title="Navigate to memory">⮞</button>
<button class="mc-btn mc-btn-remove" data-remove="${_esc(cid)}" title="Remove connection">✕</button>
</div>
</div>`;
}).join('');
} else {
connectedHtml = '<div class="mc-empty">No connections yet</div>';
}
// 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 `
<div class="mc-suggest-item" data-memid="${_esc(s.id)}">
<div class="mc-suggest-info">
<span class="mc-suggest-label" title="${_esc(s.id)}">${_esc(label)}</span>
<span class="mc-suggest-meta">${_esc(cat)} · ${_esc(proximity)}</span>
</div>
<button class="mc-btn mc-btn-add" data-add="${_esc(s.id)}" title="Add connection">+</button>
</div>`;
}).join('');
} else {
suggestHtml = '<div class="mc-empty">No nearby memories to connect</div>';
}
_panel.innerHTML = `
<div class="mc-header">
<span class="mc-title">⬡ Connections</span>
<button class="mc-close" id="mc-close-btn" aria-label="Close connections panel">✕</button>
</div>
<div class="mc-section">
<div class="mc-section-label">LINKED (${connections.length})</div>
<div class="mc-conn-list" id="mc-conn-list">${connectedHtml}</div>
</div>
<div class="mc-section">
<div class="mc-section-label">SUGGESTED</div>
<div class="mc-suggest-list" id="mc-suggest-list">${suggestHtml}</div>
</div>
`;
// 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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 };

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

160
style.css
View File

@@ -1917,3 +1917,163 @@ canvas#nexus-canvas {
background: rgba(74, 240, 192, 0.18);
border-color: #4af0c0;
}
/* ═══ MNEMOSYNE: Memory Connections Panel ═══ */
.memory-connections-panel {
position: fixed;
top: 50%;
right: 280px;
transform: translateY(-50%) translateX(12px);
width: 260px;
max-height: 70vh;
background: rgba(10, 12, 18, 0.92);
border: 1px solid rgba(74, 240, 192, 0.15);
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
z-index: 310;
display: flex;
flex-direction: column;
opacity: 0;
transition: opacity 0.2s ease, transform 0.2s ease;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
font-family: var(--font-mono, monospace);
}
.memory-connections-panel.mc-visible {
opacity: 1;
transform: translateY(-50%) translateX(0);
}
.mc-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.mc-title {
color: rgba(74, 240, 192, 0.8);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.mc-close {
background: none;
border: none;
color: rgba(255, 255, 255, 0.4);
font-size: 14px;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
line-height: 1;
}
.mc-close:hover {
color: #fff;
background: rgba(255, 255, 255, 0.1);
}
.mc-section {
padding: 8px 14px 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.mc-section:last-child { border-bottom: none; }
.mc-section-label {
color: rgba(74, 240, 192, 0.5);
font-size: 9px;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
}
.mc-conn-list, .mc-suggest-list {
max-height: 200px;
overflow-y: auto;
}
.mc-conn-list::-webkit-scrollbar, .mc-suggest-list::-webkit-scrollbar { width: 3px; }
.mc-conn-list::-webkit-scrollbar-thumb, .mc-suggest-list::-webkit-scrollbar-thumb {
background: rgba(74, 240, 192, 0.15);
border-radius: 2px;
}
.mc-conn-item, .mc-suggest-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 8px;
border-radius: 5px;
margin-bottom: 4px;
transition: background 0.15s ease;
}
.mc-conn-item:hover {
background: rgba(74, 240, 192, 0.06);
}
.mc-suggest-item:hover {
background: rgba(123, 92, 255, 0.06);
}
.mc-conn-info, .mc-suggest-info {
flex: 1;
min-width: 0;
overflow: hidden;
}
.mc-conn-label, .mc-suggest-label {
display: block;
color: var(--color-text, #ccc);
font-size: 11px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mc-conn-meta, .mc-suggest-meta {
display: block;
color: rgba(255, 255, 255, 0.3);
font-size: 9px;
margin-top: 1px;
}
.mc-conn-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
margin-left: 8px;
}
.mc-btn {
background: none;
border: 1px solid rgba(255, 255, 255, 0.12);
color: rgba(255, 255, 255, 0.5);
cursor: pointer;
border-radius: 4px;
font-size: 12px;
padding: 2px 6px;
line-height: 1;
transition: all 0.15s ease;
}
.mc-btn-nav:hover {
border-color: #4af0c0;
color: #4af0c0;
background: rgba(74, 240, 192, 0.08);
}
.mc-btn-remove:hover {
border-color: #ff4466;
color: #ff4466;
background: rgba(255, 68, 102, 0.08);
}
.mc-btn-add {
border-color: rgba(123, 92, 255, 0.3);
color: rgba(123, 92, 255, 0.7);
}
.mc-btn-add:hover {
border-color: #7b5cff;
color: #7b5cff;
background: rgba(123, 92, 255, 0.12);
}
.mc-empty {
color: rgba(255, 255, 255, 0.25);
font-size: 11px;
font-style: italic;
padding: 4px 0;
}