async function renderMemory() { const content = document.getElementById('pageContent'); content.innerHTML = `
`; window._memoryView = 'files'; await loadMemoryFiles(); } function switchMemoryView(view) { window._memoryView = view; document.querySelectorAll('#memoryTabs .tab').forEach(t => t.classList.toggle('active', t.dataset.view === view)); if (view === 'graph') loadMemoryGraph(); else loadMemoryFiles(); } async function loadMemoryFiles() { const container = document.getElementById('memoryList'); if (!container) return; try { const brain = await api.getBrain(); const files = Object.entries(brain); if (files.length === 0) { container.innerHTML = '
🧠
No memory files
'; return; } container.innerHTML = `
${files.map(([name, content]) => { const preview = content ? content.slice(0, 200) : ''; const safeName = escapeHtml(name.replace('.md', '').replace(/-/g, ' ')); return `
${safeName}
${content ? content.split('\n').length : 0} lines
${escapeHtml(preview)}${preview.length >= 200 ? '...' : ''}
`; }).join('')}
`; } catch (err) { container.innerHTML = `
âš 
${escapeHtml(err.message)}
`; } } async function loadMemoryGraph() { const container = document.getElementById('memoryList'); if (!container) return; container.innerHTML = `
Building graph...
`; try { const graph = await api.getMemoryGraph(); const nodes = graph.nodes || []; const edges = graph.edges || []; if (nodes.length === 0) { container.innerHTML = '
🕸
No graph data
Run a memory reindex or chat with agents to build connections.
'; return; } const stats = graph.stats || {}; container.innerHTML = `
📄 ${stats.nodes || 0} nodes 🔗 ${stats.edges || 0} edges ${nodes.filter(n => (n.type || '').startsWith('entity')).length} entities
Click a node to open the associated file. Drag to pan, scroll to zoom.
`; drawMemoryGraph(graph); } catch (err) { container.innerHTML = `
âš 
${escapeHtml(err.message)}
`; } } function drawMemoryGraph(graph) { const canvas = document.getElementById('memoryGraphCanvas'); if (!canvas) return; const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; const width = canvas.clientWidth || 800; const height = canvas.clientHeight || 480; canvas.width = width * dpr; canvas.height = height * dpr; ctx.scale(dpr, dpr); const nodes = graph.nodes || []; const edges = graph.edges || []; const typeColors = { brain: '#6c5ce7', skill: '#00b894', journal: '#fdcb6e', general: '#74b9ff', }; const colorFor = (t) => { if (t.startsWith('entity:')) return '#fd79a8'; return typeColors[t] || '#74b9ff'; }; // Simple layout: entities on left, docs spread on right (force-ish radial fallback) const entityNodes = nodes.filter(n => (n.type || '').startsWith('entity:')); const docNodes = nodes.filter(n => !(n.type || '').startsWith('entity:')); const positions = {}; const cx = width / 2, cy = height / 2; docNodes.forEach((n, i) => { const angle = (i / Math.max(docNodes.length, 1)) * Math.PI * 2 - Math.PI / 2; positions[n.id] = { x: cx + Math.cos(angle) * Math.min(width * 0.3, 200), y: cy + Math.sin(angle) * Math.min(height * 0.35, 180) }; }); entityNodes.forEach((n, i) => { const angle = (i / Math.max(entityNodes.length, 1)) * Math.PI * 2 - Math.PI / 2; positions[n.id] = { x: cx + Math.cos(angle) * Math.min(width * 0.38, 260), y: cy + Math.sin(angle) * Math.min(height * 0.42, 220) }; }); ctx.clearRect(0, 0, width, height); ctx.lineWidth = 1; ctx.strokeStyle = 'rgba(148,163,184,0.4)'; edges.forEach(e => { const a = positions[e.source], b = positions[e.target]; if (!a || !b) return; ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke(); }); nodes.forEach(n => { const p = positions[n.id]; if (!p) return; const color = colorFor(n.type); const isEntity = (n.type || '').startsWith('entity:'); const radius = isEntity ? 6 : 10; ctx.beginPath(); ctx.arc(p.x, p.y, radius, 0, Math.PI * 2); ctx.fillStyle = color; ctx.fill(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.stroke(); const label = (n.label || '').slice(0, 18); ctx.fillStyle = 'var(--text-secondary, #cbd5e1)'; ctx.font = '10px Inter, sans-serif'; ctx.fillText(label, p.x + radius + 4, p.y + 4); }); // Click-to-open: store layout for hit testing window._graphLayout = { positions, nodes, docById: {} }; nodes.forEach(n => { window._graphLayout.docById[n.id] = n; }); canvas.onclick = (ev) => { const rect = canvas.getBoundingClientRect(); const mx = ev.clientX - rect.left, my = ev.clientY - rect.top; for (const [id, p] of Object.entries(positions)) { if (Math.hypot(mx - p.x, my - p.y) < 14) { const n = window._graphLayout.docById[id]; if (n && n.path && n.path.endsWith('.md') && !(n.type || '').startsWith('entity:')) { editMemory(encodeURIComponent(n.path.split('/').pop())); } return; } } }; } async function editMemory(encodedName) { const name = decodeURIComponent(encodedName); const display = escapeHtml(name.replace('.md', '').replace(/-/g, ' ')); let content = ''; try { const r = await api.getBrainFile(name); content = r.content || ''; } catch {} showModal(`Edit: ${display}`, `
`, ` `); } async function saveMemory(encodedName) { const name = decodeURIComponent(encodedName); const content = document.getElementById('memContent').value; try { await api.updateBrainFile(name, content); closeModal(); showToast('Memory updated', 'success'); renderMemory(); } catch (err) { showToast(`Error: ${err.message}`, 'error'); } }