HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
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