| 1 | "use client"; |
| 2 | |
| 3 | import { useRef, useMemo, useEffect, useState, useCallback } from "react"; |
| 4 | import { Canvas, useFrame, useThree } from "@react-three/fiber"; |
| 5 | import { Text, Line } from "@react-three/drei"; |
| 6 | import * as THREE from "three"; |
| 7 | import type { HealthResponse, DataSource } from "@/lib/types"; |
| 8 | |
| 9 | |
| 10 | const SOURCE_CONFIG: Record<DataSource, { label: string; basePos: [number, number, number] }> = { |
| 11 | proxmox: { label: "PROXMOX", basePos: [-4.0, 2.2, -0.4] }, |
| 12 | "white-pc": { label: "WHITE-PC", basePos: [ 3.8, 2.6, 0.2] }, |
| 13 | "3060": { label: "RTX 3060", basePos: [ 2.8, -1.8, 0.7] }, |
| 14 | "browser-history":{ label: "BROWSER", basePos: [ 0.5, 3.8, 0.3] }, |
| 15 | agents: { label: "AGENTS", basePos: [-1.8, -3.4, 0.0] }, |
| 16 | rag: { label: "RAG", basePos: [-3.6, -0.6, 0.9] }, |
| 17 | cbm: { label: "CBM", basePos: [ 2.4, 1.0, -1.0] }, |
| 18 | serena: { label: "SERENA", basePos: [-1.0, 2.6, -1.6] }, |
| 19 | memgraph: { label: "MEMGRAPH", basePos: [ 0.3, -2.6, -1.0] }, |
| 20 | inbox: { label: "INBOX", basePos: [ 4.4, -0.6, -0.3] }, |
| 21 | }; |
| 22 | |
| 23 | const SOURCES = Object.keys(SOURCE_CONFIG) as DataSource[]; |
| 24 | |
| 25 | const CONNECTIONS: [DataSource, DataSource][] = [ |
| 26 | ["rag", "proxmox"], |
| 27 | ["rag", "agents"], |
| 28 | ["rag", "cbm"], |
| 29 | ["cbm", "serena"], |
| 30 | ["cbm", "proxmox"], |
| 31 | ["agents", "memgraph"], |
| 32 | ["agents", "inbox"], |
| 33 | ["serena", "proxmox"], |
| 34 | ["memgraph", "rag"], |
| 35 | ["inbox", "agents"], |
| 36 | ["3060", "agents"], |
| 37 | ["white-pc", "rag"], |
| 38 | ["browser-history", "memgraph"], |
| 39 | ]; |
| 40 | |
| 41 | |
| 42 | const TEAL = new THREE.Color("#16e0c8"); |
| 43 | const TEAL_DIM = new THREE.Color("#0d9e8e"); |
| 44 | const GREY = new THREE.Color("#3a3a3a"); |
| 45 | const DEAD = new THREE.Color("#1a1a1a"); |
| 46 | |
| 47 | function healthColor(staleness_h: number, status: "up" | "down"): THREE.Color { |
| 48 | if (status === "down") return DEAD; |
| 49 | if (staleness_h < 2) return TEAL; |
| 50 | if (staleness_h < 12) return TEAL_DIM; |
| 51 | if (staleness_h < 24) return GREY; |
| 52 | return DEAD; |
| 53 | } |
| 54 | |
| 55 | function healthScale(staleness_h: number, status: "up" | "down"): number { |
| 56 | if (status === "down") return 0.3; |
| 57 | if (staleness_h > 24) return 0.4; |
| 58 | if (staleness_h > 12) return 0.7; |
| 59 | return 1.0; |
| 60 | } |
| 61 | |
| 62 | |
| 63 | interface NodeProps { |
| 64 | id: DataSource; |
| 65 | targetPos: [number, number, number]; |
| 66 | color: THREE.Color; |
| 67 | scale: number; |
| 68 | label: string; |
| 69 | cursorWorld: React.MutableRefObject<THREE.Vector3>; |
| 70 | spawnProgress: number; |
| 71 | isStale: boolean; |
| 72 | collapse: React.MutableRefObject<number>; |
| 73 | } |
| 74 | |
| 75 | function OrgNode({ id, targetPos, color, scale, label, cursorWorld, spawnProgress, isStale, collapse }: NodeProps) { |
| 76 | const meshRef = useRef<THREE.Mesh>(null); |
| 77 | const currentPos = useRef(new THREE.Vector3(0, 0, 0)); |
| 78 | const target = useMemo(() => new THREE.Vector3(...targetPos), [targetPos]); |
| 79 | const glowRef = useRef<THREE.PointLight>(null); |
| 80 | const timeRef = useRef(Math.random() * Math.PI * 2); |
| 81 | |
| 82 | useFrame((_, delta) => { |
| 83 | if (!meshRef.current) return; |
| 84 | timeRef.current += delta; |
| 85 | |
| 86 | const attraction = new THREE.Vector3().copy(cursorWorld.current).sub(target); |
| 87 | const dist = attraction.length(); |
| 88 | const pull = dist < 4 ? (1 - dist / 4) * 0.4 : 0; |
| 89 | |
| 90 | const pullTarget = new THREE.Vector3() |
| 91 | .copy(target) |
| 92 | .addScaledVector(attraction.normalize(), pull * -1); |
| 93 | |
| 94 | pullTarget.multiplyScalar(1 - collapse.current * 0.82); |
| 95 | |
| 96 | currentPos.current.lerp(pullTarget.multiplyScalar(spawnProgress), 0.08); |
| 97 | meshRef.current.position.copy(currentPos.current); |
| 98 | |
| 99 | const breathe = 1 + Math.sin(timeRef.current * 0.7 + timeRef.current) * 0.05; |
| 100 | const finalScale = scale * breathe * spawnProgress * (1 - collapse.current * 0.35); |
| 101 | meshRef.current.scale.setScalar(finalScale); |
| 102 | |
| 103 | if (glowRef.current) { |
| 104 | const intensity = isStale ? 0 : 0.5 + Math.sin(timeRef.current * 1.2) * 0.2; |
| 105 | glowRef.current.intensity = intensity * spawnProgress; |
| 106 | } |
| 107 | }); |
| 108 | |
| 109 | return ( |
| 110 | <group> |
| 111 | <mesh ref={meshRef}> |
| 112 | {} |
| 113 | <sphereGeometry args={[0.32, 24, 24]} /> |
| 114 | <meshStandardMaterial |
| 115 | color={color} |
| 116 | emissive={color} |
| 117 | emissiveIntensity={isStale ? 0.08 : 0.8} |
| 118 | roughness={0.15} |
| 119 | metalness={0.4} |
| 120 | /> |
| 121 | </mesh> |
| 122 | <pointLight |
| 123 | ref={glowRef} |
| 124 | color={color} |
| 125 | intensity={0} |
| 126 | distance={3.5} |
| 127 | position={[...targetPos] as [number, number, number]} |
| 128 | /> |
| 129 | <Text |
| 130 | position={[targetPos[0], targetPos[1] + 0.55, targetPos[2]]} |
| 131 | fontSize={0.15} |
| 132 | letterSpacing={0.06} |
| 133 | color={isStale ? "#3a3a3a" : "#16e0c8"} |
| 134 | anchorX="center" |
| 135 | anchorY="bottom" |
| 136 | outlineWidth={0} |
| 137 | material-toneMapped={false} |
| 138 | > |
| 139 | {label} |
| 140 | </Text> |
| 141 | </group> |
| 142 | ); |
| 143 | } |
| 144 | |
| 145 | |
| 146 | interface SceneProps { |
| 147 | healthData: HealthResponse | null; |
| 148 | focusMode: boolean; |
| 149 | } |
| 150 | |
| 151 | function OrganismScene({ healthData, focusMode }: SceneProps) { |
| 152 | const { camera, gl } = useThree(); |
| 153 | const cursorWorld = useRef(new THREE.Vector3(0, 0, 0)); |
| 154 | const [spawnProgress, setSpawnProgress] = useState(0); |
| 155 | const startTime = useRef(Date.now()); |
| 156 | const collapse = useRef(0); |
| 157 | const centerGlowRef = useRef<THREE.PointLight>(null); |
| 158 | |
| 159 | useEffect(() => { |
| 160 | camera.position.set(0, 0, 6); |
| 161 | }, [camera]); |
| 162 | |
| 163 | useEffect(() => { |
| 164 | let rafId: number; |
| 165 | function tick() { |
| 166 | const elapsed = (Date.now() - startTime.current) / 1200; |
| 167 | const t = Math.min(elapsed, 1); |
| 168 | const eased = t === 1 ? 1 : 1 - Math.pow(2, -10 * t); |
| 169 | setSpawnProgress(eased); |
| 170 | if (t < 1) rafId = requestAnimationFrame(tick); |
| 171 | } |
| 172 | rafId = requestAnimationFrame(tick); |
| 173 | return () => cancelAnimationFrame(rafId); |
| 174 | }, []); |
| 175 | |
| 176 | useEffect(() => { |
| 177 | const canvas = gl.domElement; |
| 178 | function onMouseMove(e: MouseEvent) { |
| 179 | const rect = canvas.getBoundingClientRect(); |
| 180 | const nx = ((e.clientX - rect.left) / rect.width) * 2 - 1; |
| 181 | const ny = -((e.clientY - rect.top) / rect.height) * 2 + 1; |
| 182 | const vec = new THREE.Vector3(nx, ny, 0.5); |
| 183 | vec.unproject(camera); |
| 184 | vec.sub(camera.position).normalize(); |
| 185 | const dist = -camera.position.z / vec.z; |
| 186 | const pos = new THREE.Vector3().addVectors(camera.position, vec.multiplyScalar(dist)); |
| 187 | cursorWorld.current.copy(pos); |
| 188 | } |
| 189 | canvas.addEventListener("mousemove", onMouseMove, { passive: true }); |
| 190 | return () => canvas.removeEventListener("mousemove", onMouseMove); |
| 191 | }, [camera, gl]); |
| 192 | |
| 193 | const healthMap = useMemo(() => { |
| 194 | const map = new Map<string, { staleness_h: number; status: "up" | "down" }>(); |
| 195 | if (healthData) { |
| 196 | for (const eng of healthData.engines) { |
| 197 | map.set(eng.engine, { staleness_h: eng.staleness_h, status: eng.status }); |
| 198 | } |
| 199 | } |
| 200 | return map; |
| 201 | }, [healthData]); |
| 202 | |
| 203 | const groupRef = useRef<THREE.Group>(null); |
| 204 | useFrame((_, delta) => { |
| 205 | if (groupRef.current) { |
| 206 | groupRef.current.rotation.y += delta * 0.04; |
| 207 | groupRef.current.rotation.x += delta * 0.01; |
| 208 | } |
| 209 | const collapseTarget = focusMode ? 1 : 0; |
| 210 | collapse.current += (collapseTarget - collapse.current) * Math.min(1, delta * 4.5); |
| 211 | if (centerGlowRef.current) { |
| 212 | centerGlowRef.current.intensity = (1.5 + collapse.current * 2.5) * spawnProgress; |
| 213 | } |
| 214 | }); |
| 215 | |
| 216 | return ( |
| 217 | <> |
| 218 | {} |
| 219 | <ambientLight intensity={0.25} /> |
| 220 | <pointLight position={[0, 0, 4]} intensity={0.6} color="#ffffff" /> |
| 221 | <pointLight position={[-4, 3, 2]} intensity={0.3} color="#16e0c8" distance={12} /> |
| 222 | <pointLight ref={centerGlowRef} position={[0, 0, 0]} intensity={2.0} color="#16e0c8" distance={10} /> |
| 223 | |
| 224 | <group ref={groupRef}> |
| 225 | {} |
| 226 | {CONNECTIONS.map(([a, b], i) => { |
| 227 | const aPos = SOURCE_CONFIG[a].basePos; |
| 228 | const bPos = SOURCE_CONFIG[b].basePos; |
| 229 | const aHealth = healthMap.get(a); |
| 230 | const bHealth = healthMap.get(b); |
| 231 | const bothHealthy = |
| 232 | (!aHealth || aHealth.staleness_h < 12) && |
| 233 | (!bHealth || bHealth.staleness_h < 12); |
| 234 | return ( |
| 235 | <Line |
| 236 | key={i} |
| 237 | points={[aPos, bPos]} |
| 238 | color={bothHealthy ? "#16e0c8" : "#222"} |
| 239 | lineWidth={bothHealthy ? 0.8 : 0.4} |
| 240 | transparent |
| 241 | opacity={bothHealthy ? 0.35 * spawnProgress : 0.1 * spawnProgress} |
| 242 | /> |
| 243 | ); |
| 244 | })} |
| 245 | |
| 246 | {} |
| 247 | {SOURCES.map((id) => { |
| 248 | const cfg = SOURCE_CONFIG[id]; |
| 249 | const h = healthMap.get(id) ?? { staleness_h: 0, status: "up" as const }; |
| 250 | return ( |
| 251 | <OrgNode |
| 252 | key={id} |
| 253 | id={id} |
| 254 | targetPos={cfg.basePos} |
| 255 | color={healthColor(h.staleness_h, h.status)} |
| 256 | scale={healthScale(h.staleness_h, h.status)} |
| 257 | label={cfg.label} |
| 258 | cursorWorld={cursorWorld} |
| 259 | spawnProgress={spawnProgress} |
| 260 | isStale={h.staleness_h > 12 || h.status === "down"} |
| 261 | collapse={collapse} |
| 262 | /> |
| 263 | ); |
| 264 | })} |
| 265 | </group> |
| 266 | </> |
| 267 | ); |
| 268 | } |
| 269 | |
| 270 | |
| 271 | interface DarwinOrganismProps { |
| 272 | healthData: HealthResponse | null; |
| 273 | focusMode?: boolean; |
| 274 | } |
| 275 | |
| 276 | function RenderGate() { |
| 277 | const invalidate = useThree((s) => s.invalidate); |
| 278 | const gl = useThree((s) => s.gl); |
| 279 | const visibleRef = useRef(true); |
| 280 | const accRef = useRef(0); |
| 281 | |
| 282 | useEffect(() => { |
| 283 | const el = gl.domElement; |
| 284 | const io = new IntersectionObserver( |
| 285 | (entries) => { |
| 286 | const entry = entries[0]; |
| 287 | visibleRef.current = entry.isIntersecting; |
| 288 | if (entry.isIntersecting) invalidate(); |
| 289 | }, |
| 290 | { threshold: 0 } |
| 291 | ); |
| 292 | io.observe(el); |
| 293 | return () => io.disconnect(); |
| 294 | }, [gl, invalidate]); |
| 295 | |
| 296 | useFrame((_, delta) => { |
| 297 | if (!visibleRef.current) return; |
| 298 | accRef.current += delta; |
| 299 | if (accRef.current >= 1 / 30) { |
| 300 | accRef.current = 0; |
| 301 | invalidate(); |
| 302 | } |
| 303 | }); |
| 304 | |
| 305 | return null; |
| 306 | } |
| 307 | |
| 308 | export default function DarwinOrganism({ healthData, focusMode = false }: DarwinOrganismProps) { |
| 309 | return ( |
| 310 | <Canvas |
| 311 | frameloop="demand" |
| 312 | dpr={[1, 2]} |
| 313 | gl={{ antialias: true, alpha: true }} |
| 314 | style={{ background: "transparent" }} |
| 315 | camera={{ fov: 60, near: 0.1, far: 100 }} |
| 316 | > |
| 317 | <RenderGate /> |
| 318 | <OrganismScene healthData={healthData} focusMode={focusMode} /> |
| 319 | </Canvas> |
| 320 | ); |
| 321 | } |