claude-cortex 1.0.0 → 1.1.0

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.
Files changed (79) hide show
  1. package/dashboard/README.md +36 -0
  2. package/dashboard/components.json +22 -0
  3. package/dashboard/eslint.config.mjs +18 -0
  4. package/dashboard/next.config.ts +7 -0
  5. package/dashboard/package-lock.json +7784 -0
  6. package/dashboard/package.json +42 -0
  7. package/dashboard/postcss.config.mjs +7 -0
  8. package/dashboard/public/file.svg +1 -0
  9. package/dashboard/public/globe.svg +1 -0
  10. package/dashboard/public/next.svg +1 -0
  11. package/dashboard/public/vercel.svg +1 -0
  12. package/dashboard/public/window.svg +1 -0
  13. package/dashboard/src/app/favicon.ico +0 -0
  14. package/dashboard/src/app/globals.css +125 -0
  15. package/dashboard/src/app/layout.tsx +35 -0
  16. package/dashboard/src/app/page.tsx +338 -0
  17. package/dashboard/src/components/Providers.tsx +27 -0
  18. package/dashboard/src/components/brain/ActivityPulseSystem.tsx +229 -0
  19. package/dashboard/src/components/brain/BrainMesh.tsx +118 -0
  20. package/dashboard/src/components/brain/BrainRegions.tsx +254 -0
  21. package/dashboard/src/components/brain/BrainScene.tsx +255 -0
  22. package/dashboard/src/components/brain/CategoryLabels.tsx +103 -0
  23. package/dashboard/src/components/brain/CoreSphere.tsx +215 -0
  24. package/dashboard/src/components/brain/DataFlowParticles.tsx +123 -0
  25. package/dashboard/src/components/brain/DataStreamRings.tsx +161 -0
  26. package/dashboard/src/components/brain/ElectronFlow.tsx +323 -0
  27. package/dashboard/src/components/brain/HolographicGrid.tsx +235 -0
  28. package/dashboard/src/components/brain/MemoryLinks.tsx +271 -0
  29. package/dashboard/src/components/brain/MemoryNode.tsx +245 -0
  30. package/dashboard/src/components/brain/NeuralPathways.tsx +441 -0
  31. package/dashboard/src/components/brain/SynapseNodes.tsx +306 -0
  32. package/dashboard/src/components/brain/TimelineControls.tsx +205 -0
  33. package/dashboard/src/components/chip/ChipScene.tsx +497 -0
  34. package/dashboard/src/components/chip/ChipSubstrate.tsx +238 -0
  35. package/dashboard/src/components/chip/CortexCore.tsx +210 -0
  36. package/dashboard/src/components/chip/DataBus.tsx +416 -0
  37. package/dashboard/src/components/chip/MemoryCell.tsx +225 -0
  38. package/dashboard/src/components/chip/MemoryGrid.tsx +328 -0
  39. package/dashboard/src/components/chip/QuantumCell.tsx +316 -0
  40. package/dashboard/src/components/chip/SectionLabel.tsx +113 -0
  41. package/dashboard/src/components/chip/index.ts +14 -0
  42. package/dashboard/src/components/controls/ControlPanel.tsx +100 -0
  43. package/dashboard/src/components/dashboard/StatsPanel.tsx +164 -0
  44. package/dashboard/src/components/debug/ActivityLog.tsx +238 -0
  45. package/dashboard/src/components/debug/DebugPanel.tsx +101 -0
  46. package/dashboard/src/components/debug/QueryTester.tsx +192 -0
  47. package/dashboard/src/components/debug/RelationshipGraph.tsx +403 -0
  48. package/dashboard/src/components/debug/SqlConsole.tsx +313 -0
  49. package/dashboard/src/components/memory/MemoryDetail.tsx +325 -0
  50. package/dashboard/src/components/ui/button.tsx +62 -0
  51. package/dashboard/src/components/ui/card.tsx +92 -0
  52. package/dashboard/src/components/ui/input.tsx +21 -0
  53. package/dashboard/src/hooks/useDebouncedValue.ts +24 -0
  54. package/dashboard/src/hooks/useMemories.ts +276 -0
  55. package/dashboard/src/hooks/useSuggestions.ts +46 -0
  56. package/dashboard/src/lib/category-colors.ts +84 -0
  57. package/dashboard/src/lib/position-algorithm.ts +177 -0
  58. package/dashboard/src/lib/simplex-noise.ts +217 -0
  59. package/dashboard/src/lib/store.ts +88 -0
  60. package/dashboard/src/lib/utils.ts +6 -0
  61. package/dashboard/src/lib/websocket.ts +216 -0
  62. package/dashboard/src/types/memory.ts +73 -0
  63. package/dashboard/tsconfig.json +34 -0
  64. package/dist/api/control.d.ts +27 -0
  65. package/dist/api/control.d.ts.map +1 -0
  66. package/dist/api/control.js +60 -0
  67. package/dist/api/control.js.map +1 -0
  68. package/dist/api/visualization-server.d.ts.map +1 -1
  69. package/dist/api/visualization-server.js +109 -2
  70. package/dist/api/visualization-server.js.map +1 -1
  71. package/dist/index.d.ts +2 -1
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +80 -4
  74. package/dist/index.js.map +1 -1
  75. package/dist/memory/store.d.ts +6 -0
  76. package/dist/memory/store.d.ts.map +1 -1
  77. package/dist/memory/store.js +14 -0
  78. package/dist/memory/store.js.map +1 -1
  79. 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 }