import * as React from "react"; import Link from "next/link"; import { Check, Minus } from "lucide-react"; import type { ModelCapabilities, PolyModel } from "@/lib/ai/core/types"; import { ProviderIcon } from "@/components/brand/provider-icon"; import { PROVIDERS } from "@/lib/client/providers"; import { deriveBadges, lifecycleStatus, speedTier, type BadgeContext } from "@/lib/models/badges"; import { formatContext, formatIsoDate, formatPrice } from "@/lib/models/format"; import { PARAM_DEFS } from "@/lib/models/params"; import { BadgeChips, LifecycleChip } from "./badge-chips"; import { Meter } from "./meter"; import { cn } from "@/lib/utils"; /** * Side-by-side comparison of 2–4 models. Server-safe (no hooks): used by the public * `/compare/[slug]` page and by `/app/models/compare`. Desktop = grid table; phones = stacked * spec cards (one per row) so nothing overflows horizontally. */ export const CAPABILITY_LABELS: Record = { text: "Text", vision: "Vision (images)", reasoning: "Reasoning", tools: "Tool calling", structuredOutput: "Structured output", webSearch: "Web search", files: "File / PDF input", streaming: "Streaming", audioInput: "Audio input", audioOutput: "Audio output", imageGeneration: "Image generation", video: "Video input", }; interface SpecRow { key: string; label: string; hint?: string; value: (m: PolyModel) => React.ReactNode; /** Numeric magnitude for the meter (null = no bar). */ bar?: (m: PolyModel) => number | null; } function longTier(m: PolyModel): string { const lc = m.pricing?.longContext; if (!lc) return "—"; return `> ${formatContext(lc.thresholdTokens)}: ${formatPrice(lc.inputPerMillion)} / ${formatPrice(lc.outputPerMillion)}`; } function metaStr(m: PolyModel, key: string): string { const v = m.metadata?.[key]; return typeof v === "string" && v ? v : ""; } export function CompareTable({ models, ctx, hrefFor, connected, className }: { models: PolyModel[]; ctx: BadgeContext; hrefFor?: (m: PolyModel) => string | undefined; connected?: (m: PolyModel) => boolean; className?: string }) { const rows: SpecRow[] = [ { key: "provider", label: "Provider", value: (m) => PROVIDERS[m.provider].name }, { key: "family", label: "Family", value: (m) => m.family ?? "—" }, { key: "status", label: "Status", value: (m) => }, { key: "context", label: "Context window", value: (m) => `${formatContext(m.limits?.contextTokens)} tokens`, bar: (m) => m.limits?.contextTokens ?? null }, { key: "maxOut", label: "Max output", value: (m) => `${formatContext(m.limits?.maxOutputTokens)} tokens`, bar: (m) => m.limits?.maxOutputTokens ?? null }, { key: "in", label: "Input price", hint: "USD per 1M tokens — shorter is cheaper", value: (m) => formatPrice(m.pricing?.inputPerMillion), bar: (m) => m.pricing?.inputPerMillion ?? null }, { key: "out", label: "Output price", hint: "USD per 1M tokens — shorter is cheaper", value: (m) => formatPrice(m.pricing?.outputPerMillion), bar: (m) => m.pricing?.outputPerMillion ?? null }, { key: "cached", label: "Cached input", value: (m) => formatPrice(m.pricing?.cachedInputPerMillion) }, { key: "longTier", label: "Long-context tier", value: longTier }, { key: "speed", label: "Speed tier", value: (m) => ({ fast: "Fast", standard: "Standard", frontier: "Frontier" })[speedTier(m, ctx)] }, { key: "effort", label: "Reasoning effort", value: (m) => (m.parameters.reasoningEffort && m.parameters.reasoningEffortLevels?.length ? m.parameters.reasoningEffortLevels.join(" · ") : m.capabilities.reasoning ? "always on" : "—") }, { key: "budget", label: "Thinking budget", value: (m) => (m.parameters.thinkingBudgetRange ? `${formatContext(m.parameters.thinkingBudgetRange.min)}–${formatContext(m.parameters.thinkingBudgetRange.max)}` : "—") }, { key: "cutoff", label: "Knowledge cutoff", hint: "Only when the provider publishes it", value: (m) => metaStr(m, "knowledgeCutoff") || "—" }, { key: "release", label: "Release date", hint: "Only when the provider publishes it", value: (m) => (metaStr(m, "releaseDate") ? formatIsoDate(metaStr(m, "releaseDate")) : "—") }, { key: "badges", label: "Badges", value: (m) => }, ]; const colStyle: React.CSSProperties = { gridTemplateColumns: `150px repeat(${models.length}, minmax(0, 1fr))` }; const max = (row: SpecRow) => Math.max(0, ...models.map((m) => row.bar?.(m) ?? 0)); return (
{/* Header cards */}
= 3 ? "grid-cols-2 md:grid-cols-4" : "grid-cols-2")}> {models.map((m) => { const href = hrefFor?.(m); const inner = ( <> {PROVIDERS[m.provider].shortName} {m.displayName} {m.id} ); return href ? ( {inner} ) : (
{inner}
); })}
{/* Desktop grid */}
Spec {models.map((m) => ( {m.displayName} ))}
{rows.map((row) => { const mx = row.bar ? max(row) : 0; return (
{row.label}
{row.hint ?
{row.hint}
: null}
{models.map((m) => (
{row.value(m)}
{row.bar ? : null}
))}
); })}
{/* Mobile stacked */}
{rows.map((row) => { const mx = row.bar ? max(row) : 0; return (
{row.label}
{row.hint ?
{row.hint}
: null}
    {models.map((m) => (
  • {m.displayName} {row.value(m)}
    {row.bar ? : null}
  • ))}
); })}
({ key: k, label: CAPABILITY_LABELS[k], on: (m: PolyModel) => Boolean(m.capabilities[k]) }))} /> ({ key: d.key, label: d.label, on: (m: PolyModel) => d.supports(m), detail: (m: PolyModel) => (d.supports(m) ? d.detail?.(m) ?? null : null) }))} />
); } function Matrix({ title, models, rows }: { title: string; models: PolyModel[]; rows: { key: string; label: string; on: (m: PolyModel) => boolean; detail?: (m: PolyModel) => string | null }[] }) { const colStyle: React.CSSProperties = { gridTemplateColumns: `150px repeat(${models.length}, minmax(0, 1fr))` }; const mobileStyle: React.CSSProperties = { gridTemplateColumns: `minmax(0, 1fr) repeat(${models.length}, 40px)` }; return (

{title}

{/* Desktop */}
{models.map((m) => ( {m.displayName} ))}
{rows.map((r) => (
{r.label} {models.map((m) => ( {r.detail?.(m) ? {r.detail(m)} : null} ))}
))}
{/* Mobile: compact check grid with icon headers */}
{models.map((m) => ( ))}
{rows.map((r) => (
{r.label} {models.map((m) => ( ))}
))}

Columns: {models.map((m) => m.displayName).join(" · ")}

); } function Mark({ on }: { on: boolean }) { return on ? : ; }