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%
12.7 KB · 227 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { RailFilters } from '@/components/changes/rail-filters';4import { TerminalLayout } from '@/components/layout/terminal';5import { ActiveFilters } from '@/components/listing/filters';6import { BreadcrumbLd } from '@/components/meta/breadcrumb-ld';7import { Chip, EntityBadge } from '@/components/ui/badges';8import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';9import { EntityLink } from '@/components/ui/entity';10import { Hint } from '@/components/ui/hint';11import { Pagination, withParams } from '@/components/ui/pagination';12import { Container, Note, PageHeader } from '@/components/ui/section';13import { EmptyState, Unavailable } from '@/components/ui/unavailable';14import { api, safe } from '@/lib/api';15import { fmtDate, fmtInt } from '@/lib/format';16import { routes, SITE_NAME } from '@/lib/site';17import type { EntityDetail, EntitySummary } from '@/lib/types';1819export const metadata: Metadata = { title: 'AI research papers — authors, models introduced, datasets, benchmarks', description: 'Research papers in the atlas with their authors, publication dates, arXiv categories and the models, datasets and benchmarks they are linked to — as stated by model cards and paper metadata.', alternates: { canonical: '/papers' } };20export const revalidate = 300;2122type SP = Record<string, string | undefined>;23const LIMIT = 25;24const KEYS = ['q', 'category', 'org', 'since', 'until', 'sort', 'offset'] as const;25const SORTS = [26  { value: 'published', label: 'Recently published' },27  { value: 'updated', label: 'Recently updated' },28  { value: 'name', label: 'Title' },29];3031function str(v: unknown): string | null {32  return typeof v === 'string' && v.trim() ? v : null;33}34function list(v: unknown): string[] {35  return Array.isArray(v) ? v.filter((x) => x !== null && x !== undefined).map(String) : [];36}37type Linked = { models: EntitySummary[]; datasets: EntitySummary[]; benchmarks: EntitySummary[]; code: EntitySummary[] };38function linked(d: EntityDetail | null): Linked {39  const out: Linked = { models: [], datasets: [], benchmarks: [], code: [] };40  if (!d) return out;41  const seen = new Set<string>();42  for (const g of d.relations ?? [])43    for (const it of g.items) {44      if (seen.has(it.id)) continue;45      seen.add(it.id);46      if (it.entity_type === 'model' || it.entity_type === 'artifact') out.models.push(it);47      else if (it.entity_type === 'dataset') out.datasets.push(it);48      else if (it.entity_type === 'benchmark') out.benchmarks.push(it);49      else if (['repository', 'framework', 'library'].includes(it.entity_type)) out.code.push(it);50    }51  return out;52}5354export default async function PapersPage({ searchParams }: { searchParams: Promise<SP> }) {55  const sp = await searchParams;56  const current: Record<string, string | undefined> = {};57  for (const k of KEYS) if (sp[k]) current[k] = sp[k];58  const offset = Math.max(0, Number(current.offset) || 0);59  const sort = current.sort ?? 'published';60  const [page, orgs] = await Promise.all([safe(api.papers({ ...current, sort, limit: LIMIT, offset })), safe(api.companies({ limit: 40, sort: 'models' }))]);61  // "Introduces" needs each paper's relations: fetch the page's details in parallel (local API, ISR-cached).62  const details = page ? await Promise.all(page.items.map((p) => safe(api.entity(p.slug)))) : [];63  const href = (patch: Record<string, string | number | undefined | null>) => withParams('/papers', current, patch);64  const cats = new Map<string, number>();65  for (const p of page?.items ?? []) for (const c of list(p.attributes?.categories)) cats.set(c, (cats.get(c) ?? 0) + 1);66  const activeCount = Object.keys(current).filter((k) => !['sort', 'offset'].includes(k)).length;6768  const filters = (69    <RailFilters70      action="/papers"71      resetHref={routes.papers()}72      testId="papers"73      fields={[74        { kind: 'text', name: 'q', label: 'Title contains', value: current.q, placeholder: 'e.g. mixture of experts' },75        { kind: 'text', name: 'category', label: 'arXiv category', value: current.category, placeholder: 'cs.CL, cs.LG…', list: [...cats.keys()].sort() },76        { kind: 'select', name: 'org', label: 'Organization', value: current.org, remote: 'companies', options: (orgs?.items ?? []).filter((o) => o.slug === current.org).map((o) => ({ value: o.slug, label: o.name })), note: 'Stated publisher only — arXiv metadata carries no affiliation.' },77        { kind: 'row', fields: [{ kind: 'date', name: 'since', label: 'Since', value: current.since }, { kind: 'date', name: 'until', label: 'Until', value: current.until }] },78        { kind: 'select', name: 'sort', label: 'Sort', value: sort, any: SORTS[0]!.label, options: SORTS.slice(1) },79      ]}80    />81  );82  const inspector = (83    <div className="space-y-4 text-sm">84      <div>85        <p className="eyebrow mb-1.5">Categories on this page</p>86        {cats.size === 0 ? (87          <p className="text-xs text-ink-3">—</p>88        ) : (89          <ul className="flex flex-wrap gap-1">90            {[...cats.entries()]91              .sort((a, b) => b[1] - a[1])92              .slice(0, 16)93              .map(([c, n]) => (94                <li key={c}>95                  <Link href={href({ category: c, offset: undefined })} className="inline-flex h-7 items-center gap-1 border border-rule px-1.5 text-xs text-ink-2 hover:border-rule-strong hover:text-ink">96                    <span className="mono">{c}</span> <span className="tnum text-ink-3">{n}</span>97                  </Link>98                </li>99              ))}100          </ul>101        )}102      </div>103      <p className="text-xs text-ink-3">104        “Introduces” lists the models whose cards cite the paper (<span className="mono">described_by</span>). Author names link to researcher pages when a record exists. <Link href={`/graph?mode=research`} className="link">Research graph →</Link>105      </p>106    </div>107  );108109  return (110    <>111      <Container wide>112        <BreadcrumbLd items={[{ name: SITE_NAME, href: '/' }, { name: 'Research', href: '/papers' }]} />113        <PageHeader eyebrow="Research" title="Papers" lede="Publications linked to models, labs and benchmarks. Authors, venues and abstracts come from arXiv and publisher pages; model links come from model cards citing the paper." aside={page ? <p className="tnum text-sm text-ink-3">{fmtInt(page.total)} papers</p> : undefined} className="pb-3">114          <ActiveFilters current={current} labels={{ q: 'title', category: 'category', org: 'organization', since: 'since', until: 'until' }} makeHref={(p) => href(p)} className="mt-3" />115        </PageHeader>116      </Container>117      <div className="pb-16">118        <TerminalLayout filters={filters} inspector={inspector} filtersTitle="Filters" inspectorTitle="Context" storageKey="aia-papers-inspector" filterCount={activeCount}>119          {!page ? (120            <Unavailable what="Papers" />121          ) : page.items.length === 0 ? (122            <EmptyState title="No papers match">Try another title, category or organization.</EmptyState>123          ) : (124            <>125              <DataTable caption="Papers" compact scroll>126                <thead>127                  <tr>128                    <Th>Title</Th>129                    <Th>Authors</Th>130                    <Th>Organization</Th>131                    <Th>Published</Th>132                    <Th>133                      Introduces <Hint text="Models (and artifacts) whose model card or documentation cites this paper — inbound described_by relations." />134                    </Th>135                    <Th>Datasets</Th>136                    <Th>Benchmarks</Th>137                    <Th>Code</Th>138                  </tr>139                </thead>140                <tbody>141                  {page.items.length === 0 && <EmptyRow cols={8}>No rows.</EmptyRow>}142                  {page.items.map((e, i) => {143                    const a = e.attributes ?? {};144                    const authors = list(a.authors);145                    const l = linked(details[i] ?? null);146                    const code = str(a.code_url);147                    const primary = str(a.primary_category);148                    return (149                      <tr key={e.id}>150                        <Td primary>151                          <EntityLink e={e} />152                          <span className="mt-0.5 flex flex-wrap items-center gap-1">153                            {str(a.arxiv_id) && <span className="mono text-[11px] text-ink-3">arXiv:{str(a.arxiv_id)}</span>}154                            {primary && (155                              <Link href={href({ category: primary, offset: undefined })}>156                                <Chip tone="accent" className="mono">{primary}</Chip>157                              </Link>158                            )}159                          </span>160                        </Td>161                        <Td label="Authors" className="max-w-[18rem] text-sm text-ink-2">162                          {authors.length ? (163                            <>164                              {authors.slice(0, 3).join(', ')}165                              {authors.length > 3 && <span className="tnum text-ink-3"> +{authors.length - 3}</span>}166                            </>167                          ) : (168                            <span className="text-ink-3">—</span>169                          )}170                        </Td>171                        <Td label="Organization" className="text-ink-2">172                          {e.organization ? (173                            <Link href={routes.entity({ entity_type: 'company', slug: e.organization.slug })} className="hover:text-accent">174                              {e.organization.name}175                            </Link>176                          ) : str(a.venue) ? (177                            <span className="text-xs">{str(a.venue)}</span>178                          ) : (179                            <span className="text-ink-3">—</span>180                          )}181                        </Td>182                        <Td label="Published" className="tnum whitespace-nowrap text-ink-2">{str(a.published_at) ? fmtDate(str(a.published_at)) : <span className="text-ink-3">—</span>}</Td>183                        <Td label="Introduces">184                          {l.models.length ? (185                            <span className="flex flex-wrap gap-x-2 gap-y-0.5 text-sm">186                              {l.models.slice(0, 3).map((m) => (187                                <span key={m.id} className="inline-flex items-center gap-1">188                                  <EntityBadge type={m.entity_type} small />189                                  <EntityLink e={m} />190                                </span>191                              ))}192                              {l.models.length > 3 && <span className="tnum text-xs text-ink-3">+{l.models.length - 3}</span>}193                            </span>194                          ) : details[i] === null ? (195                            <span className="text-ink-3">unavailable</span>196                          ) : (197                            <span className="text-ink-3">—</span>198                          )}199                        </Td>200                        <Td label="Datasets" className="text-sm">{l.datasets.length ? l.datasets.slice(0, 2).map((x) => <EntityLink key={x.id} e={x} className="mr-2" />) : <span className="text-ink-3">—</span>}</Td>201                        <Td label="Benchmarks" className="text-sm">{l.benchmarks.length ? l.benchmarks.slice(0, 2).map((x) => <EntityLink key={x.id} e={x} className="mr-2" />) : <span className="text-ink-3">—</span>}</Td>202                        <Td label="Code" className="text-sm">203                          {l.code.length ? (204                            l.code.slice(0, 2).map((x) => <EntityLink key={x.id} e={x} className="mr-2" />)205                          ) : code ? (206                            <a href={code} target="_blank" rel="noopener noreferrer" className="link text-xs">207                              {code.replace(/^https?:\/\/(www\.)?/, '').slice(0, 32)}208                            </a>209                          ) : (210                            <span className="text-ink-3">—</span>211                          )}212                        </Td>213                      </tr>214                    );215                  })}216                </tbody>217              </DataTable>218              <Pagination total={page.total} limit={LIMIT} offset={offset} makeHref={(o) => href({ offset: o || undefined })} className="mt-4" />219              <Note className="mt-3">Author lists and categories are copied from the paper's own metadata (arXiv, publisher). Linked models, datasets, benchmarks and code come from stated relations only; a dash means no source stated one.</Note>220            </>221          )}222        </TerminalLayout>223      </div>224    </>225  );226}227