HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import Link from 'next/link';2import type { ReactNode } from 'react';3import { cn } from '@/lib/cn';4import { fmtDate, fmtInt, fmtParams, fmtTokens, fmtUsdPerM, num } from '@/lib/format';5import { routes, typeLabel } from '@/lib/site';6import type { EntitySummary } from '@/lib/types';7import { EntityBadge, OpennessBadge, StatusBadge } from './badges';89export function EntityLink({ e, className, children }: { e: { entity_type: string; slug: string; name: string }; className?: string; children?: ReactNode }) {10 return (11 <Link href={routes.entity(e)} className={cn('text-ink hover:text-accent hover:underline', className)}>12 {children ?? e.name}13 </Link>14 );15}1617/** Key attribute chips for a summary, type-aware (model: params · context · release; company: country; …). */18export function keyAttributes(e: EntitySummary): { label: string; value: string }[] {19 const a = e.attributes ?? {};20 const out: { label: string; value: string }[] = [];21 const push = (label: string, value: string | null | undefined) => {22 if (value && value !== '—') out.push({ label, value });23 };24 switch (e.entity_type) {25 case 'model':26 case 'quantization': {27 const p = num(a.parameter_count);28 const ap = num(a.active_parameter_count);29 if (p !== null) push('params', ap !== null && ap !== p ? `${fmtParams(p)} (${fmtParams(ap)} active)` : fmtParams(p));30 if (num(a.context_length) !== null) push('context', fmtTokens(a.context_length));31 if (typeof a.release_date === 'string') push('released', fmtDate(a.release_date));32 if (typeof a.license === 'string') push('license', a.license);33 break;34 }35 case 'company':36 case 'organization':37 case 'lab':38 case 'university':39 if (typeof a.country === 'string') push('country', a.country);40 if (a.founded) push('founded', String(a.founded).slice(0, 4));41 if (typeof a.org_kind === 'string') push('kind', a.org_kind);42 break;43 case 'paper':44 if (typeof a.published_at === 'string') push('published', fmtDate(a.published_at));45 if (Array.isArray(a.authors) && a.authors.length) push('authors', `${a.authors.length}`);46 if (typeof a.venue === 'string') push('venue', a.venue);47 if (typeof a.arxiv_id === 'string') push('arXiv', a.arxiv_id);48 break;49 case 'provider':50 if (Array.isArray(a.regions) && a.regions.length) push('regions', `${a.regions.length}`);51 break;52 case 'benchmark':53 if (typeof a.category === 'string') push('category', a.category);54 if (typeof a.metric === 'string') push('metric', a.metric);55 break;56 case 'hardware':57 case 'robot':58 if (typeof a.kind === 'string') push('kind', a.kind);59 if (num(a.memory_gb) !== null) push('memory', `${fmtInt(a.memory_gb)} GB`);60 if (num(a.memory_bandwidth_gbs) !== null) push('bandwidth', `${fmtInt(a.memory_bandwidth_gbs)} GB/s`);61 if (num(a.tdp_watts) !== null) push('TDP', `${fmtInt(a.tdp_watts)} W`);62 break;63 case 'framework':64 case 'library':65 case 'runtime':66 case 'repository':67 if (typeof a.latest_version === 'string') push('version', a.latest_version);68 if (typeof a.language === 'string') push('language', a.language);69 if (num(a['metric.stars']) !== null) push('stars', fmtInt(a['metric.stars']));70 if (typeof a.license === 'string') push('license', a.license);71 break;72 case 'dataset':73 if (typeof a.modality === 'string') push('modality', a.modality);74 if (a.size) push('size', String(a.size));75 if (typeof a.license === 'string') push('license', a.license);76 break;77 default:78 break;79 }80 return out.slice(0, 4);81}8283export function cheapestPrice(e: EntitySummary): string | null {84 const a = e.attributes ?? {};85 const v = num(a.min_input_per_mtok ?? a.best_input_per_mtok);86 return v === null ? null : fmtUsdPerM(v);87}8889/** Dense list row: badge · name (link) · org · key attributes · quality. Used by search results, related, explore. */90export function EntityRow({ e, showType = true, trailing, className, showDescription = true }: { e: EntitySummary; showType?: boolean; trailing?: ReactNode; className?: string; showDescription?: boolean }) {91 const attrs = keyAttributes(e);92 const openness = typeof e.attributes?.openness === 'string' ? e.attributes.openness : null;93 return (94 <li className={cn('border-b border-rule py-3', className)}>95 <div className="flex items-start gap-3">96 <div className="min-w-0 flex-1">97 <div className="flex flex-wrap items-center gap-x-2 gap-y-1">98 {showType && <EntityBadge type={e.entity_type} small />}99 <EntityLink e={e} className="text-[15px] font-medium" />100 {e.organization && (101 <Link href={routes.entity({ entity_type: 'company', slug: e.organization.slug })} className="text-sm text-ink-3 hover:text-accent">102 {e.organization.name}103 </Link>104 )}105 <StatusBadge status={e.status} />106 {openness && <OpennessBadge openness={openness} />}107 </div>108 {showDescription && e.description && <p className="mt-1 line-clamp-2 text-sm text-ink-2">{e.description}</p>}109 {attrs.length > 0 && (110 <p className="tnum mt-1 flex flex-wrap gap-x-3 gap-y-0.5 text-xs text-ink-3">111 {attrs.map((x) => (112 <span key={x.label}>113 <span className="text-ink-3">{x.label} </span>114 <span className="text-ink-2">{x.value}</span>115 </span>116 ))}117 </p>118 )}119 </div>120 <div className="flex shrink-0 flex-col items-end gap-1 text-right">121 {trailing}122 <QualityMark q={e.quality?.score} />123 </div>124 </div>125 </li>126 );127}128129/** Quality score 0–100 (how well AI Atlas knows the entity, not how good it is). */130export function QualityMark({ q, className, label = false }: { q: number | undefined | null; className?: string; label?: boolean }) {131 const n = num(q);132 if (n === null) return null;133 const tone = n >= 75 ? 'bg-positive' : n >= 45 ? 'bg-accent' : 'bg-warning';134 return (135 <span className={cn('inline-flex items-center gap-1.5 text-xs text-ink-3', className)} title={`Data quality ${Math.round(n)}/100 — how well AI Atlas knows this entity`}>136 {label && <span>data quality</span>}137 <span className="relative h-1 w-10 overflow-hidden rounded-sm bg-rule-strong">138 <span className={cn('absolute inset-y-0 left-0', tone)} style={{ width: `${Math.min(100, Math.max(0, n))}%` }} />139 </span>140 <span className="tnum text-ink-2">{Math.round(n)}</span>141 </span>142 );143}144145/** Inline compact list of entity links "a, b, c +4". */146export function EntityInline({ items, max = 6, total }: { items: EntitySummary[]; max?: number; total?: number }) {147 const shown = items.slice(0, max);148 const rest = (total ?? items.length) - shown.length;149 return (150 <span className="text-sm">151 {shown.map((e, i) => (152 <span key={e.id}>153 {i > 0 && <span className="text-ink-3">, </span>}154 <EntityLink e={e} />155 </span>156 ))}157 {rest > 0 && <span className="text-ink-3"> +{rest}</span>}158 </span>159 );160}161162export function typeOf(e: { entity_type: string }): string {163 return typeLabel(e.entity_type);164}165