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.
- 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,271 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Memory Links - Neural Synapse Visualization
|
|
5
|
+
* Renders organic, neuron-like connections between related memories
|
|
6
|
+
* with flowing electrical signals and synapse endpoints
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useMemo, useState, useRef, useCallback } from 'react';
|
|
10
|
+
import { useFrame } from '@react-three/fiber';
|
|
11
|
+
import { Html, CatmullRomLine } from '@react-three/drei';
|
|
12
|
+
import * as THREE from 'three';
|
|
13
|
+
import { Memory, MemoryLink } from '@/types/memory';
|
|
14
|
+
|
|
15
|
+
interface MemoryLinksProps {
|
|
16
|
+
memories: Memory[];
|
|
17
|
+
links: MemoryLink[];
|
|
18
|
+
memoryPositions: Map<number, { x: number; y: number; z: number }>;
|
|
19
|
+
onLinkClick?: (link: MemoryLink) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Default connection color (light gray) and relationship colors for hover
|
|
23
|
+
const DEFAULT_LINE_COLOR = '#cccccc';
|
|
24
|
+
const RELATIONSHIP_STYLES: Record<string, { color: string; label: string }> = {
|
|
25
|
+
references: { color: '#00d4ff', label: 'References' }, // Cyan - information flow
|
|
26
|
+
extends: { color: '#00ff88', label: 'Extends' }, // Green - growth
|
|
27
|
+
contradicts: { color: '#ff6b6b', label: 'Contradicts' }, // Red - conflict
|
|
28
|
+
related: { color: '#b388ff', label: 'Related' }, // Purple - association
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Bright signal pulse that travels along the neural fiber
|
|
32
|
+
function NeuralSignal({
|
|
33
|
+
curve,
|
|
34
|
+
speed = 1,
|
|
35
|
+
delay = 0,
|
|
36
|
+
}: {
|
|
37
|
+
curve: THREE.CatmullRomCurve3;
|
|
38
|
+
speed?: number;
|
|
39
|
+
delay?: number;
|
|
40
|
+
}) {
|
|
41
|
+
const meshRef = useRef<THREE.Mesh>(null);
|
|
42
|
+
const trailRef = useRef<THREE.Mesh>(null);
|
|
43
|
+
const progressRef = useRef(delay);
|
|
44
|
+
|
|
45
|
+
useFrame((_, delta) => {
|
|
46
|
+
if (!meshRef.current) return;
|
|
47
|
+
|
|
48
|
+
progressRef.current += delta * speed * 0.6; // Fast signal speed
|
|
49
|
+
if (progressRef.current > 1) {
|
|
50
|
+
progressRef.current = 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Get position along curve
|
|
54
|
+
const point = curve.getPoint(progressRef.current);
|
|
55
|
+
meshRef.current.position.copy(point);
|
|
56
|
+
|
|
57
|
+
// Bright throughout, slight fade at ends
|
|
58
|
+
const fadeIn = Math.min(progressRef.current * 5, 1);
|
|
59
|
+
const fadeOut = Math.min((1 - progressRef.current) * 5, 1);
|
|
60
|
+
const opacity = fadeIn * fadeOut;
|
|
61
|
+
(meshRef.current.material as THREE.MeshBasicMaterial).opacity = opacity;
|
|
62
|
+
|
|
63
|
+
// Trail effect
|
|
64
|
+
if (trailRef.current) {
|
|
65
|
+
const trailT = Math.max(0, progressRef.current - 0.08);
|
|
66
|
+
const trailPoint = curve.getPoint(trailT);
|
|
67
|
+
trailRef.current.position.copy(trailPoint);
|
|
68
|
+
(trailRef.current.material as THREE.MeshBasicMaterial).opacity = opacity * 0.5;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<>
|
|
74
|
+
{/* Main signal - bright white */}
|
|
75
|
+
<mesh ref={meshRef}>
|
|
76
|
+
<sphereGeometry args={[0.15, 12, 12]} />
|
|
77
|
+
<meshBasicMaterial color="#ffffff" transparent opacity={1} />
|
|
78
|
+
</mesh>
|
|
79
|
+
{/* Glow trail */}
|
|
80
|
+
<mesh ref={trailRef}>
|
|
81
|
+
<sphereGeometry args={[0.1, 8, 8]} />
|
|
82
|
+
<meshBasicMaterial color="#ffffff" transparent opacity={0.6} />
|
|
83
|
+
</mesh>
|
|
84
|
+
</>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Single neural connection with organic curve
|
|
89
|
+
function NeuralConnection({
|
|
90
|
+
link,
|
|
91
|
+
sourcePos,
|
|
92
|
+
targetPos,
|
|
93
|
+
isHovered,
|
|
94
|
+
onHover,
|
|
95
|
+
onUnhover,
|
|
96
|
+
}: {
|
|
97
|
+
link: MemoryLink;
|
|
98
|
+
sourcePos: { x: number; y: number; z: number };
|
|
99
|
+
targetPos: { x: number; y: number; z: number };
|
|
100
|
+
isHovered: boolean;
|
|
101
|
+
onHover: () => void;
|
|
102
|
+
onUnhover: () => void;
|
|
103
|
+
}) {
|
|
104
|
+
const style = RELATIONSHIP_STYLES[link.relationship] || RELATIONSHIP_STYLES.related;
|
|
105
|
+
|
|
106
|
+
// Create organic curved path (like an axon)
|
|
107
|
+
const { curve, points } = useMemo(() => {
|
|
108
|
+
const start = new THREE.Vector3(sourcePos.x, sourcePos.y, sourcePos.z);
|
|
109
|
+
const end = new THREE.Vector3(targetPos.x, targetPos.y, targetPos.z);
|
|
110
|
+
|
|
111
|
+
// Calculate control points for organic curve
|
|
112
|
+
const mid = new THREE.Vector3().lerpVectors(start, end, 0.5);
|
|
113
|
+
const direction = new THREE.Vector3().subVectors(end, start);
|
|
114
|
+
const length = direction.length();
|
|
115
|
+
|
|
116
|
+
// Add perpendicular offset for curve (organic look)
|
|
117
|
+
const perpendicular = new THREE.Vector3(-direction.y, direction.x, direction.z * 0.5).normalize();
|
|
118
|
+
const curveAmount = length * 0.15 * (link.strength + 0.5);
|
|
119
|
+
|
|
120
|
+
// Create control points
|
|
121
|
+
const cp1 = new THREE.Vector3().lerpVectors(start, mid, 0.33);
|
|
122
|
+
cp1.add(perpendicular.clone().multiplyScalar(curveAmount));
|
|
123
|
+
|
|
124
|
+
const cp2 = new THREE.Vector3().lerpVectors(start, mid, 0.66);
|
|
125
|
+
cp2.add(perpendicular.clone().multiplyScalar(curveAmount * 0.5));
|
|
126
|
+
|
|
127
|
+
const cp3 = new THREE.Vector3().lerpVectors(mid, end, 0.33);
|
|
128
|
+
cp3.add(perpendicular.clone().multiplyScalar(-curveAmount * 0.5));
|
|
129
|
+
|
|
130
|
+
const cp4 = new THREE.Vector3().lerpVectors(mid, end, 0.66);
|
|
131
|
+
cp4.add(perpendicular.clone().multiplyScalar(-curveAmount));
|
|
132
|
+
|
|
133
|
+
const curvePoints = [start, cp1, cp2, mid, cp3, cp4, end];
|
|
134
|
+
const curve = new THREE.CatmullRomCurve3(curvePoints);
|
|
135
|
+
const points = curve.getPoints(32);
|
|
136
|
+
|
|
137
|
+
return { curve, points };
|
|
138
|
+
}, [sourcePos, targetPos, link.strength]);
|
|
139
|
+
|
|
140
|
+
// Gray by default, relationship color on hover
|
|
141
|
+
const lineColor = isHovered ? style.color : DEFAULT_LINE_COLOR;
|
|
142
|
+
const lineWidth = isHovered ? 4 : 2 + link.strength * 1.5;
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<group>
|
|
146
|
+
{/* Neural fiber - gray by default, colored on hover */}
|
|
147
|
+
<CatmullRomLine
|
|
148
|
+
points={points}
|
|
149
|
+
color={lineColor}
|
|
150
|
+
lineWidth={lineWidth}
|
|
151
|
+
transparent
|
|
152
|
+
opacity={isHovered ? 1 : 0.8}
|
|
153
|
+
/>
|
|
154
|
+
|
|
155
|
+
{/* Bright white signal pulses traveling along fiber */}
|
|
156
|
+
<NeuralSignal curve={curve} speed={1.2 + link.strength} delay={0} />
|
|
157
|
+
<NeuralSignal curve={curve} speed={1.2 + link.strength} delay={0.5} />
|
|
158
|
+
{link.strength > 0.3 && (
|
|
159
|
+
<NeuralSignal curve={curve} speed={1.5 + link.strength} delay={0.25} />
|
|
160
|
+
)}
|
|
161
|
+
{link.strength > 0.6 && (
|
|
162
|
+
<NeuralSignal curve={curve} speed={1.8 + link.strength} delay={0.75} />
|
|
163
|
+
)}
|
|
164
|
+
|
|
165
|
+
{/* Invisible hit area for hover */}
|
|
166
|
+
<mesh
|
|
167
|
+
position={[
|
|
168
|
+
(sourcePos.x + targetPos.x) / 2,
|
|
169
|
+
(sourcePos.y + targetPos.y) / 2,
|
|
170
|
+
(sourcePos.z + targetPos.z) / 2,
|
|
171
|
+
]}
|
|
172
|
+
onPointerEnter={onHover}
|
|
173
|
+
onPointerLeave={onUnhover}
|
|
174
|
+
>
|
|
175
|
+
<sphereGeometry args={[0.5, 8, 8]} />
|
|
176
|
+
<meshBasicMaterial visible={false} />
|
|
177
|
+
</mesh>
|
|
178
|
+
|
|
179
|
+
{/* Hover tooltip */}
|
|
180
|
+
{isHovered && (
|
|
181
|
+
<Html
|
|
182
|
+
position={[
|
|
183
|
+
(sourcePos.x + targetPos.x) / 2,
|
|
184
|
+
(sourcePos.y + targetPos.y) / 2 + 0.5,
|
|
185
|
+
(sourcePos.z + targetPos.z) / 2,
|
|
186
|
+
]}
|
|
187
|
+
center
|
|
188
|
+
style={{ pointerEvents: 'none' }}
|
|
189
|
+
>
|
|
190
|
+
<div
|
|
191
|
+
className="px-3 py-2 rounded-lg shadow-xl text-xs whitespace-nowrap backdrop-blur-sm"
|
|
192
|
+
style={{
|
|
193
|
+
backgroundColor: 'rgba(15, 23, 42, 0.95)',
|
|
194
|
+
border: `2px solid ${style.color}`,
|
|
195
|
+
color: 'white',
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
<div className="flex items-center gap-2 mb-1">
|
|
199
|
+
<span
|
|
200
|
+
className="w-2 h-2 rounded-full animate-pulse"
|
|
201
|
+
style={{ backgroundColor: style.color }}
|
|
202
|
+
/>
|
|
203
|
+
<span className="font-semibold" style={{ color: style.color }}>
|
|
204
|
+
{style.label}
|
|
205
|
+
</span>
|
|
206
|
+
</div>
|
|
207
|
+
<div className="text-slate-300 text-[10px] space-y-0.5">
|
|
208
|
+
<div className="truncate max-w-[180px]">
|
|
209
|
+
{link.source_title || `Memory #${link.source_id}`}
|
|
210
|
+
</div>
|
|
211
|
+
<div className="text-slate-500">↓</div>
|
|
212
|
+
<div className="truncate max-w-[180px]">
|
|
213
|
+
{link.target_title || `Memory #${link.target_id}`}
|
|
214
|
+
</div>
|
|
215
|
+
<div className="mt-1 pt-1 border-t border-slate-700">
|
|
216
|
+
Strength: <span style={{ color: style.color }}>{(link.strength * 100).toFixed(0)}%</span>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</Html>
|
|
221
|
+
)}
|
|
222
|
+
</group>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function MemoryLinks({ memories, links, memoryPositions }: MemoryLinksProps) {
|
|
227
|
+
const [hoveredLink, setHoveredLink] = useState<string | null>(null);
|
|
228
|
+
|
|
229
|
+
// Filter to only links where both memories exist and have positions
|
|
230
|
+
const validLinks = useMemo(() => {
|
|
231
|
+
const memoryIds = new Set(memories.map(m => m.id));
|
|
232
|
+
return links.filter(link =>
|
|
233
|
+
memoryIds.has(link.source_id) &&
|
|
234
|
+
memoryIds.has(link.target_id) &&
|
|
235
|
+
memoryPositions.has(link.source_id) &&
|
|
236
|
+
memoryPositions.has(link.target_id)
|
|
237
|
+
);
|
|
238
|
+
}, [memories, links, memoryPositions]);
|
|
239
|
+
|
|
240
|
+
const handleHover = useCallback((linkId: string) => {
|
|
241
|
+
setHoveredLink(linkId);
|
|
242
|
+
}, []);
|
|
243
|
+
|
|
244
|
+
const handleUnhover = useCallback(() => {
|
|
245
|
+
setHoveredLink(null);
|
|
246
|
+
}, []);
|
|
247
|
+
|
|
248
|
+
if (validLinks.length === 0) return null;
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<group name="neural-connections">
|
|
252
|
+
{validLinks.map((link) => {
|
|
253
|
+
const sourcePos = memoryPositions.get(link.source_id)!;
|
|
254
|
+
const targetPos = memoryPositions.get(link.target_id)!;
|
|
255
|
+
const linkId = `${link.source_id}-${link.target_id}`;
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<NeuralConnection
|
|
259
|
+
key={linkId}
|
|
260
|
+
link={link}
|
|
261
|
+
sourcePos={sourcePos}
|
|
262
|
+
targetPos={targetPos}
|
|
263
|
+
isHovered={hoveredLink === linkId}
|
|
264
|
+
onHover={() => handleHover(linkId)}
|
|
265
|
+
onUnhover={handleUnhover}
|
|
266
|
+
/>
|
|
267
|
+
);
|
|
268
|
+
})}
|
|
269
|
+
</group>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Memory Node
|
|
5
|
+
* Individual memory rendered as a glowing neuron in 3D space
|
|
6
|
+
*
|
|
7
|
+
* Performance optimizations:
|
|
8
|
+
* - Reduced polygon counts on spheres (8 segments for glow, 12 for main)
|
|
9
|
+
* - Memoized geometries and materials to prevent recreation
|
|
10
|
+
* - Lazy-loaded HTML tooltips (only on hover)
|
|
11
|
+
* - Selection ring uses fewer segments
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useRef, useState, useMemo, memo } from 'react';
|
|
15
|
+
import { useFrame, useThree } from '@react-three/fiber';
|
|
16
|
+
import { Html } from '@react-three/drei';
|
|
17
|
+
import * as THREE from 'three';
|
|
18
|
+
import { Memory } from '@/types/memory';
|
|
19
|
+
import { getCategoryColor } from '@/lib/category-colors';
|
|
20
|
+
import { calculateDecayFactor } from '@/lib/position-algorithm';
|
|
21
|
+
import { getAgeColor } from './TimelineControls';
|
|
22
|
+
|
|
23
|
+
// Holographic color palette (for holographic color mode)
|
|
24
|
+
const JARVIS_GOLD = '#FFD700';
|
|
25
|
+
const JARVIS_AMBER = '#FFB347';
|
|
26
|
+
const JARVIS_ORANGE = '#FF8C00';
|
|
27
|
+
|
|
28
|
+
interface MemoryNodeProps {
|
|
29
|
+
memory: Memory;
|
|
30
|
+
position: [number, number, number];
|
|
31
|
+
onSelect: (memory: Memory) => void;
|
|
32
|
+
isSelected: boolean;
|
|
33
|
+
colorMode?: 'category' | 'health' | 'age' | 'holographic'; // category = by type, health = decay heat map, age = time-based, holographic = Jarvis-style golden
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Format memory age for display
|
|
38
|
+
*/
|
|
39
|
+
function formatAge(createdAt: string | Date): string {
|
|
40
|
+
const age = Date.now() - new Date(createdAt).getTime();
|
|
41
|
+
const hours = age / (60 * 60 * 1000);
|
|
42
|
+
|
|
43
|
+
if (hours < 1) return `${Math.round(hours * 60)}m ago`;
|
|
44
|
+
if (hours < 24) return `${Math.round(hours)}h ago`;
|
|
45
|
+
if (hours < 24 * 7) return `${Math.round(hours / 24)}d ago`;
|
|
46
|
+
if (hours < 24 * 30) return `${Math.round(hours / (24 * 7))}w ago`;
|
|
47
|
+
return `${Math.round(hours / (24 * 30))}mo ago`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Calculate holographic color based on salience - Jarvis-style golden
|
|
52
|
+
* High salience = bright gold, low salience = deep orange
|
|
53
|
+
*/
|
|
54
|
+
function getHolographicColor(salience: number): string {
|
|
55
|
+
if (salience > 0.7) return JARVIS_GOLD; // High salience - bright gold
|
|
56
|
+
if (salience > 0.4) return JARVIS_AMBER; // Medium salience - warm gold
|
|
57
|
+
return JARVIS_ORANGE; // Low salience - deep orange
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Calculate health color based on salience and decay
|
|
62
|
+
* Green (healthy) → Yellow (moderate) → Red (at risk)
|
|
63
|
+
*/
|
|
64
|
+
function getHealthColor(salience: number, decayFactor: number): string {
|
|
65
|
+
const health = salience * decayFactor;
|
|
66
|
+
|
|
67
|
+
if (health > 0.6) {
|
|
68
|
+
// Green - healthy
|
|
69
|
+
return '#22c55e';
|
|
70
|
+
} else if (health > 0.35) {
|
|
71
|
+
// Yellow - moderate
|
|
72
|
+
const t = (health - 0.35) / 0.25; // 0 to 1 within yellow range
|
|
73
|
+
// Interpolate from orange to yellow
|
|
74
|
+
const r = Math.round(245 - t * 11); // 245 to 234
|
|
75
|
+
const g = Math.round(158 + t * 21); // 158 to 179
|
|
76
|
+
return `rgb(${r}, ${g}, 66)`;
|
|
77
|
+
} else {
|
|
78
|
+
// Red/Orange - at risk
|
|
79
|
+
const t = health / 0.35; // 0 to 1 within red range
|
|
80
|
+
const r = Math.round(239); // red
|
|
81
|
+
const g = Math.round(68 + t * 90); // 68 to 158
|
|
82
|
+
return `rgb(${r}, ${g}, 68)`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Shared geometries (created once, reused by all nodes)
|
|
87
|
+
const NODE_GEOMETRY = new THREE.SphereGeometry(1, 16, 16);
|
|
88
|
+
const RING_GEOMETRY = new THREE.RingGeometry(1, 1.15, 24);
|
|
89
|
+
|
|
90
|
+
function MemoryNodeInner({
|
|
91
|
+
memory,
|
|
92
|
+
position,
|
|
93
|
+
onSelect,
|
|
94
|
+
isSelected,
|
|
95
|
+
colorMode = 'category',
|
|
96
|
+
}: MemoryNodeProps) {
|
|
97
|
+
const meshRef = useRef<THREE.Mesh>(null);
|
|
98
|
+
const [hovered, setHovered] = useState(false);
|
|
99
|
+
const { camera } = useThree();
|
|
100
|
+
|
|
101
|
+
// Calculate visual properties (memoized)
|
|
102
|
+
const decayFactor = useMemo(() => calculateDecayFactor(memory), [memory]);
|
|
103
|
+
const categoryColor = useMemo(() => getCategoryColor(memory.category), [memory.category]);
|
|
104
|
+
const healthColor = useMemo(
|
|
105
|
+
() => getHealthColor(memory.salience, decayFactor),
|
|
106
|
+
[memory.salience, decayFactor]
|
|
107
|
+
);
|
|
108
|
+
const ageColor = useMemo(() => getAgeColor(memory.createdAt), [memory.createdAt]);
|
|
109
|
+
const holographicColor = useMemo(() => getHolographicColor(memory.salience), [memory.salience]);
|
|
110
|
+
|
|
111
|
+
// Select color based on mode
|
|
112
|
+
const baseColor = useMemo(() => {
|
|
113
|
+
switch (colorMode) {
|
|
114
|
+
case 'health': return healthColor;
|
|
115
|
+
case 'age': return ageColor;
|
|
116
|
+
case 'holographic': return holographicColor;
|
|
117
|
+
default: return categoryColor;
|
|
118
|
+
}
|
|
119
|
+
}, [colorMode, healthColor, ageColor, holographicColor, categoryColor]);
|
|
120
|
+
|
|
121
|
+
// Node size based on salience (0.2 to 0.4) - larger for better visibility
|
|
122
|
+
const size = useMemo(() => 0.2 + memory.salience * 0.2, [memory.salience]);
|
|
123
|
+
|
|
124
|
+
// Solid node material - no transparency for clarity
|
|
125
|
+
const nodeMaterial = useMemo(
|
|
126
|
+
() =>
|
|
127
|
+
new THREE.MeshStandardMaterial({
|
|
128
|
+
color: baseColor,
|
|
129
|
+
emissive: baseColor,
|
|
130
|
+
emissiveIntensity: 0.3,
|
|
131
|
+
metalness: 0.2,
|
|
132
|
+
roughness: 0.5,
|
|
133
|
+
}),
|
|
134
|
+
[baseColor]
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Subtle animation - increase emissive on hover
|
|
138
|
+
useFrame(() => {
|
|
139
|
+
if (!meshRef.current) return;
|
|
140
|
+
|
|
141
|
+
// Update emissive intensity on hover for highlight effect
|
|
142
|
+
(meshRef.current.material as THREE.MeshStandardMaterial).emissiveIntensity = hovered ? 0.8 : 0.3;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<group position={position}>
|
|
147
|
+
{/* Main node - solid colored sphere */}
|
|
148
|
+
<mesh
|
|
149
|
+
ref={meshRef}
|
|
150
|
+
geometry={NODE_GEOMETRY}
|
|
151
|
+
material={nodeMaterial}
|
|
152
|
+
scale={size}
|
|
153
|
+
onClick={(e) => {
|
|
154
|
+
e.stopPropagation();
|
|
155
|
+
onSelect(memory);
|
|
156
|
+
}}
|
|
157
|
+
onPointerOver={(e) => {
|
|
158
|
+
e.stopPropagation();
|
|
159
|
+
setHovered(true);
|
|
160
|
+
document.body.style.cursor = 'pointer';
|
|
161
|
+
}}
|
|
162
|
+
onPointerOut={() => {
|
|
163
|
+
setHovered(false);
|
|
164
|
+
document.body.style.cursor = 'default';
|
|
165
|
+
}}
|
|
166
|
+
/>
|
|
167
|
+
|
|
168
|
+
{/* Selection ring - only rendered when selected */}
|
|
169
|
+
{isSelected && (
|
|
170
|
+
<mesh geometry={RING_GEOMETRY} rotation={[Math.PI / 2, 0, 0]} scale={size + 0.15}>
|
|
171
|
+
<meshBasicMaterial
|
|
172
|
+
color="#ffffff"
|
|
173
|
+
transparent
|
|
174
|
+
opacity={0.9}
|
|
175
|
+
side={THREE.DoubleSide}
|
|
176
|
+
/>
|
|
177
|
+
</mesh>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
{/* Hover tooltip - lazy loaded only on hover */}
|
|
181
|
+
{hovered && !isSelected && (
|
|
182
|
+
<Html
|
|
183
|
+
distanceFactor={8}
|
|
184
|
+
style={{
|
|
185
|
+
pointerEvents: 'none',
|
|
186
|
+
transform: 'translate(-50%, -120%)',
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
<div className="bg-slate-900/95 border border-slate-700 px-3 py-2 rounded-lg shadow-xl backdrop-blur-sm whitespace-nowrap">
|
|
190
|
+
<div className="text-white font-medium text-sm">{memory.title}</div>
|
|
191
|
+
<div className="flex items-center gap-2 mt-1 text-xs">
|
|
192
|
+
<span
|
|
193
|
+
className="px-1.5 py-0.5 rounded"
|
|
194
|
+
style={{ backgroundColor: categoryColor + '30', color: categoryColor }}
|
|
195
|
+
>
|
|
196
|
+
{memory.category}
|
|
197
|
+
</span>
|
|
198
|
+
<span className="text-slate-400">
|
|
199
|
+
{(memory.salience * 100).toFixed(0)}%
|
|
200
|
+
</span>
|
|
201
|
+
{colorMode === 'health' && (
|
|
202
|
+
<span
|
|
203
|
+
className="px-1.5 py-0.5 rounded"
|
|
204
|
+
style={{ backgroundColor: healthColor + '30', color: healthColor }}
|
|
205
|
+
>
|
|
206
|
+
{memory.salience * decayFactor > 0.6 ? 'Healthy' : memory.salience * decayFactor > 0.35 ? 'Moderate' : 'At Risk'}
|
|
207
|
+
</span>
|
|
208
|
+
)}
|
|
209
|
+
{colorMode === 'age' && (
|
|
210
|
+
<span
|
|
211
|
+
className="px-1.5 py-0.5 rounded"
|
|
212
|
+
style={{ backgroundColor: ageColor + '30', color: ageColor }}
|
|
213
|
+
>
|
|
214
|
+
{formatAge(memory.createdAt)}
|
|
215
|
+
</span>
|
|
216
|
+
)}
|
|
217
|
+
{colorMode === 'holographic' && (
|
|
218
|
+
<span
|
|
219
|
+
className="px-1.5 py-0.5 rounded"
|
|
220
|
+
style={{ backgroundColor: holographicColor + '30', color: holographicColor }}
|
|
221
|
+
>
|
|
222
|
+
{memory.salience > 0.7 ? 'High' : memory.salience > 0.4 ? 'Medium' : 'Low'}
|
|
223
|
+
</span>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</Html>
|
|
228
|
+
)}
|
|
229
|
+
</group>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Memoize the component to prevent re-renders when other nodes change
|
|
234
|
+
export const MemoryNode = memo(MemoryNodeInner, (prev, next) => {
|
|
235
|
+
return (
|
|
236
|
+
prev.memory.id === next.memory.id &&
|
|
237
|
+
prev.memory.salience === next.memory.salience &&
|
|
238
|
+
prev.memory.category === next.memory.category &&
|
|
239
|
+
prev.isSelected === next.isSelected &&
|
|
240
|
+
prev.colorMode === next.colorMode &&
|
|
241
|
+
prev.position[0] === next.position[0] &&
|
|
242
|
+
prev.position[1] === next.position[1] &&
|
|
243
|
+
prev.position[2] === next.position[2]
|
|
244
|
+
);
|
|
245
|
+
});
|