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.
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,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
+ }