SPB Git

spb/autoscholar

Public
7.4 KB · 180 lines tsx
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
Files · main
Menu

Theme