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;