When to use
- A playground or eval tool where someone sends one prompt to several models and decides which reply is better.
- Choosing a default model: speed, length and cost side by side, measured on the same prompt.
- Collecting preference data: the pick is a callback, ready to store as a label.
When not to use
- One model's reply in a chat: use Conversation with Response.
- Comparing runs that call tools step by step: use Agent Run Console.
- Comparing aggregate scores across many prompts: a table of eval results says more than two replies.
Installation
pnpm dlx shadcn add @eased-pro/multi-model-comparenpx shadcn add @eased-pro/multi-model-compareyarn dlx shadcn add @eased-pro/multi-model-comparebunx --bun shadcn add @eased-pro/multi-model-compareNeeds the @eased-pro registry and your Pro token in components.json. Create a token and copy the setup from your account .
Install the dependencies
pnpm add motion @base-ui/reactnpm install motion @base-ui/reactyarn add motion @base-ui/reactbun add motion @base-ui/reactAdd the Eased pieces it builds on
Copy each item’s files the same way, or install them with the CLI:
Add the files
- components/eased/multi-model-compare/multi-model-compare.tsx
Source is included with Eased Pro. Members download the files with the CLI command above, or through the MCP server.
Keep the folder layout
The files import each other by relative path (
../lib/motion), so keep each one at the path shown, undercomponents/eased/.
Usage
import { MultiModelCompare, type CompareModel } from "@/components/eased/multi-model-compare/multi-model-compare";export function Playground({ prompt, models, save }: { prompt: string; models: CompareModel[]; save: (id: string | null) => void }) { return <MultiModelCompare className="h-dvh" prompt={prompt} inputTokens={182} models={models} onWinnerChange={save} />;}// One model. Append each chunk as it lands, stamped with ms since the request was sent.const kestrel: CompareModel = { id: "kestrel-2", name: "Kestrel 2", provider: "Fieldline", chunks: [ { at: 1120, text: "Check these ", tokens: 3 }, { at: 1161, text: "in order:", tokens: 2 }, ], pricing: { inputPerMTok: 2.5, outputPerMTok: 10 },};// When the stream closes: { ...kestrel, endedAt: 2840 }.Every number comes from the chunks: the first chunk's at is the time to first token, tokens per second counts from it to the latest chunk, and cost adds the prompt's inputTokens to the output at each model's pricing. A model is waiting until its first chunk, streaming after it, done once it has an endedAt and failed with an error; set status: "stopped" after Stop.
API reference
MultiModelCompare
| Prop | Type | Default | Description |
|---|---|---|---|
prompt | ReactNode | What every model was asked, clamped to two lines. | |
inputTokens | number | 0 | The prompt's tokens, shown beside it and billed to every model. |
models | CompareModel[] | Two or three models, in display order. | |
winnerId / defaultWinnerId / onWinnerChange | string | null / string | null / (id) => void | null | The preferred reply. Prefer appears once every model has settled; pressing the picked one again clears it. Uncontrolled, a new run clears the pick; a controlled winnerId is yours to clear when you restart the models. |
interval | number | 250 | The chart's time step in ms. |
onStop / onRestart | () => void | Stop while any model streams, Run again once all have settled. | |
currency / locale | string | "USD" / "en-US" | |
className | string | "" | Appended to the root. Give it a height (h-dvh, flex-1); the replies scroll inside it. |
CompareModel
| Field | Type | Description |
|---|---|---|
id / name / provider | string | The series key, the card's title and its muted second line. |
chunks | CompareChunk[] | { at, text, tokens } in arrival order; at is ms since the request. text is markdown. |
endedAt | number | Ms since the request when the stream closed. Marks the reply done. |
error | string | Marks the reply failed and shows why in place of the text. |
status | CompareStatus | Overrides the derived status: waiting, streaming, done, failed or stopped. |
pricing | { inputPerMTok, outputPerMTok } | Per million tokens, in currency. Without it the Cost cell stays empty. |
color | string | The card's swatch and chart line. Default its slot, chartColor(n). |
compareStatus(model) returns the derived status, for your own controls.
Keyboard
| Key | Action |
|---|---|
Tab | Moves from Stop or Run again to each card's Prefer, then to the chart. |
← / →, Home / End | On the chart, move the crosshair a step and rewind every reply with it. Escape lets go. |
Enter / Space | Prefers a reply, or clears the pick on the preferred one. |
Accessibility
- Each card is a section named by its model; its numbers are a description list, so a cell reads as "Tokens/s, 70". An empty number reads as "None yet".
- Prefer is a toggle button (
aria-pressed) whose name includes the model: "Prefer Kestrel 2". It stays in place but inert while the replies stream, so focus never lands on it early. - One live region speaks for the screen: "All 2 replies finished. Pick the one you prefer.", then "Kestrel 2 preferred". The replies themselves are not live, so streaming text is never read out word by word.
- The chart is a slider with the crosshair's value as its text ("At 2.75s: Atlas 3 Pro 41, Kestrel 2 114") and every point in a hidden table. Status is text as well as colour.
Motion
On mount the prompt, the cards and the chart rise 4px in turn (stagger.line, once). While the replies stream each new word sharpens out of a soft blur, the numbers roll on the snappy spring, the status line swaps with y + blur, and the chart grows a step at a time, its lines and scale morphing on smooth. Scrubbing glides one crosshair across every line and rewinds each reply to the same moment, with no animation on the text. Picking a reply lifts its card 2px on the snappy spring while a raised shadow fades in behind it and its border firms; the other cards recede to 60% over 200ms. Reduced motion: fades only. Nothing rises, slides or lifts, words appear without the blur, and the pick is told by the border, shadow and fade alone.
Notes for AI
Pick this when one prompt goes to two or three models and a person decides between the replies. Feed it the raw stream: append each chunk with its at (ms since the request) and tokens, set endedAt when the stream closes, and pass pricing per model; the block derives time to first token, speed, totals, cost and the chart from those. Store the pick from onWinnerChange.
Do not compute the stats yourself or add another chart, timer or legend beside it: the cards are the legend, and the chart is already scrubbable. Do not wrap the cards in your own Card, and keep it to three models; past that, compare scores in a table.
Layout
Two models share a row from 40rem and three from 56rem; narrower, the cards scroll sideways with snap points, the next one peeking in, and a new pick scrolls its card into view. The hint above the cards moves under the prompt. A card's four numbers sit in a row from 26rem and two by two below it. The chart spans the full width under the cards. When the block is shorter than its content, it scrolls as a page. Nothing scrolls sideways at 390px except the row of cards.
Related components
- AI Chat Conversation: the thread Response comes from, for a single model's reply.
- Animated Line Chart: the chart under the cards, on its own.
- AI Agent Run Console: a single agent run, watched step by step.