Stream tokens from an LLM straight into a SuperDoc editor, ChatGPT-style. The browser appends each text delta to the document via the SuperDoc Document API. The OpenAI key never leaves the server.
Browser βββPOST /api/generateβββΆ Node proxy (server.mjs) βββΆ OpenAI
β² β
βββββ Server-Sent Events ββββββββββββββββ
β
ββββ editor.doc.insert({ value, type: 'text' })
server.mjsreadsOPENAI_API_KEYfrom.envand re-emits OpenAI's stream as SSE events.- The browser fetches
/api/generate, parses the SSE stream, and appends tokens to the SuperDoc editor. - Tokens are buffered and flushed at most every 150ms (or immediately on a newline) to avoid one document mutation per token.
for await (const chunk of streamFromServer(prompt, signal)) {
buffer += chunk;
if (chunk.includes('\n')) flush();
else if (!pendingFlush) pendingFlush = setTimeout(flush, 150);
}
function flush() {
editor.doc.insert({ value: buffer, type: 'text' });
buffer = '';
}editor.doc is the public Document API. With no target/ref, insert appends at the end of the document. Newlines become paragraph breaks.
cp .env.example .env # then add your OPENAI_API_KEY
pnpm install
pnpm dev # runs the Node proxy and Vite togetherOpen http://localhost:5180.
- This example streams plain text. For headings, lists, tables, or bold, switch to
type: 'markdown'and buffer until you have complete blocks before callinginsert. - For tracked-change-style streaming (a human reviewer can accept/reject), pass
{ changeMode: 'tracked' }as the second argument. - The component aborts the in-flight stream on unmount, and the server aborts upstream when the client disconnects, so neither side burns tokens after Stop or navigation.
- For production, add auth, rate limiting, and per-user storage around
server.mjs.