/* eslint-disable */
const { useState, useMemo, useRef, useEffect, useCallback } = React;

/* ============================================================
   POMOĆNE FUNKCIJE
   ============================================================ */
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
// brojač siguran od dijeljenja s nulom: null → "—"
const fmt = (v, d = 2) => (v === null || v === undefined || Number.isNaN(v)) ? "—" : v.toFixed(d);
// numerički stabilna sigmoida (bez Infinity)
const sigm = (z) => z >= 0 ? 1 / (1 + Math.exp(-z)) : (e => e / (1 + e))(Math.exp(z));

/* ============================================================
   PODACI ZA EVALUACIJU (deterministički generirani, [actual, score])
   ============================================================ */
const SCEN_DATA = {
  prijevara: [[0,0.01],[0,0.06],[0,0.07],[0,0.09],[0,0.12],[0,0.14],[0,0.17],[0,0.17],[0,0.18],[0,0.22],[0,0.24],[0,0.25],[0,0.25],[0,0.26],[0,0.27],[0,0.28],[0,0.29],[0,0.29],[0,0.29],[0,0.3],[0,0.32],[0,0.32],[0,0.32],[0,0.33],[0,0.33],[0,0.34],[0,0.36],[0,0.38],[0,0.39],[0,0.41],[0,0.41],[0,0.41],[0,0.41],[0,0.43],[0,0.46],[0,0.48],[1,0.49],[1,0.49],[1,0.5],[1,0.51],[0,0.55],[0,0.6],[1,0.62],[1,0.69],[0,0.71],[1,0.76],[1,0.78],[0,0.83],[1,0.86],[1,0.88]],
  bolest: [[0,0.05],[0,0.08],[0,0.17],[0,0.18],[0,0.19],[0,0.19],[0,0.22],[0,0.25],[0,0.25],[0,0.29],[0,0.29],[0,0.31],[0,0.33],[0,0.33],[0,0.33],[0,0.35],[0,0.35],[0,0.35],[0,0.36],[0,0.38],[0,0.39],[0,0.39],[0,0.46],[0,0.48],[1,0.49],[0,0.5],[1,0.53],[0,0.53],[0,0.53],[0,0.53],[1,0.54],[0,0.57],[1,0.58],[1,0.6],[0,0.6],[0,0.6],[0,0.61],[0,0.64],[1,0.65],[0,0.65],[0,0.67],[0,0.67],[1,0.7],[1,0.71],[1,0.71],[1,0.71],[1,0.76],[0,0.78],[0,0.83],[1,0.99]],
  spam: [[0,0.07],[0,0.11],[0,0.17],[0,0.19],[0,0.2],[0,0.22],[0,0.23],[0,0.25],[0,0.26],[0,0.26],[0,0.28],[0,0.31],[0,0.31],[0,0.31],[0,0.31],[0,0.34],[0,0.34],[0,0.34],[0,0.36],[0,0.38],[0,0.39],[1,0.42],[0,0.42],[0,0.44],[0,0.53],[1,0.54],[1,0.54],[1,0.6],[1,0.6],[1,0.61],[1,0.62],[0,0.62],[1,0.63],[1,0.64],[1,0.67],[1,0.68],[1,0.7],[1,0.71],[1,0.73],[1,0.73],[1,0.74],[1,0.75],[1,0.77],[1,0.78],[1,0.83],[1,0.86],[1,0.89],[1,0.9],[1,0.94],[1,0.97]],
};
const SCEN = {
  prijevara: {
    naziv: "Otkrivanje prijevare", poz: "prijevara", neg: "poštena transakcija",
    key: "recall",
    story: "Promašena prijevara (FN) izravno košta banku i klijenta. Lažna uzbuna (FP) znači samo jedan dodatni telefonski poziv za potvrdu. Zato želimo uhvatiti što više prijevara — visok recall — pa makar uz nešto lažnih uzbuna.",
  },
  bolest: {
    naziv: "Probir na bolest", poz: "bolestan", neg: "zdrav",
    key: "recall",
    story: "U probiru ne smijemo propustiti bolesnu osobu (FN je najgori ishod). Lažna uzbuna (FP) znači samo dodatnu, precizniju pretragu. Zato ciljamo vrlo visok recall; preciznost je drugotna jer slijedi potvrdni test.",
  },
  spam: {
    naziv: "Spam filter", poz: "spam", neg: "važna poruka",
    key: "precision",
    story: "Ovdje je lažna uzbuna skupa: važna poruka završi u spamu i korisnik je nikad ne vidi (FP). Propušteni spam (FN) je samo smetnja. Zato želimo visoku preciznost — kad kažemo „spam”, moramo biti sigurni.",
  },
};
const KEY_LABEL = { recall: "Recall (odziv)", precision: "Precision (preciznost)" };

/* metrike iz populacije i praga */
function confusion(cases, t) {
  let TP = 0, FP = 0, FN = 0, TN = 0;
  for (const [actual, score] of cases) {
    const pred = score >= t ? 1 : 0;
    if (actual === 1 && pred === 1) TP++;
    else if (actual === 0 && pred === 1) FP++;
    else if (actual === 1 && pred === 0) FN++;
    else TN++;
  }
  const n = TP + FP + FN + TN;
  const acc = n ? (TP + TN) / n : null;
  const prec = (TP + FP) ? TP / (TP + FP) : null;
  const rec = (TP + FN) ? TP / (TP + FN) : null;
  const f1 = (prec !== null && rec !== null && prec + rec > 0) ? 2 * prec * rec / (prec + rec) : null;
  const tpr = (TP + FN) ? TP / (TP + FN) : null;
  const fpr = (FP + TN) ? FP / (FP + TN) : null;
  return { TP, FP, FN, TN, acc, prec, rec, f1, tpr, fpr };
}
/* ROC točke + AUC iz score-ova */
function rocCurve(cases) {
  const ts = Array.from(new Set(cases.map(c => c[1]))).sort((a, b) => b - a);
  const pts = [{ fpr: 0, tpr: 0, t: 1.01 }];
  for (const t of ts) { const m = confusion(cases, t); pts.push({ fpr: m.fpr ?? 0, tpr: m.tpr ?? 0, t }); }
  pts.push({ fpr: 1, tpr: 1, t: -0.01 });
  pts.sort((a, b) => a.fpr - b.fpr || a.tpr - b.tpr);
  let auc = 0;
  for (let i = 1; i < pts.length; i++) auc += (pts[i].fpr - pts[i - 1].fpr) * (pts[i].tpr + pts[i - 1].tpr) / 2;
  return { pts, auc };
}

/* ============================================================
   WIDGET 1 — IGRAONICA NEURONA (kišobran)
   ============================================================ */
