1import { ExternalLink, GitFork, MemoryStick } from 'lucide-react';2import Link from 'next/link';3import { CompareButton } from '@/components/compare/compare-button';4import { ViewBeacon } from '@/components/layout/view-beacon';5import { EntityBadge, OpennessBadge, StatusBadge } from '@/components/ui/badges';6import { EntityLink, QualityMark } from '@/components/ui/entity';7import { Container, Note } from '@/components/ui/section';8import { TabPanel, Tabs, type TabDef } from '@/components/ui/tabs';9import { api, safe } from '@/lib/api';10import { fmtAgo, fmtDate, fmtInt, fmtParams, fmtTokens, num } from '@/lib/format';11import { routes, SITE_NAME, SITE_URL, typeLabel } from '@/lib/site';12import type { EntityDetail, EntitySummary } from '@/lib/types';13import { Capabilities, EntityList, HardwareFitTable, Identity, LineageBlock, ModelsTable, PriceHistory, PricesTable, ProvenanceSummary, RelationsBlock, ResultsTable, SourcesTable, SpecTable, TimelineList } from './blocks';14import { HistoryPanel } from './history';1516/** Search params an entity page understands (History tab). */17export type EntityPageParams = { asof?: string; property?: string };18const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/;1920/* ---------------------------------------------------------------------------------------------------------- header chips */2122function headerChips(d: EntityDetail): { label: string; value: string; key: string }[] {23 const a = d.attributes ?? {};24 const out: { label: string; value: string; key: string }[] = [];25 const add = (key: string, label: string, value: string | null) => value && out.push({ key, label, value });26 switch (d.entity_type) {27 case 'model':28 case 'quantization': {29 const p = num(a.parameter_count);30 const ap = num(a.active_parameter_count);31 add('parameter_count', 'Parameters', p === null ? null : ap !== null && ap !== p ? `${fmtParams(p)} · ${fmtParams(ap)} active` : fmtParams(p));32 add('context_length', 'Context', num(a.context_length) === null ? null : `${fmtTokens(a.context_length)} tokens`);33 add('release_date', 'Released', typeof a.release_date === 'string' ? fmtDate(a.release_date) : null);34 add('license', 'License', typeof a.license === 'string' ? a.license : null);35 add('knowledge_cutoff', 'Knowledge cutoff', typeof a.knowledge_cutoff === 'string' ? fmtDate(a.knowledge_cutoff) : null);36 break;37 }38 case 'company':39 case 'organization':40 case 'lab':41 case 'university':42 add('country', 'Country', typeof a.country === 'string' ? a.country : null);43 add('founded', 'Founded', a.founded ? String(a.founded).slice(0, 4) : null);44 add('headquarters', 'HQ', typeof a.headquarters === 'string' ? a.headquarters : null);45 add('org_kind', 'Kind', typeof a.org_kind === 'string' ? a.org_kind : null);46 add('employee_count', 'Employees', num(a.employee_count) === null ? null : fmtInt(a.employee_count));47 break;48 case 'paper':49 add('published_at', 'Published', typeof a.published_at === 'string' ? fmtDate(a.published_at) : null);50 add('venue', 'Venue', typeof a.venue === 'string' ? a.venue : null);51 add('arxiv_id', 'arXiv', typeof a.arxiv_id === 'string' ? a.arxiv_id : null);52 add('primary_category', 'Category', typeof a.primary_category === 'string' ? a.primary_category : null);53 break;54 case 'hardware':55 add('kind', 'Kind', typeof a.kind === 'string' ? a.kind : null);56 add('memory_gb', 'Memory', num(a.memory_gb) === null ? null : `${fmtInt(a.memory_gb)} GB ${typeof a.memory_type === 'string' ? a.memory_type : ''}`.trim());57 add('memory_bandwidth_gbs', 'Bandwidth', num(a.memory_bandwidth_gbs) === null ? null : `${fmtInt(a.memory_bandwidth_gbs)} GB/s`);58 add('tdp_watts', 'TDP', num(a.tdp_watts) === null ? null : `${fmtInt(a.tdp_watts)} W`);59 add('release_date', 'Released', typeof a.release_date === 'string' ? fmtDate(a.release_date) : null);60 break;61 case 'benchmark':62 add('category', 'Category', typeof a.category === 'string' ? a.category : null);63 add('task', 'Task', typeof a.task === 'string' ? a.task : null);64 add('metric', 'Metric', typeof a.metric === 'string' ? a.metric : null);65 add('creator', 'Creator', typeof a.creator === 'string' ? a.creator : null);66 break;67 case 'framework':68 case 'library':69 case 'runtime':70 case 'repository':71 add('latest_version', 'Version', typeof a.latest_version === 'string' ? a.latest_version : null);72 add('latest_release_at', 'Released', typeof a.latest_release_at === 'string' ? fmtDate(a.latest_release_at) : null);73 add('language', 'Language', typeof a.language === 'string' ? a.language : null);74 add('license', 'License', typeof a.license === 'string' ? a.license : null);75 add('metric.stars', 'Stars', num(a['metric.stars']) === null ? null : fmtInt(a['metric.stars']));76 break;77 case 'dataset':78 add('modality', 'Modality', typeof a.modality === 'string' ? a.modality : null);79 add('size', 'Size', a.size ? String(a.size) : null);80 add('license', 'License', typeof a.license === 'string' ? a.license : null);81 add('publisher', 'Publisher', typeof a.publisher === 'string' ? a.publisher : null);82 break;83 case 'provider':84 add('regions', 'Regions', Array.isArray(a.regions) && a.regions.length ? `${a.regions.length}` : null);85 break;86 default:87 break;88 }89 return out.slice(0, 5);90}9192function primaryUrl(d: EntityDetail): string | null {93 const a = d.attributes ?? {};94 for (const k of ['official_url', 'website', 'model_card_url', 'pdf_url', 'repository_url', 'spec_url', 'docs_url']) {95 const v = a[k];96 if (typeof v === 'string' && /^https?:\/\//.test(v)) return v;97 }98 return null;99}100101/* ---------------------------------------------------------------------------------------------------------- tabs per type */102103type TabKey = 'overview' | 'capabilities' | 'benchmarks' | 'providers' | 'hardware' | 'lineage' | 'research' | 'models' | 'repositories' | 'leaderboard' | 'runnable' | 'relations' | 'history' | 'timeline' | 'sources';104105function tabsFor(d: EntityDetail, claimCount?: number | null): TabDef[] {106 const t = d.entity_type;107 const c = (n: number | undefined | null) => (n ? n : undefined);108 const rel = d.relations?.reduce((n, g) => n + g.items.length, 0) ?? 0;109 const sources = d.sources?.length ?? 0;110 const timeline = d.timeline?.length ?? 0;111 const tail: TabDef[] = [112 { id: 'history', label: 'History', count: c(claimCount ?? d.counts?.claims) },113 { id: 'timeline', label: 'Timeline', count: c(timeline) },114 { id: 'sources', label: 'Sources', count: c(sources) },115 ];116 if (t === 'model' || t === 'quantization')117 return [118 { id: 'overview', label: 'Overview' },119 { id: 'capabilities', label: 'Capabilities' },120 { id: 'benchmarks', label: 'Benchmarks', count: c(d.results?.length) },121 { id: 'providers', label: 'Providers & Pricing', count: c(d.prices?.length) },122 { id: 'hardware', label: 'Hardware', count: c(d.hardware_fit?.length) },123 { id: 'lineage', label: 'Lineage', count: c((d.lineage?.ancestors.length ?? 0) + (d.lineage?.descendants.length ?? 0) + (d.lineage?.quantizations.length ?? 0)) },124 { id: 'research', label: 'Research', count: c((d.papers?.length ?? 0) + (d.repositories?.length ?? 0)) },125 ...tail,126 ];127 if (['company', 'organization', 'lab', 'university'].includes(t))128 return [129 { id: 'overview', label: 'Overview' },130 { id: 'models', label: 'Models', count: c(d.models?.total ?? d.models?.items.length) },131 { id: 'research', label: 'Research', count: c(d.papers?.length) },132 { id: 'providers', label: 'Providers', count: c(d.relations?.filter((g) => g.predicate === 'available_through' || g.predicate === 'operates').reduce((n, g) => n + g.items.length, 0)) },133 { id: 'repositories', label: 'Repositories', count: c(d.repositories?.length) },134 ...tail,135 ];136 if (t === 'provider')137 return [138 { id: 'overview', label: 'Overview' },139 { id: 'providers', label: 'Models & Pricing', count: c(d.prices?.length ?? d.models?.total) },140 { id: 'relations', label: 'Relations', count: c(rel) },141 ...tail,142 ];143 if (t === 'benchmark')144 return [145 { id: 'overview', label: 'Overview' },146 { id: 'leaderboard', label: 'Leaderboard', count: c(d.results?.length) },147 { id: 'relations', label: 'Relations', count: c(rel) },148 ...tail,149 ];150 if (t === 'hardware')151 return [152 { id: 'overview', label: 'Overview' },153 { id: 'runnable', label: 'Runnable models', count: c(d.models?.total ?? d.models?.items.length) },154 { id: 'relations', label: 'Relations', count: c(rel) },155 ...tail,156 ];157 if (t === 'paper')158 return [159 { id: 'overview', label: 'Overview' },160 { id: 'models', label: 'Related models', count: c(d.relations?.filter((g) => g.items.some((i) => i.entity_type === 'model')).reduce((n, g) => n + g.items.filter((i) => i.entity_type === 'model').length, 0)) },161 { id: 'relations', label: 'Relations', count: c(rel) },162 ...tail,163 ];164 return [{ id: 'overview', label: 'Overview' }, { id: 'relations', label: 'Relations', count: c(rel) }, ...tail];165}166167/* ---------------------------------------------------------------------------------------------------------- JSON-LD */168169function jsonLd(d: EntityDetail, canonical: string) {170 const a = d.attributes ?? {};171 const url = `${SITE_URL}${canonical}`;172 const org = d.organization ? { '@type': 'Organization', name: d.organization.name, url: `${SITE_URL}${routes.entity({ entity_type: 'company', slug: d.organization.slug })}` } : undefined;173 const base: Record<string, unknown> = { '@context': 'https://schema.org', name: d.name, url, description: d.description ?? undefined, alternateName: d.aliases?.length ? d.aliases : undefined, identifier: d.identifiers?.map((i) => ({ '@type': 'PropertyValue', propertyID: i.scheme, value: i.value })) };174 switch (d.entity_type) {175 case 'model':176 case 'quantization':177 return { ...base, '@type': ['SoftwareApplication', 'Product'], applicationCategory: 'AI model', creator: org, manufacturer: org, datePublished: typeof a.release_date === 'string' ? a.release_date : undefined, license: typeof a.license === 'string' ? a.license : undefined, offers: d.prices?.length ? d.prices.slice(0, 8).map((p) => ({ '@type': 'Offer', seller: { '@type': 'Organization', name: p.provider.name }, price: num(p.input_per_mtok) ?? undefined, priceCurrency: p.currency || 'USD', description: 'Input price per 1M tokens' })) : undefined };178 case 'company':179 case 'organization':180 case 'lab':181 case 'university':182 return { ...base, '@type': 'Organization', foundingDate: a.founded ? String(a.founded) : undefined, sameAs: typeof a.website === 'string' ? [a.website] : undefined, location: typeof a.headquarters === 'string' ? a.headquarters : undefined };183 case 'paper':184 return { ...base, '@type': 'ScholarlyArticle', headline: d.name, datePublished: typeof a.published_at === 'string' ? a.published_at : undefined, author: Array.isArray(a.authors) ? (a.authors as unknown[]).slice(0, 30).map((n) => ({ '@type': 'Person', name: String(n) })) : undefined, sameAs: [a.pdf_url, a.arxiv_id ? `https://arxiv.org/abs/${a.arxiv_id}` : null].filter(Boolean), abstract: typeof a.abstract === 'string' ? a.abstract : undefined };185 case 'hardware':186 return { ...base, '@type': 'Product', manufacturer: typeof a.manufacturer === 'string' ? { '@type': 'Organization', name: a.manufacturer } : org, category: typeof a.kind === 'string' ? a.kind : undefined };187 case 'framework':188 case 'library':189 case 'runtime':190 case 'repository':191 case 'tool':192 return { ...base, '@type': 'SoftwareSourceCode', codeRepository: typeof a.repository_url === 'string' ? a.repository_url : undefined, programmingLanguage: typeof a.language === 'string' ? a.language : undefined, license: typeof a.license === 'string' ? a.license : undefined, author: org };193 case 'dataset':194 return { ...base, '@type': 'Dataset', license: typeof a.license === 'string' ? a.license : undefined, creator: org };195 default:196 return { ...base, '@type': 'Thing' };197 }198}199200/* ---------------------------------------------------------------------------------------------------------- page */201202/**203 * Shared entity page for every type. Header (badges, name, org, description, key chips) + URL-driven tabs whose set204 * depends on the type. Panels are all server-rendered (SEO); the client Tabs only toggles visibility.205 */206export async function EntityPage({ d, canonical, related, asof: asofRaw, historyProperty }: { d: EntityDetail; canonical: string; related?: EntitySummary[] | null; asof?: string; historyProperty?: string }) {207 const a = d.attributes ?? {};208 const chips = headerChips(d);209 const openness = typeof a.openness === 'string' ? a.openness : null;210 const link = primaryUrl(d);211 // History tab data: full claim history always (it is the tab's content); the "as of" state only when requested.212 const asof = asofRaw && ISO_DAY.test(asofRaw) ? asofRaw : asofRaw ? 'invalid' : undefined;213 const property = historyProperty?.trim() || undefined;214 const [history, asofPayload] = await Promise.all([safe(api.entityHistory(d.slug, property)), asof && asof !== 'invalid' ? safe(api.entityAsOf(d.slug, asof)) : Promise.resolve(null)]);215 const claims = history?.items ?? null;216 const tabs = tabsFor(d, property ? null : claims?.length);217 const memoryGb = d.entity_type === 'hardware' ? num(a.memory_gb) : null;218 const isModel = d.entity_type === 'model' || d.entity_type === 'quantization';219 const isCompany = ['company', 'organization', 'lab', 'university'].includes(d.entity_type);220 const ld = jsonLd(d, canonical);221 const prose = typeof a.abstract === 'string' ? a.abstract : null;222 const modelRelations = d.relations?.filter((g) => g.items.some((i) => i.entity_type === 'model')).flatMap((g) => g.items.filter((i) => i.entity_type === 'model')) ?? [];223224 return (225 <Container wide>226 <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }} />227 <ViewBeacon path={canonical} />228229 <nav aria-label="Breadcrumb" className="pt-5 text-xs text-ink-3">230 <ol className="flex flex-wrap items-center gap-1.5">231 <li><Link href="/" className="hover:text-ink">AI Atlas</Link></li>232 <li aria-hidden>/</li>233 <li><Link href={routes.listing(d.entity_type)} className="hover:text-ink">{typeLabel(d.entity_type, true)}</Link></li>234 {d.organization && isModel && (235 <>236 <li aria-hidden>/</li>237 <li><Link href={routes.entity({ entity_type: 'company', slug: d.organization.slug })} className="hover:text-ink">{d.organization.name}</Link></li>238 </>239 )}240 <li aria-hidden>/</li>241 <li className="text-ink-2">{d.name}</li>242 </ol>243 </nav>244245 <header className="pb-6 pt-4 md:pb-8 md:pt-5">246 <div className="flex flex-wrap items-center gap-2">247 <EntityBadge type={d.entity_type} />248 <StatusBadge status={d.status} />249 {openness && <OpennessBadge openness={openness} />}250 {typeof a.family === 'string' && <span className="text-xs text-ink-3">family · {a.family}</span>}251 </div>252 <div className="mt-3 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">253 <div className="min-w-0">254 <h1 className="display text-[30px] md:text-[44px]">{d.name}</h1>255 <p className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[15px] text-ink-2">256 {d.organization && (257 <Link href={routes.entity({ entity_type: 'company', slug: d.organization.slug })} className="font-medium text-ink hover:text-accent">258 {d.organization.name}259 </Link>260 )}261 {link && (262 <a href={link} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-ink-3 hover:text-accent">263 {link.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '').slice(0, 48)} <ExternalLink className="size-3.5" aria-hidden />264 </a>265 )}266 </p>267 {d.description && <p className="mt-3 max-w-3xl text-[15px] leading-relaxed text-ink-2">{d.description}</p>}268 <div className="mt-4 flex flex-wrap items-center gap-2" aria-label="Actions">269 <CompareButton e={d} />270 <Link href={routes.graph(d.slug)} className="inline-flex h-9 items-center gap-1.5 border border-rule px-2.5 text-sm text-ink-2 hover:border-rule-strong hover:text-ink">271 <GitFork className="size-3.5" aria-hidden /> Explore graph272 </Link>273 {memoryGb !== null && (274 <Link href={routes.hardwareFit({ memory_gb: memoryGb, quant: '4bit', context: 8192 })} className="inline-flex h-9 items-center gap-1.5 border border-rule px-2.5 text-sm text-ink-2 hover:border-rule-strong hover:text-ink">275 <MemoryStick className="size-3.5" aria-hidden /> What can this run?276 </Link>277 )}278 </div>279 </div>280 <div className="shrink-0 text-xs text-ink-3 lg:text-right">281 <QualityMark q={d.quality?.score} label />282 <p className="mt-1" title={d.updated_at}>Updated {fmtAgo(d.updated_at)} · first seen {fmtDate(d.first_seen_at)}</p>283 <p className="mono mt-0.5 text-[11px]">{d.id}</p>284 </div>285 </div>286 {chips.length > 0 && (287 <dl className="mt-5 grid grid-cols-2 gap-x-6 gap-y-3 border-y border-rule py-3 sm:grid-cols-3 lg:grid-cols-5">288 {chips.map((c) => {289 const p = d.provenance?.[c.key];290 return (291 <div key={c.key} className="min-w-0">292 <dt className="eyebrow">{c.label}</dt>293 <dd className="tnum mt-0.5 truncate text-[15px] font-medium text-ink" title={c.value}>{c.value}</dd>294 {p && <dd className="truncate text-[11px] text-ink-3" title={`${p.source_name ?? p.url ?? ''} · tier ${p.tier} · observed ${fmtAgo(p.observed_at)}`}>T{p.tier} · {fmtAgo(p.observed_at)}</dd>}295 </div>296 );297 })}298 </dl>299 )}300 </header>301302 <Tabs tabs={tabs} className="pb-10">303 {/* ------------------------------------------------------------------------------------------ Overview */}304 <TabPanel id="overview">305 <div className="grid gap-10 lg:grid-cols-[minmax(0,1fr)_22rem]">306 <div className="min-w-0 space-y-8">307 {prose && (308 <section>309 <p className="eyebrow mb-2">Abstract</p>310 <p className="prose-atlas max-w-3xl text-[15px] leading-relaxed text-ink-2">{prose}</p>311 </section>312 )}313 {d.entity_type === 'paper' && Array.isArray(a.authors) && (a.authors as unknown[]).length > 0 && (314 <section>315 <p className="eyebrow mb-2">Authors <span className="tnum text-ink-3">{(a.authors as unknown[]).length}</span></p>316 <p className="text-sm text-ink-2">{(a.authors as unknown[]).map(String).join(', ')}</p>317 </section>318 )}319 <section>320 <p className="eyebrow mb-2">Specification</p>321 <SpecTable d={d} exclude={isModel ? [...['tool_calling', 'structured_output', 'reasoning', 'vision', 'audio', 'fine_tuning_available'], 'authors'] : ['authors']} />322 <Note className="mt-3">323 Each value shows its source, tier and observation time. Conflicting claims are kept side by side and flagged — never averaged. <Link href="/methodology" className="link">How AI Atlas records facts →</Link>324 </Note>325 </section>326 <section>327 <p className="eyebrow mb-2">Provenance</p>328 <ProvenanceSummary provenance={d.provenance} quality={d.quality} />329 </section>330 </div>331 <aside className="min-w-0 space-y-8">332 <section>333 <p className="eyebrow mb-2">Relations</p>334 <RelationsBlock relations={d.relations ?? []} />335 </section>336 <section>337 <p className="eyebrow mb-2">Identity</p>338 <Identity d={d} />339 <p className="mono mt-2 break-all text-[11px] text-ink-3">slug {d.slug}</p>340 </section>341 {related && related.length > 0 && (342 <section>343 <p className="eyebrow mb-2">Related</p>344 <ul className="divide-y divide-rule border-y border-rule">345 {related.slice(0, 8).map((r) => (346 <li key={r.id} className="flex items-center gap-2 py-2 text-sm">347 <EntityBadge type={r.entity_type} small />348 <EntityLink e={r} className="truncate" />349 {r.organization && <span className="ml-auto shrink-0 text-xs text-ink-3">{r.organization.name}</span>}350 </li>351 ))}352 </ul>353 </section>354 )}355 <section>356 <p className="eyebrow mb-2">Compare</p>357 <div className="flex flex-wrap items-center gap-3 text-sm">358 <CompareButton e={d} />359 <Link href={routes.compare([d.slug])} className="link py-2">Open compare →</Link>360 </div>361 </section>362 </aside>363 </div>364 </TabPanel>365366 {/* ------------------------------------------------------------------------------------------ Model tabs */}367 {isModel && (368 <TabPanel id="capabilities">369 <Capabilities d={d} />370 </TabPanel>371 )}372 {isModel && (373 <TabPanel id="benchmarks">374 <ResultsTable results={d.results ?? []} perspective="model" />375 </TabPanel>376 )}377 {(isModel || d.entity_type === 'provider') && (378 <TabPanel id="providers">379 <div className="space-y-8">380 {d.entity_type === 'provider' && d.models && d.models.items.length > 0 && !(d.prices?.length) && (381 <section>382 <p className="eyebrow mb-2">Models served</p>383 <ModelsTable items={d.models.items} total={d.models.total} moreHref={`/models?provider=${encodeURIComponent(d.slug)}`} />384 </section>385 )}386 <section>387 <p className="eyebrow mb-2">Current prices</p>388 <PricesTable prices={d.prices ?? []} perspective={isModel ? 'model' : 'provider'} />389 </section>390 {isModel && d.providers && d.providers.length > 0 && (391 <section>392 <p className="eyebrow mb-2">Available through <span className="tnum text-ink-3">{d.providers.length}</span></p>393 <p className="flex flex-wrap gap-x-3 gap-y-1 text-sm">394 {d.providers.map((p) => (395 <EntityLink key={p.id} e={p} />396 ))}397 </p>398 </section>399 )}400 <section>401 <p className="eyebrow mb-2">Price history</p>402 <PriceHistory history={d.price_history ?? d.prices ?? []} perspective={isModel ? 'model' : 'provider'} />403 </section>404 </div>405 </TabPanel>406 )}407 {isModel && (408 <TabPanel id="hardware">409 <HardwareFitTable rows={d.hardware_fit ?? []} />410 </TabPanel>411 )}412 {isModel && (413 <TabPanel id="lineage">414 <LineageBlock d={d} />415 </TabPanel>416 )}417 {(isModel || isCompany) && (418 <TabPanel id="research">419 <div className="grid gap-10 lg:grid-cols-2">420 <section>421 <p className="eyebrow mb-2">Papers <span className="tnum text-ink-3">{d.papers?.length ?? 0}</span></p>422 <EntityList items={d.papers ?? []} empty="No papers linked yet." />423 </section>424 <section>425 <p className="eyebrow mb-2">Repositories <span className="tnum text-ink-3">{d.repositories?.length ?? 0}</span></p>426 <EntityList items={d.repositories ?? []} empty="No repositories linked yet." />427 </section>428 </div>429 </TabPanel>430 )}431432 {/* ------------------------------------------------------------------------------------------ Company tabs */}433 {isCompany && (434 <TabPanel id="models">435 <ModelsTable items={d.models?.items ?? []} total={d.models?.total} moreHref={`/models?org=${encodeURIComponent(d.slug)}`} />436 </TabPanel>437 )}438 {isCompany && (439 <TabPanel id="providers">440 <RelationsBlock relations={(d.relations ?? []).filter((g) => ['available_through', 'operates', 'owns'].includes(g.predicate))} />441 </TabPanel>442 )}443 {isCompany && (444 <TabPanel id="repositories">445 <EntityList items={d.repositories ?? []} empty="No repositories linked yet." />446 </TabPanel>447 )}448449 {/* ------------------------------------------------------------------------------------------ Other types */}450 {d.entity_type === 'benchmark' && (451 <TabPanel id="leaderboard">452 <ResultsTable results={d.results ?? []} perspective="benchmark" />453 </TabPanel>454 )}455 {d.entity_type === 'hardware' && (456 <TabPanel id="runnable">457 <div className="mb-3 flex flex-wrap items-center gap-2">458 <span className="inline-flex items-center border border-dashed border-warning/60 px-1.5 text-[11px] font-medium uppercase tracking-wide text-warning">Estimated</span>459 <Note>Models whose estimated memory footprint fits this device — see <Link href="/methodology#estimates" className="link">estimate method</Link>.</Note>460 </div>461 <ModelsTable items={d.models?.items ?? []} total={d.models?.total} />462 </TabPanel>463 )}464 {d.entity_type === 'paper' && (465 <TabPanel id="models">466 <EntityList items={modelRelations} empty="No models linked to this paper yet." />467 </TabPanel>468 )}469 {!isModel && !isCompany && (470 <TabPanel id="relations">471 <RelationsBlock relations={d.relations ?? []} />472 </TabPanel>473 )}474475 <TabPanel id="history">476 <HistoryPanel d={d} asof={asof === 'invalid' ? asofRaw : asof} asofPayload={asof === 'invalid' ? null : asofPayload} claims={claims} property={property} />477 </TabPanel>478 <TabPanel id="timeline">479 <TimelineList events={d.timeline ?? []} slug={d.slug} />480 </TabPanel>481 <TabPanel id="sources">482 <SourcesTable sources={d.sources ?? []} />483 </TabPanel>484 </Tabs>485 </Container>486 );487}488489export function describeEntity(d: EntityDetail): string {490 const a = d.attributes ?? {};491 const bits: string[] = [];492 const t = typeLabel(d.entity_type).toLowerCase();493 bits.push(`${d.name} is a${/^[aeiou]/.test(t) ? 'n' : ''} ${t}${d.organization ? ` by ${d.organization.name}` : ''}`);494 if (d.entity_type === 'model') {495 if (num(a.parameter_count) !== null) bits.push(`with ${fmtParams(a.parameter_count)} parameters`);496 if (num(a.context_length) !== null) bits.push(`and a ${fmtTokens(a.context_length)}-token context window`);497 if (typeof a.release_date === 'string') bits.push(`released ${fmtDate(a.release_date)}`);498 }499 let s = bits.join(' ') + '.';500 if (d.description) s = `${d.description.slice(0, 160)}${d.description.length > 160 ? '…' : ''} ${s}`;501 s += ` Specifications, prices, benchmarks, lineage, timeline and sources on ${SITE_NAME}.`;502 return s.slice(0, 300);503}504