/** * Neural Network Visualizer * * Main container component with VGG-style 3D architecture visualization. * Shows layers as 3D blocks where size represents tensor dimensions. */ import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react'; import { ArchScene, type CameraView } from './ArchScene'; import { SavedModelsPanel } from './SavedModelsPanel'; import { saveModel, findModelByName, type ModelArchitecture } from '@/core/api-client'; import styles from './NeuralVisualizer.module.css'; // ============================================================================ // Types // ============================================================================ export interface ModelArchitectureData { name: string; framework: string; totalParameters: number; trainableParameters?: number; inputShape?: number[] | null; outputShape?: number[] | null; layers: Array<{ id: string; name: string; type: string; category: string; inputShape: number[] | null; outputShape: number[] | null; params: Record; numParameters: number; trainable: boolean; }>; connections: Array<{ source: string; target: string; tensorShape: number[] | null; }>; } export interface NeuralVisualizerProps { /** Model architecture data from backend */ architecture: ModelArchitectureData | null; /** Loading state */ isLoading?: boolean; /** Error message */ error?: string | null; /** Callback when a layer is selected */ onLayerSelect?: (layerId: string | null) => void; /** Callback to upload a new model */ onUploadNew?: () => void; /** Callback to load a saved model */ onLoadSavedModel?: (architecture: any) => void; } // ============================================================================ // Main Component // ============================================================================ export const NeuralVisualizer: React.FC = ({ architecture, isLoading = false, error = null, onLayerSelect, onUploadNew, onLoadSavedModel, }) => { // Ref for scene container (for screenshot) const sceneContainerRef = useRef(null); // View state const [showLabels, setShowLabels] = useState(true); const [showDimensions, setShowDimensions] = useState(true); const [showConnections, setShowConnections] = useState(false); // Selection state const [selectedLayerId, setSelectedLayerId] = useState(null); // Saved models panel state const [showSavedModels, setShowSavedModels] = useState(false); const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle'); // Key for forcing camera reset const [sceneKey, setSceneKey] = useState(0); // Camera view preset const [cameraView, setCameraView] = useState(undefined); // Get selected layer data const selectedLayer = useMemo(() => { if (!selectedLayerId || !architecture) return null; return architecture.layers.find(l => l.id === selectedLayerId) || null; }, [selectedLayerId, architecture]); // Handlers const handleLayerClick = useCallback((layerId: string) => { setSelectedLayerId(prev => prev === layerId ? null : layerId); onLayerSelect?.(layerId); }, [onLayerSelect]); const handleLayerHover = useCallback((_layerId: string | null) => { // Could show preview on hover }, []); const handleCloseDetail = useCallback(() => { setSelectedLayerId(null); onLayerSelect?.(null); }, [onLayerSelect]); const handleResetView = useCallback(() => { setSceneKey(prev => prev + 1); setCameraView(undefined); }, []); const handleSetView = useCallback((view: CameraView) => { setCameraView(view); // Force re-render to apply new view setSceneKey(prev => prev + 1); }, []); // Save image handler const handleSaveImage = useCallback(() => { const canvas = sceneContainerRef.current?.querySelector('canvas'); if (canvas) { const link = document.createElement('a'); link.download = `${architecture?.name || 'neural-network'}-visualization.png`; link.href = canvas.toDataURL('image/png'); link.click(); } }, [architecture?.name]); // Toggle fullscreen const handleFullscreen = useCallback(() => { const container = sceneContainerRef.current?.parentElement; if (!container) return; if (!document.fullscreenElement) { container.requestFullscreen?.(); } else { document.exitFullscreen?.(); } }, []); // Auto-save model after loading (only if not already saved) useEffect(() => { if (architecture && saveStatus === 'idle') { setSaveStatus('saving'); // First check if model with this name already exists findModelByName(architecture.name) .then((existing) => { if (existing) { // Model already exists, mark as saved without creating new entry console.log(`[NN3D] Model "${architecture.name}" already saved (id: ${existing.id})`); setSaveStatus('saved'); return; } // Model doesn't exist, save it const archForSave: ModelArchitecture = { name: architecture.name, framework: architecture.framework, totalParameters: architecture.totalParameters, trainableParameters: architecture.trainableParameters || 0, inputShape: architecture.inputShape || null, outputShape: architecture.outputShape || null, layers: architecture.layers, connections: architecture.connections, }; return saveModel( architecture.name, architecture.framework, architecture.totalParameters, architecture.layers.length, archForSave ).then(() => { console.log(`[NN3D] Model "${architecture.name}" saved successfully`); setSaveStatus('saved'); }); }) .catch((err) => { console.error('Failed to save model:', err); setSaveStatus('error'); }); } }, [architecture, saveStatus]); // Reset save status when architecture changes useEffect(() => { setSaveStatus('idle'); }, [architecture?.name]); // Handle loading saved model const handleLoadSavedModel = useCallback((arch: any) => { if (onLoadSavedModel) { onLoadSavedModel(arch); } setShowSavedModels(false); }, [onLoadSavedModel]); // Keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { return; } switch (e.key.toLowerCase()) { case 'l': setShowLabels(prev => !prev); break; case 'd': setShowDimensions(prev => !prev); break; case 'c': setShowConnections(prev => !prev); break; case 'r': handleResetView(); break; case 'escape': handleCloseDetail(); break; // View presets case '1': handleSetView('front'); break; case '2': handleSetView('side'); break; case '3': handleSetView('top'); break; case '4': handleSetView('isometric'); break; case '5': handleSetView('back'); break; case '6': handleSetView('bottom'); break; } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [handleCloseDetail, handleResetView, handleSetView]); // Loading state if (isLoading) { return (
// ANALYZING MODEL...
); } // Error state if (error) { return (
[ERROR]

