spb/autoscholar
Public
1import { artifacts, researchRuns, reviewFindings } from "@autoscholar/db";2import { and, desc, eq, inArray } from "drizzle-orm";3import Link from "next/link";4import { StatusBadge } from "@/components/StatusBadge";5import { fmtDateTime } from "@/lib/format";6import { db } from "@/lib/server/db";78export const dynamic = "force-dynamic";910export default async function ManuscriptsPage({ searchParams }: { searchParams: Promise<{ run?: string }> }) {11 const { run: selected } = await searchParams;12 const d = db();13 const rows = await d14 .selectDistinctOn([researchRuns.id], { run: researchRuns, pdf: artifacts })15 .from(artifacts)16 .innerJoin(researchRuns, eq(artifacts.runId, researchRuns.id))17 .where(eq(artifacts.path, "manuscript/main.pdf"))18 .orderBy(researchRuns.id, desc(artifacts.version));19 rows.sort((a, b) => b.run.updatedAt.getTime() - a.run.updatedAt.getTime());20 const cur = rows.find((r) => r.run.id === selected) ?? rows[0];21 let meta: Record<string, unknown> | null = null;22 let review: Record<string, unknown> | null = null;23 let findings: (typeof reviewFindings.$inferSelect)[] = [];24 let versions: (typeof artifacts.$inferSelect)[] = [];25 if (cur) {26 const { storage } = await import("@/lib/server/db");27 const latest = await d28 .select()29 .from(artifacts)30 .where(31 and(32 eq(artifacts.runId, cur.run.id),33 inArray(artifacts.path, [34 "manuscript/metadata.json",35 "review/review-1.json",36 "manuscript/main.pdf",37 ]),38 ),39 )40 .orderBy(desc(artifacts.version));41 const m = latest.find((a) => a.path === "manuscript/metadata.json");42 const r = latest.find((a) => a.path === "review/review-1.json");43 versions = latest.filter((a) => a.path === "manuscript/main.pdf");44 try {45 if (m) meta = JSON.parse((await storage().get(m.storageKey)).toString("utf8"));46 } catch {47 meta = null;48 }49 try {50 if (r) review = JSON.parse((await storage().get(r.storageKey)).toString("utf8"));51 } catch {52 review = null;53 }54 findings = await d.select().from(reviewFindings).where(eq(reviewFindings.runId, cur.run.id));55 }56 return (57 <div className="space-y-6">58 <header>59 <p className="eyebrow">Output</p>60 <h1 className="title-2 mt-1">Manuscripts</h1>61 <p className="muted mt-1 text-sm">62 Draft and final PDFs with metadata and review findings, from private run artifacts.63 </p>64 </header>65 {rows.length === 0 ? (66 <div className="surface p-8 text-center muted">No run has produced a manuscript yet.</div>67 ) : (68 <div className="grid gap-6 lg:grid-cols-12">69 <ul className="space-y-1 text-sm lg:col-span-3">70 {rows.map((r) => (71 <li key={r.run.id}>72 <Link73 href={`/admin/manuscripts?run=${r.run.id}`}74 className={`block rounded-md px-2 py-1.5 no-underline hover:bg-[var(--bg-muted)] ${cur?.run.id === r.run.id ? "bg-[var(--bg-muted)]" : ""}`}75 >76 {r.run.title}77 <span className="block text-xs">78 <StatusBadge status={r.run.status} />{" "}79 <span className="subtle">{fmtDateTime(r.run.updatedAt)}</span>80 </span>81 </Link>82 </li>83 ))}84 </ul>85 {cur && (86 <div className="space-y-4 lg:col-span-9">87 <div className="flex flex-wrap items-center justify-between gap-2">88 <h2 className="title-3">{(meta?.title as string) ?? cur.run.title}</h2>89 <div className="flex gap-2">90 <Link href={`/admin/runs/${cur.run.id}`} className="btn btn-sm">91 Run92 </Link>93 <Link href={`/admin/quality/${cur.run.id}`} className="btn btn-sm">94 Quality95 </Link>96 <a97 className="btn btn-sm"98 href={`/api/admin/runs/${cur.run.id}/artifacts/manuscript/main.tex`}99 >100 main.tex101 </a>102 </div>103 </div>104 <object105 data={`/api/admin/runs/${cur.run.id}/artifacts/manuscript/main.pdf`}106 type="application/pdf"107 className="surface h-[60vh] w-full"108 aria-label="Manuscript PDF preview"109 >110 <a href={`/api/admin/runs/${cur.run.id}/artifacts/manuscript/main.pdf?download=1`}>111 Download PDF112 </a>113 </object>114 {versions.length > 1 && (115 <p className="subtle text-xs">116 PDF versions:{" "}117 {versions.map((v) => (118 <a119 key={v.id}120 className="mr-2"121 href={`/api/admin/runs/${cur.run.id}/artifacts/manuscript/main.pdf?version=${v.version}`}122 >123 v{v.version}124 </a>125 ))}126 </p>127 )}128 {meta && (129 <section className="surface p-4 text-sm">130 <h3 className="serif font-semibold">Metadata</h3>131 <p className="mt-1">132 <strong>Abstract.</strong> {String(meta.abstract ?? "")}133 </p>134 <p className="muted mt-1 text-xs">135 Keywords: {(meta.keywords as string[] | undefined)?.join(", ")} · synthetic:{" "}136 {String(meta.synthetic ?? false)}137 </p>138 </section>139 )}140 {review && (141 <section className="surface p-4 text-sm">142 <h3 className="serif font-semibold">143 Independent review — round {String(review.round)} · {String(review.recommendation)}144 </h3>145 <p className="muted mt-1">{String(review.summary ?? "")}</p>146 <ul className="mt-2 grid grid-cols-2 gap-1 text-xs sm:grid-cols-3">147 {((review.rubric as Array<{ dimension: string; score: number }>) ?? []).map((r) => (148 <li key={r.dimension}>149 <span className="font-mono">{r.dimension}</span>: {r.score}/5150 </li>151 ))}152 </ul>153 <p className="subtle mt-2 text-xs">{String(review.limitationsOfThisReview ?? "")}</p>154 </section>155 )}156 {findings.length > 0 && (157 <section className="surface p-4 text-sm">158 <h3 className="serif font-semibold">Review findings ({findings.length})</h3>159 <ul className="mt-2 space-y-2">160 {findings.map((f) => (161 <li key={f.id}>162 <StatusBadge status={f.severity} /> <strong>{f.title}</strong>{" "}163 <span className="subtle text-xs">164 ({f.category}, {f.resolution})165 </span>166 <p className="muted text-xs">{f.detail}</p>167 {f.resolutionNote && <p className="text-xs">↳ {f.resolutionNote}</p>}168 </li>169 ))}170 </ul>171 </section>172 )}173 </div>174 )}175 </div>176 )}177 </div>178 );179}180