claude-cortex 1.10.0 → 1.11.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.
@@ -0,0 +1,230 @@
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 > 1.5) {
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 > 1.5) {
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
+ }
@@ -0,0 +1,131 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+
5
+ export interface ActivityDay {
6
+ date: string;
7
+ count: number;
8
+ }
9
+
10
+ interface ActivityHeatmapProps {
11
+ activity: ActivityDay[];
12
+ }
13
+
14
+ const CELL_SIZE = 12;
15
+ const CELL_GAP = 2;
16
+ const CELL_RADIUS = 2;
17
+ const WEEKS = 52;
18
+ const DAYS = 7;
19
+ const LEFT_LABEL_WIDTH = 30;
20
+ const TOP_LABEL_HEIGHT = 16;
21
+
22
+ const DAY_LABELS = ['', 'Mon', '', 'Wed', '', 'Fri', ''];
23
+ const MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
24
+
25
+ function getColor(count: number, max: number): string {
26
+ if (count === 0) return '#0f172a'; // slate-900 (darker empty)
27
+ const ratio = count / Math.max(max, 1);
28
+ if (ratio < 0.25) return '#155e75'; // cyan-800
29
+ if (ratio < 0.50) return '#0891b2'; // cyan-600
30
+ if (ratio < 0.75) return '#06b6d4'; // cyan-500
31
+ return '#22d3ee'; // cyan-400
32
+ }
33
+
34
+ export function ActivityHeatmap({ activity }: ActivityHeatmapProps) {
35
+ const { grid, monthPositions, maxCount } = useMemo(() => {
36
+ // Build lookup map
37
+ const lookup = new Map<string, number>();
38
+ for (const d of activity) {
39
+ lookup.set(d.date, d.count);
40
+ }
41
+
42
+ // Today at start of day
43
+ const today = new Date();
44
+ today.setHours(0, 0, 0, 0);
45
+
46
+ // Find the Saturday of the current week (end of last column)
47
+ const todayDay = today.getDay(); // 0=Sun
48
+ const endDate = new Date(today);
49
+ endDate.setDate(today.getDate() + (6 - todayDay));
50
+
51
+ // Start date is 52 weeks before endDate's Sunday
52
+ const startDate = new Date(endDate);
53
+ startDate.setDate(endDate.getDate() - (WEEKS * 7 - 1));
54
+
55
+ const cells: Array<{ week: number; day: number; date: string; count: number }> = [];
56
+ const months: Array<{ label: string; week: number }> = [];
57
+ let lastMonth = -1;
58
+
59
+ const cursor = new Date(startDate);
60
+ for (let w = 0; w < WEEKS; w++) {
61
+ for (let d = 0; d < DAYS; d++) {
62
+ const dateStr = cursor.toISOString().slice(0, 10);
63
+ const count = lookup.get(dateStr) || 0;
64
+ cells.push({ week: w, day: d, date: dateStr, count });
65
+
66
+ const month = cursor.getMonth();
67
+ if (month !== lastMonth && d === 0) {
68
+ months.push({ label: MONTH_LABELS[month], week: w });
69
+ lastMonth = month;
70
+ }
71
+
72
+ cursor.setDate(cursor.getDate() + 1);
73
+ }
74
+ }
75
+
76
+ const max = Math.max(...cells.map(c => c.count), 1);
77
+ return { grid: cells, monthPositions: months, maxCount: max };
78
+ }, [activity]);
79
+
80
+ const svgWidth = LEFT_LABEL_WIDTH + WEEKS * (CELL_SIZE + CELL_GAP);
81
+ const svgHeight = TOP_LABEL_HEIGHT + DAYS * (CELL_SIZE + CELL_GAP);
82
+
83
+ return (
84
+ <div className="bg-slate-900/50 rounded-lg p-4 overflow-x-auto">
85
+ <svg width={svgWidth} height={svgHeight} className="text-slate-400">
86
+ {/* Month labels */}
87
+ {monthPositions.map((m, i) => (
88
+ <text
89
+ key={i}
90
+ x={LEFT_LABEL_WIDTH + m.week * (CELL_SIZE + CELL_GAP)}
91
+ y={12}
92
+ fontSize={10}
93
+ fill="currentColor"
94
+ >
95
+ {m.label}
96
+ </text>
97
+ ))}
98
+
99
+ {/* Day labels */}
100
+ {DAY_LABELS.map((label, i) =>
101
+ label ? (
102
+ <text
103
+ key={i}
104
+ x={0}
105
+ y={TOP_LABEL_HEIGHT + i * (CELL_SIZE + CELL_GAP) + CELL_SIZE - 1}
106
+ fontSize={10}
107
+ fill="currentColor"
108
+ >
109
+ {label}
110
+ </text>
111
+ ) : null
112
+ )}
113
+
114
+ {/* Cells */}
115
+ {grid.map((cell, i) => (
116
+ <rect
117
+ key={i}
118
+ x={LEFT_LABEL_WIDTH + cell.week * (CELL_SIZE + CELL_GAP)}
119
+ y={TOP_LABEL_HEIGHT + cell.day * (CELL_SIZE + CELL_GAP)}
120
+ width={CELL_SIZE}
121
+ height={CELL_SIZE}
122
+ rx={CELL_RADIUS}
123
+ fill={getColor(cell.count, maxCount)}
124
+ >
125
+ <title>{`${cell.date}: ${cell.count} memories`}</title>
126
+ </rect>
127
+ ))}
128
+ </svg>
129
+ </div>
130
+ );
131
+ }
@@ -0,0 +1,46 @@
1
+ 'use client';
2
+
3
+ import { useActivity } from '@/hooks/useMemories';
4
+ import { useDashboardStore } from '@/lib/store';
5
+ import { ActivityHeatmap } from './ActivityHeatmap';
6
+ import { KnowledgeMapPanel } from './KnowledgeMapPanel';
7
+ import { QualityPanel } from './QualityPanel';
8
+ import type { MemoryStats } from '@/types/memory';
9
+
10
+ interface InsightsViewProps {
11
+ selectedProject?: string;
12
+ stats?: MemoryStats;
13
+ }
14
+
15
+ export function InsightsView({ selectedProject, stats }: InsightsViewProps) {
16
+ const { data: activityData } = useActivity(selectedProject);
17
+ const { setViewMode, setCategoryFilter } = useDashboardStore();
18
+
19
+ const handleNavigate = (filter: { category?: string }) => {
20
+ if (filter.category) {
21
+ setCategoryFilter(filter.category);
22
+ }
23
+ setViewMode('memories');
24
+ };
25
+
26
+ return (
27
+ <div className="h-full overflow-y-auto p-6 space-y-8">
28
+ <section>
29
+ <h2 className="text-lg font-semibold text-white mb-4">Activity</h2>
30
+ <ActivityHeatmap activity={activityData?.activity ?? []} />
31
+ </section>
32
+
33
+ {stats && (
34
+ <section>
35
+ <h2 className="text-lg font-semibold text-white mb-4">Knowledge Coverage</h2>
36
+ <KnowledgeMapPanel stats={stats} onNavigate={handleNavigate} />
37
+ </section>
38
+ )}
39
+
40
+ <section>
41
+ <h2 className="text-lg font-semibold text-white mb-4">Memory Quality</h2>
42
+ <QualityPanel project={selectedProject} />
43
+ </section>
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,80 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+ import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts';
5
+ import { getCategoryColor } from '@/lib/category-colors';
6
+ import type { MemoryStats, MemoryCategory } from '@/types/memory';
7
+
8
+ interface KnowledgeMapPanelProps {
9
+ stats: MemoryStats;
10
+ onNavigate: (filter: { category?: string }) => void;
11
+ }
12
+
13
+ export function KnowledgeMapPanel({ stats, onNavigate }: KnowledgeMapPanelProps) {
14
+ const data = useMemo(() => {
15
+ return Object.entries(stats.byCategory)
16
+ .map(([category, count]) => ({
17
+ category,
18
+ count,
19
+ color: getCategoryColor(category as MemoryCategory),
20
+ thin: count < 3,
21
+ }))
22
+ .sort((a, b) => b.count - a.count);
23
+ }, [stats.byCategory]);
24
+
25
+ if (data.length === 0) {
26
+ return (
27
+ <div className="bg-slate-900/50 rounded-lg p-4 text-slate-400 text-sm">
28
+ No category data available.
29
+ </div>
30
+ );
31
+ }
32
+
33
+ return (
34
+ <div className="bg-slate-900/50 rounded-lg p-4">
35
+ <ResponsiveContainer width="100%" height={data.length * 36 + 20}>
36
+ <BarChart data={data} layout="vertical" margin={{ left: 10, right: 30 }}>
37
+ <XAxis type="number" hide />
38
+ <YAxis
39
+ type="category"
40
+ dataKey="category"
41
+ width={100}
42
+ tick={{ fill: '#94a3b8', fontSize: 12 }}
43
+ axisLine={false}
44
+ tickLine={false}
45
+ />
46
+ <Tooltip
47
+ contentStyle={{ background: '#1e293b', border: '1px solid #334155', borderRadius: 8 }}
48
+ labelStyle={{ color: '#e2e8f0' }}
49
+ itemStyle={{ color: '#e2e8f0' }}
50
+ />
51
+ <Bar
52
+ dataKey="count"
53
+ radius={[0, 4, 4, 0]}
54
+ cursor="pointer"
55
+ onClick={(_data, index) => {
56
+ if (index !== undefined && data[index]) {
57
+ onNavigate({ category: data[index].category });
58
+ }
59
+ }}
60
+ >
61
+ {data.map((entry, index) => (
62
+ <Cell key={index} fill={entry.color} />
63
+ ))}
64
+ </Bar>
65
+ </BarChart>
66
+ </ResponsiveContainer>
67
+
68
+ {/* Thin coverage warnings */}
69
+ {data.some(d => d.thin) && (
70
+ <div className="mt-2 flex flex-wrap gap-2">
71
+ {data.filter(d => d.thin).map(d => (
72
+ <span key={d.category} className="text-xs text-amber-400 bg-amber-400/10 px-2 py-0.5 rounded">
73
+ ⚠ {d.category} ({d.count})
74
+ </span>
75
+ ))}
76
+ </div>
77
+ )}
78
+ </div>
79
+ );
80
+ }
@@ -0,0 +1,116 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Eye, Clock, Copy, AlertTriangle, ChevronDown, ChevronRight } from 'lucide-react';
5
+ import { useQuality, useContradictions } from '@/hooks/useMemories';
6
+
7
+ interface QualityPanelProps {
8
+ project?: string;
9
+ }
10
+
11
+ function Section({
12
+ icon: Icon,
13
+ title,
14
+ count,
15
+ children,
16
+ }: {
17
+ icon: React.ComponentType<{ className?: string; size?: number }>;
18
+ title: string;
19
+ count: number;
20
+ children: React.ReactNode;
21
+ }) {
22
+ const [expanded, setExpanded] = useState(false);
23
+
24
+ return (
25
+ <div className="border border-slate-800 rounded-lg overflow-hidden">
26
+ <button
27
+ onClick={() => setExpanded(!expanded)}
28
+ className="w-full flex items-center gap-2 px-3 py-2 hover:bg-slate-800/50 transition-colors"
29
+ >
30
+ {expanded ? <ChevronDown size={14} className="text-slate-400" /> : <ChevronRight size={14} className="text-slate-400" />}
31
+ <Icon size={14} className="text-slate-400" />
32
+ <span className="text-sm text-slate-200 flex-1 text-left">{title}</span>
33
+ <span className="text-xs px-1.5 py-0.5 rounded bg-slate-700 text-slate-300">{count}</span>
34
+ </button>
35
+ {expanded && <div className="px-3 pb-3 space-y-1">{children}</div>}
36
+ </div>
37
+ );
38
+ }
39
+
40
+ export function QualityPanel({ project }: QualityPanelProps) {
41
+ const { data: quality } = useQuality(project);
42
+ const { data: contradictionsData } = useContradictions(project);
43
+
44
+ const neverAccessed = quality?.neverAccessed;
45
+ const stale = quality?.stale;
46
+ const duplicates = quality?.duplicates;
47
+ const contradictions = contradictionsData?.contradictions ?? [];
48
+
49
+ return (
50
+ <div className="space-y-2">
51
+ <Section icon={Eye} title="Never Accessed" count={neverAccessed?.count ?? 0}>
52
+ {neverAccessed?.items?.length ? (
53
+ neverAccessed.items.map((item, i) => (
54
+ <div key={i} className="text-xs text-slate-400 py-1 border-b border-slate-800/50 last:border-0">
55
+ <span className="text-slate-300">{String(item.title || 'Untitled')}</span>
56
+ {item.created_at ? (
57
+ <span className="ml-2 text-slate-500">{String(item.created_at).slice(0, 10)}</span>
58
+ ) : null}
59
+ </div>
60
+ ))
61
+ ) : (
62
+ <div className="text-xs text-slate-500">None found</div>
63
+ )}
64
+ </Section>
65
+
66
+ <Section icon={Clock} title="Stale Memories" count={stale?.count ?? 0}>
67
+ {stale?.items?.length ? (
68
+ stale.items.map((item, i) => {
69
+ const score = Number(item.decayed_score ?? 0);
70
+ const color = score < 0.3 ? 'text-red-400' : score < 0.5 ? 'text-amber-400' : 'text-slate-400';
71
+ return (
72
+ <div key={i} className="text-xs text-slate-400 py-1 border-b border-slate-800/50 last:border-0 flex items-center gap-2">
73
+ <span className={`${color} font-mono`}>{score.toFixed(2)}</span>
74
+ <span className="text-slate-300">{String(item.title || 'Untitled')}</span>
75
+ </div>
76
+ );
77
+ })
78
+ ) : (
79
+ <div className="text-xs text-slate-500">None found</div>
80
+ )}
81
+ </Section>
82
+
83
+ <Section icon={Copy} title="Duplicates" count={duplicates?.count ?? 0}>
84
+ {duplicates?.items?.length ? (
85
+ duplicates.items.map((item, i) => (
86
+ <div key={i} className="text-xs text-slate-400 py-1 border-b border-slate-800/50 last:border-0">
87
+ <span className="text-slate-300">{String(item.title_a || 'Untitled')}</span>
88
+ <span className="mx-1 text-slate-600">&harr;</span>
89
+ <span className="text-slate-300">{String(item.title_b || 'Untitled')}</span>
90
+ </div>
91
+ ))
92
+ ) : (
93
+ <div className="text-xs text-slate-500">None found</div>
94
+ )}
95
+ </Section>
96
+
97
+ <Section icon={AlertTriangle} title="Contradictions" count={contradictions.length}>
98
+ {contradictions.length ? (
99
+ contradictions.map((c, i) => (
100
+ <div key={i} className="text-xs py-1 border-b border-slate-800/50 last:border-0">
101
+ <div className="flex items-center gap-2 text-slate-400">
102
+ <span className="font-mono text-amber-400">{c.score.toFixed(2)}</span>
103
+ <span className="text-slate-300">{c.memoryATitle}</span>
104
+ <span className="text-slate-600">&harr;</span>
105
+ <span className="text-slate-300">{c.memoryBTitle}</span>
106
+ </div>
107
+ {c.reason && <div className="text-slate-500 mt-0.5 ml-10">{c.reason}</div>}
108
+ </div>
109
+ ))
110
+ ) : (
111
+ <div className="text-xs text-slate-500">None found</div>
112
+ )}
113
+ </Section>
114
+ </div>
115
+ );
116
+ }