"use client"; import * as React from "react"; import Link from "next/link"; import { ArrowLeft, Coins, Info, Menu, Swords, Trophy } from "lucide-react"; import { useApp } from "@/components/app/store"; import { ProviderIcon } from "@/components/brand/provider-icon"; import { Button } from "@/components/ui/button"; import { ChipRow } from "@/components/ui/segmented"; import { Skeleton } from "@/components/ui/misc"; import { Tooltip } from "@/components/ui/tooltip"; import { useApi } from "@/lib/client/api"; import { PROVIDERS } from "@/lib/client/providers"; import { CATEGORIES, criterionById, type ScoreboardRow, type TaskCategory } from "@/lib/arena/scoring"; import { cn, formatMs, formatUsd } from "@/lib/utils"; type Filter = "all" | TaskCategory | "value"; const FILTERS: { value: Filter; label: string; icon?: React.ReactNode }[] = [{ value: "all", label: "All" }, ...CATEGORIES.map((c) => ({ value: c.value as Filter, label: c.label })), { value: "value", label: "Cost efficiency", icon: }]; interface ScoreboardResponse { rows: ScoreboardRow[]; sessions: number; votes: number; } /** Personal Arena scoreboard: win rate per model from your own votes. Mobile-first ranking list. */ export function ScoreboardView() { const { modelsByKey, setSidebarOpen, preferences } = useApp(); const [filter, setFilter] = React.useState("all"); const query = filter === "all" ? "" : filter === "value" ? "?criterion=value" : `?category=${filter}`; const { data, isLoading, error } = useApi(`/api/arena/scoreboard${query}`); const rows = React.useMemo(() => (data?.rows ?? []).filter((r) => r.sessions > 0), [data]); const ranked = React.useMemo(() => rows.filter((r) => r.decided > 0), [rows]); const unranked = React.useMemo(() => rows.filter((r) => r.decided === 0), [rows]); const nameOf = (k: string) => modelsByKey.get(k)?.displayName ?? k.split("/").slice(1).join("/"); return (

Scoreboard

Which models win your comparisons.

{filter === "value" ? "Ranked by “Best value” votes only." : filter === "all" ? "Ranked by the share of decided comparisons each model won (most criteria; ties → fastest)." : `Only comparisons classified as ${FILTERS.find((f) => f.value === filter)?.label.toLowerCase()} prompts.`} {data ? ` ${data.sessions} session${data.sessions === 1 ? "" : "s"} · ${data.votes} vote${data.votes === 1 ? "" : "s"}.` : ""}

{error ? (

Could not load the scoreboard: {(error as Error).message}

) : isLoading && !data ? (
    {[0, 1, 2, 3].map((i) => ( ))}
) : !ranked.length ? ( 0} filtered={filter !== "all"} /> ) : (
    {ranked.map((r, i) => ( ))}
)} {unranked.length && ranked.length ? (
{unranked.length} model{unranked.length === 1 ? "" : "s"} without a decided comparison
    {unranked.map((r) => (
  • {nameOf(r.modelKey)} · {r.sessions}
  • ))}
) : null}
); } function RankRow({ rank, row: r, name, showCosts, filter }: { rank: number; row: ScoreboardRow; name: string; showCosts: boolean; filter: Filter }) { const pct = Math.round(r.winRate * 100); const top = rank === 1; const crit = Object.entries(r.criteria) .sort((a, b) => b[1] - a[1]) .slice(0, 3); return (
  • {rank}

    {name}

    {PROVIDERS[r.provider as keyof typeof PROVIDERS]?.shortName ?? r.provider}
    {pct}%
    `${criterionById(id).short} ${n}`).join(" · ") : undefined} /> {showCosts ? : null}
  • ); } function Stat({ label, value, hint, className }: { label: string; value: string; hint?: string; className?: string }) { return (
    {label}
    {value}
    {hint ? (
    {hint}
    ) : null}
    ); } function EmptyScoreboard({ hasSessions, filtered }: { hasSessions: boolean; filtered: boolean }) { return (

    {filtered ? "No decided comparison in this filter yet" : hasSessions ? "Vote to build your ranking" : "Your ranking starts with a comparison"}

    Run the same prompt through several models in the Arena, then vote on criteria such as Best answer, Most accurate or Best value. Each decided comparison counts as a win for the model that took the most criteria (ties go to the fastest). The win rate is wins over the comparisons a model took part in that received a vote.

    ); }