"use client"; import React, { useState, useMemo, useRef, useEffect } from 'react'; import { Canvas } from '@react-three/fiber'; import { OrbitControls } from '@react-three/drei'; import { InputCube } from '@/components/InputCube'; import { LayerCube } from '@/components/LayerCube'; import { OutputNode } from '@/components/OutputNode'; // Clean SVG Icons const ChevronLeft = () => ; const ChevronRight = () => ; const CloseIcon = () => ; export default function NetworkVisualizer() { const [activeTab, setActiveTab] = useState({ type: 'prediction', layerIndex: null }); const [layerData, setLayerData] = useState([]); const [prediction, setPrediction] = useState(null); const [previewImage, setPreviewImage] = useState(null); const [isVideo, setIsVideo] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [blockGap, setBlockGap] = useState(0.2); const [isPanelOpen, setIsPanelOpen] = useState(true); const [zoomedFeature, setZoomedFeature] = useState(null); const videoRef = useRef(null); const canvasRef = useRef(null); const isAwaitingResponse = useRef(false); const animationFrameId = useRef(null); const lastFrameTime = useRef(0); useEffect(() => { return () => cancelAnimationFrame(animationFrameId.current); }, []); const processVideoFrame = (timestamp) => { if (!videoRef.current || videoRef.current.paused || videoRef.current.ended) { animationFrameId.current = requestAnimationFrame(processVideoFrame); return; } if (timestamp - lastFrameTime.current >= 150) { if (!isAwaitingResponse.current) { isAwaitingResponse.current = true; const canvas = canvasRef.current; const ctx = canvas.getContext('2d', { willReadFrequently: true }); ctx.drawImage(videoRef.current, 0, 0, 227, 227); canvas.toBlob(async (blob) => { if (!blob) { isAwaitingResponse.current = false; return; } const formData = new FormData(); formData.append("file", blob, "frame.jpg"); try { const response = await fetch("/predict", { method: "POST", body: formData, }); const data = await response.json(); if (data && data.layers) { setLayerData(data.layers); setPrediction(data.prediction); } } catch (error) { console.error("Frame dropped:", error); } finally { isAwaitingResponse.current = false; } }, 'image/jpeg', 0.6); lastFrameTime.current = timestamp; } } animationFrameId.current = requestAnimationFrame(processVideoFrame); }; const layout = useMemo(() => { const defaultData = [ { layer_index: 1, shape: [55, 55, 96], texture_b64: null }, { layer_index: 2, shape: [27, 27, 256], texture_b64: null }, { layer_index: 3, shape: [13, 13, 384], texture_b64: null }, { layer_index: 4, shape: [13, 13, 384], texture_b64: null }, { layer_index: 5, shape: [13, 13, 256], texture_b64: null } ]; const activeData = layerData.length > 0 ? layerData : defaultData; let currentX = -6; const inputPos = previewImage ? [currentX, 0, 0] : null; if (previewImage) currentX += 0.1 + (blockGap * 0.5); const mappedLayers = activeData.map((layer) => { const s0 = layer.shape && layer.shape[0] ? layer.shape[0] : 10; const s1 = layer.shape && layer.shape[1] ? layer.shape[1] : 10; const s2 = layer.shape && layer.shape[2] ? layer.shape[2] : 10; const sizeY = Math.max(0.8, s0 * 0.07); const sizeZ = Math.max(0.8, s1 * 0.07); const sizeX = Math.max(0.8, s2 * 0.01); const xPos = currentX + sizeX / 2; currentX = xPos + sizeX / 2 + blockGap; return { ...layer, size: [sizeX, sizeY, sizeZ], xPos }; }); return { inputPos, mappedLayers, outputPos: [currentX, 0, 0] }; }, [layerData, previewImage, blockGap]); const activeLayerPanelData = activeTab.type === 'layer' ? layerData.find(l => l.layer_index === activeTab.layerIndex) || layout.mappedLayers.find(l => l.layer_index === activeTab.layerIndex) : null; const tabs = ['Output', ...layout.mappedLayers.map(l => l.layer_index)]; const currentTabIndex = activeTab.type === 'prediction' ? 0 : tabs.indexOf(activeTab.layerIndex); const handlePrevTab = () => { const newIdx = currentTabIndex === 0 ? tabs.length - 1 : currentTabIndex - 1; const val = tabs[newIdx]; if (val === 'Output') setActiveTab({ type: 'prediction', layerIndex: null }); else setActiveTab({ type: 'layer', layerIndex: val }); }; const handleNextTab = () => { const newIdx = currentTabIndex === tabs.length - 1 ? 0 : currentTabIndex + 1; const val = tabs[newIdx]; if (val === 'Output') setActiveTab({ type: 'prediction', layerIndex: null }); else setActiveTab({ type: 'layer', layerIndex: val }); }; const handleFileUpload = async (event) => { const file = event.target.files[0]; if (!file) return; isAwaitingResponse.current = false; if (videoRef.current) { videoRef.current.pause(); videoRef.current.removeAttribute('src'); videoRef.current.load(); } const fileUrl = URL.createObjectURL(file); const isVid = file.type.startsWith('video/'); setIsVideo(isVid); setPreviewImage(fileUrl); setActiveTab({ type: 'prediction', layerIndex: null }); setZoomedFeature(null); setIsPanelOpen(true); if (isVid) return; setIsProcessing(true); const formData = new FormData(); formData.append("file", file); try { const response = await fetch("/predict", { method: "POST", body: formData, }); const data = await response.json(); setLayerData(data.layers); setPrediction(data.prediction); } catch (error) { console.error("Error:", error); alert("Make sure your FastAPI server is running!"); } finally { setIsProcessing(false); } }; return (