69 lines
2.4 KiB
TypeScript
69 lines
2.4 KiB
TypeScript
import { desc, eq } from "drizzle-orm";
|
|
import { Hono } from "hono";
|
|
import { randomUUID } from "node:crypto";
|
|
import { mkdirSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import type { Db } from "../db";
|
|
import { labDrafts } from "../db/schema";
|
|
import { extractFromText } from "../lib/extract";
|
|
import { pdfToText } from "../lib/pdf";
|
|
|
|
export type LabsDeps = { db: Db; key: Buffer; dataDir: string; llmFetch?: typeof fetch };
|
|
|
|
const MAX_UPLOAD = 15 * 1024 * 1024;
|
|
|
|
export function labsRoutes(deps: LabsDeps) {
|
|
const app = new Hono();
|
|
|
|
app.post("/labs/upload", async (c) => {
|
|
const body = await c.req.parseBody();
|
|
const file = body.file;
|
|
if (!(file instanceof File)) return c.json({ error: "file field required" }, 400);
|
|
if (!file.name.toLowerCase().endsWith(".pdf") && file.type !== "application/pdf") {
|
|
return c.json({ error: "not_a_pdf" }, 400);
|
|
}
|
|
if (file.size > MAX_UPLOAD) return c.json({ error: "too_large" }, 400);
|
|
|
|
const id = randomUUID();
|
|
const uploadsDir = join(deps.dataDir, "uploads");
|
|
mkdirSync(uploadsDir, { recursive: true });
|
|
const filePath = join(uploadsDir, `${id}.pdf`);
|
|
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
await Bun.write(filePath, bytes);
|
|
|
|
// Extraction failures land on the draft row so the user sees them in the
|
|
// review UI instead of the upload 500ing.
|
|
let extracted: string | null = null;
|
|
let error: string | null = null;
|
|
try {
|
|
const text = await pdfToText(bytes);
|
|
if (text.trim().length < 20) throw new Error("pdf_error: no text layer (scanned PDFs are not supported yet)");
|
|
const draft = await extractFromText({ db: deps.db, key: deps.key, fetchImpl: deps.llmFetch }, text);
|
|
extracted = JSON.stringify(draft);
|
|
} catch (e) {
|
|
error = e instanceof Error ? e.message : "extraction failed";
|
|
}
|
|
|
|
await deps.db.insert(labDrafts).values({
|
|
id, filename: file.name, filePath, status: "pending", extracted, error, createdAt: Date.now(),
|
|
});
|
|
return c.json({ id }, 201);
|
|
});
|
|
|
|
app.get("/labs/drafts", async (c) => {
|
|
const rows = await deps.db.select().from(labDrafts).orderBy(desc(labDrafts.createdAt));
|
|
return c.json({
|
|
drafts: rows.map((r) => ({
|
|
id: r.id,
|
|
filename: r.filename,
|
|
status: r.status,
|
|
error: r.error,
|
|
markerCount: r.extracted ? (JSON.parse(r.extracted).markers?.length ?? 0) : 0,
|
|
createdAt: r.createdAt,
|
|
})),
|
|
});
|
|
});
|
|
|
|
return app;
|
|
}
|