// visualizations.jsx
// Interactive sections for the article — original implementations.

const { useState, useMemo, useEffect, useRef } = React;

/* ------------------------------------------------------------------
   TokenizerDemo — fake BPE-ish tokenizer for visualization.
   Splits on whitespace + common French sub-units. Not a real tokenizer.
   ------------------------------------------------------------------ */

function fakeTokenId(tok) {
  let h = 0;
  for (let i = 0; i < tok.length; i++) h = h * 31 + tok.charCodeAt(i) >>> 0;
  return 100 + h % 49900;
}

function tokenizeFr(text) {
  if (!text) return [];
  const pieces = text.match(/[\p{L}\p{M}]+|[0-9]+|[^\s\p{L}\p{M}0-9]+|\s+/gu) || [];
  const out = [];
  for (const p of pieces) {
    if (/^\s+$/.test(p)) {out.push({ s: p, kind: 'space' });continue;}
    if (/[^\p{L}\p{M}0-9]/u.test(p) && p.length <= 3) {
      out.push({ s: p, kind: 'punct' });continue;
    }
    if (p.length <= 5) {
      out.push({ s: p, kind: 'word' });
    } else {
      const suffixes = ['ement', 'tions', 'tion', 'ment', 'ance', 'ence', 'eur', 'eux', 'ait', 'ais', 'ant', 'ées', 'ée', 'er', 'ez', 'es'];
      let rest = p;const parts = [];
      let matched = null;
      for (const suf of suffixes) {
        if (rest.toLowerCase().endsWith(suf) && rest.length - suf.length >= 3) {
          matched = suf;break;
        }
      }
      if (matched) {
        parts.push(rest.slice(0, rest.length - matched.length));
        parts.push(rest.slice(rest.length - matched.length));
      } else if (rest.length > 7) {
        parts.push(rest.slice(0, Math.ceil(rest.length / 2)));
        parts.push(rest.slice(Math.ceil(rest.length / 2)));
      } else {
        parts.push(rest);
      }
      parts.forEach((pp, i) => out.push({ s: pp, kind: i === 0 ? 'word' : 'subword' }));
    }
  }
  return out.map((t) => ({ ...t, id: fakeTokenId(t.s.toLowerCase()) }));
}

function TokenizerDemo() {
  const [text, setText] = useState("Recrutez votre premier Collaborateur IA.");
  const tokens = useMemo(() => tokenizeFr(text), [text]);
  const visible = tokens.filter((t) => t.kind !== 'space');
  const charCount = text.length;

  return (
    <div className="panel">
      <div className="panel__head">
        <div>
          <div className="panel__title">Découpez une phrase en jetons</div>
          <div className="panel__hint">Tapez du texte — chaque morceau coloré est un jeton, identifié par un numéro.</div>
        </div>
        <div className="panel__id">DÉMO · 01</div>
      </div>

      <input
        className="input"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Tapez une phrase en français…"
        maxLength={140} />
      

      <div className="token-row" aria-live="polite">
        {tokens.map((t, i) => t.kind === 'space' ?
        <span key={i} style={{ width: 4 }} aria-hidden="true" /> :

        <span key={i} className="token-chip">
            <span className="token-chip__txt">{t.s}</span>
            <span className="token-chip__id">#{t.id}</span>
          </span>
        )}
      </div>

      <div className="stats">
        <div className="stat">
          <div className="stat__label">Caractères</div>
          <div className="stat__value">{charCount}</div>
        </div>
        <div className="stat">
          <div className="stat__label">Jetons</div>
          <div className="stat__value accent">{visible.length}</div>
        </div>
        <div className="stat">
          <div className="stat__label">Ratio</div>
          <div className="stat__value">{visible.length ? (charCount / visible.length).toFixed(1) : '—'}</div>
        </div>
      </div>

      <p className="cap">
        <b>Note.</b> Démo pédagogique : un vrai tokenizer (BPE, SentencePiece) apprend ses fragments
        sur des milliards de mots et finit avec ~50 000 à 200 000 entrées dans son vocabulaire.
      </p>
    </div>);

}

/* ------------------------------------------------------------------
   EmbeddingMap — 2D projection of toy word vectors.
   Named axes, 3 modes : carte (nearest neighbors), distance, analogie.
   ------------------------------------------------------------------ */

