/** * Bob - Yjs-only collaborator for the showcase demo. * * Bob is NOT a browser. He's a Node process that speaks the same * Yjs CRDT protocol as the editor tabs and manipulates the shared * document directly via Y.XmlElement / Y.XmlText. Every write shows * up in Alice's Tiptap editor in real time without any DOM typing, * input-rule surprises, or PM position races. * * Bob owns the "The recipe" section (top of doc). His flow: * 1. Wait for Alice's first author to land, then push two * university co-authors (MIT, ETH Zurich). * 2. Wait for the "The recipe" H2 to appear in the shared * fragment (Alice writes the scaffold from her browser). * 3. Fill slot 0 with the definition paragraph + inline Q/K/V * math atoms. * 4. Register the Vaswani 2017 paper in the shared citations * map, append a citation chip at the end of slot 0, and * spawn a Bibliography block at the bottom of the doc if * none exists. * 5. Drop the canonical attention formula as a `` * atom into slot 1. That's a different agent (not Alice) * contributing a display-math equation to the article. * 6. Fill slot 2 with the softmax explanation. * 7. Fill slot 3 with the multi-head teaser. */ import * as Y from "yjs"; import { addAuthorRecord, appendInline, broadcastCursorAtEnd, claimSlotAfterHeading, clearAwarenessCursor, findHeadingIndex, insertInlineNode, registerCitation, setAffiliationUrl, sleep, typeIntoParagraph, waitForAffiliationCount, waitForAuthorCount, waitForCondition, writeBlockMathAtSlot, type PersonaHandle, } from "./lib/yjs-client.js"; function jitter(minMs: number, maxMs: number): number { return minMs + Math.random() * (maxMs - minMs); } /** * Ensure a `` atom exists at the end of the shared * fragment. If one is already there (e.g. from a previous run that * wasn't reset) we keep it; otherwise we push a fresh, empty one * and let `BibliographyView` on Alice's side fill `renderedHtml` * via the formatting endpoint. */ function ensureBibliographySection(handle: PersonaHandle): void { const existing = handle.fragment .toArray() .find((c) => c instanceof Y.XmlElement && c.nodeName === "bibliography"); if (existing) return; handle.ydoc.transact(() => { const bib = new Y.XmlElement("bibliography"); handle.fragment.push([bib]); bib.setAttribute("renderedHtml", ""); // Tiptap doesn't require an explicit trailing paragraph after // the bibliography, but the original doc shape has one so the // cursor always has a landing spot past the biblio in Alice's // editor. const trailing = new Y.XmlElement("paragraph"); handle.fragment.push([trailing]); }); } export async function runBob( bob: PersonaHandle, speed: number, ): Promise { console.log("[bob] connected as Yjs peer, waiting for Alice's first author"); // Give Alice a beat to start typing the title before we pile on // the author list; looks more natural on camera. await sleep(jitter(800, 1200)); // 1) Authors ----------------------------------------------------- // Alice seeds the byline with herself + the "Hugging Face" // affiliation (index 1) but the modal she drives doesn't expose a // URL field for the affiliation. Bob then: // - waits for that affiliation to land in the shared Y.Array, // - patches its `url` to https://huggingface.co so the byline // renders the affiliation as a real link, // - appends two more authors that REUSE affiliation index 1 // instead of creating fresh institutions, each with a personal // huggingface.co profile URL. // Net effect on the byline: three co-authors (Alice + Bob's two) // all under the same Hugging Face affiliation, with both the // affiliation chip and each author name linkable. const seeded = await waitForAuthorCount(bob, 1, 30_000); if (!seeded) { console.warn("[bob] Alice's first author never appeared, proceeding"); } const affSeeded = await waitForAffiliationCount(bob, 1, 30_000); if (!affSeeded) { console.warn("[bob] Hugging Face affiliation never appeared, proceeding"); } await sleep(jitter(250, 500)); setAffiliationUrl(bob, 1, "https://huggingface.co"); await sleep(jitter(180, 320)); addAuthorRecord(bob, { name: "Bob Mercier", url: "https://huggingface.co/bmercier", existingAffiliationIndices: [1], }); await sleep(jitter(300, 550)); addAuthorRecord(bob, { name: "Mira Patel", url: "https://huggingface.co/mpatel", existingAffiliationIndices: [1], }); console.log("[bob] authors added (both at Hugging Face)"); // 2) Wait for scaffold ------------------------------------------ // Alice's Playwright scaffold emits the H2 headings + empty // paragraph slots. We gate on the "The recipe" heading showing // up in our synced fragment before we start writing body text // so `claimSlotAfterHeading` finds real anchors. const hasHeading = await waitForCondition( bob.fragment, () => findHeadingIndex(bob.fragment, "The recipe") !== -1, 60_000, ); if (!hasHeading) { console.warn("[bob] 'The recipe' heading never arrived, skipping body"); return; } await sleep(jitter(150, 300)); // 3) Slot 0 - definition + inline math -------------------------- // We type the prose into the paragraph via `typeIntoParagraph` // (chunked Y.XmlText inserts with human cadence so Alice sees // text stream in), then splice in Y.XmlElement("inlineMath") // atoms for Q, K, V. Each insert is a single atomic CRDT op, // no input-rule interpretation involved. const slot0 = claimSlotAfterHeading(bob.fragment, "The recipe", 0); if (!slot0) { console.warn("[bob] could not locate slot 0 of 'The recipe'"); return; } await sleep(jitter(80, 160)); const bodyCps = 38 / Math.max(0.5, speed / 2); const fastCps = 48 / Math.max(0.5, speed / 2); await typeIntoParagraph( slot0, "Attention lets a token look at every other token, scoring each pair through queries ", { cps: bodyCps, chunkSize: 2 }, undefined, bob, ); await insertInlineNode(slot0, { type: "inlineMath", latex: "Q" }, 200, bob); await typeIntoParagraph(slot0, ", keys ", { cps: bodyCps }, undefined, bob); await insertInlineNode(slot0, { type: "inlineMath", latex: "K" }, 160, bob); await typeIntoParagraph( slot0, " and values ", { cps: bodyCps }, undefined, bob, ); await insertInlineNode(slot0, { type: "inlineMath", latex: "V" }, 160, bob); await typeIntoParagraph(slot0, ".", { cps: bodyCps }, undefined, bob); await sleep(jitter(240, 400)); // 4) Citation + bibliography ----------------------------------- // Register the entry in the shared Y.Map("citations"). The // frontend's citation NodeView listens on that map, so any chip // with matching `key` auto-resolves its label from the entry. // // We still write the `label` attr ourselves so the chip renders // "(Vaswani et al., 2017)" as soon as it lands even if the // NodeView observer hasn't fired yet (APA-ish placeholder). const citationKey = "vaswani2017attention"; registerCitation(bob, citationKey, { id: citationKey, "citation-key": citationKey, type: "article-journal", title: "Attention Is All You Need", author: [ { family: "Vaswani", given: "Ashish" }, { family: "Shazeer", given: "Noam" }, { family: "Parmar", given: "Niki" }, { family: "Uszkoreit", given: "Jakob" }, { family: "Jones", given: "Llion" }, { family: "Gomez", given: "Aidan N." }, { family: "Kaiser", given: "Lukasz" }, { family: "Polosukhin", given: "Illia" }, ], issued: { "date-parts": [[2017]] }, "container-title": "Advances in Neural Information Processing Systems", volume: "30", URL: "https://arxiv.org/abs/1706.03762", DOI: "10.48550/arXiv.1706.03762", }); appendInline(slot0, [ { type: "citation", key: citationKey, label: "(Vaswani et al., 2017)", }, ]); broadcastCursorAtEnd(bob, slot0); ensureBibliographySection(bob); console.log("[bob] citation + bibliography inserted"); await sleep(jitter(320, 520)); // 5) Slot 1 - block math (the attention formula) -------------- // Atomic CRDT replace: the empty scaffold paragraph is swapped // for a `` atom carrying the canonical scaled-dot- // product-attention formula. Alice sees the KaTeX render pop // in the moment the sync hits her editor - crisp "another // agent dropped an equation" moment on camera. Short beat // before and after so the viewer registers the transition // from prose to display math. await sleep(jitter(220, 380)); const mathNode = writeBlockMathAtSlot( bob.fragment, "The recipe", 1, "\\text{Attention}(Q, K, V) = \\text{softmax}\\!\\left(\\frac{QK^\\top}{\\sqrt{d_k}}\\right) V", ); if (mathNode) { console.log("[bob] block math equation inserted"); } else { console.warn("[bob] could not insert block math"); } await sleep(jitter(320, 500)); // 6) Slot 2 - softmax explanation ------------------------------ const slot2 = claimSlotAfterHeading(bob.fragment, "The recipe", 2); if (slot2) { await sleep(jitter(80, 150)); await typeIntoParagraph( slot2, "The softmax turns those similarity scores into weights, and the weighted sum over values produces one contextual vector per token.", { cps: fastCps, chunkSize: 2 }, undefined, bob, ); } else { console.warn("[bob] could not locate slot 2"); } await sleep(jitter(200, 320)); // 7) Slot 3 - multi-head teaser -------------------------------- const slot3 = claimSlotAfterHeading(bob.fragment, "The recipe", 3); if (slot3) { await sleep(jitter(80, 150)); await typeIntoParagraph( slot3, "In practice the same operation runs in parallel through several independent projections, one per head, and the outputs are concatenated - that is multi-head attention.", { cps: fastCps, chunkSize: 2 }, undefined, bob, ); } else { console.warn("[bob] could not locate slot 3"); } await sleep(jitter(200, 320)); // Clear Bob's caret so it doesn't linger past the last paragraph // he wrote; Alice gets a clean frame for the hue drag / publish. clearAwarenessCursor(bob); console.log("[bob] scenario complete"); }