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

/* ============================================================
   POMOĆNE FUNKCIJE
   ============================================================ */
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const fmt = (v, d = 2) => (v === null || v === undefined || Number.isNaN(v)) ? "—" : v.toFixed(d);
const mapr = (v, a, b, c, d) => c + (v - a) / (b - a) * (d - c);
const CLS = ["#6b4f1d", "#2a5d57", "#8a3a1f"];        // Vino 1 / 2 / 3
const CLSW = ["#efe3c7", "#dde9e6", "#ecd6c8"];
const CLSN = ["Vino 1", "Vino 2", "Vino 3"];

/* ============================================================
   WIDGET 1 — STANDARDIZACIJA: TKO ODLUČUJE O UDALJENOSTI?
   (Proline × Alcohol, povuci upitno vino, gledaj tko mu je najbliži)
   ============================================================ */
function StandardizationLab() {
  const cv = useRef(null);
  const drag = useRef(false);
  const [std, setStd] = useState(false);
  const [q, setQ] = useState([720, 13.0]);             // [proline_raw, alcohol_raw]
  const D = WINE.proalc;

  // statistike za standardizaciju
  const stats = useMemo(() => {
    const n = D.length;
    const pm = D.reduce((s, r) => s + r[0], 0) / n, am = D.reduce((s, r) => s + r[1], 0) / n;
    const ps = Math.sqrt(D.reduce((s, r) => s + (r[0] - pm) ** 2, 0) / n);
    const as_ = Math.sqrt(D.reduce((s, r) => s + (r[1] - am) ** 2, 0) / n);
    return { pm, am, ps, as_ };
  }, []);
  const PR = [200, 1700], AR = [10.5, 15], ZR = [-3, 3];
  const W = 720, H = 430, PADL = 56, PADR = 18, PADT = 22, PADB = 46;

  const zx = p => (p - stats.pm) / stats.ps, zy = a => (a - stats.am) / stats.as_;
  const sx = (p) => std ? mapr(zx(p), ZR[0], ZR[1], PADL, W - PADR) : mapr(p, PR[0], PR[1], PADL, W - PADR);
  const sy = (a) => std ? mapr(zy(a), ZR[1], ZR[0], PADT, H - PADB) : mapr(a, AR[1], AR[0], PADT, H - PADB);

  // udaljenost u aktivnom prostoru + udio značajki (zbroj kvadrata po svim točkama)
  const info = useMemo(() => {
    let sp = 0, sa = 0, best = -1, bd = Infinity;
    D.forEach((r, i) => {
      const dp = std ? zx(q[0]) - zx(r[0]) : q[0] - r[0];
      const da = std ? zy(q[1]) - zy(r[1]) : q[1] - r[1];
      sp += dp * dp; sa += da * da;
      const d = dp * dp + da * da;
      if (d < bd) { bd = d; best = i; }
    });
    const tot = sp + sa || 1;
    return { pPct: sp / tot * 100, aPct: sa / tot * 100, best };
  }, [q, std, stats]);

  useEffect(() => {
    const ctx = cv.current.getContext("2d");
    ctx.clearRect(0, 0, W, H);
    // osi
    ctx.strokeStyle = "#8a8064"; ctx.lineWidth = 1.5;
    ctx.beginPath(); ctx.moveTo(PADL, PADT); ctx.lineTo(PADL, H - PADB); ctx.lineTo(W - PADR, H - PADB); ctx.stroke();
    ctx.fillStyle = "#6f6754"; ctx.font = "11px JetBrains Mono"; ctx.textAlign = "center";
    ctx.fillText(std ? "Proline (standardizirano, z)" : "Proline (200 – 1680)", (PADL + W) / 2, H - 12);
    ctx.save(); ctx.translate(16, (PADT + H - PADB) / 2); ctx.rotate(-Math.PI / 2);
    ctx.fillText(std ? "Alcohol (z)" : "Alcohol (11 – 14.8)", 0, 0); ctx.restore();
    // mrežne crte na z=0 kad je standardizirano
    if (std) {
      ctx.strokeStyle = "#d8cdb4"; ctx.setLineDash([4, 4]);
      ctx.beginPath(); ctx.moveTo(sx(stats.pm), PADT); ctx.lineTo(sx(stats.pm), H - PADB); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(PADL, sy(stats.am)); ctx.lineTo(W - PADR, sy(stats.am)); ctx.stroke();
      ctx.setLineDash([]);
    }
    // točke
    D.forEach((r, i) => {
      ctx.beginPath(); ctx.arc(sx(r[0]), sy(r[1]), 5, 0, 7);
      ctx.fillStyle = CLS[r[4]]; ctx.globalAlpha = .8; ctx.fill(); ctx.globalAlpha = 1;
    });
    // linija do najbližeg + isticanje
    const nb = D[info.best];
    ctx.strokeStyle = "#1a1814"; ctx.lineWidth = 1.5; ctx.setLineDash([3, 3]);
    ctx.beginPath(); ctx.moveTo(sx(q[0]), sy(q[1])); ctx.lineTo(sx(nb[0]), sy(nb[1])); ctx.stroke(); ctx.setLineDash([]);
    ctx.beginPath(); ctx.arc(sx(nb[0]), sy(nb[1]), 9, 0, 7);
    ctx.strokeStyle = "#1a1814"; ctx.lineWidth = 2; ctx.stroke();
    // upitno vino
    ctx.beginPath(); ctx.arc(sx(q[0]), sy(q[1]), 10, 0, 7);
    ctx.fillStyle = "#fbf7ec"; ctx.fill();
    ctx.strokeStyle = "#1a1814"; ctx.lineWidth = 2.5; ctx.stroke();
    ctx.fillStyle = "#1a1814"; ctx.font = "600 11px JetBrains Mono"; ctx.textAlign = "center";
    ctx.fillText("?", sx(q[0]), sy(q[1]) + 4);
  }, [q, std, info, stats]);

  const evt = (e) => {
    const rect = cv.current.getBoundingClientRect();
    const mx = (e.clientX - rect.left) * (W / rect.width);
    const my = (e.clientY - rect.top) * (H / rect.height);
    return { mx, my };
  };
  const setFromScreen = (mx, my) => {
    let p, a;
    if (std) {
      p = stats.pm + mapr(clamp(mx, PADL, W - PADR), PADL, W - PADR, ZR[0], ZR[1]) * stats.ps;
      a = stats.am + mapr(clamp(my, PADT, H - PADB), PADT, H - PADB, ZR[1], ZR[0]) * stats.as_;
    } else {
      p = mapr(clamp(mx, PADL, W - PADR), PADL, W - PADR, PR[0], PR[1]);
      a = mapr(clamp(my, PADT, H - PADB), PADT, H - PADB, AR[1], AR[0]);
    }
    setQ([clamp(p, 250, 1680), clamp(a, 11, 14.8)]);
  };
  const down = (e) => { drag.current = true; const { mx, my } = evt(e); setFromScreen(mx, my); };
  const move = (e) => { if (!drag.current) return; const { mx, my } = evt(e); setFromScreen(mx, my); };
  const up = () => { drag.current = false; };

  const nbCls = D[info.best][4];
  let hint = std
    ? "Sada obje osi govore istim jezikom (z-vrijednost). Udaljenost je uravnotežena pa je „najbliži susjed” vino koje je doista slično po oba svojstva."
    : "Povuci upitno vino gore-dolje (mijenjaš alkohol). Najbliži susjed se gotovo ne mijenja — jer Proline (raspon ~1500) potpuno nadglasa alkohol (raspon ~4).";

  return (
    <div className="widget">
      <span className="wlab">Proline × Alcohol · povuci „?” i gledaj tko mu je najbliži</span>
      <div className="ctrls" style={{ marginTop: 0, marginBottom: 12 }}>
        <div className="ctrl" style={{ minWidth: "auto" }}>
          <label>Standardizacija</label>
          <div className="seg">
            <button className={std ? "" : "on"} onClick={() => setStd(false)}>ISKLJUČENA</button>
            <button className={std ? "on" : ""} onClick={() => setStd(true)}>UKLJUČENA</button>
          </div>
        </div>
      </div>
      <canvas ref={cv} width="720" height="430" style={{ cursor: "grab", touchAction: "none" }}
        onMouseDown={down} onMouseMove={move} onMouseUp={up} onMouseLeave={up}></canvas>
      <div className="legend">
        {CLSN.map((n, i) => <span key={i}><span className="dotc" style={{ background: CLS[i] }}></span> {n}</span>)}
        <span>○ upitno vino &nbsp;·&nbsp; ⎯ najbliži susjed</span>
      </div>
      <div className="wlab" style={{ marginTop: 16 }}>Tko zapravo određuje udaljenost?</div>
      <div className="load-bars" style={{ height: 28 }}>
        <div className="load-seg" style={{ left: 0, width: info.pPct + "%", height: 22, top: 3, background: CLS[0], borderRadius: "3px 0 0 3px" }}></div>
        <div className="load-seg" style={{ left: info.pPct + "%", width: info.aPct + "%", height: 22, top: 3, background: CLS[1], borderRadius: "0 3px 3px 0" }}></div>
      </div>
      <div className="readout">
        <div className="stat" style={{ borderColor: CLS[0] }}><span className="k">Proline udio</span><span className="v">{info.pPct.toFixed(0)}%</span></div>
        <div className="stat" style={{ borderColor: CLS[1] }}><span className="k">Alcohol udio</span><span className="v">{info.aPct.toFixed(0)}%</span></div>
        <div className="stat acc"><span className="k">Najbliži je</span><span className="v" style={{ fontSize: 17 }}>{CLSN[nbCls]}</span></div>
      </div>
      <div className="hintline">{hint}</div>
      <div className="note" style={{ marginTop: 14 }}>
        <strong>Pokušaj ovo:</strong> ostavi standardizaciju isključenu i povuci „?” skroz gore pa skroz dolje.
        Primijeti da Proline drži ≈ 100% udjela u udaljenosti. Zatim uključi standardizaciju — udio se izjednači
        (~50/50) i tek tada „najbliži susjed” ima smisla. Zato PCA i k-Means <em>uvijek</em> rade na standardiziranim podacima.
      </div>
    </div>
  );
}