const WORDS = [
// top-left : verbes RH (action × humain)
{ w: "recruter", x: 22, y: 22, cat: "verb-h" },
{ w: "embaucher", x: 28, y: 30, cat: "verb-h" },
{ w: "former", x: 16, y: 32, cat: "verb-h" },
{ w: "encadrer", x: 30, y: 18, cat: "verb-h" },
// top-right : rôles humains (entité × humain)
{ w: "dirigeant", x: 78, y: 22, cat: "role" },
{ w: "salarié", x: 72, y: 30, cat: "role" },
{ w: "collaborateur", x: 86, y: 28, cat: "role" },
{ w: "stagiaire", x: 70, y: 18, cat: "role" },
// bottom-left : verbes naturels (action × non-humain)
{ w: "chasser", x: 22, y: 70, cat: "verb-n" },
{ w: "courir", x: 28, y: 80, cat: "verb-n" },
{ w: "dormir", x: 14, y: 76, cat: "verb-n" },
{ w: "voler", x: 32, y: 66, cat: "verb-n" },
// bottom-right : entités non-humaines (animaux + villes)
{ w: "chien", x: 62, y: 64, cat: "anim" },
{ w: "chat", x: 68, y: 72, cat: "anim" },
{ w: "lion", x: 60, y: 78, cat: "anim" },
{ w: "Paris", x: 84, y: 66, cat: "city" },
{ w: "Lyon", x: 90, y: 74, cat: "city" },
{ w: "Nantes", x: 82, y: 82, cat: "city" }];


const CATS = {
  "verb-h": { label: "Verbes RH", dot: "var(--vermillon)" },
  "role": { label: "Rôles humains", dot: "var(--ink)" },
  "verb-n": { label: "Verbes naturels", dot: "var(--vermillon-deep)" },
  "anim": { label: "Animaux", dot: "var(--ink-blue)" },
  "city": { label: "Villes", dot: "var(--ink-60)" }
};

const dist2D = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
const simScore = (a, b) => Math.max(0, 1 - dist2D(a, b) / 90);
const findWord = (w) => WORDS.find((x) => x.w === w);

const ANALOGIES = [
{
  id: "synonymes",
  label: "Synonymes",
  note: "Les mots qui veulent dire la même chose se collent. Le modèle apprend cette proximité sans qu'on la lui décrive — juste en lisant.",
  pairs: [
  { from: "recruter", to: "embaucher" },
  { from: "chien", to: "chat" },
  { from: "Paris", to: "Lyon" }]

},
{
  id: "action-agent",
  label: "Action → agent",
  note: "Les vecteurs « action humaine → agent humain » et « action animale → agent animal » sont à peu près parallèles. La géométrie encode la relation — personne ne l'a écrite à la main.",
  pairs: [
  { from: "recruter", to: "dirigeant" },
  { from: "chasser", to: "lion" }]

},
{
  id: "humain-nature",
  label: "Humain → nature",
  note: "Même direction pour « passer du monde du travail au monde naturel », que l'on parte d'un verbe ou d'un nom. Les axes ne sont jamais étiquetés explicitement — ils émergent.",
  pairs: [
  { from: "recruter", to: "chasser" },
  { from: "dirigeant", to: "lion" }]

}];