function NeuronPlayground() {
  const cv = useRef(null);
  const [x, setX] = useState([0.70, 0.40, 0.80]);
  const [w, setW] = useState([0.5, 0.3, 1.2]);
  const [b, setB] = useState(-1.0);
  const [act, setAct] = useState("sigmoid");

  const labels = ["Tamni oblaci", "Vlažnost zraka", "Prognoza kiše"];
  const z = w[0] * x[0] + w[1] * x[1] + w[2] * x[2] + b;
  const a = act === "step" ? (z >= 0 ? 1 : 0) : act === "relu" ? Math.max(0, z) : sigm(z);
  const decision = act === "relu" ? (a > 0) : (a >= 0.5);
  const contrib = [w[0] * x[0], w[1] * x[1], w[2] * x[2]];

  useEffect(() => {
    const ctx = cv.current.getContext("2d");
    const W = 720, H = 360;
    ctx.clearRect(0, 0, W, H);
    const inX = 178, nodeX = 390, nodeY = 150, lampX = 610, lampY = 120;
    const inY = [70, 150, 230];
    // edges
    for (let i = 0; i < 3; i++) {
      const c = contrib[i];
      ctx.strokeStyle = w[i] >= 0 ? "#2a5d57" : "#8a3a1f";
      ctx.lineWidth = 1 + Math.min(8, Math.abs(w[i]) * 3.2);
      ctx.beginPath(); ctx.moveTo(inX + 26, inY[i]); ctx.lineTo(nodeX - 42, nodeY); ctx.stroke();
      ctx.fillStyle = w[i] >= 0 ? "#2a5d57" : "#8a3a1f";
      ctx.font = "600 12px JetBrains Mono"; ctx.textAlign = "center";
      ctx.fillText((c >= 0 ? "+" : "") + c.toFixed(2), (inX + 26 + nodeX - 42) / 2, (inY[i] + nodeY) / 2 - 6);
    }
    // input nodes
    for (let i = 0; i < 3; i++) {
      ctx.fillStyle = "#fbf7ec"; ctx.strokeStyle = "#6f6754"; ctx.lineWidth = 1.5;
      ctx.beginPath(); ctx.arc(inX, inY[i], 24, 0, 7); ctx.fill(); ctx.stroke();
      ctx.fillStyle = "#1a1814"; ctx.font = "600 15px Source Serif 4"; ctx.textAlign = "center";
      ctx.fillText(x[i].toFixed(2), inX, inY[i] + 5);
      ctx.fillStyle = "#3a352c"; ctx.font = "12px Manrope"; ctx.textAlign = "right";
      ctx.fillText(labels[i], inX - 32, inY[i] + 4);
    }
    // neuron
    ctx.fillStyle = "#efe3c7"; ctx.strokeStyle = "#6b4f1d"; ctx.lineWidth = 2.5;
    ctx.beginPath(); ctx.arc(nodeX, nodeY, 42, 0, 7); ctx.fill(); ctx.stroke();
    ctx.fillStyle = "#3a352c"; ctx.font = "11px JetBrains Mono"; ctx.textAlign = "center";
    ctx.fillText("z = Σwx+b", nodeX, nodeY - 8);
    ctx.fillStyle = "#1a1814"; ctx.font = "600 20px Source Serif 4";
    ctx.fillText(z.toFixed(2), nodeX, nodeY + 16);
    // edge to lamp
    ctx.strokeStyle = "#b8ab8e"; ctx.lineWidth = 2.5;
    ctx.beginPath(); ctx.moveTo(nodeX + 42, nodeY); ctx.lineTo(lampX - 38, lampY); ctx.stroke();
    // lamp
    const fill = act === "relu" ? clamp(a / 3, 0, 1) : a;
    ctx.fillStyle = `rgba(201,165,102,${0.15 + 0.85 * fill})`;
    ctx.strokeStyle = decision ? "#9a7a16" : "#b8ab8e"; ctx.lineWidth = 3;
    ctx.beginPath(); ctx.arc(lampX, lampY, 36, 0, 7); ctx.fill(); ctx.stroke();
    ctx.fillStyle = "#1a1814"; ctx.font = "600 18px Source Serif 4"; ctx.textAlign = "center";
    ctx.fillText(a.toFixed(2), lampX, lampY + 6);
    ctx.font = "600 15px Manrope"; ctx.fillStyle = decision ? "#6b4f1d" : "#6f6754";
    ctx.fillText(decision ? "PONESI KIŠOBRAN ☂" : "OSTAVI GA", lampX, lampY + 60);
    // contribution bar
    const barY = 300, cx = W / 2, half = 280;
    const R = Math.max(2.5, Math.abs(z) + 0.6, Math.abs(b) + 0.6);
    const X = v => cx + v / R * half;
    ctx.fillStyle = "#6f6754"; ctx.font = "11px JetBrains Mono"; ctx.textAlign = "left";
    ctx.fillText("doprinosi (zbroj → z):", cx - half, barY - 26);
    let cum = 0;
    const segs = [contrib[0], contrib[1], contrib[2], b];
    for (const val of segs) {
      const x0 = X(cum), x1 = X(cum + val);
      ctx.fillStyle = val >= 0 ? "#2a5d57" : "#8a3a1f"; ctx.globalAlpha = 0.55;
      ctx.fillRect(Math.min(x0, x1), barY - 12, Math.abs(x1 - x0), 24);
      ctx.globalAlpha = 1;
      cum += val;
    }
    // zero line (threshold)
    ctx.strokeStyle = "#1a1814"; ctx.lineWidth = 2; ctx.setLineDash([4, 4]);
    ctx.beginPath(); ctx.moveTo(X(0), barY - 22); ctx.lineTo(X(0), barY + 22); ctx.stroke();
    ctx.setLineDash([]);
    ctx.fillStyle = "#1a1814"; ctx.font = "10px JetBrains Mono"; ctx.textAlign = "center";
    ctx.fillText("prag 0", X(0), barY + 36);
    // marker at z
    ctx.fillStyle = "#6b4f1d";
    ctx.beginPath(); ctx.moveTo(X(z), barY - 18); ctx.lineTo(X(z) - 5, barY - 26); ctx.lineTo(X(z) + 5, barY - 26); ctx.fill();
  }, [x, w, b, act, z, a, decision]);

  let hint = "Pomiči težine i sklonost (bias) pa gledaj kako se mijenja odluka. Zelena veza gura prema „da”, crvena prema „ne”.";
  if (w.every(v => Math.abs(v) < 0.05)) hint = "Sve težine su na nuli — neuron ignorira ulaze i odlučuje samo na temelju sklonosti (bias). Podigni neku težinu.";
  else if (act === "relu" && a > 1.2) hint = "ReLU nema gornju granicu pa izlaz raste neograničeno. Zato je za odluku „da/ne” praktičnija sigmoida koja daje broj između 0 i 1.";
  else if (act !== "relu" && Math.abs(a - 0.5) < 0.06) hint = "Model je neodlučan — z je tik uz prag. Mali pomak bilo kojeg klizača prevagnut će odluku.";
  else if (w.some(v => v < -0.05)) hint = "Crvena veza znači da taj ulaz gura odluku prema „ne”: što je njegova vrijednost veća, model je sigurniji da kišobran NE treba.";

  const wlab = ["Važnost oblaka", "Važnost vlažnosti", "Važnost prognoze"];
  return (
    <div className="widget">
      <span className="wlab">jedan neuron · „hoću li ponijeti kišobran?”</span>
      <canvas ref={cv} width="720" height="360"></canvas>
      <div className="ctrls">
        {labels.map((l, i) => (
          <div className="ctrl" key={"x" + i}>
            <label>{l}: <span className="val">{x[i].toFixed(2)}</span></label>
            <input type="range" min="0" max="1" step="0.05" value={x[i]}
              onChange={e => setX(p => p.map((v, j) => j === i ? +e.target.value : v))} />
          </div>
        ))}
      </div>
      <div className="ctrls" style={{ marginTop: 0 }}>
        {wlab.map((l, i) => (
          <div className="ctrl" key={"w" + i}>
            <label>{l} (w{i + 1}): <span className="val">{w[i].toFixed(1)}</span></label>
            <input type="range" min="-2" max="2" step="0.1" value={w[i]}
              onChange={e => setW(p => p.map((v, j) => j === i ? +e.target.value : v))} />
          </div>
        ))}
        <div className="ctrl">
          <label>Sklonost / prag (bias): <span className="val">{b.toFixed(1)}</span></label>
          <input type="range" min="-3" max="3" step="0.1" value={b} onChange={e => setB(+e.target.value)} />
          <span className="sub">negativno = lijenost, pozitivno = oprez</span>
        </div>
        <div className="ctrl" style={{ minWidth: "auto" }}>
          <label>Aktivacija</label>
          <div className="seg">
            {["step", "sigmoid", "relu"].map(k =>
              <button key={k} className={act === k ? "on" : ""} onClick={() => setAct(k)}>
                {k === "step" ? "Step" : k === "sigmoid" ? "Sigmoida" : "ReLU"}</button>)}
          </div>
        </div>
      </div>
      <div className="legend">
        <span><span className="dotc" style={{ background: "#2a5d57" }}></span> pozitivna težina (za)</span>
        <span><span className="dotc" style={{ background: "#8a3a1f" }}></span> negativna težina (protiv)</span>
      </div>
      <div className="hintline">{hint}</div>
      <div className="note" style={{ marginTop: 14 }}>
        <strong>Pokušaj ovo:</strong> (1) Učini model „lijenim” — spusti bias na ≈ −2,5 pa makar i uz oblake i prognozu kaže „ostavi ga”.
        (2) Neka <em>samo prognoza</em> odlučuje — postavi w1 = w2 = 0, a w3 visok, pa pomiči samo prognozu.
      </div>
    </div>
  );
}

