Project 2: Real-Time Document Summarizer with Streaming UI

Project 2: Real-Time Document Summarizer with Streaming UI

Deep Dive Learning Guide

Source: AI_SDK_LEARNING_PROJECTS.md Main Programming Language: TypeScript Alternative Languages: JavaScript, Python, Go Difficulty: Level 2: Intermediate (The Developer) Time Estimate: 1 week Prerequisites: React/Next.js basics, TypeScript fundamentals


Learning Objectives

By completing this project, you will master:

  1. Server-Sent Events (SSE) Architecture - Understand how to implement unidirectional real-time data flow from server to client over HTTP
  2. AI SDK streamText API - Learn to use the core streaming function that powers ChatGPT-style interfaces
  3. Async Iterators and AsyncIterableStream - Master JavaScriptโ€™s for await...of pattern and async generator functions
  4. React State Management with Streams - Implement efficient incremental state updates without causing excessive re-renders
  5. AbortController and Cancellation Patterns - Build robust cleanup mechanisms for stopping streams mid-flight
  6. Next.js API Routes for Streaming - Configure server-side streaming with proper headers and response handling
  7. Error Handling in Streaming Contexts - Implement graceful degradation when streams fail mid-response

Deep Theoretical Foundation

Server-Sent Events (SSE) Protocol

Server-Sent Events is a standard that allows servers to push data to web clients over HTTP. Unlike WebSockets, SSE is unidirectional (server to client only), simpler to implement, and built on standard HTTP.

SSE Message Format:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    SSE MESSAGE STRUCTURE                     โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                              โ”‚
โ”‚  Each message consists of one or more lines:                โ”‚
โ”‚                                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚ data: Hello, this is the first chunk               โ”‚    โ”‚
โ”‚  โ”‚                                                      โ”‚    โ”‚
โ”‚  โ”‚ data: More data in the same message                 โ”‚    โ”‚
โ”‚  โ”‚                                                      โ”‚    โ”‚
โ”‚  โ”‚ event: customEvent                                   โ”‚    โ”‚
โ”‚  โ”‚ data: {"type": "token", "content": "word"}          โ”‚    โ”‚
โ”‚  โ”‚                                                      โ”‚    โ”‚
โ”‚  โ”‚ id: 12345                                            โ”‚    โ”‚
โ”‚  โ”‚ data: Message with ID for reconnection              โ”‚    โ”‚
โ”‚  โ”‚                                                      โ”‚    โ”‚
โ”‚  โ”‚ retry: 3000                                          โ”‚    โ”‚
โ”‚  โ”‚ data: Client should reconnect after 3 seconds       โ”‚    โ”‚
โ”‚  โ”‚                                                      โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”‚  IMPORTANT: Each message ends with TWO newlines (\n\n)      โ”‚
โ”‚  Single newlines separate fields within a message            โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

SSE Message Structure

SSE vs WebSockets Comparison:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                 SSE vs WEBSOCKETS COMPARISON                        โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                     โ”‚
โ”‚  SERVER-SENT EVENTS                  WEBSOCKETS                     โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€                  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€                     โ”‚
โ”‚                                                                     โ”‚
โ”‚  Direction:                          Direction:                     โ”‚
โ”‚  Server โ†’ Client (unidirectional)    Bidirectional                  โ”‚
โ”‚                                                                     โ”‚
โ”‚  Protocol:                           Protocol:                      โ”‚
โ”‚  HTTP/1.1 or HTTP/2                  ws:// or wss://                โ”‚
โ”‚  (no upgrade needed for HTTP/1.1)    (requires protocol upgrade)   โ”‚
โ”‚                                                                     โ”‚
โ”‚  Reconnection:                       Reconnection:                  โ”‚
โ”‚  Automatic (built-in)                Manual (you implement)         โ”‚
โ”‚                                                                     โ”‚
โ”‚  Use Case:                           Use Case:                      โ”‚
โ”‚  LLM streaming, live updates         Chat, gaming, real-time collab โ”‚
โ”‚                                                                     โ”‚
โ”‚  Complexity:                         Complexity:                    โ”‚
โ”‚  Simple                              More complex                   โ”‚
โ”‚                                                                     โ”‚
โ”‚  Browser Support:                    Browser Support:               โ”‚
โ”‚  All modern browsers                 All modern browsers            โ”‚
โ”‚  (EventSource API)                   (WebSocket API)                โ”‚
โ”‚                                                                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

SSE vs WebSockets Comparison

Why SSE for LLM Streaming:

LLM responses are inherently unidirectional - the user sends a prompt once, then receives a stream of tokens. SSE is perfectly suited because:

  1. No bidirectional channel needed after the initial request
  2. Automatic reconnection handles network hiccups
  3. Works through HTTP proxies without special configuration
  4. Simpler server implementation (just formatted text output)

Reference: โ€œJavaScript: The Definitive Guideโ€ by David Flanagan - Ch. 15.11 (Server-Sent Events)


How streamText Works Internally

The AI SDKโ€™s streamText function is the core abstraction that converts LLM API responses into a consumable stream.

