ok
This commit is contained in:
@@ -0,0 +1,490 @@
|
||||
/*
|
||||
* 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();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AI Lab — de kracht van een AI-agent</title>
|
||||
<meta name="description" content="AI Lab: een zelfstandige demo met een live lerend neuraal netwerk, generative art en het verhaal van de AI die dit bouwde." />
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="hero container">
|
||||
<p class="hero-badge">100% in de browser · geen internet · geen installatie</p>
|
||||
<h1 class="hero-title">AI <span>Lab</span></h1>
|
||||
<p class="hero-sub">Alles op deze pagina is door een <strong>AI-agent</strong> geprogrammeerd — in één sessie, zonder voorbeeldcode. Bewonder, speel, en test zelf waartoe een AI in staat is.</p>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
|
||||
<!-- Terminal -->
|
||||
<section class="panel" id="terminal">
|
||||
<div class="panel-head">
|
||||
<span class="dot"></span>
|
||||
<h2>Cline — het verhaal</h2>
|
||||
<span class="tag">terminal</span>
|
||||
</div>
|
||||
<div class="term-body" id="termBody" aria-live="polite"></div>
|
||||
<div class="term-foot">
|
||||
<button class="btn ghost" id="termReplay" style="display:none">▶ Opnieuw afspelen</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Network -->
|
||||
<section class="panel" id="network">
|
||||
<div class="panel-head">
|
||||
<span class="dot cyan"></span>
|
||||
<h2>Neuraal netwerk dat écht leert</h2>
|
||||
<span class="tag">JS · backpropagation</span>
|
||||
</div>
|
||||
<div class="nn-stats" id="nnStats">initialiseren…</div>
|
||||
<div class="nn-grid">
|
||||
<figure class="canvas-frame">
|
||||
<figcaption>De beslisgrens wordt live scherper</figcaption>
|
||||
<canvas id="nnCanvas" width="460" height="320"></canvas>
|
||||
</figure>
|
||||
<figure class="canvas-frame">
|
||||
<figcaption>De fout (loss) daalt</figcaption>
|
||||
<canvas id="lossCanvas" width="460" height="320"></canvas>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="nn-controls">
|
||||
<div class="btn-row">
|
||||
<button class="btn dataset active" data-set="XOR">XOR</button>
|
||||
<button class="btn dataset" data-set="CIRKEL">Cirkel</button>
|
||||
<button class="btn dataset" data-set="SPIRAAL">Spiraal</button>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn accent" id="nnPause">⏸ Pauze</button>
|
||||
<button class="btn" id="nnReset">↻ Herstart</button>
|
||||
</div>
|
||||
<label class="slider">
|
||||
<span>Snelheid</span>
|
||||
<input type="range" id="nnSpeed" min="1" max="300" value="40" />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Art -->
|
||||
<section class="panel" id="art">
|
||||
<div class="panel-head">
|
||||
<span class="dot violet"></span>
|
||||
<h2>Generative art</h2>
|
||||
<span class="tag">canvas · flow field</span>
|
||||
</div>
|
||||
<div class="art-wrap">
|
||||
<canvas id="artCanvas"></canvas>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn accent" id="artNew">✦ Nieuw kunstwerk</button>
|
||||
<button class="btn" id="artPause">⏸ Pauze</button>
|
||||
<button class="btn" id="artPng">⬇ Opslaan als PNG</button>
|
||||
</div>
|
||||
<p class="hint">Beweeg met je muis over het kunstwerk — de deeltjes reageren.</p>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<footer class="footer container">
|
||||
<p>AI Lab · gebouwd door een AI-agent (Cline). Elke pixel en elk cijfer hierboven is runtime gegenereerd.</p>
|
||||
</footer>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+409
@@ -0,0 +1,409 @@
|
||||
/* ===== AI Lab — dark thema ===== */
|
||||
:root {
|
||||
--bg: #070b16;
|
||||
--panel: #0e1526;
|
||||
--panel2: #131b30;
|
||||
--line: #223049;
|
||||
--text: #e6ecf7;
|
||||
--muted: #8ea3c0;
|
||||
--cyan: #22d3ee;
|
||||
--violet: #a78bfa;
|
||||
--amber: #fbbf24;
|
||||
--green: #34d399;
|
||||
--red: #f43f5e;
|
||||
--mono: "Cascadia Code", "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Segoe UI", system-ui, -apple-system, Roboto, Arial, sans-serif;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -2;
|
||||
background:
|
||||
radial-gradient(60% 40% at 15% 0%, rgba(34, 211, 238, 0.10), transparent 60%),
|
||||
radial-gradient(50% 40% at 90% 10%, rgba(167, 139, 250, 0.12), transparent 60%),
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--cyan);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1060px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* ===== Hero ===== */
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 64px 20px 24px;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
display: inline-block;
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
padding: 6px 14px;
|
||||
background: rgba(19, 27, 48, 0.6);
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
margin: 0;
|
||||
font-size: clamp(3rem, 10vw, 5.5rem);
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
background: linear-gradient(90deg, #22d3ee, #a78bfa 55%, #fbbf24);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.hero-sub {
|
||||
max-width: 640px;
|
||||
margin: 18px auto 0;
|
||||
color: var(--muted);
|
||||
font-size: 1.08rem;
|
||||
}
|
||||
|
||||
.hero-sub strong {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ===== Panels ===== */
|
||||
.panel {
|
||||
background: linear-gradient(180deg, var(--panel), rgba(14, 21, 38, 0.5));
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
padding: 22px;
|
||||
margin: 26px 0;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.panel-head h2 {
|
||||
margin: 0;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tag {
|
||||
margin-left: auto;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.72rem;
|
||||
color: var(--muted);
|
||||
background: var(--panel2);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
padding: 3px 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--amber);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dot.cyan {
|
||||
background: var(--cyan);
|
||||
}
|
||||
|
||||
.dot.violet {
|
||||
background: var(--violet);
|
||||
}
|
||||
|
||||
/* ===== Terminal ===== */
|
||||
.term-body {
|
||||
background: #05070d;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.86rem;
|
||||
line-height: 1.65;
|
||||
height: 320px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.term-spacer {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.term-prompt {
|
||||
color: #b9cdf5;
|
||||
}
|
||||
|
||||
.term-plan {
|
||||
color: #c9bcf8;
|
||||
}
|
||||
|
||||
.term-tool {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.term-ok {
|
||||
color: var(--green);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.term-body::after {
|
||||
content: "▍";
|
||||
color: var(--cyan);
|
||||
animation: blink 1s steps(2) infinite;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 49% { opacity: 1; }
|
||||
50%, 100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.term-foot {
|
||||
margin-top: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* ===== Knoppen ===== */
|
||||
.btn {
|
||||
font-family: inherit;
|
||||
font-size: 0.92rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
background: var(--panel2);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 9px;
|
||||
padding: 9px 16px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s ease, border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
border-color: var(--cyan);
|
||||
color: #fff;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn.accent {
|
||||
background: linear-gradient(90deg, #164e63, #155e75);
|
||||
border-color: rgba(34, 211, 238, 0.4);
|
||||
color: #a5f3fc;
|
||||
}
|
||||
|
||||
.btn.accent:hover {
|
||||
background: linear-gradient(90deg, #0e7490, #155e75);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn.ghost {
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.btn.active {
|
||||
background: var(--cyan);
|
||||
color: #062630;
|
||||
border-color: var(--cyan);
|
||||
}
|
||||
|
||||
.btn-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
margin: 12px 0 0;
|
||||
}
|
||||
|
||||
/* ===== Netwerk ===== */
|
||||
.nn-stats {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.86rem;
|
||||
color: var(--green);
|
||||
background: var(--panel2);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 14px;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nn-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.15fr 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.canvas-frame {
|
||||
margin: 0;
|
||||
background: var(--panel2);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.canvas-frame figcaption {
|
||||
font-size: 0.78rem;
|
||||
color: var(--muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.canvas-frame canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 6px;
|
||||
background: #05070d;
|
||||
}
|
||||
|
||||
.nn-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.slider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.slider input[type="range"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 150px;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: var(--line);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.slider input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: var(--cyan);
|
||||
border: 2px solid #062630;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slider input[type="range"]::-moz-range-thumb {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: var(--cyan);
|
||||
border: 2px solid #062630;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ===== Art ===== */
|
||||
.art-wrap {
|
||||
background: var(--panel2);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.art-wrap canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
background: #05070d;
|
||||
}
|
||||
|
||||
/* ===== Footer ===== */
|
||||
.footer {
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
padding: 34px 20px 44px;
|
||||
}
|
||||
|
||||
.footer p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.footer::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 60px;
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(90deg, var(--cyan), var(--violet));
|
||||
margin: 0 auto 18px;
|
||||
}
|
||||
|
||||
/* ===== Responsive ===== */
|
||||
@media (max-width: 820px) {
|
||||
.nn-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.term-body {
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.nn-controls {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.hero {
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
'use strict';
|
||||
// Vluchtige Node-test: laadt app.js met een minimale DOM-stub en
|
||||
// controleert of het neuraal netwerk echt leert (XOR / cirkel / spiraal).
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8');
|
||||
|
||||
function makeCtx() {
|
||||
const data = new Uint8ClampedArray(200 * 150 * 4);
|
||||
return {
|
||||
createImageData: () => ({ width: 200, height: 150, data }),
|
||||
putImageData: () => {}, clearRect: () => {}, drawImage: () => {},
|
||||
fillRect: () => {}, fillText: () => {}, beginPath: () => {}, arc: () => {},
|
||||
fill: () => {}, stroke: () => {}, moveTo: () => {}, lineTo: () => {}, strokeRect: () => {}
|
||||
};
|
||||
}
|
||||
function makeEl() {
|
||||
return {
|
||||
width: 460, height: 320, style: {}, textContent: '', innerHTML: '',
|
||||
scrollTop: 0, scrollHeight: 0,
|
||||
classList: { add() {}, remove() {}, toggle() { return false; } },
|
||||
appendChild() {}, addEventListener() {}, setAttribute() {}, click() {},
|
||||
getContext: makeCtx, parentElement: { clientWidth: 800 }, dataset: {}, value: '6',
|
||||
toDataURL: () => 'data:image/png;base64,xx'
|
||||
};
|
||||
}
|
||||
const documentStub = {
|
||||
getElementById: () => makeEl(),
|
||||
querySelectorAll: () => [],
|
||||
createElement: () => makeEl()
|
||||
};
|
||||
const rafNoop = () => {};
|
||||
|
||||
const code = src + '\n;globalThis.__NN = NeuralNet; globalThis.__makeDataset = makeDataset;';
|
||||
new Function('document', 'requestAnimationFrame', 'window', 'setTimeout', code)(
|
||||
documentStub, rafNoop, { addEventListener() {} }, setTimeout
|
||||
);
|
||||
|
||||
const NeuralNet = globalThis.__NN;
|
||||
const makeDataset = globalThis.__makeDataset;
|
||||
|
||||
function trainOn(name, epochs, lr, hidden) {
|
||||
const net = new NeuralNet([2, hidden, 1]);
|
||||
const samples = makeDataset(name);
|
||||
const idx = samples.map((_, i) => i);
|
||||
let last = null;
|
||||
for (let e = 0; e < epochs; e++) {
|
||||
for (let i = idx.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
const t = idx[i]; idx[i] = idx[j]; idx[j] = t;
|
||||
}
|
||||
let sum = 0;
|
||||
for (const i of idx) sum += net.trainSample(samples[i].x, samples[i].y, lr);
|
||||
last = sum / idx.length;
|
||||
}
|
||||
const test = makeDataset(name);
|
||||
const n = Math.min(200, test.length);
|
||||
let correct = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
if ((net.predict(test[i].x) > 0.5 ? 1 : 0) === test[i].y) correct++;
|
||||
}
|
||||
return { acc: (correct / n) * 100, loss: last };
|
||||
}
|
||||
|
||||
// Finale controle — zelfde instelling als app.js (Adam, lr 0.05)
|
||||
function trial(name, hidden, epochs) {
|
||||
const res = [];
|
||||
for (let t = 0; t < 6; t++) res.push(trainOn(name, epochs, 0.05, hidden));
|
||||
const worst = Math.min(...res.map((a) => a.acc));
|
||||
return { worst, raw: res.map((a) => a.acc.toFixed(1) + '%').join(' ') };
|
||||
}
|
||||
|
||||
const xor0 = trainOn('XOR', 0, 0.05, 10);
|
||||
const xor = trial('XOR', 10, 250);
|
||||
const cir = trial('CIRKEL', 14, 400);
|
||||
const spi = trial('SPIRAAL', 30, 1400);
|
||||
|
||||
console.log('XOR 0ep :', xor0.acc.toFixed(1) + '%');
|
||||
console.log('XOR 250ep :', xor.raw, ' (slechtste ' + xor.worst.toFixed(1) + '%)');
|
||||
console.log('CIRKEL 400ep :', cir.raw, ' (slechtste ' + cir.worst.toFixed(1) + '%)');
|
||||
console.log('SPIRAAL 1400ep :', spi.raw, ' (slechtste ' + spi.worst.toFixed(1) + '%)');
|
||||
|
||||
let fail = 0;
|
||||
if (xor0.acc < 35 || xor0.acc > 70) { console.error('FAIL: XOR ongetraind moet ~50% zijn'); fail = 1; }
|
||||
if (xor.worst < 85) { console.error('FAIL: XOR convergerde niet'); fail = 1; }
|
||||
if (cir.worst < 90) { console.error('FAIL: Cirkel convergerde niet'); fail = 1; }
|
||||
if (spi.worst < 85) { console.error('FAIL: Spiraal convergerde niet'); fail = 1; }
|
||||
|
||||
if (fail) {
|
||||
console.error('VALIDATIE MISLUKT');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('PASS: alle drie datasets leren betrouwbaar met Adam (lr 0.05).');
|
||||
process.exit(0);
|
||||
|
||||
if (fail) process.exit(1);
|
||||
console.log('PASS: app.js laadt schoon én het netwerk leert alle drie datasets.');
|
||||
process.exit(0);
|
||||
Reference in New Issue
Block a user