spb/autoscholar
Public
1import { listSeries, searchPapers } from "@autoscholar/core";2import { PublicSearchQuery } from "@autoscholar/schemas";3import type { Metadata } from "next";4import { Disclosure } from "@/components/Disclosure";5import { EmptyResearchState, EmptyState } from "@/components/EmptyState";6import { FilterSidebar, LibraryFilters } from "@/components/LibraryFilters";7import { Pagination } from "@/components/Pagination";8import { PaperCard } from "@/components/PaperCard";9import { t } from "@/lib/i18n";10import { db } from "@/lib/server/db";1112export const dynamic = "force-dynamic";13export const metadata: Metadata = {14 title: "Research library",15 description: "Searchable library of AutoScholar working papers.",16 alternates: { canonical: "/research" },17};1819export default async function ResearchPage({20 searchParams,21}: {22 searchParams: Promise<Record<string, string | string[] | undefined>>;23}) {24 const sp = await searchParams;25 const raw: Record<string, string | undefined> = {};26 for (const [k, v] of Object.entries(sp)) raw[k] = Array.isArray(v) ? v[0] : v;27 for (const k of Object.keys(raw)) if (raw[k] === "") delete raw[k];28 const parsed = PublicSearchQuery.safeParse(raw);29 const q = parsed.success ? parsed.data : PublicSearchQuery.parse({});30 const d = db();31 const res = await searchPapers(d, q);32 const series = await listSeries(d);33 const hrefFor = (p: number) => {34 const u = new URLSearchParams();35 for (const [k, v] of Object.entries(raw)) if (v && k !== "page") u.set(k, v);36 u.set("page", String(p));37 return `/research?${u.toString()}`;38 };39 const anyFilter = Object.keys(raw).some((k) => !["page", "sort", "pageSize"].includes(k));40 return (41 <div className="container-x section-tight">42 <header className="max-w-2xl">43 <p className="eyebrow">Library</p>44 <h1 className="title-1 mt-2">Working papers</h1>45 <p className="muted mt-2 text-sm leading-relaxed">46 Search titles, abstracts, keywords and verified metadata. Every entry is an AI-generated working47 paper with its evidence attached.48 </p>49 </header>50 {!parsed.success && (51 <p role="alert" className="mt-4 text-sm" style={{ color: "var(--color-danger-500)" }}>52 Some filters were invalid and have been ignored.53 </p>54 )}55 <div className="mt-6 grid gap-8 lg:grid-cols-12">56 <div className="lg:col-span-9 lg:col-start-4 lg:row-start-1">57 <LibraryFilters q={raw} series={series} total={res.total} />58 </div>59 <aside60 className="hidden lg:col-span-3 lg:col-start-1 lg:row-span-2 lg:row-start-1 lg:block"61 aria-label="Filters"62 >63 <FilterSidebar q={raw} series={series} />64 </aside>65 <div className="lg:col-span-9 lg:col-start-4">66 {res.items.length === 0 ? (67 anyFilter ? (68 <EmptyState title={t("search.empty")}>69 <a href="/research">Clear all filters</a>70 </EmptyState>71 ) : (72 <EmptyResearchState />73 )74 ) : (75 <ul className="border-t hairline">76 {res.items.map((p, i) => (77 <li key={p.publicId}>78 <PaperCard p={p} variant="row" index={i} />79 </li>80 ))}81 </ul>82 )}83 <div className="mt-6 flex items-center justify-between gap-4">84 <Pagination page={res.page} pageSize={res.pageSize} total={res.total} hrefFor={hrefFor} />85 </div>86 <div className="mt-8">87 <Disclosure compact />88 </div>89 </div>90 </div>91 </div>92 );93}94