Stream slides as they're written
Stream the model's reply with LangChain and emit each slide the moment its closing tag arrives, so users watch the deck appear instead of waiting 30 seconds.
Writing a ten-slide deck takes the model 20 to 40 seconds. A spinner for 40 seconds feels broken. The same 40 seconds, with a slide appearing every four, feels fast. The total time doesn't change; what people experience does.
- <deck><TitleSlide title=nothing complete yet
- "Cobots"/><Bullets→ slide 1 ready
- Slide title="Why">…nothing complete yet
- </BulletsSlide><Card…→ slide 2 ready
Streaming with LangChain
const stream = await model.stream(messages, { signal });
for await (const chunk of stream) {
for (const slideXml of slides.push(contentText(chunk))) {
onSlide(++count, slideXml); // a whole slide just arrived
}
}Finding complete slides
Chunks are cut wherever the network cut them: in the middle of a word, a tag or an attribute. So after each chunk, the stream looks at all the text so far ():
- Until
<deckappears, the model is still writing its preamble. Nothing to do. - From there,
slideRangesfromdeck-edit.jsfinds slides. It only matches complete tags, so a slide only counts once its closing tag (or its/>) has fully arrived. - Remember how many slides you've already emitted, and return only the new ones.
In pipeline.js, each streamed slide becomes an onEvent({ type: "slide", number, xml }). In module 12 the server turns that into a live preview in the browser.
Your project so far
23 files · 2 new or changed in this lesson
src/stream.js
// A deck takes the model 20-40 seconds to write. Streaming lets us show each
// slide the moment its closing tag arrives, instead of a spinner for all of it.
import { slideRanges } from "./deck-edit.js";
/**
* Feed streamed text in with push(); each call returns the slides that became
* complete since the last call (as XML strings), in order.
*/
export function createSlideStream() {
let buffer = "";
let emitted = 0;
return {
push(text) {
buffer += text;
const deckStart = buffer.indexOf("<deck");
if (deckStart === -1) return []; // still in the model's preamble
const deck = buffer.slice(deckStart);
const ranges = slideRanges(deck); // only counts slides whose closing tag has arrived
const fresh = ranges.slice(emitted).map(({ start, end }) => deck.slice(start, end));
emitted = ranges.length;
return fresh;
},
get text() {
return buffer;
},
};
}Key takeaways
- Streaming doesn't make generation faster, but people see progress within seconds.
- A slide is complete when its closing tag arrives; chunks can split anywhere.
- Emit each complete slide once, and keep the full text for the final compile.
Sign in to run the exercise
Reading is free. Writing code here needs an account so we have somewhere to keep your Gemini key and the +35 XP you are about to earn.