/* ============================================================
   WIDGET 2 — PCA: LOV NA KUT S NAJVIŠE INFORMACIJA
   (intuicija na DVIJE varijable: Flavonoidi × Intenzitet boje)
   ============================================================ */
function VarianceHunter() {
  const cv = useRef(null);
  const [deg, setDeg] = useState(20);
  const [showDrops, setShowDrops] = useState(true);
  const [bestSeen, setBestSeen] = useState(0);
  const raf = useRef(null);
  const P = WINE.flci;
  const W = 720, H = 420, cx = W / 2, cy = H / 2 - 18, SC = 58;

  // centriraj (već je ~0, ali svejedno)
  const C = useMemo(() => {
    const n = P.length, mx = P.reduce((s, r) => s + r[0], 0) / n, my = P.reduce((s, r) => s + r[1], 0) / n;
    return P.map(r => [r[0] - mx, r[1] - my, r[2]]);
  }, []);
  const totVar = useMemo(() => C.reduce((s, r) => s + r[0] * r[0] + r[1] * r[1], 0) / C.length, [C]);

  const pct = (d) => {
    const t = d * Math.PI / 180, u = [Math.cos(t), Math.sin(t)];
    const vp = C.reduce((s, r) => { const p = r[0] * u[0] + r[1] * u[1]; return s + p * p; }, 0) / C.length;
    return vp / totVar * 100;
  };
  const cur = pct(deg);
  const maxPct = useMemo(() => { let m = 0; for (let d = 0; d < 180; d++) m = Math.max(m, pct(d)); return m; }, [C]);
  useEffect(() => { if (cur > bestSeen) setBestSeen(cur); }, [cur]);

  const findBest = () => {
    let bd = 0, bm = 0; for (let d = 0; d < 180; d++) { const p = pct(d); if (p > bm) { bm = p; bd = d; } }
    cancelAnimationFrame(raf.current);
    const step = () => {
      setDeg(prev => {
        const diff = bd - prev;
        if (Math.abs(diff) < 0.6) return bd;
        raf.current = requestAnimationFrame(step);
        return prev + Math.sign(diff) * Math.max(0.6, Math.abs(diff) * 0.12);
      });
    };
    raf.current = requestAnimationFrame(step);
  };

  useEffect(() => {
    const ctx = cv.current.getContext("2d"); ctx.clearRect(0, 0, W, H);
    const t = deg * Math.PI / 180, u = [Math.cos(t), Math.sin(t)];
    const toS = (x, y) => [cx + x * SC, cy - y * SC];
    // os kroz ishodište (oba smjera)
    const L = 250;
    const [ax1, ay1] = [cx - u[0] * L, cy + u[1] * L], [ax2, ay2] = [cx + u[0] * L, cy - u[1] * L];
    // projekcijske okomice
    if (showDrops) {
      ctx.strokeStyle = "rgba(111,103,84,.4)"; ctx.lineWidth = 1;
      C.forEach(r => {
        const [px, py] = toS(r[0], r[1]);
        const proj = r[0] * u[0] + r[1] * u[1];
        const [qx, qy] = toS(proj * u[0], proj * u[1]);
        ctx.beginPath(); ctx.moveTo(px, py); ctx.lineTo(qx, qy); ctx.stroke();
      });
    }
    // os
    ctx.strokeStyle = "#6b4f1d"; ctx.lineWidth = 2.5;
    ctx.beginPath(); ctx.moveTo(ax1, ay1); ctx.lineTo(ax2, ay2); ctx.stroke();
    // projekcije (kvačice na osi)
    C.forEach(r => {
      const proj = r[0] * u[0] + r[1] * u[1];
      const [qx, qy] = toS(proj * u[0], proj * u[1]);
      ctx.beginPath(); ctx.arc(qx, qy, 3, 0, 7); ctx.fillStyle = CLS[r[2]]; ctx.fill();
    });
    // točke
    C.forEach(r => {
      const [px, py] = toS(r[0], r[1]);
      ctx.beginPath(); ctx.arc(px, py, 5, 0, 7);
      ctx.fillStyle = CLS[r[2]]; ctx.globalAlpha = .85; ctx.fill(); ctx.globalAlpha = 1;
    });
    // "rug" — 1D raspored projekcija ispod
    const ry = H - 16;
    ctx.strokeStyle = "#b8ab8e"; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(60, ry); ctx.lineTo(W - 60, ry); ctx.stroke();
    C.forEach(r => {
      const proj = r[0] * u[0] + r[1] * u[1];
      const x = clamp(mapr(proj, -3.2, 3.2, 60, W - 60), 60, W - 60);
      ctx.beginPath(); ctx.moveTo(x, ry - 6); ctx.lineTo(x, ry + 6); ctx.strokeStyle = CLS[r[2]]; ctx.globalAlpha = .6; ctx.stroke(); ctx.globalAlpha = 1;
    });
    ctx.fillStyle = "#6f6754"; ctx.font = "10px JetBrains Mono"; ctx.textAlign = "center";
    ctx.fillText("položaji vina projiciranih na os (1D)", W / 2, ry - 12);
  }, [deg, showDrops, C]);

  const atBest = Math.abs(cur - maxPct) < 0.6;
  let hint = "Okreći kut osi i gledaj „raširenost”. Iz nekih smjerova vina se stisnu u crtu (malo informacija).";
  if (atBest) hint = "★ Ovo je PC1! Uz ovaj kut su vina najraširenija — PCA bira upravo taj smjer. Primijeti da se i tri vrste vina najljepše razdvoje, iako PCA ne zna za vrste.";
  else if (cur < 44) hint = "Loš kut: točke se gotovo preklapaju u crtu, gubimo najviše informacija. Zavrti dalje.";

  return (
    <div className="widget">
      <span className="wlab">intuicija PCA na DVIJE varijable · Flavonoidi × Intenzitet boje (standardizirano)</span>
      <canvas ref={cv} width="720" height="420"></canvas>
      <div className="ctrls">
        <div className="ctrl" style={{ flex: 1, minWidth: 240 }}>
          <label>Kut osi: <span className="val">{deg.toFixed(0)}°</span></label>
          <input type="range" min="0" max="179" step="1" value={deg} onChange={e => { cancelAnimationFrame(raf.current); setDeg(+e.target.value); }} />
        </div>
        <div className="wbtns">
          <button className="btn-prim" onClick={findBest}>✦ Pronađi najbolji kut</button>
        </div>
        <label className="toggle"><input type="checkbox" checked={showDrops} onChange={e => setShowDrops(e.target.checked)} /> projekcijske okomice</label>
      </div>
      <div className="readout">
        <div className={"stat " + (atBest ? "good key" : "acc")}><span className="k">Objašnjeno informacija {atBest && <span className="badge">PC1</span>}</span><span className="v">{cur.toFixed(0)}%</span></div>
        <div className="stat"><span className="k">Najbolje do sada</span><span className="v">{bestSeen.toFixed(0)}%</span></div>
        <div className="stat"><span className="k">Maksimum (PC1)</span><span className="v">{maxPct.toFixed(0)}%</span></div>
      </div>
      <div className="hintline">{hint}</div>
      <div className="note" style={{ marginTop: 14 }}>
        <strong>Najbolji kut za grupsku fotografiju.</strong> Mnoštvo ljudi stoji na trgu, a ti smiješ slikati samo iz jednog
        smjera. Iz „tankog” smjera svi se stope u crtu; iz pravog smjera ljudi su najrašireniji i svako lice se vidi. PCA
        obilazi sve smjerove i bira onaj uz koji su podaci <b>najrašireniji</b> (najveća varijanca) — to je <b>PC1</b>.
        Druga komponenta (PC2) je najbolji smjer <b>okomit</b> na prvi.
      </div>
    </div>
  );
}