Internal Architecture:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   streamText() INTERNAL FLOW                         โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  YOUR CODE                                                           โ”‚
โ”‚     โ”‚                                                                โ”‚
โ”‚     โ”‚  const result = streamText({                                  โ”‚
โ”‚     โ”‚    model: openai('gpt-4'),                                    โ”‚
โ”‚     โ”‚    prompt: "Summarize this document..."                       โ”‚
โ”‚     โ”‚  });                                                          โ”‚
โ”‚     โ”‚                                                                โ”‚
โ”‚     โ–ผ                                                                โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚                     AI SDK Core                               โ”‚   โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  1. Model Adapter Layer                                 โ”‚  โ”‚   โ”‚
โ”‚  โ”‚  โ”‚     - Translates to provider-specific API format        โ”‚  โ”‚   โ”‚
โ”‚  โ”‚  โ”‚     - Handles authentication                            โ”‚  โ”‚   โ”‚
โ”‚  โ”‚  โ”‚     - Sets streaming: true for the provider             โ”‚  โ”‚   โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚   โ”‚
โ”‚  โ”‚                          โ”‚                                    โ”‚   โ”‚
โ”‚  โ”‚                          โ–ผ                                    โ”‚   โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  2. Provider API Call                                   โ”‚  โ”‚   โ”‚
โ”‚  โ”‚  โ”‚     - HTTPS request to OpenAI/Anthropic/etc.            โ”‚  โ”‚   โ”‚
โ”‚  โ”‚  โ”‚     - Response arrives as chunked transfer encoding     โ”‚  โ”‚   โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚   โ”‚
โ”‚  โ”‚                          โ”‚                                    โ”‚   โ”‚
โ”‚  โ”‚                          โ–ผ                                    โ”‚   โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  3. Stream Parser                                       โ”‚  โ”‚   โ”‚
โ”‚  โ”‚  โ”‚     - Parses provider-specific chunk format             โ”‚  โ”‚   โ”‚
โ”‚  โ”‚  โ”‚     - Extracts text deltas, finish reasons, metadata    โ”‚  โ”‚   โ”‚
โ”‚  โ”‚  โ”‚     - Normalizes to unified StreamTextResult            โ”‚  โ”‚   โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚   โ”‚
โ”‚  โ”‚                          โ”‚                                    โ”‚   โ”‚
โ”‚  โ”‚                          โ–ผ                                    โ”‚   โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  4. AsyncIterableStream Creation                        โ”‚  โ”‚   โ”‚
โ”‚  โ”‚  โ”‚     - Wraps parsed chunks in async iterator protocol    โ”‚  โ”‚   โ”‚
โ”‚  โ”‚  โ”‚     - Exposes .textStream, .fullStream properties       โ”‚  โ”‚   โ”‚
โ”‚  โ”‚  โ”‚     - Provides toDataStreamResponse() for SSE output    โ”‚  โ”‚   โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                          โ”‚                                           โ”‚
โ”‚                          โ–ผ                                           โ”‚
โ”‚  YOUR CODE RECEIVES:                                                 โ”‚
โ”‚     - result.textStream (AsyncIterableStream<string>)               โ”‚
โ”‚     - result.fullStream (includes tool calls, finish reason)        โ”‚
โ”‚     - result.toDataStreamResponse() (for Next.js API routes)        โ”‚
โ”‚                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

streamText Internal Flow

Key Properties of streamText Result:

const result = streamText({
  model: openai('gpt-4'),
  prompt: 'Summarize...'
});

// Different stream access patterns:
result.textStream          // AsyncIterableStream of just text
result.fullStream          // Includes all events (text, tool calls, finish)
result.toDataStreamResponse()  // Converts to SSE Response for Next.js
result.toTextStreamResponse()  // Plain text stream (simpler, no metadata)

AsyncIterableStream and Async Iterators in JavaScript

Understanding async iterators is crucial for working with streams. The for await...of syntax is how you consume streaming data in JavaScript.

The Async Iterator Protocol:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                 ASYNC ITERATOR PROTOCOL                              โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  SYNCHRONOUS ITERATOR              ASYNC ITERATOR                    โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€              โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€                    โ”‚
โ”‚                                                                      โ”‚
โ”‚  const iter = {                    const asyncIter = {              โ”‚
โ”‚    [Symbol.iterator]() {             [Symbol.asyncIterator]() {     โ”‚
โ”‚      return {                          return {                      โ”‚
โ”‚        next() {                          async next() {              โ”‚
โ”‚          return {                          return {                  โ”‚
โ”‚            value: data,                      value: data,            โ”‚
โ”‚            done: false                       done: false             โ”‚
โ”‚          };                                };                        โ”‚
โ”‚        }                                 }                           โ”‚
โ”‚      };                                };                            โ”‚
โ”‚    }                                 }                               โ”‚
โ”‚  };                                };                                โ”‚
โ”‚                                                                      โ”‚
โ”‚  for (const x of iter) { }         for await (const x of asyncIter) โ”‚
โ”‚                                      { }                             โ”‚
โ”‚                                                                      โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  KEY DIFFERENCE: next() returns Promise<{value, done}>               โ”‚
โ”‚                                                                      โ”‚
โ”‚  This allows yielding values as they become available               โ”‚
โ”‚  (perfect for streaming HTTP responses!)                            โ”‚
โ”‚                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Async Iterator Protocol

Consuming the Stream:

// Method 1: for await...of (most common)
for await (const chunk of result.textStream) {
  console.log(chunk);  // Each token/word as it arrives
}

// Method 2: Manual iteration
const reader = result.textStream[Symbol.asyncIterator]();
let chunk = await reader.next();
while (!chunk.done) {
  console.log(chunk.value);
  chunk = await reader.next();
}

// Method 3: Collect all (loses streaming benefit)
const fullText = await result.text;  // Waits for completion

Reference: โ€œJavaScript: The Definitive Guideโ€ by David Flanagan - Ch. 13 (Asynchronous JavaScript)


React State Management with Streams

Updating React state during streaming requires careful consideration to avoid performance issues.

