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%
7.2 KB · 165 lines tsx
Raw Blame History
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