/* ============================================================
   LOADINGS PANEL — što PC1 i PC2 stvarno znače (svih 5 značajki)
   ============================================================ */
const LOAD_NAMES = {
  flavanoids: "Flavonoidi", proline: "Prolin", color_intensity: "Intenzitet boje",
  "od280/od315_of_diluted_wines": "OD315 (razrij. vina)", alcohol: "Alkohol",
};
function LoadingsPanel() {
  const L = WINE.loadings;
  const keys = Object.keys(L);
  const seg = (v, col) => {
    const w = Math.abs(v) * 46;                 // % širine pola trake
    const left = v >= 0 ? 50 : 50 - w;
    return <div className="load-seg" style={{ left: left + "%", width: w + "%", background: col }}></div>;
  };
  return (
    <div className="widget">
      <span className="wlab">stvarni rezultat PCA · 5 najvažnijih značajki (ono što vidiš u Orangeu)</span>
      <div className="readout" style={{ marginTop: 0, marginBottom: 16 }}>
        <div className="stat acc"><span className="k">PC1 nosi</span><span className="v">{WINE.pcaTop5[0]}%</span></div>
        <div className="stat acc"><span className="k">PC2 nosi</span><span className="v">{WINE.pcaTop5[1]}%</span></div>
        <div className="stat key"><span className="k">PC1 + PC2</span><span className="v">{(WINE.pcaTop5[0] + WINE.pcaTop5[1]).toFixed(1)}%</span></div>
      </div>
      <div className="legend" style={{ marginBottom: 8 }}>
        <span><span className="dotc" style={{ background: "#6b4f1d" }}></span> doprinos PC1</span>
        <span><span className="dotc" style={{ background: "#2a5d57" }}></span> doprinos PC2</span>
        <span>lijevo = negativan · desno = pozitivan</span>
      </div>
      {keys.map(k => (
        <div className="load-row" key={k}>
          <span className="ln">{LOAD_NAMES[k]}</span>
          <div>
            <div className="load-bars"><div className="load-mid"></div>{seg(L[k][0], "#6b4f1d")}</div>
            <div className="load-bars" style={{ marginTop: 3 }}><div className="load-mid"></div>{seg(L[k][1], "#2a5d57")}</div>
          </div>
        </div>
      ))}
      <div className="hintline" style={{ marginTop: 14 }}>
        <b>PC1</b> je „opće bogatstvo” vina — visoki Flavonoidi, Prolin i OD315 zajedno. <b>PC2</b> razdvaja po
        <b> boji i tijelu</b> — visok Intenzitet boje i Alkohol. Svaka komponenta je <b>ponderirana mješavina</b> izvornih svojstava.
      </div>
      <div className="callout" style={{ marginTop: 14 }}>
        <h4>A zašto u nekim izvorima piše ~55%?</h4>
        <p>Ako PCA pustiš na <b>svih 13</b> značajki (bez odabira top-5), prve dvije komponente nose tek ≈ {WINE.pcaFull[0]}% + {WINE.pcaFull[1]}% = ≈55%.
          Više početnih značajki znači da se informacija <b>raspoređuje kroz više komponenti</b>. Kad prvo zadržiš samo 5 najvažnijih, prve dvije nose puno više (≈{(WINE.pcaTop5[0] + WINE.pcaTop5[1]).toFixed(0)}%).</p>
      </div>
    </div>
  );
}