The Challenge:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚             STREAM STATE UPDATE CHALLENGE                            โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  NAIVE APPROACH (PROBLEMATIC):                                       โ”‚
โ”‚                                                                      โ”‚
โ”‚  for await (const chunk of stream) {                                โ”‚
โ”‚    setText(prev => prev + chunk);  // Re-render on EVERY chunk!     โ”‚
โ”‚  }                                                                   โ”‚
โ”‚                                                                      โ”‚
โ”‚  Token arrives every ~50ms                                           โ”‚
โ”‚  = 20 re-renders per second                                          โ”‚
โ”‚  = Janky, unresponsive UI                                            โ”‚
โ”‚                                                                      โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  BETTER APPROACHES:                                                  โ”‚
โ”‚                                                                      โ”‚
โ”‚  1. USE AI SDK HOOKS (Recommended)                                   โ”‚
โ”‚     const { messages, isLoading } = useChat();                       โ”‚
โ”‚     // Handles batching internally                                   โ”‚
โ”‚                                                                      โ”‚
โ”‚  2. BATCHED UPDATES                                                  โ”‚
โ”‚     let buffer = '';                                                 โ”‚
โ”‚     for await (const chunk of stream) {                             โ”‚
โ”‚       buffer += chunk;                                               โ”‚
โ”‚       if (buffer.length >= 50) {  // Batch every 50 chars           โ”‚
โ”‚         setText(prev => prev + buffer);                              โ”‚
โ”‚         buffer = '';                                                 โ”‚
โ”‚       }                                                              โ”‚
โ”‚     }                                                                โ”‚
โ”‚                                                                      โ”‚
โ”‚  3. useTransition FOR NON-URGENT UPDATES                             โ”‚
โ”‚     const [isPending, startTransition] = useTransition();            โ”‚
โ”‚     startTransition(() => setText(prev => prev + chunk));           โ”‚
โ”‚     // Allows React to interrupt for more urgent updates             โ”‚
โ”‚                                                                      โ”‚
โ”‚  4. useDeferredValue FOR DISPLAY                                     โ”‚
โ”‚     const deferredText = useDeferredValue(text);                     โ”‚
โ”‚     // Renders stale value if new value causes jank                  โ”‚
โ”‚                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Reference: โ€œLearning React, 2nd Editionโ€ by Eve Porcello - Ch. 8 (Hooks), focusing on useTransition and useDeferredValue


AbortController and Cancellation Patterns

Users need the ability to cancel long-running streams. AbortController is the standard mechanism.

The Cancellation Flow:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                 ABORT CONTROLLER FLOW                                โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  CLIENT SIDE:                                                        โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€                                                       โ”‚
โ”‚                                                                      โ”‚
โ”‚  const controller = new AbortController();                           โ”‚
โ”‚                                                                      โ”‚
โ”‚  // Start the request                                                โ”‚
โ”‚  fetch('/api/summarize', {                                          โ”‚
โ”‚    method: 'POST',                                                   โ”‚
โ”‚    body: JSON.stringify({ text }),                                  โ”‚
โ”‚    signal: controller.signal  // <โ”€โ”€ Pass the signal                โ”‚
โ”‚  });                                                                 โ”‚
โ”‚                                                                      โ”‚
โ”‚  // User clicks "Cancel"                                             โ”‚
โ”‚  controller.abort();  // <โ”€โ”€ Aborts fetch AND server-side stream    โ”‚
โ”‚                                                                      โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  SERVER SIDE (Next.js API Route):                                    โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€                                   โ”‚
โ”‚                                                                      โ”‚
โ”‚  export async function POST(request: Request) {                      โ”‚
โ”‚    const { text } = await request.json();                           โ”‚
โ”‚                                                                      โ”‚
โ”‚    const result = streamText({                                       โ”‚
โ”‚      model: openai('gpt-4'),                                        โ”‚
โ”‚      prompt: text,                                                   โ”‚
โ”‚      abortSignal: request.signal,  // <โ”€โ”€ Pass request's signal     โ”‚
โ”‚    });                                                               โ”‚
โ”‚                                                                      โ”‚
โ”‚    return result.toDataStreamResponse();                            โ”‚
โ”‚  }                                                                   โ”‚
โ”‚                                                                      โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  CLEANUP FLOW:                                                       โ”‚
โ”‚                                                                      โ”‚
โ”‚  User clicks Cancel                                                  โ”‚
โ”‚        โ”‚                                                             โ”‚
โ”‚        โ–ผ                                                             โ”‚
โ”‚  controller.abort() called                                           โ”‚
โ”‚        โ”‚                                                             โ”‚
โ”‚        โ”œโ”€โ”€โ”€โ–บ fetch throws AbortError on client                       โ”‚
โ”‚        โ”‚                                                             โ”‚
โ”‚        โ””โ”€โ”€โ”€โ–บ request.signal.aborted becomes true on server          โ”‚
โ”‚                    โ”‚                                                 โ”‚
โ”‚                    โ–ผ                                                 โ”‚
โ”‚              streamText stops requesting tokens from LLM             โ”‚
โ”‚                    โ”‚                                                 โ”‚
โ”‚                    โ–ผ                                                 โ”‚
โ”‚              Response stream closes cleanly                          โ”‚
โ”‚                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

AbortController Flow

Cleanup in React Components:

useEffect(() => {
  const controller = new AbortController();

  fetchAndStream(controller.signal);

  return () => {
    controller.abort();  // Cleanup on unmount!
  };
}, []);

Next.js API Routes for Streaming

Next.js App Router provides native support for streaming responses.