function EmbeddingMap() {
  const [mode, setMode] = useState("carte");
  const [hover, setHover] = useState(null);
  const [picked, setPicked] = useState([]);
  const [analogyId, setAnalogyId] = useState(ANALOGIES[0].id);

  const neighbors = useMemo(() => {
    if (mode !== "carte" || !hover) return [];
    const me = findWord(hover);
    if (!me) return [];
    return WORDS.
    filter((w) => w.w !== hover).
    map((w) => ({ w, sim: simScore(me, w), d: dist2D(me, w) })).
    sort((a, b) => a.d - b.d).
    slice(0, 3);
  }, [mode, hover]);

  const onPointClick = (word) => {
    if (mode !== "distance") return;
    setPicked((prev) => {
      if (prev.length === 0) return [word];
      if (prev.length === 1) return prev[0] === word ? [] : [prev[0], word];
      return [word];
    });
  };
  const pickedSim = picked.length === 2 ? simScore(findWord(picked[0]), findWord(picked[1])) : null;
  const analogy = ANALOGIES.find((a) => a.id === analogyId);

  const lines = useMemo(() => {
    if (mode === "carte" && hover) {
      const me = findWord(hover);
      return neighbors.map((n, i) => ({ x1: me.x, y1: me.y, x2: n.w.x, y2: n.w.y, rank: i, sim: n.sim }));
    }
    if (mode === "distance" && picked.length === 2) {
      const a = findWord(picked[0]);const b = findWord(picked[1]);
      return [{ x1: a.x, y1: a.y, x2: b.x, y2: b.y, rank: 0, sim: pickedSim }];
    }
    if (mode === "analogie" && analogy) {
      return analogy.pairs.map((p, i) => {
        const a = findWord(p.from);const b = findWord(p.to);
        return { x1: a.x, y1: a.y, x2: b.x, y2: b.y, rank: i, arrow: true };
      });
    }
    return [];
  }, [mode, hover, neighbors, picked, analogy, pickedSim]);

  const emphasized = useMemo(() => {
    const s = new Set();
    if (mode === "carte" && hover) {
      s.add(hover);
      neighbors.forEach((n) => s.add(n.w.w));
    }
    if (mode === "distance") picked.forEach((p) => s.add(p));
    if (mode === "analogie" && analogy) {
      analogy.pairs.forEach((p) => {s.add(p.from);s.add(p.to);});
    }
    return s;
  }, [mode, hover, neighbors, picked, analogy]);

  const hint =
  mode === "carte" ?
  "Survolez un mot — le modèle trace ses trois plus proches voisins. Mêmes catégories, sens proches." :
  mode === "distance" ?
  "Cliquez deux mots successivement. L'écart dans l'espace devient une mesure de proximité sémantique." :
  "Choisissez une relation. Les flèches montrent que les mêmes directions encodent les mêmes relations.";

  return (
    <div className="panel">
      <div className="panel__head">
        <div>
          <div className="panel__title">Chaque jeton devient une coordonnée</div>
          <div className="panel__hint">{hint}</div>
        </div>
        <div className="panel__id">DÉMO · 02</div>
      </div>

      <div className="emb-modes" role="tablist" aria-label="Mode d'exploration">
        {["carte", "distance", "analogie"].map((m) =>
        <button
          key={m}
          role="tab"
          aria-selected={mode === m}
          className={"emb-mode" + (mode === m ? " is-active" : "")}
          onClick={() => {setMode(m);setHover(null);setPicked([]);}}>
          
            {m === "carte" ? "Carte" : m === "distance" ? "Distance" : "Analogie"}
          </button>
        )}
      </div>

      <div className="scatter">
        <div className="scatter__axis-line scatter__axis-line--x" aria-hidden="true" />
        <div className="scatter__axis-line scatter__axis-line--y" aria-hidden="true" />

        <div className="scatter__pole scatter__pole--left">Action <small>· verbe</small></div>
        <div className="scatter__pole scatter__pole--right">Entité <small>· nom</small></div>
        <div className="scatter__pole scatter__pole--top">Humain · social</div>
        <div className="scatter__pole scatter__pole--bottom">Non-humain · nature</div>

        <div className="scatter__quad scatter__quad--tl">Verbes RH</div>
        <div className="scatter__quad scatter__quad--tr">Rôles humains</div>
        <div className="scatter__quad scatter__quad--bl">Verbes naturels</div>
        <div className="scatter__quad scatter__quad--br">Animaux · villes</div>

        <svg className="scatter__svg" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
          <defs>
            <marker id="emb-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
              <path d="M0,0 L10,5 L0,10 z" fill="var(--vermillon)" />
            </marker>
          </defs>
          {lines.map((l, i) => {
            const stroke =
            mode === "distance" ?
            `rgba(14, 34, 85, ${0.35 + (l.sim || 0) * 0.65})` :
            mode === "analogie" ?
            "var(--vermillon)" :
            `rgba(181, 72, 44, ${0.7 - i * 0.18})`;
            return (
              <line
                key={i}
                x1={l.x1} y1={l.y1} x2={l.x2} y2={l.y2}
                stroke={stroke}
                strokeDasharray={mode === "carte" && i > 0 ? "1.5 1.2" : undefined}
                markerEnd={l.arrow ? "url(#emb-arrow)" : undefined}
                vectorEffect="non-scaling-stroke"
                style={{ strokeWidth: mode === "analogie" ? "1.8px" : "1.4px" }} />);


          })}
        </svg>

        {WORDS.map((w) =>
        <span
          key={w.w}
          className={
          "scatter__pt" + (
          emphasized.has(w.w) ? " is-hi" : "") + (
          mode === "distance" ? " is-clickable" : "")
          }
          style={{ left: `${w.x}%`, top: `${w.y}%` }}
          data-cat={w.cat}
          onMouseEnter={() => mode === "carte" && setHover(w.w)}
          onMouseLeave={() => mode === "carte" && setHover(null)}
          onClick={() => onPointClick(w.w)}>
          
            {w.w}
          </span>
        )}
      </div>

      {mode === "carte" &&
      <div className="emb-footer">
          {hover ?
        <div className="emb-footer__list">
              <div className="emb-footer__head">
                Plus proches voisins de <b>« {hover} »</b>
              </div>
              <ol>
                {neighbors.map((n, i) =>
            <li key={n.w.w}>
                    <span className="emb-footer__rank">#{i + 1}</span>
                    <span className="emb-footer__word">{n.w.w}</span>
                    <span className="emb-footer__sim">{(n.sim * 100).toFixed(0)} %</span>
                  </li>
            )}
              </ol>
            </div> :

        <div className="emb-footer__placeholder">Survolez un mot pour voir ses trois plus proches voisins.</div>
        }
        </div>
      }

      {mode === "distance" &&
      <div className="emb-footer">
          {picked.length === 0 &&
        <div className="emb-footer__placeholder">Choisissez un premier mot dans la carte.</div>
        }
          {picked.length === 1 &&
        <div className="emb-footer__placeholder">
              Premier mot&nbsp;: <b>« {picked[0]} »</b>. Choisissez maintenant le second.
            </div>
        }
          {picked.length === 2 &&
        <div className="emb-distance">
              <div className="emb-distance__pair">
                <span className="emb-distance__w">{picked[0]}</span>
                <span className="emb-distance__arrow">↔</span>
                <span className="emb-distance__w">{picked[1]}</span>
              </div>
              <div className="emb-distance__bar">
                <div className="emb-distance__fill" style={{ width: `${pickedSim * 100}%` }} />
              </div>
              <div className="emb-distance__num">
                <b>{(pickedSim * 100).toFixed(0)} %</b> de similarité
                <span className="emb-distance__sep"> · </span>
                <span>{dist2D(findWord(picked[0]), findWord(picked[1])).toFixed(1)} d'écart</span>
              </div>
              <button className="btn btn--ghost btn--xs" onClick={() => setPicked([])}>Réinitialiser</button>
            </div>
        }
        </div>
      }

      {mode === "analogie" &&
      <div className="emb-footer">
          <div className="emb-analogy__chips">
            {ANALOGIES.map((a) =>
          <button
            key={a.id}
            className={"emb-analogy__chip" + (a.id === analogyId ? " is-active" : "")}
            onClick={() => setAnalogyId(a.id)}>
            
                {a.label}
              </button>
          )}
          </div>
          <div className="emb-analogy__note">{analogy.note}</div>
        </div>
      }

      <div className="scatter__cat-legend">
        {Object.entries(CATS).map(([k, v]) =>
        <span key={k}><i style={{ background: v.dot }}></i> {v.label}</span>
        )}
      </div>

      <p className="cap">
        <b>Projection 2D, schématique.</b> Dans un vrai modèle, chaque jeton vit dans un espace de 4 096 ou 12 288 dimensions. On a aplati pour l'œil — mais ces axes 
        <i>Action ↔ Entité </i>
        et <i>Humain ↔ Nature</i> ne sont pas écrits par un ingénieur. Ils émergent de l'entraînement,
        à côté de milliers d'autres axes que le modèle découvre seul.
      </p>
    </div>);

}