/* ============================================================
   WIDGET 3 — KLASTERI vs STVARNE KLASE (most nenadzirano → nadzirano)
   ============================================================ */
function bestPerm(conf) {
  const perms = [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]];
  let best = -1, bp = perms[0];
  for (const p of perms) { const s = conf[0][p[0]] + conf[1][p[1]] + conf[2][p[2]]; if (s > best) { best = s; bp = p; } }
  return { map: bp, correct: best };
}
function ClusterVsTruth() {
  const cv = useRef(null);
  const P = WINE.proj;                                   // [pc1, pc2, klasa]
  const W = 720, H = 430, PADL = 50, PADR = 18, PADT = 22, PADB = 40;
  const xs = P.map(p => p[0]), ys = P.map(p => p[1]);
  const XR = [Math.min(...xs) - .3, Math.max(...xs) + .3], YR = [Math.min(...ys) - .3, Math.max(...ys) + .3];
  const sx = x => mapr(x, XR[0], XR[1], PADL, W - PADR), sy = y => mapr(y, YR[1], YR[0], PADT, H - PADB);

  // determinističko k-means++ (bez slučajnosti, bez oznaka)
  const initC = useMemo(() => {
    const pts = P.map(p => [p[0], p[1]]);
    const mean = [pts.reduce((s, p) => s + p[0], 0) / pts.length, pts.reduce((s, p) => s + p[1], 0) / pts.length];
    const d2 = (a, b) => (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2;
    let c0 = pts[0], m0 = -1; pts.forEach(p => { const d = d2(p, mean); if (d > m0) { m0 = d; c0 = p; } });
    let c1 = pts[0], m1 = -1; pts.forEach(p => { const d = d2(p, c0); if (d > m1) { m1 = d; c1 = p; } });
    let c2 = pts[0], m2 = -1; pts.forEach(p => { const d = Math.min(d2(p, c0), d2(p, c1)); if (d > m2) { m2 = d; c2 = p; } });
    return [c0.slice(), c1.slice(), c2.slice()];
  }, []);

  const [cents, setCents] = useState(initC);
  const [iter, setIter] = useState(0);
  const [colorBy, setColorBy] = useState("cluster");

  const assign = useCallback((cs) => P.map(p => {
    let bi = 0, bd = Infinity;
    cs.forEach((c, i) => { const d = (p[0] - c[0]) ** 2 + (p[1] - c[1]) ** 2; if (d < bd) { bd = d; bi = i; } });
    return bi;
  }), []);

  const labels = useMemo(() => assign(cents), [cents, assign]);
  const conf = useMemo(() => {
    const g = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
    P.forEach((p, i) => g[p[2]][labels[i]]++);
    return g;
  }, [labels]);
  const { map, correct } = useMemo(() => bestPerm(conf), [conf]);
  const agreement = (correct / P.length * 100);

  const step = () => {
    const sums = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];      // [sx, sy, n]
    P.forEach((p, i) => { const c = labels[i]; sums[c][0] += p[0]; sums[c][1] += p[1]; sums[c][2]++; });
    const nc = cents.map((c, i) => sums[i][2] ? [sums[i][0] / sums[i][2], sums[i][1] / sums[i][2]] : c);
    setCents(nc); setIter(it => it + 1);
  };
  const reset = () => { setCents(initC.map(c => c.slice())); setIter(0); };
  const stable = iter >= 8;

  // map[trueClass] = klaster; trebamo i klaster→klasa (za bojanje da se dvije slike poklope)
  const cl2cls = [0, 0, 0]; map.forEach((clu, tc) => { cl2cls[clu] = tc; });

  useEffect(() => {
    const ctx = cv.current.getContext("2d"); ctx.clearRect(0, 0, W, H);
    ctx.strokeStyle = "#8a8064"; ctx.lineWidth = 1.5;
    ctx.beginPath(); ctx.moveTo(PADL, PADT); ctx.lineTo(PADL, H - PADB); ctx.lineTo(W - PADR, H - PADB); ctx.stroke();
    ctx.fillStyle = "#6f6754"; ctx.font = "11px JetBrains Mono"; ctx.textAlign = "center";
    ctx.fillText("PC1", (PADL + W) / 2, H - 10);
    ctx.save(); ctx.translate(16, (PADT + H - PADB) / 2); ctx.rotate(-Math.PI / 2); ctx.fillText("PC2", 0, 0); ctx.restore();
    P.forEach((p, i) => {
      const col = colorBy === "class" ? CLS[p[2]] : CLS[cl2cls[labels[i]]];
      ctx.beginPath(); ctx.arc(sx(p[0]), sy(p[1]), 5, 0, 7);
      ctx.fillStyle = col; ctx.globalAlpha = .82; ctx.fill(); ctx.globalAlpha = 1;
    });
    if (colorBy === "cluster") {
      cents.forEach((c, i) => {
        ctx.strokeStyle = "#1a1814"; ctx.lineWidth = 3; const X = sx(c[0]), Y = sy(c[1]);
        ctx.beginPath(); ctx.moveTo(X - 8, Y - 8); ctx.lineTo(X + 8, Y + 8); ctx.moveTo(X + 8, Y - 8); ctx.lineTo(X - 8, Y + 8); ctx.stroke();
      });
    }
  }, [labels, cents, colorBy, map]);

  return (
    <div className="widget">
      <span className="wlab">k-Means (k=3) na Wine · usporedi klastere sa stvarnim vrstama</span>
      <canvas ref={cv} width="720" height="430"></canvas>
      <div className="ctrls">
        <div className="wbtns">
          <button className="btn-prim" disabled={stable} onClick={step}>{stable ? "Stabilno ✓" : "▶ Korak k-Meansa"}</button>
          <button className="btn-ghost" onClick={reset}>↻ Reset</button>
        </div>
        <div className="ctrl" style={{ minWidth: "auto" }}>
          <label>Oboji točke po</label>
          <div className="seg">
            <button className={colorBy === "cluster" ? "on" : ""} onClick={() => setColorBy("cluster")}>Klaster (×)</button>
            <button className={colorBy === "class" ? "on" : ""} onClick={() => setColorBy("class")}>Stvarna vrsta</button>
          </div>
        </div>
      </div>
      <div className="readout">
        <div className="stat key acc"><span className="k">Poklapanje sa stvarnim vrstama</span><span className="v">{agreement.toFixed(1)}%</span></div>
        <div className="stat"><span className="k">Korak</span><span className="v">{iter}</span></div>
        <div className="stat warn"><span className="k">Pogrešno svrstano</span><span className="v">{P.length - correct}</span></div>
      </div>
      <div style={{ display: "flex", flexWrap: "wrap", gap: 24, marginTop: 16, alignItems: "flex-start" }}>
        <table className="cm" style={{ borderSpacing: 5 }}>
          <thead><tr><th></th>{[0, 1, 2].map(j => <th key={j}>Klaster ≈ {CLSN[j]}</th>)}</tr></thead>
          <tbody>
            {[0, 1, 2].map(tc => (
              <tr key={tc}><th>{CLSN[tc]}</th>
                {[0, 1, 2].map(j => {
                  const cl = map[j];                       // stupac j = klaster pridružen vrsti j
                  const isDiag = j === tc;
                  return <td key={j} className={isDiag ? "ok" : (conf[tc][cl] > 0 ? "err" : "")} style={{ width: 78, height: 48 }}>
                    <span className="cn" style={{ fontSize: 20 }}>{conf[tc][cl]}</span></td>;
                })}
              </tr>
            ))}
          </tbody>
        </table>
        <div style={{ flex: 1, minWidth: 230 }}>
          <div className="hintline" style={{ marginTop: 0 }}>
            Klikaj „Korak” dok ne piše <b>Stabilno</b>, pa prebacuj bojanje. Dvije slike su gotovo identične — a stroju
            <b> nitko nije rekao</b> koja je vrsta vina. Najteža mu je granica {CLSN[1]} ↔ {CLSN[2]}.
          </div>
        </div>
      </div>
      <div className="note" style={{ marginTop: 14 }}>
        <strong>Vrećica izmiješanog sitniša.</strong> Stroju daš novčiće i kažeš samo: izvaži i izmjeri svaki, pa složi u
        3 hrpe slične robe — ne kažeš koliko vrijede. Hrpe se gotovo poklope sa stvarnim apoenima; pogriješi tek na
        ponekom izlizanom, graničnom novčiću. Tu nenadzirano učenje dotiče nadzirano: <b>kemija sama iscrtava vrste</b>,
        a oznake (vrste) samo potvrde ono što je struktura već pokazala.
      </div>
    </div>
  );
}