API Route Setup:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚            NEXT.JS STREAMING API ROUTE                               โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  File: app/api/summarize/route.ts                                   โ”‚
โ”‚                                                                      โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”‚
โ”‚  โ”‚  import { streamText } from 'ai';                          โ”‚     โ”‚
โ”‚  โ”‚  import { openai } from '@ai-sdk/openai';                  โ”‚     โ”‚
โ”‚  โ”‚                                                             โ”‚     โ”‚
โ”‚  โ”‚  export async function POST(request: Request) {            โ”‚     โ”‚
โ”‚  โ”‚    const { document } = await request.json();              โ”‚     โ”‚
โ”‚  โ”‚                                                             โ”‚     โ”‚
โ”‚  โ”‚    const result = streamText({                              โ”‚     โ”‚
โ”‚  โ”‚      model: openai('gpt-4-turbo'),                          โ”‚     โ”‚
โ”‚  โ”‚      system: 'You are a document summarizer...',           โ”‚     โ”‚
โ”‚  โ”‚      prompt: `Summarize:\n\n${document}`,                  โ”‚     โ”‚
โ”‚  โ”‚      abortSignal: request.signal,                           โ”‚     โ”‚
โ”‚  โ”‚    });                                                      โ”‚     โ”‚
โ”‚  โ”‚                                                             โ”‚     โ”‚
โ”‚  โ”‚    return result.toDataStreamResponse();                   โ”‚     โ”‚
โ”‚  โ”‚  }                                                          โ”‚     โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ”‚
โ”‚                                                                      โ”‚
โ”‚  RESPONSE HEADERS (set automatically):                               โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€                               โ”‚
โ”‚  Content-Type: text/event-stream                                     โ”‚
โ”‚  Cache-Control: no-cache                                             โ”‚
โ”‚  Connection: keep-alive                                              โ”‚
โ”‚                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Reference: โ€œNode.js Design Patternsโ€ by Mario Casciaro - Ch. 6 (Streams)


Complete Project Specification

Functional Requirements

  1. Document Input
    • Text area for pasting documents (5,000+ words supported)
    • Real-time word count display
    • Clear/reset functionality
  2. Streaming Summary Generation
    • Character-by-character display of summary
    • Visual cursor indicator during streaming
    • Progress indicator (word count of generated summary)
    • Estimated time remaining (optional)
  3. Summary Output Structure
    • Key Points (3-5 bullet points)
    • Main Themes (2-3 items)
    • One-Paragraph Summary
  4. User Controls
    • Summarize button to start generation
    • Cancel button during streaming
    • Copy to clipboard button
    • New document button
  5. Error Handling
    • Display partial results on error
    • Show error message with retry option
    • Graceful handling of network issues

Non-Functional Requirements

  1. Performance
    • First token visible within 1 second of request
    • Smooth 60fps scrolling during streaming
    • No visible jank during state updates
  2. Reliability
    • Automatic cleanup on component unmount
    • Proper AbortController handling
    • Memory leak prevention
  3. Accessibility
    • Screen reader announcements for streaming state
    • Keyboard navigation support
    • Focus management during state transitions

Real World Outcome

When you open the web app in your browser, here is exactly what you will see and experience:

Initial State:

+-----------------------------------------------------------------------+
|  Document Summarizer                                                   |
+-----------------------------------------------------------------------+
|                                                                        |
|  Paste your document here:                                             |
|  +------------------------------------------------------------------+  |
|  |                                                                   |  |
|  |  Paste or type your document text...                             |  |
|  |                                                                   |  |
|  |                                                                   |  |
|  |                                                                   |  |
|  +------------------------------------------------------------------+  |
|                                                                        |
|  Document length: 0 words                   [ Summarize ]              |
|                                                                        |
+-----------------------------------------------------------------------+

After Pasting a Document (5,000+ words):

+-----------------------------------------------------------------------+
|  Document Summarizer                                                   |
+-----------------------------------------------------------------------+
|                                                                        |
|  Paste your document here:                                             |
|  +------------------------------------------------------------------+  |
|  | The field of quantum computing has seen remarkable progress      |  |
|  | over the past decade. Recent breakthroughs in error              |  |
|  | correction, qubit stability, and algorithmic development         |  |
|  | have brought us closer than ever to practical quantum            |  |
|  | advantage. This comprehensive analysis examines...               |  |
|  | [... 5,234 more words ...]                                       |  |
|  +------------------------------------------------------------------+  |
|                                                                        |
|  Document length: 5,847 words               [ Summarize ]              |
|                                                                        |
+-----------------------------------------------------------------------+

While Streaming (the magic happens!):

+-----------------------------------------------------------------------+
|  Document Summarizer                                                   |
+-----------------------------------------------------------------------+
|                                                                        |
|  Summary                                                               |
|  -------------------------------------------------------------------   |
|  Generating...                              Progress: 234 words        |
|  +------------------------------------------------------------------+  |
|  |                                                                   |  |
|  | ## Key Points                                                     |  |
|  |                                                                   |  |
|  | The article examines recent quantum computing breakthroughs,     |  |
|  | focusing on three critical areas:                                |  |
|  |                                                                   |  |
|  | 1. **Error Correction**: IBM's new surface code approach         |  |
|  |    achieves 99.5% fidelity, a significant improvement over       |  |
|  |    previous methods. This breakthrough addresses one of the_     |  |
|  |                                                                   |  |
|  +------------------------------------------------------------------+  |
|                                                                        |
|  [ Cancel ]                                                            |
|                                                                        |
+-----------------------------------------------------------------------+

The cursor (_) moves in real-time as each token arrives from the LLM. The user watches the summary build word by word - this is the โ€œChatGPT effectโ€ that makes AI feel alive.

Completed Summary:

+-----------------------------------------------------------------------+
|  Document Summarizer                                                   |
+-----------------------------------------------------------------------+
|                                                                        |
|  Summary                                              [Complete]       |
|  -------------------------------------------------------------------   |
|  Generated in 4.2s                          Total: 312 words           |
|  +------------------------------------------------------------------+  |
|  |                                                                   |  |
|  | ## Key Points                                                     |  |
|  |                                                                   |  |
|  | The article examines recent quantum computing breakthroughs,     |  |
|  | focusing on three critical areas:                                |  |
|  |                                                                   |  |
|  | 1. **Error Correction**: IBM's new surface code approach         |  |
|  |    achieves 99.5% fidelity, a significant improvement...         |  |
|  |                                                                   |  |
|  | 2. **Qubit Scaling**: Google's 1,000-qubit processor             |  |
|  |    demonstrates exponential progress in hardware capacity...     |  |
|  |                                                                   |  |
|  | 3. **Commercial Applications**: First production deployments     |  |
|  |    in drug discovery and financial modeling show...              |  |
|  |                                                                   |  |
|  | ## Main Themes                                                    |  |
|  | - Race between IBM, Google, and emerging startups                |  |
|  | - Shift from theoretical to practical quantum advantage          |  |
|  | - Growing investment from pharmaceutical and finance sectors     |  |
|  |                                                                   |  |
|  | ## One-Paragraph Summary                                          |  |
|  | Quantum computing is transitioning from experimental to          |  |
|  | practical, with major players achieving key milestones in        |  |
|  | error correction and scaling that enable real-world use cases.   |  |
|  |                                                                   |  |
|  +------------------------------------------------------------------+  |
|                                                                        |
|  [ Copy to Clipboard ]      [ Summarize Again ]      [ New Doc ]       |
|                                                                        |
+-----------------------------------------------------------------------+

