"use client"; import * as React from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { AlertTriangle, Check, GitCompareArrows, MessageSquare, Minus, Star, Swords, Tag } from "lucide-react"; import type { ModelCapabilities, PolyModel } from "@/lib/client/types"; import { useApp } from "@/components/app/store"; import { ProviderIcon } from "@/components/brand/provider-icon"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { ResponsiveDialog } from "@/components/ui/sheet"; import { CopyButton } from "@/components/ui/misc"; import { PromptDialog } from "@/components/common/prompt-dialog"; import { ModelBadges, StatusBadge, useBadgeContext } from "@/components/chat/model-badges"; import { CAPABILITY_LABELS } from "./compare-table"; import { PROVIDERS } from "@/lib/client/providers"; import { PARAM_DEFS, SETTINGS_GROUPS } from "@/lib/models/params"; import { speedTier, shutdownDateOf } from "@/lib/models/badges"; import { formatContext, formatIsoDate, formatPrice } from "@/lib/models/format"; import { cn } from "@/lib/utils"; /** * Model profile — every field the registry knows, "—" for everything it does not. * Release dates and knowledge cutoffs are shown only when the provider publishes them. */ export interface ModelProfileProps { model: PolyModel | null; open: boolean; onOpenChange: (open: boolean) => void; /** Override "Use in chat" (e.g. the selector picks the model instead of navigating). */ onUse?: (m: PolyModel) => void; /** Optional current model key to seed "Compare" with a second column. */ compareWith?: string | null; } function metaStr(m: PolyModel, key: string): string | null { const v = m.metadata?.[key]; return typeof v === "string" && v ? v : null; } export function ModelProfile({ model: m, open, onOpenChange, onUse, compareWith }: ModelProfileProps) { const router = useRouter(); const { connectedProviders, favorites, labels, toggleFavorite, setLabel, setSelectedModelKey } = useApp(); const ctx = useBadgeContext(); const [labelOpen, setLabelOpen] = React.useState(false); if (!m) return null; const connected = connectedProviders.has(m.provider); const fav = favorites.has(m.key); const label = labels[m.key]; const sd = shutdownDateOf(m); const lc = m.pricing?.longContext; const tier = speedTier(m, ctx); const aliases = Array.isArray(m.metadata?.aliases) ? (m.metadata!.aliases as string[]) : []; const description = metaStr(m, "description") ?? metaStr(m, "notes"); const compareKeys = [...new Set([compareWith, m.key].filter((k): k is string => Boolean(k)))]; const compareHref = `/app/models/compare?m=${encodeURIComponent(compareKeys.join(","))}`; const use = () => { onOpenChange(false); if (onUse) return onUse(m); setSelectedModelKey(m.key); router.push("/app/chat"); }; const facts: { label: string; value: React.ReactNode; hint?: string }[] = [ { label: "Context", value: formatContext(m.limits?.contextTokens) }, { label: "Max output", value: formatContext(m.limits?.maxOutputTokens) }, { label: "Input $/1M", value: formatPrice(m.pricing?.inputPerMillion) }, { label: "Output $/1M", value: formatPrice(m.pricing?.outputPerMillion) }, { label: "Cached input $/1M", value: formatPrice(m.pricing?.cachedInputPerMillion) }, { label: "Long-context tier", value: lc ? `> ${formatContext(lc.thresholdTokens)}: ${formatPrice(lc.inputPerMillion)} / ${formatPrice(lc.outputPerMillion)}` : "—", hint: lc ? "input / output above the threshold" : undefined }, { label: "Speed tier", value: { fast: "Fast", standard: "Standard", frontier: "Frontier" }[tier] }, { label: "Release date", value: metaStr(m, "releaseDate") ? formatIsoDate(metaStr(m, "releaseDate")) : "—", hint: "as published by the provider" }, { label: "Knowledge cutoff", value: metaStr(m, "knowledgeCutoff") ?? "—", hint: "as published by the provider" }, { label: "Listed by provider", value: formatIsoDate(metaStr(m, "createdAt")), hint: "creation timestamp from the provider’s model list" }, { label: "First seen in PolyLLM", value: formatIsoDate(metaStr(m, "firstSeenAt")) }, { label: "Prices as of", value: m.pricing?.asOf ?? "—" }, ]; return ( <> {connected ? `${PROVIDERS[m.provider].shortName} key connected` : `Connect a ${PROVIDERS[m.provider].shortName} key to use this model`}
{connected ? ( ) : ( )}
} >
{/* Header */}

{m.displayName}

{label ? ( {label} ) : null}
{m.key} {m.family ? · {m.family} : null}
{sd ? (

{PROVIDERS[m.provider].name} has scheduled this model for shutdown on {formatIsoDate(sd)}. Move presets to a newer model before then.

) : null} {description ?

{description}

: null} {/* Facts */}
{facts.map((f) => (
{f.label}
{f.value}
{f.hint ?
{f.hint}
: null}
))}
{/* Capabilities */}

Capabilities

    {(Object.keys(CAPABILITY_LABELS) as (keyof ModelCapabilities)[]).map((k) => { const on = m.capabilities[k]; return (
  • {on ? : } {CAPABILITY_LABELS[k]}
  • ); })}
{/* Parameters */}

Supported parameters

{SETTINGS_GROUPS.map((g) => { const defs = PARAM_DEFS.filter((d) => d.group === g.key); return (

{g.label}

    {defs.map((d) => { const on = d.supports(m); const detail = on ? d.detail?.(m) : null; return (
  • {d.label} {detail ? {detail} : null}
  • ); })}
); })}
{m.metadata?.samplingMode === "conditional" ?

Sampling parameters are accepted only when reasoning effort is “none”.

: null} {m.metadata?.alwaysReasoning ?

This model always reasons; effort only changes how much.

: null}
{/* Lifecycle */}

Lifecycle

Provider status
{m.status}
Shutdown date
{sd ? formatIsoDate(sd) : "—"}
Availability
{connected ? "Usable with your key" : "No key connected"}
{aliases.length ? (

Aliases: {aliases.join(", ")}

) : null} {m.metadata?.source ? (

Source: {String(m.metadata.source)} {m.pricing?.source ? <>· pricing from {String(m.pricing.source)} : null}

) : null}
setLabel(m.key, v)} /> ); }