/* ============================================================
   WIDGET 2 — TRENIRAJ MREŽU KORAK-PO-KORAK (XOR 2-2-1)
   ============================================================ */
const XOR_X = [[0, 0], [0, 1], [1, 0], [1, 1]];
const XOR_Y = [0, 1, 1, 0];
function freshNet(useDefault) {
  if (useDefault) return {
    W1: [[-0.7313, 0.6949], [0.5275, -0.4899]], b1: [-0.0091, -0.101],
    W2: [0.3032, 0.5774], b2: -0.8123,
  };
  const r = () => (Math.random() * 2 - 1);
  return { W1: [[r(), r()], [r(), r()]], b1: [r(), r()], W2: [r(), r()], b2: r() };
}
function forwardOne(net, x) {
  const hin = [net.W1[0][0] * x[0] + net.W1[1][0] * x[1] + net.b1[0],
               net.W1[0][1] * x[0] + net.W1[1][1] * x[1] + net.b1[1]];
  const h = [sigm(hin[0]), sigm(hin[1])];
  const oin = net.W2[0] * h[0] + net.W2[1] * h[1] + net.b2;
  const o = sigm(oin);
  return { h, o };
}
function epoch(net, lr) {
  const dW1 = [[0, 0], [0, 0]], db1 = [0, 0], dW2 = [0, 0]; let db2 = 0, loss = 0;
  for (let k = 0; k < 4; k++) {
    const x = XOR_X[k], y = XOR_Y[k];
    const { h, o } = forwardOne(net, x);
    loss += (o - y) * (o - y);
    const dOut = (o - y) * o * (1 - o);
    dW2[0] += dOut * h[0]; dW2[1] += dOut * h[1]; db2 += dOut;
    const dh = [dOut * net.W2[0] * h[0] * (1 - h[0]), dOut * net.W2[1] * h[1] * (1 - h[1])];
    for (let j = 0; j < 2; j++) { dW1[0][j] += dh[j] * x[0]; dW1[1][j] += dh[j] * x[1]; db1[j] += dh[j]; }
  }
  const n = { W1: net.W1.map(r => r.slice()), b1: net.b1.slice(), W2: net.W2.slice(), b2: net.b2 };
  for (let i = 0; i < 2; i++) for (let j = 0; j < 2; j++) n.W1[i][j] -= lr * dW1[i][j] / 4;
  for (let j = 0; j < 2; j++) n.b1[j] -= lr * db1[j] / 4;
  n.W2[0] -= lr * dW2[0] / 4; n.W2[1] -= lr * dW2[1] / 4; n.b2 -= lr * db2 / 4;
  return { net: n, loss: loss / 4 };
}

