Response Block

  1. I'll look up what Vercel AI Gateway is and how to use it.

Customize

Installation

Installs are paused for now. You can still preview and customize every component on its page.

It also installs Thinking State, which draws the orb in the header.

Usage

Response Block is the reply side of a chat with an LLM or agent. It doesn't care which model or chat UI you use: your app keeps its own input box, and for each reply you render one Response Block and pass it what the AI is doing and what it wrote. Every part can be restyled or swapped out, see Customize.

import { ResponseBlock, type ResponseStatus, type ResponseStep } from "@/components/response-block";

type Message =
  | { id: string; role: "user"; text: string }
  | { id: string; role: "assistant"; status: ResponseStatus; steps: ResponseStep[]; answer: string };

export function Chat({ messages, onRetry }: { messages: Message[]; onRetry: (id: string) => void }) {
  return (
    <div className="flex flex-col gap-6">
      {messages.map((message) =>
        message.role === "user" ? (
          <p key={message.id} className="bg-muted self-end rounded-2xl px-4 py-2 text-sm">
            {message.text}
          </p>
        ) : (
          <ResponseBlock
            key={message.id}
            status={message.status}
            steps={message.steps}
            answer={message.answer}
            onRetry={() => onRetry(message.id)}
          />
        ),
      )}
    </div>
  );
}

Pick a demo in the panel above: a research answer or a coding task. They're sample data played back, not connected to any AI, and none of it is part of what you install.

How it works

A reply has four parts:

  1. Header. The Thinking State orb and what's happening, with the time on the right in the orb's current color. The orb changes with the work: thinking, searching, analyzing a page, building, debugging after a failed step, and pondering while it writes. When it's done, it goes still.
  2. Steps. One line for each thing the AI did. They stay open while it works and fold away at the end.
  3. Answer. The reply, formatted from the model's markdown.
  4. Actions. Copy and Resend, once it's done.

You pass three things:

PropWhat it is
status"searching", "summarizing", "done" or "failed". The header and orb follow it.
stepsWhat the AI did, in order. They show while it works and fold away when it's done.
answerThe reply as markdown, exactly as the model wrote it.

Each step has an id and a type:

TypeShows
searchThe query, and its results as a list you can open.
readA page it opened, with the site's icon and domain.
noteA short line from the model, like "I'll read the docs first."
toolAnything the model ran, like a command, a file edit or a merged pull request. It shows the kind's icon, a title, an optional meta chip and optional output in a code block that opens on click (not on phones, to save space). Only add these when the model really ran something.

A tool's kind picks its icon and how it's counted in the finished header, like "Merged 1 pull request". Leave it out, or pass one that isn't listed, and it counts as tool.

KindCounted as
commandran 2 commands
edit, create, deleteedited, created or deleted 2 files
viewread 2 files
code-searchran 2 code searches
test, buildran 2 test runs, ran 2 builds
installinstalled 2 packages
debugfixed 2 bugs
branch, pushcreated 2 branches, pushed 2 branches
commitmade 2 commits
pull-request, review, mergeopened, reviewed or merged 2 pull requests
deployshipped 2 deploys
apicalled 2 APIs
databaseran 2 queries
browsertook 2 browser actions
email, messagesent 2 emails, sent 2 messages
calendarscheduled 2 events
imagegenerated 2 images
memorysaved 2 memories
agentstarted 2 agents
taskupdated 2 tasks
toolused 2 tools

When more than two kinds of tools ran, the header folds them into "used 5 tools" so it stays short.

search, read and tool steps take a status of "running", "done" or "failed". A failed tool turns its icon red, and the orb switches to debugging until a later step of the same kind succeeds.

The answer picks its own format from the markdown: paragraphs, plain and numbered lists, bold, quotes, tables, code blocks and links with a site chip. Images work three ways: an image on its own line shows full width with its caption, images on neighboring lines form a grid, and an image inside a sentence or list item becomes a small circle, like a logo. Raw JSON becomes a code block on its own. Nothing is ever rendered as HTML, and only safe links and images load.

When it's done, two small buttons appear under the answer: Copy, and Resend when you pass onRetry.

Live updates

Keep status, steps and answer in state and update them as your AI streams. mergeById adds a step the first time its id appears and updates it after that, so lines change in place:

import { mergeById } from "@/components/response-block";

setSteps((steps) => mergeById(steps, [{ id: call.id, status: "done", results }]));
setAnswer((answer) => answer + chunk);

Set startedAt when the reply starts and endedAt when it ends, and the time in the header counts itself.

Customize

Add classes to any part with classNames, or swap in your own code block, link or image with components. Text size scales everything, so className="text-sm" makes the whole block bigger.

<ResponseBlock
  status="done"
  answer={answer}
  className="text-sm"
  classNames={{ answer: "text-foreground", actions: "justify-end" }}
  components={{
    Code: ({ code, language }) => <MyCodeBlock code={code} language={language} />,
  }}
/>

Everything works with the keyboard and screen readers, and nothing moves when reduced motion is on.

Props

PropType

Where the reply is.

search, read, note and tool steps, in order.

The model's markdown, or blocks you build yourself.

When it started. The time counts up from here.

When it finished.

The time in milliseconds, if you track it yourself.

Replaces the header text.

Folds the steps away when it's done.

Defaulttrue

Shows Copy and Resend when it's done.

Defaulttrue

Shows Resend.

Extra classes for each part.

Your own code block, link or image.

Set a text size to scale the whole block.