KQ-1296 · SIDE/BY/SIDE

Side by Side

Two models answer one prompt in parallel columns, each streaming at its own pace — its time to first token, then its token rate, scaled by speed — with a live clock, a progress hairline and a Done stamp that lands on flick. Once both finish, a highlighter sweeps across what each says that the other does not, in reading order within 600ms, and the stats tally beneath them with bars that grow on glide. The vote puck drags 1:1, rubber-bands past the ends and settles on snap at the stop the throw was heading for, the preferred column takes its hue as a badge lands on recoil, and the reason you pick moves into the summary on glide; the stops are a radio group on the arrow keys, and on a phone the answers become two tabs.

KQ-1296 · SIDE/BY/SIDE

Prompt

Why can a retry with exponential backoff still overload a service that is coming back up?

Fernworks Model 30.0 s

Waiting to start

Gaugeworks Reasoner0.0 s

Waiting to start

First token

Fernworks Model 3: —Gaugeworks Reasoner: —

Total

Fernworks Model 3: —Gaugeworks Reasoner: —

Length

Fernworks Model 3: 0 tokGaugeworks Reasoner: 0 tok

Rate

Fernworks Model 3: —Gaugeworks Reasoner: —

Which answer is better

Vote when both finish

streaming both answers · at 1× speed

Install

One command — the source lands in your repo. Or copy it from the Code tab.

$ pnpm dlx shadcn@latest add @kinetiq/side-by-side

Props

speed
number
default 1
Playback speed of both streams as a multiple of real time. The clocks and stats still read in the models' real time.
diff
"phrases" | "sentences" | "off"
default "phrases"
What is marked once both finish: runs of two or more words the other answer never uses, whole sentences with under half their words in common, or nothing.
vote
"pick" | "scale"
default "pick"
The vote bar: three stops (A better, tie, B better), or five with much better at each end.
prompt
string
default defaultComparisonPrompt
The prompt both models answered, shown above the columns.
answers
[ComparisonAnswer, ComparisonAnswer]
default defaultComparisonAnswers
The two answers: id, model, text, latencyMs and tokensPerSecond. Set an answer's status to drive its column yourself as text arrives; leave it unset and the column plays the text in at its own pace.
autoPlay
boolean
default true
Start streaming when the surface first scrolls into view. Off: both answers show complete, with their marks.
value / defaultValue
ComparisonVote | null
default null
The vote: choice ("a", "b" or "tie"), margin (1 or 2) and an optional reason. Controlled or uncontrolled.
onValueChange
(vote: ComparisonVote | null) => void
default —
Fires from the drag, key or press that changed the vote or its reason.
reasons
string[]
default —
Why a winner won, offered after a vote for a side. Defaults to More accurate, Clearer, More concise, Safer advice.
onRerun
() => void
default —
Run again was pressed: both streams replay from the start and an uncontrolled vote clears.
onRetry
(answerId: string) => void
default —
Retry was pressed on a column whose answer has status "error".
onFinish
(answerId: string, stats: ComparisonStats) => void
default —
A column finished playing, with its first-token time, total time, token count and rate.
status
"ready" | "loading" | "error"
default "ready"
Whether the run has data yet: skeleton columns while loading, a message with Run again on error.
labels
{ a: string; b: string }
default { a: "A", b: "B" }
The two columns' short names, used on their badges, the stats and the vote stops.
label
string
default "Model comparison"
The surface's accessible name.
sound
boolean
default false
A tick for each stop the puck crosses and each reason picked; a chime when a vote lands, pitched by side.
disabled
boolean
default false
Read only: no vote, no reason, no re-run.
className
string
default —
Classes for the root. It is 560px tall by default; pass a height class to change it.