FlameF0X commited on
Commit
db6ea98
·
verified ·
1 Parent(s): 7d26cbf

Update src/App.js

Browse files
Files changed (1) hide show
  1. src/App.js +613 -19
src/App.js CHANGED
@@ -1,25 +1,619 @@
1
- import logo from './logo.svg';
2
- import './App.css';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
- function App() {
5
  return (
6
- <div className="App">
7
- <header className="App-header">
8
- <img src={logo} className="App-logo" alt="logo" />
9
- <p>
10
- Edit <code>src/App.js</code> and save to reload.
11
- </p>
12
- <a
13
- className="App-link"
14
- href="https://reactjs.org"
15
- target="_blank"
16
- rel="noopener noreferrer"
17
- >
18
- Learn React
19
- </a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  </header>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  </div>
22
  );
23
- }
24
 
25
- export default App;
 
1
+ import React, { useState, useEffect, useRef, useMemo } from 'react';
2
+ import {
3
+ Activity,
4
+ Settings,
5
+ Play,
6
+ Square,
7
+ Trash2,
8
+ Layers,
9
+ Cpu,
10
+ Zap,
11
+ Plus,
12
+ Info,
13
+ Database,
14
+ Move,
15
+ Maximize,
16
+ Search,
17
+ ZoomIn,
18
+ ZoomOut
19
+ } from 'lucide-react';
20
+ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
21
+
22
+ // --- BLOCK DEFINITIONS ---
23
+ const BLOCK_TYPES = {
24
+ INPUT: {
25
+ id: 'INPUT',
26
+ label: 'Data In',
27
+ category: 'IO',
28
+ color: 'bg-emerald-500',
29
+ outputs: 1,
30
+ inputs: 0,
31
+ config: { batchSize: 32, seqLen: 128, vocabSize: 5000 }
32
+ },
33
+ EMBED: {
34
+ id: 'EMBED',
35
+ label: 'Embedding',
36
+ category: 'Core',
37
+ color: 'bg-blue-500',
38
+ outputs: 1,
39
+ inputs: 1,
40
+ config: { dim: 512 }
41
+ },
42
+ ATTN: {
43
+ id: 'ATTN',
44
+ label: 'Attention',
45
+ category: 'Core',
46
+ color: 'bg-indigo-600',
47
+ outputs: 1,
48
+ inputs: 1,
49
+ config: { heads: 8, headDim: 64, dropout: 0.1 }
50
+ },
51
+ FFN: {
52
+ id: 'FFN',
53
+ label: 'Feed Forward',
54
+ category: 'Core',
55
+ color: 'bg-blue-600',
56
+ outputs: 1,
57
+ inputs: 1,
58
+ config: { mult: 4, dropout: 0.1 }
59
+ },
60
+ NORM: {
61
+ id: 'NORM',
62
+ label: 'Layer Norm',
63
+ category: 'Core',
64
+ color: 'bg-slate-500',
65
+ outputs: 1,
66
+ inputs: 1,
67
+ config: { eps: 1e-5 }
68
+ },
69
+ LSTM: {
70
+ id: 'LSTM',
71
+ label: 'LSTM',
72
+ category: 'RNN',
73
+ color: 'bg-amber-500',
74
+ outputs: 1,
75
+ inputs: 1,
76
+ config: { hidden: 512, layers: 1 }
77
+ },
78
+ MAMBA: {
79
+ id: 'MAMBA',
80
+ label: 'Mamba (SSM)',
81
+ category: 'Modern',
82
+ color: 'bg-purple-600',
83
+ outputs: 1,
84
+ inputs: 1,
85
+ config: { d_state: 16, d_conv: 4, expand: 2 }
86
+ },
87
+ RWKV: {
88
+ id: 'RWKV',
89
+ label: 'RWKV',
90
+ category: 'Modern',
91
+ color: 'bg-fuchsia-600',
92
+ outputs: 1,
93
+ inputs: 1,
94
+ config: { mode: 'v5', head_size: 64 }
95
+ },
96
+ RESIDUAL: {
97
+ id: 'RESIDUAL',
98
+ label: 'Residual +',
99
+ category: 'Utility',
100
+ color: 'bg-cyan-500',
101
+ outputs: 1,
102
+ inputs: 2,
103
+ config: {}
104
+ },
105
+ OUTPUT: {
106
+ id: 'OUTPUT',
107
+ label: 'Data Out',
108
+ category: 'IO',
109
+ color: 'bg-red-500',
110
+ outputs: 0,
111
+ inputs: 1,
112
+ config: { loss: 'CrossEntropy' }
113
+ }
114
+ };
115
+
116
+ const App = () => {
117
+ // State for Blocks and Connections
118
+ const [nodes, setNodes] = useState([
119
+ { id: 'n1', type: 'INPUT', x: 50, y: 150, config: BLOCK_TYPES.INPUT.config },
120
+ { id: 'n2', type: 'EMBED', x: 250, y: 150, config: BLOCK_TYPES.EMBED.config },
121
+ { id: 'n3', type: 'OUTPUT', x: 450, y: 150, config: BLOCK_TYPES.OUTPUT.config },
122
+ ]);
123
+ const [connections, setConnections] = useState([
124
+ { id: 'c1', from: 'n1', to: 'n2' },
125
+ { id: 'c2', from: 'n2', to: 'n3' }
126
+ ]);
127
+
128
+ // UI & Viewport State
129
+ const [viewOffset, setViewOffset] = useState({ x: 0, y: 0 });
130
+ const [zoom, setZoom] = useState(1);
131
+ const [isPanning, setIsPanning] = useState(false);
132
+ const [panStart, setPanStart] = useState({ x: 0, y: 0 });
133
+
134
+ const [selectedNodeId, setSelectedNodeId] = useState(null);
135
+ const [isTraining, setIsTraining] = useState(false);
136
+ const [trainingData, setTrainingData] = useState([]);
137
+ const [learningRate, setLearningRate] = useState(0.001);
138
+ const [isDraggingNode, setIsDraggingNode] = useState(null);
139
+ const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
140
+ const [connectingFrom, setConnectingFrom] = useState(null);
141
+
142
+ const canvasRef = useRef(null);
143
+
144
+ // --- SIMULATION ENGINE ---
145
+ const runTrainingStep = (epoch) => {
146
+ const hasInput = nodes.some(n => n.type === 'INPUT');
147
+ const hasOutput = nodes.some(n => n.type === 'OUTPUT');
148
+ if (!hasInput || !hasOutput) return null;
149
+
150
+ let capacity = 0;
151
+ let vanishingGradientRisk = 1.0;
152
+
153
+ nodes.forEach(n => {
154
+ if (n.type === 'EMBED') capacity += (n.config.dim || 512) * 0.1;
155
+ if (n.type === 'ATTN') capacity += (n.config.heads * n.config.headDim) * 0.5;
156
+ if (n.type === 'FFN') capacity += (n.config.mult * 512) * 0.3;
157
+ if (n.type === 'LSTM') {
158
+ capacity += (n.config.hidden || 512) * 0.4;
159
+ vanishingGradientRisk *= 0.85;
160
+ }
161
+ if (n.type === 'NORM') vanishingGradientRisk = Math.min(1.0, vanishingGradientRisk + 0.2);
162
+ if (n.type === 'RESIDUAL') vanishingGradientRisk = Math.min(1.0, vanishingGradientRisk + 0.3);
163
+ });
164
+
165
+ const trainability = Math.max(0.1, (capacity / 500) * vanishingGradientRisk);
166
+ const noise = (Math.random() - 0.5) * 0.05;
167
+ const lrFactor = learningRate > 0.01 ? (0.5 + Math.random()) : 1.0;
168
+
169
+ const baseLoss = 8.0;
170
+ const decay = 0.05 * trainability * (1 / lrFactor);
171
+ const loss = baseLoss * Math.exp(-decay * epoch) + (0.2 / (epoch + 1)) + noise;
172
+ const acc = Math.min(0.99, 1 - (loss / baseLoss) + (Math.random() * 0.02));
173
+
174
+ return {
175
+ epoch,
176
+ loss: Math.max(0, loss).toFixed(4),
177
+ accuracy: Math.max(0, acc).toFixed(4)
178
+ };
179
+ };
180
+
181
+ useEffect(() => {
182
+ let interval;
183
+ if (isTraining) {
184
+ interval = setInterval(() => {
185
+ setTrainingData(prev => {
186
+ const nextEpoch = prev.length;
187
+ if (nextEpoch > 150) { setIsTraining(false); return prev; }
188
+ const step = runTrainingStep(nextEpoch);
189
+ return [...prev, step];
190
+ });
191
+ }, 150);
192
+ }
193
+ return () => clearInterval(interval);
194
+ }, [isTraining, nodes, connections, learningRate]);
195
+
196
+ // --- INTERACTION HANDLERS ---
197
+ const handleWheel = (e) => {
198
+ e.preventDefault();
199
+ const zoomSpeed = 0.001;
200
+ const newZoom = Math.min(Math.max(zoom - e.deltaY * zoomSpeed, 0.2), 2);
201
+ setZoom(newZoom);
202
+ };
203
+
204
+ const addNode = (type) => {
205
+ const newNode = {
206
+ id: `n${Date.now()}`,
207
+ type,
208
+ // Place node relative to current view center and zoom
209
+ x: (-viewOffset.x + 100) / zoom,
210
+ y: (-viewOffset.y + 100) / zoom,
211
+ config: { ...BLOCK_TYPES[type].config }
212
+ };
213
+ setNodes([...nodes, newNode]);
214
+ };
215
+
216
+ const deleteNode = (id) => {
217
+ setNodes(nodes.filter(n => n.id !== id));
218
+ setConnections(connections.filter(c => c.from !== id && c.to !== id));
219
+ if (selectedNodeId === id) setSelectedNodeId(null);
220
+ };
221
+
222
+ const deleteConnection = (id) => {
223
+ setConnections(connections.filter(c => c.id !== id));
224
+ };
225
+
226
+ const handleCanvasMouseDown = (e) => {
227
+ if (e.target === canvasRef.current || e.target.tagName === 'svg' || e.target.id === 'grid-background') {
228
+ setIsPanning(true);
229
+ setPanStart({ x: e.clientX - viewOffset.x, y: e.clientY - viewOffset.y });
230
+ setSelectedNodeId(null);
231
+ }
232
+ };
233
+
234
+ const handleNodeMouseDown = (e, id) => {
235
+ e.stopPropagation();
236
+ if (connectingFrom) return;
237
+ setIsDraggingNode(id);
238
+ const node = nodes.find(n => n.id === id);
239
+ // Dragging logic needs to account for zoom
240
+ setDragOffset({ x: e.clientX / zoom - node.x, y: e.clientY / zoom - node.y });
241
+ setSelectedNodeId(id);
242
+ };
243
+
244
+ const handleMouseMove = (e) => {
245
+ if (isDraggingNode) {
246
+ setNodes(nodes.map(n => n.id === isDraggingNode ? { ...n, x: e.clientX / zoom - dragOffset.x, y: e.clientY / zoom - dragOffset.y } : n));
247
+ } else if (isPanning) {
248
+ setViewOffset({
249
+ x: e.clientX - panStart.x,
250
+ y: e.clientY - panStart.y
251
+ });
252
+ }
253
+ };
254
+
255
+ const handleMouseUp = () => {
256
+ setIsDraggingNode(null);
257
+ setIsPanning(false);
258
+ };
259
+
260
+ const startConnection = (e, id) => {
261
+ e.stopPropagation();
262
+ setConnectingFrom(id);
263
+ };
264
+
265
+ const endConnection = (e, id) => {
266
+ e.stopPropagation();
267
+ if (connectingFrom && connectingFrom !== id) {
268
+ if (!connections.some(c => c.from === connectingFrom && c.to === id)) {
269
+ setConnections([...connections, { id: `c${Date.now()}`, from: connectingFrom, to: id }]);
270
+ }
271
+ }
272
+ setConnectingFrom(null);
273
+ };
274
+
275
+ const updateConfig = (key, val) => {
276
+ setNodes(nodes.map(n => n.id === selectedNodeId ? { ...n, config: { ...n.config, [key]: val } } : n));
277
+ };
278
+
279
+ const resetView = () => {
280
+ setViewOffset({ x: 0, y: 0 });
281
+ setZoom(1);
282
+ };
283
+
284
+ const selectedNode = nodes.find(n => n.id === selectedNodeId);
285
+
286
+ // Calculated properties for infinite grid
287
+ const gridSize = 24 * zoom;
288
+ const gridOffsetX = viewOffset.x % gridSize;
289
+ const gridOffsetY = viewOffset.y % gridSize;
290
 
 
291
  return (
292
+ <div className="flex flex-col h-screen w-screen bg-slate-950 text-slate-100 overflow-hidden font-sans">
293
+ {/* Header */}
294
+ <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">
295
+ <div className="flex items-center gap-3">
296
+ <div className="p-2 bg-indigo-500 rounded-lg">
297
+ <Layers size={20} className="text-white" />
298
+ </div>
299
+ <h1 className="text-lg font-bold tracking-tight">Arch-Sim <span className="text-slate-500 font-normal">v1.2</span></h1>
300
+ </div>
301
+
302
+ <div className="flex items-center gap-4">
303
+ <div className="flex items-center bg-slate-800 rounded-full px-4 py-1.5 gap-3 border border-slate-700">
304
+ <span className="text-xs text-slate-400 uppercase font-semibold">LR</span>
305
+ <input
306
+ type="range"
307
+ min="0.0001"
308
+ max="0.05"
309
+ step="0.0001"
310
+ value={learningRate}
311
+ onChange={(e) => setLearningRate(parseFloat(e.target.value))}
312
+ className="w-24 accent-indigo-500"
313
+ />
314
+ <span className="text-xs font-mono w-12">{learningRate}</span>
315
+ </div>
316
+
317
+ <button
318
+ onClick={() => {
319
+ if (isTraining) setIsTraining(false);
320
+ else {
321
+ setTrainingData([]);
322
+ setIsTraining(true);
323
+ }
324
+ }}
325
+ className={`flex items-center gap-2 px-5 py-2 rounded-lg font-medium transition-all ${
326
+ isTraining ? 'bg-red-500 hover:bg-red-600' : 'bg-emerald-500 hover:bg-emerald-600'
327
+ }`}
328
+ >
329
+ {isTraining ? <><Square size={16} fill="currentColor" /> Stop</> : <><Play size={16} fill="currentColor" /> Start Training</>}
330
+ </button>
331
+ </div>
332
  </header>
333
+
334
+ <main className="flex-1 flex overflow-hidden">
335
+ {/* Sidebar: Blocks Palette */}
336
+ <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">
337
+ <div>
338
+ <h3 className="text-xs font-bold text-slate-500 uppercase tracking-widest mb-4">Components</h3>
339
+ <div className="grid grid-cols-1 gap-2">
340
+ {Object.entries(BLOCK_TYPES).map(([key, type]) => (
341
+ <button
342
+ key={key}
343
+ onClick={() => addNode(key)}
344
+ 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"
345
+ >
346
+ <div className={`w-8 h-8 rounded-lg ${type.color} flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform`}>
347
+ <Cpu size={14} />
348
+ </div>
349
+ <div>
350
+ <div className="text-sm font-semibold">{type.label}</div>
351
+ <div className="text-[10px] text-slate-500 uppercase">{type.category}</div>
352
+ </div>
353
+ <Plus size={14} className="ml-auto text-slate-600 group-hover:text-indigo-400" />
354
+ </button>
355
+ ))}
356
+ </div>
357
+ </div>
358
+ </aside>
359
+
360
+ {/* Canvas Area */}
361
+ <div
362
+ className="flex-1 relative bg-slate-950 overflow-hidden cursor-grab active:cursor-grabbing outline-none"
363
+ onMouseDown={handleCanvasMouseDown}
364
+ onMouseMove={handleMouseMove}
365
+ onMouseUp={handleMouseUp}
366
+ onWheel={handleWheel}
367
+ ref={canvasRef}
368
+ >
369
+ {/* Infinite Dot Grid Background */}
370
+ <div
371
+ id="grid-background"
372
+ className="absolute inset-0 pointer-events-none"
373
+ style={{
374
+ backgroundImage: `radial-gradient(circle, #1e293b 1px, transparent 1px)`,
375
+ backgroundSize: `${gridSize}px ${gridSize}px`,
376
+ backgroundPosition: `${gridOffsetX}px ${gridOffsetY}px`
377
+ }}
378
+ />
379
+
380
+ {/* Viewport Transform Container */}
381
+ <div
382
+ className="absolute inset-0 pointer-events-none"
383
+ style={{
384
+ transform: `translate(${viewOffset.x}px, ${viewOffset.y}px) scale(${zoom})`,
385
+ transformOrigin: '0 0'
386
+ }}
387
+ >
388
+ {/* Connections SVG */}
389
+ <svg className="absolute inset-0 w-[10000px] h-[10000px] pointer-events-auto overflow-visible">
390
+ <defs>
391
+ <marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
392
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="#6366f1" />
393
+ </marker>
394
+ </defs>
395
+ {connections.map(conn => {
396
+ const fromNode = nodes.find(n => n.id === conn.from);
397
+ const toNode = nodes.find(n => n.id === conn.to);
398
+ if (!fromNode || !toNode) return null;
399
+
400
+ const x1 = fromNode.x + 160;
401
+ const y1 = fromNode.y + 40;
402
+ const x2 = toNode.x;
403
+ const y2 = toNode.y + 40;
404
+ const dx = (x2 - x1) / 2;
405
+
406
+ return (
407
+ <g key={conn.id} className="group cursor-pointer">
408
+ <path
409
+ d={`M ${x1} ${y1} C ${x1 + dx} ${y1}, ${x2 - dx} ${y2}, ${x2} ${y2}`}
410
+ stroke="transparent"
411
+ strokeWidth={20 / zoom} // Adjust hit area based on zoom
412
+ fill="none"
413
+ onClick={(e) => { e.stopPropagation(); deleteConnection(conn.id); }}
414
+ />
415
+ <path
416
+ d={`M ${x1} ${y1} C ${x1 + dx} ${y1}, ${x2 - dx} ${y2}, ${x2} ${y2}`}
417
+ stroke="#6366f1"
418
+ strokeWidth={3}
419
+ strokeDasharray={isTraining ? "8 4" : "0"}
420
+ className={`${isTraining ? "animate-[dash_1s_linear_infinite]" : ""} group-hover:stroke-red-500 transition-colors`}
421
+ fill="none"
422
+ markerEnd="url(#arrow)"
423
+ opacity="0.6"
424
+ />
425
+ </g>
426
+ );
427
+ })}
428
+ </svg>
429
+
430
+ {/* Nodes */}
431
+ {nodes.map(node => {
432
+ const type = BLOCK_TYPES[node.type];
433
+ const isSelected = selectedNodeId === node.id;
434
+ return (
435
+ <div
436
+ key={node.id}
437
+ style={{ left: node.x, top: node.y }}
438
+ onMouseDown={(e) => handleNodeMouseDown(e, node.id)}
439
+ className={`absolute w-40 p-4 rounded-xl border-2 transition-shadow cursor-move select-none pointer-events-auto ${
440
+ isSelected ? 'border-indigo-500 bg-slate-800 shadow-[0_0_25px_rgba(99,102,241,0.2)]' : 'border-slate-800 bg-slate-900/90'
441
+ }`}
442
+ >
443
+ <div className="flex justify-between items-start mb-2">
444
+ <div className={`w-8 h-8 rounded-lg ${type.color} flex items-center justify-center shadow-lg mb-2`}>
445
+ <Cpu size={14} />
446
+ </div>
447
+ {isSelected && (
448
+ <button onClick={(e) => { e.stopPropagation(); deleteNode(node.id); }} className="text-slate-500 hover:text-red-400">
449
+ <Trash2 size={14} />
450
+ </button>
451
+ )}
452
+ </div>
453
+
454
+ <h4 className="text-xs font-bold uppercase tracking-wide truncate">{type.label}</h4>
455
+ <div className="text-[10px] text-slate-500 mb-4">{node.id}</div>
456
+
457
+ {/* Input Port */}
458
+ {type.inputs > 0 && (
459
+ <div
460
+ onMouseUp={(e) => endConnection(e, node.id)}
461
+ 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' : 'border-indigo-500'} flex items-center justify-center hover:bg-indigo-500 cursor-pointer z-30`}
462
+ >
463
+ <div className="w-1.5 h-1.5 bg-white rounded-full" />
464
+ </div>
465
+ )}
466
+ {/* Output Port */}
467
+ {type.outputs > 0 && (
468
+ <div
469
+ onMouseDown={(e) => startConnection(e, node.id)}
470
+ 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"
471
+ >
472
+ <div className="w-1.5 h-1.5 bg-white rounded-full" />
473
+ </div>
474
+ )}
475
+ </div>
476
+ );
477
+ })}
478
+ </div>
479
+
480
+ {/* Navigation Controls Overlay */}
481
+ <div className="absolute bottom-6 left-6 flex flex-col gap-2 z-20">
482
+ <div className="flex gap-2">
483
+ <button
484
+ onClick={resetView}
485
+ 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"
486
+ title="Reset View"
487
+ >
488
+ <Maximize size={18} />
489
+ <span className="text-[10px] font-bold uppercase tracking-widest">{Math.round(zoom * 100)}%</span>
490
+ </button>
491
+ <button
492
+ onClick={() => setZoom(prev => Math.min(prev + 0.1, 2))}
493
+ className="p-3 bg-slate-900 border border-slate-700 rounded-lg hover:bg-slate-800 transition-colors shadow-xl text-slate-400"
494
+ >
495
+ <ZoomIn size={18} />
496
+ </button>
497
+ <button
498
+ onClick={() => setZoom(prev => Math.max(prev - 0.1, 0.2))}
499
+ className="p-3 bg-slate-900 border border-slate-700 rounded-lg hover:bg-slate-800 transition-colors shadow-xl text-slate-400"
500
+ >
501
+ <ZoomOut size={18} />
502
+ </button>
503
+ </div>
504
+ <div className="p-3 bg-slate-900/80 border border-slate-700 rounded-lg shadow-xl text-[10px] text-slate-400 flex items-center gap-2">
505
+ <Search size={14} /> Scroll to zoom, Drag background to pan
506
+ </div>
507
+ </div>
508
+ </div>
509
+
510
+ {/* Right Sidebar: Config & Monitoring */}
511
+ <aside className="w-80 border-l border-slate-800 bg-slate-900/50 flex flex-col z-10">
512
+ <div className="h-1/2 p-6 flex flex-col border-b border-slate-800">
513
+ <div className="flex items-center gap-2 mb-4 text-slate-400 uppercase text-xs font-bold tracking-widest">
514
+ <Activity size={14} /> Training Metrics
515
+ </div>
516
+
517
+ <div className="flex-1 bg-slate-900 rounded-xl p-2 border border-slate-800 overflow-hidden">
518
+ {trainingData.length > 0 ? (
519
+ <ResponsiveContainer width="100%" height="100%">
520
+ <LineChart data={trainingData}>
521
+ <CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
522
+ <XAxis dataKey="epoch" hide />
523
+ <YAxis hide domain={[0, 'auto']} />
524
+ <Tooltip
525
+ contentStyle={{ backgroundColor: '#0f172a', border: '1px solid #1e293b', borderRadius: '8px' }}
526
+ itemStyle={{ color: '#6366f1' }}
527
+ />
528
+ <Line type="monotone" dataKey="loss" stroke="#6366f1" strokeWidth={2} dot={false} isAnimationActive={false} />
529
+ </LineChart>
530
+ </ResponsiveContainer>
531
+ ) : (
532
+ <div className="h-full flex flex-col items-center justify-center text-slate-600 italic text-sm text-center">
533
+ Simulation idle.
534
+ </div>
535
+ )}
536
+ </div>
537
+
538
+ <div className="mt-4 grid grid-cols-2 gap-4">
539
+ <div className="p-3 bg-slate-900/50 rounded-lg border border-slate-800 text-center">
540
+ <div className="text-[10px] uppercase text-slate-500 font-bold mb-1">Loss</div>
541
+ <div className="text-lg font-mono text-indigo-400">
542
+ {trainingData.length > 0 ? trainingData[trainingData.length-1].loss : '0.0000'}
543
+ </div>
544
+ </div>
545
+ <div className="p-3 bg-slate-900/50 rounded-lg border border-slate-800 text-center">
546
+ <div className="text-[10px] uppercase text-slate-500 font-bold mb-1">Accuracy</div>
547
+ <div className="text-lg font-mono text-emerald-400">
548
+ {trainingData.length > 0 ? `${(trainingData[trainingData.length-1].accuracy * 100).toFixed(1)}%` : '0.0%'}
549
+ </div>
550
+ </div>
551
+ </div>
552
+ </div>
553
+
554
+ <div className="h-1/2 p-6 overflow-y-auto">
555
+ <div className="flex items-center gap-2 mb-6 text-slate-400 uppercase text-xs font-bold tracking-widest">
556
+ <Settings size={14} /> {selectedNode ? 'Block Config' : 'Project Info'}
557
+ </div>
558
+
559
+ {selectedNode ? (
560
+ <div className="space-y-6">
561
+ <div className="flex items-center gap-3 p-3 bg-indigo-500/10 rounded-xl border border-indigo-500/20">
562
+ <div className={`w-8 h-8 rounded-lg ${BLOCK_TYPES[selectedNode.type].color} flex items-center justify-center`}>
563
+ <Cpu size={14} />
564
+ </div>
565
+ <div>
566
+ <div className="font-bold text-sm">{BLOCK_TYPES[selectedNode.type].label}</div>
567
+ <div className="text-[10px] text-indigo-300 font-mono uppercase">{selectedNode.id}</div>
568
+ </div>
569
+ </div>
570
+
571
+ <div className="space-y-4">
572
+ {Object.entries(selectedNode.config).map(([key, val]) => (
573
+ <div key={key} className="space-y-1.5">
574
+ <label className="text-[11px] font-bold text-slate-500 uppercase flex justify-between">
575
+ {key} <span>{val}</span>
576
+ </label>
577
+ <input
578
+ type={typeof val === 'number' ? 'range' : 'text'}
579
+ min={1}
580
+ max={1024}
581
+ value={val}
582
+ onChange={(e) => updateConfig(key, typeof val === 'number' ? parseInt(e.target.value) : e.target.value)}
583
+ className="w-full h-1.5 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-indigo-500"
584
+ />
585
+ </div>
586
+ ))}
587
+ </div>
588
+ </div>
589
+ ) : (
590
+ <div className="space-y-4">
591
+ <div className="p-4 bg-slate-800/30 rounded-xl border border-slate-700/50 text-xs text-slate-400 space-y-3">
592
+ <p><strong className="text-slate-300">Navigation:</strong> Click and drag the background to pan. <span className="text-indigo-400 font-bold">Use mouse wheel to zoom.</span></p>
593
+ <p><strong className="text-slate-300">Infinite Grid:</strong> The canvas background scales and repeats automatically as you navigate.</p>
594
+ </div>
595
+ </div>
596
+ )}
597
+ </div>
598
+ </aside>
599
+ </main>
600
+
601
+ <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">
602
+ <div className="flex gap-4">
603
+ <span>Blocks: {nodes.length}</span>
604
+ <span>Links: {connections.length}</span>
605
+ <span>Zoom: {(zoom * 100).toFixed(0)}%</span>
606
+ </div>
607
+ <div>Educational Arch-Sim Environment</div>
608
+ </footer>
609
+
610
+ <style>{`
611
+ @keyframes dash {
612
+ to { stroke-dashoffset: -12; }
613
+ }
614
+ `}</style>
615
  </div>
616
  );
617
+ };
618
 
619
+ export default App;