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%
19.3 KB · 371 lines tsx
Raw Blame History
1'use client';2import { Columns3, ScanSearch } from 'lucide-react';3import Link from 'next/link';4import { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';5import { CompareButton } from '@/components/compare/compare-button';6import { TerminalLayout } from '@/components/layout/terminal';7import { StatusBadge } from '@/components/ui/badges';8import { EntityLink, QualityMark } from '@/components/ui/entity';9import { Sheet } from '@/components/ui/sheet';10import { WatchButton } from '@/components/watchlist/watch-button';11import { cn } from '@/lib/cn';12import { DASH, fmtDate, fmtInt, fmtParams, fmtTokens, fmtUsdPerM, num } from '@/lib/format';13import { routes, STATUS_LABELS } from '@/lib/site';14import type { ModelRow } from '@/lib/types';15import { ArtifactKindChip, IdentityBadge, OpennessChip } from './badges';16import { IDENTITY_LABEL, opennessLabel, rowPrice } from './shared';1718/*19  /models terminal (client): shares the "inspected row" between the table (main pane) and the inspector (right pane / mobile sheet).20  The filter rail is a server-rendered node passed through untouched. Column visibility lives in localStorage['aia-models-cols'].21*/2223type Ctx = { selected: ModelRow | null; select: (m: ModelRow | null, opts?: { sheet?: boolean }) => void };24const InspectCtx = createContext<Ctx>({ selected: null, select: () => undefined });2526export const COLUMNS = [27  { key: 'params', label: 'Params', num: true },28  { key: 'context', label: 'Context', num: true },29  { key: 'openness', label: 'Openness' },30  { key: 'license', label: 'Licence' },31  { key: 'release', label: 'Released' },32  { key: 'price', label: 'Best price in / out', num: true },33  { key: 'quality', label: 'Data quality', num: true },34] as const;35type ColKey = (typeof COLUMNS)[number]['key'];36const COLS_KEY = 'aia-models-cols';37const DEFAULT_COLS: ColKey[] = ['params', 'context', 'openness', 'license', 'release', 'quality'];3839export function ModelsTerminal({ filters, filterCount, children, items }: { filters: ReactNode; filterCount: number; children: ReactNode; items: ModelRow[] }) {40  const [selected, setSelected] = useState<ModelRow | null>(null);41  const [sheet, setSheet] = useState(false);42  const select = useCallback((m: ModelRow | null, opts?: { sheet?: boolean }) => {43    setSelected(m);44    if (m && opts?.sheet && typeof window !== 'undefined' && window.matchMedia('(max-width: 1023px)').matches) setSheet(true);45  }, []);46  // Default inspector target: the first row (so the pane is never empty when rows exist).47  const target = selected ?? items[0] ?? null;48  const value = useMemo(() => ({ selected, select }), [selected, select]);49  return (50    <InspectCtx.Provider value={value}>51      <TerminalLayout filters={filters} filtersTitle="Filters" filterCount={filterCount} inspector={<Inspector m={target} placeholder={!selected} />} inspectorTitle="Inspector" storageKey="aia-models-inspector">52        {children}53      </TerminalLayout>54      <Sheet open={sheet} onClose={() => setSheet(false)} side="bottom" eyebrow="Inspector" title={target?.name}>55        <Inspector m={target} placeholder={false} />56      </Sheet>57    </InspectCtx.Provider>58  );59}6061/* ---------------------------------------------------------------------------------------------------------- inspector */6263function Inspector({ m, placeholder }: { m: ModelRow | null; placeholder: boolean }) {64  if (!m) return <p className="text-sm text-ink-3">No row to inspect — adjust the filters.</p>;65  const a = m.attributes ?? {};66  const p = num(a.parameter_count);67  const ap = num(a.active_parameter_count);68  const facts: { k: string; v: ReactNode }[] = [69    { k: 'Parameters', v: p === null ? DASH : `${fmtParams(p)}${ap !== null && ap !== p ? ` · ${fmtParams(ap)} active` : ''}` },70    { k: 'Context', v: num(a.context_length) === null ? DASH : `${fmtTokens(a.context_length)} tokens` },71    { k: 'Max output', v: num(a.max_output_tokens) === null ? DASH : `${fmtTokens(a.max_output_tokens)} tokens` },72    { k: 'Openness', v: opennessLabel(a.openness) },73    { k: 'Licence', v: typeof a.license_key === 'string' ? a.license_key : typeof a.license === 'string' ? a.license : DASH },74    { k: 'Released', v: typeof a.release_date === 'string' ? fmtDate(a.release_date) : DASH },75    { k: 'Status', v: STATUS_LABELS[m.status] ?? m.status ?? DASH },76    { k: 'Knowledge cutoff', v: typeof a.knowledge_cutoff === 'string' ? fmtDate(a.knowledge_cutoff) : DASH },77    { k: 'Modalities', v: Array.isArray(a.modalities) && a.modalities.length ? (a.modalities as string[]).join(', ') : DASH },78    { k: 'Family', v: m.family ? <Link href={routes.family(m.family.slug)} className="link">{m.family.name}</Link> : typeof a.family === 'string' ? a.family : DASH },79    { k: 'Identity', v: m.identity_confidence ? IDENTITY_LABEL[m.identity_confidence] : DASH },80  ];81  const isArtifact = m.entity_type === 'artifact';82  return (83    <div className="space-y-4 text-sm" data-models-inspector data-inspected={m.slug}>84      {placeholder && <p className="text-[11px] text-ink-3">Showing the first row — press “Inspect” on any row (or focus it and press Enter).</p>}85      <div>86        <p className="flex flex-wrap items-center gap-1.5">87          <EntityLink e={isArtifact ? { ...m, entity_type: 'artifact' } : m} className="text-[15px] font-semibold" />88          <StatusBadge status={m.status !== 'active' ? m.status : null} />89          {isArtifact && <ArtifactKindChip kind={m.artifact_kind} />}90        </p>91        {m.organization && (92          <Link href={routes.entity({ entity_type: 'company', slug: m.organization.slug })} className="text-xs text-ink-3 hover:text-accent">93            {m.organization.name}94          </Link>95        )}96        {isArtifact && m.canonical && (97          <p className="mt-1 text-xs text-ink-2">98            Packaging of <EntityLink e={m.canonical} className="font-medium" /> — not an independent model.99          </p>100        )}101        {m.description && <p className="mt-2 line-clamp-4 text-[13px] leading-relaxed text-ink-2">{m.description}</p>}102      </div>103      <dl className="kv [&>div]:grid-cols-[7rem_minmax(0,1fr)] [&>div]:py-1">104        {facts.map((f) => (105          <div key={f.k}>106            <dt>{f.k}</dt>107            <dd className="tnum text-ink">{f.v}</dd>108          </div>109        ))}110      </dl>111      <div className="text-[11px] leading-5 text-ink-3">112        <p className="flex items-center gap-2">113          <QualityMark q={m.quality?.score} label /> {m.quality?.score === undefined && 'Data quality not computed yet'}114        </p>115        <p title={m.updated_at}>Updated {fmtDate(m.updated_at)} · first seen {fmtDate(m.first_seen_at)}</p>116        <p>117          {fmtInt(m.counts?.claims)} claims · {fmtInt(m.counts?.relations)} relations · {fmtInt(m.counts?.events)} events118        </p>119        <p>Field-level provenance (source, tier, observed time) is on the model page — every value opens the evidence drawer there.</p>120      </div>121      <div className="flex flex-wrap items-center gap-1.5">122        <CompareButton e={m} size="sm" />123        <WatchButton e={m} size="sm" />124        <Link href={routes.entity(isArtifact ? { ...m, entity_type: 'artifact' } : m)} className="inline-flex h-7 items-center border border-rule px-1.5 text-xs text-ink-2 hover:border-rule-strong hover:text-ink">125          Open page →126        </Link>127        <Link href={routes.graph(m.slug)} className="inline-flex h-7 items-center border border-rule px-1.5 text-xs text-ink-2 hover:border-rule-strong hover:text-ink">128          Graph129        </Link>130      </div>131    </div>132  );133}134135/* ---------------------------------------------------------------------------------------------------------- table */136137export function ModelsTable({ items, sort, order, sortHref, orgHrefTemplate, offset }: { items: ModelRow[]; sort: string; order?: string; sortHref: Record<string, string>; /** URL with `__ORG__` where the organization slug goes (functions cannot cross the server → client boundary). */ orgHrefTemplate: string; offset: number }) {138  const orgHref = (slug: string) => orgHrefTemplate.replace('__ORG__', encodeURIComponent(slug));139  const { selected, select } = useContext(InspectCtx);140  const [cols, setCols] = useState<ColKey[]>(DEFAULT_COLS);141  const [ready, setReady] = useState(false);142  const [chooser, setChooser] = useState(false);143  const bodyRef = useRef<HTMLTableSectionElement>(null);144  useEffect(() => {145    try {146      const raw = localStorage.getItem(COLS_KEY);147      if (raw) {148        const arr = JSON.parse(raw) as unknown;149        if (Array.isArray(arr)) setCols(COLUMNS.map((c) => c.key).filter((k) => (arr as string[]).includes(k)));150      }151    } catch {152      /* ignore */153    }154    setReady(true);155  }, []);156  const toggleCol = (k: ColKey) => {157    setCols((cur) => {158      const next = cur.includes(k) ? cur.filter((x) => x !== k) : COLUMNS.map((c) => c.key).filter((x) => x === k || cur.includes(x));159      try {160        localStorage.setItem(COLS_KEY, JSON.stringify(next));161      } catch {162        /* ignore */163      }164      return next;165    });166  };167  const show = (k: ColKey) => !ready || cols.includes(k);168169  const onRowKey = (e: React.KeyboardEvent<HTMLTableRowElement>, m: ModelRow, i: number) => {170    if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {171      e.preventDefault();172      const rows = bodyRef.current?.querySelectorAll<HTMLTableRowElement>('tr[data-row]');173      rows?.[i + (e.key === 'ArrowDown' ? 1 : -1)]?.focus();174    } else if (e.key === 'Enter' || e.key === ' ' || e.key.toLowerCase() === 'i') {175      if ((e.target as HTMLElement).closest('a, button, input')) return;176      e.preventDefault();177      select(m, { sheet: e.key !== 'i' });178    }179  };180181  const SortTh = ({ s, children, num: n, hide }: { s: string; children: ReactNode; num?: boolean; hide?: boolean }) =>182    hide ? null : (183      <th scope="col" className={cn(n && 'num')} aria-sort={sort === s ? (order === 'asc' ? 'ascending' : 'descending') : undefined}>184        <Link href={sortHref[s] ?? '#'} className={cn('inline-flex items-center gap-0.5', sort === s ? 'text-ink' : 'hover:text-ink')}>185          {children}186          {sort === s && <span aria-hidden>{order === 'asc' ? '↑' : '↓'}</span>}187        </Link>188      </th>189    );190191  return (192    <div data-models-table>193      <div className="flex items-center justify-end gap-2 pb-1">194        <div className="relative">195          <button type="button" onClick={() => setChooser((c) => !c)} aria-expanded={chooser} aria-haspopup="true" className="inline-flex h-8 items-center gap-1.5 border border-rule px-2 text-xs text-ink-2 hover:border-rule-strong hover:text-ink" data-column-chooser>196            <Columns3 className="size-3.5" aria-hidden /> Columns <span className="tnum text-ink-3">{ready ? cols.length : DEFAULT_COLS.length}/{COLUMNS.length}</span>197          </button>198          {chooser && (199            <div className="panel absolute right-0 top-9 z-40 w-56 p-2 shadow-lg" role="group" aria-label="Visible columns">200              {COLUMNS.map((c) => (201                <label key={c.key} className="flex min-h-9 items-center gap-2 px-1 text-[13px] text-ink-2 hover:bg-surface-2">202                  <input type="checkbox" checked={show(c.key)} onChange={() => toggleCol(c.key)} className="size-4 accent-[var(--accent)]" /> {c.label}203                </label>204              ))}205              <p className="px-1 pt-1 text-[10px] text-ink-3">Saved in this browser.</p>206            </div>207          )}208        </div>209      </div>210      {/* ≥ md: the dense table scrolls horizontally inside its pane when needed (a scroll container offsets a sticky header, so the header stays static); < md rows stack. */}211      <div className="relative scrollbar-thin md:overflow-x-auto">212      <table className="data-table stack compact md:[&_td]:align-top md:[&_td]:px-2 md:[&_th]:px-2">213        <caption className="sr-only">Models</caption>214        <thead>215          <tr>216            <th scope="col" className="tnum hidden w-8 text-ink-3 xl:table-cell">217              #218            </th>219            <SortTh s="name">Model</SortTh>220            <SortTh s="params" num hide={!show('params')}>221              Params222            </SortTh>223            <SortTh s="context" num hide={!show('context')}>224              Context225            </SortTh>226            {show('openness') && <th scope="col">Openness</th>}227            {show('license') && <th scope="col">Licence</th>}228            <SortTh s="release" hide={!show('release')}>229              Released230            </SortTh>231            <SortTh s="cheapest" num hide={!show('price')}>232              Best price in / out233            </SortTh>234            <SortTh s="quality" num hide={!show('quality')}>235              Data quality236            </SortTh>237            <th scope="col" className="text-right">238              <span className="sr-only">Actions</span>239            </th>240          </tr>241        </thead>242        <tbody ref={bodyRef}>243          {items.length === 0 && (244            <tr>245              <td colSpan={10} className="py-10 text-center text-sm text-ink-3">246                No models match these filters.247              </td>248            </tr>249          )}250          {items.map((m, i) => {251            const a = m.attributes ?? {};252            const p = num(a.parameter_count);253            const ap = num(a.active_parameter_count);254            const on = selected?.slug === m.slug;255            const isArtifact = m.entity_type === 'artifact';256            const licence = typeof a.license_key === 'string' ? a.license_key : typeof a.license === 'string' ? a.license : null;257            const pin = rowPrice(a, 'input');258            const pout = rowPrice(a, 'output');259            return (260              <tr261                key={m.id}262                data-row={m.slug}263                tabIndex={0}264                aria-selected={on}265                onKeyDown={(e) => onRowKey(e, m, i)}266                onClick={(e) => {267                  if ((e.target as HTMLElement).closest('a, button, input')) return;268                  select(m);269                }}270                className={cn('cursor-default focus-visible:outline-2 focus-visible:outline-accent', on && 'bg-accent-soft/40')}271              >272                <td className="tnum hide-stack hidden text-ink-3 xl:table-cell">{fmtInt(offset + i + 1)}</td>273                <td className="primary min-w-[11rem] md:max-w-[20rem]">274                  <div className="min-w-0">275                    <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5">276                      <EntityLink e={isArtifact ? { ...m, entity_type: 'artifact' } : m} />277                      <StatusBadge status={m.status !== 'active' ? m.status : null} />278                      <IdentityBadge level={m.identity_confidence} />279                      {isArtifact && <ArtifactKindChip kind={m.artifact_kind} />}280                    </div>281                    <div className="flex flex-wrap items-center gap-x-1.5 text-xs text-ink-3">282                      {m.organization && (283                        <Link href={orgHref(m.organization.slug)} className="hover:text-accent">284                          {m.organization.name}285                        </Link>286                      )}287                      {m.family ? (288                        <>289                          <span aria-hidden>·</span>290                          <Link href={routes.family(m.family.slug)} className="hover:text-accent">291                            {m.family.name}292                          </Link>293                        </>294                      ) : typeof a.family === 'string' ? (295                        <>296                          <span aria-hidden>·</span>297                          <span>{a.family}</span>298                        </>299                      ) : null}300                      {isArtifact && m.canonical && (301                        <>302                          <span aria-hidden>·</span>303                          <span>304                            of <EntityLink e={m.canonical} className="text-ink-2" />305                          </span>306                        </>307                      )}308                    </div>309                  </div>310                </td>311                {show('params') && (312                  <td className="num tnum" data-label="Params">313                    {p === null ? <span className="text-ink-3">{DASH}</span> : fmtParams(p)}314                    {ap !== null && ap !== p && <span className="block text-[11px] text-ink-3">{fmtParams(ap)} active</span>}315                  </td>316                )}317                {show('context') && (318                  <td className="num tnum" data-label="Context">319                    {num(a.context_length) === null ? <span className="text-ink-3">{DASH}</span> : fmtTokens(a.context_length)}320                  </td>321                )}322                {show('openness') && (323                  <td data-label="Openness">324                    {typeof a.openness === 'string' ? <OpennessChip openness={a.openness} /> : <span className="text-ink-3">{DASH}</span>}325                  </td>326                )}327                {show('license') && (328                  <td data-label="Licence" className="max-w-[11rem] truncate text-ink-2" title={licence ?? undefined}>329                    {licence ? <Link href={`/licenses/${encodeURIComponent(licence)}`} className="hover:text-accent">{licence}</Link> : <span className="text-ink-3">{DASH}</span>}330                  </td>331                )}332                {show('release') && (333                  <td data-label="Released" className="tnum whitespace-nowrap text-ink-2">334                    {typeof a.release_date === 'string' ? fmtDate(a.release_date) : <span className="text-ink-3">{DASH}</span>}335                  </td>336                )}337                {show('price') && (338                  <td className="num tnum whitespace-nowrap" data-label="Best price in / out" title={pin === null && pout === null ? 'No price on this row — see the model page for provider deployments' : 'Cheapest current offer across providers, USD per 1M tokens'}>339                    {pin === null && pout === null ? (340                      <span className="text-ink-3">{DASH}</span>341                    ) : (342                      <span className="text-accent-2">343                        {fmtUsdPerM(pin)} <span className="text-ink-3">/</span> {fmtUsdPerM(pout)}344                      </span>345                    )}346                  </td>347                )}348                {show('quality') && (349                  <td className="num" data-label="Data quality">350                    <QualityMark q={m.quality?.score} />351                  </td>352                )}353                <td className="text-right whitespace-nowrap">354                  <span className="inline-flex items-center justify-end gap-1">355                    <button type="button" onClick={() => select(m, { sheet: true })} aria-pressed={on} className={cn('inline-flex h-7 items-center gap-1 border px-1.5 text-xs whitespace-nowrap', on ? 'border-accent bg-accent-soft text-accent' : 'border-rule text-ink-2 hover:border-rule-strong hover:text-ink')} data-inspect={m.slug} title="Inspect: key facts in the side pane" aria-label={`Inspect ${m.name}`}>356                      <ScanSearch className="size-3" aria-hidden /> <span className="md:sr-only xl:not-sr-only">Inspect</span>357                    </button>358                    <CompareButton e={m} size="sm" label="" />359                    <WatchButton e={m} size="sm" label="" />360                  </span>361                </td>362              </tr>363            );364          })}365        </tbody>366      </table>367      </div>368    </div>369  );370}371