SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
7.0 KB · 129 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import { Brain, EyeOff, Trophy, X } from "lucide-react";4import { ProviderIcon } from "@/components/brand/provider-icon";5import { Badge } from "@/components/ui/badge";6import { SimpleMarkdown } from "@/components/markdown/simple-markdown";7import { useIsMobile, useSnapCarousel } from "@/lib/client/hooks";8import type { ArenaShareSnapshot } from "@/lib/arena/export";9import type { LiveMetrics } from "@/lib/arena/metrics";10import { providerName } from "@/lib/client/providers";11import { cn } from "@/lib/utils";12import { ModelTabs } from "./model-tabs";13import { MetricsStrip } from "./metrics-strip";14import { ComparisonTable } from "./comparison-table";15import { WinnerCard } from "./winner-card";16import type { ColumnStatus } from "./types";1718function statusOf(s: string): ColumnStatus {19  return s === "complete" ? "done" : s === "error" ? "error" : "stopped";20}2122function metricsOf(r: ArenaShareSnapshot["responses"][number]): LiveMetrics {23  const out = r.usage?.outputTokens ?? null;24  const gen = typeof r.latencyMs === "number" ? Math.max(1, r.latencyMs - (r.ttftMs ?? 0)) : null;25  return { status: statusOf(r.status), elapsedMs: r.latencyMs ?? 0, ttftMs: r.ttftMs, inputTokens: r.usage?.inputTokens ?? null, outputTokens: out, reasoningTokens: r.usage?.reasoningTokens ?? null, cachedTokens: null, tokensPerSecond: out && gen ? Math.round((out / gen) * 1000) : null, costUsd: r.costUsd, exact: true };26}2728const GRID: Record<number, string> = { 1: "md:grid-cols-1", 2: "md:grid-cols-2", 3: "md:grid-cols-2 xl:grid-cols-3", 4: "md:grid-cols-2" };2930/** Public, read-only rendering of a shared Arena snapshot: swipe carousel on phones, grid on desktop. */31export function SharedArenaView({ snapshot: s }: { snapshot: ArenaShareSnapshot }) {32  const isMobile = useIsMobile();33  const responses = s.responses;34  const { ref, index, scrollTo } = useSnapCarousel<HTMLDivElement>(responses.length);35  const winnerKey = s.winner?.responseId ?? null;36  const metrics = React.useMemo(() => responses.map(metricsOf), [responses]);3738  const tabs = responses.map((r, i) => ({ key: r.id, name: r.displayName, provider: r.provider, status: statusOf(r.status), tokensPerSecond: metrics[i].tokensPerSecond, isWinner: r.id === winnerKey, blindIndex: i }));39  const rows = responses.map((r, i) => ({ key: r.id, name: r.displayName, provider: r.provider, blindIndex: i, metrics: metrics[i], criteriaWon: r.criteriaWon, isWinner: r.id === winnerKey }));4041  return (42    <div className="space-y-4">43      {isMobile ? (44        <>45          <div className="sticky top-14 z-30 -mx-4 bg-bg/95 px-4 py-2 backdrop-blur-sm">46            <ModelTabs items={tabs} active={index} onSelect={(i) => scrollTo(i)} />47          </div>48          <div ref={ref} className="snap-row" aria-roledescription="carousel" data-no-edge-swipe>49            {responses.map((r, i) => (50              <div key={r.id} className="px-0.5">51                <ResponseCard r={r} metrics={metrics[i]} isWinner={r.id === winnerKey} />52              </div>53            ))}54          </div>55          <p className="text-center text-[11.5px] text-fg-subtle">56            Swipe to compare · {index + 1} / {responses.length}57          </p>58        </>59      ) : (60        <div className={cn("grid grid-cols-1 gap-3", GRID[responses.length] ?? "md:grid-cols-2")}>61          {responses.map((r, i) => (62            <ResponseCard key={r.id} r={r} metrics={metrics[i]} isWinner={r.id === winnerKey} />63          ))}64        </div>65      )}6667      {s.winner ? <WinnerCard winner={{ ...s.winner, votes: s.winner.criteriaWon.length }} name={responses.find((r) => r.id === s.winner!.responseId)?.displayName ?? s.winner.modelKey} provider={responses.find((r) => r.id === s.winner!.responseId)?.provider ?? null} /> : null}68      <ComparisonTable rows={rows} />69    </div>70  );71}7273function ResponseCard({ r, metrics, isWinner }: { r: ArenaShareSnapshot["responses"][number]; metrics: LiveMetrics; isWinner: boolean }) {74  return (75    <article className={cn("flex min-w-0 flex-col overflow-hidden rounded-xl border bg-bg-elevated", isWinner ? "border-accent/60 shadow-glow" : "border-border")} aria-label={`${r.displayName} response`}>76      <header className="flex items-center gap-2 border-b border-border px-3 py-2">77        <span className="flex size-7 shrink-0 items-center justify-center rounded-md border border-border bg-bg-subtle">78          <ProviderIcon provider={r.provider} size={15} />79        </span>80        <div className="min-w-0 flex-1">81          <p className="truncate text-[13px] font-semibold leading-5">{r.displayName}</p>82          <p className="truncate text-[11.5px] text-fg-muted">{providerName(r.provider)}</p>83        </div>84        {isWinner ? (85          <Badge variant="accent" className="gap-1">86            <Trophy /> Winner87          </Badge>88        ) : null}89        {r.criteriaWon.length && !isWinner ? <Badge variant="outline">{r.criteriaWon.length} vote{r.criteriaWon.length === 1 ? "" : "s"}</Badge> : null}90      </header>91      <div className="min-w-0 flex-1 space-y-2.5 px-3.5 py-3 text-[14.5px] leading-6 md:max-h-[min(62vh,720px)] md:overflow-y-auto md:scrollbar-thin">92        {r.reasoning ? (93          <details className="group rounded-lg border border-border bg-bg-subtle/60">94            <summary className="flex cursor-pointer select-none items-center gap-2 px-3 py-2 text-[12.5px] text-fg-muted marker:content-none [&::-webkit-details-marker]:hidden">95              <Brain className="size-3.5" aria-hidden /> Reasoning96              <span className="ml-auto text-[11px] text-fg-subtle group-open:hidden">Show</span>97              <span className="ml-auto hidden text-[11px] text-fg-subtle group-open:inline">Hide</span>98            </summary>99            <div className="border-t border-border px-3 py-2 text-[13px] leading-6 text-fg-muted">100              <SimpleMarkdown className="text-[13px]">{r.reasoning}</SimpleMarkdown>101            </div>102          </details>103        ) : null}104        {r.error ? (105          <div className="flex items-start gap-2 rounded-lg border border-danger/30 bg-danger-soft px-3 py-2 text-[13px] text-danger">106            <X className="mt-0.5 size-4 shrink-0" />107            <div className="min-w-0">108              <div className="font-medium break-words">{r.error.message}</div>109              <div className="font-mono text-[11px] opacity-80">{r.error.code}</div>110            </div>111          </div>112        ) : null}113        {r.content ? <SimpleMarkdown>{r.content}</SimpleMarkdown> : !r.error ? <p className="text-[13px] italic text-fg-subtle">{r.status === "stopped" ? "Stopped before any output." : "Empty response."}</p> : null}114      </div>115      <footer className="border-t border-border bg-bg-subtle/50 px-3 py-2">116        <MetricsStrip metrics={metrics} />117      </footer>118    </article>119  );120}121122export function BlindNotice() {123  return (124    <p className="inline-flex items-center gap-1.5 rounded-md bg-bg-muted px-2 py-1 text-[11.5px] text-fg-muted">125      <EyeOff className="size-3.5" /> Blind Arena — identities were hidden until the vote126    </p>127  );128}129