TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import * as React from "react";3import { Coins, Timer, Trophy, Type } from "lucide-react";4import { ProviderIcon } from "@/components/brand/provider-icon";5import { Badge } from "@/components/ui/badge";6import { criterionById, type Criterion, type WinnerResult } from "@/lib/arena/scoring";7import { formatDelta } from "@/lib/arena/metrics";8import { cn } from "@/lib/utils";9import { BlindAvatar } from "./blind";1011interface Props {12 winner: WinnerResult;13 name: string;14 provider: string | null;15 /** Blind Arena, identity still hidden → neutral avatar and letter label. */16 hidden?: boolean;17 blindIndex?: number;18 customCriteria?: Criterion[];19 showCosts?: boolean;20 className?: string;21}2223/**24 * Arena Winner card: shown once every response is final and at least one vote exists.25 * Most criteria won; ties go to the fastest first token. Deltas compare the winner with the mean of the others.26 */27export function WinnerCard({ winner, name, provider, hidden, blindIndex = 0, customCriteria = [], showCosts = true, className }: Props) {28 const criteria = winner.criteriaWon.map((id) => criterionById(id, customCriteria));29 const d = winner.deltas;30 const better = (v: number | null) => (v === null ? null : v < 0);31 return (32 <section className={cn("animate-pop overflow-hidden rounded-xl border border-accent/40 bg-accent-soft/40 p-3.5 sm:p-4", className)} aria-label="Arena winner">33 <div className="flex items-start gap-3">34 <span className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-accent text-accent-fg shadow-glow">35 <Trophy className="size-5" />36 </span>37 <div className="min-w-0 flex-1">38 <p className="text-[11px] font-medium uppercase tracking-[0.12em] text-accent">Arena winner</p>39 <div className="mt-0.5 flex min-w-0 items-center gap-2">40 {hidden ? <BlindAvatar index={blindIndex} size={20} /> : <ProviderIcon provider={provider} size={16} />}41 <h3 className="truncate text-[16px] font-semibold tracking-tight">{name}</h3>42 </div>43 <p className="mt-1 text-[12.5px] text-fg-muted">44 Won {criteria.length} of your criteri{criteria.length === 1 ? "on" : "a"}45 {winner.tieBreak === "fastest" ? " — tie broken by time to first token" : winner.tieBreak === "order" ? " — tie broken by order" : ""}.46 </p>47 <ul className="mt-2 flex flex-wrap gap-1.5" aria-label="Criteria won">48 {criteria.map((c) => (49 <li key={c.id}>50 <Badge variant="accent">{c.label}</Badge>51 </li>52 ))}53 </ul>54 </div>55 </div>56 {d.others > 0 ? (57 <dl className="mt-3 grid grid-cols-3 gap-2 border-t border-accent/20 pt-3 text-[12px] tabular-nums">58 {showCosts ? <Delta icon={<Coins />} label="Δ cost" value={formatDelta(d.costUsd, "usd")} good={better(d.costUsd)} hint="vs. others" /> : null}59 <Delta icon={<Timer />} label="Δ TTFT" value={formatDelta(d.ttftMs, "ms")} good={better(d.ttftMs)} hint="vs. others" />60 <Delta icon={<Type />} label="Δ output" value={formatDelta(d.outputTokens, "tokens")} good={null} hint="vs. others" />61 </dl>62 ) : null}63 </section>64 );65}6667function Delta({ icon, label, value, good, hint }: { icon: React.ReactNode; label: string; value: string; good: boolean | null; hint: string }) {68 return (69 <div className="min-w-0">70 <dt className="flex items-center gap-1 text-[10.5px] uppercase tracking-wide text-fg-subtle [&_svg]:size-3">71 {icon}72 {label}73 </dt>74 <dd className={cn("truncate text-[13px] font-semibold", good === true ? "text-success" : good === false ? "text-warning" : "text-fg")}>{value}</dd>75 <dd className="truncate text-[10.5px] text-fg-subtle">{hint}</dd>76 </div>77 );78}79