HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import { Search, X } from 'lucide-react';2import type { Metadata } from 'next';3import Link from 'next/link';4import { Chip, EntityBadge, OpennessBadge } from '@/components/ui/badges';5import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';6import { cheapestPrice, EntityLink, EntityRow, QualityMark } from '@/components/ui/entity';7import { Hint } from '@/components/ui/hint';8import { Pagination, withParams } from '@/components/ui/pagination';9import { Container, Note, PageHeader } from '@/components/ui/section';10import { EmptyState, Unavailable } from '@/components/ui/unavailable';11import { apiD3, safe } from '@/lib/api';12import { fmtDate, fmtInt, fmtParams, fmtTokens, num } from '@/lib/format';13import { EXAMPLE_QUERIES, exploreNav, PALETTE_PREFIXES, routes, typeLabel } from '@/lib/site';14import type { EntitySummary } from '@/lib/types';15import { compiledType, impliedColumns, toModelsParams, withoutSpan } from './compiled';1617type SP = { q?: string; type?: string; offset?: string };18const LIMIT = 30;1920export async function generateMetadata({ searchParams }: { searchParams: Promise<SP> }): Promise<Metadata> {21 const { q } = await searchParams;22 return { title: q ? `“${q}” — search` : 'Search', description: 'Plain-English search over the AI Atlas graph: the compiler turns your words into filters (organization, parameters, context, openness, licence, dates, price) and shows them back as removable chips.', robots: { index: false, follow: true }, alternates: { canonical: '/search' } };23}2425export default async function SearchPage({ searchParams }: { searchParams: Promise<SP> }) {26 const sp = await searchParams;27 const q = (sp.q ?? '').trim();28 const type = sp.type ?? '';29 const offset = Math.max(0, Number(sp.offset) || 0);30 const res = q ? await safe(apiD3.search(q, { type: type || undefined, limit: LIMIT, offset })) : null;31 const query = res?.query;32 const compiled = query?.compiled ?? [];33 const cType = compiledType(query);34 const isModelTable = (type || cType) === 'model' && compiled.length > 0;35 const mapped = isModelTable ? toModelsParams(compiled, query?.residual) : null;36 const cols = impliedColumns(compiled, query?.sort);37 const current = { q, type };38 const unrec = query?.unrecognised ?? [];39 const exploreHref = mapped ? `/explore?type=model&${mapped.params.toString()}` : `/explore${cType ? `?type=${encodeURIComponent(cType)}` : ''}`;4041 return (42 <Container wide>43 <PageHeader eyebrow="Search 3.0" title={q ? <>Results for <span className="text-ink-2">“{q}”</span></> : 'Search the atlas'} lede={!q ? 'Names, ids, providers, benchmarks — or a plain-English question. The compiler turns your words into filters and shows them back as chips you can remove.' : undefined}>44 <form action="/search" method="get" role="search" className="mt-5 flex max-w-3xl items-stretch border border-rule-strong bg-surface focus-within:border-accent">45 <span className="flex items-center pl-3 text-ink-3">46 <Search className="size-5" aria-hidden />47 </span>48 <input name="q" type="search" defaultValue={q} placeholder="open reasoning models over 30B released in 2026…" className="h-12 min-w-0 flex-1 bg-transparent px-3 text-[16px] text-ink placeholder:text-ink-3 focus:outline-none" autoComplete="off" aria-label="Search query" />49 {type && <input type="hidden" name="type" value={type} />}50 <button type="submit" className="bg-ink px-4 text-sm font-medium text-canvas hover:opacity-90">51 Search52 </button>53 </form>5455 {q && res && (56 <section className="mt-4 border-y border-rule py-3" aria-label="Compiled query" data-compiled>57 <div className="flex flex-wrap items-start gap-x-6 gap-y-2">58 <div className="min-w-0 flex-1">59 <p className="eyebrow mb-1.5 flex items-center gap-1">60 Compiled as61 <Hint align="right" text="The search compiler (v2) recognises organizations, parameter and context bounds, openness, licences, dates, prices, modalities and sort hints. Each chip shows the words it came from; removing a chip re-runs the search without those words." />62 {query?.semantic && <span className="ml-2 normal-case tracking-normal text-ink-3">· semantic ranking on</span>}63 </p>64 {compiled.length === 0 ? (65 <p className="text-sm text-ink-2">66 No structured filter recognised — full-text search for “{q}”{cType ? ` among ${typeLabel(cType, true).toLowerCase()}` : ''}.67 </p>68 ) : (69 <ul className="flex flex-wrap gap-1.5" data-compiled-chips>70 {compiled.map((f, i) => {71 const nextText = withoutSpan(q, f.source_span);72 const removable = !!f.source_span && nextText !== q;73 return (74 <li key={`${f.filter}-${i}`} className="inline-flex h-8 items-stretch border border-rule bg-surface text-xs" data-compiled-chip={f.filter}>75 <span className="flex items-center gap-1.5 px-2 text-ink" title={f.source_span ? `from “${f.source_span}”` : undefined}>76 <span className="mono text-[10px] uppercase text-ink-3">{f.filter}</span>77 {f.label}78 </span>79 {removable ? (80 <Link href={nextText ? withParams('/search', current, { q: nextText, offset: undefined }) : routes.search('')} className="flex items-center border-l border-rule px-1.5 text-ink-3 hover:bg-surface-2 hover:text-danger" aria-label={`Remove ${f.label}`} title="Remove this filter (re-runs the search without its words)">81 <X className="size-3" aria-hidden />82 </Link>83 ) : (84 <span className="flex items-center border-l border-rule px-1.5 text-ink-3" title="Implicit — not tied to words in your query">85 ·86 </span>87 )}88 </li>89 );90 })}91 </ul>92 )}93 <p className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-ink-3">94 {query?.sort && (95 <span>96 Sort: <span className="text-ink-2">{query.sort}</span>97 </span>98 )}99 {query?.residual?.trim() && (100 <span>101 Free text: <span className="text-ink-2">“{query.residual.trim()}”</span>102 </span>103 )}104 {unrec.length > 0 && (105 <span data-unrecognised>106 Not understood: <span className="text-warning">{unrec.join(' · ')}</span>107 <Hint align="right" text="Words the compiler could not turn into a filter (an organization name it does not know, a benchmark alias…) stay as free text and are matched against names and descriptions." />108 </span>109 )}110 {query?.note && <span className="text-warning">{query.note}</span>}111 </p>112 </div>113 <div className="flex flex-wrap items-center gap-2 text-xs">114 {mapped && (115 <Link href={`/models?${mapped.params.toString()}`} className="inline-flex h-9 items-center border border-rule px-2.5 text-ink-2 hover:border-rule-strong hover:text-ink">116 Open in Models →117 </Link>118 )}119 <Link href={exploreHref} className="inline-flex h-9 items-center border border-rule px-2.5 text-ink-2 hover:border-rule-strong hover:text-ink" data-open-builder>120 Open in Explore builder →121 </Link>122 {mapped?.links.map((l) => (123 <Link key={l.href} href={l.href} className="link">124 {l.label}125 </Link>126 ))}127 </div>128 </div>129 {mapped && mapped.unmapped.length > 0 && (130 <Note className="mt-2">131 Not expressible in /models: {mapped.unmapped.map((u, i) => (132 <span key={i}>133 {i > 0 && ' · '}134 <span className="text-ink-2">{u.filter.label}</span> ({u.why})135 </span>136 ))}137 .138 </Note>139 )}140 </section>141 )}142143 {q && (144 <ul className="no-scrollbar -mx-4 mt-4 flex gap-1 overflow-x-auto px-4 md:mx-0 md:px-0" aria-label="Filter by type">145 {[{ label: 'All', type: '' }, ...exploreNav.map((n) => ({ label: n.label.replace(' & Pricing', ''), type: n.type }))].map((t) => {146 const on = t.type === type;147 return (148 <li key={t.type} className="shrink-0">149 <Link href={withParams('/search', current, { type: t.type || undefined, offset: undefined })} className={`inline-flex h-9 items-center border px-3 text-sm ${on ? 'border-ink bg-ink text-canvas' : 'border-rule text-ink-2 hover:border-rule-strong hover:text-ink'}`} aria-current={on ? 'true' : undefined}>150 {t.label}151 </Link>152 </li>153 );154 })}155 </ul>156 )}157 </PageHeader>158159 {!q ? (160 <div className="grid gap-10 pb-16 md:grid-cols-2">161 <div>162 <p className="eyebrow mb-2">Try</p>163 <ul className="flex flex-wrap gap-2">164 {[...EXAMPLE_QUERIES, 'open reasoning models over 30B released in 2026', 'cheapest models under $1/M output with 1M context', 'papers by DeepSeek'].map((ex) => (165 <li key={ex}>166 <Link href={routes.search(ex)} className="inline-block border border-rule px-2.5 py-1.5 text-sm text-ink-2 hover:border-rule-strong hover:text-ink">167 {ex}168 </Link>169 </li>170 ))}171 </ul>172 </div>173 <div>174 <p className="eyebrow mb-2">Prefixes (also in the ⌘K palette)</p>175 <ul className="space-y-1 text-sm">176 {Object.entries(PALETTE_PREFIXES).map(([k, v]) => (177 <li key={k} className="flex items-center gap-3">178 <span className="mono w-8 text-ink">{k}:</span>179 <Link href={withParams('/search', {}, { type: v.types[0] })} className="text-ink-2 hover:text-ink">180 {v.label}181 </Link>182 </li>183 ))}184 </ul>185 <p className="mt-3 text-xs text-ink-3">186 Prefer a form? <Link href={routes.explore()} className="link">Open the Explore builder →</Link>187 </p>188 </div>189 </div>190 ) : !res ? (191 <Unavailable what="Search" className="mb-16" reason="The search service did not answer (rate limit or outage)." />192 ) : res.items.length === 0 ? (193 <EmptyState title={`Nothing matched “${q}”`} className="mb-16">194 Try fewer words, a provider's API id, or <Link href={routes.explore()} className="link">the builder</Link>.195 {compiled.length > 0 && (196 <>197 {' '}198 Remove a chip above to widen the search.199 </>200 )}201 </EmptyState>202 ) : (203 <div className="pb-16">204 <p className="tnum mb-2 text-xs text-ink-3">205 {fmtInt(res.total)} result{res.total === 1 ? '' : 's'}206 {isModelTable ? ' · dense model table (columns implied by the query)' : ''}207 </p>208 {isModelTable ? <ModelResults items={res.items} cols={cols} /> : (209 <ul className="border-t border-rule">210 {res.items.map((e) => (211 <EntityRow key={e.id} e={e} trailing={<EntityBadge type={e.entity_type} small className="md:hidden" />} />212 ))}213 </ul>214 )}215 <Pagination total={res.total} limit={LIMIT} offset={offset} makeHref={(o) => withParams('/search', current, { offset: o || undefined })} className="mt-4" />216 </div>217 )}218 </Container>219 );220}221222function ModelResults({ items, cols }: { items: (EntitySummary & { rank: number })[]; cols: Set<string> }) {223 return (224 <DataTable caption="Models matching the compiled query" compact scroll>225 <thead>226 <tr>227 <Th>Model</Th>228 <Th>Organization</Th>229 {cols.has('params') && <Th num>Params</Th>}230 {cols.has('context') && <Th num>Context</Th>}231 {cols.has('price') && <Th num>Cheapest input</Th>}232 {cols.has('openness') && <Th>Openness · licence</Th>}233 {cols.has('reasoning') && <Th>Reasoning</Th>}234 {cols.has('modalities') && <Th>Modalities</Th>}235 {cols.has('released') && <Th>Released</Th>}236 <Th num>Quality</Th>237 </tr>238 </thead>239 <tbody>240 {items.length === 0 && <EmptyRow cols={10}>No rows.</EmptyRow>}241 {items.map((e) => {242 const a = e.attributes ?? {};243 const ap = num(a.active_parameter_count);244 const p = num(a.parameter_count);245 return (246 <tr key={e.id}>247 <Td primary>248 <EntityLink e={e} />249 {e.entity_type !== 'model' && <EntityBadge type={e.entity_type} small className="ml-2" />}250 </Td>251 <Td label="Organization" className="text-ink-2">252 {e.organization ? <Link href={routes.entity({ entity_type: 'company', slug: e.organization.slug })} className="hover:text-accent">{e.organization.name}</Link> : <span className="text-ink-3">—</span>}253 </Td>254 {cols.has('params') && (255 <Td num label="Params" className="tnum">256 {p === null ? <span className="text-ink-3">—</span> : ap !== null && ap !== p ? `${fmtParams(p)} · ${fmtParams(ap)} active` : fmtParams(p)}257 </Td>258 )}259 {cols.has('context') && <Td num label="Context" className="tnum">{num(a.context_length) === null ? <span className="text-ink-3">—</span> : fmtTokens(a.context_length)}</Td>}260 {cols.has('price') && <Td num label="Cheapest input" className="tnum text-accent-2">{cheapestPrice(e) ?? <span className="text-ink-3">—</span>}</Td>}261 {cols.has('openness') && (262 <Td label="Openness">263 <span className="inline-flex flex-wrap items-center gap-1">264 {typeof a.openness === 'string' ? <OpennessBadge openness={a.openness} /> : <span className="text-ink-3">—</span>}265 {typeof a.license === 'string' && <Chip>{a.license}</Chip>}266 </span>267 </Td>268 )}269 {cols.has('reasoning') && <Td label="Reasoning" className="text-ink-2">{a.reasoning === true ? 'yes' : a.reasoning === false ? 'no' : <span className="text-ink-3">—</span>}</Td>}270 {cols.has('modalities') && <Td label="Modalities" className="text-xs text-ink-2">{Array.isArray(a.modalities) && a.modalities.length ? (a.modalities as unknown[]).map(String).join(', ') : <span className="text-ink-3">—</span>}</Td>}271 {cols.has('released') && <Td label="Released" className="tnum text-ink-2">{typeof a.release_date === 'string' ? fmtDate(a.release_date) : <span className="text-ink-3">—</span>}</Td>}272 <Td num label="Quality">273 <QualityMark q={e.quality?.score} />274 </Td>275 </tr>276 );277 })}278 </tbody>279 </DataTable>280 );281}282