SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
27.7 KB · 506 lines tsx
Raw Blame History
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