Error State (mid-stream failure):

+-----------------------------------------------------------------------+
|  Document Summarizer                                                   |
+-----------------------------------------------------------------------+
|                                                                        |
|  Summary                                               [Error]         |
|  -------------------------------------------------------------------   |
|  Stopped after 2.1s                         Partial: 156 words         |
|  +------------------------------------------------------------------+  |
|  |                                                                   |  |
|  | ## Key Points                                                     |  |
|  |                                                                   |  |
|  | The article examines recent quantum computing breakthroughs,     |  |
|  | focusing on three critical areas:                                |  |
|  |                                                                   |  |
|  | 1. **Error Correction**: IBM's new surface code approach         |  |
|  |    achieves 99.5% fidelity...                                    |  |
|  |                                                                   |  |
|  | ----------------------------------------------------------------  |  |
|  | Stream interrupted: Connection timeout                            |  |
|  |    Showing partial results above.                                |  |
|  |                                                                   |  |
|  +------------------------------------------------------------------+  |
|                                                                        |
|  [ Retry ]                  [ Copy Partial ]         [ New Doc ]       |
|                                                                        |
+-----------------------------------------------------------------------+

Key UX behaviors to implement:

  1. The text area scrolls automatically to keep the cursor visible
  2. Word count updates in real-time as tokens arrive
  3. โ€œCancelโ€ button appears only during streaming
  4. Partial results are preserved even on error
  5. Copy button works even during streaming (copies current content)

Solution Architecture

Full Streaming Pipeline

+-------------------------------------------------------------------------+
|                    COMPLETE STREAMING PIPELINE                           |
+-------------------------------------------------------------------------+
|                                                                          |
|  BROWSER (Client)                                                        |
|  ----------------                                                        |
|                                                                          |
|  +------------------+     +-------------------+     +----------------+   |
|  |  DocumentInput   |     |  SummaryDisplay   |     |  ControlBar    |   |
|  |  Component       |---->|  Component        |<----|  Component     |   |
|  |                  |     |  (streaming text) |     |  (Cancel/Copy) |   |
|  +------------------+     +-------------------+     +----------------+   |
|           |                        ^                       |             |
|           |                        |                       |             |
|           v                        |                       v             |
|  +--------------------------------------------------------------+       |
|  |                    useSummarize() Hook                        |       |
|  |  - AbortController management                                 |       |
|  |  - State: text, isLoading, error                              |       |
|  |  - Fetch with SSE consumption                                 |       |
|  +--------------------------------------------------------------+       |
|           |                        ^                                     |
|           | POST /api/summarize    | SSE stream (text/event-stream)     |
|           v                        |                                     |
+-----------+------------------------+-------------------------------------+
            |                        |
            |   NETWORK (HTTP/1.1)   |
            |                        |
+-----------v------------------------+-------------------------------------+
|                                                                          |
|  NEXT.JS SERVER                                                          |
|  ---------------                                                         |
|                                                                          |
|  +--------------------------------------------------------------+       |
|  |  app/api/summarize/route.ts                                   |       |
|  |                                                                |       |
|  |  export async function POST(request: Request) {               |       |
|  |    const { document } = await request.json();                 |       |
|  |                                                                |       |
|  |    const result = streamText({                                |       |
|  |      model: openai('gpt-4-turbo'),                            |       |
|  |      prompt: document,                                         |       |
|  |      abortSignal: request.signal,                             |       |
|  |    });                                                         |       |
|  |                                                                |       |
|  |    return result.toDataStreamResponse();                      |       |
|  |  }                                                             |       |
|  +--------------------------------------------------------------+       |
|           |                        ^                                     |
|           |  HTTP POST             |  Chunked response                   |
|           v                        |                                     |
+-----------+------------------------+-------------------------------------+
            |                        |
            |   AI SDK CORE          |
            |                        |
+-----------v------------------------+-------------------------------------+
|                                                                          |
|  AI SDK                                                                  |
|  -------                                                                 |
|                                                                          |
|  +--------------------------------------------------------------+       |
|  |  streamText()                                                  |       |
|  |                                                                |       |
|  |  1. Translates prompt to OpenAI format                        |       |
|  |  2. Sends request with stream: true                           |       |
|  |  3. Parses chunked response                                    |       |
|  |  4. Returns AsyncIterableStream                                |       |
|  +--------------------------------------------------------------+       |
|           |                        ^                                     |
|           |                        |                                     |
|           v                        |                                     |
+-----------+------------------------+-------------------------------------+
            |                        |
            |   HTTPS                |
            |                        |
+-----------v------------------------+-------------------------------------+
|                                                                          |
|  LLM PROVIDER (OpenAI/Anthropic)                                        |
|  --------------------------------                                        |
|                                                                          |
|  +--------------------------------------------------------------+       |
|  |  GPT-4 / Claude                                                |       |
|  |                                                                |       |
|  |  Generates tokens one at a time                                |       |
|  |  Returns chunked transfer encoding response                    |       |
|  |  Each chunk: {"choices":[{"delta":{"content":"token"}}]}      |       |
|  +--------------------------------------------------------------+       |
|                                                                          |
+-------------------------------------------------------------------------+

