1import Link from 'next/link';2import { DataTable, Td, Th } from '@/components/ui/data-table';3import { KeyValue, type KVRow } from '@/components/ui/key-value';4import { SourceCell } from '@/components/ui/provenance';5import { Note } from '@/components/ui/section';6import { EmptyState, Unavailable } from '@/components/ui/unavailable';7import { cn } from '@/lib/cn';8import { fmtDate, fmtDateTime, fmtInt, fmtValue } from '@/lib/format';9import { PROSE_KEYS, propertyLabel, routes } from '@/lib/site';10import type { AsOfPayload, Claim, EntityDetail } from '@/lib/types';11import { AsOfPicker } from './asof-picker';1213/* Canonical order for model properties (mirrors SpecTable in blocks.tsx; kept local to avoid touching that file). */14const 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'];15const HIDDEN = new Set(['name', 'slug', 'id', 'entity_type']);16const ORDER = new Map(MODEL_ORDER.map((k, i) => [k, i]));17const byCanonical = (a: string, b: string) => (ORDER.get(a) ?? 999) - (ORDER.get(b) ?? 999) || a.localeCompare(b);1819const STATUS_CLS: Record<string, string> = {20 current: 'text-positive bg-positive-soft',21 superseded: 'text-ink-3 bg-surface-2',22 conflicting: 'text-danger bg-danger-soft',23 retracted: 'text-warning bg-warning-soft',24};25function ClaimStatus({ status }: { status: string }) {26 return <span className={cn('inline-flex items-center whitespace-nowrap rounded-[3px] px-1.5 py-[1px] text-[11px] font-medium leading-4 tracking-wide', STATUS_CLS[status] ?? 'text-ink-2 bg-surface-2')}>{status}</span>;27}2829/* ------------------------------------------------------------------------------------------------------ as of */3031export function AsOfBlock({ d, asof, payload }: { d: EntityDetail; asof: string; payload: AsOfPayload | null }) {32 const back = `${routes.entity(d)}?tab=history`;33 if (!payload) {34 return (35 <div className="hairline pt-4">36 <Unavailable what={`State as of ${asof}`} reason="Could not load the state for that date — check the date (YYYY-MM-DD, not in the future)." />37 <p className="mt-2 text-sm"><Link href={back} className="link">Back to today →</Link></p>38 </div>39 );40 }41 const attrs = payload.attributes ?? {};42 const keys = Object.keys(attrs).filter((k) => !HIDDEN.has(k) && !PROSE_KEYS.has(k) && attrs[k] !== null && attrs[k] !== undefined && attrs[k] !== '' && !(Array.isArray(attrs[k]) && (attrs[k] as unknown[]).length === 0));43 keys.sort(byCanonical);44 const rows: KVRow[] = keys.map((k) => ({ key: k, raw: attrs[k] }));45 return (46 <div className="hairline pt-4">47 <div className="flex flex-wrap items-center justify-between gap-2 bg-accent-soft px-3 py-2.5 text-sm">48 <p className="text-ink">49 <span className="font-medium">Viewing AI Atlas as of {fmtDate(payload.date)}</span>50 <span className="text-ink-2"> — attributes exactly as the atlas knew them on that day; later corrections are not shown.</span>51 </p>52 <Link href={back} className="link shrink-0 py-2">Back to today →</Link>53 </div>54 {!payload.existed ? (55 <EmptyState title={`${d.name} was not yet in AI Atlas on ${fmtDate(payload.date)}`} className="mt-4">56 First seen {fmtDate(payload.first_seen_at)}. Nothing is inferred backwards: no attribute is shown for dates before the first observation.57 </EmptyState>58 ) : (59 <div className="mt-4">60 <p className="eyebrow mb-2">61 Attributes as of {fmtDate(payload.date)} <span className="tnum text-ink-3">{fmtInt(payload.claims?.length ?? 0)} claims in force</span>62 </p>63 <KeyValue rows={rows} />64 </div>65 )}66 </div>67 );68}6970/* ------------------------------------------------------------------------------------------------------ claim history */7172export function ClaimHistory({ d, claims, property }: { d: EntityDetail; claims: Claim[] | null; property?: string }) {73 const base = `${routes.entity(d)}?tab=history`;74 if (!claims) return <Unavailable what="Claim history" />;75 if (!claims.length)76 return (77 <EmptyState title={property ? `No claims recorded for ${propertyLabel(property)}` : 'No claims recorded yet'}>78 {property ? <Link href={base} className="link">Show all properties →</Link> : 'Claims appear when a source states a fact; every later change is kept as a new claim.'}79 </EmptyState>80 );81 const groups = new Map<string, Claim[]>();82 for (const c of claims) {83 const arr = groups.get(c.property);84 if (arr) arr.push(c);85 else groups.set(c.property, [c]);86 }87 const keys = [...groups.keys()].sort(byCanonical);88 const conflicting = claims.filter((c) => c.status === 'conflicting').length;89 return (90 <div className="space-y-8">91 <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-ink-3">92 <span className="tnum">{fmtInt(claims.length)} claims · {fmtInt(keys.length)} properties</span>93 {conflicting > 0 && <span className="font-medium text-danger">{fmtInt(conflicting)} conflicting</span>}94 {property && (95 <Link href={base} className="link">96 Show all properties97 </Link>98 )}99 </div>100 {keys.map((k) => {101 const rows = groups.get(k) ?? [];102 return (103 <section key={k} id={`h-${k}`} className="scroll-mt-20">104 <h3 className="mb-2 flex flex-wrap items-baseline gap-x-2">105 <Link href={`${base}&property=${encodeURIComponent(k)}`} className="text-sm font-semibold text-ink hover:text-accent" aria-current={property === k ? 'true' : undefined}>106 {propertyLabel(k)}107 </Link>108 <span className="mono text-[11px] text-ink-3">{k}</span>109 <span className="tnum text-xs text-ink-3">{fmtInt(rows.length)}</span>110 {rows.some((c) => c.status === 'conflicting') && <span className="text-xs font-medium text-danger">conflicting claims</span>}111 </h3>112 <DataTable compact caption={`Claim history for ${propertyLabel(k)}`}>113 <thead>114 <tr>115 <Th>Value</Th>116 <Th>Valid from → to</Th>117 <Th>Status</Th>118 <Th>Source</Th>119 <Th>Confidence</Th>120 <Th>Extractor</Th>121 </tr>122 </thead>123 <tbody>124 {rows.map((c) => (125 <tr key={c.id} className={c.status === 'conflicting' ? 'border-l-2 border-danger' : undefined}>126 <Td primary className={c.status === 'conflicting' ? 'pl-2 md:pl-2' : undefined}>127 <span className="tnum">{fmtValue(c.value, c.property)}</span>128 {c.unit && !/parameter_count|context_length|max_output_tokens|memory_gb|file_size_gb/.test(c.property) && <span className="text-xs text-ink-3"> {c.unit}</span>}129 </Td>130 <Td label="Valid" className="tnum text-xs text-ink-2">131 <time dateTime={c.valid_from}>{fmtDateTime(c.valid_from)}</time>132 <span className="text-ink-3"> → </span>133 {c.valid_to ? <time dateTime={c.valid_to}>{fmtDateTime(c.valid_to)}</time> : <span className="font-medium text-positive">current</span>}134 </Td>135 <Td label="Status"><ClaimStatus status={c.status} /></Td>136 <Td label="Source"><SourceCell url={c.source_url} tier={c.tier} name={c.source_name} /></Td>137 <Td label="Confidence" className={cn('text-xs', c.confidence === 'conflicted' && 'text-danger', c.confidence === 'low' && 'text-warning', !['conflicted', 'low'].includes(c.confidence) && 'text-ink-2')}>{c.confidence}</Td>138 <Td label="Extractor" className="mono text-xs text-ink-3">{c.extractor}</Td>139 </tr>140 ))}141 </tbody>142 </DataTable>143 </section>144 );145 })}146 <Note>147 Claims are temporal and append-only: a new observation closes the previous claim (<span className="mono">valid_to</span>) instead of overwriting it. Conflicting claims from different sources are kept side by side and flagged — never averaged. <Link href="/methodology" className="link">Methodology →</Link>148 </Note>149 </div>150 );151}152153/* ------------------------------------------------------------------------------------------------------ panel */154155export function HistoryPanel({ d, asof, asofPayload, claims, property }: { d: EntityDetail; asof?: string; asofPayload: AsOfPayload | null; claims: Claim[] | null; property?: string }) {156 return (157 <div className="space-y-10">158 <section>159 <p className="eyebrow mb-2">As of</p>160 <p className="mb-3 max-w-2xl text-sm text-ink-2">Rewind the record: see this entity's attributes exactly as AI Atlas knew them on a given day.</p>161 <AsOfPicker value={asof} />162 {asof && <div className="mt-4"><AsOfBlock d={d} asof={asof} payload={asofPayload} /></div>}163 </section>164 <section>165 <p className="eyebrow mb-2">Claim history{property ? <> · {propertyLabel(property)}</> : null}</p>166 <ClaimHistory d={d} claims={claims} property={property} />167 </section>168 </div>169 );170}171