Analysis Failed

{error}

); } // Empty state if (!architecture) { return (

Neural Network Visualizer

Drop a model file to visualize its architecture

// .pt .pth .onnx .h5 .keras .safetensors
{/* Saved Models Panel */} {showSavedModels && ( setShowSavedModels(false)} /> )}
); } return (
{/* Top Toolbar */}
{architecture.name} {saveStatus === 'saved' && ( [SAVED] )}
{onUploadNew && ( )}
{/* 3D Architecture Scene */}
{/* Camera Control Panel */}
// CAMERA_VIEWS
{/* Controls Panel */}
// DISPLAY_OPTIONS
// NAVIGATION
[?]
L_DRAG - MOVE
CTRL+L_DRAG - ROTATE
R_DRAG - ROTATE
SCROLL - ZOOM
// SHORTCUTS
1-4 VIEWS
R RESET
L LABELS
D DIMS
C CONNECT
{/* Layer Detail Panel */} {selectedLayer && (

{selectedLayer.name}

{selectedLayer.category.toUpperCase()}
TYPE
{selectedLayer.type}
{selectedLayer.inputShape && (
INPUT_SHAPE
[{selectedLayer.inputShape.join(', ')}]
)} {selectedLayer.outputShape && (
OUTPUT_SHAPE
[{selectedLayer.outputShape.join(', ')}]
)} {selectedLayer.numParameters > 0 && (
PARAMETERS
{selectedLayer.numParameters.toLocaleString()}
)} {Object.keys(selectedLayer.params).length > 0 && (
CONFIG
{Object.entries(selectedLayer.params).map(([key, value]) => (
{key} {typeof value === 'object' ? JSON.stringify(value) : String(value)}
))}
)}
)} {/* Stats Bar */}
Model {architecture.name}
Framework {architecture.framework}
Layers {architecture.layers.length}
Parameters {formatParams(architecture.totalParameters)}
{/* Saved Models Panel */} {showSavedModels && ( setShowSavedModels(false)} /> )}
); }; // Helper function formatParams(params: number): string { if (params >= 1e9) return `${(params / 1e9).toFixed(1)}B`; if (params >= 1e6) return `${(params / 1e6).toFixed(1)}M`; if (params >= 1e3) return `${(params / 1e3).toFixed(1)}K`; return params.toString(); } export default NeuralVisualizer;