function BackpropTrainer() {
  const cv = useRef(null);
  const [net, setNet] = useState(() => freshNet(true));
  const [step, setStep] = useState(0);
  const [loss, setLoss] = useState(() => {
    let l = 0; const n0 = freshNet(true);
    for (let k = 0; k < 4; k++) { const { o } = forwardOne(n0, XOR_X[k]); l += (o - XOR_Y[k]) ** 2; }
    return l / 4;
  });
  const [hist, setHist] = useState([]);
  const [lr, setLr] = useState(2.0);
  const [running, setRunning] = useState(false);
  const raf = useRef(null);
  const netRef = useRef(net);
  netRef.current = net;

  const preds = useMemo(() => XOR_X.map(x => forwardOne(net, x).o), [net]);

  const doSteps = useCallback((count) => {
    let n = netRef.current, l = 0;
    const newHist = [];
    for (let i = 0; i < count; i++) { const r = epoch(n, lr); n = r.net; l = r.loss; newHist.push(l); }
    netRef.current = n;
    setNet(n); setLoss(l);
    setStep(s => s + count);
    setHist(h => [...h, ...newHist].slice(-600));
  }, [lr]);

  useEffect(() => {
    if (!running) return;
    const tick = () => {
      doSteps(12);
      if (netRef.current && (() => {
        let l = 0; for (let k = 0; k < 4; k++) { const { o } = forwardOne(netRef.current, XOR_X[k]); l += (o - XOR_Y[k]) ** 2; } return l / 4;
      })() < 0.008) { setRunning(false); return; }
      raf.current = requestAnimationFrame(tick);
    };
    raf.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf.current);
  }, [running, doSteps]);

  const reset = (useDefault) => {
    setRunning(false);
    const n = freshNet(useDefault); netRef.current = n; setNet(n);
    let l = 0; for (let k = 0; k < 4; k++) { const { o } = forwardOne(n, XOR_X[k]); l += (o - XOR_Y[k]) ** 2; }
    setLoss(l / 4); setStep(0); setHist([]);
  };

  useEffect(() => {
    const ctx = cv.current.getContext("2d");
    const W = 720, H = 320; ctx.clearRect(0, 0, W, H);
    // --- left: network diagram ---
    const ix = [70, 70], iy = [90, 210], hx = [250, 250], hy = [90, 210], ox = 410, oy = 150;
    const edge = (x1, y1, x2, y2, wv) => {
      ctx.strokeStyle = wv >= 0 ? "#2a5d57" : "#8a3a1f";
      ctx.lineWidth = 0.8 + Math.min(7, Math.abs(wv) * 1.4);
      ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
    };
    for (let i = 0; i < 2; i++) for (let j = 0; j < 2; j++) edge(ix[i] + 22, iy[i], hx[j] - 22, hy[j], net.W1[i][j]);
    for (let j = 0; j < 2; j++) edge(hx[j] + 22, hy[j], ox - 22, oy, net.W2[j]);
    const node = (x, y, txt, sub) => {
      ctx.fillStyle = "#fbf7ec"; ctx.strokeStyle = "#6b4f1d"; ctx.lineWidth = 2;
      ctx.beginPath(); ctx.arc(x, y, 22, 0, 7); ctx.fill(); ctx.stroke();
      ctx.fillStyle = "#1a1814"; ctx.font = "600 13px Source Serif 4"; ctx.textAlign = "center";
      ctx.fillText(txt, x, y + 4);
      if (sub) { ctx.fillStyle = "#6f6754"; ctx.font = "10px JetBrains Mono"; ctx.fillText(sub, x, y - 30); }
    };
    node(ix[0], iy[0], "x1", "ulaz"); node(ix[1], iy[1], "x2", "ulaz");
    node(hx[0], hy[0], "h1", "skriveni"); node(hx[1], hy[1], "h2", "");
    node(ox, oy, preds.length ? preds[0].toFixed(2) : "", "izlaz");
    // --- right: loss curve ---
    const gx = 470, gy = 30, gw = 230, gh = 200;
    ctx.strokeStyle = "#b8ab8e"; ctx.lineWidth = 1; ctx.strokeRect(gx, gy, gw, gh);
    ctx.fillStyle = "#6f6754"; ctx.font = "11px JetBrains Mono"; ctx.textAlign = "left";
    ctx.fillText("pogreška (MSE) kroz korake", gx, gy - 8);
    const maxL = Math.max(0.28, ...hist, loss);
    const series = hist.length ? hist : [loss];
    ctx.strokeStyle = "#8a3a1f"; ctx.lineWidth = 2; ctx.beginPath();
    series.forEach((l, i) => {
      const px = gx + (series.length === 1 ? 0 : i / (series.length - 1) * gw);
      const py = gy + gh - clamp(l / maxL, 0, 1) * gh;
      i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
    });
    ctx.stroke();
    ctx.fillStyle = "#3a352c"; ctx.font = "600 13px Source Serif 4"; ctx.textAlign = "right";
    ctx.fillText("MSE = " + loss.toFixed(4), gx + gw, gy + gh + 18);
  }, [net, preds, hist, loss]);

  let hint = "Klikni „Korak” da vidiš jedan ciklus učenja, ili „Pokreni do kraja”. Prati krivulju pogreške — treba padati.";
  if (loss < 0.02) hint = "Mreža je naučila XOR! Pogledaj tablicu: sve četiri predikcije sad su blizu točnih odgovora (0, 1, 1, 0). Jedan neuron to NE bi mogao — tek skriveni sloj nauči ovaj „zakrivljeni” obrazac.";
  else if (loss > 0.2 && step > 0) hint = "Pogreška još stoji na visokom platou (~0,25) — mreža još nije „provalila” obrazac. To je normalno; nastavi korake ili pokreni do kraja i gledaj nagli pad.";
  else if (step > 0) hint = "Pogreška pada — mreža sad razdvaja primjere. Backpropagation je upravo ovo: izmjeri pogrešku, podijeli odgovornost unatrag, ispravi svaku težinu malčice.";

  return (
    <div className="widget">
      <span className="wlab">mreža 2-2-1 uči XOR · vidljivi backpropagation</span>
      <canvas ref={cv} width="720" height="320"></canvas>
      <div className="ctrls">
        <div className="wbtns">
          <button className="btn-prim" disabled={running} onClick={() => doSteps(1)}>▶ Korak</button>
          <button className="btn-ghost" onClick={() => setRunning(r => !r)}>{running ? "⏸ Pauza" : "⏩ Pokreni do kraja"}</button>
          <button className="btn-ghost" onClick={() => reset(true)}>↻ Reset</button>
          <button className="btn-ghost" onClick={() => reset(false)}>🎲 Nove težine</button>
        </div>
        <div className="ctrl">
          <label>Brzina učenja (learning rate): <span className="val">{lr.toFixed(1)}</span></label>
          <input type="range" min="0.3" max="3" step="0.1" value={lr} onChange={e => setLr(+e.target.value)} />
        </div>
      </div>
      <div className="readout">
        <div className="stat acc"><span className="k">Korak (epoha)</span><span className="v">{step}</span></div>
        <div className={"stat " + (loss < 0.02 ? "good" : "warn")}><span className="k">Pogreška (MSE)</span><span className="v">{loss.toFixed(4)}</span></div>
      </div>
      <table className="minitab" style={{ maxWidth: 460, marginTop: 16 }}>
        <thead><tr><th>Ulaz (x1, x2)</th><th>Točan odgovor</th><th>Predikcija mreže</th></tr></thead>
        <tbody>
          {XOR_X.map((x, i) => {
            const ok = Math.abs(preds[i] - XOR_Y[i]) < 0.1;
            return <tr key={i}><td>({x[0]}, {x[1]})</td><td><b>{XOR_Y[i]}</b></td>
              <td className={ok ? "fit-good" : "fit-bad"}>{preds[i].toFixed(3)} {ok ? "✓" : ""}</td></tr>;
          })}
        </tbody>
      </table>
      <div className="hintline">{hint}</div>
    </div>
  );
}

/* ============================================================
   WIDGET 3+4 — CONFUSION MATRIX + THRESHOLD + ROC
   ============================================================ */
