/* * AI Lab — self-contained demo. * Alles op deze pagina is in een sessie door een AI-agent (Cline) geschreven. * Geen libraries, geen internet. Alleen HTML + CSS + JS die aan elkaar * getypt zijn tijdens het bouwen van deze demo. */ 'use strict'; /* ========================================================= * A. Cline-terminal: het verhaal achter deze demo * ========================================================= */ const SCRIPT = [ { t: 'prompt', tx: 'Jij: Bouw een self-contained demo-pagina die laat zien hoe krachtig een AI coding agent is. Alles draait in de browser: geen backend, geen API-keys, geen installaties.' }, { t: 'plan', tx: 'Cline analyseert de vraag en stelt een plan voor — "AI Lab" in 3 blokken.' }, { t: 'plan', tx: ' 1. Een neuraal netwerk dat écht leert (JS, backpropagation, geen libraries)' }, { t: 'plan', tx: ' 2. Generative art op canvas die reageert op je muis' }, { t: 'plan', tx: ' 3. Een terminal die vertelt hoe dit allemaal is ontstaan' }, { t: 'tool', tx: 'Scannen van de map… 0 bestanden (schone start)' }, { t: 'tool', tx: 'index.html schrijven … klaar' }, { t: 'tool', tx: 'styles.css schrijven … klaar' }, { t: 'tool', tx: 'app.js schrijven … klaar (NeuralNet: forward + backprop in ±90 regels)' }, { t: 'tool', tx: 'node --check app.js → geen fouten' }, { t: 'tool', tx: 'python3 -m http.server 8080 → HTTP 200' }, { t: 'ok', tx: 'Klaar. Dit alles is door een AI-agent in één sessie geschreven — en draait nu op je scherm.' } ]; const termBody = document.getElementById('termBody'); const replayBtn = document.getElementById('termReplay'); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); function pushLine(cls, text) { const div = document.createElement('div'); div.className = cls; div.textContent = text; termBody.appendChild(div); termBody.scrollTop = termBody.scrollHeight; return div; } async function typeInto(el, text, cps) { for (let i = 0; i < text.length; i++) { el.textContent = text.slice(0, i + 1); termBody.scrollTop = termBody.scrollHeight; await sleep(cps); } } async function runScript() { replayBtn.style.display = 'none'; termBody.innerHTML = ''; for (const line of SCRIPT) { if (line.t === 'prompt' || line.t === 'plan') { const spacer = document.createElement('div'); spacer.className = 'term-spacer'; termBody.appendChild(spacer); } const el = pushLine(line.t, ''); const cps = line.t === 'tool' ? 3 : line.t === 'ok' ? 9 : 11; const txt = line.t === 'tool' ? '$ ' + line.tx : line.tx; await typeInto(el, txt, cps); await sleep(line.t === 'ok' ? 300 : 170); } replayBtn.style.display = 'inline-flex'; } function bootTerminal() { replayBtn.addEventListener('click', () => runScript()); setTimeout(runScript, 600); } /* ========================================================= * B. Neuraal netwerk dat écht leert * ========================================================= */ const nnCanvas = document.getElementById('nnCanvas'); const nnCtx = nnCanvas.getContext('2d'); const lossCanvas = document.getElementById('lossCanvas'); const lossCtx = lossCanvas.getContext('2d'); const nnStats = document.getElementById('nnStats'); const nnPauseBtn = document.getElementById('nnPause'); const nnResetBtn = document.getElementById('nnReset'); const nnSpeed = document.getElementById('nnSpeed'); const tanh = Math.tanh; const sig = (v) => 1 / (1 + Math.exp(-v)); function randn() { let u = 0, v = 0; while (!u) u = Math.random(); while (!v) v = Math.random(); return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v); } class NeuralNet { constructor(shape) { this.shape = shape; this.W = []; this.b = []; this.m1W = []; this.m2W = []; this.m1b = []; this.m2b = []; this.t = 0; for (let i = 0; i < shape.length - 1; i++) { const s = Math.sqrt(2 / shape[i]); const W = Array.from({ length: shape[i + 1] }, () => Array.from({ length: shape[i] }, () => randn() * s)); this.W.push(W); this.b.push(new Array(shape[i + 1]).fill(0)); this.m1W.push(W.map((r) => r.map(() => 0))); this.m2W.push(W.map((r) => r.map(() => 0))); this.m1b.push(new Array(shape[i + 1]).fill(0)); this.m2b.push(new Array(shape[i + 1]).fill(0)); } } forward(x) { this.a = [x]; for (let l = 0; l < this.W.length; l++) { const n = this.W[l].length; const z = new Array(n).fill(0); const prev = this.a[l]; for (let j = 0; j < n; j++) { let s = this.b[l][j]; const row = this.W[l][j]; for (let k = 0; k < prev.length; k++) s += row[k] * prev[k]; z[j] = s; } const last = l === this.W.length - 1; this.a.push(last ? z.map(sig) : z.map(tanh)); } return this.a[this.a.length - 1]; } trainSample(x, y, lr) { this.t += 1; const b1 = 0.9, b2 = 0.999, eps = 1e-8; const bc1 = 1 - Math.pow(b1, this.t); const bc2 = 1 - Math.pow(b2, this.t); const out = this.forward(x)[0]; let d = [out - y]; // gradient dL/dz van de output (sigmoïde + cross-entropy) for (let l = this.W.length - 1; l >= 0; l--) { const aPrev = this.a[l]; const Wl = this.W[l]; for (let j = 0; j < Wl.length; j++) { const gb = d[j]; this.m1b[l][j] = b1 * this.m1b[l][j] + (1 - b1) * gb; this.m2b[l][j] = b2 * this.m2b[l][j] + (1 - b2) * gb * gb; this.b[l][j] -= lr * (this.m1b[l][j] / bc1) / (Math.sqrt(this.m2b[l][j] / bc2) + eps); for (let k = 0; k < aPrev.length; k++) { const gw = gb * aPrev[k]; this.m1W[l][j][k] = b1 * this.m1W[l][j][k] + (1 - b1) * gw; this.m2W[l][j][k] = b2 * this.m2W[l][j][k] + (1 - b2) * gw * gw; Wl[j][k] -= lr * (this.m1W[l][j][k] / bc1) / (Math.sqrt(this.m2W[l][j][k] / bc2) + eps); } } if (l > 0) { const dNext = new Array(this.a[l].length).fill(0); for (let k = 0; k < this.a[l].length; k++) { let acc = 0; for (let j = 0; j < Wl.length; j++) acc += Wl[j][k] * d[j]; const a = this.a[l][k]; dNext[k] = acc * (1 - a * a); // tanh' = 1 - a² } d = dNext; } } return -(y * Math.log(out + 1e-9) + (1 - y) * Math.log(1 - out + 1e-9)); } predict(x) { return this.forward(x)[0]; } } function makeDataset(name) { const N = name === 'SPIRAAL' ? 420 : 320; const pts = []; if (name === 'XOR') { for (let i = 0; i < N; i++) { const x = Math.random() * 2 - 1; const y = Math.random() * 2 - 1; pts.push({ x: [x, y], y: (x * y < 0) ? 1 : 0 }); } } else if (name === 'CIRKEL') { for (let i = 0; i < N; i++) { const x = Math.random() * 2 - 1; const y = Math.random() * 2 - 1; pts.push({ x: [x, y], y: (x * x + y * y < 0.64) ? 1 : 0 }); } } else { for (let i = 0; i < N; i++) { const arm = i < N / 2 ? 0 : 1; const frac = (i % (N / 2)) / (N / 2); const t = frac * Math.PI * 3.6; const r = 0.95 * frac; const a = t + arm * Math.PI; pts.push({ x: [r * Math.cos(a) + (Math.random() - 0.5) * 0.04, r * Math.sin(a) + (Math.random() - 0.5) * 0.04], y: arm }); } } return pts; } let nn = null, samples = [], indices = [], epochLoss = 0, epochs = 0, history = []; let nnRunning = true, datasetName = 'XOR'; function resetNetwork() { const h = datasetName === 'SPIRAAL' ? 30 : (datasetName === 'CIRKEL' ? 14 : 10); nn = new NeuralNet([2, h, 1]); samples = makeDataset(datasetName); indices = samples.map((_, i) => i); epochLoss = 0; epochs = 0; history = []; } function shuffle(arr) { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); const t = arr[i]; arr[i] = arr[j]; arr[j] = t; } } function evalAccuracy() { const t = makeDataset(datasetName); let correct = 0; const n = Math.min(200, t.length); for (let i = 0; i < n; i++) { if ((nn.predict(t[i].x) > 0.5 ? 1 : 0) === t[i].y) correct++; } return (correct / n) * 100; } const bThumb = document.createElement('canvas'); bThumb.width = 200; bThumb.height = 150; const bCtx = bThumb.getContext('2d'); const C0 = [15, 23, 42]; const C1 = [251, 191, 36]; const C2 = [34, 211, 238]; function paintBoundary() { const img = bCtx.createImageData(200, 150); const data = img.data; for (let py = 0; py < 150; py++) { const y = ((149 - py) / 149) * 2 - 1; for (let px = 0; px < 200; px++) { const x = (px / 199) * 2 - 1; const p = nn.predict([x, y]); let r = C0[0] + (C1[0] - C0[0]) * p; let g = C0[1] + (C1[1] - C0[1]) * p; let b = C0[2] + (C1[2] - C0[2]) * p; const near = Math.abs(p - 0.5); if (near < 0.07) { const w = 1 - near / 0.07; r += (C2[0] - r) * w; g += (C2[1] - g) * w; b += (C2[2] - b) * w; } const i = (py * 200 + px) * 4; data[i] = r; data[i + 1] = g; data[i + 2] = b; data[i + 3] = 255; } } bCtx.putImageData(img, 0, 0); } function paintPoints() { for (const s of samples) { const sx = ((s.x[0] + 1) / 2) * nnCanvas.width; const sy = (1 - (s.x[1] + 1) / 2) * nnCanvas.height; nnCtx.beginPath(); nnCtx.arc(sx, sy, 3.2, 0, Math.PI * 2); nnCtx.fillStyle = s.y === 1 ? '#fbbf24' : '#7dd3fc'; nnCtx.fill(); nnCtx.strokeStyle = 'rgba(255,255,255,0.35)'; nnCtx.lineWidth = 0.8; nnCtx.stroke(); } } function paintLoss() { const w = lossCanvas.width, h = lossCanvas.height; lossCtx.fillStyle = '#05070d'; lossCtx.fillRect(0, 0, w, h); if (history.length < 2) return; const top = Math.max(...history.slice(0, 40), 1); const bottom = Math.max(0, Math.min(...history) * 0.85); const range = (top - bottom) || 1; const pad = 12; lossCtx.strokeStyle = '#34d399'; lossCtx.lineWidth = 2; lossCtx.beginPath(); for (let i = 0; i < history.length; i++) { const px = pad + (i / (history.length - 1)) * (w - 2 * pad); const py = pad + (1 - (history[i] - bottom) / range) * (h - 2 * pad); if (i === 0) lossCtx.moveTo(px, py); else lossCtx.lineTo(px, py); } lossCtx.stroke(); lossCtx.fillStyle = '#8ea3c0'; lossCtx.font = '12px Menlo, Consolas, monospace'; lossCtx.fillText('loss ' + epochLoss.toFixed(4), 14, h - 10); } function updateStats() { nnStats.textContent = 'epoch ' + epochs.toLocaleString('nl-NL') + ' · loss ' + epochLoss.toFixed(4) + ' · accuratie ' + evalAccuracy().toFixed(1) + '% · ' + (nnRunning ? 'trainen…' : 'gepauzeerd'); } function nnLoop() { if (nnRunning && nn) { const e = Math.max(1, Math.floor(Number(nnSpeed.value))); for (let ep = 0; ep < e; ep++) { shuffle(indices); let sum = 0; for (const i of indices) sum += nn.trainSample(samples[i].x, samples[i].y, 0.05); epochLoss = sum / indices.length; epochs++; } history.push(epochLoss); if (history.length > 420) history.shift(); } paintBoundary(); nnCtx.clearRect(0, 0, nnCanvas.width, nnCanvas.height); nnCtx.drawImage(bThumb, 0, 0, nnCanvas.width, nnCanvas.height); paintPoints(); paintLoss(); updateStats(); requestAnimationFrame(nnLoop); } function bootNetwork() { resetNetwork(); nnSpeed.value = 6; document.querySelectorAll('.dataset').forEach((b) => { b.addEventListener('click', () => { document.querySelectorAll('.dataset').forEach((x) => x.classList.remove('active')); b.classList.add('active'); datasetName = b.dataset.set; resetNetwork(); }); }); nnPauseBtn.addEventListener('click', () => { nnRunning = !nnRunning; nnPauseBtn.textContent = nnRunning ? '⏸ Pauze' : '▶ Doorgaan'; }); nnResetBtn.addEventListener('click', () => resetNetwork()); nnLoop(); } /* ========================================================= * C. Generative art — flow field dat reageert op je muis * ========================================================= */ const artCanvas = document.getElementById('artCanvas'); const artCtx = artCanvas.getContext('2d'); const artNewBtn = document.getElementById('artNew'); const artPauseBtn = document.getElementById('artPause'); const artPngBtn = document.getElementById('artPng'); const PALETTES = [ ['#0b1020', '#22d3ee', '#a78bfa'], ['#0b1020', '#fbbf24', '#f43f5e'], ['#0b1020', '#34d399', '#22d3ee'], ['#120b20', '#e879f9', '#60a5fa'], ['#0b1620', '#f59e0b', '#ef4444'] ]; let particles = [], artRunning = true, artT = 0; let artW = 0, artH = 0, mx = -9999, my = -9999; let palIdx = 0, fieldSeed = 12.3; function hexToRgb(h) { const n = parseInt(h.slice(1), 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; } function sizeArtCanvas() { const wrap = artCanvas.parentElement; artW = wrap.clientWidth || 800; artH = Math.round(artW * 0.5); artCanvas.width = artW; artCanvas.height = artH; } function fieldAngle(nx, ny) { const s = fieldSeed; const t = artT * 0.06; return Math.sin(nx * 2.4 + s) * Math.cos(ny * 3.1 - s * 0.3 + t) + Math.sin(nx * 1.2 + ny * 1.7 - s + t) * 0.9 + Math.cos(nx * 0.6 - ny * 2.2 + s * 0.7 + t) * 0.5; } function initArt() { fieldSeed = Math.random() * 1000; palIdx = Math.floor(Math.random() * PALETTES.length); sizeArtCanvas(); const n = Math.max(160, Math.min(760, Math.round((artW * artH) / 1300))); particles = []; for (let i = 0; i < n; i++) { particles.push({ x: Math.random() * artW, y: Math.random() * artH }); } artCtx.clearRect(0, 0, artW, artH); } function drawParticles(ctx) { const c2 = hexToRgb(PALETTES[palIdx][1]); const c3 = hexToRgb(PALETTES[palIdx][2]); for (const p of particles) { const ang = fieldAngle(p.x / artW, p.y / artH) * Math.PI * 0.9; const vx = Math.cos(ang), vy = Math.sin(ang); const oldX = p.x, oldY = p.y; let pushX = 0, pushY = 0; if (mx > -1000) { const dx = p.x - mx, dy = p.y - my; const dist = Math.hypot(dx, dy); if (dist < 120 && dist > 0.001) { const f = (120 - dist) * 0.06; pushX = (dx / dist) * f; pushY = (dy / dist) * f; } } p.x += vx * 2.2 + pushX; p.y += vy * 2.2 + pushY; if (p.x < 0) p.x += artW; else if (p.x > artW) p.x -= artW; if (p.y < 0) p.y += artH; else if (p.y > artH) p.y -= artH; const mix = 0.5 + 0.5 * Math.sin(ang * 3 + p.x * 0.01); const r = c2[0] + (c3[0] - c2[0]) * mix; const g = c2[1] + (c3[1] - c2[1]) * mix; const b = c2[2] + (c3[2] - c2[2]) * mix; ctx.strokeStyle = 'rgba(' + (r | 0) + ',' + (g | 0) + ',' + (b | 0) + ',0.85)'; ctx.lineWidth = 1.4; ctx.beginPath(); ctx.moveTo(oldX, oldY); ctx.lineTo(p.x, p.y); ctx.stroke(); } } function artFrame() { if (artRunning) { artT++; artCtx.fillStyle = 'rgba(7,11,22,0.10)'; artCtx.fillRect(0, 0, artW, artH); artCtx.globalCompositeOperation = 'lighter'; drawParticles(artCtx); artCtx.globalCompositeOperation = 'source-over'; } requestAnimationFrame(artFrame); } function exportPng() { const c = document.createElement('canvas'); c.width = artW; c.height = artH; const cx = c.getContext('2d'); cx.fillStyle = '#070b16'; cx.fillRect(0, 0, artW, artH); cx.globalCompositeOperation = 'lighter'; drawParticles(cx); const a = document.createElement('a'); a.download = 'ai-lab-art.png'; a.href = c.toDataURL('image/png'); a.click(); } function bootArt() { initArt(); artNewBtn.addEventListener('click', () => initArt()); artPauseBtn.addEventListener('click', () => { artRunning = !artRunning; artPauseBtn.textContent = artRunning ? '⏸ Pauze' : '▶ Afspelen'; }); artPngBtn.addEventListener('click', exportPng); artCanvas.addEventListener('mousemove', (e) => { const r = artCanvas.getBoundingClientRect(); mx = e.clientX - r.left; my = e.clientY - r.top; }); artCanvas.addEventListener('mouseleave', () => { mx = -9999; my = -9999; }); window.addEventListener('resize', () => sizeArtCanvas()); artFrame(); } /* ========================================================= * Boot — start de drie demo-blokken * ========================================================= */ bootTerminal(); bootNetwork(); bootArt();