Skip to content
Sign in
PlaygroundResponse compareMenu

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.

Two answers to the same prompt with names hidden. Vote to reveal them.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/response-compare.json

Usage

tsx
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

Both answers stream at once with their own carets and live stats.

Four models

Four columns with names shown, after picking a winner.

States

A failed and a waiting response, a tie, and stacked tabs at a narrow width.

Props

PropTypeNotes
responsesCompareResponse[]Two to four. { id, model, text, status?, errorText?, ttftMs?, durationMs?, outputTokens?, tokensPerSecond? }
promptReactNodeShown above the columns, clamped to two lines
blind, revealedbooleanHide names until the vote, or reveal them yourself
vote, defaultVote, onVoteResponseCompareVote | nullControlled or uncontrolled
votingbooleanShow the vote bar. Defaults to true
syncScroll, defaultSyncScroll, onSyncScrollChangebooleanColumns scroll together. On by default
bodyHeightnumber | stringHeight of each response body. Defaults to 360
stackBelownumberContainer width in px below which columns become tabs. Defaults to 640
renderBody(response) => ReactNodeReplace the Markdown body
onRetry(id) => voidShows Retry response on a failed column

Keyboard

KeyAction
TabSync scroll, each response body, then the vote buttons
ArrowUp, ArrowDown, PageUp, PageDownScroll the focused response. With sync on, the others follow
ArrowLeft, ArrowRightSwitch 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-busy while 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.