/* ------------------------------------------------------------------
   AttentionHeatmap — pre-baked weights for one sentence.
   ------------------------------------------------------------------ */

const ATTN_TOKENS = ["Le", "dirigeant", "a", "embauché", "sa", "Recrue", "IA", "parce", "qu'", "elle", "parle", "français"];

const ATTN_W = {
  "elle": [0.04, 0.42, 0.02, 0.06, 0.05, 0.28, 0.03, 0.02, 0.02, 0.06, 0.00, 0.00],
  "parle": [0.02, 0.18, 0.02, 0.06, 0.04, 0.20, 0.18, 0.02, 0.02, 0.22, 0.04, 0.00],
  "français": [0.02, 0.10, 0.02, 0.04, 0.04, 0.14, 0.30, 0.02, 0.02, 0.10, 0.18, 0.04],
  "Recrue": [0.06, 0.30, 0.04, 0.20, 0.08, 0.20, 0.10, 0.00, 0.00, 0.00, 0.00, 0.00],
  "IA": [0.04, 0.10, 0.02, 0.12, 0.04, 0.58, 0.08, 0.00, 0.00, 0.00, 0.00, 0.00],
  "embauché": [0.06, 0.55, 0.10, 0.10, 0.04, 0.06, 0.04, 0.00, 0.00, 0.00, 0.00, 0.00]
};

