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.7 KB · 150 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { ActiveFilters, type FacetGroup, Facets, FilterBar, ListingLayout } from '@/components/listing/filters';4import { BreadcrumbLd } from '@/components/meta/breadcrumb-ld';5import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';6import { EntityBadge } from '@/components/ui/badges';7import { EntityLink, QualityMark } from '@/components/ui/entity';8import { Hint } from '@/components/ui/hint';9import { Pagination, withParams } from '@/components/ui/pagination';10import { Container, PageHeader } from '@/components/ui/section';11import { Unavailable } from '@/components/ui/unavailable';12import { api, safe } from '@/lib/api';13import { fmtInt, num, titleCase } from '@/lib/format';14import { routes, SITE_NAME } from '@/lib/site';1516export const metadata: Metadata = {17  title: 'Organizations — companies, labs and universities building AI',18  description: 'Every organization in the atlas — companies, research labs, universities — with country, kind, canonical model count and papers, each attribute backed by a source.',19  alternates: { canonical: '/companies' },20};21export const revalidate = 300;2223type SP = Record<string, string | undefined>;24const LIMIT = 50;25const KEYS = ['q', 'country', 'kind', 'sort', 'order', 'offset'] as const;26const SORTS = [27  { value: 'models', label: 'Most models' },28  { value: 'name', label: 'Name' },29  { value: 'updated', label: 'Recently updated' },30  { value: 'quality', label: 'Data quality' },31];3233export default async function CompaniesPage({ searchParams }: { searchParams: Promise<SP> }) {34  const sp = await searchParams;35  const current: Record<string, string | undefined> = {};36  for (const k of KEYS) if (sp[k]) current[k] = sp[k];37  const offset = Math.max(0, Number(current.offset) || 0);38  const sort = current.sort ?? 'models';39  const [page, stats] = await Promise.all([safe(api.companies({ ...current, sort, limit: LIMIT, offset, facets: 1 })), safe(api.stats())]);40  const href = (patch: Record<string, string | number | undefined | null>) => withParams('/companies', current, patch);41  const facetGroups: FacetGroup[] = [42    { key: 'country', label: 'Country', items: (page?.facets?.countries ?? []).map((x) => ({ value: x.value, count: x.count })) },43    { key: 'kind', label: 'Kind', items: (page?.facets?.kinds ?? []).map((x) => ({ value: x.value, label: titleCase(x.value), count: x.count })) },44  ];45  const sortHref = (s: string) => href({ sort: s, order: sort === s && current.order !== 'asc' ? 'asc' : undefined, offset: undefined });46  const definition = stats?.definitions?.organizations_total ?? 'Organizations = companies, labs and universities (entity types company, organization, lab, university); merged duplicates excluded.';47  const SortTh = ({ s, children, num: n }: { s: string; children: React.ReactNode; num?: boolean }) => (48    <Th num={n} aria-sort={sort === s ? (current.order === 'asc' ? 'ascending' : 'descending') : undefined}>49      <Link href={sortHref(s)} className={sort === s ? 'text-ink' : 'hover:text-ink'}>50        {children}51        {sort === s && <span aria-hidden> {current.order === 'asc' ? '↑' : '↓'}</span>}52      </Link>53    </Th>54  );5556  return (57    <Container wide>58      <BreadcrumbLd items={[{ name: SITE_NAME, href: '/' }, { name: 'Organizations', href: '/companies' }]} />59      <PageHeader60        eyebrow="Organizations"61        title="Companies, labs & universities"62        lede="Who builds, serves and studies AI. Model counts are canonical releases; paper counts are stated publisher relations. Every organization page separates corporate news from model events."63        aside={64          page ? (65            <p className="tnum flex items-center gap-1 text-sm text-ink-3">66              {fmtInt(page.total)} organizations <span className="text-ink-3">(companies, labs, universities)</span>67              <Hint text={definition} align="right" />68            </p>69          ) : undefined70        }71      >72        <FilterBar73          action="/companies"74          className="mt-6"75          resetHref={routes.companies()}76          fields={[77            { kind: 'text', name: 'q', label: 'Name', value: current.q, placeholder: 'e.g. Anthropic' },78            { kind: 'text', name: 'country', label: 'Country (ISO-2)', value: current.country, placeholder: 'US, FR, CN…' },79            ...(current.kind ? [{ kind: 'hidden' as const, name: 'kind', value: current.kind }] : []),80          ]}81          sort={{ value: sort, options: SORTS }}82        />83        <ActiveFilters current={current} labels={{ q: 'query', country: 'country', kind: 'kind' }} makeHref={(p) => href(p)} className="mt-3" />84      </PageHeader>85      <div className="pb-16">86        <ListingLayout facets={<Facets groups={facetGroups} current={current} makeHref={(p) => href(p)} />}>87          {!page ? (88            <Unavailable what="Organizations" />89          ) : (90            <>91              <DataTable caption="Organizations">92                <thead>93                  <tr>94                    <SortTh s="name">Organization</SortTh>95                    <Th>Type</Th>96                    <Th>Country</Th>97                    <Th>Kind</Th>98                    <Th>Founded</Th>99                    <SortTh s="models" num>100                      Models101                    </SortTh>102                    <Th num>Papers</Th>103                    <SortTh s="quality" num>104                      Quality105                    </SortTh>106                  </tr>107                </thead>108                <tbody>109                  {page.items.length === 0 && <EmptyRow cols={8}>No organizations match these filters.</EmptyRow>}110                  {page.items.map((c) => {111                    const a = c.attributes ?? {};112                    return (113                      <tr key={c.id}>114                        <Td primary>115                          <EntityLink e={c} />116                          {c.description && <span className="block max-w-md truncate text-xs text-ink-3">{c.description}</span>}117                        </Td>118                        <Td label="Type">119                          <EntityBadge type={c.entity_type} small />120                        </Td>121                        <Td label="Country" className="mono text-ink-2">{typeof a.country === 'string' ? a.country : <span className="text-ink-3">—</span>}</Td>122                        <Td label="Kind" className="text-ink-2">{typeof a.org_kind === 'string' ? titleCase(a.org_kind) : <span className="text-ink-3">—</span>}</Td>123                        <Td label="Founded" className="tnum text-ink-2">{a.founded ? String(a.founded).slice(0, 4) : <span className="text-ink-3">—</span>}</Td>124                        <Td num label="Models" className="tnum">125                          {num(c.model_count) ? (126                            <Link href={`/models?org=${encodeURIComponent(c.slug)}`} className="hover:text-accent">127                              {fmtInt(c.model_count)}128                            </Link>129                          ) : (130                            <span className="text-ink-3">0</span>131                          )}132                        </Td>133                        <Td num label="Papers" className="tnum">{num(c.paper_count) ? fmtInt(c.paper_count) : <span className="text-ink-3">0</span>}</Td>134                        <Td num label="Quality">135                          <QualityMark q={c.quality?.score} />136                        </Td>137                      </tr>138                    );139                  })}140                </tbody>141              </DataTable>142              <Pagination total={page.total} limit={LIMIT} offset={offset} makeHref={(o) => href({ offset: o || undefined })} className="mt-4" />143            </>144          )}145        </ListingLayout>146      </div>147    </Container>148  );149}150