import { ExternalLink, GitFork, MemoryStick } from 'lucide-react'; import Link from 'next/link'; import { CompareButton } from '@/components/compare/compare-button'; import { WatchButton } from '@/components/watchlist/watch-button'; import { ViewBeacon } from '@/components/layout/view-beacon'; import { EntityBadge, OpennessBadge, StatusBadge } from '@/components/ui/badges'; import { EntityLink, QualityMark } from '@/components/ui/entity'; import { Container, Note } from '@/components/ui/section'; import { TabPanel, Tabs, type TabDef } from '@/components/ui/tabs'; import { api, safe } from '@/lib/api'; import { fmtAgo, fmtDate, fmtInt, fmtParams, fmtTokens, num } from '@/lib/format'; import { routes, SITE_NAME, SITE_URL, typeLabel } from '@/lib/site'; import type { EntityDetail, EntitySummary } from '@/lib/types'; import { Capabilities, EntityList, HardwareFitTable, Identity, LineageBlock, ModelsTable, PriceHistory, PricesTable, ProvenanceSummary, RelationsBlock, ResultsTable, SourcesTable, SpecTable, TimelineList } from './blocks'; import { HistoryPanel } from './history'; /** Search params an entity page understands (History tab). */ export type EntityPageParams = { asof?: string; property?: string }; const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/; /* ---------------------------------------------------------------------------------------------------------- header chips */ function headerChips(d: EntityDetail): { label: string; value: string; key: string }[] { const a = d.attributes ?? {}; const out: { label: string; value: string; key: string }[] = []; const add = (key: string, label: string, value: string | null) => value && out.push({ key, label, value }); switch (d.entity_type) { case 'model': case 'quantization': { const p = num(a.parameter_count); const ap = num(a.active_parameter_count); add('parameter_count', 'Parameters', p === null ? null : ap !== null && ap !== p ? `${fmtParams(p)} · ${fmtParams(ap)} active` : fmtParams(p)); add('context_length', 'Context', num(a.context_length) === null ? null : `${fmtTokens(a.context_length)} tokens`); add('release_date', 'Released', typeof a.release_date === 'string' ? fmtDate(a.release_date) : null); add('license', 'License', typeof a.license === 'string' ? a.license : null); add('knowledge_cutoff', 'Knowledge cutoff', typeof a.knowledge_cutoff === 'string' ? fmtDate(a.knowledge_cutoff) : null); break; } case 'company': case 'organization': case 'lab': case 'university': add('country', 'Country', typeof a.country === 'string' ? a.country : null); add('founded', 'Founded', a.founded ? String(a.founded).slice(0, 4) : null); add('headquarters', 'HQ', typeof a.headquarters === 'string' ? a.headquarters : null); add('org_kind', 'Kind', typeof a.org_kind === 'string' ? a.org_kind : null); add('employee_count', 'Employees', num(a.employee_count) === null ? null : fmtInt(a.employee_count)); break; case 'paper': add('published_at', 'Published', typeof a.published_at === 'string' ? fmtDate(a.published_at) : null); add('venue', 'Venue', typeof a.venue === 'string' ? a.venue : null); add('arxiv_id', 'arXiv', typeof a.arxiv_id === 'string' ? a.arxiv_id : null); add('primary_category', 'Category', typeof a.primary_category === 'string' ? a.primary_category : null); break; case 'hardware': add('kind', 'Kind', typeof a.kind === 'string' ? a.kind : null); add('memory_gb', 'Memory', num(a.memory_gb) === null ? null : `${fmtInt(a.memory_gb)} GB ${typeof a.memory_type === 'string' ? a.memory_type : ''}`.trim()); add('memory_bandwidth_gbs', 'Bandwidth', num(a.memory_bandwidth_gbs) === null ? null : `${fmtInt(a.memory_bandwidth_gbs)} GB/s`); add('tdp_watts', 'TDP', num(a.tdp_watts) === null ? null : `${fmtInt(a.tdp_watts)} W`); add('release_date', 'Released', typeof a.release_date === 'string' ? fmtDate(a.release_date) : null); break; case 'benchmark': add('category', 'Category', typeof a.category === 'string' ? a.category : null); add('task', 'Task', typeof a.task === 'string' ? a.task : null); add('metric', 'Metric', typeof a.metric === 'string' ? a.metric : null); add('creator', 'Creator', typeof a.creator === 'string' ? a.creator : null); break; case 'framework': case 'library': case 'runtime': case 'repository': add('latest_version', 'Version', typeof a.latest_version === 'string' ? a.latest_version : null); add('latest_release_at', 'Released', typeof a.latest_release_at === 'string' ? fmtDate(a.latest_release_at) : null); add('language', 'Language', typeof a.language === 'string' ? a.language : null); add('license', 'License', typeof a.license === 'string' ? a.license : null); add('metric.stars', 'Stars', num(a['metric.stars']) === null ? null : fmtInt(a['metric.stars'])); break; case 'dataset': add('modality', 'Modality', typeof a.modality === 'string' ? a.modality : null); add('size', 'Size', a.size ? String(a.size) : null); add('license', 'License', typeof a.license === 'string' ? a.license : null); add('publisher', 'Publisher', typeof a.publisher === 'string' ? a.publisher : null); break; case 'provider': add('regions', 'Regions', Array.isArray(a.regions) && a.regions.length ? `${a.regions.length}` : null); break; default: break; } return out.slice(0, 5); } function primaryUrl(d: EntityDetail): string | null { const a = d.attributes ?? {}; for (const k of ['official_url', 'website', 'model_card_url', 'pdf_url', 'repository_url', 'spec_url', 'docs_url']) { const v = a[k]; if (typeof v === 'string' && /^https?:\/\//.test(v)) return v; } return null; } /* ---------------------------------------------------------------------------------------------------------- tabs per type */ type TabKey = 'overview' | 'capabilities' | 'benchmarks' | 'providers' | 'hardware' | 'lineage' | 'research' | 'models' | 'repositories' | 'leaderboard' | 'runnable' | 'relations' | 'history' | 'timeline' | 'sources'; function tabsFor(d: EntityDetail, claimCount?: number | null): TabDef[] { const t = d.entity_type; const c = (n: number | undefined | null) => (n ? n : undefined); const rel = d.relations?.reduce((n, g) => n + g.items.length, 0) ?? 0; const sources = d.sources?.length ?? 0; const timeline = d.timeline?.length ?? 0; const tail: TabDef[] = [ { id: 'history', label: 'History', count: c(claimCount ?? d.counts?.claims) }, { id: 'timeline', label: 'Timeline', count: c(timeline) }, { id: 'sources', label: 'Sources', count: c(sources) }, ]; if (t === 'model' || t === 'quantization') return [ { id: 'overview', label: 'Overview' }, { id: 'capabilities', label: 'Capabilities' }, { id: 'benchmarks', label: 'Benchmarks', count: c(d.results?.length) }, { id: 'providers', label: 'Providers & Pricing', count: c(d.prices?.length) }, { id: 'hardware', label: 'Hardware', count: c(d.hardware_fit?.length) }, { id: 'lineage', label: 'Lineage', count: c((d.lineage?.ancestors.length ?? 0) + (d.lineage?.descendants.length ?? 0) + (d.lineage?.quantizations.length ?? 0)) }, { id: 'research', label: 'Research', count: c((d.papers?.length ?? 0) + (d.repositories?.length ?? 0)) }, ...tail, ]; if (['company', 'organization', 'lab', 'university'].includes(t)) return [ { id: 'overview', label: 'Overview' }, { id: 'models', label: 'Models', count: c(d.models?.total ?? d.models?.items.length) }, { id: 'research', label: 'Research', count: c(d.papers?.length) }, { 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)) }, { id: 'repositories', label: 'Repositories', count: c(d.repositories?.length) }, ...tail, ]; if (t === 'provider') return [ { id: 'overview', label: 'Overview' }, { id: 'providers', label: 'Models & Pricing', count: c(d.prices?.length ?? d.models?.total) }, { id: 'relations', label: 'Relations', count: c(rel) }, ...tail, ]; if (t === 'benchmark') return [ { id: 'overview', label: 'Overview' }, { id: 'leaderboard', label: 'Leaderboard', count: c(d.results?.length) }, { id: 'relations', label: 'Relations', count: c(rel) }, ...tail, ]; if (t === 'hardware') return [ { id: 'overview', label: 'Overview' }, { id: 'runnable', label: 'Runnable models', count: c(d.models?.total ?? d.models?.items.length) }, { id: 'relations', label: 'Relations', count: c(rel) }, ...tail, ]; if (t === 'paper') return [ { id: 'overview', label: 'Overview' }, { 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)) }, { id: 'relations', label: 'Relations', count: c(rel) }, ...tail, ]; return [{ id: 'overview', label: 'Overview' }, { id: 'relations', label: 'Relations', count: c(rel) }, ...tail]; } /* ---------------------------------------------------------------------------------------------------------- JSON-LD */ function jsonLd(d: EntityDetail, canonical: string) { const a = d.attributes ?? {}; const url = `${SITE_URL}${canonical}`; const org = d.organization ? { '@type': 'Organization', name: d.organization.name, url: `${SITE_URL}${routes.entity({ entity_type: 'company', slug: d.organization.slug })}` } : undefined; const base: Record = { '@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 })) }; switch (d.entity_type) { case 'model': case 'quantization': 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 }; case 'company': case 'organization': case 'lab': case 'university': 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 }; case 'paper': 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 }; case 'hardware': return { ...base, '@type': 'Product', manufacturer: typeof a.manufacturer === 'string' ? { '@type': 'Organization', name: a.manufacturer } : org, category: typeof a.kind === 'string' ? a.kind : undefined }; case 'framework': case 'library': case 'runtime': case 'repository': case 'tool': 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 }; case 'dataset': return { ...base, '@type': 'Dataset', license: typeof a.license === 'string' ? a.license : undefined, creator: org }; default: return { ...base, '@type': 'Thing' }; } } /* ---------------------------------------------------------------------------------------------------------- page */ /** * Shared entity page for every type. Header (badges, name, org, description, key chips) + URL-driven tabs whose set * depends on the type. Panels are all server-rendered (SEO); the client Tabs only toggles visibility. */ export async function EntityPage({ d, canonical, related, asof: asofRaw, historyProperty }: { d: EntityDetail; canonical: string; related?: EntitySummary[] | null; asof?: string; historyProperty?: string }) { const a = d.attributes ?? {}; const chips = headerChips(d); const openness = typeof a.openness === 'string' ? a.openness : null; const link = primaryUrl(d); // History tab data: full claim history always (it is the tab's content); the "as of" state only when requested. const asof = asofRaw && ISO_DAY.test(asofRaw) ? asofRaw : asofRaw ? 'invalid' : undefined; const property = historyProperty?.trim() || undefined; const [history, asofPayload] = await Promise.all([safe(api.entityHistory(d.slug, property)), asof && asof !== 'invalid' ? safe(api.entityAsOf(d.slug, asof)) : Promise.resolve(null)]); const claims = history?.items ?? null; const tabs = tabsFor(d, property ? null : claims?.length); const memoryGb = d.entity_type === 'hardware' ? num(a.memory_gb) : null; const isModel = d.entity_type === 'model' || d.entity_type === 'quantization'; const isCompany = ['company', 'organization', 'lab', 'university'].includes(d.entity_type); const ld = jsonLd(d, canonical); const prose = typeof a.abstract === 'string' ? a.abstract : null; const modelRelations = d.relations?.filter((g) => g.items.some((i) => i.entity_type === 'model')).flatMap((g) => g.items.filter((i) => i.entity_type === 'model')) ?? []; return (