function AttentionHeatmap() {
  const focusable = Object.keys(ATTN_W);
  const [active, setActive] = useState("elle");
  const weights = ATTN_W[active] || ATTN_TOKENS.map(() => 0);
  const activeIdx = ATTN_TOKENS.indexOf(active);
  const max = Math.max(...weights);

  // Top-3 attended (past tokens only)
  const top = weights
    .map((w, i) => ({ tok: ATTN_TOKENS[i], w, i }))
    .filter(x => x.i < activeIdx && x.w >= 0.04)
    .sort((a, b) => b.w - a.w)
    .slice(0, 3);

  return (
    <div className="panel panel--inset">
      <div className="panel__head">
        <div>
          <div className="panel__title">Sur quels mots précédents le modèle « regarde »</div>
          <div className="panel__hint">Choisissez un mot dans la phrase ci-dessous. Les <b>barres verticales</b> sous chaque mot précédent montrent la part d'attention que le mot choisi lui consacre.</div>
        </div>
        <div className="panel__id">DÉMO · 03</div>
      </div>

      {/* The sentence + per-token attention bars, perfectly aligned */}
      <div className="attn2">
        <div className="attn2__sentence-label">Phrase d'exemple</div>
        <div
          className="attn2__row attn2__row--tokens"
          style={{ gridTemplateColumns: `repeat(${ATTN_TOKENS.length}, 1fr)` }}
        >
          {ATTN_TOKENS.map((tok, i) => {
            const isActive = i === activeIdx;
            const isPast = i < activeIdx;
            const w = isPast ? weights[i] : 0;
            const bg = isActive
              ? 'var(--vermillon)'
              : isPast && w > 0
                ? `rgba(181,72,44,${0.06 + (w / (max || 1)) * 0.42})`
                : 'transparent';
            const color = isActive ? 'var(--paper)' : (i > activeIdx ? 'var(--ink-40)' : 'var(--ink)');
            const border = isActive ? '1px solid var(--vermillon)' : '1px solid var(--ink-10)';
            return (
              <div
                key={i}
                className={'attn2__tok' + (isActive ? ' is-active' : '') + (i > activeIdx ? ' is-future' : '')}
                style={{ background: bg, color, border }}
                title={isPast ? `${active} → ${tok} : ${(w * 100).toFixed(0)}%` : undefined}
              >
                {tok}
                {isActive && <span className="attn2__tok-flag">focus</span>}
              </div>
            );
          })}
        </div>

        <div
          className="attn2__row attn2__row--bars"
          style={{ gridTemplateColumns: `repeat(${ATTN_TOKENS.length}, 1fr)` }}
          aria-label="Barres d'attention"
        >
          {ATTN_TOKENS.map((tok, i) => {
            const isPast = i < activeIdx;
            const w = isPast ? weights[i] : 0;
            const h = (w / (max || 1)) * 100;
            const isTop = top.length > 0 && top[0].i === i;
            return (
              <div className="attn2__col" key={i}>
                <div className="attn2__bar-track">
                  {isPast && w > 0 && (
                    <div
                      className={'attn2__bar-fill' + (isTop ? ' is-top' : '')}
                      style={{ height: `${h}%` }}
                    />
                  )}
                </div>
                <div className="attn2__pct">
                  {isPast && w >= 0.04 ? `${Math.round(w * 100)}%` : ''}
                </div>
              </div>
            );
          })}
        </div>
      </div>

      {/* Summary statement */}
      <div className="attn2__summary">
        <div className="attn2__summary-lead">
          Quand le modèle traite
          <span className="attn2__summary-active">« {active} »</span>
          il consacre son attention surtout à&nbsp;:
        </div>
        <ol className="attn2__top">
          {top.length === 0 && <li className="attn2__top-empty">Aucun mot précédent ne reçoit plus de 4 %.</li>}
          {top.map((t, i) => (
            <li key={t.i}>
              <span className="attn2__top-rank">#{i + 1}</span>
              <span className="attn2__top-word">{t.tok}</span>
              <span className="attn2__top-bar">
                <span className="attn2__top-bar-fill" style={{ width: `${t.w * 100}%` }} />
              </span>
              <span className="attn2__top-pct">{Math.round(t.w * 100)} %</span>
            </li>
          ))}
        </ol>
      </div>

      {/* Focus picker */}
      <div className="attn2__picker">
        <span className="attn2__picker-label">Choisissez un autre mot à analyser&nbsp;:</span>
        <div className="attn2__picker-chips">
          {focusable.map((t) => (
            <button
              key={t}
              className={'attn2__chip' + (t === active ? ' is-active' : '')}
              onClick={() => setActive(t)}
            >
              {t}
            </button>
          ))}
        </div>
      </div>

      <p className="cap" style={{ marginTop: 24 }}>
        <b>Lecture.</b> Le mot vermillon est celui que le modèle est en train de traiter.
        Les barres verticales sous chaque mot <i>précédent</i> indiquent la part de son attention
        qu'il leur consacre. La barre la plus haute en vermillon est le mot qui « capte » le plus.
        Pour <i>« elle »</i>, ce sont <i>« dirigeant »</i> et <i>« Recrue »</i> — les deux candidats
        possibles du pronom.
      </p>
    </div>
  );
}

/* ------------------------------------------------------------------
   NextTokenPredictor — branching pre-baked tree.
   The user can pick any candidate and see the prediction tree fork.
   ------------------------------------------------------------------ */