/* ============================================================
   WIDGET 4 — BANK: BAKE-OFF MODELA + ZAMKA „DURATION”
   ============================================================ */
const METRICS = [
  { k: "AUC", lab: "AUC", note: "AUC = sposobnost da kupce rangira na vrh liste. Ne ovisi o omjeru klasa — najpoštenija mjera za kampanju." },
  { k: "CA", lab: "Točnost (CA)", note: "Točnost vara: i „lijeni model” koji uvijek kaže NE ima ≈89%. Zato CA sam ne odlučuje." },
  { k: "R", lab: "Recall (kupci)", note: "Recall na klasi „da”: koliko stvarnih kupaca model uhvati. I najbolji uhvati tek ~pola." },
  { k: "P", lab: "Preciznost (kupci)", note: "Preciznost: kad model kaže „kupac”, koliko je puta u pravu." },
  { k: "F1", lab: "F1 (kupci)", note: "F1 spaja preciznost i recall na klasi kupaca — pošteniji prikaz nego sirova točnost." },
];
function ModelBakeoff() {
  const [metric, setMetric] = useState("AUC");
  const [dur, setDur] = useState(true);
  const data = dur ? BANK.withDuration : BANK.withoutDuration;
  const rows = [...data].map(m => ({ ...m })).sort((a, b) => b[metric] - a[metric]);
  const mObj = METRICS.find(m => m.k === metric);

  return (
    <div className="widget">
      <span className="wlab">5 modela · poredaj po onome što ti je važno</span>
      <div className="ctrls" style={{ marginTop: 0 }}>
        <div className="ctrl" style={{ minWidth: "auto", flex: 1 }}>
          <label>Mjera</label>
          <div className="seg" style={{ flexWrap: "wrap" }}>
            {METRICS.map(m => <button key={m.k} className={metric === m.k ? "on" : ""} onClick={() => setMetric(m.k)}>{m.lab}</button>)}
          </div>
        </div>
        <div className="ctrl" style={{ minWidth: "auto" }}>
          <label>Koristi značajku „duration”</label>
          <div className="lever">
            <button className={dur ? "on da" : ""} onClick={() => setDur(true)}>DA</button>
            <button className={!dur ? "on ne" : ""} onClick={() => setDur(false)}>NE</button>
          </div>
        </div>
      </div>

      <div className="board" style={{ marginTop: 16 }}>
        {rows.map((m, i) => {
          const v = m[metric];
          return (
            <div className={"brow r" + (i + 1)} key={m.name}>
              <span className="bname">
                {i < 3 && <span className="medal">{i + 1}</span>}{m.name}
              </span>
              <div className="btrack">
                <div className={"bfill" + (v < 0.7 ? " lo" : "")} style={{ width: (v * 100) + "%" }}></div>
              </div>
              <span className="bval">{v.toFixed(3)}</span>
            </div>
          );
        })}
      </div>
      <div className="board-scale">skala 0 – 1</div>

      {dur ? (
        <div className="banner leak"><span className="bi">⚠</span><span>
          <b>Spektakularan rezultat — ali varka.</b> „duration” (trajanje poziva) znaš tek <b>nakon</b> poziva.
          Dugačak poziv je <i>posljedica</i> zainteresiranog klijenta, a ne podatak kojim biraš koga zvati.
          Uključivanje te značajke je <b>curenje podataka</b> i napuhuje AUC za ~0,15.</span></div>
      ) : (
        <div className="banner safe"><span className="bi">✓</span><span>
          <b>Realan, upotrebljiv model.</b> Koristi samo ono što znaš <b>prije</b> poziva (dob, posao, kontakt,
          ekonomski pokazatelji…). AUC padne, ali ovo je broj na koji se u praksi možeš osloniti.</span></div>
      )}

      <div className="hintline" style={{ marginTop: 14 }}>{mObj.note}</div>

      <div className="note" style={{ marginTop: 14 }}>
        <strong>Pokušaj ovo:</strong> (1) Gledaj <b>Točnost</b> — svi modeli izgledaju ~0,90, kao da su jednako dobri.
        (2) Prebaci na <b>AUC</b> — Gradient Boosting izbija na vrh, razlike postaju vidljive. (3) Spusti polugu
        „duration” na <b>NE</b> i gledaj kako <b>svima</b> AUC propadne. Pobjednik ovisi o tome <em>što mjeriš</em> i
        <em> koje značajke smiješ koristiti</em> — ne biraj slijepo po točnosti.
      </div>
    </div>
  );
}

