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