import { useEffect, useRef, useState } from "react"; import type { ChatMessage, JobExtract, MatchResult, PlanResponse } from "../../shared/types"; import type { GeneratedResume } from "../App"; import { ApiRequestError, compilePdf, post } from "../api"; interface WorkspaceProps { jd: JobExtract; messages: ChatMessage[]; onMessagesChange: (messages: ChatMessage[]) => void; onBack: () => void; onGenerated: (result: GeneratedResume) => void; } export function Workspace({ jd, messages, onMessagesChange, onBack, onGenerated }: WorkspaceProps) { const [match, setMatch] = useState(null); const [matchError, setMatchError] = useState(""); const [draft, setDraft] = useState(""); const [chatBusy, setChatBusy] = useState(false); const [genStep, setGenStep] = useState(""); const [genError, setGenError] = useState(""); const chatEndRef = useRef(null); useEffect(() => { let cancelled = false; post("/api/match", { text: jd.text }) .then((m) => { if (!cancelled) setMatch(m); }) .catch((err: unknown) => { if (!cancelled) setMatchError(err instanceof Error ? err.message : String(err)); }); return () => { cancelled = true; }; }, [jd.text]); useEffect(() => { chatEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, chatBusy]); async function sendChat(): Promise { const content = draft.trim(); if (content.length === 0 || chatBusy || !match) return; const next: ChatMessage[] = [...messages, { role: "user", content }]; onMessagesChange(next); setDraft(""); setChatBusy(true); try { const { reply } = await post<{ reply: string }>("/api/chat", { jd, match, messages: next }); onMessagesChange([...next, { role: "assistant", content: reply }]); } catch (err) { const message = err instanceof Error ? err.message : String(err); onMessagesChange([...next, { role: "assistant", content: `(error) ${message}` }]); } finally { setChatBusy(false); } } async function generate(): Promise { if (!match || genStep) return; setGenError(""); try { setGenStep("Planning selection with AI…"); const planRes = await post("/api/plan", { jd, match, chat: messages }); setGenStep("Rendering LaTeX…"); const { latex } = await post<{ latex: string }>("/api/latex", { plan: planRes.plan }); setGenStep("Compiling PDF…"); const blob = await compilePdf(latex); onGenerated({ plan: planRes.plan, source: planRes.source, note: planRes.note, latex, pdfUrl: URL.createObjectURL(blob), }); } catch (err) { if (err instanceof ApiRequestError && err.log) { setGenError(`${err.message}\n\n${err.log}`); } else { setGenError(err instanceof Error ? err.message : String(err)); } setGenStep(""); } } const matchedItems = match?.items.filter((i) => i.score > 0) ?? []; return (
{jd.title} {jd.source === "manual" ? "pasted manually" : `${jd.url} · extracted via ${jd.source}`}
{genError &&
{genError}
}

Job Posting

{jd.text}

Matched Material {match && — {match.skills.length} skills hit}

{matchError &&
{matchError}
} {!match && !matchError &&
Matching against the corpus…
} {match && ( <>
{match.skills.map((s) => ( {s.name} ×{s.hits} ))} {match.skills.length === 0 && No skill overlap found.}
{matchedItems.map((item) => (
{item.type} {item.title} {item.company ? `${item.company} · ` : ""} {item.dates} · score {item.score}
{item.matchedSkills.map((s) => ( {s} ))}
    {item.bullets.slice(0, 3).map((b) => (
  • {b}
  • ))}
))}
)}

Refine with the agent

{messages.length === 0 && (
Ask the agent to emphasize certain skills, pick different projects, or adjust the headline — the conversation is folded into the plan when you hit Generate.
)} {messages.map((m, i) => (
{m.content}
))} {chatBusy &&
}
{ e.preventDefault(); void sendChat(); }} > setDraft(e.target.value)} placeholder={match ? "e.g. Lead with the cryptography work, skip consumer apps" : "Waiting for match…"} disabled={!match} />
); }