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
+
+
+
+
+
+
+ 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;