|
|
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; |