// Help overlay — spotlights each stage in turn with contextual info. // When open: dims the UI, draws a glowing outline around the current stage element, // and shows a floating info card pointing to it. const INTRO_INFO = { key: '__intro', n: 0, title: 'What am I looking at?', tag: 'a live picture of a language model thinking', body: 'This is a visualization of how a large language model (the kind that powers chatbots) turns your prompt into its next word — one word at a time, forever. Type something at the top, hit play, and watch it run.', body2: 'Inside every LLM there’s a long assembly line of math. Text goes in, flows through five distinct stages of processing, and a new word pops out the end. Then the whole thing runs again with that new word tacked on. The panels down the middle of the screen are those five stages, top to bottom. The narration on the right tells you what each stage is doing in plain English.', body3: 'None of the numbers you see come from a real model — I’m drawing pretend values so you can follow the structure. But the shape of what’s happening — the pieces, their order, and why they exist — is genuinely how it works.', }; const STAGE_INFO = [ { key: 'stage-tokenize', n: 1, title: 'Tokenize', tag: 'text → integer ids', body: 'Your text gets chopped into sub-word pieces. Each piece gets an integer id — the model only speaks numbers. Look for the chips with text on top and #ids underneath; the amber one is the token currently being processed.', what_real: 'Ids, sub-words, and the leading-space distinction (“ the” ≠ “the”) are real BPE behavior.', what_fake: 'My splitter is a toy — real BPE merges pieces like “token”+“ization”. Ids here are hashes, not GPT-2 ids.', }, { key: 'stage-embed', n: 2, title: 'Embed + Position', tag: 'id → 4096-dim vector', body: 'Each id pulls one row from a giant lookup table (the embedding matrix). That row is the token’s “fingerprint” — ~4096 numbers that encode what it means. Position information gets added so the model knows word order.', what_real: 'The row-lookup metaphor is exact. Real shape is 50000 × 4096, ~200M numbers.', what_fake: 'Showing a 14×32 slice. Modern LLMs use RoPE inside attention instead of position-adding.', }, { key: 'stage-attention', n: 3, title: 'Multi-head self-attention', tag: 'softmax(QKᵀ/√d)·V', body: 'Each token asks a question (Q), every past token advertises what it has (K), and the match decides how much to mix their content (V). The triangular shape is the causal mask — you can only look backward. Multiple “heads” do this in parallel, each specializing (recent, uniform, first-token, mid-range).', what_real: 'Causal mask, 8 heads, Q/K/V split, head specialization — all real findings.', what_fake: 'Patterns are cleaned up for readability. Real attention is messier.', }, { // Spotlights the residual-stream rain panel (bottom right). Uses the // three-paragraph body layout because the concept spans the whole model. key: 'stage-residual', n: null, title: 'The residual stream', tag: 'the main bus every layer reads from + writes to', body: 'Here\'s the piece that ties it all together. Every token carries a vector — roughly 4096 numbers — through the entire model. That vector is the residual stream. Attention reads it, writes an update, and adds it back. MLP does the same. The vector isn\'t replaced; it accumulates.', body2: 'This happens 32 times — once per transformer block. Layer 15 can leave a note on the stream that layer 27 picks up on. It\'s how the model does multi-step reasoning inside a single forward pass.', body3: 'When researchers talk about "features," "activation steering," or interpretability — they mean directions in this stream. The rain beside this card is a nod to it; the real residual stream is the vector sitting at the heart of every stage you just saw.', }, { key: 'stage-mlp', n: 4, title: 'Feed-forward × layers', tag: 'expand · nonlinearity · project', body: 'After attention, each token’s vector expands to 4× its width, passes through a nonlinearity (GELU/SwiGLU), and squeezes back down. This is where most of the model’s parameters live — about two-thirds. The 32 dots below track stacked layers.', what_real: 'The expand–nonlinear–project shape, 4× ratio, 32-layer stack, and ⅔-of-params fact are all real.', what_fake: 'Bars are 24→48→24; real is 4096→16384→4096. Layers run top-to-bottom in one pass, not sequentially in time.', }, { key: 'stage-logits', n: 5, title: 'Logits → Sample', tag: 'linear · softmax · sample', body: 'A final projection scores every token in the vocabulary. Temperature divides those scores before softmax — low = peakier/safer, high = flatter/weirder. A random draw picks the winner and that becomes the next token. Then the whole pipeline runs again.', what_real: 'Linear head, softmax, temperature, random sampling — all exactly as done in production.', what_fake: 'I show ~10 candidates; real models rank the full 50k+ vocab every step.', }, ]; function HelpOverlay({ open, onClose, stageIdx, setStageIdx }) { const [rect, setRect] = React.useState(null); // stageIdx: -1 = intro, 0..STAGE_INFO.length-1 = tour stages. // "hasMultiBody" = uses the wider 3-paragraph card layout (residual stream). // "noTarget" = renders centered with no spotlight (intro only, now). const isIntro = stageIdx < 0; const curInfo = !isIntro && STAGE_INFO[stageIdx]; const hasMultiBody = !isIntro && curInfo && (curInfo.body2 || curInfo.body3); const noTarget = isIntro; const totalSteps = STAGE_INFO.length + 1; // intro + stages // Measure the target element every frame while open (handles scroll, resize) React.useEffect(() => { if (!open || noTarget) { setRect(null); return; } let raf; const measure = () => { const el = document.querySelector(`[data-help-target="${STAGE_INFO[stageIdx].key}"]`); if (el) { const r = el.getBoundingClientRect(); setRect({ x: r.left, y: r.top, w: r.width, h: r.height }); } raf = requestAnimationFrame(measure); }; raf = requestAnimationFrame(measure); return () => cancelAnimationFrame(raf); }, [open, stageIdx, noTarget]); // Scroll the target into view once when stage changes React.useEffect(() => { if (!open || noTarget) return; const el = document.querySelector(`[data-help-target="${STAGE_INFO[stageIdx].key}"]`); if (el) { const parent = el.closest('[data-scroll-container]') || el.parentElement; if (parent) { const er = el.getBoundingClientRect(); const pr = parent.getBoundingClientRect(); if (er.top < pr.top || er.bottom > pr.bottom) { parent.scrollTo({ top: parent.scrollTop + (er.top - pr.top) - 20, behavior: 'smooth' }); } } } }, [open, stageIdx, noTarget]); // Keyboard nav — only active when help is open React.useEffect(() => { if (!open) return; const onKey = (e) => { // Don't capture keys if user is typing in an input const t = e.target; if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return; if (e.key === 'Escape') { e.preventDefault(); onClose(); } else if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); setStageIdx((i) => Math.min(STAGE_INFO.length - 1, i + 1)); } else if (e.key === 'ArrowLeft') { e.preventDefault(); setStageIdx((i) => Math.max(-1, i - 1)); } }; // Capture phase + stopPropagation so page-level shortcuts don't also fire const wrapper = (e) => { const t = e.target; if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return; if (['Escape', 'ArrowLeft', 'ArrowRight', ' '].includes(e.key)) { e.stopPropagation(); onKey(e); } }; window.addEventListener('keydown', wrapper, true); return () => window.removeEventListener('keydown', wrapper, true); }, [open, onClose, setStageIdx]); if (!open) return null; const info = isIntro ? INTRO_INFO : STAGE_INFO[stageIdx]; // The residual-stream interstitial gets intro-style (centered, larger, multi-body) treatment. const wide = isIntro || hasMultiBody; // Position the info card to the right of the highlight, or left if no room const vw = window.innerWidth, vh = window.innerHeight; const cardW = wide ? 680 : 460; const estCardH = wide ? 560 : 440; let cardX, cardY; if (noTarget) { // center the card cardX = (vw - cardW) / 2; cardY = Math.max(40, (vh - estCardH) / 2); } else { cardY = rect ? clamp(rect.y, 20, vh - estCardH - 20) : 100; if (rect) { if (rect.x + rect.w + cardW + 30 < vw) cardX = rect.x + rect.w + 18; else if (rect.x - cardW - 30 > 0) cardX = rect.x - cardW - 18; else cardX = vw - cardW - 20; } else cardX = vw - cardW - 20; } return (