import { ExternalLink } from 'lucide-react';
import { ScrollX } from '@/components/models/scroll-x';
import Link from 'next/link';
import { ChangeRow } from '@/components/changes/change-row';
import { Legend, LineChart, type Series, Sparkline, stepPoints } from '@/components/charts/charts';
import { Chip, ConfidenceBadge, Estimated, TierBadge } from '@/components/ui/badges';
import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';
import { EntityInline, EntityLink, EntityRow, QualityMark } from '@/components/ui/entity';
import { KeyValue, type KVRow } from '@/components/ui/key-value';
import { SourceCell } from '@/components/ui/provenance';
import { Note } from '@/components/ui/section';
import { EmptyState } from '@/components/ui/unavailable';
import { fmtAgo, fmtDate, fmtGb, fmtInt, fmtParams, fmtScore, fmtTokens, fmtUsdPerM, fmtValue, num } from '@/lib/format';
import { predicateLabel, PROSE_KEYS, propertyLabel, routes, typeLabel } from '@/lib/site';
import type { BenchmarkResult, ChangeEvent, EntityDetail, EntitySummary, HardwareFitRow, Price, Provenance, RelationGroup, SourceRef } from '@/lib/types';
/* ------------------------------------------------------------------------------------------------------ spec table */
const MODEL_ORDER = ['family', 'version', 'release_date', 'status', 'openness', 'license', 'architecture', 'parameter_count', 'active_parameter_count', 'is_moe', 'context_length', 'max_output_tokens', 'knowledge_cutoff', 'training_data_cutoff', 'modalities', 'modalities_input', 'modalities_output', 'languages', 'tokenizer', 'api_model_id', 'api_alias', 'base_model', 'quantization', 'quant_format', 'file_size_gb', 'deprecation_date', 'retirement_date', 'retirement_tentative', 'hf_repo', 'pipeline_tag', 'official_url', 'model_card_url', 'paper_url', 'repository_url', 'metric.downloads', 'metric.likes'];
const CAPABILITY_KEYS = ['tool_calling', 'structured_output', 'reasoning', 'vision', 'audio', 'fine_tuning_available'];
const HIDDEN = new Set(['name', 'slug', 'id', 'entity_type']);
/** Every non-prose attribute as a KeyValue list, model keys in canonical order, others alphabetical. */
export function SpecTable({ d, exclude = [] }: { d: EntityDetail; exclude?: string[] }) {
const attrs = d.attributes ?? {};
const skip = new Set([...exclude, ...HIDDEN, ...PROSE_KEYS]);
const keys = Object.keys(attrs).filter((k) => !skip.has(k) && attrs[k] !== null && attrs[k] !== undefined && attrs[k] !== '' && !(Array.isArray(attrs[k]) && (attrs[k] as unknown[]).length === 0));
const order = new Map(MODEL_ORDER.map((k, i) => [k, i]));
keys.sort((a, b) => (order.get(a) ?? 999) - (order.get(b) ?? 999) || a.localeCompare(b));
const rows: KVRow[] = keys.map((k) => ({ key: k, raw: attrs[k] }));
return ;
}
/** Identity block: aliases + identifiers (scheme: value). */
export function Identity({ d }: { d: EntityDetail }) {
if (!d.aliases?.length && !d.identifiers?.length) return null;
return (
{d.identifiers?.length > 0 && (
{d.identifiers.map((i) => (
{i.scheme}
{i.value}
))}
)}
{d.aliases?.length > 0 && (
Also known as: {d.aliases.join(', ')}
)}
);
}
/* ------------------------------------------------------------------------------------------------------ capabilities */
export function Capabilities({ d }: { d: EntityDetail }) {
const a = d.attributes ?? {};
const mods = (Array.isArray(a.modalities) ? a.modalities : []) as string[];
const inMods = (Array.isArray(a.modalities_input) ? a.modalities_input : []) as string[];
const outMods = (Array.isArray(a.modalities_output) ? a.modalities_output : []) as string[];
const flags = CAPABILITY_KEYS.map((k) => ({ key: k, value: a[k] }));
const known = flags.filter((f) => typeof f.value === 'boolean');
return (
Modalities
{mods.length || inMods.length || outMods.length ? (
{mods.length > 0 && (
Modalities
{mods.map((m) => {m} )}
)}
{inMods.length > 0 && (
Input
{inMods.map((m) => {m} )}
)}
{outMods.length > 0 && (
Output
{outMods.map((m) => {m} )}
)}
) : (
Modalities unavailable.
)}
Capabilities
{known.length === 0 &&
No capability flags have been observed from a source yet — we do not infer them. }
);
}
/* ------------------------------------------------------------------------------------------------------ benchmarks */
function configSummary(c: Record): string {
const parts = Object.entries(c ?? {})
.filter(([, v]) => v !== null && v !== undefined && v !== '')
.slice(0, 4)
.map(([k, v]) => `${k}=${typeof v === 'object' ? JSON.stringify(v) : String(v)}`);
return parts.join(' · ');
}
/** Results table. `perspective="model"` shows the benchmark column; `"benchmark"` shows rank + model. */
export function ResultsTable({ results, perspective }: { results: BenchmarkResult[]; perspective: 'model' | 'benchmark' }) {
if (!results.length) return Results appear when a tier 1–3 source publishes them; we never copy scores without a source. ;
return (
<>
{perspective === 'benchmark' && # }
{perspective === 'model' ? 'Benchmark' : 'Model'}
Score
Metric
Config
Evaluated
Source
{results.map((r, i) => {
const target = perspective === 'model' ? r.benchmark : r.model;
return (
{perspective === 'benchmark' && {i + 1} }
{perspective === 'benchmark' && r.model.organization && {r.model.organization.name} }
{fmtScore(r.score)}
{r.unit && r.unit !== '%' ? {r.unit} : r.unit === '%' ? '%' : ''}
{r.metric ?? '—'}{r.higher_is_better === false && (lower is better) }
{configSummary(r.config) || '—'}
{fmtDate(r.evaluated_at)}
);
})}
Scores are reported as published, with their evaluation configuration (harness, prompting, judge). Results with different configs are not directly comparable — see methodology.
>
);
}
/* ------------------------------------------------------------------------------------------------------ prices */
export function PricesTable({ prices, perspective }: { prices: Price[]; perspective: 'model' | 'provider' }) {
if (!prices.length) return Prices appear when a provider publishes a public pricing page we crawl. ;
const sorted = [...prices].sort((a, b) => (num(a.input_per_mtok) ?? Infinity) - (num(b.input_per_mtok) ?? Infinity));
return (
<>
{perspective === 'model' ? 'Provider' : 'Model'}
Input / 1M
Output / 1M
Cached in
Batch in / out
Context
Observed
Source
{sorted.map((p) => {
const target = perspective === 'model' ? p.provider : p.model;
return (
{p.provider_model_id && {p.provider_model_id} }
{perspective === 'provider' && p.model.organization && {p.model.organization.name} }
{fmtUsdPerM(p.input_per_mtok)}
{fmtUsdPerM(p.output_per_mtok)}
{fmtUsdPerM(p.cached_input_per_mtok)}
{num(p.batch_input_per_mtok) === null && num(p.batch_output_per_mtok) === null ? '—' : `${fmtUsdPerM(p.batch_input_per_mtok)} / ${fmtUsdPerM(p.batch_output_per_mtok)}`}
{num(p.context_length) === null ? '—' : fmtTokens(p.context_length)}
{fmtAgo(p.observed_at)}
);
})}
USD per 1M tokens as published by each provider ({sorted[0]?.currency ?? 'USD'}). Rows are append-only: every change is kept in the history below.
>
);
}
/** Price history: step lines per provider (input price) + output as second chart when > 1 point. */
export function PriceHistory({ history, perspective = 'model' }: { history: Price[]; perspective?: 'model' | 'provider' }) {
if (!history.length) return null;
const byKey = new Map();
for (const p of history) {
const k = perspective === 'model' ? p.provider.name : p.model.name;
const arr = byKey.get(k);
if (arr) arr.push(p);
else byKey.set(k, [p]);
}
const build = (field: 'input_per_mtok' | 'output_per_mtok'): Series[] =>
[...byKey.entries()].slice(0, 8).map(([name, rows]) => ({
name,
points: stepPoints([...rows.sort((a, b) => a.valid_from.localeCompare(b.valid_from)).map((r) => ({ at: r.valid_from, value: num(r[field]) })), ...(rows.every((r) => r.valid_to) ? [] : [{ at: new Date().toISOString(), value: num([...rows].sort((a, b) => b.valid_from.localeCompare(a.valid_from))[0]?.[field]) }])]),
}));
const inSeries = build('input_per_mtok').filter((s) => s.points.length > 0);
const totalPoints = inSeries.reduce((n, s) => n + s.points.length, 0);
if (totalPoints < 2) return Price history starts with the first observation — no changes recorded yet ({history.length} row{history.length === 1 ? '' : 's'}). ;
const outSeries = build('output_per_mtok').filter((s) => s.points.length > 0);
return (
Input price · USD / 1M tokens
fmtUsdPerM(v)} yDomain={[0, Math.max(...inSeries.flatMap((s) => s.points.map((p) => p.y))) * 1.15 || 1]} />
Output price · USD / 1M tokens
fmtUsdPerM(v)} yDomain={[0, Math.max(...outSeries.flatMap((s) => s.points.map((p) => p.y))) * 1.15 || 1]} />
);
}
/** Tiny per-row sparkline of input price for a model across its history. */
export function PriceSpark({ history, provider }: { history: Price[]; provider: string }) {
const vals = history.filter((p) => p.provider.slug === provider).sort((a, b) => a.valid_from.localeCompare(b.valid_from)).map((p) => num(p.input_per_mtok)).filter((v): v is number => v !== null);
return ;
}
/* ------------------------------------------------------------------------------------------------------ hardware fit */
export function HardwareFitTable({ rows }: { rows: HardwareFitRow[] }) {
if (!rows.length) return Estimates need a parameter count; this model has none recorded from a source. ;
return (
<>
Memory need = bytes per parameter (4-bit ≈ 0.5 × 1.15 overhead, 8-bit 1.0, fp16 2.0) + a KV-cache allowance. Not a measurement.
Hardware
Quantization
Memory
Est. need
Fits
{rows.map((r, i) => (
{r.quantization}
{fmtGb(r.hardware.attributes?.memory_gb as never)}
{fmtGb(r.estimated_memory_gb, 1)}
{r.fits ? 'Yes' : 'No'}
))}
>
);
}
/* ------------------------------------------------------------------------------------------------------ lineage & relations */
export function LineageBlock({ d }: { d: EntityDetail }) {
const l = d.lineage;
if (!l || (!l.ancestors.length && !l.descendants.length && !l.quantizations.length)) return Lineage comes from explicit `derived_from`, `fine_tuned_from`, `distilled_from` and `quantized_from` relations stated by sources. ;
const Col = ({ title, items, hint }: { title: string; items: EntitySummary[]; hint: string }) => (
{title} {items.length}
{items.length ? (
{items.map((e) => (
{num(e.attributes?.parameter_count) !== null ? fmtParams(e.attributes.parameter_count) : e.organization?.name ?? ''}
))}
) : (
{hint}
)}
);
return (
This model
{d.name}
{num(d.attributes?.parameter_count) !== null ? `${fmtParams(d.attributes.parameter_count)} params` : ''}
);
}
export function RelationsBlock({ relations, exclude = [] }: { relations: RelationGroup[]; exclude?: string[] }) {
const groups = relations.filter((g) => g.items.length && !exclude.includes(g.predicate));
if (!groups.length) return No relations recorded.
;
return (
{groups.map((g) => (
{predicateLabel(g.predicate, g.direction)}
{g.total > 8 && ({fmtInt(g.total)} total) }
))}
);
}
/* ------------------------------------------------------------------------------------------------------ lists */
export function EntityList({ items, empty = 'Nothing recorded yet.', showType = false }: { items: EntitySummary[]; empty?: string; showType?: boolean }) {
if (!items.length) return {empty}
;
return (
);
}
/** Dense models table for company / provider / hardware pages. */
export function ModelsTable({ items, total, moreHref }: { items: EntitySummary[]; total?: number; moreHref?: string }) {
if (!items.length) return ;
return (
<>
Model
Params
Context
Openness
Released
Status
Quality
{items.map((m) => {
const a = m.attributes ?? {};
return (
{num(a.parameter_count) === null ? '—' : fmtParams(a.parameter_count)}
{num(a.context_length) === null ? '—' : fmtTokens(a.context_length)}
{typeof a.openness === 'string' ? fmtValue(a.openness) : '—'}
{typeof a.release_date === 'string' ? fmtDate(a.release_date) : '—'}
{m.status && m.status !== 'unknown' ? m.status : '—'}
);
})}
{total !== undefined && total > items.length && moreHref && (
All {fmtInt(total)} models →
)}
>
);
}
/* ------------------------------------------------------------------------------------------------------ timeline & sources */
export function TimelineList({ events, slug }: { events: ChangeEvent[]; slug?: string }) {
if (!events.length) return Events are generated by the change engine when a material property, price or result changes. ;
return (
<>
{slug && (
Full timeline →
)}
>
);
}
export function SourcesTable({ sources }: { sources: SourceRef[] }) {
if (!sources.length) return ;
const sorted = [...sources].sort((a, b) => (a.tier ?? 9) - (b.tier ?? 9) || (b.last_observed_at ?? '').localeCompare(a.last_observed_at ?? ''));
return (
<>
Source
Document
Type
Tier
Last observed
Snapshots
{sorted.map((s) => (
{s.source_name ?? s.domain ?? '—'}
{s.url.replace(/^https?:\/\/(www\.)?/, '')}
{s.doc_type}
{fmtAgo(s.last_observed_at)}
{fmtInt(s.snapshots)}
))}
Tier 1 = official/primary, 2 = quality secondary, 3 = community, 4 = unverified. Every snapshot is archived; see all sources and the methodology.
>
);
}
/** Summary of provenance across all attributes: sources count, tiers distribution, freshest observation. */
export function ProvenanceSummary({ provenance, quality }: { provenance: Provenance; quality: EntityDetail['quality'] }) {
const entries = Object.values(provenance ?? {});
const tiers = [1, 2, 3, 4].map((t) => ({ t, n: entries.filter((e) => e.tier === t).length })).filter((x) => x.n);
const newest = entries.map((e) => e.observed_at).sort().at(-1);
const conflicts = num(quality?.conflicts) ?? 0;
return (
Attributed facts
{fmtInt(entries.length)}
Source tiers
{tiers.length ? tiers.map((x) => ) : — }{tiers.length > 0 && {tiers.map((x) => x.n).join(' / ')} }
Freshest observation
{newest ? fmtAgo(newest) : '—'}
Conflicts
{conflicts ? `${conflicts} flagged` : 'None'}
);
}
export function typeTitle(e: { entity_type: string }): string {
return typeLabel(e.entity_type);
}
export { routes };