Component Breakdown

+-------------------------------------------------------------------------+
|                      COMPONENT ARCHITECTURE                              |
+-------------------------------------------------------------------------+
|                                                                          |
|  app/                                                                    |
|  +-- page.tsx                 # Main page, composes components          |
|  |                                                                       |
|  +-- components/                                                         |
|  |   +-- DocumentInput.tsx    # Textarea with word count                |
|  |   +-- SummaryDisplay.tsx   # Streaming text with cursor              |
|  |   +-- ControlBar.tsx       # Summarize/Cancel/Copy buttons           |
|  |   +-- ProgressIndicator.tsx # Word count and time display            |
|  |   +-- ErrorBoundary.tsx    # Catch and display errors                |
|  |                                                                       |
|  +-- hooks/                                                              |
|  |   +-- useSummarize.ts      # Custom hook for streaming logic         |
|  |                                                                       |
|  +-- api/                                                                |
|      +-- summarize/                                                      |
|          +-- route.ts         # API endpoint with streamText            |
|                                                                          |
+-------------------------------------------------------------------------+

Data Flow with SSE Events

+-------------------------------------------------------------------------+
|                     SSE EVENT DATA FLOW                                  |
+-------------------------------------------------------------------------+
|                                                                          |
|  TIME                                                                    |
|   |                                                                      |
|   v    CLIENT                        SERVER                    LLM      |
|   |                                                                      |
|   0ms  POST /api/summarize  ------>                                      |
|        {document: "..."}                                                 |
|                                                                          |
|  50ms                                 Request received                   |
|                                       streamText() called                |
|                                                                          |
| 100ms                                                        <----  token|
|                                       Parse chunk                        |
|                                                                          |
| 110ms                       <------  data: {"text": "The"}              |
|        setState("The")                                                   |
|        render()                                                          |
|                                                                          |
| 150ms                                                        <----  token|
|                                       Parse chunk                        |
|                                                                          |
| 160ms                       <------  data: {"text": " article"}         |
|        setState("The article")                                           |
|        render()                                                          |
|                                                                          |
|  ...   (continues for each token)                                        |
|                                                                          |
| 4200ms                                                       <----  done |
|                                       Parse finish                       |
|                                                                          |
| 4210ms                      <------  data: {"finish": "stop"}           |
|        setIsComplete(true)                                               |
|        render()                                                          |
|                                                                          |
| 4220ms  Connection closed   <------>  Response complete                  |
|                                                                          |
+-------------------------------------------------------------------------+
document-summarizer/
+-- app/
|   +-- page.tsx                    # Main page component
|   +-- layout.tsx                  # Root layout
|   +-- globals.css                 # Global styles
|   +-- api/
|       +-- summarize/
|           +-- route.ts            # Streaming API endpoint
|
+-- components/
|   +-- document-input.tsx          # Text input component
|   +-- summary-display.tsx         # Streaming text display
|   +-- control-bar.tsx             # Action buttons
|   +-- progress-indicator.tsx      # Progress display
|   +-- streaming-cursor.tsx        # Animated cursor
|   +-- error-display.tsx           # Error state component
|
+-- hooks/
|   +-- use-summarize.ts            # Main streaming hook
|   +-- use-word-count.ts           # Word counting utility
|   +-- use-scroll-to-bottom.ts     # Auto-scroll behavior
|
+-- lib/
|   +-- prompts.ts                  # System prompts
|   +-- utils.ts                    # Utility functions
|
+-- types/
|   +-- index.ts                    # TypeScript types
|
+-- __tests__/
|   +-- hooks/
|   |   +-- use-summarize.test.ts
|   +-- components/
|   |   +-- summary-display.test.tsx
|   +-- api/
|       +-- summarize.test.ts
|
+-- package.json
+-- tsconfig.json
+-- next.config.js
+-- .env.local                      # OPENAI_API_KEY

Phased Implementation Guide

Phase 1: Basic Streaming Setup (Days 1-2)

Milestone: First token renders in the browser from your API route

Tasks:

  1. Create Next.js project with TypeScript
  2. Install AI SDK: npm install ai @ai-sdk/openai
  3. Set up environment variables for OpenAI API key
  4. Create basic API route at /api/summarize
  5. Implement minimal streamText call
  6. Create simple page that displays streamed text

Verification:

// Minimal working API route
export async function POST(request: Request) {
  const { text } = await request.json();

  const result = streamText({
    model: openai('gpt-4-turbo'),
    prompt: `Summarize: ${text}`,
  });

  return result.toDataStreamResponse();
}

Phase 2: Client-Side Streaming Consumption (Days 3-4)

Milestone: Stream displays character-by-character with state updates

Tasks:

  1. Create useSummarize custom hook
  2. Implement EventSource or fetch with streaming reader
  3. Handle incremental state updates
  4. Add loading state management
  5. Implement basic error handling

Key Code:

// hooks/use-summarize.ts
export function useSummarize() {
  const [text, setText] = useState('');
  const [isLoading, setIsLoading] = useState(false);

  const summarize = async (document: string) => {
    setIsLoading(true);
    setText('');

    const response = await fetch('/api/summarize', {
      method: 'POST',
      body: JSON.stringify({ document }),
    });

    const reader = response.body?.getReader();
    const decoder = new TextDecoder();

    while (reader) {
      const { done, value } = await reader.read();
      if (done) break;

      const chunk = decoder.decode(value);
      // Parse SSE format and extract text
      setText(prev => prev + extractText(chunk));
    }

    setIsLoading(false);
  };

  return { text, isLoading, summarize };
}

Phase 3: Cancellation and Cleanup (Days 4-5)

Milestone: User can cancel mid-stream, no memory leaks on unmount

