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%
13.5 KB · 258 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import Link from "next/link";4import { useRouter } from "next/navigation";5import { AlertTriangle, Check, GitCompareArrows, MessageSquare, Minus, Star, Swords, Tag } from "lucide-react";6import type { ModelCapabilities, PolyModel } from "@/lib/client/types";7import { useApp } from "@/components/app/store";8import { ProviderIcon } from "@/components/brand/provider-icon";9import { Button } from "@/components/ui/button";10import { Badge } from "@/components/ui/badge";11import { ResponsiveDialog } from "@/components/ui/sheet";12import { CopyButton } from "@/components/ui/misc";13import { PromptDialog } from "@/components/common/prompt-dialog";14import { ModelBadges, StatusBadge, useBadgeContext } from "@/components/chat/model-badges";15import { CAPABILITY_LABELS } from "./compare-table";16import { PROVIDERS } from "@/lib/client/providers";17import { PARAM_DEFS, SETTINGS_GROUPS } from "@/lib/models/params";18import { speedTier, shutdownDateOf } from "@/lib/models/badges";19import { formatContext, formatIsoDate, formatPrice } from "@/lib/models/format";20import { cn } from "@/lib/utils";2122/**23 * Model profile — every field the registry knows, "—" for everything it does not.24 * Release dates and knowledge cutoffs are shown only when the provider publishes them.25 */26export interface ModelProfileProps {27  model: PolyModel | null;28  open: boolean;29  onOpenChange: (open: boolean) => void;30  /** Override "Use in chat" (e.g. the selector picks the model instead of navigating). */31  onUse?: (m: PolyModel) => void;32  /** Optional current model key to seed "Compare" with a second column. */33  compareWith?: string | null;34}3536function metaStr(m: PolyModel, key: string): string | null {37  const v = m.metadata?.[key];38  return typeof v === "string" && v ? v : null;39}4041export function ModelProfile({ model: m, open, onOpenChange, onUse, compareWith }: ModelProfileProps) {42  const router = useRouter();43  const { connectedProviders, favorites, labels, toggleFavorite, setLabel, setSelectedModelKey } = useApp();44  const ctx = useBadgeContext();45  const [labelOpen, setLabelOpen] = React.useState(false);46  if (!m) return null;4748  const connected = connectedProviders.has(m.provider);49  const fav = favorites.has(m.key);50  const label = labels[m.key];51  const sd = shutdownDateOf(m);52  const lc = m.pricing?.longContext;53  const tier = speedTier(m, ctx);54  const aliases = Array.isArray(m.metadata?.aliases) ? (m.metadata!.aliases as string[]) : [];55  const description = metaStr(m, "description") ?? metaStr(m, "notes");56  const compareKeys = [...new Set([compareWith, m.key].filter((k): k is string => Boolean(k)))];57  const compareHref = `/app/models/compare?m=${encodeURIComponent(compareKeys.join(","))}`;5859  const use = () => {60    onOpenChange(false);61    if (onUse) return onUse(m);62    setSelectedModelKey(m.key);63    router.push("/app/chat");64  };6566  const facts: { label: string; value: React.ReactNode; hint?: string }[] = [67    { label: "Context", value: formatContext(m.limits?.contextTokens) },68    { label: "Max output", value: formatContext(m.limits?.maxOutputTokens) },69    { label: "Input $/1M", value: formatPrice(m.pricing?.inputPerMillion) },70    { label: "Output $/1M", value: formatPrice(m.pricing?.outputPerMillion) },71    { label: "Cached input $/1M", value: formatPrice(m.pricing?.cachedInputPerMillion) },72    { label: "Long-context tier", value: lc ? `> ${formatContext(lc.thresholdTokens)}: ${formatPrice(lc.inputPerMillion)} / ${formatPrice(lc.outputPerMillion)}` : "—", hint: lc ? "input / output above the threshold" : undefined },73    { label: "Speed tier", value: { fast: "Fast", standard: "Standard", frontier: "Frontier" }[tier] },74    { label: "Release date", value: metaStr(m, "releaseDate") ? formatIsoDate(metaStr(m, "releaseDate")) : "—", hint: "as published by the provider" },75    { label: "Knowledge cutoff", value: metaStr(m, "knowledgeCutoff") ?? "—", hint: "as published by the provider" },76    { label: "Listed by provider", value: formatIsoDate(metaStr(m, "createdAt")), hint: "creation timestamp from the provider’s model list" },77    { label: "First seen in PolyLLM", value: formatIsoDate(metaStr(m, "firstSeenAt")) },78    { label: "Prices as of", value: m.pricing?.asOf ?? "—" },79  ];8081  return (82    <>83      <ResponsiveDialog84        open={open}85        onOpenChange={onOpenChange}86        size="lg"87        snap="full"88        hideTitle89        title={m.displayName}90        description={`${PROVIDERS[m.provider].name} model profile`}91        footer={92          <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">93            <span className="text-xs text-fg-subtle">{connected ? `${PROVIDERS[m.provider].shortName} key connected` : `Connect a ${PROVIDERS[m.provider].shortName} key to use this model`}</span>94            <div className="grid grid-cols-3 gap-2 sm:flex">95              <Button variant="outline" size="sm" className="h-10 sm:h-8" onClick={() => { onOpenChange(false); router.push(compareHref); }}>96                <GitCompareArrows /> <span className="truncate">Compare</span>97              </Button>98              <Button variant="outline" size="sm" className="h-10 sm:h-8" asChild>99                <Link href={`/app/arena?models=${encodeURIComponent(m.key)}`} onClick={() => onOpenChange(false)}>100                  <Swords /> <span className="truncate">Arena</span>101                </Link>102              </Button>103              {connected ? (104                <Button size="sm" className="h-10 sm:h-8" onClick={use} disabled={m.status === "deprecated"}>105                  <MessageSquare /> <span className="truncate">Use in chat</span>106                </Button>107              ) : (108                <Button size="sm" className="h-10 sm:h-8" asChild>109                  <Link href="/app/settings/providers">Connect</Link>110                </Button>111              )}112            </div>113          </div>114        }115      >116        <div className="space-y-6 pt-1">117          {/* Header */}118          <div className="flex items-start gap-3">119            <span className="flex size-11 shrink-0 items-center justify-center rounded-xl bg-bg-subtle">120              <ProviderIcon provider={m.provider} size={22} />121            </span>122            <div className="min-w-0 flex-1">123              <div className="flex flex-wrap items-center gap-x-2 gap-y-1">124                <h2 className="text-lg font-semibold leading-6 tracking-tight">{m.displayName}</h2>125                {label ? (126                  <span className="inline-flex items-center gap-1 rounded-md bg-accent-soft pl-1.5 pr-0.5 text-[12px] font-medium text-accent">127                    {label}128                    <button type="button" className="tap rounded p-0.5 text-accent/70 hover:text-accent" aria-label="Remove custom label" onClick={() => void setLabel(m.key, null)}>129                      <Minus className="size-3" />130                    </button>131                  </span>132                ) : null}133                <StatusBadge model={m} connected={connected} ctx={ctx} showActive />134              </div>135              <div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-[12px] text-fg-muted">136                <span className="inline-flex items-center gap-1 font-mono">137                  {m.key}138                  <CopyButton value={m.key} label="Copy model key" className="size-6" />139                </span>140                {m.family ? <span>· {m.family}</span> : null}141              </div>142              <ModelBadges model={m} ctx={ctx} max={7} className="mt-2" />143            </div>144            <div className="flex shrink-0 flex-col gap-1">145              <Button variant="ghost" size="icon" aria-pressed={fav} aria-label={fav ? "Remove from favorites" : "Add to favorites"} onClick={() => void toggleFavorite(m.key)} className={cn(fav && "text-warning")}>146                <Star key={fav ? "on" : "off"} className={cn(fav && "fill-current animate-pop")} />147              </Button>148              <Button variant="ghost" size="icon" aria-label="Set custom label" onClick={() => setLabelOpen(true)}>149                <Tag />150              </Button>151            </div>152          </div>153154          {sd ? (155            <div className="flex items-start gap-2 rounded-lg bg-warning-soft px-3 py-2 text-xs text-warning">156              <AlertTriangle className="mt-0.5 size-3.5 shrink-0" />157              <p>158                {PROVIDERS[m.provider].name} has scheduled this model for shutdown on <span className="font-medium">{formatIsoDate(sd)}</span>. Move presets to a newer model before then.159              </p>160            </div>161          ) : null}162163          {description ? <p className="text-[13px] leading-5 text-fg-muted">{description}</p> : null}164165          {/* Facts */}166          <dl className="grid grid-cols-2 gap-2 sm:grid-cols-3">167            {facts.map((f) => (168              <div key={f.label} className="panel px-3 py-2">169                <dt className="text-[11px] text-fg-subtle">{f.label}</dt>170                <dd className="mt-0.5 truncate font-mono text-[13px] tabular-nums" title={typeof f.value === "string" ? f.value : undefined}>171                  {f.value}172                </dd>173                {f.hint ? <dd className="text-[10px] text-fg-subtle">{f.hint}</dd> : null}174              </div>175            ))}176          </dl>177178          {/* Capabilities */}179          <section>180            <h3 className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-fg-subtle">Capabilities</h3>181            <ul className="grid grid-cols-2 gap-1.5 sm:grid-cols-3">182              {(Object.keys(CAPABILITY_LABELS) as (keyof ModelCapabilities)[]).map((k) => {183                const on = m.capabilities[k];184                return (185                  <li key={k} className={cn("flex items-center gap-2 rounded-md px-2.5 py-1.5 text-[12.5px]", on ? "bg-bg-subtle text-fg" : "text-fg-subtle")}>186                    {on ? <Check className="size-3.5 text-success" aria-hidden /> : <Minus className="size-3.5 text-border-strong" aria-hidden />}187                    {CAPABILITY_LABELS[k]}188                  </li>189                );190              })}191            </ul>192          </section>193194          {/* Parameters */}195          <section>196            <h3 className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-fg-subtle">Supported parameters</h3>197            <div className="space-y-3">198              {SETTINGS_GROUPS.map((g) => {199                const defs = PARAM_DEFS.filter((d) => d.group === g.key);200                return (201                  <div key={g.key}>202                    <p className="mb-1 text-[11px] font-medium text-fg-muted">{g.label}</p>203                    <ul className="grid grid-cols-2 gap-1.5 sm:grid-cols-3">204                      {defs.map((d) => {205                        const on = d.supports(m);206                        const detail = on ? d.detail?.(m) : null;207                        return (208                          <li key={d.key} className={cn("rounded-md px-2.5 py-1.5 text-[12.5px]", on ? "bg-bg-subtle text-fg" : "text-fg-subtle line-through decoration-border-strong")}>209                            <span>{d.label}</span>210                            {detail ? <span className="mt-0.5 block font-mono text-[10.5px] text-fg-muted no-underline">{detail}</span> : null}211                          </li>212                        );213                      })}214                    </ul>215                  </div>216                );217              })}218            </div>219            {m.metadata?.samplingMode === "conditional" ? <p className="mt-2 text-[11.5px] text-fg-subtle">Sampling parameters are accepted only when reasoning effort is “none”.</p> : null}220            {m.metadata?.alwaysReasoning ? <p className="mt-1 text-[11.5px] text-fg-subtle">This model always reasons; effort only changes how much.</p> : null}221          </section>222223          {/* Lifecycle */}224          <section>225            <h3 className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-fg-subtle">Lifecycle</h3>226            <dl className="grid grid-cols-2 gap-2 text-[12.5px] sm:grid-cols-3">227              <div className="panel px-3 py-2">228                <dt className="text-[11px] text-fg-subtle">Provider status</dt>229                <dd className="mt-0.5 capitalize">{m.status}</dd>230              </div>231              <div className="panel px-3 py-2">232                <dt className="text-[11px] text-fg-subtle">Shutdown date</dt>233                <dd className="mt-0.5 font-mono">{sd ? formatIsoDate(sd) : "—"}</dd>234              </div>235              <div className="panel px-3 py-2">236                <dt className="text-[11px] text-fg-subtle">Availability</dt>237                <dd className="mt-0.5">{connected ? "Usable with your key" : "No key connected"}</dd>238              </div>239            </dl>240            {aliases.length ? (241              <p className="mt-2 text-xs text-fg-muted">242                Aliases: <span className="font-mono">{aliases.join(", ")}</span>243              </p>244            ) : null}245            {m.metadata?.source ? (246              <p className="mt-1 text-[11px] text-fg-subtle">247                Source: <Badge variant="outline">{String(m.metadata.source)}</Badge> {m.pricing?.source ? <>· pricing from {String(m.pricing.source)}</> : null}248              </p>249            ) : null}250          </section>251        </div>252      </ResponsiveDialog>253254      <PromptDialog open={labelOpen} onOpenChange={setLabelOpen} title="Custom label" description={`Shown next to ${m.displayName} everywhere in PolyLLM.`} label="Label" placeholder="My research model" defaultValue={label ?? ""} maxLength={60} confirmLabel="Save" onSubmit={(v) => setLabel(m.key, v)} />255    </>256  );257}258