/* ============================================================
   MALI CALLOUT — LIJENI MODEL (paradoks točnosti, primijenjeno)
   ============================================================ */
function LazyCallout() {
  const yes = Math.round(100 * BANK.balance.yes);
  const cells = Array.from({ length: 100 }, (_, i) => i < yes);
  return (
    <div className="callout">
      <h4>„Lijeni model”: uvijek reci NE</h4>
      <div className="callout-grid">
        <div className="clientgrid">
          {cells.map((isYes, i) => <div key={i} className={"cell" + (isYes ? " yes" : "")}></div>)}
        </div>
        <div>
          <div className="readout" style={{ marginTop: 0 }}>
            <div className="stat good"><span className="k">Točnost (CA)</span><span className="v">{(BANK.balance.no * 100).toFixed(1)}%</span></div>
            <div className="stat warn"><span className="k">Uhvaćenih kupaca</span><span className="v">0%</span></div>
          </div>
          <p style={{ marginTop: 10 }}>Samo ~{yes} od 100 klijenata kaže „da”. Model koji za <b>svakoga</b> kaže „neće”
            ima ~89% točnosti — a banci je <b>beskoristan</b> jer ne nađe nijednog kupca. Zato gledamo AUC i recall.
            <br /><span className="minihint">Zašto se to događa općenito → susret 05 (paradoks točnosti).</span></p>
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   KVIZ
   ============================================================ */
const QUIZ = [
  { q: "Zašto podatke standardiziramo (z-score) prije PCA i k-Meansa?",
    a: ["Da bismo ubrzali računanje", "Da značajka s velikim rasponom (npr. Proline) ne nadglasa sve ostale u udaljenosti", "Jer modeli ne rade s decimalnim brojevima"],
    correct: 1, why: "Bez standardizacije Proline (raspon ~1500) potpuno dominira udaljenostima, a značajke malog raspona nestanu. Standardizacija svima daje jednaku priliku." },
  { q: "Što PCA bira kao prvu glavnu komponentu (PC1)?",
    a: ["Smjer u kojem su podaci najrašireniji (najveća varijanca)", "Najvažniju izvornu varijablu", "Smjer u kojem se klase najviše preklapaju"],
    correct: 0, why: "PC1 je smjer maksimalne raširenosti (varijance) — nosi najviše informacija. PC2 je najbolji smjer okomit na njega." },
  { q: "k-Means je na Wine podacima ~95% pogodio prave 3 vrste, a da mu nitko nije dao oznake. Što to znači?",
    a: ["Da je k-Means zapravo nadzirani model", "Da kemijski sastav sam po sebi razdvaja vrste — struktura postoji i prije oznaka", "Da su podaci slučajni"],
    correct: 1, why: "Grupe se pojave i bez oznaka jer ih kemija stvarno razdvaja. Tu nenadzirano učenje prelazi u nadzirano: oznake potvrde postojeću strukturu." },
  { q: "Zašto je značajka „duration” problematična u Bank modelu, iako najviše diže AUC?",
    a: ["Jer je mjerena u sekundama", "Jer trajanje poziva znamo tek NAKON poziva — to je curenje podataka", "Jer je nebitna za odluku"],
    correct: 1, why: "Dugačak poziv je posljedica zainteresiranog klijenta. Kad slažeš listu koga zvati, poziv se još nije dogodio — pa duration ne postoji. Korištenje te značajke napuhuje rezultat (curenje podataka)." },
];
function Quiz() {
  const [ans, setAns] = useState({});
  const [score, setScore] = useState(0);
  const pick = (qi, oi) => {
    if (ans[qi] !== undefined) return;
    setAns(a => ({ ...a, [qi]: oi }));
    if (oi === QUIZ[qi].correct) setScore(s => s + 1);
  };
  const done = Object.keys(ans).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 = ans[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>
            {ans[qi] !== undefined && (
              <div className={"fb show " + (ans[qi] === item.correct ? "ok" : "no")}>
                {ans[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 16</span></span>
        </a>
        <nav className="topbar-right"><a href="../">← Susreti</a></nav>
      </header>

      <header className="hero">
        <div className="floatword">struktura</div>
        <p className="kicker">Klasa 16 · Umjetna inteligencija</p>
        <h1>Učenje <em>bez učitelja</em><br />i <em>s učiteljem</em></h1>
        <p className="lede">Danas radimo dvije vježbe u Orangeu. S vinima učimo <b>nenadzirano</b> — bez oznaka,
          struktura se pojavi sama. S bankom učimo <b>nadzirano</b> — iz oznaka, pa mjerimo i biramo model.
          Ovdje pojmove ne čitaš nego ih pomičeš, vrtiš i uspoređuješ.</p>
        <div className="uses">
          <span className="use">📏 Standardizacija</span>
          <span className="use">🧭 PCA</span>
          <span className="use">🪙 Klasteri vs istina</span>
          <span className="use">🏁 Bake-off modela</span>
          <span className="use">⚠ Curenje podataka</span>
        </div>
      </header>

      <section>
        <p className="sec-tag">Pregled</p>
        <h2>Dvije polovice, jedna nit</h2>
        <p className="intro">Najprije s <b>vinima</b> (178 uzoraka, 13 kemijskih mjerenja) gledamo kako se grupe pojave
          <b> bez ijedne oznake</b>: standardiziramo, sažmemo s PCA-om i klasteriramo. Zatim s <b>bankom</b>
          (4119 klijenata) prelazimo na nadzirano učenje: treniramo 5 modela i pošteno ih mjerimo. Nit koja ih spaja:
          ako struktura u podacima <i>postoji</i>, nenadzirano će je naći — a nadzirano je onda samo iskoristi.</p>
        <div className="steps">
          <div className="step"><span className="ic">📏</span><h3>Standardizacija</h3>
            <p>Zašto jedna „velika” varijabla pokvari sve udaljenosti i kako to popraviti.</p>
            <span className="algo">→ Tko određuje udaljenost</span></div>
          <div className="step"><span className="ic">🧭</span><h3>PCA</h3>
            <p>Traženje najboljeg kuta gledanja: 13 brojeva sažmemo u 2, uz najmanji gubitak.</p>
            <span className="algo">→ Lov na kut</span></div>
          <div className="step"><span className="ic">🪙</span><h3>Klasteri vs istina</h3>
            <p>k-Means bez oznaka pronađe prave vrste vina ~95%. Most do nadziranog učenja.</p>
            <span className="algo">→ Usporedba</span></div>
          <div className="step"><span className="ic">🏁</span><h3>Bake-off</h3>
            <p>5 modela, koji je najbolji? Ovisi o mjeri — i o tome smiješ li koristiti „duration”.</p>
            <span className="algo">→ Banka</span></div>
        </div>
      </section>

      <section>
        <p className="sec-tag">Dio 1 · vino · nenadzirano</p>
        <h2>Tko zapravo određuje udaljenost?</h2>
        <p className="intro">Sve metode koje gledaju „koliko su dvije stvari slične” (PCA, k-Means, najbliži susjed)
          računaju <b>udaljenost</b>. Ako jedna značajka ima ogroman raspon brojeva, ona sama odluči o svemu. Zamisli
          da uspoređuješ ljude, a visinu mjeriš u milimetrima, a broj cipele u brojevima — milimetri zdrobe sve ostalo.</p>
        <StandardizationLab />
        <div className="callout">
          <h4>Rank (Information Gain) — kako biramo značajke</h4>
          <p>Prije PCA-a Rank pokazuje koje varijable najjače razlikuju poznate klase. Na Wine podacima top-5 su:
            <b> Flavonoidi, Prolin, Intenzitet boje, OD315, Alkohol</b>. Na njima dalje radi PCA i k-Means — manje šuma, brže računanje.</p>
        </div>
      </section>

      <section>
        <p className="sec-tag">Dio 1 · vino · PCA</p>
        <h2>Najbolji kut gledanja na podatke</h2>
        <p className="intro">13 kemijskih mjerenja je previše da bismo ih nacrtali. <b>PCA</b> pronalazi nekoliko novih
          smjerova duž kojih su vina najraširenija, pa složenih 13 brojeva sažmemo u 2 — gubeći tek malo detalja.
          Prvo izgradi intuiciju na samo <b>dvije</b> varijable, a zatim pogledaj stvarni rezultat.</p>
        <VarianceHunter />
        <LoadingsPanel />
        <div className="callout" style={{ marginTop: 16 }}>
          <h4>Pažnja: PCA ne grupira</h4>
          <p>PCA samo bira <b>najinformativniji kut gledanja</b> i sažima dimenzije. Grupe (vrste vina) se onda <i>vide</i>
            same od sebe na grafu — ali ih PCA nije stvorio. Grupiranje radi k-Means, u sljedećem koraku.</p>
        </div>
      </section>

      <section>
        <p className="sec-tag">Dio 1 · vino · klasteriranje</p>
        <h2>Stroj koji pogodi vrste — bez oznaka</h2>
        <p className="intro">Sada pustimo <b>k-Means</b> (k=3) da sam složi vina u 3 hrpe, koristeći samo kemiju i ništa
          o vrstama. Pa otkrijemo oznake i usporedimo. Koliko se klasteri poklapaju sa stvarnim vrstama?</p>
        <ClusterVsTruth />
      </section>

      <section>
        <p className="sec-tag">Dio 2 · banka · nadzirano</p>
        <h2>Koji je model najbolji? Ovisi što pitaš</h2>
        <p className="intro">Banka želi znati hoće li klijent prihvatiti oročeni depozit. Podaci su <b>nebalansirani</b>:
          samo ~11% kaže „da”. Trenirali smo 5 modela i izmjerili ih 5-strukom kros-validacijom. Prije nego što proglasiš
          pobjednika — pogledaj „lijeni model”.</p>
        <LazyCallout />
        <div style={{ height: 18 }}></div>
        <ModelBakeoff />
        <div className="callout" style={{ marginTop: 16 }}>
          <h4>Rank na banci — i zašto je „duration” crvena zastava</h4>
          <p>Rank ovdje stavlja <b>duration</b> (trajanje poziva) na vrh važnosti. Ali Rank nije odluka — najveća
            važnost može značiti <b>curenje podataka</b>. Provjeri možeš li značajku doista znati <i>prije</i> trenutka odluke.</p>
        </div>
      </section>

      <section>
        <p className="sec-tag">Provjeri se</p>
        <h2>Kratki kviz</h2>
        <p className="intro">Četiri pitanja, odgovor odmah. Klikni opciju koju misliš da je točna.</p>
        <Quiz />
        <div className="note">
          <strong>Sve ovo radiš „za prave” u Orangeu.</strong> Wine: <code>Datasets → Preprocess (Standardize) → Rank →
          PCA → Scatter Plot → k-Means</code>. Banka: <code>Continuize (One-Hot) → Rank → Test &amp; Score → ROC →
          Confusion Matrix</code>. Brojke na ovoj stranici izračunate su iz pravih skupova podataka.
        </div>
      </section>

      <footer>
        <p className="sent">„Ako struktura postoji, nenadzirano je nađe. Nadzirano je onda samo iskoristi.”</p>
        <p>Klasa 16 — Nenadzirano i nadzirano učenje (Wine i Bank Marketing) · Umjetna inteligencija (Algebra)<br />
          Radi offline u pregledniku. Brojke su izračunate iz stvarnih podataka i poučne su, ne službene statistike.</p>
      </footer>
    </div>
  );
}

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