Files

89 lines
3.4 KiB
TypeScript

import { Hono } from "hono";
import type { ChatMessage, JobExtract, MatchResult, ResumePlan } from "../shared/types";
import { buildPlan, chatReply } from "./ai";
import { loadCorpus } from "./db";
import { extractJob } from "./extract";
import { renderLatex } from "./latex";
import { matchCorpus } from "./match";
const app = new Hono<{ Bindings: Env }>();
app.onError((err, c) => {
console.error(JSON.stringify({ level: "error", path: c.req.path, message: err.message }));
return c.json({ error: err.message }, 500);
});
app.post("/api/extract", async (c) => {
const { url } = await c.req.json<{ url?: string }>();
const trimmed = url?.trim() ?? "";
if (!/^https?:\/\//i.test(trimmed)) {
return c.json({ error: "Enter a full http(s) URL to a job posting." }, 400);
}
let jd: JobExtract;
try {
jd = await extractJob(trimmed);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return c.json({ error: `Could not fetch the posting: ${message}` }, 502);
}
if (jd.text.length < 120) {
return c.json(
{ error: "The page yielded almost no text (likely rendered by JavaScript). Try the direct job-board URL (Greenhouse/Lever/Workable)." },
422,
);
}
return c.json(jd);
});
app.post("/api/match", async (c) => {
const { text } = await c.req.json<{ text?: string }>();
if (!text) return c.json({ error: "Missing job text." }, 400);
const corpus = await loadCorpus(c.env.DB);
return c.json(matchCorpus(corpus, text));
});
app.post("/api/chat", async (c) => {
const body = await c.req.json<{ jd: JobExtract; match: MatchResult; messages: ChatMessage[] }>();
const corpus = await loadCorpus(c.env.DB);
try {
const reply = await chatReply(c.env, corpus, body.jd, body.match, body.messages ?? []);
return c.json({ reply });
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return c.json({ error: `Workers AI is unavailable (${message}). Check \`wrangler login\` / network.` }, 502);
}
});
app.post("/api/plan", async (c) => {
const body = await c.req.json<{ jd: JobExtract; match?: MatchResult; chat?: ChatMessage[] }>();
if (!body.jd?.text) return c.json({ error: "Missing job extract." }, 400);
const corpus = await loadCorpus(c.env.DB);
const match = body.match ?? matchCorpus(corpus, body.jd.text);
return c.json(await buildPlan(c.env, corpus, body.jd, match, body.chat ?? []));
});
app.post("/api/latex", async (c) => {
const { plan } = await c.req.json<{ plan?: ResumePlan }>();
if (!plan) return c.json({ error: "Missing plan." }, 400);
const corpus = await loadCorpus(c.env.DB);
return c.json({ latex: renderLatex(corpus, plan) });
});
/** Production fallback compiler; local dev uses the Vite /local/compile middleware (real pdflatex). */
app.post("/api/pdf", async (c) => {
const { latex } = await c.req.json<{ latex?: string }>();
if (!latex) return c.json({ error: "Missing latex source." }, 400);
const res = await fetch("https://latex.ytotech.com/builds/sync", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ compiler: "pdflatex", resources: [{ main: true, content: latex }] }),
});
if (res.ok && res.body) {
return new Response(res.body, { headers: { "Content-Type": "application/pdf" } });
}
const log = (await res.text()).slice(0, 4000);
return c.json({ error: `Remote LaTeX service responded ${res.status}`, log }, 502);
});
export default app;