Response compare
Two to four model responses to the same prompt, side by side, like an arena or an A/B test. Columns stream in parallel with their own carets and stats, blind mode hides model names until the vote, scrolling can move together, and narrow screens stack into tabs.
On this page
Installation
Usage
import { ResponseCompare } from "@/components/ui/chatcn/response-compare"
<ResponseCompare
prompt={prompt}
responses={[
{ id: "a", model: "claude-sonnet-5-5", text: a.text, status: a.status },
{ id: "b", model: "gpt-6.1-sol", text: b.text, status: b.status },
]}
blind
onVote={(vote) => saveVote(vote)}
/>Responses render with the kit's Markdown, so each column heals unfinished syntax and shows its own caret while its status is streaming. A pending column shows the Thinking label after 300 ms. Per column stats come from GenerationStats in live mode, so they never shift while the columns stream. Pass renderBody to render bodies yourself.
Votes are { type: "winner", id }, { type: "tie" } or { type: "bad" }. Voting opens once every response finishes. In blind mode, names read Model A and Model B until the first vote, then the real model ids swap in.
Examples
Streaming in parallel
Four models
States
Props
| Prop | Type | Notes |
|---|---|---|
responses | CompareResponse[] | Two to four. { id, model, text, status?, errorText?, ttftMs?, durationMs?, outputTokens?, tokensPerSecond? } |
prompt | ReactNode | Shown above the columns, clamped to two lines |
blind, revealed | boolean | Hide names until the vote, or reveal them yourself |
vote, defaultVote, onVote | ResponseCompareVote | null | Controlled or uncontrolled |
voting | boolean | Show the vote bar. Defaults to true |
syncScroll, defaultSyncScroll, onSyncScrollChange | boolean | Columns scroll together. On by default |
bodyHeight | number | string | Height of each response body. Defaults to 360 |
stackBelow | number | Container width in px below which columns become tabs. Defaults to 640 |
renderBody | (response) => ReactNode | Replace the Markdown body |
onRetry | (id) => void | Shows Retry response on a failed column |
Keyboard
| Key | Action |
|---|---|
Tab | Sync scroll, each response body, then the vote buttons |
ArrowUp, ArrowDown, PageUp, PageDown | Scroll the focused response. With sync on, the others follow |
ArrowLeft, ArrowRight | Switch responses in the stacked tabs |
Accessibility
- Each column is a labelled section and each body a focusable, labelled region, so it scrolls from the keyboard. Bodies are
aria-busywhile streaming, and each answer is announced once when it finishes. - Vote buttons use
aria-pressed, and the result line is a polite live region that names the winner once revealed. - The winner gets an ink letter mark plus a Preferred label with a check, never color alone.
- Synced scrolling follows the column under the pointer or focus, by position ratio, so answers of different lengths stay aligned.
Motion
- Text appends with no per-word effects. Each caret is solid while its tokens flow and blinks when that column waits.
- Revealing names is a rare moment, so the model id swaps in with a 220 ms blur crossfade. Under reduced motion it just appears.
- Switching tabs is instant.