Frontend · How to use a skill

build-streaming-chat-ui

Builds the chat window for an AI assistant in React - message list, input box, words appearing as they stream in, a stop button, auto-scroll, loading and error states, and safe rendering of the bot's markdown. Use when the user wants a ChatGPT-like chat interface, a chat widget for their site, or streaming AI responses shown on screen.

Version 1.0 · Updated 2026-10-31

View raw SKILL.md

Install in Claude Code, Cursor or Codex (optional)

Only for AI coding tools that read skill folders. Run one line in a terminal, in your project folder. It downloads this skill's text files from TheSkillVault and nothing else; you can read them first with “View raw SKILL.md”.

Claude Code (all your projects)

curl -fsSL --create-dirs 'https://theskillvault.ai/api/skills/build-streaming-chat-ui/raw' -o ~/'.claude/skills/build-streaming-chat-ui/SKILL.md'

Cursor (this project)

curl -fsSL --create-dirs 'https://theskillvault.ai/api/skills/build-streaming-chat-ui/raw' -o '.cursor/skills/build-streaming-chat-ui/SKILL.md'

Codex and other Agent Skills tools (this project)

curl -fsSL --create-dirs 'https://theskillvault.ai/api/skills/build-streaming-chat-ui/raw' -o '.agents/skills/build-streaming-chat-ui/SKILL.md'

Or connect the whole library over MCP

Add TheSkillVault to Cursor or add this to your app's MCP settings (read-only; help):

{
  "mcpServers": {
    "theskillvault": {
      "url": "https://theskillvault.ai/mcp"
    }
  }
}

Build a streaming chat UI

Show the reply word by word as it arrives, so the chat feels fast even when the model is slow.

When to use

  • You have a chat API route and need the screen part.
  • Replies appear all at once after a long wait, and you want streaming.
  • You want a small chat widget in the corner of your site.

Steps

  1. Hold the state. Keep messages (role + text), input, status ("idle", "streaming" or "error") and an AbortController for stopping.
  2. Send. On submit, add the user's message and an empty assistant message. Then POST the history to /api/chat with the controller's signal.
  3. Read the stream. Use res.body.getReader() with a TextDecoder, and append each chunk to the last assistant message.
  4. Stop button. While streaming, show "Stop", which calls controller.abort(). Keep whatever text already arrived.
  5. Auto-scroll. Scroll to the newest message, but only when the user is already near the bottom.
  6. States. Disable Send while the input is empty or a reply is streaming. Show a typing indicator before the first chunk. On error, show "Something went wrong - try again" with a Retry button.
  7. Render safely. Show bot text as plain text, or use a markdown renderer that does not allow raw HTML. Never use dangerouslySetInnerHTML on model output.
  8. Accessibility. Give the message list aria-live="polite", label the input, and let Enter send while Shift+Enter adds a new line.

Example (core of the send function)

async function send(text: string) {
  const next = [...messages, { role: "user", content: text }, { role: "assistant", content: "" }];
  setMessages(next); setStatus("streaming");
  const ctrl = new AbortController(); ctrlRef.current = ctrl;
  try {
    const res = await fetch("/api/chat", { method: "POST", body: JSON.stringify({ messages: next.slice(0, -1) }), signal: ctrl.signal });
    if (!res.ok || !res.body) throw new Error(String(res.status));
    const reader = res.body.getReader(); const dec = new TextDecoder();
    for (;;) {
      const { done, value } = await reader.read(); if (done) break;
      const chunk = dec.decode(value, { stream: true });
      setMessages((m) => { const c = [...m]; c[c.length - 1] = { ...c[c.length - 1], content: c[c.length - 1].content + chunk }; return c; });
    }
    setStatus("idle");
  } catch (e) { setStatus((e as Error).name === "AbortError" ? "idle" : "error"); }
}

Gotchas

  • Model output can contain HTML or script tags. Rendering it as raw HTML is an XSS hole.
  • Do not store chats with personal or private data in localStorage on shared computers without telling users.
  • React strict mode runs effects twice in development, so do not auto-send from an effect.

Works with any stack

The same pattern works in Vue, Svelte or plain JavaScript. If you use the Vercel AI SDK, its useChat hook handles steps 1-4 instead, but check that its stream format matches your route.

Done when

  • Words appear as they stream, and Stop halts them right away.
  • A failed request shows an error with Retry, and the page does not freeze.
  • A reply containing <script> shows as text and does not run.

Example prompt

"Build a ChatGPT-style chat window for my app that streams the answer and has a stop button."

Licence: CC BY 4.0. Attribution when you reuse it: From TheSkillVault, https://theskillvault.ai, CC BY 4.0