// Each node keyed by history.join("|"). "" is the root.
// Token strings include their leading space when needed.
const TREE = {
  "": {
    ctx: "Recrutez votre premier ",
    next: [
      { tok: "Collaborateur", p: 0.42 },
      { tok: "Stagiaire",     p: 0.16 },
      { tok: "salarié",       p: 0.12 },
      { tok: "outil",         p: 0.09 },
      { tok: "modèle",        p: 0.07 },
      { tok: "agent",         p: 0.06 },
      { tok: "intégrateur",   p: 0.03 },
      { tok: "produit",       p: 0.02 },
    ],
  },

  // === branche dominante : Collaborateur → IA → . ===
  "Collaborateur": {
    ctx: "Recrutez votre premier Collaborateur ",
    next: [
      { tok: "IA",      p: 0.74 },
      { tok: "à",       p: 0.06 },
      { tok: "en",      p: 0.05 },
      { tok: "dès",     p: 0.04 },
      { tok: "humain",  p: 0.03 },
      { tok: "junior",  p: 0.02 },
      { tok: ".",       p: 0.02 },
      { tok: "pour",    p: 0.02 },
    ],
  },
  "Collaborateur|IA": {
    ctx: "Recrutez votre premier Collaborateur IA",
    next: [
      { tok: ".",     p: 0.48 },
      { tok: " —",    p: 0.12 },
      { tok: ",",     p: 0.10 },
      { tok: " en",   p: 0.07 },
      { tok: " avec", p: 0.06 },
      { tok: " sans", p: 0.05 },
      { tok: " pour", p: 0.04 },
      { tok: " et",   p: 0.03 },
    ],
  },

  // === branche alternative : Stagiaire ===
  "Stagiaire": {
    ctx: "Recrutez votre premier Stagiaire ",
    next: [
      { tok: "IA",     p: 0.46 },
      { tok: "à",      p: 0.10 },
      { tok: "en",     p: 0.09 },
      { tok: "pour",   p: 0.08 },
      { tok: "dès",    p: 0.07 },
      { tok: "humain", p: 0.06 },
      { tok: ",",      p: 0.05 },
      { tok: "puis",   p: 0.04 },
    ],
  },
  "Stagiaire|IA": {
    ctx: "Recrutez votre premier Stagiaire IA",
    next: [
      { tok: " avant",  p: 0.18 },
      { tok: ",",       p: 0.16 },
      { tok: " puis",   p: 0.14 },
      { tok: ".",       p: 0.12 },
      { tok: " pour",   p: 0.10 },
      { tok: " sans",   p: 0.09 },
      { tok: " — ",     p: 0.07 },
      { tok: " en",     p: 0.06 },
    ],
  },

  // === alternatives au premier niveau ===
  "salarié": {
    ctx: "Recrutez votre premier salarié ",
    next: [
      { tok: ".",      p: 0.18 },
      { tok: "en",     p: 0.14 },
      { tok: "dès",    p: 0.12 },
      { tok: "à",      p: 0.11 },
      { tok: ",",      p: 0.10 },
      { tok: "pour",   p: 0.09 },
      { tok: "avant",  p: 0.07 },
      { tok: "junior", p: 0.06 },
    ],
  },
  "outil": {
    ctx: "Recrutez votre premier outil ",
    next: [
      { tok: "de",            p: 0.30 },
      { tok: "d'",            p: 0.16 },
      { tok: "IA",            p: 0.14 },
      { tok: ",",             p: 0.10 },
      { tok: ".",             p: 0.08 },
      { tok: "métier",        p: 0.07 },
      { tok: "professionnel", p: 0.06 },
      { tok: "numérique",     p: 0.05 },
    ],
  },
  "modèle": {
    ctx: "Recrutez votre premier modèle ",
    next: [
      { tok: "de",        p: 0.34 },
      { tok: "d'",        p: 0.18 },
      { tok: "IA",        p: 0.14 },
      { tok: ",",         p: 0.08 },
      { tok: "génératif", p: 0.07 },
      { tok: "interne",   p: 0.06 },
      { tok: ".",         p: 0.05 },
      { tok: "métier",    p: 0.04 },
    ],
  },
  "agent": {
    ctx: "Recrutez votre premier agent ",
    next: [
      { tok: "IA",        p: 0.42 },
      { tok: "autonome",  p: 0.16 },
      { tok: "de",        p: 0.10 },
      { tok: "logiciel",  p: 0.08 },
      { tok: ",",         p: 0.07 },
      { tok: "commercial",p: 0.06 },
      { tok: "interne",   p: 0.05 },
      { tok: ".",         p: 0.04 },
    ],
  },
};

// Append a token to a context string, with smart spacing.
function appendTok(ctx, tok) {
  if (!ctx) return tok;
  if (ctx.endsWith(" ") || tok.startsWith(" ") || /^[.,;:!?—]/.test(tok)) return ctx + tok;
  return ctx + " " + tok;
}
// Reconstruct full context from history when we've stepped past the tree.
function reconstructCtx(history) {
  for (let i = history.length; i >= 0; i--) {
    const key = history.slice(0, i).join("|");
    const node = TREE[key];
    if (node) {
      let s = node.ctx;
      for (let j = i; j < history.length; j++) s = appendTok(s, history[j]);
      return s;
    }
  }
  return TREE[""].ctx;
}

