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,306 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Synapse Nodes
|
|
5
|
+
* Glowing junction points where neural connections meet
|
|
6
|
+
* Activity level affects glow intensity and pulsing speed
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useRef, useMemo, useEffect } from 'react';
|
|
10
|
+
import { useFrame } from '@react-three/fiber';
|
|
11
|
+
import * as THREE from 'three';
|
|
12
|
+
|
|
13
|
+
// Jarvis-style golden/orange color palette
|
|
14
|
+
const JARVIS_GOLD = '#FFD700';
|
|
15
|
+
const JARVIS_ORANGE = '#FF8C00';
|
|
16
|
+
const JARVIS_AMBER = '#FFB347';
|
|
17
|
+
|
|
18
|
+
// Shared geometries to prevent memory leaks (created once, reused by all nodes)
|
|
19
|
+
const SYNAPSE_OUTER_GEOMETRY = new THREE.SphereGeometry(1, 8, 8);
|
|
20
|
+
const SYNAPSE_INNER_GEOMETRY = new THREE.SphereGeometry(1, 12, 12);
|
|
21
|
+
const SYNAPSE_CORE_GEOMETRY = new THREE.SphereGeometry(1, 16, 16);
|
|
22
|
+
const SPARK_GEOMETRY = new THREE.SphereGeometry(1, 6, 6);
|
|
23
|
+
|
|
24
|
+
interface SynapseNodeProps {
|
|
25
|
+
position: [number, number, number];
|
|
26
|
+
activity?: number; // 0-1, affects visual intensity
|
|
27
|
+
color?: string;
|
|
28
|
+
size?: number;
|
|
29
|
+
label?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Individual synapse junction point
|
|
34
|
+
*/
|
|
35
|
+
export function SynapseNode({
|
|
36
|
+
position,
|
|
37
|
+
activity = 0.5,
|
|
38
|
+
color,
|
|
39
|
+
size = 0.15,
|
|
40
|
+
}: SynapseNodeProps) {
|
|
41
|
+
const meshRef = useRef<THREE.Mesh>(null);
|
|
42
|
+
const glowRef = useRef<THREE.Mesh>(null);
|
|
43
|
+
const outerGlowRef = useRef<THREE.Mesh>(null);
|
|
44
|
+
|
|
45
|
+
// Determine color based on activity if not specified - Jarvis golden theme
|
|
46
|
+
const synapseColor = useMemo(() => {
|
|
47
|
+
if (color) return color;
|
|
48
|
+
if (activity > 0.7) return JARVIS_GOLD; // High activity - bright gold
|
|
49
|
+
if (activity > 0.4) return JARVIS_AMBER; // Medium activity - warm gold
|
|
50
|
+
return JARVIS_ORANGE; // Low activity - deep orange
|
|
51
|
+
}, [color, activity]);
|
|
52
|
+
|
|
53
|
+
useFrame((state) => {
|
|
54
|
+
if (!meshRef.current) return;
|
|
55
|
+
|
|
56
|
+
const time = state.clock.elapsedTime;
|
|
57
|
+
|
|
58
|
+
// Pulsing speed based on activity (more active = faster pulse)
|
|
59
|
+
const pulseSpeed = 2 + activity * 4;
|
|
60
|
+
const pulse = Math.sin(time * pulseSpeed) * 0.5 + 0.5;
|
|
61
|
+
|
|
62
|
+
// Scale pulsing
|
|
63
|
+
const scale = size * (1 + pulse * 0.3 * activity);
|
|
64
|
+
meshRef.current.scale.setScalar(scale);
|
|
65
|
+
|
|
66
|
+
// Core opacity
|
|
67
|
+
(meshRef.current.material as THREE.MeshBasicMaterial).opacity =
|
|
68
|
+
0.7 + pulse * 0.3;
|
|
69
|
+
|
|
70
|
+
// Inner glow
|
|
71
|
+
if (glowRef.current) {
|
|
72
|
+
glowRef.current.scale.setScalar(scale * 2);
|
|
73
|
+
(glowRef.current.material as THREE.MeshBasicMaterial).opacity =
|
|
74
|
+
(0.3 + pulse * 0.2) * activity;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Outer glow (slower pulse)
|
|
78
|
+
if (outerGlowRef.current) {
|
|
79
|
+
const outerPulse = Math.sin(time * pulseSpeed * 0.5) * 0.5 + 0.5;
|
|
80
|
+
outerGlowRef.current.scale.setScalar(scale * 3.5 * (1 + outerPulse * 0.2));
|
|
81
|
+
(outerGlowRef.current.material as THREE.MeshBasicMaterial).opacity =
|
|
82
|
+
(0.1 + outerPulse * 0.1) * activity;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Materials are created per-node since color varies, but geometries are shared
|
|
87
|
+
const outerMaterial = useMemo(
|
|
88
|
+
() => new THREE.MeshBasicMaterial({
|
|
89
|
+
color: synapseColor,
|
|
90
|
+
transparent: true,
|
|
91
|
+
opacity: 0.1,
|
|
92
|
+
depthWrite: false,
|
|
93
|
+
}),
|
|
94
|
+
[synapseColor]
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const innerMaterial = useMemo(
|
|
98
|
+
() => new THREE.MeshBasicMaterial({
|
|
99
|
+
color: synapseColor,
|
|
100
|
+
transparent: true,
|
|
101
|
+
opacity: 0.3,
|
|
102
|
+
depthWrite: false,
|
|
103
|
+
}),
|
|
104
|
+
[synapseColor]
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const coreMaterial = useMemo(
|
|
108
|
+
() => new THREE.MeshBasicMaterial({
|
|
109
|
+
color: synapseColor,
|
|
110
|
+
transparent: true,
|
|
111
|
+
opacity: 0.9,
|
|
112
|
+
}),
|
|
113
|
+
[synapseColor]
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Cleanup materials on unmount
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
return () => {
|
|
119
|
+
outerMaterial.dispose();
|
|
120
|
+
innerMaterial.dispose();
|
|
121
|
+
coreMaterial.dispose();
|
|
122
|
+
};
|
|
123
|
+
}, [outerMaterial, innerMaterial, coreMaterial]);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<group position={position}>
|
|
127
|
+
{/* Outer glow halo - uses shared geometry */}
|
|
128
|
+
<mesh ref={outerGlowRef} geometry={SYNAPSE_OUTER_GEOMETRY} material={outerMaterial} />
|
|
129
|
+
|
|
130
|
+
{/* Inner glow - uses shared geometry */}
|
|
131
|
+
<mesh ref={glowRef} geometry={SYNAPSE_INNER_GEOMETRY} material={innerMaterial} />
|
|
132
|
+
|
|
133
|
+
{/* Core node - uses shared geometry */}
|
|
134
|
+
<mesh ref={meshRef} geometry={SYNAPSE_CORE_GEOMETRY} material={coreMaterial} />
|
|
135
|
+
</group>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Action potential burst - triggered when a memory is accessed
|
|
141
|
+
*/
|
|
142
|
+
interface ActionPotentialProps {
|
|
143
|
+
position: [number, number, number];
|
|
144
|
+
onComplete?: () => void;
|
|
145
|
+
color?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function ActionPotential({
|
|
149
|
+
position,
|
|
150
|
+
onComplete,
|
|
151
|
+
color = JARVIS_GOLD, // Changed from white to Jarvis gold
|
|
152
|
+
}: ActionPotentialProps) {
|
|
153
|
+
const meshRef = useRef<THREE.Mesh>(null);
|
|
154
|
+
const progressRef = useRef(0);
|
|
155
|
+
|
|
156
|
+
useFrame((_, delta) => {
|
|
157
|
+
if (!meshRef.current) return;
|
|
158
|
+
|
|
159
|
+
progressRef.current += delta * 2;
|
|
160
|
+
|
|
161
|
+
if (progressRef.current >= 1) {
|
|
162
|
+
onComplete?.();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Expand and fade
|
|
167
|
+
const scale = 0.1 + progressRef.current * 2;
|
|
168
|
+
meshRef.current.scale.setScalar(scale);
|
|
169
|
+
(meshRef.current.material as THREE.MeshBasicMaterial).opacity =
|
|
170
|
+
(1 - progressRef.current) * 0.8;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const material = useMemo(
|
|
174
|
+
() => new THREE.MeshBasicMaterial({
|
|
175
|
+
color,
|
|
176
|
+
transparent: true,
|
|
177
|
+
opacity: 0.8,
|
|
178
|
+
depthWrite: false,
|
|
179
|
+
}),
|
|
180
|
+
[color]
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
return () => material.dispose();
|
|
185
|
+
}, [material]);
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<mesh ref={meshRef} position={position} geometry={SYNAPSE_CORE_GEOMETRY} material={material} />
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Synaptic spark - small burst effect
|
|
194
|
+
*/
|
|
195
|
+
export function SynapticSpark({
|
|
196
|
+
position,
|
|
197
|
+
direction,
|
|
198
|
+
color = JARVIS_AMBER, // Changed from cyan to Jarvis amber
|
|
199
|
+
}: {
|
|
200
|
+
position: [number, number, number];
|
|
201
|
+
direction: [number, number, number];
|
|
202
|
+
color?: string;
|
|
203
|
+
}) {
|
|
204
|
+
const meshRef = useRef<THREE.Mesh>(null);
|
|
205
|
+
const progressRef = useRef(0);
|
|
206
|
+
|
|
207
|
+
useFrame((_, delta) => {
|
|
208
|
+
if (!meshRef.current) return;
|
|
209
|
+
|
|
210
|
+
progressRef.current += delta * 3;
|
|
211
|
+
|
|
212
|
+
if (progressRef.current >= 1) {
|
|
213
|
+
meshRef.current.visible = false;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Move in direction
|
|
218
|
+
meshRef.current.position.set(
|
|
219
|
+
position[0] + direction[0] * progressRef.current * 0.5,
|
|
220
|
+
position[1] + direction[1] * progressRef.current * 0.5,
|
|
221
|
+
position[2] + direction[2] * progressRef.current * 0.5
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
// Shrink and fade
|
|
225
|
+
meshRef.current.scale.setScalar(0.1 * (1 - progressRef.current));
|
|
226
|
+
(meshRef.current.material as THREE.MeshBasicMaterial).opacity =
|
|
227
|
+
(1 - progressRef.current) * 0.9;
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const material = useMemo(
|
|
231
|
+
() => new THREE.MeshBasicMaterial({
|
|
232
|
+
color,
|
|
233
|
+
transparent: true,
|
|
234
|
+
opacity: 0.9,
|
|
235
|
+
}),
|
|
236
|
+
[color]
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
return () => material.dispose();
|
|
241
|
+
}, [material]);
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<mesh ref={meshRef} position={position} geometry={SPARK_GEOMETRY} material={material} />
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Collection of synapse nodes for the brain visualization
|
|
250
|
+
*/
|
|
251
|
+
interface SynapseNetworkProps {
|
|
252
|
+
nodes?: Array<{
|
|
253
|
+
position: [number, number, number];
|
|
254
|
+
activity: number;
|
|
255
|
+
color?: string;
|
|
256
|
+
}>;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
type SynapseNodeData = {
|
|
260
|
+
position: [number, number, number];
|
|
261
|
+
activity: number;
|
|
262
|
+
color?: string;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
export function SynapseNetwork({ nodes }: SynapseNetworkProps) {
|
|
266
|
+
// Default synapse positions throughout the brain
|
|
267
|
+
const defaultNodes = useMemo<SynapseNodeData[]>(
|
|
268
|
+
() => [
|
|
269
|
+
// Frontal region (STM)
|
|
270
|
+
{ position: [0.5, 0.3, 2.5], activity: 0.8 },
|
|
271
|
+
{ position: [-0.5, 0.2, 2.3], activity: 0.7 },
|
|
272
|
+
{ position: [0, 0.5, 2.8], activity: 0.9 },
|
|
273
|
+
|
|
274
|
+
// Middle region (Episodic)
|
|
275
|
+
{ position: [1, 0.3, 0.5], activity: 0.6 },
|
|
276
|
+
{ position: [-1, 0.2, 0.3], activity: 0.5 },
|
|
277
|
+
{ position: [0, 0.8, 0], activity: 0.7 },
|
|
278
|
+
{ position: [1.5, 0, -0.5], activity: 0.4 },
|
|
279
|
+
{ position: [-1.5, 0.1, -0.3], activity: 0.5 },
|
|
280
|
+
|
|
281
|
+
// Back region (LTM)
|
|
282
|
+
{ position: [0.3, 0.2, -2.5], activity: 0.3 },
|
|
283
|
+
{ position: [-0.5, 0.3, -2.3], activity: 0.4 },
|
|
284
|
+
{ position: [0, 0.4, -2.8], activity: 0.3 },
|
|
285
|
+
{ position: [1, 0.1, -2], activity: 0.2 },
|
|
286
|
+
{ position: [-1, 0.2, -2.2], activity: 0.25 },
|
|
287
|
+
],
|
|
288
|
+
[]
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const synapseNodes: SynapseNodeData[] = nodes || defaultNodes;
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<group name="synapse-network">
|
|
295
|
+
{synapseNodes.map((node, i) => (
|
|
296
|
+
<SynapseNode
|
|
297
|
+
key={i}
|
|
298
|
+
position={node.position}
|
|
299
|
+
activity={node.activity}
|
|
300
|
+
color={node.color}
|
|
301
|
+
size={0.08 + node.activity * 0.04}
|
|
302
|
+
/>
|
|
303
|
+
))}
|
|
304
|
+
</group>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Timeline Controls
|
|
5
|
+
*
|
|
6
|
+
* Provides timeline-based filtering and age visualization for memories.
|
|
7
|
+
* Shows a slider to filter by time range and displays age distribution.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useMemo, useState, useCallback } from 'react';
|
|
11
|
+
import { Memory } from '@/types/memory';
|
|
12
|
+
|
|
13
|
+
type ColorMode = 'category' | 'health' | 'age' | 'holographic';
|
|
14
|
+
|
|
15
|
+
interface TimelineControlsProps {
|
|
16
|
+
memories: Memory[];
|
|
17
|
+
onTimeRangeChange: (range: { start: Date | null; end: Date | null }) => void;
|
|
18
|
+
colorMode: ColorMode;
|
|
19
|
+
onColorModeChange: (mode: ColorMode) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Time presets for quick filtering
|
|
23
|
+
const TIME_PRESETS = [
|
|
24
|
+
{ label: 'All', hours: null },
|
|
25
|
+
{ label: '1h', hours: 1 },
|
|
26
|
+
{ label: '24h', hours: 24 },
|
|
27
|
+
{ label: '7d', hours: 24 * 7 },
|
|
28
|
+
{ label: '30d', hours: 24 * 30 },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export function TimelineControls({
|
|
32
|
+
memories,
|
|
33
|
+
onTimeRangeChange,
|
|
34
|
+
colorMode,
|
|
35
|
+
onColorModeChange,
|
|
36
|
+
}: TimelineControlsProps) {
|
|
37
|
+
const [activePreset, setActivePreset] = useState<number | null>(null);
|
|
38
|
+
|
|
39
|
+
// Calculate memory age distribution for the histogram
|
|
40
|
+
const ageDistribution = useMemo(() => {
|
|
41
|
+
if (!memories.length) return [];
|
|
42
|
+
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
const buckets = [
|
|
45
|
+
{ label: '<1h', max: 60 * 60 * 1000, count: 0 },
|
|
46
|
+
{ label: '1-24h', max: 24 * 60 * 60 * 1000, count: 0 },
|
|
47
|
+
{ label: '1-7d', max: 7 * 24 * 60 * 60 * 1000, count: 0 },
|
|
48
|
+
{ label: '7-30d', max: 30 * 24 * 60 * 60 * 1000, count: 0 },
|
|
49
|
+
{ label: '>30d', max: Infinity, count: 0 },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
memories.forEach((memory) => {
|
|
53
|
+
const age = now - new Date(memory.createdAt).getTime();
|
|
54
|
+
for (const bucket of buckets) {
|
|
55
|
+
if (age < bucket.max) {
|
|
56
|
+
bucket.count++;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const maxCount = Math.max(...buckets.map((b) => b.count), 1);
|
|
63
|
+
return buckets.map((b) => ({ ...b, height: (b.count / maxCount) * 100 }));
|
|
64
|
+
}, [memories]);
|
|
65
|
+
|
|
66
|
+
// Calculate oldest and newest memory dates
|
|
67
|
+
const dateRange = useMemo(() => {
|
|
68
|
+
if (!memories.length) return { oldest: null, newest: null };
|
|
69
|
+
const dates = memories.map((m) => new Date(m.createdAt).getTime());
|
|
70
|
+
return {
|
|
71
|
+
oldest: new Date(Math.min(...dates)),
|
|
72
|
+
newest: new Date(Math.max(...dates)),
|
|
73
|
+
};
|
|
74
|
+
}, [memories]);
|
|
75
|
+
|
|
76
|
+
const handlePresetClick = useCallback(
|
|
77
|
+
(hours: number | null, index: number) => {
|
|
78
|
+
setActivePreset(index);
|
|
79
|
+
if (hours === null) {
|
|
80
|
+
onTimeRangeChange({ start: null, end: null });
|
|
81
|
+
} else {
|
|
82
|
+
const end = new Date();
|
|
83
|
+
const start = new Date(end.getTime() - hours * 60 * 60 * 1000);
|
|
84
|
+
onTimeRangeChange({ start, end });
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
[onTimeRangeChange]
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="absolute bottom-4 right-4 bg-slate-900/90 border border-slate-700 rounded-lg p-3 backdrop-blur-sm min-w-[200px]">
|
|
92
|
+
{/* Color Mode Toggle */}
|
|
93
|
+
<div className="mb-3">
|
|
94
|
+
<div className="text-xs text-slate-400 mb-1.5">Color Mode</div>
|
|
95
|
+
<div className="flex gap-1">
|
|
96
|
+
{[
|
|
97
|
+
{ value: 'category', label: 'Category', icon: '🎨' },
|
|
98
|
+
{ value: 'health', label: 'Health', icon: '💚' },
|
|
99
|
+
{ value: 'age', label: 'Age', icon: '⏰' },
|
|
100
|
+
{ value: 'holographic', label: 'Holo', icon: '✨' },
|
|
101
|
+
].map((mode) => (
|
|
102
|
+
<button
|
|
103
|
+
key={mode.value}
|
|
104
|
+
onClick={() => onColorModeChange(mode.value as ColorMode)}
|
|
105
|
+
className={`flex-1 px-2 py-1 text-[10px] rounded transition-colors ${
|
|
106
|
+
colorMode === mode.value
|
|
107
|
+
? mode.value === 'holographic' ? 'bg-amber-500 text-white' : 'bg-blue-600 text-white'
|
|
108
|
+
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
|
|
109
|
+
}`}
|
|
110
|
+
>
|
|
111
|
+
<span className="mr-0.5">{mode.icon}</span>
|
|
112
|
+
{mode.label}
|
|
113
|
+
</button>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{/* Time Filter Presets */}
|
|
119
|
+
<div className="mb-3">
|
|
120
|
+
<div className="text-xs text-slate-400 mb-1.5">Time Filter</div>
|
|
121
|
+
<div className="flex gap-1">
|
|
122
|
+
{TIME_PRESETS.map((preset, i) => (
|
|
123
|
+
<button
|
|
124
|
+
key={preset.label}
|
|
125
|
+
onClick={() => handlePresetClick(preset.hours, i)}
|
|
126
|
+
className={`flex-1 px-2 py-1 text-[10px] rounded transition-colors ${
|
|
127
|
+
activePreset === i
|
|
128
|
+
? 'bg-cyan-600 text-white'
|
|
129
|
+
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
|
|
130
|
+
}`}
|
|
131
|
+
>
|
|
132
|
+
{preset.label}
|
|
133
|
+
</button>
|
|
134
|
+
))}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{/* Age Distribution Histogram */}
|
|
139
|
+
<div>
|
|
140
|
+
<div className="text-xs text-slate-400 mb-1.5">Age Distribution</div>
|
|
141
|
+
<div className="flex items-end gap-0.5 h-8">
|
|
142
|
+
{ageDistribution.map((bucket) => (
|
|
143
|
+
<div key={bucket.label} className="flex-1 flex flex-col items-center">
|
|
144
|
+
<div
|
|
145
|
+
className="w-full bg-gradient-to-t from-cyan-600 to-cyan-400 rounded-t transition-all"
|
|
146
|
+
style={{ height: `${Math.max(bucket.height, 4)}%` }}
|
|
147
|
+
title={`${bucket.label}: ${bucket.count} memories`}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
<div className="flex gap-0.5 mt-0.5">
|
|
153
|
+
{ageDistribution.map((bucket) => (
|
|
154
|
+
<div
|
|
155
|
+
key={bucket.label}
|
|
156
|
+
className="flex-1 text-[8px] text-slate-500 text-center truncate"
|
|
157
|
+
>
|
|
158
|
+
{bucket.label}
|
|
159
|
+
</div>
|
|
160
|
+
))}
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{/* Date Range Info */}
|
|
165
|
+
{dateRange.oldest && dateRange.newest && (
|
|
166
|
+
<div className="mt-2 pt-2 border-t border-slate-700 text-[9px] text-slate-500">
|
|
167
|
+
<div className="flex justify-between">
|
|
168
|
+
<span>Oldest:</span>
|
|
169
|
+
<span>{dateRange.oldest.toLocaleDateString()}</span>
|
|
170
|
+
</div>
|
|
171
|
+
<div className="flex justify-between">
|
|
172
|
+
<span>Newest:</span>
|
|
173
|
+
<span>{dateRange.newest.toLocaleDateString()}</span>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get age-based color for a memory
|
|
183
|
+
* New (cyan) → Recent (green) → Old (amber) → Ancient (red)
|
|
184
|
+
*/
|
|
185
|
+
export function getAgeColor(createdAt: string | Date): string {
|
|
186
|
+
const age = Date.now() - new Date(createdAt).getTime();
|
|
187
|
+
const hours = age / (60 * 60 * 1000);
|
|
188
|
+
|
|
189
|
+
if (hours < 1) {
|
|
190
|
+
// Very new - bright cyan
|
|
191
|
+
return '#22d3ee';
|
|
192
|
+
} else if (hours < 24) {
|
|
193
|
+
// Recent - green
|
|
194
|
+
return '#22c55e';
|
|
195
|
+
} else if (hours < 24 * 7) {
|
|
196
|
+
// This week - yellow
|
|
197
|
+
return '#eab308';
|
|
198
|
+
} else if (hours < 24 * 30) {
|
|
199
|
+
// This month - amber/orange
|
|
200
|
+
return '#f97316';
|
|
201
|
+
} else {
|
|
202
|
+
// Old - red
|
|
203
|
+
return '#ef4444';
|
|
204
|
+
}
|
|
205
|
+
}
|