function EvalLab() {
  const [scen, setScen] = useState("prijevara");
  const [t, setT] = useState(0.5);
  const [compare, setCompare] = useState(false);
  const cvBee = useRef(null);
  const cvRoc = useRef(null);
  const cases = SCEN_DATA[scen];
  const m = useMemo(() => confusion(cases, t), [cases, t]);
  const info = SCEN[scen];

  // modeli za usporedbu ROC-a (isti actuals):
  // - osrednji = pola signala, pola šuma → stvarno slabiji POREDAK (niži AUC, ne samo stisnut score)
  // - nasumičan = dijagonalni baseline (AUC = 0,50, „bacanje novčića”)
  const models = useMemo(() => {
    const noise = i => { const r = Math.abs(Math.sin((i + 1) * 12.9898) * 43758.5453); return r - Math.floor(r); };
    const medium = cases.map(([a, s], i) => [a, clamp(0.5 * s + 0.5 * noise(i), 0, 1)]);
    return {
      good: { ...rocCurve(cases), col: "#6b4f1d", naziv: "Ovaj model" },
      medium: { ...rocCurve(medium), col: "#9a7a16", naziv: "Osrednji model" },
      random: { pts: [{ fpr: 0, tpr: 0 }, { fpr: 1, tpr: 1 }], auc: 0.5, col: "#b8ab8e", naziv: "Nasumičan model" },
    };
  }, [cases]);

  // --- beeswarm ---
  useEffect(() => {
    const ctx = cvBee.current.getContext("2d");
    const W = 720, H = 230; ctx.clearRect(0, 0, W, H);
    const PADL = 30, PADR = 16, top = 24, bot = 40;
    const X = s => PADL + s * (W - PADL - PADR);
    const tx = X(t);
    // regions
    ctx.fillStyle = "rgba(42,93,87,.07)"; ctx.fillRect(PADL, top, tx - PADL, H - top - bot);
    ctx.fillStyle = "rgba(138,58,31,.07)"; ctx.fillRect(tx, top, W - PADR - tx, H - top - bot);
    ctx.fillStyle = "#6f6754"; ctx.font = "11px JetBrains Mono"; ctx.textAlign = "center";
    ctx.fillText("predviđeno NEGATIVNO", (PADL + tx) / 2, H - 22);
    ctx.fillText("predviđeno POZITIVNO", (tx + W - PADR) / 2, H - 22);
    // beeswarm stacking by x-bucket
    const buckets = {};
    const r = 6;
    cases.forEach(([actual, score]) => {
      const px = X(score);
      const key = Math.round(px / (r * 1.7));
      buckets[key] = (buckets[key] || 0);
      const k = buckets[key]++;
      const py = (top + (H - top - bot) / 2) + (k % 2 === 0 ? 1 : -1) * Math.ceil(k / 2) * (r * 1.7) * 0.62;
      ctx.beginPath(); ctx.arc(px, py, r, 0, 7);
      ctx.fillStyle = actual === 1 ? "#8a3a1f" : "#2a5d57"; ctx.fill();
      ctx.strokeStyle = "#1a1814"; ctx.lineWidth = 0.5; ctx.stroke();
    });
    // threshold line
    ctx.strokeStyle = "#1a1814"; ctx.lineWidth = 2.5;
    ctx.beginPath(); ctx.moveTo(tx, top - 6); ctx.lineTo(tx, H - bot); ctx.stroke();
    ctx.fillStyle = "#1a1814"; ctx.font = "600 12px JetBrains Mono"; ctx.textAlign = "center";
    ctx.fillText("PRAG = " + t.toFixed(2), tx, 14);
    // x axis label
    ctx.fillStyle = "#6f6754"; ctx.font = "11px JetBrains Mono";
    ctx.fillText("score modela (0 → 1)", W / 2, H - 6);
  }, [cases, t]);

  // --- ROC ---
  useEffect(() => {
    const ctx = cvRoc.current.getContext("2d");
    const S = 340; ctx.clearRect(0, 0, S, S);
    const PAD = 40, plot = S - PAD - 14;
    const X = f => PAD + f * plot, Y = tp => PAD + (1 - tp) * plot;
    // grid + axes
    ctx.strokeStyle = "#d8cdb4"; ctx.lineWidth = 1;
    for (let g = 0; g <= 1.0001; g += 0.25) {
      ctx.beginPath(); ctx.moveTo(X(g), Y(0)); ctx.lineTo(X(g), Y(1)); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(X(0), Y(g)); ctx.lineTo(X(1), Y(g)); ctx.stroke();
    }
    ctx.strokeStyle = "#8a8064"; ctx.lineWidth = 1.5;
    ctx.beginPath(); ctx.moveTo(X(0), Y(0)); ctx.lineTo(X(1), Y(0)); ctx.stroke();
    ctx.beginPath(); ctx.moveTo(X(0), Y(0)); ctx.lineTo(X(0), Y(1)); ctx.stroke();
    // diagonal
    ctx.strokeStyle = "#b8ab8e"; ctx.lineWidth = 1.5; ctx.setLineDash([5, 5]);
    ctx.beginPath(); ctx.moveTo(X(0), Y(0)); ctx.lineTo(X(1), Y(1)); ctx.stroke(); ctx.setLineDash([]);
    const drawCurve = (rc, col, width, fill) => {
      if (fill) {
        ctx.fillStyle = "rgba(107,79,29,.10)"; ctx.beginPath(); ctx.moveTo(X(0), Y(0));
        rc.pts.forEach(p => ctx.lineTo(X(p.fpr), Y(p.tpr))); ctx.lineTo(X(1), Y(0)); ctx.closePath(); ctx.fill();
      }
      ctx.strokeStyle = col; ctx.lineWidth = width; ctx.beginPath();
      rc.pts.forEach((p, i) => i === 0 ? ctx.moveTo(X(p.fpr), Y(p.tpr)) : ctx.lineTo(X(p.fpr), Y(p.tpr)));
      ctx.stroke();
    };
    if (compare) { drawCurve(models.random, models.random.col, 2); drawCurve(models.medium, models.medium.col, 2); }
    drawCurve(models.good, models.good.col, 3, !compare);
    // current point
    if (m.fpr !== null && m.tpr !== null) {
      ctx.fillStyle = "#8a3a1f"; ctx.beginPath(); ctx.arc(X(m.fpr), Y(m.tpr), 7, 0, 7); ctx.fill();
      ctx.strokeStyle = "#fbf7ec"; ctx.lineWidth = 2; ctx.stroke();
    }
    // labels
    ctx.fillStyle = "#3a352c"; ctx.font = "11px JetBrains Mono"; ctx.textAlign = "center";
    ctx.fillText("FPR (lažno pozitivni)", S / 2, S - 6);
    ctx.save(); ctx.translate(12, S / 2); ctx.rotate(-Math.PI / 2); ctx.fillText("TPR (recall)", 0, 0); ctx.restore();
  }, [models, m, compare]);

  const metricCard = (key, label, val) => (
    <div className={"stat " + (info.key === key ? "key acc" : "")}>
      <span className="k">{label}{info.key === key && <span className="badge">najvažnije</span>}</span>
      <span className="v">{val}</span>
    </div>
  );
  const aucTxt = a => a >= 0.9 ? "izvrstan" : a >= 0.8 ? "dobar" : a >= 0.7 ? "osrednji" : a >= 0.6 ? "slab" : "≈ nasumičan";

  let hint = "Pomiči prag i gledaj kako se mijenja ravnoteža između promašaja (FN) i lažnih uzbuna (FP).";
  if (t < 0.4) hint = "Nizak prag: hvataš gotovo sve pozitivne (visok recall), ali dižeš i mnogo lažnih uzbuna (niska preciznost).";
  else if (t > 0.62) hint = "Visok prag: gotovo svaka uzbuna je prava (visoka preciznost), ali ti pravi pozitivni proklize (raste FN, pada recall).";

  return (
    <div className="widget">
      <span className="wlab">prag odlučivanja · matrica zabune · ROC</span>
      <div className="ctrls" style={{ marginTop: 0 }}>
        <div className="ctrl">
          <label>Scenarij</label>
          <select value={scen} onChange={e => setScen(e.target.value)}>
            <option value="prijevara">Otkrivanje prijevare</option>
            <option value="bolest">Probir na bolest</option>
            <option value="spam">Spam filter</option>
          </select>
        </div>
        <div className="ctrl" style={{ flex: 1, minWidth: 220 }}>
          <label>Prag (threshold): <span className="val">{t.toFixed(2)}</span></label>
          <input type="range" min="0" max="1" step="0.01" value={t} onChange={e => setT(+e.target.value)} />
        </div>
      </div>
      <div className="scen">
        Pozitivna klasa = <b>{info.poz}</b>, negativna = <b>{info.neg}</b>.
        <p className="story">{info.story}</p>
        <span className="keymetric">Ključna metrika: {KEY_LABEL[info.key]}</span>
      </div>
      <canvas ref={cvBee} width="720" height="230"></canvas>
      <div className="legend">
        <span><span className="dotc" style={{ background: "#8a3a1f" }}></span> stvarno {info.poz} (pozitivan)</span>
        <span><span className="dotc" style={{ background: "#2a5d57" }}></span> stvarno {info.neg} (negativan)</span>
      </div>

      <div style={{ display: "flex", flexWrap: "wrap", gap: 24, marginTop: 18, alignItems: "flex-start" }}>
        <div>
          <table className="cm">
            <thead><tr><th></th><th>Predv. POZITIVNO</th><th>Predv. NEGATIVNO</th></tr></thead>
            <tbody>
              <tr><th>Stvarno POZ</th>
                <td className="ok"><span className="cn">{m.TP}</span><span className="cl">TP · pogodak</span></td>
                <td className="err"><span className="cn">{m.FN}</span><span className="cl">FN · promašaj</span></td></tr>
              <tr><th>Stvarno NEG</th>
                <td className="err"><span className="cn">{m.FP}</span><span className="cl">FP · lažna uzbuna</span></td>
                <td className="ok"><span className="cn">{m.TN}</span><span className="cl">TN · točno odbačen</span></td></tr>
            </tbody>
          </table>
        </div>
        <div className="readout" style={{ marginTop: 0, flex: 1 }}>
          {metricCard("accuracy", "Accuracy", fmt(m.acc))}
          {metricCard("precision", "Precision", fmt(m.prec))}
          {metricCard("recall", "Recall", fmt(m.rec))}
          {metricCard("f1", "F1", fmt(m.f1))}
        </div>
      </div>
      <div className="hintline">{hint}</div>

      <div style={{ display: "flex", flexWrap: "wrap", gap: 24, marginTop: 22, alignItems: "flex-start" }}>
        <canvas ref={cvRoc} width="340" height="340" style={{ maxWidth: 340 }}></canvas>
        <div style={{ flex: 1, minWidth: 220 }}>
          <span className="wlab">ROC krivulja</span>
          <div className="readout" style={{ marginTop: 0 }}>
            <div className="stat acc"><span className="k">AUC (ovaj model)</span><span className="v">{models.good.auc.toFixed(3)}</span></div>
            <div className="stat"><span className="k">Ocjena</span><span className="v" style={{ fontSize: 16 }}>{aucTxt(models.good.auc)}</span></div>
          </div>
          <label className="toggle" style={{ marginTop: 12 }}>
            <input type="checkbox" checked={compare} onChange={e => setCompare(e.target.checked)} /> usporedi s osrednjim i nasumičnim modelom
          </label>
          {compare && (
            <div className="legend" style={{ marginTop: 8 }}>
              <span><span className="dotc" style={{ background: "#6b4f1d" }}></span> ovaj ({models.good.auc.toFixed(2)})</span>
              <span><span className="dotc" style={{ background: "#9a7a16" }}></span> osrednji ({models.medium.auc.toFixed(2)})</span>
              <span><span className="dotc" style={{ background: "#b8ab8e" }}></span> nasumičan ({models.random.auc.toFixed(2)})</span>
            </div>
          )}
          <p className="story" style={{ marginTop: 12 }}>
            Crna točka je tvoj trenutni prag — pomicanjem klizača ona klizi po krivulji. Što je krivulja bliža
            gornjem lijevom kutu, model bolje razdvaja klase. AUC = 0,5 (dijagonala) znači pogađanje novčićem.
          </p>
          <p className="story" style={{ marginTop: 8, fontSize: 14 }}>
            <b>Oprez:</b> kod jako nebalansiranih podataka (malo pozitivnih) ROC zna izgledati lijepo iako model
            nije osobito koristan — tada je <b>PR-krivulja</b> (precision–recall) često iskreniji pokazatelj.
          </p>
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   WIDGET 5 — REGRESIJSKE METRIKE
   ============================================================ */
const REG_INIT = [
  { id: 1, y: 10, yhat: 11 }, { id: 2, y: 20, yhat: 18 }, { id: 3, y: 30, yhat: 33 },
  { id: 4, y: 40, yhat: 39 }, { id: 5, y: 50, yhat: 52 }, { id: 6, y: 60, yhat: 58 },
];
function RegressionMetrics() {
  const cv = useRef(null);
  const [pts, setPts] = useState(() => REG_INIT.map(p => ({ ...p })));
  const drag = useRef(null);
  const LO = 0, HI = 80;

  const met = useMemo(() => {
    const n = pts.length;
    const errs = pts.map(p => p.y - p.yhat);
    const mae = errs.reduce((s, e) => s + Math.abs(e), 0) / n;
    const mse = errs.reduce((s, e) => s + e * e, 0) / n;
    const rmse = Math.sqrt(mse);
    const ybar = pts.reduce((s, p) => s + p.y, 0) / n;
    const ssTot = pts.reduce((s, p) => s + (p.y - ybar) ** 2, 0);
    const ssRes = errs.reduce((s, e) => s + e * e, 0);
    const r2 = ssTot === 0 ? null : 1 - ssRes / ssTot;
    return { mae, mse, rmse, r2 };
  }, [pts]);

  const geom = () => { const W = 440, H = 440, PAD = 44; const sp = (W - PAD - 14); return { W, H, PAD, sp }; };
  const toPx = (g, v) => g.PAD + (v - LO) / (HI - LO) * g.sp;
  const toPy = (g, v) => g.H - g.PAD - (v - LO) / (HI - LO) * g.sp;
  const fromPy = (g, py) => LO + (g.H - g.PAD - py) / g.sp * (HI - LO);

  useEffect(() => {
    const ctx = cv.current.getContext("2d"); const g = geom();
    ctx.clearRect(0, 0, g.W, g.H);
    ctx.strokeStyle = "#d8cdb4"; ctx.lineWidth = 1; ctx.fillStyle = "#6f6754"; ctx.font = "11px JetBrains Mono";
    for (let v = 0; v <= HI; v += 20) {
      ctx.beginPath(); ctx.moveTo(toPx(g, v), toPy(g, 0)); ctx.lineTo(toPx(g, v), toPy(g, HI)); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(toPx(g, 0), toPy(g, v)); ctx.lineTo(toPx(g, HI), toPy(g, v)); ctx.stroke();
    }
    // diagonal y = yhat
    ctx.strokeStyle = "#b8ab8e"; ctx.lineWidth = 1.5; ctx.setLineDash([5, 5]);
    ctx.beginPath(); ctx.moveTo(toPx(g, 0), toPy(g, 0)); ctx.lineTo(toPx(g, HI), toPy(g, HI)); ctx.stroke(); ctx.setLineDash([]);
    ctx.fillStyle = "#6f6754"; ctx.textAlign = "center";
    ctx.fillText("savršena predikcija (ŷ = y)", toPx(g, 58), toPy(g, 66));
    // residuals + points
    pts.forEach(p => {
      const e = Math.abs(p.y - p.yhat);
      ctx.strokeStyle = `rgba(138,58,31,${clamp(0.25 + e / 30, 0.25, 0.95)})`; ctx.lineWidth = 1.5;
      ctx.beginPath(); ctx.moveTo(toPx(g, p.y), toPy(g, p.yhat)); ctx.lineTo(toPx(g, p.y), toPy(g, p.y)); ctx.stroke();
    });
    pts.forEach(p => {
      ctx.fillStyle = "#6b4f1d"; ctx.strokeStyle = "#fbf7ec"; ctx.lineWidth = 2;
      ctx.beginPath(); ctx.arc(toPx(g, p.y), toPy(g, p.yhat), 8, 0, 7); ctx.fill(); ctx.stroke();
    });
    ctx.fillStyle = "#3a352c"; ctx.font = "11px JetBrains Mono"; ctx.textAlign = "center";
    ctx.fillText("stvarno (y)", g.W / 2, g.H - 8);
    ctx.save(); ctx.translate(12, g.H / 2); ctx.rotate(-Math.PI / 2); ctx.fillText("predviđeno (ŷ)", 0, 0); ctx.restore();
  }, [pts]);

  const evt = (e) => {
    const g = geom(); const rect = cv.current.getBoundingClientRect();
    const sx = cv.current.width / rect.width, sy = cv.current.height / rect.height;
    return { mx: (e.clientX - rect.left) * sx, my: (e.clientY - rect.top) * sy, g };
  };
  const down = (e) => {
    const { mx, my, g } = evt(e);
    let best = null, bd = 18;
    pts.forEach(p => { const d = Math.hypot(toPx(g, p.y) - mx, toPy(g, p.yhat) - my); if (d < bd) { bd = d; best = p.id; } });
    if (best !== null) drag.current = best;
  };
  const move = (e) => {
    if (drag.current === null) return;
    const { my, g } = evt(e);
    const v = clamp(Math.round(fromPy(g, my)), LO, HI);
    setPts(ps => ps.map(p => p.id === drag.current ? { ...p, yhat: v } : p));
  };
  const up = () => { drag.current = null; };

  const addOutlier = () => setPts(ps => ps.map(p => p.id === 4 ? { ...p, yhat: 8 } : p));
  const reset = () => setPts(REG_INIT.map(p => ({ ...p })));

  return (
    <div className="widget">
      <span className="wlab">stvarno vs predviđeno · povuci točku po okomici</span>
      <div style={{ display: "flex", flexWrap: "wrap", gap: 24, alignItems: "flex-start" }}>
        <canvas ref={cv} width="440" height="440" style={{ maxWidth: 440, cursor: "grab", touchAction: "none" }}
          onMouseDown={down} onMouseMove={move} onMouseUp={up} onMouseLeave={up}></canvas>
        <div style={{ flex: 1, minWidth: 240 }}>
          <div className="wbtns" style={{ marginBottom: 14 }}>
            <button className="btn-prim" onClick={addOutlier}>Dodaj outlier</button>
            <button className="btn-ghost" onClick={reset}>↻ Reset</button>
          </div>
          <div className="readout" style={{ marginTop: 0 }}>
            <div className="stat"><span className="k">MAE</span><span className="v">{fmt(met.mae)}</span></div>
            <div className="stat warn"><span className="k">MSE</span><span className="v">{fmt(met.mse, 1)}</span></div>
            <div className="stat warn"><span className="k">RMSE</span><span className="v">{fmt(met.rmse)}</span></div>
            <div className="stat acc"><span className="k">R²</span><span className="v">{fmt(met.r2)}</span></div>
          </div>
          <table className="minitab" style={{ marginTop: 14, fontSize: 13 }}>
            <tbody>
              <tr><td><b>MAE</b></td><td>prosječna apsolutna greška — sve greške jednako važne</td></tr>
              <tr><td><b>MSE</b></td><td>prosječna <em>kvadratna</em> greška — velike greške kažnjava nesrazmjerno</td></tr>
              <tr><td><b>RMSE</b></td><td>korijen iz MSE — u istim jedinicama kao y, ali i dalje osjetljiv na outliere</td></tr>
              <tr><td><b>R²</b></td><td>koliko varijance model objašnjava: 1 = savršeno, 0 = kao prosjek, &lt;0 = gore</td></tr>
            </tbody>
          </table>
        </div>
      </div>
      <div className="hintline">
        Povuci jednu točku daleko od dijagonale (ili klikni „Dodaj outlier”). Gledaj: MAE poraste umjereno, ali
        MSE i RMSE skoče naglo — jer kvadriraju grešku. R² padne. Zato je MSE/RMSE osjetljiv na outliere, a MAE robustan.
      </div>
    </div>
  );
}

/* ============================================================
   KVIZ
   ============================================================ */
const QUIZ = [
  { q: "Model za rijetku bolest uvijek kaže „zdrav” i postiže 97% točnosti. Zašto je svejedno loš?",
    a: ["Jer je accuracy uvijek nepouzdana metrika", "Jer kod nebalansiranih klasa visoka točnost skriva da model ne hvata nijedan stvarni slučaj (recall = 0)", "Jer mu je preciznost previsoka"],
    correct: 1, why: "Kad je pozitivnih jako malo, „uvijek negativno” daje visoku točnost, a recall je 0 — nijedan bolesnik nije uhvaćen. Zato gledamo recall i preciznost." },
  { q: "U backpropagationu, što se događa s pojedinom težinom nakon što mreža izmjeri pogrešku?",
    a: ["Postavi se na nulu i kreće ispočetka", "Ispravi se malčice, razmjerno tome koliko je ta težina pridonijela pogrešci", "Sve se težine promijene jednako"],
    correct: 1, why: "Pogreška se propagira unatrag i svaka težina dobije ispravak razmjeran svom doprinosu pogrešci — male, postupne korekcije, ne restart." },
  { q: "Banka želi uhvatiti što više prijevara, a lažna uzbuna znači samo dodatni telefonski poziv. Koju metriku maksimiziraju i kako pomiču prag?",
    a: ["Preciznost; dižu prag", "Recall; spuštaju prag", "Accuracy; ostavljaju prag na 0,5"],
    correct: 1, why: "Skupa je propuštena prijevara (FN), pa žele visok recall. Spuštanjem praga hvataju više pozitivnih (recall ↑), uz cijenu više lažnih uzbuna." },
  { q: "Dodaš jedan veliki outlier u regresiju. Koja se metrika najmanje promijeni?",
    a: ["MAE", "MSE", "RMSE"],
    correct: 0, why: "MSE i RMSE kvadriraju grešku pa veliki outlier nesrazmjerno napuhnu. MAE zbraja apsolutne greške, pa reagira blaže — zato je robusniji na outliere." },
];
function Quiz() {
  const [answered, setAnswered] = useState({});
  const [score, setScore] = useState(0);
  const pick = (qi, oi) => {
    if (answered[qi] !== undefined) return;
    const ok = oi === QUIZ[qi].correct;
    setAnswered(a => ({ ...a, [qi]: oi }));
    if (ok) setScore(s => s + 1);
  };
  const done = Object.keys(answered).length === QUIZ.length;
  return (
    <div>
      <div className="quiz">
        {QUIZ.map((item, qi) => (
          <div className="q" key={qi}>
            <div className="qt">{qi + 1}. {item.q}</div>
            <div className="opts">
              {item.a.map((txt, oi) => {
                const picked = answered[qi];
                let cls = "opt";
                if (picked !== undefined) {
                  if (oi === item.correct) cls += " correct";
                  else if (oi === picked) cls += " wrong";
                }
                return <button key={oi} className={cls} disabled={picked !== undefined} onClick={() => pick(qi, oi)}>{txt}</button>;
              })}
            </div>
            {answered[qi] !== undefined && (
              <div className={"fb show " + (answered[qi] === item.correct ? "ok" : "no")}>
                {answered[qi] === item.correct ? "✔ Točno! " : "✗ Netočno. "}{item.why}
              </div>
            )}
          </div>
        ))}
      </div>
      {done && <div className="score">Rezultat: {score} / {QUIZ.length}</div>}
    </div>
  );
}

/* ============================================================
   APP
   ============================================================ */
function App() {
  return (
    <div className="wrap">
      <header className="topbar">
        <a className="brand" href="../">
          <span className="brand-mark" aria-hidden="true">D</span>
          <span><span className="brand-name">Dragutin Oreški</span>
            <span className="brand-meta">UI Suputnik · Susret 15</span></span>
        </a>
        <nav className="topbar-right"><a href="../">← Susreti</a></nav>
      </header>

      <header className="hero">
        <div className="floatword">greška</div>
        <p className="kicker">Klasa 15 · Umjetna inteligencija</p>
        <h1>Mreža uči iz grešaka,<br /><em>mi mjerimo greške</em></h1>
        <p className="lede">Cijeli današnji susret vrti se oko jedne riječi — <b>greška</b>. Neuronska mreža
          uči tako što ispravlja vlastite greške, a evaluacija je vještina mjerenja grešaka koje su ostale.
          Ovdje pojmove ne čitaš nego ih pomičeš, treniraš i gledaš kako reagiraju.</p>
        <div className="uses">
          <span className="use">🧠 Neuron i mreža</span>
          <span className="use">🔁 Backpropagation</span>
          <span className="use">🎯 Metrike i prag</span>
          <span className="use">📐 ROC i regresija</span>
        </div>
      </header>

      <section>
        <p className="sec-tag">Pregled</p>
        <h2>Dvije polovice, jedna nit</h2>
        <p className="intro">Prvo gradimo intuiciju o tome <b>kako mreža uči</b> (neuron → mreža → backpropagation),
          a zatim kako <b>procjenjujemo</b> je li naučila dobro. Važna razlika koju nosi cijeli susret:
          <b> treniranje mijenja model</b> (mijenja težine), dok <b>evaluacija ne mijenja model</b> — ona samo
          mjeri kako se ponaša na podacima koje nikad nije vidio.</p>
        <div className="steps">
          <div className="step"><span className="ic">🧠</span><h3>Neuron</h3>
            <p>Ponderirana suma ulaza + prag, pa aktivacija. Igraj se težinama i gledaj odluku.</p>
            <span className="algo">→ Igraonica neurona</span></div>
          <div className="step"><span className="ic">🔁</span><h3>Učenje</h3>
            <p>Mreža pogađa, mjeri pogrešku i ispravlja težine unatrag. Treniraj je korak po korak.</p>
            <span className="algo">→ Backpropagation uživo</span></div>
          <div className="step"><span className="ic">🎯</span><h3>Mjerenje</h3>
            <p>Confusion matrica, prag, ROC i regresijske metrike — svaka mjeri drugu vrstu greške.</p>
            <span className="algo">→ Evaluacija modela</span></div>
        </div>
      </section>

      <section>
        <p className="sec-tag">Dio 1 · neuron</p>
        <h2>Što je zapravo neuron?</h2>
        <p className="intro">Umjetni neuron nije čarolija: uzme nekoliko ulaza, svakom da <b>težinu</b> (koliko mu vjeruje),
          zbroji ih, doda <b>sklonost (bias)</b> i kroz <b>aktivacijsku funkciju</b> izbaci odluku. Zamisli odluku
          „hoću li ponijeti kišobran?”. Pomiči težine i bias — kad je zbroj <i>z</i> iznad praga, lampica se pali.</p>
        <NeuronPlayground />
      </section>

      <section>
        <p className="sec-tag">Dio 2 · backpropagation</p>
        <h2>Kako mreža uči iz grešaka</h2>
        <p className="intro">Jedan neuron ne može sve. Ova malena mreža (2 ulaza → 2 skrivena neurona → 1 izlaz) uči
          <b> XOR</b> — pravilo „<i>točno jedan</i> od dva ulaza je 1”. Klikaj <b>Korak</b> i prati ciklus:
          mreža <b>pogađa</b>, <b>izmjeri pogrešku</b>, <b>podijeli odgovornost</b> za pogrešku unatrag i
          <b> ispravi svaku težinu</b> malčice. Krivulja pogreške najprije stoji, pa naglo padne — to je trenutak
          kad mreža „provali” obrazac.</p>
        <BackpropTrainer />
      </section>

      <section>
        <p className="sec-tag">Dio 3 · podsjetnik</p>
        <h2>Premalo i previše naučeno</h2>
        <p className="intro">Prije nego što mjerimo, moramo znati <b>na čemu</b> mjerimo. Model uvijek testiramo na
          podacima koje nije vidio (npr. podjela 80% za učenje, 20% za test), jer cilj nije <i>zapamtiti</i> nego
          <b> generalizirati</b>. Dvije tipične bolesti:</p>
        <table className="minitab" style={{ maxWidth: 760 }}>
          <thead><tr><th>Stanje</th><th>Greška na učenju</th><th>Greška na testu</th><th>Što se događa</th></tr></thead>
          <tbody>
            <tr><td><b>Underfitting</b><br />(premalo naučeno)</td><td className="fit-bad">velika</td><td className="fit-bad">velika</td>
              <td>Model je prejednostavan (visoka pristranost) — ne hvata ni obrazac u podacima za učenje.</td></tr>
            <tr><td><b>Taman</b><br />(dobra ravnoteža)</td><td className="fit-good">mala</td><td className="fit-good">mala</td>
              <td>Model je naučio obrazac, a ne šum — dobro radi i na novim podacima.</td></tr>
            <tr><td><b>Overfitting</b><br />(previše naučeno)</td><td className="fit-good">vrlo mala</td><td className="fit-bad">velika</td>
              <td>Model je „nabubao” podatke za učenje, uključujući šum (visoka varijanca) — pada na novima.</td></tr>
          </tbody>
        </table>
        <div className="note">Zlatno pravilo: <b>velik jaz</b> između male greške na učenju i velike greške na testu
          je znak overfittinga. Lijek: jednostavniji model, više podataka ili rezanje složenosti.</div>
      </section>

      <section>
        <p className="sec-tag">Dio 4 · evaluacija klasifikacije</p>
        <h2>Matrica zabune, prag i ROC</h2>
        <p className="intro">Klasifikator zapravo daje <b>vjerojatnost</b> (score 0–1), a mi biramo <b>prag</b> iznad
          kojeg slučaj proglašavamo pozitivnim. Pomicanjem praga mijenjamo ravnotežu pogrešaka. Izaberi scenarij,
          pomiči prag i gledaj kako se uživo mijenjaju matrica zabune, sve četiri metrike i točka na ROC krivulji.</p>
        <EvalLab />
      </section>

      <section>
        <p className="sec-tag">Dio 5 · evaluacija regresije</p>
        <h2>Kad model predviđa broj</h2>
        <p className="intro">Kod regresije ne brojimo pogotke nego mjerimo <b>koliko</b> smo promašili. Svaka metrika
          gleda grešku drukčije. Povuci točke (ili dodaj outlier) i otkrij zašto se MAE i RMSE razilaze.</p>
        <RegressionMetrics />
      </section>

      <section>
        <p className="sec-tag">Provjeri se</p>
        <h2>Kratki kviz</h2>
        <p className="intro">Četiri pitanja, odgovor odmah. Klikni na opciju koju misliš da je točna.</p>
        <Quiz />
        <div className="note">
          <strong>Želiš dalje?</strong> Sve ovo radi se „za prave” u <b>Orange Data Mining</b>: widget
          <code>Neural Network</code> trenira mrežu, a <code>Test & Score</code> i <code>ROC Analysis</code> daju
          točno ove metrike i krivulje — samo na cijelim skupovima (Iris, student-performance, mushrooms). To je sljedeća vježba.
        </div>
      </section>

      <footer>
        <p className="sent">„Mreža uči iz grešaka. Mi mjerimo koliko ih je ostalo.”</p>
        <p>Klasa 15 — Neuronske mreže i evaluacija modela · Umjetna inteligencija (Algebra)<br />
          Radi offline u pregledniku. Brojke su poučne, ne službene statistike.</p>
      </footer>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