Tasks:

  1. Add AbortController to fetch request
  2. Pass abort signal to API route
  3. Implement Cancel button in UI
  4. Add cleanup in useEffect
  5. Handle AbortError gracefully

Key Code:

const controllerRef = useRef<AbortController | null>(null);

const summarize = async (document: string) => {
  // Cancel any existing request
  controllerRef.current?.abort();
  controllerRef.current = new AbortController();

  try {
    const response = await fetch('/api/summarize', {
      method: 'POST',
      body: JSON.stringify({ document }),
      signal: controllerRef.current.signal,
    });
    // ... streaming logic
  } catch (error) {
    if (error.name === 'AbortError') {
      // User cancelled - not an error
      return;
    }
    throw error;
  }
};

const cancel = () => {
  controllerRef.current?.abort();
  setIsLoading(false);
};

Phase 4: Polish and UX (Days 5-6)

Milestone: Production-quality UI with all features

Tasks:

  1. Add animated cursor during streaming
  2. Implement auto-scroll to keep cursor visible
  3. Add word count progress indicator
  4. Implement Copy to Clipboard functionality
  5. Add proper error display with retry
  6. Style with Tailwind CSS

Features to Implement:

  • Streaming cursor (_) that appears during generation
  • Smooth scrolling to bottom of output
  • Real-time word count
  • Time elapsed display
  • Copy button (works during streaming too)

Phase 5: Testing and Optimization (Days 6-7)

Milestone: Tested, optimized, production-ready

Tasks:

  1. Write unit tests for hooks
  2. Add integration tests for API route
  3. Implement React 18 transitions for smooth updates
  4. Add error boundaries
  5. Performance optimization
  6. Accessibility improvements

Testing Strategy

Mocking Streams for Unit Tests

// __tests__/hooks/use-summarize.test.ts
import { renderHook, act } from '@testing-library/react';
import { useSummarize } from '@/hooks/use-summarize';

// Mock streaming response
function createMockStreamResponse(chunks: string[]) {
  const encoder = new TextEncoder();
  let index = 0;

  return new ReadableStream({
    pull(controller) {
      if (index < chunks.length) {
        controller.enqueue(encoder.encode(`data: ${chunks[index]}\n\n`));
        index++;
      } else {
        controller.close();
      }
    },
  });
}

describe('useSummarize', () => {
  beforeEach(() => {
    global.fetch = jest.fn();
  });

  it('accumulates streamed text', async () => {
    const mockStream = createMockStreamResponse([
      '{"text":"Hello"}',
      '{"text":" World"}',
    ]);

    (global.fetch as jest.Mock).mockResolvedValue({
      ok: true,
      body: mockStream,
    });

    const { result } = renderHook(() => useSummarize());

    await act(async () => {
      await result.current.summarize('Test document');
    });

    expect(result.current.text).toBe('Hello World');
  });
});

Testing SSE Connections

// __tests__/api/summarize.test.ts
import { POST } from '@/app/api/summarize/route';

describe('/api/summarize', () => {
  it('returns SSE content type', async () => {
    const request = new Request('http://localhost/api/summarize', {
      method: 'POST',
      body: JSON.stringify({ document: 'Test' }),
    });

    const response = await POST(request);

    expect(response.headers.get('Content-Type'))
      .toBe('text/event-stream');
  });

  it('streams response chunks', async () => {
    const request = new Request('http://localhost/api/summarize', {
      method: 'POST',
      body: JSON.stringify({ document: 'Test' }),
    });

    const response = await POST(request);
    const reader = response.body?.getReader();

    let receivedChunks = 0;
    while (reader) {
      const { done, value } = await reader.read();
      if (done) break;
      receivedChunks++;
    }

    expect(receivedChunks).toBeGreaterThan(0);
  });
});

Testing Cancellation

it('cleans up on abort', async () => {
  const controller = new AbortController();
  const mockStream = createSlowMockStream();

  (global.fetch as jest.Mock).mockResolvedValue({
    ok: true,
    body: mockStream,
  });

  const { result } = renderHook(() => useSummarize());

  // Start streaming
  const promise = act(async () => {
    await result.current.summarize('Test');
  });

  // Cancel mid-stream
  act(() => {
    result.current.cancel();
  });

  await promise;

  expect(result.current.isLoading).toBe(false);
  expect(result.current.text).not.toBe(''); // Partial preserved
});

Common Pitfalls and Debugging

1. Not Closing the Stream Properly

Problem: Memory leaks, hanging connections

// WRONG
const response = await fetch('/api/summarize');
const reader = response.body?.getReader();
// ... but never closing reader

Solution:

// CORRECT
try {
  const reader = response.body?.getReader();
  // ... process stream
} finally {
  reader?.releaseLock();
  response.body?.cancel();
}

2. Missing SSE Format in Manual Implementation

Problem: Client doesnโ€™t receive data

// WRONG - Raw text, not SSE format
controller.enqueue(encoder.encode('Hello'));

Solution:

// CORRECT - SSE format with data: prefix and double newline
controller.enqueue(encoder.encode('data: Hello\n\n'));

3. State Updates Causing Excessive Re-renders

Problem: Janky UI during streaming

// WRONG - Re-render on every single token
for await (const chunk of stream) {
  setText(prev => prev + chunk);
}

Solution:

// CORRECT - Batch updates
import { startTransition } from 'react';

for await (const chunk of stream) {
  startTransition(() => {
    setText(prev => prev + chunk);
  });
}

4. Forgetting AbortSignal on Server

Problem: LLM keeps generating after client cancels

// WRONG - No abort signal
const result = streamText({
  model: openai('gpt-4'),
  prompt: text,
});

Solution:

// CORRECT - Pass request signal
const result = streamText({
  model: openai('gpt-4'),
  prompt: text,
  abortSignal: request.signal,
});

5. Not Handling Partial Results on Error

Problem: User loses all content when stream fails

// WRONG - Clear everything on error
catch (error) {
  setText('');
  setError(error);
}

