claude-cortex 1.0.0 → 1.1.1
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/dashboard/README.md +36 -0
- package/dashboard/components.json +22 -0
- package/dashboard/eslint.config.mjs +18 -0
- package/dashboard/next.config.ts +7 -0
- package/dashboard/package-lock.json +7784 -0
- package/dashboard/package.json +42 -0
- package/dashboard/postcss.config.mjs +7 -0
- package/dashboard/public/file.svg +1 -0
- package/dashboard/public/globe.svg +1 -0
- package/dashboard/public/next.svg +1 -0
- package/dashboard/public/vercel.svg +1 -0
- package/dashboard/public/window.svg +1 -0
- package/dashboard/src/app/favicon.ico +0 -0
- package/dashboard/src/app/globals.css +125 -0
- package/dashboard/src/app/layout.tsx +35 -0
- package/dashboard/src/app/page.tsx +338 -0
- package/dashboard/src/components/Providers.tsx +27 -0
- package/dashboard/src/components/brain/ActivityPulseSystem.tsx +229 -0
- package/dashboard/src/components/brain/BrainMesh.tsx +118 -0
- package/dashboard/src/components/brain/BrainRegions.tsx +254 -0
- package/dashboard/src/components/brain/BrainScene.tsx +255 -0
- package/dashboard/src/components/brain/CategoryLabels.tsx +103 -0
- package/dashboard/src/components/brain/CoreSphere.tsx +215 -0
- package/dashboard/src/components/brain/DataFlowParticles.tsx +123 -0
- package/dashboard/src/components/brain/DataStreamRings.tsx +161 -0
- package/dashboard/src/components/brain/ElectronFlow.tsx +323 -0
- package/dashboard/src/components/brain/HolographicGrid.tsx +235 -0
- package/dashboard/src/components/brain/MemoryLinks.tsx +271 -0
- package/dashboard/src/components/brain/MemoryNode.tsx +245 -0
- package/dashboard/src/components/brain/NeuralPathways.tsx +441 -0
- package/dashboard/src/components/brain/SynapseNodes.tsx +306 -0
- package/dashboard/src/components/brain/TimelineControls.tsx +205 -0
- package/dashboard/src/components/chip/ChipScene.tsx +497 -0
- package/dashboard/src/components/chip/ChipSubstrate.tsx +238 -0
- package/dashboard/src/components/chip/CortexCore.tsx +210 -0
- package/dashboard/src/components/chip/DataBus.tsx +416 -0
- package/dashboard/src/components/chip/MemoryCell.tsx +225 -0
- package/dashboard/src/components/chip/MemoryGrid.tsx +328 -0
- package/dashboard/src/components/chip/QuantumCell.tsx +316 -0
- package/dashboard/src/components/chip/SectionLabel.tsx +113 -0
- package/dashboard/src/components/chip/index.ts +14 -0
- package/dashboard/src/components/controls/ControlPanel.tsx +100 -0
- package/dashboard/src/components/dashboard/StatsPanel.tsx +164 -0
- package/dashboard/src/components/debug/ActivityLog.tsx +238 -0
- package/dashboard/src/components/debug/DebugPanel.tsx +101 -0
- package/dashboard/src/components/debug/QueryTester.tsx +192 -0
- package/dashboard/src/components/debug/RelationshipGraph.tsx +403 -0
- package/dashboard/src/components/debug/SqlConsole.tsx +313 -0
- package/dashboard/src/components/memory/MemoryDetail.tsx +325 -0
- package/dashboard/src/components/ui/button.tsx +62 -0
- package/dashboard/src/components/ui/card.tsx +92 -0
- package/dashboard/src/components/ui/input.tsx +21 -0
- package/dashboard/src/hooks/useDebouncedValue.ts +24 -0
- package/dashboard/src/hooks/useMemories.ts +276 -0
- package/dashboard/src/hooks/useSuggestions.ts +46 -0
- package/dashboard/src/lib/category-colors.ts +84 -0
- package/dashboard/src/lib/position-algorithm.ts +177 -0
- package/dashboard/src/lib/simplex-noise.ts +217 -0
- package/dashboard/src/lib/store.ts +88 -0
- package/dashboard/src/lib/utils.ts +6 -0
- package/dashboard/src/lib/websocket.ts +216 -0
- package/dashboard/src/types/memory.ts +73 -0
- package/dashboard/tsconfig.json +34 -0
- package/dist/api/control.d.ts +27 -0
- package/dist/api/control.d.ts.map +1 -0
- package/dist/api/control.js +60 -0
- package/dist/api/control.js.map +1 -0
- package/dist/api/visualization-server.d.ts.map +1 -1
- package/dist/api/visualization-server.js +109 -2
- package/dist/api/visualization-server.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +80 -4
- package/dist/index.js.map +1 -1
- package/dist/memory/store.d.ts +6 -0
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/memory/store.js +14 -0
- package/dist/memory/store.js.map +1 -1
- package/package.json +7 -3
|
@@ -0,0 +1,313 @@
|
|
|
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 } 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
|
+
|
|
63
|
+
// Keyboard shortcut: Ctrl+Enter to execute
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
66
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
executeQuery();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
73
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
74
|
+
}, [query, allowWrite]);
|
|
75
|
+
|
|
76
|
+
const executeQuery = async () => {
|
|
77
|
+
if (!query.trim()) return;
|
|
78
|
+
|
|
79
|
+
// Safety check for destructive operations
|
|
80
|
+
const upperQuery = query.toUpperCase();
|
|
81
|
+
if (!allowWrite) {
|
|
82
|
+
if (upperQuery.includes('DROP') || upperQuery.includes('DELETE') ||
|
|
83
|
+
upperQuery.includes('TRUNCATE') || upperQuery.includes('INSERT') ||
|
|
84
|
+
upperQuery.includes('UPDATE') || upperQuery.includes('ALTER')) {
|
|
85
|
+
setResult({
|
|
86
|
+
columns: [],
|
|
87
|
+
rows: [],
|
|
88
|
+
rowCount: 0,
|
|
89
|
+
executionTime: 0,
|
|
90
|
+
error: 'Write operations are disabled. Enable "Allow writes" to execute this query.',
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Block DROP/TRUNCATE even in write mode
|
|
97
|
+
if (upperQuery.includes('DROP') || upperQuery.includes('TRUNCATE')) {
|
|
98
|
+
setResult({
|
|
99
|
+
columns: [],
|
|
100
|
+
rows: [],
|
|
101
|
+
rowCount: 0,
|
|
102
|
+
executionTime: 0,
|
|
103
|
+
error: 'DROP and TRUNCATE operations are blocked for safety.',
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
setIsExecuting(true);
|
|
109
|
+
const startTime = performance.now();
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const response = await fetch(`${API_BASE}/api/sql`, {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: { 'Content-Type': 'application/json' },
|
|
115
|
+
body: JSON.stringify({ query: query.trim(), allowWrite }),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const data = await response.json();
|
|
119
|
+
const executionTime = performance.now() - startTime;
|
|
120
|
+
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
setResult({
|
|
123
|
+
columns: [],
|
|
124
|
+
rows: [],
|
|
125
|
+
rowCount: 0,
|
|
126
|
+
executionTime,
|
|
127
|
+
error: data.error || 'Query failed',
|
|
128
|
+
});
|
|
129
|
+
} else {
|
|
130
|
+
setResult({
|
|
131
|
+
columns: data.columns || [],
|
|
132
|
+
rows: data.rows || [],
|
|
133
|
+
rowCount: data.rowCount || data.rows?.length || 0,
|
|
134
|
+
executionTime,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Add to history
|
|
138
|
+
setHistory((prev) => {
|
|
139
|
+
const updated = [query, ...prev.filter((q) => q !== query)];
|
|
140
|
+
return updated.slice(0, 20); // Keep last 20
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
} catch (err) {
|
|
144
|
+
setResult({
|
|
145
|
+
columns: [],
|
|
146
|
+
rows: [],
|
|
147
|
+
rowCount: 0,
|
|
148
|
+
executionTime: performance.now() - startTime,
|
|
149
|
+
error: (err as Error).message,
|
|
150
|
+
});
|
|
151
|
+
} finally {
|
|
152
|
+
setIsExecuting(false);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const loadTemplate = (templateQuery: string) => {
|
|
157
|
+
setQuery(templateQuery);
|
|
158
|
+
textareaRef.current?.focus();
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const loadFromHistory = (historicalQuery: string) => {
|
|
162
|
+
setQuery(historicalQuery);
|
|
163
|
+
textareaRef.current?.focus();
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div className="h-full flex flex-col">
|
|
168
|
+
{/* Query Editor */}
|
|
169
|
+
<div className="p-3 border-b border-slate-700">
|
|
170
|
+
<div className="flex gap-2 mb-2">
|
|
171
|
+
{/* Template Dropdown */}
|
|
172
|
+
<select
|
|
173
|
+
onChange={(e) => e.target.value && loadTemplate(e.target.value)}
|
|
174
|
+
className="bg-slate-800 border border-slate-600 text-white text-xs rounded px-2 py-1"
|
|
175
|
+
value=""
|
|
176
|
+
>
|
|
177
|
+
<option value="">Templates...</option>
|
|
178
|
+
{QUERY_TEMPLATES.map((t) => (
|
|
179
|
+
<option key={t.label} value={t.query}>
|
|
180
|
+
{t.label}
|
|
181
|
+
</option>
|
|
182
|
+
))}
|
|
183
|
+
</select>
|
|
184
|
+
|
|
185
|
+
{/* History Dropdown */}
|
|
186
|
+
{history.length > 0 && (
|
|
187
|
+
<select
|
|
188
|
+
onChange={(e) => e.target.value && loadFromHistory(e.target.value)}
|
|
189
|
+
className="bg-slate-800 border border-slate-600 text-white text-xs rounded px-2 py-1"
|
|
190
|
+
value=""
|
|
191
|
+
>
|
|
192
|
+
<option value="">History...</option>
|
|
193
|
+
{history.map((q, i) => (
|
|
194
|
+
<option key={i} value={q}>
|
|
195
|
+
{q.slice(0, 50)}...
|
|
196
|
+
</option>
|
|
197
|
+
))}
|
|
198
|
+
</select>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
<div className="flex-1" />
|
|
202
|
+
|
|
203
|
+
{/* Allow Write Toggle */}
|
|
204
|
+
<label className="flex items-center gap-1 text-xs text-slate-400 cursor-pointer">
|
|
205
|
+
<input
|
|
206
|
+
type="checkbox"
|
|
207
|
+
checked={allowWrite}
|
|
208
|
+
onChange={(e) => setAllowWrite(e.target.checked)}
|
|
209
|
+
className="rounded border-slate-600 bg-slate-800"
|
|
210
|
+
/>
|
|
211
|
+
Allow writes
|
|
212
|
+
</label>
|
|
213
|
+
|
|
214
|
+
<Button
|
|
215
|
+
onClick={executeQuery}
|
|
216
|
+
disabled={isExecuting || !query.trim()}
|
|
217
|
+
size="sm"
|
|
218
|
+
className="bg-blue-600 hover:bg-blue-700"
|
|
219
|
+
>
|
|
220
|
+
{isExecuting ? 'Running...' : 'Execute (Ctrl+Enter)'}
|
|
221
|
+
</Button>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<textarea
|
|
225
|
+
ref={textareaRef}
|
|
226
|
+
value={query}
|
|
227
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
228
|
+
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"
|
|
229
|
+
placeholder="Enter SQL query..."
|
|
230
|
+
spellCheck={false}
|
|
231
|
+
/>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
{/* Results */}
|
|
235
|
+
<div className="flex-1 overflow-auto p-3">
|
|
236
|
+
{result?.error && (
|
|
237
|
+
<div className="p-3 rounded-lg bg-red-500/20 border border-red-500/50 text-red-300 text-sm mb-3">
|
|
238
|
+
{result.error}
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
{result && !result.error && (
|
|
243
|
+
<>
|
|
244
|
+
<div className="text-xs text-slate-400 mb-2">
|
|
245
|
+
{result.rowCount} row{result.rowCount !== 1 ? 's' : ''} returned in{' '}
|
|
246
|
+
{result.executionTime.toFixed(0)}ms
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
{result.rows.length > 0 ? (
|
|
250
|
+
<div className="overflow-x-auto">
|
|
251
|
+
<table className="w-full text-xs border-collapse">
|
|
252
|
+
<thead>
|
|
253
|
+
<tr className="border-b border-slate-700">
|
|
254
|
+
{result.columns.map((col) => (
|
|
255
|
+
<th
|
|
256
|
+
key={col}
|
|
257
|
+
className="text-left text-slate-400 font-medium py-2 px-3 bg-slate-800/50"
|
|
258
|
+
>
|
|
259
|
+
{col}
|
|
260
|
+
</th>
|
|
261
|
+
))}
|
|
262
|
+
</tr>
|
|
263
|
+
</thead>
|
|
264
|
+
<tbody>
|
|
265
|
+
{result.rows.map((row, i) => (
|
|
266
|
+
<tr key={i} className="border-b border-slate-800 hover:bg-slate-800/30">
|
|
267
|
+
{result.columns.map((col) => (
|
|
268
|
+
<td key={col} className="py-2 px-3 text-white">
|
|
269
|
+
{formatCellValue(row[col])}
|
|
270
|
+
</td>
|
|
271
|
+
))}
|
|
272
|
+
</tr>
|
|
273
|
+
))}
|
|
274
|
+
</tbody>
|
|
275
|
+
</table>
|
|
276
|
+
</div>
|
|
277
|
+
) : (
|
|
278
|
+
<div className="text-slate-500 text-center py-4">No rows returned</div>
|
|
279
|
+
)}
|
|
280
|
+
</>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{!result && (
|
|
284
|
+
<div className="text-slate-500 text-sm text-center py-8">
|
|
285
|
+
Enter a SQL query and press Execute or Ctrl+Enter
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function formatCellValue(value: unknown): string {
|
|
294
|
+
if (value === null || value === undefined) return '—';
|
|
295
|
+
if (typeof value === 'string') {
|
|
296
|
+
// Truncate long strings
|
|
297
|
+
if (value.length > 100) {
|
|
298
|
+
return value.slice(0, 100) + '...';
|
|
299
|
+
}
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
302
|
+
if (typeof value === 'number') {
|
|
303
|
+
// Format decimals nicely
|
|
304
|
+
if (!Number.isInteger(value)) {
|
|
305
|
+
return value.toFixed(4);
|
|
306
|
+
}
|
|
307
|
+
return value.toString();
|
|
308
|
+
}
|
|
309
|
+
if (typeof value === 'boolean') {
|
|
310
|
+
return value ? 'true' : 'false';
|
|
311
|
+
}
|
|
312
|
+
return JSON.stringify(value);
|
|
313
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Memory Detail
|
|
5
|
+
* Shows detailed information about a selected memory
|
|
6
|
+
* including related memories and decay visualization
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useMemo } from 'react';
|
|
10
|
+
import { Memory, MemoryLink } from '@/types/memory';
|
|
11
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
12
|
+
import { Button } from '@/components/ui/button';
|
|
13
|
+
import { getCategoryColor, getTypeColor } from '@/lib/category-colors';
|
|
14
|
+
import { calculateDecayFactor } from '@/lib/position-algorithm';
|
|
15
|
+
|
|
16
|
+
interface MemoryDetailProps {
|
|
17
|
+
memory: Memory;
|
|
18
|
+
links?: MemoryLink[];
|
|
19
|
+
memories?: Memory[];
|
|
20
|
+
onClose: () => void;
|
|
21
|
+
onReinforce?: (id: number) => void;
|
|
22
|
+
onSelectMemory?: (id: number) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Relationship styling
|
|
26
|
+
const RELATIONSHIP_STYLES: Record<string, { color: string; label: string; icon: string }> = {
|
|
27
|
+
references: { color: '#60a5fa', label: 'References', icon: '→' },
|
|
28
|
+
extends: { color: '#34d399', label: 'Extends', icon: '⊃' },
|
|
29
|
+
contradicts: { color: '#f87171', label: 'Contradicts', icon: '⊗' },
|
|
30
|
+
related: { color: '#a78bfa', label: 'Related', icon: '~' },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Get health status based on decay
|
|
34
|
+
function getHealthStatus(decayFactor: number): { label: string; color: string; bgColor: string } {
|
|
35
|
+
if (decayFactor > 0.7) {
|
|
36
|
+
return { label: 'Healthy', color: '#22C55E', bgColor: 'rgba(34, 197, 94, 0.15)' };
|
|
37
|
+
}
|
|
38
|
+
if (decayFactor > 0.4) {
|
|
39
|
+
return { label: 'Fading', color: '#EAB308', bgColor: 'rgba(234, 179, 8, 0.15)' };
|
|
40
|
+
}
|
|
41
|
+
return { label: 'Critical', color: '#EF4444', bgColor: 'rgba(239, 68, 68, 0.15)' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function MemoryDetail({
|
|
45
|
+
memory,
|
|
46
|
+
links = [],
|
|
47
|
+
memories = [],
|
|
48
|
+
onClose,
|
|
49
|
+
onReinforce,
|
|
50
|
+
onSelectMemory,
|
|
51
|
+
}: MemoryDetailProps) {
|
|
52
|
+
const decayFactor = calculateDecayFactor(memory);
|
|
53
|
+
const categoryColor = getCategoryColor(memory.category);
|
|
54
|
+
const typeColor = getTypeColor(memory.type);
|
|
55
|
+
const healthStatus = getHealthStatus(decayFactor);
|
|
56
|
+
|
|
57
|
+
// Find related memories through links
|
|
58
|
+
const relatedMemories = useMemo(() => {
|
|
59
|
+
const related: Array<{
|
|
60
|
+
memory: Memory;
|
|
61
|
+
relationship: string;
|
|
62
|
+
strength: number;
|
|
63
|
+
direction: 'from' | 'to';
|
|
64
|
+
}> = [];
|
|
65
|
+
|
|
66
|
+
for (const link of links) {
|
|
67
|
+
if (link.source_id === memory.id) {
|
|
68
|
+
const target = memories.find(m => m.id === link.target_id);
|
|
69
|
+
if (target) {
|
|
70
|
+
related.push({
|
|
71
|
+
memory: target,
|
|
72
|
+
relationship: link.relationship,
|
|
73
|
+
strength: link.strength,
|
|
74
|
+
direction: 'to',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
} else if (link.target_id === memory.id) {
|
|
78
|
+
const source = memories.find(m => m.id === link.source_id);
|
|
79
|
+
if (source) {
|
|
80
|
+
related.push({
|
|
81
|
+
memory: source,
|
|
82
|
+
relationship: link.relationship,
|
|
83
|
+
strength: link.strength,
|
|
84
|
+
direction: 'from',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Sort by strength
|
|
91
|
+
return related.sort((a, b) => b.strength - a.strength);
|
|
92
|
+
}, [memory.id, links, memories]);
|
|
93
|
+
|
|
94
|
+
const formatDate = (dateStr: string) => {
|
|
95
|
+
const date = new Date(dateStr);
|
|
96
|
+
return date.toLocaleString();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const timeSince = (dateStr: string) => {
|
|
100
|
+
const date = new Date(dateStr);
|
|
101
|
+
const now = new Date();
|
|
102
|
+
const hours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));
|
|
103
|
+
|
|
104
|
+
if (hours < 1) return 'Just now';
|
|
105
|
+
if (hours < 24) return `${hours}h ago`;
|
|
106
|
+
const days = Math.floor(hours / 24);
|
|
107
|
+
if (days < 7) return `${days}d ago`;
|
|
108
|
+
return `${Math.floor(days / 7)}w ago`;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<Card className="bg-slate-900 border-slate-700 h-full overflow-auto">
|
|
113
|
+
<CardHeader className="border-b border-slate-700 pb-3">
|
|
114
|
+
<div className="flex items-start justify-between gap-2">
|
|
115
|
+
<CardTitle className="text-lg font-semibold text-white leading-tight">
|
|
116
|
+
{memory.title}
|
|
117
|
+
</CardTitle>
|
|
118
|
+
<Button
|
|
119
|
+
variant="ghost"
|
|
120
|
+
size="sm"
|
|
121
|
+
onClick={onClose}
|
|
122
|
+
className="text-slate-400 hover:text-white -mt-1"
|
|
123
|
+
>
|
|
124
|
+
✕
|
|
125
|
+
</Button>
|
|
126
|
+
</div>
|
|
127
|
+
<div className="flex items-center gap-2 mt-2">
|
|
128
|
+
<span
|
|
129
|
+
className="px-2 py-0.5 rounded text-xs font-medium"
|
|
130
|
+
style={{
|
|
131
|
+
backgroundColor: categoryColor + '20',
|
|
132
|
+
color: categoryColor,
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
{memory.category}
|
|
136
|
+
</span>
|
|
137
|
+
<span
|
|
138
|
+
className="px-2 py-0.5 rounded text-xs font-medium"
|
|
139
|
+
style={{
|
|
140
|
+
backgroundColor: typeColor + '20',
|
|
141
|
+
color: typeColor,
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
{memory.type.replace('_', '-')}
|
|
145
|
+
</span>
|
|
146
|
+
</div>
|
|
147
|
+
</CardHeader>
|
|
148
|
+
|
|
149
|
+
<CardContent className="p-4 space-y-4">
|
|
150
|
+
{/* Content */}
|
|
151
|
+
<div>
|
|
152
|
+
<h4 className="text-xs font-medium text-slate-400 mb-1">Content</h4>
|
|
153
|
+
<p className="text-sm text-slate-200 whitespace-pre-wrap leading-relaxed">
|
|
154
|
+
{memory.content}
|
|
155
|
+
</p>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{/* Health Status Banner */}
|
|
159
|
+
<div
|
|
160
|
+
className="rounded-lg p-3 flex items-center gap-3"
|
|
161
|
+
style={{ backgroundColor: healthStatus.bgColor }}
|
|
162
|
+
>
|
|
163
|
+
<div
|
|
164
|
+
className="w-3 h-3 rounded-full animate-pulse"
|
|
165
|
+
style={{ backgroundColor: healthStatus.color }}
|
|
166
|
+
/>
|
|
167
|
+
<div>
|
|
168
|
+
<div className="text-sm font-medium" style={{ color: healthStatus.color }}>
|
|
169
|
+
{healthStatus.label}
|
|
170
|
+
</div>
|
|
171
|
+
<div className="text-xs text-slate-400">
|
|
172
|
+
{decayFactor > 0.7
|
|
173
|
+
? 'Memory is strong and stable'
|
|
174
|
+
: decayFactor > 0.4
|
|
175
|
+
? 'Memory is fading - reinforce to preserve'
|
|
176
|
+
: 'Memory at risk of deletion - reinforce now'}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{/* Metrics */}
|
|
182
|
+
<div className="grid grid-cols-2 gap-3">
|
|
183
|
+
<div className="bg-slate-800 rounded-lg p-3">
|
|
184
|
+
<div className="text-xs text-slate-400">Salience</div>
|
|
185
|
+
<div className="text-lg font-bold text-white">
|
|
186
|
+
{(memory.salience * 100).toFixed(0)}%
|
|
187
|
+
</div>
|
|
188
|
+
<div className="mt-1 h-1.5 bg-slate-700 rounded-full overflow-hidden">
|
|
189
|
+
<div
|
|
190
|
+
className="h-full bg-gradient-to-r from-red-500 via-yellow-500 to-green-500 rounded-full transition-all"
|
|
191
|
+
style={{ width: `${memory.salience * 100}%` }}
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<div className="bg-slate-800 rounded-lg p-3">
|
|
197
|
+
<div className="text-xs text-slate-400">Decay Factor</div>
|
|
198
|
+
<div className="text-lg font-bold text-white">
|
|
199
|
+
{(decayFactor * 100).toFixed(0)}%
|
|
200
|
+
</div>
|
|
201
|
+
<div className="mt-1 h-1.5 bg-slate-700 rounded-full overflow-hidden">
|
|
202
|
+
<div
|
|
203
|
+
className="h-full rounded-full transition-all"
|
|
204
|
+
style={{
|
|
205
|
+
width: `${decayFactor * 100}%`,
|
|
206
|
+
backgroundColor: healthStatus.color,
|
|
207
|
+
}}
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
{/* Access info */}
|
|
214
|
+
<div className="bg-slate-800 rounded-lg p-3 space-y-2">
|
|
215
|
+
<div className="flex justify-between items-center">
|
|
216
|
+
<span className="text-xs text-slate-400">Access Count</span>
|
|
217
|
+
<span className="text-sm font-medium text-white">
|
|
218
|
+
{memory.accessCount} times
|
|
219
|
+
</span>
|
|
220
|
+
</div>
|
|
221
|
+
<div className="flex justify-between items-center">
|
|
222
|
+
<span className="text-xs text-slate-400">Last Accessed</span>
|
|
223
|
+
<span className="text-sm text-white">
|
|
224
|
+
{timeSince(memory.lastAccessed)}
|
|
225
|
+
</span>
|
|
226
|
+
</div>
|
|
227
|
+
<div className="flex justify-between items-center">
|
|
228
|
+
<span className="text-xs text-slate-400">Created</span>
|
|
229
|
+
<span className="text-sm text-white">
|
|
230
|
+
{formatDate(memory.createdAt)}
|
|
231
|
+
</span>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
{/* Related Memories */}
|
|
236
|
+
{relatedMemories.length > 0 && (
|
|
237
|
+
<div>
|
|
238
|
+
<h4 className="text-xs font-medium text-slate-400 mb-2 flex items-center gap-2">
|
|
239
|
+
<span className="inline-block w-4 h-4">🔗</span>
|
|
240
|
+
Related Memories ({relatedMemories.length})
|
|
241
|
+
</h4>
|
|
242
|
+
<div className="space-y-2 max-h-48 overflow-y-auto">
|
|
243
|
+
{relatedMemories.map(({ memory: related, relationship, strength, direction }) => {
|
|
244
|
+
const style = RELATIONSHIP_STYLES[relationship] || RELATIONSHIP_STYLES.related;
|
|
245
|
+
const relatedCategoryColor = getCategoryColor(related.category);
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<button
|
|
249
|
+
key={related.id}
|
|
250
|
+
onClick={() => onSelectMemory?.(related.id)}
|
|
251
|
+
className="w-full text-left p-2 bg-slate-800 hover:bg-slate-750 rounded-lg transition-colors group"
|
|
252
|
+
>
|
|
253
|
+
<div className="flex items-center gap-2 mb-1">
|
|
254
|
+
<span
|
|
255
|
+
className="w-1.5 h-1.5 rounded-full"
|
|
256
|
+
style={{ backgroundColor: style.color }}
|
|
257
|
+
/>
|
|
258
|
+
<span
|
|
259
|
+
className="text-[10px] font-medium"
|
|
260
|
+
style={{ color: style.color }}
|
|
261
|
+
>
|
|
262
|
+
{direction === 'to' ? `${style.icon} ${style.label}` : `${style.label} ${style.icon}`}
|
|
263
|
+
</span>
|
|
264
|
+
<span className="text-[10px] text-slate-500 ml-auto">
|
|
265
|
+
{(strength * 100).toFixed(0)}%
|
|
266
|
+
</span>
|
|
267
|
+
</div>
|
|
268
|
+
<div className="text-sm text-white truncate group-hover:text-blue-400 transition-colors">
|
|
269
|
+
{related.title}
|
|
270
|
+
</div>
|
|
271
|
+
<div className="flex items-center gap-2 mt-1">
|
|
272
|
+
<span
|
|
273
|
+
className="px-1.5 py-0.5 rounded text-[10px]"
|
|
274
|
+
style={{
|
|
275
|
+
backgroundColor: relatedCategoryColor + '20',
|
|
276
|
+
color: relatedCategoryColor,
|
|
277
|
+
}}
|
|
278
|
+
>
|
|
279
|
+
{related.category}
|
|
280
|
+
</span>
|
|
281
|
+
<span className="text-[10px] text-slate-500">
|
|
282
|
+
{(related.salience * 100).toFixed(0)}% salience
|
|
283
|
+
</span>
|
|
284
|
+
</div>
|
|
285
|
+
</button>
|
|
286
|
+
);
|
|
287
|
+
})}
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
|
|
292
|
+
{/* Tags */}
|
|
293
|
+
{memory.tags && memory.tags.length > 0 && (
|
|
294
|
+
<div>
|
|
295
|
+
<h4 className="text-xs font-medium text-slate-400 mb-2">Tags</h4>
|
|
296
|
+
<div className="flex flex-wrap gap-1">
|
|
297
|
+
{memory.tags.map((tag, i) => (
|
|
298
|
+
<span
|
|
299
|
+
key={i}
|
|
300
|
+
className="px-2 py-0.5 bg-slate-700 text-slate-300 rounded text-xs"
|
|
301
|
+
>
|
|
302
|
+
{tag}
|
|
303
|
+
</span>
|
|
304
|
+
))}
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
|
|
309
|
+
{/* Actions */}
|
|
310
|
+
<div className="flex gap-2 pt-2">
|
|
311
|
+
{onReinforce && (
|
|
312
|
+
<Button
|
|
313
|
+
variant="default"
|
|
314
|
+
size="sm"
|
|
315
|
+
onClick={() => onReinforce(memory.id)}
|
|
316
|
+
className="flex-1 bg-blue-600 hover:bg-blue-700"
|
|
317
|
+
>
|
|
318
|
+
⚡ Reinforce Memory
|
|
319
|
+
</Button>
|
|
320
|
+
)}
|
|
321
|
+
</div>
|
|
322
|
+
</CardContent>
|
|
323
|
+
</Card>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
13
|
+
destructive:
|
|
14
|
+
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
15
|
+
outline:
|
|
16
|
+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
17
|
+
secondary:
|
|
18
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
19
|
+
ghost:
|
|
20
|
+
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
21
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
25
|
+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
26
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
27
|
+
icon: "size-9",
|
|
28
|
+
"icon-sm": "size-8",
|
|
29
|
+
"icon-lg": "size-10",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: {
|
|
33
|
+
variant: "default",
|
|
34
|
+
size: "default",
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
function Button({
|
|
40
|
+
className,
|
|
41
|
+
variant = "default",
|
|
42
|
+
size = "default",
|
|
43
|
+
asChild = false,
|
|
44
|
+
...props
|
|
45
|
+
}: React.ComponentProps<"button"> &
|
|
46
|
+
VariantProps<typeof buttonVariants> & {
|
|
47
|
+
asChild?: boolean
|
|
48
|
+
}) {
|
|
49
|
+
const Comp = asChild ? Slot : "button"
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Comp
|
|
53
|
+
data-slot="button"
|
|
54
|
+
data-variant={variant}
|
|
55
|
+
data-size={size}
|
|
56
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
57
|
+
{...props}
|
|
58
|
+
/>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { Button, buttonVariants }
|