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:
- 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.
- Steps. One line for each thing the AI did. They stay open while it works and fold away at the end.
- Answer. The reply, formatted from the model's markdown.
- Actions. Copy and Resend, once it's done.
You pass three things:
| Prop | What it is |
|---|---|
status | "searching", "summarizing", "done" or "failed". The header and orb follow it. |
steps | What the AI did, in order. They show while it works and fold away when it's done. |
answer | The reply as markdown, exactly as the model wrote it. |
Each step has an id and a type:
| Type | Shows |
|---|---|
search | The query, and its results as a list you can open. |
read | A page it opened, with the site's icon and domain. |
note | A short line from the model, like "I'll read the docs first." |
tool | Anything 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.
| Kind | Counted as |
|---|---|
command | ran 2 commands |
edit, create, delete | edited, created or deleted 2 files |
view | read 2 files |
code-search | ran 2 code searches |
test, build | ran 2 test runs, ran 2 builds |
install | installed 2 packages |
debug | fixed 2 bugs |
branch, push | created 2 branches, pushed 2 branches |
commit | made 2 commits |
pull-request, review, merge | opened, reviewed or merged 2 pull requests |
deploy | shipped 2 deploys |
api | called 2 APIs |
database | ran 2 queries |
browser | took 2 browser actions |
email, message | sent 2 emails, sent 2 messages |
calendar | scheduled 2 events |
image | generated 2 images |
memory | saved 2 memories |
agent | started 2 agents |
task | updated 2 tasks |
tool | used 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
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.
trueShows Copy and Resend when it's done.
trueShows Resend.
Extra classes for each part.
Your own code block, link or image.
Set a text size to scale the whole block.