cnn / index.html
kimhyunwoo's picture
Update index.html
55eec5f verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D CNN Visualizer</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'neon-green': '#00ff00',
'dark-green': '#002200',
},
fontFamily: {
mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', 'monospace'],
}
}
}
}
</script>
<!-- Import Map for React & Three.js ecosystem -->
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18.2.0",
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
"three": "https://esm.sh/three@0.160.0",
"@react-three/fiber": "https://esm.sh/@react-three/fiber@8.15.12?external=react,react-dom,three",
"@react-three/drei": "https://esm.sh/@react-three/drei@9.96.1?external=react,react-dom,three,@react-three/fiber",
"lucide-react": "https://esm.sh/lucide-react@0.303.0?external=react"
}
}
</script>
<!-- Babel for JSX -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
body { margin: 0; background-color: #000; overflow: hidden; color: white; }
canvas { touch-action: none; }
.hud-panel {
background: rgba(0, 10, 0, 0.85);
border: 1px solid #004400;
box-shadow: 0 0 20px rgba(0, 255, 0, 0.1);
backdrop-filter: blur(10px);
}
.btn-holo {
background: linear-gradient(180deg, rgba(0,40,0,0.8) 0%, rgba(0,20,0,0.9) 100%);
border: 1px solid #00ff00;
color: #00ff00;
text-shadow: 0 0 5px rgba(0,255,0,0.5);
}
.btn-holo:hover:not(:disabled) {
background: #00ff00;
color: #000;
box-shadow: 0 0 15px #00ff00;
}
.btn-holo:disabled {
border-color: #004400;
color: #004400;
cursor: not-allowed;
}
.neon-text {
text-shadow: 0 0 10px rgba(0, 255, 0, 0.6);
}
/* Scanline effect */
.scanlines {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: linear-gradient(to bottom, rgba(255,255,255,0), rgba(255,255,255,0) 50%, rgba(0,0,0,0.1) 50%, rgba(0,0,0,0.1));
background-size: 100% 4px;
pointer-events: none;
z-index: 50;
opacity: 0.3;
}
</style>
</head>
<body>
<div id="root"></div>
<div class="scanlines"></div>
<script type="text/babel" data-type="module">
import React, { useState, useEffect, useRef, useMemo, useLayoutEffect } from 'react';
import { createRoot } from 'react-dom/client';
import { Canvas, useFrame } from '@react-three/fiber';
import { Text, Stars, Environment, Grid } from '@react-three/drei';
import * as THREE from 'three';
import { Play, RotateCcw, Activity, Layers, Cpu, Scan, Zap, BrainCircuit, Eraser } from 'lucide-react';
// --- 1. CONFIGURATION ---
const ARCHITECTURE = [
{ id: 'input', type: 'input', width: 28, height: 28, depth: 1, z: 0, label: 'INPUT (28x28)' },
{ id: 'conv1', type: 'conv', width: 24, height: 24, depth: 4, z: -15, label: 'CONV LAYER 1' },
{ id: 'pool1', type: 'pool', width: 12, height: 12, depth: 4, z: -25, label: 'POOLING 1' },
{ id: 'conv2', type: 'conv', width: 8, height: 8, depth: 8, z: -35, label: 'CONV LAYER 2' },
{ id: 'pool2', type: 'pool', width: 4, height: 4, depth: 8, z: -45, label: 'POOLING 2' },
{ id: 'flat', type: 'flatten', width: 16, height: 8, depth: 1, z: -52, label: 'FLATTEN' },
{ id: 'fc', type: 'fc', width: 1, height: 10, depth: 1, z: -62, label: 'CLASSIFICATION' }
];
// --- 2. MATH ENGINE (Synthethic Neural Network) ---
// Define vector strokes for digits to generate "ideal" weights
const DIGIT_STROKES = {
0: [[[10,2],[4,6],[4,14],[10,18],[16,14],[16,6],[10,2]]],
1: [[[10,2],[10,18]]],
2: [[[4,6],[6,2],[14,2],[16,6],[4,18],[18,18]]],
3: [[[4,4],[16,4],[10,10],[16,16],[4,16]]],
4: [[[14,2],[4,12],[18,12]],[[14,2],[14,18]]],
5: [[[16,4],[6,4],[4,10],[16,12],[4,18]]],
6: [[[16,4],[4,12],[4,16],[10,18],[16,14],[10,10]]],
7: [[[4,4],[16,4],[8,18]]],
8: [[[10,10],[4,6],[10,2],[16,6],[10,10],[4,14],[10,18],[16,14],[10,10]]],
9: [[[16,10],[10,4],[4,8],[10,12],[16,10],[10,18]]]
};
const createGrid = (w, h, val = 0) => Array.from({ length: h }, () => Array(w).fill(val));
// Rasterize lines into grid
const drawLine = (grid, p1, p2, intensity) => {
let x0 = Math.round(p1[0]), y0 = Math.round(p1[1]);
let x1 = Math.round(p2[0]), y1 = Math.round(p2[1]);
const dx = Math.abs(x1 - x0), dy = Math.abs(y1 - y0);
const sx = (x0 < x1) ? 1 : -1, sy = (y0 < y1) ? 1 : -1;
let err = dx - dy;
while(true) {
if(y0 >= 0 && y0 < 20 && x0 >= 0 && x0 < 20) grid[y0][x0] = Math.max(grid[y0][x0], intensity);
if(x0 === x1 && y0 === y1) break;
const e2 = 2 * err;
if(e2 > -dy) { err -= dy; x0 += sx; }
if(e2 < dx) { err += dx; y0 += sy; }
}
};
const gaussianBlur = (grid) => {
const size = 20;
const result = createGrid(size, size);
const kernel = [[0.06, 0.12, 0.06], [0.12, 0.25, 0.12], [0.06, 0.12, 0.06]];
for(let y=1; y<size-1; y++) {
for(let x=1; x<size-1; x++) {
let sum = 0;
for(let ky=-1; ky<=1; ky++) for(let kx=-1; kx<=1; kx++) sum += grid[y+ky][x+kx] * kernel[ky+1][kx+1];
result[y][x] = sum;
}
}
return result;
};
// Initialize Brain
let TRAINED_WEIGHTS = {};
const trainBrain = () => {
Object.keys(DIGIT_STROKES).forEach(key => {
const strokes = DIGIT_STROKES[key];
const grid = createGrid(20, 20);
strokes.forEach(part => {
for(let i=0; i<part.length-1; i++) drawLine(grid, part[i], part[i+1], 1.0);
});
// Blur heavily to create a "probability field"
TRAINED_WEIGHTS[key] = gaussianBlur(gaussianBlur(grid));
});
};
// Preprocessing
const getBoundingBox = (grid) => {
let minX = 28, minY = 28, maxX = 0, maxY = 0, hasPixels = false;
for(let y=0; y<28; y++) for(let x=0; x<28; x++) if(grid[y][x] > 0.1) {
hasPixels = true;
if(x < minX) minX = x;
if(x > maxX) maxX = x;
if(y < minY) minY = y;
if(y > maxY) maxY = y;
}
return { minX, minY, w: maxX - minX + 1, h: maxY - minY + 1, hasPixels };
};
const scaleToFit = (grid) => {
const box = getBoundingBox(grid);
if(!box.hasPixels) return grid;
const canvas = document.createElement('canvas');
canvas.width = 28; canvas.height = 28;
const ctx = canvas.getContext('2d');
// Draw original data to canvas
const imgData = ctx.createImageData(28, 28);
for(let i=0; i<28*28; i++) {
const val = grid[Math.floor(i/28)][i%28] * 255;
imgData.data[i*4+0] = val; imgData.data[i*4+1] = val; imgData.data[i*4+2] = val; imgData.data[i*4+3] = 255;
}
// Crop to temporary canvas
const temp = document.createElement('canvas');
temp.width = box.w; temp.height = box.h;
temp.getContext('2d').putImageData(imgData, -box.minX, -box.minY, 0, 0, 28, 28);
// Scale and center back to 28x28 (target inner 20x20)
ctx.clearRect(0,0,28,28);
const scale = Math.min(20/box.w, 20/box.h);
ctx.drawImage(temp, (28 - box.w*scale)/2, (28 - box.h*scale)/2, box.w*scale, box.h*scale);
const finalData = ctx.getImageData(0,0,28,28).data;
const finalGrid = createGrid(28,28);
for(let i=0; i<28*28; i++) finalGrid[Math.floor(i/28)][i%28] = finalData[i*4+1] / 255;
return finalGrid;
};
const predict = (rawGrid) => {
const scaled = scaleToFit(rawGrid);
// Extract center 20x20
const inputVec = [];
for(let y=4; y<24; y++) for(let x=4; x<24; x++) inputVec.push(scaled[y][x]);
const inputMag = Math.sqrt(inputVec.reduce((acc, v) => acc + v*v, 0));
const scores = Array(10).fill(0);
if(inputMag > 0.1) {
for(let d=0; d<=9; d++) {
const proto = TRAINED_WEIGHTS[d];
const protoVec = [];
proto.forEach(row => row.forEach(v => protoVec.push(v)));
let dot = 0, protoMag = 0;
for(let i=0; i<400; i++) {
dot += inputVec[i] * protoVec[i];
protoMag += protoVec[i] * protoVec[i];
}
scores[d] = dot / (inputMag * Math.sqrt(protoMag) + 0.0001);
}
}
// Softmax
const exp = scores.map(s => Math.exp(s * 10)); // Amplify differences
const sum = exp.reduce((a,b) => a+b, 0);
const probs = sum > 0 ? exp.map(e => e/sum) : Array(10).fill(0);
return { probs, scaled };
};
const convolve = (input, kernels) => {
const h = input.length, w = input[0].length, kSize = kernels[0].length;
return kernels.map(k => {
const dim = h - kSize + 1;
const layer = createGrid(dim, dim);
for(let y=0; y<dim; y++) for(let x=0; x<dim; x++) {
let sum = 0;
for(let ky=0; ky<kSize; ky++) for(let kx=0; kx<kSize; kx++) sum += input[y+ky][x+kx] * k[ky][kx];
layer[y][x] = Math.max(0, sum); // ReLU
}
return layer;
});
};
const maxPool = (inputs) => inputs.map(layer => {
const h = layer.length, w = layer[0].length;
const out = createGrid(Math.floor(w/2), Math.floor(h/2));
for(let y=0; y<out.length; y++) for(let x=0; x<out[0].length; x++)
out[y][x] = Math.max(layer[y*2][x*2], layer[y*2][x*2+1], layer[y*2+1][x*2], layer[y*2+1][x*2+1]);
return out;
});
// --- 3. AUDIO ENGINE ---
class AudioEngine {
constructor() {
this.ctx = null;
this.master = null;
}
init() {
if(this.ctx) return;
const Ctx = window.AudioContext || window.webkitAudioContext;
this.ctx = new Ctx();
this.master = this.ctx.createGain();
this.master.gain.value = 0.1;
this.master.connect(this.ctx.destination);
}
playTone(freq, type, dur) {
if(!this.ctx) return;
if(this.ctx.state === 'suspended') this.ctx.resume();
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
gain.gain.setValueAtTime(0, this.ctx.currentTime);
gain.gain.linearRampToValueAtTime(1, this.ctx.currentTime + 0.01);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + dur);
osc.connect(gain);
gain.connect(this.master);
osc.start();
osc.stop(this.ctx.currentTime + dur);
}
playStep(step) {
if(step===0) this.playTone(400, 'sine', 0.1);
if(step===1) this.playTone(150, 'sawtooth', 0.1);
if(step===2) this.playTone(200, 'square', 0.1);
if(step===3) this.playTone(300, 'sawtooth', 0.1);
if(step===4) this.playTone(600, 'square', 0.1);
if(step===5) this.playTone(800, 'triangle', 0.1);
if(step===6) { this.playTone(440, 'sine', 0.3); setTimeout(()=>this.playTone(660,'sine',0.3), 100); }
}
}
const audio = new AudioEngine();
// --- 4. 3D COMPONENTS ---
const VoxelLayer = ({ config, data, active }) => {
const meshRef = useRef();
const dummy = useMemo(() => new THREE.Object3D(), []);
const color = useMemo(() => new THREE.Color(), []);
// Calculate total instances needed
const count = config.type === 'fc' ? 10 : (config.type === 'flatten' ? 128 : config.width * config.height * config.depth);
useLayoutEffect(() => {
if(!meshRef.current) return;
let idx = 0;
// --- VISUALIZATION LOGIC ---
if(config.type === 'fc') {
// Output bars
for(let i=0; i<10; i++) {
const val = data[0]?.[i]?.[0] || 0;
dummy.position.set(0, (4.5 - i) * 0.7, 0);
dummy.scale.set(val > 0.01 ? 0.5 + val * 4 : 0.5, 0.5, 0.5);
dummy.updateMatrix();
meshRef.current.setMatrixAt(idx, dummy.matrix);
if(active && val > 0.01) color.setHSL(0.33, 1, 0.5);
else color.setHex(0x002200);
meshRef.current.setColorAt(idx, color);
idx++;
}
} else if (config.type === 'flatten') {
// Flatten grid representation
const flatData = [];
if(data.length) data.forEach(slice => slice.forEach(row => row.forEach(v => flatData.push(v))));
for(let y=0; y<8; y++) {
for(let x=0; x<16; x++) {
const val = flatData[idx] || 0;
dummy.position.set((x-8)*0.25, (y-4)*0.25, 0);
if(val > 0.1) {
dummy.scale.set(0.9, 0.9, 0.9);
color.setHSL(0.35, 1, 0.2 + val*0.8);
} else {
dummy.scale.set(0,0,0);
}
dummy.updateMatrix();
meshRef.current.setMatrixAt(idx, dummy.matrix);
meshRef.current.setColorAt(idx, color);
idx++;
}
}
} else {
// Convolutional Volumes
const gap = 0.3;
const layerW = config.width * 0.2;
const totalW = (layerW + gap) * config.depth;
const startX = -totalW / 2;
for(let z=0; z<config.depth; z++) {
const zOff = startX + z * (layerW + gap);
for(let y=0; y<config.height; y++) {
for(let x=0; x<config.width; x++) {
const val = data[z]?.[config.height-1-y]?.[x] || 0;
dummy.position.set(zOff + x*0.2, (y - config.height/2)*0.2, 0);
if(val > 0.1) {
dummy.scale.set(0.9,0.9,0.9);
color.setHSL(0.35, 1, 0.2 + val*0.8);
} else {
dummy.scale.set(0,0,0);
}
dummy.updateMatrix();
meshRef.current.setMatrixAt(idx, dummy.matrix);
meshRef.current.setColorAt(idx, color);
idx++;
}
}
}
}
meshRef.current.instanceMatrix.needsUpdate = true;
if(meshRef.current.instanceColor) meshRef.current.instanceColor.needsUpdate = true;
}, [config, data, active]);
return (
<group position={[0, 0, config.z]}>
<Text position={[0, config.height * 0.12 + 2, 0]} fontSize={0.6} color={active?"#fff":"#004400"}>
{config.label}
</Text>
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
<boxGeometry args={[0.2, 0.2, 0.2]} />
<meshStandardMaterial color="#0f0" transparent opacity={0.9} blending={THREE.AdditiveBlending} toneMapped={false} />
</instancedMesh>
{config.type === 'fc' && Array.from({length:10}).map((_, i) => (
<group key={i} position={[0, (4.5 - i) * 0.7, 0]}>
<Text position={[-1.5, 0, 0]} fontSize={0.4} color="#0f0">{i}</Text>
<Text position={[4, 0, 0]} fontSize={0.4} color="#fff">
{((data[0]?.[i]?.[0] || 0) * 100).toFixed(1)}%
</Text>
</group>
))}
</group>
);
};
const CameraController = ({ step }) => {
useFrame((state) => {
const targetPos = new THREE.Vector3();
const targetLook = new THREE.Vector3();
if(step === -1) {
targetPos.set(25, 10, 5);
targetLook.set(0, 0, -35);
} else {
const zMap = {0:0, 1:-15, 2:-25, 3:-35, 4:-45, 5:-52, 6:-62};
const z = zMap[step] || 0;
targetPos.set(18, 5, z + 8);
targetLook.set(0, 0, z);
}
state.camera.position.lerp(targetPos, 0.05);
const look = new THREE.Vector3(0,0,-1).applyQuaternion(state.camera.quaternion).add(state.camera.position);
look.lerp(targetLook, 0.05);
state.camera.lookAt(look);
});
return null;
};
// --- 5. DRAWING PAD ---
const DrawingPad = ({ data, onChange, disabled }) => {
const canvasRef = useRef(null);
const [isDrawing, setIsDrawing] = useState(false);
useEffect(() => {
const ctx = canvasRef.current?.getContext('2d');
if(ctx && data.every(r=>r.every(v=>v===0))) {
ctx.fillStyle = 'black'; ctx.fillRect(0,0,280,280);
// Draw Grid
ctx.strokeStyle = '#002200'; ctx.lineWidth = 1;
ctx.beginPath();
for(let i=0; i<=280; i+=28) { ctx.moveTo(i,0); ctx.lineTo(i,280); ctx.moveTo(0,i); ctx.lineTo(280,i); }
ctx.stroke();
}
}, [data]);
const getPos = (e) => {
const r = canvasRef.current.getBoundingClientRect();
const x = (e.touches?e.touches[0].clientX:e.clientX) - r.left;
const y = (e.touches?e.touches[0].clientY:e.clientY) - r.top;
const scaleX = canvasRef.current.width / r.width;
const scaleY = canvasRef.current.height / r.height;
return { x: x*scaleX, y: y*scaleY };
};
const draw = (e) => {
if(disabled || !isDrawing) return;
const ctx = canvasRef.current.getContext('2d');
const {x,y} = getPos(e);
ctx.strokeStyle = '#0f0';
ctx.lineWidth = 25;
ctx.lineCap = 'round';
ctx.shadowBlur = 10; ctx.shadowColor = '#0f0';
ctx.lineTo(x,y); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x,y);
};
const start = (e) => {
if(disabled) return;
setIsDrawing(true);
const {x,y} = getPos(e);
const ctx = canvasRef.current.getContext('2d');
ctx.beginPath(); ctx.moveTo(x,y);
};
const end = () => {
if(!isDrawing) return;
setIsDrawing(false);
const ctx = canvasRef.current.getContext('2d');
const temp = document.createElement('canvas'); temp.width=28; temp.height=28;
temp.getContext('2d').drawImage(canvasRef.current,0,0,28,28);
const img = temp.getContext('2d').getImageData(0,0,28,28).data;
const grid = createGrid(28,28);
for(let i=0; i<28*28; i++) grid[Math.floor(i/28)][i%28] = img[i*4+1]/255;
onChange(grid);
};
return (
<canvas ref={canvasRef} width={280} height={280}
className={`w-[220px] h-[220px] rounded border border-green-800 bg-black cursor-crosshair ${disabled ? 'opacity-50 pointer-events-none' : ''}`}
onMouseDown={start} onMouseMove={draw} onMouseUp={end} onMouseLeave={()=>setIsDrawing(false)}
onTouchStart={start} onTouchMove={draw} onTouchEnd={end}
/>
);
};
// --- 6. MAIN APP ---
const App = () => {
const [activations, setActivations] = useState({});
const [step, setStep] = useState(-1);
const [processing, setProcessing] = useState(false);
const [inputData, setInputData] = useState(createGrid(28,28));
useEffect(() => {
trainBrain();
reset();
audio.init();
}, []);
const reset = () => {
setStep(-1);
setActivations(ARCHITECTURE.reduce((acc,l) => ({...acc, [l.id]: []}), {}));
setInputData(createGrid(28,28));
};
const delay = ms => new Promise(r => setTimeout(r, ms));
const run = async () => {
if(processing) return;
setProcessing(true);
// Inference
const { probs, scaled } = predict(inputData);
// Pipeline Animation
setActivations(prev => ({...prev, input: [scaled]}));
setStep(0); audio.playStep(0); await delay(600);
const k1 = [[[1,1,1],[0,0,0],[-1,-1,-1]], [[1,0,-1],[1,0,-1],[1,0,-1]], [[0,-1,0],[-1,4,-1],[0,-1,0]], [[-1,-1,-1],[-1,8,-1],[-1,-1,-1]]];
const c1 = convolve(scaled, k1);
setActivations(prev => ({...prev, conv1: c1}));
setStep(1); audio.playStep(1); await delay(800);
const p1 = maxPool(c1);
setActivations(prev => ({...prev, pool1: p1}));
setStep(2); audio.playStep(2); await delay(800);
const c2 = p1.map(layer => convolve(layer, [[[0.5,0.5],[0.5,0.5]]])[0]).concat(p1.map(l => l)); // Fake doubling depth
setActivations(prev => ({...prev, conv2: c2}));
setStep(3); audio.playStep(3); await delay(800);
const p2 = maxPool(c2);
setActivations(prev => ({...prev, pool2: p2}));
setStep(4); audio.playStep(4); await delay(800);
setActivations(prev => ({...prev, flat: p2}));
setStep(5); audio.playStep(5); await delay(600);
setActivations(prev => ({...prev, fc: [probs.map(p=>[p])]}));
setStep(6); audio.playStep(6);
await delay(3000);
setProcessing(false);
setStep(-1);
};
return (
<div className="w-full h-screen relative bg-black font-mono">
<Canvas shadows camera={{ position: [25, 10, 5], fov: 45 }}>
<CameraController step={step} />
<color attach="background" args={['#000200']} />
<fog attach="fog" args={['#000200', 20, 90]} />
<ambientLight intensity={0.2} />
<pointLight position={[10, 20, 10]} intensity={1.5} color="#00ff00" distance={50} />
<group>
{ARCHITECTURE.map((cfg, i) => (
<VoxelLayer key={cfg.id} config={cfg} data={activations[cfg.id] || []} active={step===i} />
))}
<Grid args={[200, 200]} cellSize={1} cellThickness={1} sectionSize={5} sectionThickness={1.5} fadeDistance={60} sectionColor="#004400" cellColor="#001100" position={[0, -5, -30]} />
</group>
<Stars radius={100} depth={50} count={3000} factor={4} saturation={0} fade speed={1} />
<Environment preset="city" />
</Canvas>
{/* HUD UI */}
<div className="absolute inset-0 pointer-events-none flex flex-col justify-between p-4 z-10">
{/* Header */}
<div className="flex justify-between items-start">
<div className="hud-panel p-4 rounded-br-2xl border-l-4 border-l-green-500">
<h1 className="text-2xl md:text-4xl font-black tracking-tighter neon-text flex items-center gap-3">
<Cpu className="text-neon-green animate-pulse" /> DEEP <span className="text-neon-green">CNN</span>
</h1>
<div className="text-xs text-green-400 mt-2 flex items-center gap-2">
<Activity size={12} /> SYSTEM: {processing ? "PROCESSING TENSORS..." : "ONLINE"}
</div>
</div>
</div>
{/* Controls */}
<div className="flex flex-col md:flex-row items-end gap-6 pointer-events-auto">
<div className="hud-panel p-4 rounded-tr-2xl backdrop-blur-xl max-w-sm">
<div className="flex justify-between items-center mb-2 text-green-400">
<div className="text-xs font-bold tracking-widest flex items-center gap-2">
<Scan size={14} /> INPUT SENSOR
</div>
<button onClick={reset} disabled={processing} className="hover:text-white transition-colors">
<RotateCcw size={16} />
</button>
</div>
<DrawingPad data={inputData} onChange={setInputData} disabled={processing} />
<button onClick={run} disabled={processing} className="w-full mt-4 py-3 rounded btn-holo flex justify-center items-center gap-2 font-bold transition-all">
{processing ? <Activity className="animate-spin" size={18} /> : <Play size={18} fill="currentColor" />}
{processing ? 'CALCULATING...' : 'RUN INFERENCE'}
</button>
</div>
{/* Pipeline Status */}
<div className="hud-panel p-5 hidden md:block rounded-t-xl min-w-[260px] border-b-0">
<div className="text-xs text-green-500 font-bold mb-3 flex items-center gap-2">
<Layers size={14} /> PIPELINE STATUS
</div>
<div className="space-y-2">
{ARCHITECTURE.map((l, i) => (
<div key={l.id} className={`flex items-center gap-3 text-xs transition-all duration-300 ${step===i ? 'text-white translate-x-2' : 'text-green-900'}`}>
<div className={`w-2 h-2 rounded-sm ${step===i ? 'bg-neon-green shadow-[0_0_8px_#0f0]' : 'bg-green-900'}`} />
<span className={step===i ? 'font-bold' : ''}>{l.label}</span>
{step===i && <Zap size={10} className="ml-auto text-yellow-400 animate-pulse" />}
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
};
const root = createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>