mirror of
https://github.com/prdlk/cv.git
synced 2026-08-02 17:31:41 +00:00
89 lines
3.4 KiB
TypeScript
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;
|