Note. 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.
);
}
/* ------------------------------------------------------------------
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 (
Projection 2D, schématique. 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
Action ↔ Entité
et Humain ↔ Nature 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.
Choisissez un mot dans la phrase ci-dessous. Les barres verticales sous chaque mot précédent montrent la part d'attention que le mot choisi lui consacre.
DÉMO · 03
{/* The sentence + per-token attention bars, perfectly aligned */}
Quand le modèle traite
« {active} »
il consacre son attention surtout à :
{top.length === 0 &&
Aucun mot précédent ne reçoit plus de 4 %.
}
{top.map((t, i) => (
#{i + 1}{t.tok}{Math.round(t.w * 100)} %
))}
{/* Focus picker */}
Choisissez un autre mot à analyser :
{focusable.map((t) => (
))}
Lecture. Le mot vermillon est celui que le modèle est en train de traiter.
Les barres verticales sous chaque mot précédent 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 « elle », ce sont « dirigeant » et « Recrue » — les deux candidats
possibles du pronom.
);
}
/* ------------------------------------------------------------------
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.complete(prompt);
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.complete(prompt);
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 (
Prédire le mot suivant, encore et encore
À chaque étape, le modèle propose une distribution sur tout son vocabulaire — on affiche le top-8.
Cliquez un mot pour suivre cette branche : la distribution suivante change vraiment.
DÉMO · 04
{rootCtx}
{chosenSuffix && {chosenSuffix}}
{liveLoading && !cur && (
Calcul de la distribution en cours
Cette branche n'est pas en cache — un vrai modèle est interrogé pour générer les 8 prochains jetons.
)}
{liveError && !cur && (
Échec du calcul. {liveError}.
)}
{cur && (
{isLive && (
Distribution calculée en direct par Claude · branche hors cache
)}
{sortedCands.map((c, i) => (
))}
)}
{liveSuggestion && (
Claude propose comme prochain jeton : {JSON.stringify(liveSuggestion)}
)}
Une boucle, pas une réflexion. 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.
);
}
/* ------------------------------------------------------------------
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 (