/* tailor.jsx — "Tailor to job" Pro panel */ function TailorDrawer({ data, patch, coverLetter, setCoverLetter, showToast, onGotoCoverLetter, onClose }) { const { useState: S, useRef: R } = React; const isPro = !!window.IS_PRO; const [jd, setJd] = S(""); const [phase, setPhase] = S("input"); // "input" | "loading" | "review" const [status, setStatus] = S(""); const [result, setResult] = S(null); const [applied, setApplied] = S(false); const [limitHit, setLimitHit] = S(false); const [remaining, setRemaining] = S(() => { var v = window.getAIUsesRemaining ? window.getAIUsesRemaining() : null; return v; }); const abortRef = R(false); const canRun = !limitHit; const reset = () => { setPhase("input"); setResult(null); setApplied(false); setStatus(""); abortRef.current = false; }; /* ── helpers ──────────────────────────────────────────────── */ function groqOnce(messages, maxTokens, onDone, onError) { let buf = ""; window.groqStream({ messages, maxTokens, model: "llama-3.3-70b-versatile", onChunk: (_, full) => { buf = full; }, onDone: (full) => onDone(full || buf), onError: (err) => onError(err), }); } /* ── run tailoring ─────────────────────────────────────────── */ const run = () => { const jdTrim = jd.trim(); if (!jdTrim || !canRun) return; if (!window.groqStream) { showToast && showToast("AI not available"); return; } abortRef.current = false; setPhase("loading"); setStatus("Reading your résumé…"); setResult(null); setApplied(false); const ctx = window.buildResumeCtx ? window.buildResumeCtx(data) : ""; const cvPrompt = [ { role: "system", content: window.GROQ_SYSTEM || "" }, { role: "user", content: "Tailor this résumé for the job description below.\n\n" + "RÉSUMÉ:\n" + ctx + "\n\n" + "JOB DESCRIPTION:\n" + jdTrim + "\n\n" + "Return ONLY a JSON object — no markdown fences, no explanation — with this exact shape:\n" + '{"summary":"","bulletEdits":[{"role":"","company":"","bulletIndex":<0-based int>,"newBullet":""}],"newSkills":["",""]}\n\n' + "Rules:\n" + "• summary: rewrite to directly target this role. Mirror key terms.\n" + "• bulletEdits: max 4 edits total, only the bullets that most need improvement for this JD.\n" + "• newSkills: up to 6 keywords/tools from the JD not already in the résumé. Empty array [] if none.\n" + "Return pure JSON only." }, ]; const clPrompt = [ { role: "system", content: window.GROQ_SYSTEM || "" }, { role: "user", content: "Write a targeted cover letter body for the job below using my résumé.\n\n" + "RÉSUMÉ:\n" + ctx + "\n\n" + "JOB DESCRIPTION:\n" + jdTrim + "\n\n" + "3 paragraphs separated by a blank line. Under 350 words. Professional but warm.\n" + "Paragraph 1: why this specific role + company excites me + who I am.\n" + "Paragraph 2: 2 concrete achievements from my background that map to the role's needs.\n" + "Paragraph 3: brief closing with call to action.\n" + "Return ONLY the letter body — no date, salutation, or sign-off." }, ]; let cvDone = false, clDone = false; let cvResult = null, clResult = null; let errored = false; function tryFinish() { if (abortRef.current || errored) return; if (!cvDone || !clDone) return; let parsed = null; try { const raw = cvResult.trim().replace(/^```(?:json)?|```$/gm, "").trim(); parsed = JSON.parse(raw); } catch(e) { try { const m = cvResult.match(/\{[\s\S]*\}/); if (m) parsed = JSON.parse(m[0]); } catch(e2) {} } if (!parsed) { errored = true; setStatus("Couldn't parse AI response. Try again."); setPhase("input"); return; } setResult({ summary: parsed.summary || "", bulletEdits: parsed.bulletEdits || [], newSkills: parsed.newSkills || [], coverLetter: clResult || "" }); setPhase("review"); } setStatus("Tailoring your CV…"); const handleErr = (err) => { if (abortRef.current) return; errored = true; if (err === "free_limit_reached") { setLimitHit(true); reset(); } else { setStatus("Error: " + err); setPhase("input"); } }; groqOnce(cvPrompt, 900, (full) => { if (abortRef.current) return; cvResult = full; cvDone = true; setStatus("Writing cover letter…"); const r = window.getAIUsesRemaining ? window.getAIUsesRemaining() : null; setRemaining(r); tryFinish(); }, handleErr); groqOnce(clPrompt, 700, (full) => { if (abortRef.current) return; clResult = full; clDone = true; tryFinish(); }, handleErr); }; /* ── apply all to resume ───────────────────────────────────── */ const applyAll = () => { if (!result || applied) return; patch(d => { let next = { ...d }; if (result.summary) next = { ...next, summary: result.summary }; if (result.bulletEdits && result.bulletEdits.length) { next = { ...next, experience: (next.experience || []).map(x => { const edits = result.bulletEdits.filter(e => e.role === x.role && e.company === x.company); if (!edits.length) return x; const bullets = [...(x.bullets || [])]; edits.forEach(e => { if (bullets[e.bulletIndex] !== undefined) bullets[e.bulletIndex] = e.newBullet; }); return { ...x, bullets }; }), }; } if (result.newSkills && result.newSkills.length) { const groups = next.skills ? [...next.skills] : []; if (groups.length) { const existing = groups.flatMap(g => g.items || []).map(s => s.toLowerCase()); const toAdd = result.newSkills.filter(s => !existing.includes(s.toLowerCase())); if (toAdd.length) { groups[0] = { ...groups[0], items: [...(groups[0].items || []), ...toAdd] }; } next = { ...next, skills: groups }; } } return next; }); if (result.coverLetter) { setCoverLetter(cl => ({ ...cl, body: result.coverLetter })); if (showToast) showToast("Applied! Cover letter saved — opening tab…"); if (onGotoCoverLetter) setTimeout(onGotoCoverLetter, 900); } else { if (showToast) showToast("Changes applied to your résumé."); } setApplied(true); }; /* ── render ────────────────────────────────────────────────── */ return ( <>
{/* Header */}

Tailor to job Pro

Paste a job description — AI rewrites your CV and drafts a cover letter.

{/* Usage bar — free users only */} {!isPro && remaining !== null && remaining !== Infinity && (
{remaining} / 10 free {remaining === 1 ? "use" : "uses"} left
)}
{/* Limit hit banner */} {limitHit && (
You've used all 10 free AI requests
Upgrade to Pro for unlimited AI tailoring, cover letters, and priority support.
Upgrade to Pro →
)} {/* ── INPUT PHASE ── */} {phase === "input" && ( <>