'use client'; /** * Brain Scene * Main 3D visualization of the memory brain * Composes all brain visualization components into a cohesive scene * * Features: * - Real-time activity pulses on memory events * - Category region labels * - Color modes: category, health (decay), age * - Timeline filtering */ import { Suspense, useMemo, useState, useCallback } from 'react'; import { Canvas } from '@react-three/fiber'; import { OrbitControls } from '@react-three/drei'; // Post-processing disabled for cleaner appearance // import { EffectComposer, Bloom, Vignette, ChromaticAberration, Noise } from '@react-three/postprocessing'; import { Memory, MemoryLink } from '@/types/memory'; import { MemoryNode } from './MemoryNode'; import { MemoryLinks } from './MemoryLinks'; import { BrainMesh } from './BrainMesh'; import { ActivityPulseSystem, Pulse } from './ActivityPulseSystem'; import { TimelineControls } from './TimelineControls'; // Removed for cleaner design: BrainRegions, Stars import { calculateMemoryPosition } from '@/lib/position-algorithm'; import { useMemoryWebSocket } from '@/lib/websocket'; type ColorMode = 'category' | 'health' | 'age' | 'holographic'; interface BrainSceneProps { memories: Memory[]; links?: MemoryLink[]; selectedMemory: Memory | null; onSelectMemory: (memory: Memory | null) => void; } interface BrainContentProps extends BrainSceneProps { colorMode: ColorMode; pulses: Pulse[]; onPulseComplete: (id: number) => void; memoryCategoryCounts: Record; } function BrainContent({ memories = [], links = [], selectedMemory, onSelectMemory, colorMode, pulses, onPulseComplete, memoryCategoryCounts: _memoryCategoryCounts, }: BrainContentProps) { // Calculate positions for all memories (deduplicate by ID to prevent React key errors) const memoryPositions = useMemo(() => { if (!memories || memories.length === 0) return []; // Use Map to deduplicate by memory ID const uniqueMemories = new Map(memories.map(m => [m.id, m])); return Array.from(uniqueMemories.values()).map((memory) => ({ memory, position: calculateMemoryPosition(memory), })); }, [memories]); // Create a map for quick position lookup by memory ID const positionMap = useMemo(() => { const map = new Map(); memoryPositions.forEach(({ memory, position }) => { map.set(memory.id, position); }); return map; }, [memoryPositions]); return ( <> {/* Simple, even lighting for maximum clarity */} {/* Ghost brain outline - barely visible context */} {/* Neural connections - the main visual feature */} {/* Memory nodes - clear, colored spheres */} {memoryPositions.map(({ memory, position }) => ( ))} {/* Activity pulses for real-time events */} {/* Camera controls - user controlled, no auto-rotate */} ); } export function BrainScene({ memories = [], links = [], selectedMemory, onSelectMemory, }: BrainSceneProps) { const [colorMode, setColorMode] = useState('category'); const [pulses, setPulses] = useState([]); const [timeRange, setTimeRange] = useState<{ start: Date | null; end: Date | null }>({ start: null, end: null, }); const pulseIdRef = { current: 0 }; // Calculate memory counts by category const memoryCategoryCounts = useMemo(() => { const counts: Record = {}; memories.forEach((m) => { counts[m.category] = (counts[m.category] || 0) + 1; }); return counts; }, [memories]); // Filter memories by time range const filteredMemories = useMemo(() => { if (!timeRange.start && !timeRange.end) return memories; return memories.filter((m) => { const createdAt = new Date(m.createdAt).getTime(); if (timeRange.start && createdAt < timeRange.start.getTime()) return false; if (timeRange.end && createdAt > timeRange.end.getTime()) return false; return true; }); }, [memories, timeRange]); // Create position map for pulse positioning const positionMap = useMemo(() => { const map = new Map(); memories.forEach((memory) => { map.set(memory.id, calculateMemoryPosition(memory)); }); return map; }, [memories]); // Create a pulse for memory events const createPulse = useCallback( (type: Pulse['type'], memoryId: number, targetMemoryId?: number) => { const position = positionMap.get(memoryId); if (!position) return; const targetPosition = targetMemoryId ? positionMap.get(targetMemoryId) : undefined; const pulse: Pulse = { id: pulseIdRef.current++, type, position: [position.x, position.y, position.z], targetPosition: targetPosition ? [targetPosition.x, targetPosition.y, targetPosition.z] : undefined, startTime: Date.now(), duration: type === 'created' ? 2000 : type === 'accessed' ? 1000 : 1500, color: type === 'created' ? '#22c55e' : type === 'accessed' ? '#3b82f6' : '#a855f7', }; setPulses((prev) => [...prev, pulse]); }, [positionMap] ); // Handle pulse completion const handlePulseComplete = useCallback((id: number) => { setPulses((prev) => prev.filter((p) => p.id !== id)); }, []); // Listen for WebSocket events to trigger pulses useMemoryWebSocket({ onMessage: (event) => { const data = event.data as Record | undefined; switch (event.type) { case 'memory_created': if (data?.memoryId) createPulse('created', data.memoryId as number); break; case 'memory_accessed': if (data?.memoryId) createPulse('accessed', data.memoryId as number); break; case 'link_discovered': if (data?.sourceId && data?.targetId) createPulse('linked', data.sourceId as number, data.targetId as number); break; } }, }); return (
onSelectMemory(null)} > {/* Simple info overlay */}

{filteredMemories.length} memories

Click to select • Drag to rotate

{/* Timeline and color controls */}
); }