| | import React, { useState, useEffect, useRef } from 'react'; |
| | import { |
| | Activity, |
| | Settings, |
| | Play, |
| | Square, |
| | Trash2, |
| | Layers, |
| | Cpu, |
| | Zap, |
| | Plus, |
| | Info, |
| | Database, |
| | Move, |
| | Maximize, |
| | Search, |
| | ZoomIn, |
| | ZoomOut, |
| | Clock, |
| | AlertTriangle |
| | } from 'lucide-react'; |
| | import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; |
| |
|
| | |
| | const BLOCK_TYPES = { |
| | INPUT: { |
| | id: 'INPUT', |
| | label: 'Data In', |
| | category: 'IO', |
| | color: 'bg-emerald-500', |
| | outputs: 1, |
| | inputs: 0, |
| | config: { batchSize: 32, seqLen: 128, vocabSize: 5000 } |
| | }, |
| | EMBED: { |
| | id: 'EMBED', |
| | label: 'Embedding', |
| | category: 'Core', |
| | color: 'bg-blue-500', |
| | outputs: 1, |
| | inputs: 1, |
| | config: { dim: 512 } |
| | }, |
| | ATTN: { |
| | id: 'ATTN', |
| | label: 'Attention', |
| | category: 'Core', |
| | color: 'bg-indigo-600', |
| | outputs: 1, |
| | inputs: 1, |
| | config: { heads: 8, headDim: 64, dropout: 0.1 } |
| | }, |
| | FFN: { |
| | id: 'FFN', |
| | label: 'Feed Forward', |
| | category: 'Core', |
| | color: 'bg-blue-600', |
| | outputs: 1, |
| | inputs: 1, |
| | config: { mult: 4, dropout: 0.1 } |
| | }, |
| | NORM: { |
| | id: 'NORM', |
| | label: 'Layer Norm', |
| | category: 'Core', |
| | color: 'bg-slate-500', |
| | outputs: 1, |
| | inputs: 1, |
| | config: { eps: 1e-5 } |
| | }, |
| | LSTM: { |
| | id: 'LSTM', |
| | label: 'LSTM', |
| | category: 'RNN', |
| | color: 'bg-amber-500', |
| | outputs: 1, |
| | inputs: 1, |
| | config: { hidden: 512, layers: 1 } |
| | }, |
| | MAMBA: { |
| | id: 'MAMBA', |
| | label: 'Mamba (SSM)', |
| | category: 'Modern', |
| | color: 'bg-purple-600', |
| | outputs: 1, |
| | inputs: 1, |
| | config: { d_state: 16, d_conv: 4, expand: 2 } |
| | }, |
| | RWKV: { |
| | id: 'RWKV', |
| | label: 'RWKV', |
| | category: 'Modern', |
| | color: 'bg-fuchsia-600', |
| | outputs: 1, |
| | inputs: 1, |
| | config: { mode: 'v5', head_size: 64 } |
| | }, |
| | RESIDUAL: { |
| | id: 'RESIDUAL', |
| | label: 'Residual +', |
| | category: 'Utility', |
| | color: 'bg-cyan-500', |
| | outputs: 1, |
| | inputs: 2, |
| | config: {} |
| | }, |
| | OUTPUT: { |
| | id: 'OUTPUT', |
| | label: 'Data Out', |
| | category: 'IO', |
| | color: 'bg-red-500', |
| | outputs: 0, |
| | inputs: 1, |
| | config: { loss: 'CrossEntropy' } |
| | } |
| | }; |
| |
|
| | const App = () => { |
| | |
| | const [nodes, setNodes] = useState([ |
| | { id: 'n1', type: 'INPUT', x: 50, y: 150, config: BLOCK_TYPES.INPUT.config }, |
| | { id: 'n2', type: 'EMBED', x: 250, y: 150, config: BLOCK_TYPES.EMBED.config }, |
| | { id: 'n3', type: 'OUTPUT', x: 450, y: 150, config: BLOCK_TYPES.OUTPUT.config }, |
| | ]); |
| | const [connections, setConnections] = useState([ |
| | { id: 'c1', from: 'n1', to: 'n2' }, |
| | { id: 'c2', from: 'n2', to: 'n3' } |
| | ]); |
| | |
| | |
| | const [viewOffset, setViewOffset] = useState({ x: 0, y: 0 }); |
| | const [zoom, setZoom] = useState(1); |
| | const [isPanning, setIsPanning] = useState(false); |
| | const [panStart, setPanStart] = useState({ x: 0, y: 0 }); |
| | |
| | const [selectedNodeId, setSelectedNodeId] = useState(null); |
| | const [isTraining, setIsTraining] = useState(false); |
| | const [trainingData, setTrainingData] = useState([]); |
| | const [learningRate, setLearningRate] = useState(0.001); |
| | const [maxEpochs, setMaxEpochs] = useState(150); |
| | const [simAlert, setSimAlert] = useState(null); |
| | |
| | const [isDraggingNode, setIsDraggingNode] = useState(null); |
| | const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); |
| | const [connectingFrom, setConnectingFrom] = useState(null); |
| | |
| | const canvasRef = useRef(null); |
| |
|
| | |
| |
|
| | const findPath = (startType, endType) => { |
| | const startNodes = nodes.filter(n => n.type === startType); |
| | const endNodes = nodes.filter(n => n.type === endType); |
| | |
| | for (const start of startNodes) { |
| | let visited = new Set(); |
| | let queue = [[start.id, []]]; |
| |
|
| | while (queue.length > 0) { |
| | const [currId, path] = queue.shift(); |
| | if (visited.has(currId)) continue; |
| | visited.add(currId); |
| |
|
| | const currNode = nodes.find(n => n.id === currId); |
| | const currentPath = [...path, currNode]; |
| |
|
| | if (currNode.type === endType) return currentPath; |
| |
|
| | const nextConnections = connections.filter(c => c.from === currId); |
| | for (const conn of nextConnections) { |
| | queue.push([conn.to, currentPath]); |
| | } |
| | } |
| | } |
| | return null; |
| | }; |
| |
|
| | const runTrainingStep = (epoch) => { |
| | |
| | const activePath = findPath('INPUT', 'OUTPUT'); |
| |
|
| | if (!activePath) { |
| | setSimAlert("Broken Computation Path: Input cannot reach Output."); |
| | return null; |
| | } |
| |
|
| | setSimAlert(null); |
| |
|
| | |
| | let depth = activePath.length; |
| | let totalCapacity = 0; |
| | let vanishingGradientRisk = 1.0; |
| | let bottleneck = Infinity; |
| |
|
| | activePath.forEach(node => { |
| | const type = node.type; |
| | const cfg = node.config; |
| |
|
| | |
| | if (cfg.dim) totalCapacity += cfg.dim; |
| | if (cfg.hidden) totalCapacity += cfg.hidden; |
| | if (cfg.heads) totalCapacity += (cfg.heads * cfg.headDim); |
| |
|
| | |
| | if (cfg.dim) bottleneck = Math.min(bottleneck, cfg.dim); |
| |
|
| | |
| | if (type === 'LSTM') vanishingGradientRisk *= 0.95; |
| | if (type === 'ATTN' || type === 'MAMBA') vanishingGradientRisk *= 0.99; |
| | if (type === 'NORM') vanishingGradientRisk = Math.min(1.0, vanishingGradientRisk * 1.5); |
| | if (type === 'RESIDUAL') vanishingGradientRisk = Math.min(1.0, vanishingGradientRisk * 1.8); |
| | }); |
| |
|
| | |
| | |
| | if (learningRate > 0.04 && Math.random() > 0.95) { |
| | setSimAlert("Gradient Explosion: Learning rate too high!"); |
| | return { epoch, loss: (Math.random() * 10 + 5).toFixed(4), accuracy: "0.0000" }; |
| | } |
| |
|
| | |
| | |
| | const effectiveLearningPower = learningRate * Math.pow(vanishingGradientRisk, depth / 2); |
| |
|
| | |
| | |
| | const floorLoss = bottleneck < 128 ? 1.5 : 0.05; |
| |
|
| | |
| | const prevLoss = trainingData.length > 0 ? parseFloat(trainingData[trainingData.length-1].loss) : 8.0; |
| | |
| | |
| | const delta = (prevLoss - floorLoss) * effectiveLearningPower * 5; |
| | const noise = (Math.random() - 0.5) * (0.02 / (epoch + 1)); |
| | |
| | const newLoss = Math.max(floorLoss, prevLoss - delta + noise); |
| | const newAcc = Math.min(0.99, 1 - (newLoss / 8) + (Math.random() * 0.01)); |
| |
|
| | return { |
| | epoch, |
| | loss: newLoss.toFixed(4), |
| | accuracy: newAcc.toFixed(4) |
| | }; |
| | }; |
| |
|
| | useEffect(() => { |
| | let interval; |
| | if (isTraining) { |
| | interval = setInterval(() => { |
| | setTrainingData(prev => { |
| | const nextEpoch = prev.length; |
| | if (nextEpoch >= maxEpochs) { |
| | setIsTraining(false); |
| | return prev; |
| | } |
| | const step = runTrainingStep(nextEpoch); |
| | if (!step) { |
| | setIsTraining(false); |
| | return prev; |
| | } |
| | return [...prev, step]; |
| | }); |
| | }, 100); |
| | } |
| | return () => clearInterval(interval); |
| | }, [isTraining, nodes, connections, learningRate, maxEpochs, trainingData]); |
| |
|
| | |
| | const handleWheel = (e) => { |
| | e.preventDefault(); |
| | const zoomSpeed = 0.001; |
| | const newZoom = Math.min(Math.max(zoom - e.deltaY * zoomSpeed, 0.2), 2); |
| | setZoom(newZoom); |
| | }; |
| |
|
| | const addNode = (type) => { |
| | const newNode = { |
| | id: `n${Date.now()}`, |
| | type, |
| | x: (-viewOffset.x + 100) / zoom, |
| | y: (-viewOffset.y + 100) / zoom, |
| | config: { ...BLOCK_TYPES[type].config } |
| | }; |
| | setNodes([...nodes, newNode]); |
| | }; |
| |
|
| | const deleteNode = (id) => { |
| | setNodes(nodes.filter(n => n.id !== id)); |
| | setConnections(connections.filter(c => c.from !== id && c.to !== id)); |
| | if (selectedNodeId === id) setSelectedNodeId(null); |
| | }; |
| |
|
| | const deleteConnection = (id) => { |
| | setConnections(connections.filter(c => c.id !== id)); |
| | }; |
| |
|
| | const handleCanvasMouseDown = (e) => { |
| | if (e.target === canvasRef.current || e.target.tagName === 'svg' || e.target.id === 'grid-background') { |
| | setIsPanning(true); |
| | setPanStart({ x: e.clientX - viewOffset.x, y: e.clientY - viewOffset.y }); |
| | setSelectedNodeId(null); |
| | } |
| | }; |
| |
|
| | const handleNodeMouseDown = (e, id) => { |
| | e.stopPropagation(); |
| | if (connectingFrom) return; |
| | setIsDraggingNode(id); |
| | const node = nodes.find(n => n.id === id); |
| | setDragOffset({ x: e.clientX / zoom - node.x, y: e.clientY / zoom - node.y }); |
| | setSelectedNodeId(id); |
| | }; |
| |
|
| | const handleMouseMove = (e) => { |
| | if (isDraggingNode) { |
| | setNodes(nodes.map(n => n.id === isDraggingNode ? { ...n, x: e.clientX / zoom - dragOffset.x, y: e.clientY / zoom - dragOffset.y } : n)); |
| | } else if (isPanning) { |
| | setViewOffset({ |
| | x: e.clientX - panStart.x, |
| | y: e.clientY - panStart.y |
| | }); |
| | } |
| | }; |
| |
|
| | const handleMouseUp = () => { |
| | setIsDraggingNode(null); |
| | setIsPanning(false); |
| | }; |
| |
|
| | const startConnection = (e, id) => { |
| | e.stopPropagation(); |
| | setConnectingFrom(id); |
| | }; |
| |
|
| | const endConnection = (e, id) => { |
| | e.stopPropagation(); |
| | if (connectingFrom && connectingFrom !== id) { |
| | if (!connections.some(c => c.from === connectingFrom && c.to === id)) { |
| | setConnections([...connections, { id: `c${Date.now()}`, from: connectingFrom, to: id }]); |
| | } |
| | } |
| | setConnectingFrom(null); |
| | }; |
| |
|
| | const updateConfig = (key, val) => { |
| | setNodes(nodes.map(n => n.id === selectedNodeId ? { ...n, config: { ...n.config, [key]: val } } : n)); |
| | }; |
| |
|
| | const resetView = () => { |
| | setViewOffset({ x: 0, y: 0 }); |
| | setZoom(1); |
| | }; |
| |
|
| | const selectedNode = nodes.find(n => n.id === selectedNodeId); |
| |
|
| | |
| | const gridSize = 24 * zoom; |
| | const gridOffsetX = viewOffset.x % gridSize; |
| | const gridOffsetY = viewOffset.y % gridSize; |
| | |
| | const currentEpoch = trainingData.length; |
| | const trainingProgress = (currentEpoch / maxEpochs) * 100; |
| |
|
| | return ( |
| | <div className="flex flex-col h-screen w-screen bg-slate-950 text-slate-100 overflow-hidden font-sans"> |
| | {/* Header */} |
| | <header className="h-16 border-b border-slate-800 flex items-center justify-between px-6 bg-slate-900/50 backdrop-blur-md z-20"> |
| | <div className="flex items-center gap-3"> |
| | <div className="p-2 bg-indigo-500 rounded-lg shadow-[0_0_15px_rgba(99,102,241,0.4)]"> |
| | <Layers size={20} className="text-white" /> |
| | </div> |
| | <h1 className="text-lg font-bold tracking-tight">Arch-Sim <span className="text-slate-500 font-normal">v2.0 Accurate</span></h1> |
| | </div> |
| | |
| | <div className="flex items-center gap-4"> |
| | <div className="flex items-center bg-slate-800 rounded-full px-4 py-1.5 gap-3 border border-slate-700"> |
| | <span className="text-[10px] text-slate-400 uppercase font-bold tracking-wider">LR</span> |
| | <input |
| | type="range" |
| | min="0.0001" |
| | max="0.05" |
| | step="0.0001" |
| | value={learningRate} |
| | onChange={(e) => setLearningRate(parseFloat(e.target.value))} |
| | className="w-20 accent-indigo-500" |
| | /> |
| | <span className="text-[11px] font-mono text-indigo-300 w-10">{learningRate}</span> |
| | </div> |
| | |
| | <div className="flex items-center bg-slate-800 rounded-full px-4 py-1.5 gap-3 border border-slate-700"> |
| | <Clock size={12} className="text-slate-400" /> |
| | <span className="text-[10px] text-slate-400 uppercase font-bold tracking-wider whitespace-nowrap">Max Epochs</span> |
| | <input |
| | type="range" |
| | min="50" |
| | max="1000" |
| | step="50" |
| | value={maxEpochs} |
| | onChange={(e) => setMaxEpochs(parseInt(e.target.value))} |
| | className="w-20 accent-indigo-500" |
| | /> |
| | <span className="text-[11px] font-mono text-indigo-300 w-8">{maxEpochs}</span> |
| | </div> |
| | |
| | <button |
| | onClick={() => { |
| | if (isTraining) setIsTraining(false); |
| | else { |
| | setTrainingData([]); |
| | setIsTraining(true); |
| | } |
| | }} |
| | className={`flex items-center gap-2 px-5 py-2 rounded-lg font-medium transition-all shadow-lg ${ |
| | isTraining ? 'bg-red-500 hover:bg-red-600 shadow-red-500/20' : 'bg-emerald-500 hover:bg-emerald-600 shadow-emerald-500/20' |
| | }`} |
| | > |
| | {isTraining ? <><Square size={14} fill="currentColor" /> Stop</> : <><Play size={14} fill="currentColor" /> Train</>} |
| | </button> |
| | </div> |
| | </header> |
| |
|
| | <main className="flex-1 flex overflow-hidden"> |
| | {/* Sidebar: Blocks Palette */} |
| | <aside className="w-64 border-r border-slate-800 bg-slate-900/30 p-4 flex flex-col gap-6 overflow-y-auto z-10"> |
| | <div> |
| | <h3 className="text-xs font-bold text-slate-500 uppercase tracking-widest mb-4">Architecture Blocks</h3> |
| | <div className="grid grid-cols-1 gap-2"> |
| | {Object.entries(BLOCK_TYPES).map(([key, type]) => ( |
| | <button |
| | key={key} |
| | onClick={() => addNode(key)} |
| | className="flex items-center gap-3 p-3 rounded-xl border border-slate-800 bg-slate-900/50 hover:border-indigo-500/50 hover:bg-slate-800/50 transition-all text-left group" |
| | > |
| | <div className={`w-8 h-8 rounded-lg ${type.color} flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform`}> |
| | <Cpu size={14} /> |
| | </div> |
| | <div> |
| | <div className="text-sm font-semibold">{type.label}</div> |
| | <div className="text-[10px] text-slate-500 uppercase">{type.category}</div> |
| | </div> |
| | <Plus size={14} className="ml-auto text-slate-600 group-hover:text-indigo-400" /> |
| | </button> |
| | ))} |
| | </div> |
| | </div> |
| | </aside> |
| | |
| | {/* Canvas Area */} |
| | <div |
| | className="flex-1 relative bg-slate-950 overflow-hidden cursor-grab active:cursor-grabbing outline-none" |
| | onMouseDown={handleCanvasMouseDown} |
| | onMouseMove={handleMouseMove} |
| | onMouseUp={handleMouseUp} |
| | onWheel={handleWheel} |
| | ref={canvasRef} |
| | > |
| | {/* Infinite Dot Grid Background */} |
| | <div |
| | id="grid-background" |
| | className="absolute inset-0 pointer-events-none" |
| | style={{ |
| | backgroundImage: `radial-gradient(circle, #1e293b 1px, transparent 1px)`, |
| | backgroundSize: `${gridSize}px ${gridSize}px`, |
| | backgroundPosition: `${gridOffsetX}px ${gridOffsetY}px` |
| | }} |
| | /> |
| | |
| | {/* Alert Message */} |
| | {simAlert && ( |
| | <div className="absolute top-6 left-1/2 -translate-x-1/2 bg-red-500/90 text-white px-4 py-2 rounded-full flex items-center gap-2 text-sm font-bold shadow-2xl z-50 animate-bounce"> |
| | <AlertTriangle size={16} /> |
| | {simAlert} |
| | </div> |
| | )} |
| | |
| | {/* Viewport Transform Container */} |
| | <div |
| | className="absolute inset-0 pointer-events-none" |
| | style={{ |
| | transform: `translate(${viewOffset.x}px, ${viewOffset.y}px) scale(${zoom})`, |
| | transformOrigin: '0 0' |
| | }} |
| | > |
| | {/* Connections SVG */} |
| | <svg className="absolute inset-0 w-[10000px] h-[10000px] pointer-events-auto overflow-visible"> |
| | <defs> |
| | <marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> |
| | <path d="M 0 0 L 10 5 L 0 10 z" fill="#6366f1" /> |
| | </marker> |
| | </defs> |
| | {connections.map(conn => { |
| | const fromNode = nodes.find(n => n.id === conn.from); |
| | const toNode = nodes.find(n => n.id === conn.to); |
| | if (!fromNode || !toNode) return null; |
| | |
| | const x1 = fromNode.x + 160; |
| | const y1 = fromNode.y + 40; |
| | const x2 = toNode.x; |
| | const y2 = toNode.y + 40; |
| | const dx = (x2 - x1) / 2; |
| | |
| | return ( |
| | <g key={conn.id} className="group cursor-pointer"> |
| | <path |
| | d={`M ${x1} ${y1} C ${x1 + dx} ${y1}, ${x2 - dx} ${y2}, ${x2} ${y2}`} |
| | stroke="transparent" |
| | strokeWidth={20 / zoom} |
| | fill="none" |
| | onClick={(e) => { e.stopPropagation(); deleteConnection(conn.id); }} |
| | /> |
| | <path |
| | d={`M ${x1} ${y1} C ${x1 + dx} ${y1}, ${x2 - dx} ${y2}, ${x2} ${y2}`} |
| | stroke="#6366f1" |
| | strokeWidth={3} |
| | strokeDasharray={isTraining ? "8 4" : "0"} |
| | className={`${isTraining ? "animate-[dash_1s_linear_infinite]" : ""} group-hover:stroke-red-500 transition-colors`} |
| | fill="none" |
| | markerEnd="url(#arrow)" |
| | opacity="0.6" |
| | /> |
| | </g> |
| | ); |
| | })} |
| | </svg> |
| | |
| | {/* Nodes */} |
| | {nodes.map(node => { |
| | const type = BLOCK_TYPES[node.type]; |
| | const isSelected = selectedNodeId === node.id; |
| | return ( |
| | <div |
| | key={node.id} |
| | style={{ left: node.x, top: node.y }} |
| | onMouseDown={(e) => handleNodeMouseDown(e, node.id)} |
| | className={`absolute w-40 p-4 rounded-xl border-2 transition-all cursor-move select-none pointer-events-auto ${ |
| | isSelected ? 'border-indigo-500 bg-slate-800 shadow-[0_0_35px_rgba(99,102,241,0.3)]' : 'border-slate-800 bg-slate-900/90' |
| | }`} |
| | > |
| | <div className="flex justify-between items-start mb-2"> |
| | <div className={`w-8 h-8 rounded-lg ${type.color} flex items-center justify-center shadow-lg mb-2`}> |
| | <Cpu size={14} /> |
| | </div> |
| | {isSelected && ( |
| | <button onClick={(e) => { e.stopPropagation(); deleteNode(node.id); }} className="text-slate-500 hover:text-red-400"> |
| | <Trash2 size={14} /> |
| | </button> |
| | )} |
| | </div> |
| | |
| | <h4 className="text-xs font-bold uppercase tracking-wide truncate">{type.label}</h4> |
| | <div className="text-[10px] text-slate-500 mb-4">{node.id}</div> |
| | |
| | {type.inputs > 0 && ( |
| | <div |
| | onMouseUp={(e) => endConnection(e, node.id)} |
| | className={`absolute -left-3 top-1/2 -translate-y-1/2 w-6 h-6 rounded-full bg-slate-800 border-2 ${connectingFrom ? 'border-yellow-400 animate-pulse scale-125' : 'border-indigo-500'} flex items-center justify-center hover:bg-indigo-500 cursor-pointer z-30 transition-transform`} |
| | > |
| | <div className="w-1.5 h-1.5 bg-white rounded-full" /> |
| | </div> |
| | )} |
| | {type.outputs > 0 && ( |
| | <div |
| | onMouseDown={(e) => startConnection(e, node.id)} |
| | className="absolute -right-3 top-1/2 -translate-y-1/2 w-6 h-6 rounded-full bg-slate-800 border-2 border-indigo-500 flex items-center justify-center hover:bg-indigo-500 cursor-pointer z-30" |
| | > |
| | <div className="w-1.5 h-1.5 bg-white rounded-full" /> |
| | </div> |
| | )} |
| | </div> |
| | ); |
| | })} |
| | </div> |
| | |
| | {/* Navigation Controls Overlay */} |
| | <div className="absolute bottom-6 left-6 flex flex-col gap-2 z-20"> |
| | <div className="flex gap-2"> |
| | <button |
| | onClick={resetView} |
| | className="p-3 bg-slate-900 border border-slate-700 rounded-lg hover:bg-slate-800 transition-colors shadow-xl text-slate-400 flex items-center gap-2" |
| | > |
| | <Maximize size={18} /> |
| | <span className="text-[10px] font-bold uppercase tracking-widest">{Math.round(zoom * 100)}%</span> |
| | </button> |
| | <button |
| | onClick={() => setZoom(prev => Math.min(prev + 0.1, 2))} |
| | className="p-3 bg-slate-900 border border-slate-700 rounded-lg hover:bg-slate-800 transition-colors shadow-xl text-slate-400" |
| | > |
| | <ZoomIn size={18} /> |
| | </button> |
| | <button |
| | onClick={() => setZoom(prev => Math.max(prev - 0.1, 0.2))} |
| | className="p-3 bg-slate-900 border border-slate-700 rounded-lg hover:bg-slate-800 transition-colors shadow-xl text-slate-400" |
| | > |
| | <ZoomOut size={18} /> |
| | </button> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | {/* Right Sidebar: Config & Monitoring */} |
| | <aside className="w-80 border-l border-slate-800 bg-slate-900/50 flex flex-col z-10"> |
| | <div className="h-[55%] p-6 flex flex-col border-b border-slate-800 overflow-hidden"> |
| | <div className="flex items-center justify-between mb-4"> |
| | <div className="flex items-center gap-2 text-slate-400 uppercase text-xs font-bold tracking-widest"> |
| | <Activity size={14} /> Training Metrics |
| | </div> |
| | <div className="text-[10px] font-mono text-slate-500 uppercase"> |
| | Epoch {currentEpoch}/{maxEpochs} |
| | </div> |
| | </div> |
| | |
| | {/* Progress Bar */} |
| | <div className="w-full h-1.5 bg-slate-800 rounded-full mb-4 overflow-hidden"> |
| | <div |
| | className="h-full bg-indigo-500 transition-all duration-300 shadow-[0_0_10px_rgba(99,102,241,0.5)]" |
| | style={{ width: `${trainingProgress}%` }} |
| | /> |
| | </div> |
| | |
| | <div className="flex-1 bg-slate-900 rounded-xl p-2 border border-slate-800 overflow-hidden relative"> |
| | {trainingData.length > 0 ? ( |
| | <ResponsiveContainer width="100%" height="100%"> |
| | <LineChart data={trainingData}> |
| | <CartesianGrid strokeDasharray="3 3" stroke="#1e293b" /> |
| | <XAxis dataKey="epoch" hide /> |
| | <YAxis hide domain={[0, 'auto']} /> |
| | <Tooltip |
| | contentStyle={{ backgroundColor: '#0f172a', border: '1px solid #1e293b', borderRadius: '8px' }} |
| | itemStyle={{ color: '#6366f1' }} |
| | /> |
| | <Line type="monotone" dataKey="loss" stroke="#6366f1" strokeWidth={2} dot={false} isAnimationActive={false} /> |
| | </LineChart> |
| | </ResponsiveContainer> |
| | ) : ( |
| | <div className="h-full flex flex-col items-center justify-center text-slate-600 italic text-xs text-center p-4"> |
| | Simulation idle. Ensure INPUT is linked to OUTPUT and click Train. |
| | </div> |
| | )} |
| | </div> |
| | |
| | <div className="mt-4 grid grid-cols-2 gap-4"> |
| | <div className="p-3 bg-slate-900/50 rounded-lg border border-slate-800 text-center"> |
| | <div className="text-[10px] uppercase text-slate-500 font-bold mb-1">Loss</div> |
| | <div className="text-lg font-mono text-indigo-400"> |
| | {trainingData.length > 0 ? trainingData[trainingData.length-1].loss : '0.0000'} |
| | </div> |
| | </div> |
| | <div className="p-3 bg-slate-900/50 rounded-lg border border-slate-800 text-center"> |
| | <div className="text-[10px] uppercase text-slate-500 font-bold mb-1">Accuracy</div> |
| | <div className="text-lg font-mono text-emerald-400"> |
| | {trainingData.length > 0 ? `${(trainingData[trainingData.length-1].accuracy * 100).toFixed(1)}%` : '0.0%'} |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | <div className="flex-1 p-6 overflow-y-auto"> |
| | <div className="flex items-center gap-2 mb-6 text-slate-400 uppercase text-xs font-bold tracking-widest"> |
| | <Settings size={14} /> {selectedNode ? 'Block Parameters' : 'Sim Intelligence'} |
| | </div> |
| | |
| | {selectedNode ? ( |
| | <div className="space-y-6"> |
| | <div className="flex items-center gap-3 p-3 bg-indigo-500/10 rounded-xl border border-indigo-500/20"> |
| | <div className={`w-8 h-8 rounded-lg ${BLOCK_TYPES[selectedNode.type].color} flex items-center justify-center`}> |
| | <Cpu size={14} /> |
| | </div> |
| | <div> |
| | <div className="font-bold text-sm">{BLOCK_TYPES[selectedNode.type].label}</div> |
| | <div className="text-[10px] text-indigo-300 font-mono uppercase">{selectedNode.id}</div> |
| | </div> |
| | </div> |
| | |
| | <div className="space-y-4"> |
| | {Object.entries(selectedNode.config).map(([key, val]) => ( |
| | <div key={key} className="space-y-1.5"> |
| | <label className="text-[11px] font-bold text-slate-500 uppercase flex justify-between"> |
| | {key} <span>{val}</span> |
| | </label> |
| | <input |
| | type={typeof val === 'number' ? 'range' : 'text'} |
| | min={1} |
| | max={1024} |
| | value={val} |
| | onChange={(e) => updateConfig(key, typeof val === 'number' ? parseInt(e.target.value) : e.target.value)} |
| | className="w-full h-1.5 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-indigo-500" |
| | /> |
| | </div> |
| | ))} |
| | </div> |
| | </div> |
| | ) : ( |
| | <div className="space-y-4"> |
| | <div className="p-4 bg-slate-800/30 rounded-xl border border-slate-700/50 text-xs text-slate-400 space-y-3 leading-relaxed"> |
| | <p><strong className="text-slate-200 block mb-1">Graph Dependency:</strong> Training logic now uses a BFS trace. If a node isn't part of the direct path from Data In to Data Out, its parameters won't contribute to capacity.</p> |
| | <p><strong className="text-slate-200 block mb-1">Architecture Physics:</strong> Adding layers increases depth, which increases the risk of Vanishing Gradients unless you add <span className="text-cyan-400">Residual</span> or <span className="text-slate-300">Norm</span> blocks.</p> |
| | <p><strong className="text-slate-200 block mb-1">Convergence:</strong> The loss floor is determined by the "bottleneck" (the smallest hidden dimension in your path).</p> |
| | </div> |
| | </div> |
| | )} |
| | </div> |
| | </aside> |
| | </main> |
| |
|
| | <footer className="h-10 border-t border-slate-800 bg-slate-900/80 px-4 flex items-center justify-between text-[10px] text-slate-500 uppercase tracking-widest font-semibold"> |
| | <div className="flex gap-4"> |
| | <span>Active Nodes: {nodes.length}</span> |
| | <span>Valid Path: {findPath('INPUT', 'OUTPUT') ? 'YES' : 'NO'}</span> |
| | <span>LR: {learningRate}</span> |
| | </div> |
| | <div>Scientific Arch-Sim Engine v2.0</div> |
| | </footer> |
| |
|
| | <style>{` |
| | @keyframes dash { |
| | to { stroke-dashoffset: -12; } |
| | } |
| | input[type="range"]::-webkit-slider-thumb { |
| | -webkit-appearance: none; |
| | appearance: none; |
| | width: 12px; |
| | height: 12px; |
| | background: #6366f1; |
| | border-radius: 50%; |
| | cursor: pointer; |
| | box-shadow: 0 0 10px rgba(99,102,241,0.5); |
| | } |
| | `}</style> |
| | </div> |
| | ); |
| | }; |
| |
|
| | export default App; |