From a5644fef6f5250a1eaed9f6e418f732860406a12 Mon Sep 17 00:00:00 2001 From: fuwasegu Date: Fri, 12 Dec 2025 11:23:47 +0900 Subject: [PATCH] graph-view --- .gitignore | 1 + exocortex/__init__.py | 2 +- exocortex/dashboard/static/app.js | 271 +++++++++++++++++++------- exocortex/dashboard/static/index.html | 20 +- exocortex/dashboard/static/styles.css | 70 ++++++- 5 files changed, 286 insertions(+), 78 deletions(-) diff --git a/.gitignore b/.gitignore index 360db98..f9ac565 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ env/ # IDE .idea/ .vscode/ +.cursor/ *.swp *.swo .DS_Store diff --git a/exocortex/__init__.py b/exocortex/__init__.py index 609e35a..61c536f 100644 --- a/exocortex/__init__.py +++ b/exocortex/__init__.py @@ -4,4 +4,4 @@ storing and retrieving development insights across projects. """ -__version__ = "0.8.0" +__version__ = "0.9.0" diff --git a/exocortex/dashboard/static/app.js b/exocortex/dashboard/static/app.js index 61726bf..f00a6a5 100644 --- a/exocortex/dashboard/static/app.js +++ b/exocortex/dashboard/static/app.js @@ -28,9 +28,13 @@ const elements = { filterType: document.getElementById('filter-type'), filterContext: document.getElementById('filter-context'), searchInput: document.getElementById('memory-search'), - graphCanvas: document.getElementById('graph-canvas'), + graphNetwork: document.getElementById('graph-network'), }; +// Graph state +let networkInstance = null; +let physicsEnabled = true; + // Type icons const TYPE_ICONS = { insight: 'πŸ’‘', @@ -348,89 +352,213 @@ function appendLogEntry(content) { // ============ Graph Visualization ============ +const TYPE_COLORS = { + insight: '#00ffff', + success: '#00ff88', + failure: '#ff4757', + decision: '#ff9500', + note: '#8b949e', +}; + function renderGraph() { - const canvas = elements.graphCanvas; - const ctx = canvas.getContext('2d'); - const container = document.getElementById('graph-container'); - - // Set canvas size - canvas.width = container.clientWidth; - canvas.height = container.clientHeight; - + const container = elements.graphNetwork; const { nodes, edges } = state.graph; if (nodes.length === 0) { - ctx.fillStyle = '#8b949e'; - ctx.font = '16px Outfit'; - ctx.textAlign = 'center'; - ctx.fillText('No graph data available', canvas.width / 2, canvas.height / 2); + container.innerHTML = '
No graph data available
'; return; } - // Simple force-directed layout - const positions = {}; - const centerX = canvas.width / 2; - const centerY = canvas.height / 2; - const radius = Math.min(canvas.width, canvas.height) * 0.35; - - // Initial positions in a circle - nodes.forEach((node, i) => { - const angle = (i / nodes.length) * Math.PI * 2; - positions[node.id] = { - x: centerX + Math.cos(angle) * radius, - y: centerY + Math.sin(angle) * radius, + // Prepare vis.js data + const visNodes = new vis.DataSet(nodes.map(node => { + const color = TYPE_COLORS[node.type] || TYPE_COLORS.note; + return { + id: node.id, + label: node.label.length > 25 ? node.label.substring(0, 25) + '...' : node.label, + title: `
${TYPE_ICONS[node.type] || 'πŸ“'} ${node.type}
${node.label}
`, + color: { + background: color, + border: color, + highlight: { + background: color, + border: '#ffffff', + }, + hover: { + background: color, + border: '#ffffff', + }, + }, + font: { + color: '#e6edf3', + size: 12, + face: 'JetBrains Mono, monospace', + }, + borderWidth: 2, + shadow: { + enabled: true, + color: color, + size: 15, + x: 0, + y: 0, + }, + size: 18, }; - }); + })); + + const visEdges = new vis.DataSet(edges.map((edge, idx) => ({ + id: idx, + from: edge.source, + to: edge.target, + title: edge.relation_type || 'related', + color: { + color: 'rgba(0, 255, 255, 0.3)', + highlight: 'rgba(0, 255, 255, 0.8)', + hover: 'rgba(0, 255, 255, 0.6)', + }, + width: 1.5, + smooth: { + enabled: true, + type: 'continuous', + roundness: 0.5, + }, + arrows: { + to: { + enabled: edge.relation_type && edge.relation_type !== 'related', + scaleFactor: 0.5, + }, + }, + }))); + + // Network options - synapse-like physics + const options = { + nodes: { + shape: 'dot', + scaling: { + min: 10, + max: 30, + }, + }, + edges: { + smooth: { + enabled: true, + type: 'continuous', + }, + }, + physics: { + enabled: physicsEnabled, + solver: 'forceAtlas2Based', + forceAtlas2Based: { + gravitationalConstant: -50, + centralGravity: 0.01, + springLength: 150, + springConstant: 0.08, + damping: 0.4, + avoidOverlap: 0.5, + }, + stabilization: { + enabled: true, + iterations: 200, + updateInterval: 25, + }, + }, + interaction: { + hover: true, + tooltipDelay: 100, + zoomView: true, + dragView: true, + dragNodes: true, + navigationButtons: false, + keyboard: { + enabled: true, + speed: { x: 10, y: 10, zoom: 0.02 }, + bindToWindow: false, + }, + zoomSpeed: 1, + }, + layout: { + improvedLayout: true, + randomSeed: 42, + }, + }; - // Draw edges - ctx.strokeStyle = 'rgba(0, 255, 255, 0.2)'; - ctx.lineWidth = 1; + // Clear previous network + if (networkInstance) { + networkInstance.destroy(); + networkInstance = null; + } - edges.forEach(edge => { - const from = positions[edge.source]; - const to = positions[edge.target]; - - if (from && to) { - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + // Create new network + const data = { nodes: visNodes, edges: visEdges }; + networkInstance = new vis.Network(container, data, options); + + // Click handler - show memory detail + networkInstance.on('click', function(params) { + if (params.nodes.length > 0) { + const nodeId = params.nodes[0]; + fetchMemoryDetail(nodeId); } }); - // Draw nodes - const TYPE_COLORS = { - insight: '#00ffff', - success: '#00ff88', - failure: '#ff4757', - decision: '#ff9500', - note: '#8b949e', - }; - - nodes.forEach(node => { - const pos = positions[node.id]; - const color = TYPE_COLORS[node.type] || TYPE_COLORS.note; - - // Node circle - ctx.beginPath(); - ctx.arc(pos.x, pos.y, 8, 0, Math.PI * 2); - ctx.fillStyle = color; - ctx.fill(); - - // Glow effect - ctx.shadowColor = color; - ctx.shadowBlur = 10; - ctx.fill(); - ctx.shadowBlur = 0; - - // Label - ctx.fillStyle = '#e6edf3'; - ctx.font = '11px JetBrains Mono'; - ctx.textAlign = 'center'; - ctx.fillText(node.label.substring(0, 20), pos.x, pos.y + 20); + // Double-click to focus + networkInstance.on('doubleClick', function(params) { + if (params.nodes.length > 0) { + networkInstance.focus(params.nodes[0], { + scale: 1.5, + animation: { + duration: 500, + easingFunction: 'easeInOutQuad', + }, + }); + } }); } +// Graph control functions +function setupGraphControls() { + const zoomInBtn = document.getElementById('graph-zoom-in'); + const zoomOutBtn = document.getElementById('graph-zoom-out'); + const fitBtn = document.getElementById('graph-fit'); + const physicsBtn = document.getElementById('graph-physics'); + + if (zoomInBtn) { + zoomInBtn.addEventListener('click', () => { + if (networkInstance) { + const scale = networkInstance.getScale(); + networkInstance.moveTo({ scale: scale * 1.3, animation: { duration: 300 } }); + } + }); + } + + if (zoomOutBtn) { + zoomOutBtn.addEventListener('click', () => { + if (networkInstance) { + const scale = networkInstance.getScale(); + networkInstance.moveTo({ scale: scale / 1.3, animation: { duration: 300 } }); + } + }); + } + + if (fitBtn) { + fitBtn.addEventListener('click', () => { + if (networkInstance) { + networkInstance.fit({ animation: { duration: 500, easingFunction: 'easeInOutQuad' } }); + } + }); + } + + if (physicsBtn) { + physicsBtn.addEventListener('click', () => { + physicsEnabled = !physicsEnabled; + physicsBtn.classList.toggle('active', physicsEnabled); + if (networkInstance) { + networkInstance.setOptions({ physics: { enabled: physicsEnabled } }); + } + }); + // Set initial state + physicsBtn.classList.toggle('active', physicsEnabled); + } +} + // ============ Utilities ============ function formatDate(dateStr) { @@ -526,11 +654,14 @@ function init() { // Window resize handler for graph window.addEventListener('resize', () => { - if (state.currentTab === 'graph') { - renderGraph(); + if (state.currentTab === 'graph' && networkInstance) { + networkInstance.fit({ animation: false }); } }); + // Setup graph controls + setupGraphControls(); + // Initial load fetchStats(); fetchHealth(); diff --git a/exocortex/dashboard/static/index.html b/exocortex/dashboard/static/index.html index fa0e0b9..9529c57 100644 --- a/exocortex/dashboard/static/index.html +++ b/exocortex/dashboard/static/index.html @@ -8,6 +8,9 @@ + + +
@@ -139,10 +142,23 @@

πŸ’€ Dream Log

πŸ•ΈοΈ Knowledge Graph

-

Connections between your memories

+

Connections between your memories β€” Scroll to zoom, drag to pan

+
+
+ + + +
- +
+
+
+ Insight + Success + Failure + Decision + Note
diff --git a/exocortex/dashboard/static/styles.css b/exocortex/dashboard/static/styles.css index 3a998d3..1366dc2 100644 --- a/exocortex/dashboard/static/styles.css +++ b/exocortex/dashboard/static/styles.css @@ -588,7 +588,7 @@ body { /* ============ Graph ============ */ .graph-header { - margin-bottom: 1rem; + margin-bottom: 0.5rem; } .graph-header h2 { @@ -601,20 +601,80 @@ body { font-size: 0.9rem; } +.graph-controls { + display: flex; + gap: 0.5rem; + margin-bottom: 0.75rem; +} + +.graph-btn { + background: var(--bg-card); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: var(--radius-md); + color: var(--text-primary); + padding: 0.5rem 0.75rem; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.2s ease; +} + +.graph-btn:hover { + background: rgba(0, 255, 255, 0.1); + border-color: var(--accent-cyan); +} + +.graph-btn.active { + background: rgba(0, 255, 255, 0.2); + border-color: var(--accent-cyan); + color: var(--accent-cyan); +} + .graph-container { width: 100%; - height: 60vh; - background: var(--bg-card); - border: 1px solid rgba(255, 255, 255, 0.05); + height: 65vh; + background: radial-gradient(ellipse at center, #1a1f2e 0%, #0d1117 100%); + border: 1px solid rgba(0, 255, 255, 0.1); border-radius: var(--radius-lg); overflow: hidden; + position: relative; } -#graph-canvas { +#graph-network { width: 100%; height: 100%; } +.graph-legend { + display: flex; + gap: 1.5rem; + justify-content: center; + margin-top: 1rem; + padding: 0.75rem; + background: var(--bg-card); + border-radius: var(--radius-md); +} + +.legend-item { + display: flex; + align-items: center; + gap: 0.4rem; + font-size: 0.85rem; + color: var(--text-secondary); +} + +.legend-dot { + width: 12px; + height: 12px; + border-radius: 50%; + box-shadow: 0 0 8px currentColor; +} + +.legend-dot.insight { background: #00ffff; color: #00ffff; } +.legend-dot.success { background: #00ff88; color: #00ff88; } +.legend-dot.failure { background: #ff4757; color: #ff4757; } +.legend-dot.decision { background: #ff9500; color: #ff9500; } +.legend-dot.note { background: #8b949e; color: #8b949e; } + /* ============ Footer ============ */ .footer { display: flex;