Solution:

// CORRECT - Preserve partial results
catch (error) {
  // Don't clear text - keep what we received
  setError(error);
  setIsPartial(true);
}

6. Race Conditions with Multiple Requests

Problem: Old request completes after new one started

// WRONG - No request tracking
const summarize = async (doc) => {
  const response = await fetch(...);
  for await (const chunk of response) {
    setText(prev => prev + chunk);  // Which request?
  }
};

Solution:

// CORRECT - Track request ID
const requestIdRef = useRef(0);

const summarize = async (doc) => {
  const thisRequestId = ++requestIdRef.current;

  for await (const chunk of response) {
    if (thisRequestId !== requestIdRef.current) return;
    setText(prev => prev + chunk);
  }
};

7. Missing Error Boundaries

Problem: Uncaught errors crash the entire app

// WRONG - No error boundary
<SummaryDisplay text={text} />

Solution:

// CORRECT - Wrap with error boundary
<ErrorBoundary fallback={<ErrorDisplay />}>
  <SummaryDisplay text={text} />
</ErrorBoundary>

Extensions and Challenges

Extension 1: Multiple Summary Formats

Add options for different summary types:

  • Executive Summary (1-2 paragraphs)
  • Bullet Points Only (5-10 points)
  • Academic Abstract (formal style)
  • TL;DR (1-2 sentences)

Use streamObject to return structured data:

const result = streamObject({
  model: openai('gpt-4-turbo'),
  schema: z.object({
    keyPoints: z.array(z.string()),
    themes: z.array(z.string()),
    summary: z.string(),
  }),
  prompt: document,
});

Extension 2: Document Upload Support

Add file upload for:

  • PDF documents (with pdf-parse)
  • Word documents (with mammoth)
  • Markdown files
  • Plain text files

Implement chunking for documents that exceed context limits.

Extension 3: Summary History with Local Storage

Store summaries locally:

  • Save summaries with timestamps
  • Allow re-viewing past summaries
  • Export history as JSON
  • Clear individual or all history

Extension 4: Comparison Mode

Summarize two documents and compare:

  • Side-by-side streaming display
  • Highlight similarities and differences
  • Generate comparison summary

Resources

Books with Specific Chapters

Topic Book Chapter/Section
Async JavaScript & Iterators โ€œJavaScript: The Definitive Guideโ€ by David Flanagan Ch. 13 (Asynchronous JavaScript)
Server-Sent Events โ€œJavaScript: The Definitive Guideโ€ by David Flanagan Ch. 15.11 (Server-Sent Events)
React State Management โ€œLearning React, 2nd Editionโ€ by Eve Porcello Ch. 8 (Hooks)
React Server Integration โ€œLearning React, 2nd Editionโ€ by Eve Porcello Ch. 12 (React and Server)
Streaming in Node.js โ€œNode.js Design Patterns, 3rd Editionโ€ by Mario Casciaro Ch. 6 (Streams)
Error Handling Patterns โ€œRelease It!, 2nd Editionโ€ by Michael Nygard Ch. 5 (Stability Patterns)
Web APIs & Fetch โ€œJavaScript: The Definitive Guideโ€ by David Flanagan Ch. 15 (Web APIs)
  1. Start with Flanagan Ch. 13 to understand async/await and async iterators
  2. Read Flanagan Ch. 15.11 for SSE fundamentals
  3. Move to Porcello Ch. 8 for React hooks patterns
  4. Then tackle the AI SDK documentation with this foundation
  5. Read Casciaro Ch. 6 for deeper stream understanding

Online Resources


Self-Assessment Checklist

Use these questions to verify your understanding before considering this project complete:

Fundamentals (Must answer all correctly)

  • Q1: What is the difference between SSE and WebSockets? When would you use each?
  • Q2: What does for await...of do and how is it different from regular for...of?
  • Q3: Explain what Symbol.asyncIterator is and why it matters for streaming.
  • Q4: What HTTP headers are required for an SSE response?
  • Q5: Why does toDataStreamResponse() exist? What does it do?

Implementation (Demonstrate by doing)

  • Q6: Your stream is working but updates feel janky. What React 18 feature would you use and why?
  • Q7: A user navigates away from the page mid-stream. What happens if you donโ€™t implement cleanup?
  • Q8: The network drops mid-stream. What data does the user see? How do you handle this gracefully?
  • Q9: Two summarization requests are made quickly in succession. What bug might occur and how do you prevent it?
  • Q10: How do you test a streaming API route without actually calling the LLM?

Architecture (Explain in your own words)

  • Q11: Draw the data flow from user click to rendered text. Include all layers (React, API route, SDK, LLM).
  • Q12: Why does the AI SDK parse the LLM response before passing it to you? What format does it come in raw?
  • Q13: What is โ€œbackpressureโ€ in streaming and does it affect your browser-based client?
  • Q14: Explain why SSE is a better choice than polling for LLM responses.
  • Q15: What information is in result.fullStream that isnโ€™t in result.textStream?

Debugging Scenarios (How would you fix?)

  • Q16: Your API returns 200 but the browser shows no text. Whatโ€™s likely wrong?
  • Q17: Cancellation works on the client but the LLM keeps generating (you see charges). What did you forget?
  • Q18: The summary appears all at once at the end instead of streaming. Whatโ€™s wrong with your client code?

The Core Question This Project Answers

โ€œHow do I stream LLM responses in real-time to create responsive, interactive UIs?โ€

This is about understanding the entire streaming pipeline from the AI SDKโ€™s async iterators through Server-Sent Events to React state updates. Youโ€™re not just calling an API - youโ€™re building a real-time data flow that makes AI feel alive and responsive.

When you can explain every layer of this pipeline and handle edge cases gracefully, youโ€™ve mastered one of the most important patterns in modern AI application development.


Next Project: P03 - Code Review Agent with Tool Calling