# Streaming and Waiting

Course: [AI UX & Generative UI](https://www.devtutorials.ai/learn/ai-ux) · Section: Trustworthy AI Interfaces
Source: https://www.devtutorials.ai/learn/ai-ux/trustworthy-ui/streaming-and-waiting

> Replace a 14-second spinner with an answer that shows each step, streams as it is written, and has a Stop button that really cancels the work.

## Why AI Waiting Feels So Long

**Perceived wait** = how long a wait *feels* to the user, not how long it really is.

In this lesson, you will turn a 14-second spinner into an answer that shows each step, streams as it is written, and can be stopped.

A support agent asks the AI: "Where is my parcel for order #A-1042?"

The agent behind the app works hard. It looks up the order. It checks the courier. Then it writes the answer.

But the screen shows only a spinner. For 14 seconds.

Press **Play**, or use **Next** to go one step at a time.

- **Frames 1 to 3:** the right side shows the real work. The left side shows only "Loading…".
- **The gauge:** the seconds of waiting with no clue about what is happening.
- **Frame 4:** the whole answer appears at once, at the very end.

**Simple lesson:** a spinner hides real work. Users cannot tell busy from broken.

### A Real-World Example

Think about ordering food in an app.

- **App A** shows "Placing order…" for 25 minutes. Then the food arrives.
- **App B** shows "Order received", then "Cooking", then "Driver on the way", with a map.

Both took 25 minutes. App B *felt* faster, and you never wondered if it had failed.

AI answers work the same way:

- **"Order received"** = the first sign of progress, within a second.
- **"Cooking", "Driver on the way"** = the agent's tool steps.
- **The map** = the answer streaming in as it is written.

Click the button. The **Build** tab plays the scripted agent through the naive interface.

- **The screen:** what the user sees at each moment.
- **Events from the agent:** what the agent was really doing at that moment.
- **Checks for this scenario:** the rules this interface breaks.

**Simple lesson:** the agent's work is already there. The interface just has to show it.

## What You Will Learn

In this lesson, you will learn:

1. Why AI waiting is different from normal loading.
2. How to show progress from the first second.
3. How streaming works, and where it cannot help.
4. How to build a Stop button that really stops.
5. The checks your interface must pass.

**About the Build tab**

- The **Build** tab in the AI UX Lab plays a **scripted agent**. Its events (tool calls, words, errors) come with fixed timings, so every run is the same.
- Your interface is a set of choices in the lab, so the checks can be exact. In a real app, the same rules are checked on the page, for example with Playwright.
- The **Before (naive)** button shows the starting interface. **My interface** shows your changes.

## Where the 14 Seconds Go

### Two Kinds of Waiting

An AI answer has two parts:

1. **Tool time.** The agent calls tools: it looks up the order, then checks the courier. No words exist yet.
2. **Writing time.** The model writes the answer, a few words at a time.

**Token** = a small piece of text the model writes, about one short word.

In this scenario, tool time is about 9 seconds. Writing time is about 5.

### What Each Interface Shows

Watch the same 14-second answer on three interfaces.

Step through all four frames.

- **Frames 1 and 2:** only "stream + steps" shows anything. No words exist yet, so streaming alone cannot help.
- **Frame 3:** words arrive. Now streaming helps too.
- **Frame 4:** when each interface first shows something useful.

**Easy way to remember:** steps cover the tool time. Streaming covers the writing time. You need both.

**Simple lesson:** show something useful within one second, every time.

## Show Progress From the First Second

### Tool Steps

**Tool step** = one small chip that says what the agent is doing, such as "Checking the courier".

Show the chip the moment the tool starts. Tick it when the tool finishes.

This tells the user the AI is working, and on what. It also explains a slow answer: "Oh, the courier check is slow."

### Skeleton

**Skeleton** = grey bars where the answer will appear.

It shows the shape of the answer before the words arrive. Use it with tool steps, not instead of them.

### Streaming

**Streaming** = showing each token on screen as soon as it arrives, instead of waiting for the whole answer.

In React, you read the response bit by bit and add each token to the text:

```tsx
const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();
for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  for (const line of value.split("\n").filter(Boolean)) {
    const ev = JSON.parse(line);
    if (ev.type === "tool-start") setSteps((s) => [...s, { label: ev.label, done: false }]);
    if (ev.type === "token") setText((t) => t + ev.text);
  }
}
```

Each line from the server is one event. The UI reacts to each one as it comes.

**Simple lesson:** tool steps for the tool time, streaming for the writing time, a skeleton for the shape.

## A Stop Button That Really Stops

### Hiding Is Not Stopping

A Stop button that only hides the answer is a trap. The request keeps running on the server. When it finishes, it can still save, send or charge.

Step through both frames.

- **Frame 1:** at 5 seconds, the user presses Stop on both sides.
- **Frame 2:** on the left, the work finished anyway and saved the answer to the ticket. On the right, nothing happened.

### Cancel the Request

In the browser, an `AbortController` cancels the request itself:

```tsx
const abort = useRef<AbortController | null>(null);

async function ask(question: string) {
  abort.current = new AbortController();
  await fetch("/api/ask", { method: "POST", body: question, signal: abort.current.signal });
}

const stop = () => abort.current?.abort();
```

`abort()` closes the connection. Your server should notice and stop the agent, so no later step runs.

**Check the server side too.**

- Real tools differ in what happens after the browser disconnects.
- Some keep the agent running unless you pass the cancel on.
- Test it with your own stack: press Stop, then check that nothing was saved.

**Simple lesson:** Stop must cancel the work, not just the screen.

## Fix the 14-Second Wait

### The Four Choices

In the Build tab, Lesson 2 has four choices:

| Choice | Values | What it does |
|---|---|---|
| Show the answer | spinner or tokens | Wait for the whole answer, or stream it |
| Show each step | yes or no | A chip per tool step, ticked when done |
| Skeleton | yes or no | Grey bars where the answer will go |
| Stop button | yes or no | Cancels the request, so nothing runs later |

### The Checks

Your interface must pass three scenarios:

- **A 14-second answer:** first words within 1 s of the first token, and each tool step shown when it starts.
- **A quick answer:** first words within 1 s.
- **The user gives up:** Stop cancels the work, with no side effects later.

### Lab Exercise: Fix the Wait

Your task: change the Lesson 2 choices in the Build tab until every scenario passes. Then press **Check all scenarios**.

Click the button and open the **Build** tab.

- **The choices:** at the top left, under "My interface".
- **Play:** watch your interface handle the scenario, moment by moment.
- **Check all scenarios:** runs all three. A failing check says which moment went wrong.

**Simple lesson:** show steps from the first second, stream the words, and make Stop real.

### What Changes per Framework

| Option | Streaming and Stop |
|---|---|
| Hand-built React | `fetch` with a stream reader and an `AbortController`, as above |
| CopilotKit | Streams messages and tool calls to the UI over AG-UI |
| Vercel AI SDK UI | `useChat` streams the reply and gives a `stop()` function |
| Syncfusion AI components | AI AssistView: a ready-made panel for AI replies (optional, not graded) |

## Build It in React

The Build tab uses switches. Real apps use code. The lab's **Code** tab gives you a React component and a scripted agent. Your component listens to the agent's events and draws the page. The checks then look at the real page, the way a Playwright test would.

The key move is to add each event to the screen the moment it arrives:

```tsx
useEffect(() => agent.on((e) => {
  if (e.type === "tool-start") setSteps((s) => [...s, e]);
  if (e.type === "token") setText((t) => t + e.text);
}), [agent]);
```

The Stop button must call `agent.cancel()`. Hiding the answer is not enough.

## TL;DR

1. **Perceived wait** = how long a wait feels. A spinner makes it feel longer.
2. **Two kinds of waiting** = tool time (no words yet) and writing time.
3. **Tool steps** = chips that show what the agent is doing. They cover the tool time.
4. **Streaming** = show tokens as they arrive. It covers the writing time.
5. **Skeleton** = the shape of the answer before the words.
6. **Stop** = abort the request, so nothing runs later.

## Common mistakes

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Streaming only | "Streaming fixes slow answers." | Add tool steps for the time before the first word. | The screen is blank for 9 seconds while tools run. |
| A Stop that only hides | "The user pressed Stop, so it stopped." | Abort the request and the agent. | The answer still saves or sends after Stop. |
| A skeleton with no steps | "Grey bars show progress." | Use steps to say what is happening. | A skeleton for 14 seconds looks frozen too. |
| Updating on every token with heavy work | "Re-render everything each time." | Append the text; keep each update small. | The page stutters while the answer streams. |
| Hiding which step is slow | "Users don't care about tools." | Show the step that is running. | Users blame the AI for a slow courier system. |

## Check Your Understanding

## Exercises

## What to Remember

- A spinner hides real work. Users cannot tell busy from broken.
- Show something useful within one second.
- Tool steps cover the tool time. Streaming covers the writing time.
- A skeleton shows the shape, but does not say what is happening.
- Stop must cancel the request, not just hide the answer.
- Test Stop on your own stack: nothing should save or send after it.

## Further Reading

- [Response times: the 3 important limits, by Nielsen Norman Group](https://www.nngroup.com/articles/response-times-3-important-limits/): why 1 second and 10 seconds matter.
- [AbortController, on MDN](https://developer.mozilla.org/en-US/docs/Web/API/AbortController): how to cancel a request in the browser.

Next: **Sources and Doubt**, where a confident wrong answer gets citation chips, a "not sure" state and a scope note.
