shieldcortex 2.1.1 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/hooks/clawdbot/cortex-memory/HOOK.md +2 -2
- package/package.json +13 -9
- package/dashboard/components.json +0 -22
- package/dashboard/eslint.config.mjs +0 -42
- package/dashboard/next.config.ts +0 -7
- package/dashboard/package-lock.json +0 -8053
- package/dashboard/package.json +0 -44
- package/dashboard/postcss.config.mjs +0 -7
- package/dashboard/public/file.svg +0 -1
- package/dashboard/public/globe.svg +0 -1
- package/dashboard/public/next.svg +0 -1
- package/dashboard/public/vercel.svg +0 -1
- package/dashboard/public/window.svg +0 -1
- package/dashboard/scripts/ensure-api.mjs +0 -76
- package/dashboard/src/app/error.tsx +0 -49
- package/dashboard/src/app/favicon.ico +0 -0
- package/dashboard/src/app/globals.css +0 -130
- package/dashboard/src/app/layout.tsx +0 -35
- package/dashboard/src/app/page.tsx +0 -364
- package/dashboard/src/components/Providers.tsx +0 -27
- package/dashboard/src/components/brain/ActivityPulseSystem.tsx +0 -229
- package/dashboard/src/components/brain/BrainMesh.tsx +0 -133
- package/dashboard/src/components/brain/BrainRegions.tsx +0 -254
- package/dashboard/src/components/brain/BrainScene.tsx +0 -255
- package/dashboard/src/components/brain/CategoryLabels.tsx +0 -103
- package/dashboard/src/components/brain/CoreSphere.tsx +0 -215
- package/dashboard/src/components/brain/DataFlowParticles.tsx +0 -123
- package/dashboard/src/components/brain/DataStreamRings.tsx +0 -161
- package/dashboard/src/components/brain/ElectronFlow.tsx +0 -323
- package/dashboard/src/components/brain/HolographicGrid.tsx +0 -235
- package/dashboard/src/components/brain/MemoryLinks.tsx +0 -271
- package/dashboard/src/components/brain/MemoryNode.tsx +0 -245
- package/dashboard/src/components/brain/NeuralPathways.tsx +0 -441
- package/dashboard/src/components/brain/SynapseNodes.tsx +0 -312
- package/dashboard/src/components/brain/TimelineControls.tsx +0 -205
- package/dashboard/src/components/chip/ChipScene.tsx +0 -497
- package/dashboard/src/components/chip/ChipSubstrate.tsx +0 -238
- package/dashboard/src/components/chip/CortexCore.tsx +0 -210
- package/dashboard/src/components/chip/DataBus.tsx +0 -416
- package/dashboard/src/components/chip/MemoryCell.tsx +0 -225
- package/dashboard/src/components/chip/MemoryGrid.tsx +0 -328
- package/dashboard/src/components/chip/QuantumCell.tsx +0 -316
- package/dashboard/src/components/chip/SectionLabel.tsx +0 -113
- package/dashboard/src/components/chip/index.ts +0 -14
- package/dashboard/src/components/controls/ControlPanel.tsx +0 -106
- package/dashboard/src/components/controls/VersionPanel.tsx +0 -185
- package/dashboard/src/components/dashboard/StatsPanel.tsx +0 -164
- package/dashboard/src/components/debug/ActivityLog.tsx +0 -250
- package/dashboard/src/components/debug/DebugPanel.tsx +0 -101
- package/dashboard/src/components/debug/QueryTester.tsx +0 -192
- package/dashboard/src/components/debug/RelationshipGraph.tsx +0 -403
- package/dashboard/src/components/debug/SqlConsole.tsx +0 -319
- package/dashboard/src/components/graph/KnowledgeGraph.tsx +0 -230
- package/dashboard/src/components/graph/OntologyGraph.tsx +0 -631
- package/dashboard/src/components/insights/ActivityHeatmap.tsx +0 -131
- package/dashboard/src/components/insights/InsightsView.tsx +0 -46
- package/dashboard/src/components/insights/KnowledgeMapPanel.tsx +0 -80
- package/dashboard/src/components/insights/QualityPanel.tsx +0 -116
- package/dashboard/src/components/memories/MemoriesView.tsx +0 -150
- package/dashboard/src/components/memories/MemoryCard.tsx +0 -103
- package/dashboard/src/components/memory/MemoryDetail.tsx +0 -325
- package/dashboard/src/components/nav/NavRail.tsx +0 -54
- package/dashboard/src/components/ui/button.tsx +0 -62
- package/dashboard/src/components/ui/card.tsx +0 -92
- package/dashboard/src/components/ui/input.tsx +0 -21
- package/dashboard/src/hooks/useDebouncedValue.ts +0 -24
- package/dashboard/src/hooks/useMemories.ts +0 -458
- package/dashboard/src/hooks/useSuggestions.ts +0 -46
- package/dashboard/src/lib/category-colors.ts +0 -84
- package/dashboard/src/lib/position-algorithm.ts +0 -177
- package/dashboard/src/lib/simplex-noise.ts +0 -217
- package/dashboard/src/lib/store.ts +0 -88
- package/dashboard/src/lib/utils.ts +0 -6
- package/dashboard/src/lib/websocket.ts +0 -249
- package/dashboard/src/types/memory.ts +0 -73
- package/dashboard/tsconfig.json +0 -34
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* SQL Console Component
|
|
5
|
-
*
|
|
6
|
-
* Allows executing SQL queries against the memory database.
|
|
7
|
-
* Read-only by default with optional write mode.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
11
|
-
import { Button } from '@/components/ui/button';
|
|
12
|
-
|
|
13
|
-
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
|
14
|
-
|
|
15
|
-
// Predefined query templates
|
|
16
|
-
const QUERY_TEMPLATES = [
|
|
17
|
-
{
|
|
18
|
-
label: 'Top memories by salience',
|
|
19
|
-
query: 'SELECT id, title, salience, type, category FROM memories ORDER BY salience DESC LIMIT 20',
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
label: 'Memory type distribution',
|
|
23
|
-
query: "SELECT type, COUNT(*) as count FROM memories GROUP BY type",
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
label: 'Category distribution',
|
|
27
|
-
query: 'SELECT category, COUNT(*) as count FROM memories GROUP BY category ORDER BY count DESC',
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
label: 'Contradiction links',
|
|
31
|
-
query: "SELECT * FROM memory_links WHERE relationship = 'contradicts'",
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
label: 'Recently accessed',
|
|
35
|
-
query: 'SELECT id, title, last_accessed, access_count FROM memories ORDER BY last_accessed DESC LIMIT 20',
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
label: 'Low salience (at risk)',
|
|
39
|
-
query: 'SELECT id, title, salience, decayed_score, type FROM memories WHERE decayed_score < 0.3 ORDER BY decayed_score ASC LIMIT 20',
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
label: 'Memories by project',
|
|
43
|
-
query: 'SELECT project, COUNT(*) as count FROM memories GROUP BY project ORDER BY count DESC',
|
|
44
|
-
},
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
interface QueryResult {
|
|
48
|
-
columns: string[];
|
|
49
|
-
rows: Record<string, unknown>[];
|
|
50
|
-
rowCount: number;
|
|
51
|
-
executionTime: number;
|
|
52
|
-
error?: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function SqlConsole() {
|
|
56
|
-
const [query, setQuery] = useState(QUERY_TEMPLATES[0].query);
|
|
57
|
-
const [result, setResult] = useState<QueryResult | null>(null);
|
|
58
|
-
const [isExecuting, setIsExecuting] = useState(false);
|
|
59
|
-
const [allowWrite, setAllowWrite] = useState(false);
|
|
60
|
-
const [history, setHistory] = useState<string[]>([]);
|
|
61
|
-
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
62
|
-
const executeQueryRef = useRef<() => void>(() => {});
|
|
63
|
-
|
|
64
|
-
const executeQuery = useCallback(async () => {
|
|
65
|
-
if (!query.trim()) return;
|
|
66
|
-
|
|
67
|
-
// Safety check for destructive operations
|
|
68
|
-
const upperQuery = query.toUpperCase();
|
|
69
|
-
if (!allowWrite) {
|
|
70
|
-
if (upperQuery.includes('DROP') || upperQuery.includes('DELETE') ||
|
|
71
|
-
upperQuery.includes('TRUNCATE') || upperQuery.includes('INSERT') ||
|
|
72
|
-
upperQuery.includes('UPDATE') || upperQuery.includes('ALTER')) {
|
|
73
|
-
setResult({
|
|
74
|
-
columns: [],
|
|
75
|
-
rows: [],
|
|
76
|
-
rowCount: 0,
|
|
77
|
-
executionTime: 0,
|
|
78
|
-
error: 'Write operations are disabled. Enable "Allow writes" to execute this query.',
|
|
79
|
-
});
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Block DROP/TRUNCATE even in write mode
|
|
85
|
-
if (upperQuery.includes('DROP') || upperQuery.includes('TRUNCATE')) {
|
|
86
|
-
setResult({
|
|
87
|
-
columns: [],
|
|
88
|
-
rows: [],
|
|
89
|
-
rowCount: 0,
|
|
90
|
-
executionTime: 0,
|
|
91
|
-
error: 'DROP and TRUNCATE operations are blocked for safety.',
|
|
92
|
-
});
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
setIsExecuting(true);
|
|
97
|
-
const startTime = performance.now();
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
const response = await fetch(`${API_BASE}/api/sql`, {
|
|
101
|
-
method: 'POST',
|
|
102
|
-
headers: { 'Content-Type': 'application/json' },
|
|
103
|
-
body: JSON.stringify({ query: query.trim(), allowWrite }),
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
const data = await response.json();
|
|
107
|
-
const executionTime = performance.now() - startTime;
|
|
108
|
-
|
|
109
|
-
if (!response.ok) {
|
|
110
|
-
setResult({
|
|
111
|
-
columns: [],
|
|
112
|
-
rows: [],
|
|
113
|
-
rowCount: 0,
|
|
114
|
-
executionTime,
|
|
115
|
-
error: data.error || 'Query failed',
|
|
116
|
-
});
|
|
117
|
-
} else {
|
|
118
|
-
setResult({
|
|
119
|
-
columns: data.columns || [],
|
|
120
|
-
rows: data.rows || [],
|
|
121
|
-
rowCount: data.rowCount || data.rows?.length || 0,
|
|
122
|
-
executionTime,
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// Add to history
|
|
126
|
-
setHistory((prev) => {
|
|
127
|
-
const updated = [query, ...prev.filter((q) => q !== query)];
|
|
128
|
-
return updated.slice(0, 20); // Keep last 20
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
} catch (err) {
|
|
132
|
-
setResult({
|
|
133
|
-
columns: [],
|
|
134
|
-
rows: [],
|
|
135
|
-
rowCount: 0,
|
|
136
|
-
executionTime: performance.now() - startTime,
|
|
137
|
-
error: (err as Error).message,
|
|
138
|
-
});
|
|
139
|
-
} finally {
|
|
140
|
-
setIsExecuting(false);
|
|
141
|
-
}
|
|
142
|
-
}, [query, allowWrite]);
|
|
143
|
-
|
|
144
|
-
// Keep ref in sync
|
|
145
|
-
useEffect(() => {
|
|
146
|
-
executeQueryRef.current = executeQuery;
|
|
147
|
-
}, [executeQuery]);
|
|
148
|
-
|
|
149
|
-
// Keyboard shortcut: Ctrl+Enter to execute
|
|
150
|
-
useEffect(() => {
|
|
151
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
152
|
-
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
153
|
-
e.preventDefault();
|
|
154
|
-
executeQueryRef.current();
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
document.addEventListener('keydown', handleKeyDown);
|
|
159
|
-
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
160
|
-
}, []);
|
|
161
|
-
|
|
162
|
-
const loadTemplate = (templateQuery: string) => {
|
|
163
|
-
setQuery(templateQuery);
|
|
164
|
-
textareaRef.current?.focus();
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
const loadFromHistory = (historicalQuery: string) => {
|
|
168
|
-
setQuery(historicalQuery);
|
|
169
|
-
textareaRef.current?.focus();
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
return (
|
|
173
|
-
<div className="h-full flex flex-col">
|
|
174
|
-
{/* Query Editor */}
|
|
175
|
-
<div className="p-3 border-b border-slate-700">
|
|
176
|
-
<div className="flex gap-2 mb-2">
|
|
177
|
-
{/* Template Dropdown */}
|
|
178
|
-
<select
|
|
179
|
-
onChange={(e) => e.target.value && loadTemplate(e.target.value)}
|
|
180
|
-
className="bg-slate-800 border border-slate-600 text-white text-xs rounded px-2 py-1"
|
|
181
|
-
value=""
|
|
182
|
-
>
|
|
183
|
-
<option value="">Templates...</option>
|
|
184
|
-
{QUERY_TEMPLATES.map((t) => (
|
|
185
|
-
<option key={t.label} value={t.query}>
|
|
186
|
-
{t.label}
|
|
187
|
-
</option>
|
|
188
|
-
))}
|
|
189
|
-
</select>
|
|
190
|
-
|
|
191
|
-
{/* History Dropdown */}
|
|
192
|
-
{history.length > 0 && (
|
|
193
|
-
<select
|
|
194
|
-
onChange={(e) => e.target.value && loadFromHistory(e.target.value)}
|
|
195
|
-
className="bg-slate-800 border border-slate-600 text-white text-xs rounded px-2 py-1"
|
|
196
|
-
value=""
|
|
197
|
-
>
|
|
198
|
-
<option value="">History...</option>
|
|
199
|
-
{history.map((q, i) => (
|
|
200
|
-
<option key={i} value={q}>
|
|
201
|
-
{q.slice(0, 50)}...
|
|
202
|
-
</option>
|
|
203
|
-
))}
|
|
204
|
-
</select>
|
|
205
|
-
)}
|
|
206
|
-
|
|
207
|
-
<div className="flex-1" />
|
|
208
|
-
|
|
209
|
-
{/* Allow Write Toggle */}
|
|
210
|
-
<label className="flex items-center gap-1 text-xs text-slate-400 cursor-pointer">
|
|
211
|
-
<input
|
|
212
|
-
type="checkbox"
|
|
213
|
-
checked={allowWrite}
|
|
214
|
-
onChange={(e) => setAllowWrite(e.target.checked)}
|
|
215
|
-
className="rounded border-slate-600 bg-slate-800"
|
|
216
|
-
/>
|
|
217
|
-
Allow writes
|
|
218
|
-
</label>
|
|
219
|
-
|
|
220
|
-
<Button
|
|
221
|
-
onClick={executeQuery}
|
|
222
|
-
disabled={isExecuting || !query.trim()}
|
|
223
|
-
size="sm"
|
|
224
|
-
className="bg-blue-600 hover:bg-blue-700"
|
|
225
|
-
>
|
|
226
|
-
{isExecuting ? 'Running...' : 'Execute (Ctrl+Enter)'}
|
|
227
|
-
</Button>
|
|
228
|
-
</div>
|
|
229
|
-
|
|
230
|
-
<textarea
|
|
231
|
-
ref={textareaRef}
|
|
232
|
-
value={query}
|
|
233
|
-
onChange={(e) => setQuery(e.target.value)}
|
|
234
|
-
className="w-full h-24 bg-slate-900 border border-slate-600 rounded p-2 text-white font-mono text-sm resize-none focus:outline-none focus:border-blue-500"
|
|
235
|
-
placeholder="Enter SQL query..."
|
|
236
|
-
spellCheck={false}
|
|
237
|
-
/>
|
|
238
|
-
</div>
|
|
239
|
-
|
|
240
|
-
{/* Results */}
|
|
241
|
-
<div className="flex-1 overflow-auto p-3">
|
|
242
|
-
{result?.error && (
|
|
243
|
-
<div className="p-3 rounded-lg bg-red-500/20 border border-red-500/50 text-red-300 text-sm mb-3">
|
|
244
|
-
{result.error}
|
|
245
|
-
</div>
|
|
246
|
-
)}
|
|
247
|
-
|
|
248
|
-
{result && !result.error && (
|
|
249
|
-
<>
|
|
250
|
-
<div className="text-xs text-slate-400 mb-2">
|
|
251
|
-
{result.rowCount} row{result.rowCount !== 1 ? 's' : ''} returned in{' '}
|
|
252
|
-
{result.executionTime.toFixed(0)}ms
|
|
253
|
-
</div>
|
|
254
|
-
|
|
255
|
-
{result.rows.length > 0 ? (
|
|
256
|
-
<div className="overflow-x-auto">
|
|
257
|
-
<table className="w-full text-xs border-collapse">
|
|
258
|
-
<thead>
|
|
259
|
-
<tr className="border-b border-slate-700">
|
|
260
|
-
{result.columns.map((col) => (
|
|
261
|
-
<th
|
|
262
|
-
key={col}
|
|
263
|
-
className="text-left text-slate-400 font-medium py-2 px-3 bg-slate-800/50"
|
|
264
|
-
>
|
|
265
|
-
{col}
|
|
266
|
-
</th>
|
|
267
|
-
))}
|
|
268
|
-
</tr>
|
|
269
|
-
</thead>
|
|
270
|
-
<tbody>
|
|
271
|
-
{result.rows.map((row, i) => (
|
|
272
|
-
<tr key={i} className="border-b border-slate-800 hover:bg-slate-800/30">
|
|
273
|
-
{result.columns.map((col) => (
|
|
274
|
-
<td key={col} className="py-2 px-3 text-white">
|
|
275
|
-
{formatCellValue(row[col])}
|
|
276
|
-
</td>
|
|
277
|
-
))}
|
|
278
|
-
</tr>
|
|
279
|
-
))}
|
|
280
|
-
</tbody>
|
|
281
|
-
</table>
|
|
282
|
-
</div>
|
|
283
|
-
) : (
|
|
284
|
-
<div className="text-slate-500 text-center py-4">No rows returned</div>
|
|
285
|
-
)}
|
|
286
|
-
</>
|
|
287
|
-
)}
|
|
288
|
-
|
|
289
|
-
{!result && (
|
|
290
|
-
<div className="text-slate-500 text-sm text-center py-8">
|
|
291
|
-
Enter a SQL query and press Execute or Ctrl+Enter
|
|
292
|
-
</div>
|
|
293
|
-
)}
|
|
294
|
-
</div>
|
|
295
|
-
</div>
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function formatCellValue(value: unknown): string {
|
|
300
|
-
if (value === null || value === undefined) return '—';
|
|
301
|
-
if (typeof value === 'string') {
|
|
302
|
-
// Truncate long strings
|
|
303
|
-
if (value.length > 100) {
|
|
304
|
-
return value.slice(0, 100) + '...';
|
|
305
|
-
}
|
|
306
|
-
return value;
|
|
307
|
-
}
|
|
308
|
-
if (typeof value === 'number') {
|
|
309
|
-
// Format decimals nicely
|
|
310
|
-
if (!Number.isInteger(value)) {
|
|
311
|
-
return value.toFixed(4);
|
|
312
|
-
}
|
|
313
|
-
return value.toString();
|
|
314
|
-
}
|
|
315
|
-
if (typeof value === 'boolean') {
|
|
316
|
-
return value ? 'true' : 'false';
|
|
317
|
-
}
|
|
318
|
-
return JSON.stringify(value);
|
|
319
|
-
}
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
-
import dynamic from 'next/dynamic';
|
|
5
|
-
import type { Memory, MemoryLink } from '@/types/memory';
|
|
6
|
-
import { getCategoryColor } from '@/lib/category-colors';
|
|
7
|
-
|
|
8
|
-
const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), { ssr: false });
|
|
9
|
-
|
|
10
|
-
interface GraphNode {
|
|
11
|
-
id: number;
|
|
12
|
-
name: string;
|
|
13
|
-
category: Memory['category'];
|
|
14
|
-
type: Memory['type'];
|
|
15
|
-
salience: number;
|
|
16
|
-
decayedScore: number;
|
|
17
|
-
val: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface GraphLink {
|
|
21
|
-
source: number;
|
|
22
|
-
target: number;
|
|
23
|
-
strength: number;
|
|
24
|
-
relationship: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface GraphData {
|
|
28
|
-
nodes: GraphNode[];
|
|
29
|
-
links: GraphLink[];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface KnowledgeGraphProps {
|
|
33
|
-
memories: Memory[];
|
|
34
|
-
links: MemoryLink[];
|
|
35
|
-
selectedMemory: Memory | null;
|
|
36
|
-
onSelectMemory: (m: Memory | null) => void;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export default function KnowledgeGraph({
|
|
40
|
-
memories,
|
|
41
|
-
links,
|
|
42
|
-
selectedMemory,
|
|
43
|
-
onSelectMemory,
|
|
44
|
-
}: KnowledgeGraphProps) {
|
|
45
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
46
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
|
-
const graphRef = useRef<any>(null);
|
|
48
|
-
const [dimensions, setDimensions] = useState({ width: 800, height: 600 });
|
|
49
|
-
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
const el = containerRef.current;
|
|
52
|
-
if (!el) return;
|
|
53
|
-
|
|
54
|
-
const update = () => {
|
|
55
|
-
setDimensions({ width: el.clientWidth, height: el.clientHeight });
|
|
56
|
-
};
|
|
57
|
-
update();
|
|
58
|
-
|
|
59
|
-
const observer = new ResizeObserver(update);
|
|
60
|
-
observer.observe(el);
|
|
61
|
-
return () => observer.disconnect();
|
|
62
|
-
}, []);
|
|
63
|
-
|
|
64
|
-
const graphData: GraphData = useMemo(() => {
|
|
65
|
-
const nodeIds = new Set(memories.map((m) => m.id));
|
|
66
|
-
return {
|
|
67
|
-
nodes: memories.map((m) => ({
|
|
68
|
-
id: m.id,
|
|
69
|
-
name: m.title,
|
|
70
|
-
category: m.category,
|
|
71
|
-
type: m.type,
|
|
72
|
-
salience: m.salience,
|
|
73
|
-
decayedScore: m.decayedScore ?? m.salience,
|
|
74
|
-
val: m.salience * 10,
|
|
75
|
-
})),
|
|
76
|
-
links: links
|
|
77
|
-
.filter((l) => nodeIds.has(l.source_id) && nodeIds.has(l.target_id))
|
|
78
|
-
.map((l) => ({
|
|
79
|
-
source: l.source_id,
|
|
80
|
-
target: l.target_id,
|
|
81
|
-
strength: l.strength,
|
|
82
|
-
relationship: l.relationship,
|
|
83
|
-
})),
|
|
84
|
-
};
|
|
85
|
-
}, [memories, links]);
|
|
86
|
-
|
|
87
|
-
const nodeCanvasObject = useCallback(
|
|
88
|
-
(node: GraphNode, ctx: CanvasRenderingContext2D, globalScale: number) => {
|
|
89
|
-
const x = (node as unknown as { x: number }).x;
|
|
90
|
-
const y = (node as unknown as { y: number }).y;
|
|
91
|
-
if (x == null || y == null) return;
|
|
92
|
-
|
|
93
|
-
const radius = Math.max(3, node.salience * 8);
|
|
94
|
-
const color = getCategoryColor(node.category);
|
|
95
|
-
const opacity = Math.max(0.2, node.decayedScore);
|
|
96
|
-
const isSelected = selectedMemory?.id === node.id;
|
|
97
|
-
|
|
98
|
-
ctx.beginPath();
|
|
99
|
-
ctx.arc(x, y, radius, 0, 2 * Math.PI);
|
|
100
|
-
ctx.fillStyle = color + Math.round(opacity * 255).toString(16).padStart(2, '0');
|
|
101
|
-
ctx.fill();
|
|
102
|
-
|
|
103
|
-
// Border style based on type
|
|
104
|
-
ctx.lineWidth = isSelected ? 2 : 1;
|
|
105
|
-
ctx.strokeStyle = isSelected ? '#ffffff' : color;
|
|
106
|
-
if (node.type === 'short_term') {
|
|
107
|
-
ctx.setLineDash([3, 3]);
|
|
108
|
-
} else if (node.type === 'episodic') {
|
|
109
|
-
ctx.setLineDash([1, 2]);
|
|
110
|
-
} else {
|
|
111
|
-
ctx.setLineDash([]);
|
|
112
|
-
}
|
|
113
|
-
ctx.stroke();
|
|
114
|
-
ctx.setLineDash([]);
|
|
115
|
-
|
|
116
|
-
// Label when zoomed in enough
|
|
117
|
-
if (globalScale > 3) {
|
|
118
|
-
const maxChars = 40;
|
|
119
|
-
const label = node.name.length > maxChars ? node.name.slice(0, maxChars) + '…' : node.name;
|
|
120
|
-
const fontSize = Math.max(10, 12 / globalScale);
|
|
121
|
-
ctx.font = `${fontSize}px Sans-Serif`;
|
|
122
|
-
ctx.textAlign = 'center';
|
|
123
|
-
ctx.textBaseline = 'top';
|
|
124
|
-
|
|
125
|
-
const textWidth = ctx.measureText(label).width;
|
|
126
|
-
const textY = y + radius + 3;
|
|
127
|
-
const padding = 2;
|
|
128
|
-
|
|
129
|
-
// Dark background pill behind text
|
|
130
|
-
ctx.fillStyle = 'rgba(2, 6, 23, 0.85)';
|
|
131
|
-
ctx.beginPath();
|
|
132
|
-
ctx.roundRect(
|
|
133
|
-
x - textWidth / 2 - padding,
|
|
134
|
-
textY - padding,
|
|
135
|
-
textWidth + padding * 2,
|
|
136
|
-
fontSize + padding * 2,
|
|
137
|
-
3,
|
|
138
|
-
);
|
|
139
|
-
ctx.fill();
|
|
140
|
-
|
|
141
|
-
// White text with full opacity
|
|
142
|
-
ctx.fillStyle = '#e2e8f0';
|
|
143
|
-
ctx.fillText(label, x, textY);
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
[selectedMemory],
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
const linkCanvasObject = useCallback(
|
|
150
|
-
(link: GraphLink, ctx: CanvasRenderingContext2D) => {
|
|
151
|
-
const source = link.source as unknown as { x: number; y: number };
|
|
152
|
-
const target = link.target as unknown as { x: number; y: number };
|
|
153
|
-
if (!source?.x || !target?.x) return;
|
|
154
|
-
|
|
155
|
-
ctx.beginPath();
|
|
156
|
-
ctx.moveTo(source.x, source.y);
|
|
157
|
-
ctx.lineTo(target.x, target.y);
|
|
158
|
-
ctx.strokeStyle = '#334155';
|
|
159
|
-
ctx.lineWidth = link.strength * 3;
|
|
160
|
-
ctx.stroke();
|
|
161
|
-
},
|
|
162
|
-
[],
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
const lastZoomRef = useRef(1);
|
|
166
|
-
const handleZoom = useCallback(
|
|
167
|
-
({ k }: { k: number }) => {
|
|
168
|
-
const fg = graphRef.current;
|
|
169
|
-
if (!fg) return;
|
|
170
|
-
const prev = lastZoomRef.current;
|
|
171
|
-
if (Math.abs(k - prev) < 0.2) return;
|
|
172
|
-
lastZoomRef.current = k;
|
|
173
|
-
|
|
174
|
-
const charge = fg.d3Force('charge');
|
|
175
|
-
if (!charge) return;
|
|
176
|
-
|
|
177
|
-
if (k > 3) {
|
|
178
|
-
// Zoomed in past label threshold — spread nodes for readability
|
|
179
|
-
charge.strength(-50 * k);
|
|
180
|
-
fg.d3ReheatSimulation();
|
|
181
|
-
} else {
|
|
182
|
-
// Zoomed out — restore default compact layout
|
|
183
|
-
charge.strength(-30);
|
|
184
|
-
fg.d3ReheatSimulation();
|
|
185
|
-
}
|
|
186
|
-
},
|
|
187
|
-
[],
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
const handleNodeClick = useCallback(
|
|
191
|
-
(node: GraphNode) => {
|
|
192
|
-
const memory = memories.find((m) => m.id === node.id) ?? null;
|
|
193
|
-
onSelectMemory(memory);
|
|
194
|
-
},
|
|
195
|
-
[memories, onSelectMemory],
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
const nodeLabel = useCallback(
|
|
199
|
-
(node: GraphNode) =>
|
|
200
|
-
`${node.name}\nCategory: ${node.category}\nSalience: ${node.salience.toFixed(2)}`,
|
|
201
|
-
[],
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
return (
|
|
205
|
-
<div ref={containerRef} style={{ width: '100%', height: '100%' }}>
|
|
206
|
-
{dimensions.width > 0 && (
|
|
207
|
-
<ForceGraph2D
|
|
208
|
-
ref={graphRef as never}
|
|
209
|
-
graphData={graphData}
|
|
210
|
-
width={dimensions.width}
|
|
211
|
-
height={dimensions.height}
|
|
212
|
-
backgroundColor="rgba(0,0,0,0)"
|
|
213
|
-
nodeCanvasObject={nodeCanvasObject as never}
|
|
214
|
-
nodeLabel={nodeLabel as never}
|
|
215
|
-
onNodeClick={handleNodeClick as never}
|
|
216
|
-
onZoom={handleZoom as never}
|
|
217
|
-
linkCanvasObject={linkCanvasObject as never}
|
|
218
|
-
linkDirectionalParticles={2}
|
|
219
|
-
linkDirectionalParticleWidth={2}
|
|
220
|
-
linkDirectionalParticleSpeed={0.005}
|
|
221
|
-
linkDirectionalParticleColor={() => '#22d3ee'}
|
|
222
|
-
d3AlphaDecay={0.02}
|
|
223
|
-
d3VelocityDecay={0.3}
|
|
224
|
-
warmupTicks={100}
|
|
225
|
-
cooldownTicks={200}
|
|
226
|
-
/>
|
|
227
|
-
)}
|
|
228
|
-
</div>
|
|
229
|
-
);
|
|
230
|
-
}
|