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%
15.2 KB · 273 lines tsx
Raw Blame History
1import { ExternalLink, FileText } from 'lucide-react';2import Link from 'next/link';3import { SourcesTable, TimelineList } from '@/components/entity/blocks';4import { ViewBeacon } from '@/components/layout/view-beacon';5import { BreadcrumbLd, Breadcrumbs } from '@/components/meta/breadcrumb-ld';6import { WatchButton } from '@/components/watchlist/watch-button';7import { Chip, EntityBadge } from '@/components/ui/badges';8import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';9import { EntityLink, QualityMark } from '@/components/ui/entity';10import { KeyValue } from '@/components/ui/key-value';11import { Container, Note, Section } from '@/components/ui/section';12import { EmptyState } from '@/components/ui/unavailable';13import { api, safe } from '@/lib/api';14import { fmtAgo, fmtDate, fmtInt, fmtParams, fmtTokens, num } from '@/lib/format';15import { routes, SITE_NAME, SITE_URL, typeLabel } from '@/lib/site';16import type { EntityDetail, EntitySummary } from '@/lib/types';1718/*19  Paper page (server, async). Sections: header (title, published, venue, arXiv, PDF/code) → abstract → authors (researcher20  pages when the `authored` relation names them) → organizations → models introduced (`described_by` inbound) → datasets →21  benchmarks → repositories → related papers (shared entities) → sources → timeline. Everything comes from the detail22  payload's relations; nothing is inferred beyond grouping by entity type.23*/2425function relItems(d: EntityDetail, types: string[], predicate?: string): EntitySummary[] {26  const out: EntitySummary[] = [];27  const seen = new Set<string>();28  for (const g of d.relations ?? []) {29    if (predicate && g.predicate !== predicate) continue;30    for (const it of g.items) {31      if (!types.includes(it.entity_type) || seen.has(it.id)) continue;32      seen.add(it.id);33      out.push(it);34    }35  }36  return out;37}3839function ModelsIntroduced({ items }: { items: EntitySummary[] }) {40  if (!items.length) return <EmptyState title="No model links this paper yet">Model pages link papers through their model cards and documentation; the relation is written only when a source states it.</EmptyState>;41  return (42    <DataTable caption="Models introduced or described" compact>43      <thead>44        <tr>45          <Th>Model</Th>46          <Th>Type</Th>47          <Th>Organization</Th>48          <Th num>Params</Th>49          <Th num>Context</Th>50          <Th>Released</Th>51        </tr>52      </thead>53      <tbody>54        {items.map((m) => {55          const a = m.attributes ?? {};56          return (57            <tr key={m.id}>58              <Td primary>59                <EntityLink e={m} />60              </Td>61              <Td label="Type">62                <EntityBadge type={m.entity_type} small />63              </Td>64              <Td label="Organization" className="text-ink-2">{m.organization?.name ?? '—'}</Td>65              <Td num label="Params" className="tnum">{fmtParams(a.parameter_count)}</Td>66              <Td num label="Context" className="tnum">{num(a.context_length) === null ? '—' : fmtTokens(a.context_length)}</Td>67              <Td label="Released" className="tnum text-ink-2">{typeof a.release_date === 'string' ? fmtDate(a.release_date) : '—'}</Td>68            </tr>69          );70        })}71      </tbody>72    </DataTable>73  );74}7576function List({ items, empty }: { items: EntitySummary[]; empty: string }) {77  if (!items.length) return <p className="text-sm text-ink-3">{empty}</p>;78  return (79    <ul className="divide-y divide-rule border-y border-rule">80      {items.map((e) => (81        <li key={e.id} className="flex items-center gap-2 py-2 text-sm">82          <EntityBadge type={e.entity_type} small />83          <EntityLink e={e} className="truncate" />84          {e.organization && <span className="ml-auto shrink-0 text-xs text-ink-3">{e.organization.name}</span>}85        </li>86      ))}87    </ul>88  );89}9091export async function PaperPage({ d, canonical }: { d: EntityDetail; canonical: string }) {92  const a = d.attributes ?? {};93  const authors: string[] = Array.isArray(a.authors) ? (a.authors as unknown[]).map(String) : [];94  const researchers = relItems(d, ['researcher'], 'authored');95  const byName = new Map(researchers.map((r) => [r.name.toLowerCase(), r]));96  const models = relItems(d, ['model', 'artifact', 'quantization']);97  const datasets = relItems(d, ['dataset']);98  const benchmarks = relItems(d, ['benchmark']);99  const repos = [...relItems(d, ['repository', 'framework', 'library']), ...(d.repositories ?? [])].filter((x, i, arr) => arr.findIndex((y) => y.id === x.id) === i);100  const orgs = relItems(d, ['company', 'organization', 'lab', 'university']);101  const related = await safe(api.entityRelated(d.slug, 12));102  const relatedPapers = (related?.items ?? []).filter((r) => r.entity_type === 'paper' && r.id !== d.id);103  const relatedOther = (related?.items ?? []).filter((r) => r.entity_type !== 'paper' && r.id !== d.id);104  const arxiv = typeof a.arxiv_id === 'string' ? a.arxiv_id : d.identifiers?.find((i) => i.scheme === 'arxiv')?.value;105  const pdf = typeof a.pdf_url === 'string' ? a.pdf_url : arxiv ? `https://arxiv.org/pdf/${arxiv}` : null;106  const code = typeof a.code_url === 'string' ? a.code_url : null;107  const cats = [...new Set([...(typeof a.primary_category === 'string' ? [a.primary_category] : []), ...(Array.isArray(a.categories) ? (a.categories as unknown[]).map(String) : [])])];108  const crumbs = [{ name: SITE_NAME, href: '/' }, { name: 'Research', href: '/papers' }, { name: d.name, href: canonical }];109  const ld = {110    '@context': 'https://schema.org',111    '@type': 'ScholarlyArticle',112    headline: d.name,113    name: d.name,114    url: `${SITE_URL}${canonical}`,115    datePublished: typeof a.published_at === 'string' ? a.published_at : undefined,116    author: authors.slice(0, 30).map((n) => ({ '@type': 'Person', name: n })),117    abstract: typeof a.abstract === 'string' ? a.abstract : undefined,118    sameAs: [pdf, arxiv ? `https://arxiv.org/abs/${arxiv}` : null].filter(Boolean),119    identifier: arxiv ? { '@type': 'PropertyValue', propertyID: 'arxiv', value: arxiv } : undefined,120    publisher: d.organization ? { '@type': 'Organization', name: d.organization.name } : undefined,121  };122  const specRows = ['published_at', 'venue', 'doi', 'arxiv_id', 'primary_category', 'pdf_url', 'code_url', 'updated_at'].filter((k) => a[k] !== undefined && a[k] !== null && a[k] !== '').map((k) => ({ key: k, raw: a[k] }));123124  return (125    <Container wide>126      <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }} />127      <BreadcrumbLd items={crumbs} />128      <ViewBeacon path={canonical} />129      <Breadcrumbs items={crumbs} />130      <header className="pb-6 pt-4 md:pb-8 md:pt-5">131        <div className="flex flex-wrap items-center gap-2">132          <EntityBadge type="paper" />133          {cats.map((c) => (134            <Link key={c} href={`/papers?category=${encodeURIComponent(c)}`}>135              <Chip tone={c === a.primary_category ? 'accent' : 'neutral'} className="mono">{c}</Chip>136            </Link>137          ))}138        </div>139        <div className="mt-3 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">140          <div className="min-w-0">141            <h1 className="display text-[26px] leading-tight md:text-[36px]">{d.name}</h1>142            <p className="tnum mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[15px] text-ink-2">143              {typeof a.published_at === 'string' && <span>Published {fmtDate(a.published_at)}</span>}144              {typeof a.venue === 'string' && <span>· {a.venue}</span>}145              {arxiv && (146                <a href={`https://arxiv.org/abs/${arxiv}`} target="_blank" rel="noopener noreferrer" className="mono inline-flex items-center gap-1 text-ink-3 hover:text-accent">147                  arXiv:{arxiv} <ExternalLink className="size-3.5" aria-hidden />148                </a>149              )}150            </p>151            <div className="mt-4 flex flex-wrap items-center gap-2" aria-label="Actions">152              {pdf && (153                <a href={pdf} target="_blank" rel="noopener noreferrer" className="inline-flex h-9 items-center gap-1.5 border border-rule px-2.5 text-sm text-ink-2 hover:border-rule-strong hover:text-ink">154                  <FileText className="size-3.5" aria-hidden /> PDF155                </a>156              )}157              {code && (158                <a href={code} target="_blank" rel="noopener noreferrer" className="inline-flex h-9 items-center gap-1.5 border border-rule px-2.5 text-sm text-ink-2 hover:border-rule-strong hover:text-ink">159                  Code <ExternalLink className="size-3.5" aria-hidden />160                </a>161              )}162              <WatchButton e={d} />163              <Link href={`/graph/${encodeURIComponent(d.slug)}?mode=research`} className="inline-flex h-9 items-center gap-1.5 border border-rule px-2.5 text-sm text-ink-2 hover:border-rule-strong hover:text-ink">164                Research graph165              </Link>166            </div>167          </div>168          <div className="shrink-0 text-xs text-ink-3 lg:text-right">169            <QualityMark q={d.quality?.score} label />170            <p className="mt-1" title={d.updated_at}>171              Updated {fmtAgo(d.updated_at)} · first seen {fmtDate(d.first_seen_at)}172            </p>173            <p className="mono mt-0.5 text-[11px]">{d.id}</p>174          </div>175        </div>176      </header>177178      <div className="grid gap-10 pb-16 lg:grid-cols-[minmax(0,1fr)_22rem]">179        <div className="min-w-0">180          {typeof a.abstract === 'string' && (181            <Section id="abstract" eyebrow="Abstract" hairline={false} className="pt-0">182              <p className="prose-atlas max-w-3xl text-[15px] leading-relaxed text-ink-2">{a.abstract}</p>183            </Section>184          )}185          <Section id="authors" eyebrow="Authors" title={<>Authors <span className="tnum text-base font-normal text-ink-3">{fmtInt(authors.length || researchers.length)}</span></>}>186            {authors.length === 0 && researchers.length === 0 ? (187              <p className="text-sm text-ink-3">Author list not stated by the source.</p>188            ) : (189              <p className="flex flex-wrap gap-x-3 gap-y-1 text-sm">190                {(authors.length ? authors : researchers.map((r) => r.name)).map((n, i) => {191                  const r = byName.get(n.toLowerCase());192                  return r ? (193                    <Link key={`${n}-${i}`} href={routes.entity(r)} className="text-ink hover:text-accent hover:underline">194                      {n}195                    </Link>196                  ) : (197                    <span key={`${n}-${i}`} className="text-ink-2">198                      {n}199                    </span>200                  );201                })}202              </p>203            )}204            <Note className="mt-2">Linked names open researcher pages (created from the paper's author list; name-only, no affiliation unless a source states it). Unlinked names have no researcher record yet.</Note>205          </Section>206          <Section id="organizations" eyebrow="Organizations" title={<>Organizations <span className="tnum text-base font-normal text-ink-3">{fmtInt(orgs.length + (d.organization ? 1 : 0))}</span></>}>207            {d.organization || orgs.length ? (208              <List items={[...(d.organization ? [{ id: d.organization.id, entity_type: 'company', slug: d.organization.slug, name: d.organization.name, description: null, status: 'active', organization: null, attributes: {}, quality: {}, counts: {}, first_seen_at: '', last_seen_at: '', updated_at: '' } as EntitySummary] : []), ...orgs]} empty="" />209            ) : (210              <p className="text-sm text-ink-3">No organization stated. arXiv metadata does not carry affiliations; an organization is linked only when a model card or lab page cites the paper.</p>211            )}212          </Section>213          <Section id="models" eyebrow="Models" title={<>Models introduced or described <span className="tnum text-base font-normal text-ink-3">{fmtInt(models.length)}</span></>} lede="Inbound described_by relations from model cards and documentation.">214            <ModelsIntroduced items={models} />215          </Section>216          <div className="grid gap-8 md:grid-cols-2">217            <Section id="datasets" eyebrow="Datasets" title={<>Datasets used <span className="tnum text-base font-normal text-ink-3">{fmtInt(datasets.length)}</span></>}>218              <List items={datasets} empty="No dataset relation recorded." />219            </Section>220            <Section id="benchmarks" eyebrow="Benchmarks" title={<>Benchmarks used <span className="tnum text-base font-normal text-ink-3">{fmtInt(benchmarks.length)}</span></>}>221              <List items={benchmarks} empty="No benchmark relation recorded." />222            </Section>223          </div>224          <Section id="repositories" eyebrow="Code" title={<>Repositories & frameworks <span className="tnum text-base font-normal text-ink-3">{fmtInt(repos.length)}</span></>}>225            <List items={repos} empty="No repository linked." />226          </Section>227          <Section id="related" eyebrow="Related" title={<>Related papers <span className="tnum text-base font-normal text-ink-3">{fmtInt(relatedPapers.length)}</span></>} lede="Papers sharing an organization, a family or a relation with this one.">228            <List items={relatedPapers} empty="No related paper found through shared entities." />229            {relatedOther.length > 0 && (230              <>231                <p className="eyebrow mb-1.5 mt-5">Other related entities</p>232                <List items={relatedOther} empty="" />233              </>234            )}235          </Section>236          <Section id="timeline" eyebrow="Timeline" title={<>Timeline <span className="tnum text-base font-normal text-ink-3">{fmtInt(d.timeline?.length ?? 0)}</span></>}>237            <TimelineList events={d.timeline ?? []} slug={d.slug} />238          </Section>239          <Section id="sources" eyebrow="Sources" title={<>Sources <span className="tnum text-base font-normal text-ink-3">{fmtInt(d.sources?.length ?? 0)}</span></>}>240            <SourcesTable sources={d.sources ?? []} />241          </Section>242        </div>243        <aside className="min-w-0 space-y-8">244          <section>245            <p className="eyebrow mb-2">Record</p>246            <KeyValue rows={specRows} provenance={d.provenance} slug={d.slug} entity={{ name: d.name, entity_type: d.entity_type }} dense />247            <p className="mono mt-2 break-all text-[11px] text-ink-3">slug {d.slug}</p>248          </section>249          {d.identifiers?.length > 0 && (250            <section>251              <p className="eyebrow mb-2">Identifiers</p>252              <ul className="space-y-0.5 text-xs">253                {d.identifiers.map((i) => (254                  <li key={`${i.scheme}:${i.value}`} className="flex gap-2">255                    <span className="mono text-ink-3">{i.scheme}</span>256                    <span className="mono truncate text-ink-2">{i.value}</span>257                  </li>258                ))}259              </ul>260            </section>261          )}262          <section>263            <p className="eyebrow mb-2">Type</p>264            <p className="text-sm text-ink-2">265              {typeLabel(d.entity_type)} · <Link href={routes.methodology()} className="link">how papers are recorded</Link>266            </p>267          </section>268        </aside>269      </div>270    </Container>271  );272}273