function NextTokenPredictor() {
  const [history, setHistory] = useState([]);
  const [liveCache, setLiveCache] = useState({});      // historyKey → { ctx, next, live: true }
  const [liveLoading, setLiveLoading] = useState(false);
  const [liveError, setLiveError] = useState(null);
  const [liveSuggestion, setLiveSuggestion] = useState(null); // single-token compare
  const [liveSuggestionLoading, setLiveSuggestionLoading] = useState(false);
  const inflightRef = useRef(null);

  const key = history.join("|");
  const treeNode = TREE[key];
  const cachedNode = liveCache[key];
  const cur = treeNode || cachedNode;
  const isLive = !treeNode && !!cachedNode;
  const ctx = cur ? cur.ctx : reconstructCtx(history);
  const sortedCands = cur ? [...cur.next].sort((a, b) => b.p - a.p) : [];
  const topTok = sortedCands[0]?.tok;
  const needsFetch = !cur && history.length > 0 && !liveLoading;

  // Fetch live distribution from Claude for unknown branches.
  const fetchDistribution = async (h) => {
    const k = h.join("|");
    if (inflightRef.current === k) return;
    if (TREE[k] || liveCache[k]) return;
    inflightRef.current = k;
    setLiveLoading(true);
    setLiveError(null);

    const fullCtx = reconstructCtx(h);
    const prompt =
`Tu joues le rôle d'un modèle de langue français. Voici un contexte exact (à terminer, ne pas répéter) :

« ${fullCtx} »

Donne les 8 prochains jetons les plus probables, avec leur probabilité estimée (entre 0 et 1, sommant à environ 1).

Format de réponse STRICT — uniquement du JSON valide, sans préambule, sans markdown, sans backticks, sans commentaire :
[{"tok":"...","p":0.45},{"tok":"...","p":0.18}, ...]

Règles :
- Un "tok" est un mot, un fragment de mot, ou un signe de ponctuation seul.
- Si le jeton doit suivre une espace, mets l'espace au début du "tok" (ex: " avec", " puis").
- Si c'est de la ponctuation collée au mot précédent (",", ".", "!", "?"), pas d'espace.
- Trie par probabilité décroissante.
- Exactement 8 entrées.

Réponds avec le JSON et rien d'autre.`;

    try {
      const r = await window.claude.predict("distribution", fullCtx);
      const m = (r || "").match(/\[[\s\S]*\]/);
      if (!m) throw new Error("réponse non-JSON");
      const parsed = JSON.parse(m[0]);
      if (!Array.isArray(parsed) || parsed.length === 0) throw new Error("liste vide");
      const cleaned = parsed
        .filter(x => x && typeof x.tok === "string" && typeof x.p === "number" && x.tok.length > 0)
        .slice(0, 8)
        .map(x => ({ tok: x.tok, p: Math.max(0, Math.min(1, x.p)) }));
      if (cleaned.length === 0) throw new Error("aucun candidat valide");
      setLiveCache(prev => ({ ...prev, [k]: { ctx: fullCtx, next: cleaned, live: true } }));
    } catch (e) {
      setLiveError(e.message || "erreur");
    } finally {
      setLiveLoading(false);
      inflightRef.current = null;
    }
  };

  useEffect(() => {
    if (needsFetch) fetchDistribution(history);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [key]);

  const pick = (tok) => {
    setHistory((h) => [...h, tok]);
    setLiveSuggestion(null);
  };
  const advanceTop = () => { if (topTok) pick(topTok); };
  const stepBack = () => { setHistory((h) => h.slice(0, -1)); setLiveSuggestion(null); setLiveError(null); };
  const reset = () => { setHistory([]); setLiveSuggestion(null); setLiveError(null); };
  const retry = () => { setLiveError(null); fetchDistribution(history); };

  const askLive = async () => {
    setLiveSuggestionLoading(true);
    setLiveSuggestion(null);
    try {
      const prompt = `Tu es un modèle de langue. Étant donné le contexte exact ci-dessous (en français), donne UNIQUEMENT le mot ou caractère suivant le plus probable, sans guillemets, sans phrase d'explication, sans ponctuation supplémentaire. Si le mot suivant est de la ponctuation, donne juste ce signe.\n\nContexte : « ${ctx}»\n\nProchain jeton :`;
      const r = await window.claude.predict("next", ctx);
      const out = (r || '').trim().split('\n')[0].slice(0, 24);
      setLiveSuggestion(out);
    } catch (e) {
      setLiveSuggestion("(erreur — réseau)");
    } finally {
      setLiveSuggestionLoading(false);
    }
  };

  // Display: split ctx into baseline + chosen suffix highlighted in vermillon.
  const rootCtx = TREE[""].ctx;
  const chosenSuffix = ctx.startsWith(rootCtx) ? ctx.slice(rootCtx.length) : "";

  return (
    <div className="panel">
      <div className="panel__head">
        <div>
          <div className="panel__title">Prédire le mot suivant, encore et encore</div>
          <div className="panel__hint">
            À chaque étape, le modèle propose une distribution sur tout son vocabulaire — on affiche le top-8.
            <b> Cliquez un mot pour suivre cette branche</b> : la distribution suivante change vraiment.
          </div>
        </div>
        <div className="panel__id">DÉMO · 04</div>
      </div>

      <div className="predict">
        <div className="predict__ctx">
          {rootCtx}
          {chosenSuffix && <span className="predict__ctx-chosen">{chosenSuffix}</span>}
          <span className="predict__ctx-cursor" aria-hidden="true" />
        </div>

        {liveLoading && !cur && (
          <div className="predict__fetching">
            <span className="predict__fetching-spinner" aria-hidden="true" />
            <div>
              <div className="predict__fetching-title">Calcul de la distribution en cours</div>
              <div className="predict__fetching-sub">Cette branche n'est pas en cache — un vrai modèle est interrogé pour générer les 8 prochains jetons.</div>
            </div>
          </div>
        )}

        {liveError && !cur && (
          <div className="predict__error">
            <b>Échec du calcul.</b> {liveError}.
            <button className="btn btn--ghost btn--xs" onClick={retry} style={{ marginLeft: 12 }}>Réessayer</button>
            <button className="btn btn--ghost btn--xs" onClick={stepBack} style={{ marginLeft: 6 }}>← Reculer</button>
          </div>
        )}

        {cur && (
          <React.Fragment>
            {isLive && (
              <div className="predict__source">
                <span className="predict__source-dot" /> Distribution calculée en direct par Claude · branche hors cache
              </div>
            )}
            <div className="predict__candidates">
              {sortedCands.map((c, i) => (
                <button
                  key={c.tok + "-" + i}
                  className={"predict__cand" + (i === 0 ? " is-top" : "")}
                  onClick={() => pick(c.tok)}
                  title="Cliquez pour suivre cette branche"
                >
                  <span className="predict__cand-bar" style={{ width: `${c.p * 100}%` }} />
                  <span className="predict__cand-tok">{JSON.stringify(c.tok)}</span>
                  <span className="predict__cand-pct">{(c.p * 100).toFixed(0)} %</span>
                  <span className="predict__cand-pick">{i === 0 ? 'Top 1' : '→'}</span>
                </button>
              ))}
            </div>
          </React.Fragment>
        )}

        <div className="predict__controls">
          <button
            className="btn btn--primary"
            onClick={advanceTop}
            disabled={!cur || !topTok || liveLoading}
            title="Suivre la prédiction la plus probable"
          >
            Suivre le Top-1 →
          </button>
          <button
            className="btn btn--ghost"
            onClick={stepBack}
            disabled={history.length === 0}
          >
            ← Reculer
          </button>
          <button className="btn btn--ghost" onClick={reset} disabled={history.length === 0}>
            Recommencer
          </button>
          <button className="btn btn--ghost" onClick={askLive} disabled={liveSuggestionLoading || !cur}>
            {liveSuggestionLoading ? 'Comparaison…' : 'Comparer avec Claude'}
          </button>
        </div>

        {liveSuggestion && (
          <div className="predict__live">
            <b>Claude propose comme prochain jeton&nbsp;: </b>
            <span>{JSON.stringify(liveSuggestion)}</span>
          </div>
        )}
      </div>

      <p className="cap" style={{ marginTop: 24 }}>
        <b>Une boucle, pas une réflexion.</b> Le modèle ne « pense » pas la phrase entière.
        Il tire un jeton, le rajoute au contexte, recalcule la distribution, retire un jeton.
        Les chemins canoniques sont en cache local pour la fluidité ; les bifurcations
        inédites sont calculées en direct par Claude. Vous pouvez écrire toute la phrase.
      </p>
    </div>
  );
}

/* ------------------------------------------------------------------
   LayerStack — visual of an attention stack.
   ------------------------------------------------------------------ */

function LayerStack() {
  const layers = [
  { n: 1, name: "Coordonnée & position", sees: "Les jetons reçoivent une coordonnée + leur place dans la phrase." },
  { n: 2, name: "Attention — couche basse", sees: "Détecte la grammaire locale : déterminant, accord, ponctuation." },
  { n: 6, name: "Attention — couche médiane", sees: "Lie les pronoms à leurs antécédents, repère les questions." },
  { n: 12, name: "Attention — couche profonde", sees: "Suit des thèmes sur plusieurs paragraphes, anticipe le style." },
  { n: 24, name: "Attention — couche très profonde", sees: "Combine raisonnement, intention, ton du locuteur." },
  { n: 32, name: "Sortie — prédiction du jeton", sees: "Produit la distribution sur le vocabulaire pour le jeton suivant.", pivot: true }];

  return (
    <div className="layers">
      {layers.map((l) =>
      <div key={l.n} className={"layer" + (l.pivot ? " is-pivot" : "")}>
          <div className="layer__n">{l.n}</div>
          <div className="layer__name">{l.name}</div>
          <div className="layer__sees">« {l.sees} »</div>
        </div>
      )}
    </div>);

}

Object.assign(window, { TokenizerDemo, EmbeddingMap, AttentionHeatmap, NextTokenPredictor, LayerStack });