/* import-cv.jsx — Import existing CV via file upload or paste */ function ImportCVModal({ onApply, onClose }) { const { useState: S, useRef: R, useEffect: E } = React; const [tab, setTab] = S("paste"); const [pasteText, setPaste] = S(""); const [file, setFile] = S(null); const [stage, setStage] = S("input"); // "input" | "parsing" | "review" | "error" const [parsed, setParsed] = S(null); const [statusMsg, setStatus] = S("Reading your CV…"); const [error, setError] = S(""); const [dragOver, setDragOver] = S(false); const [progress, setProgress] = S(0); const [limitHit, setLimitHit] = S(false); const [remaining, setRemaining] = S(() => window.getAIUsesRemaining ? window.getAIUsesRemaining() : 10); const fileRef = R(null); // Close on Escape E(() => { const onKey = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); // Animate progress bar while parsing (asymptotic to 90%, jumps to 100 when done) E(() => { if (stage !== "parsing") return; setProgress(0); const id = setInterval(() => { setProgress(p => { const next = p + (90 - p) * 0.045; return next >= 89.5 ? 89.5 : next; }); }, 180); return () => clearInterval(id); }, [stage]); /* ── Text extraction ──────────────────────────────────────── */ const extractFromFile = async (f) => { const ext = f.name.split(".").pop().toLowerCase(); if (ext === "pdf") { const lib = window.pdfjsLib; if (!lib) throw new Error("PDF library not loaded — please paste your CV text instead."); lib.GlobalWorkerOptions.workerSrc = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js"; const ab = await f.arrayBuffer(); const pdf = await lib.getDocument({ data: ab }).promise; let text = ""; for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); const content = await page.getTextContent(); text += content.items.map((x) => x.str).join(" ") + "\n"; } return text.trim(); } if (ext === "docx") { if (!window.mammoth) throw new Error("DOCX library not loaded — please paste your CV text instead."); const ab = await f.arrayBuffer(); const result = await window.mammoth.extractRawText({ arrayBuffer: ab }); return result.value.trim(); } if (ext === "txt") return await f.text(); throw new Error("Unsupported format. Please upload PDF, DOCX, or TXT — or paste your CV text."); }; /* ── AI parse ─────────────────────────────────────────────── */ const parseWithAI = (text) => new Promise((resolve, reject) => { if (!window.groqStream) { reject(new Error("AI not available")); return; } const prompt = `Parse the following CV/resume and return a single JSON object. Return ONLY valid JSON — no markdown, no code fences, no explanation. JSON structure (fill all fields, use empty string or [] when unknown): { "basics": { "name": "", "title": "", "email": "", "phone": "", "location": "", "website": "", "linkedin": "" }, "summary": "", "experience": [{ "id": "e1", "company": "", "role": "", "location": "", "start": "Mon YYYY", "end": "Mon YYYY or Present", "bullets": [] }], "projects": [{ "id": "p1", "name": "", "tech": "", "detail": "", "bullets": [] }], "education": [{ "id": "ed1", "school": "", "degree": "", "location": "", "start": "YYYY", "end": "YYYY", "detail": "" }], "skills": [{ "id": "s1", "group": "Category", "items": [] }], "certs": [] } Rules: - IDs must be sequential: e1,e2… p1,p2… ed1,ed2… s1,s2… - Use "Present" for current roles - Group skills logically (Technical, Tools, Languages, Soft Skills…) - Include all certifications and courses found CV: ${text.slice(0, 14000)}`; window.groqStream({ messages: [ { role: "system", content: "You are a resume parser. Output only valid JSON, nothing else." }, { role: "user", content: prompt }, ], maxTokens: 3500, onChunk: () => {}, onDone: (full) => { try { let s = full.trim() .replace(/^```json\s*/i, "").replace(/^```\s*/i, "").replace(/\s*```$/i, ""); resolve(JSON.parse(s)); } catch (_) { const m = full.match(/\{[\s\S]*\}/); if (m) { try { resolve(JSON.parse(m[0])); return; } catch (_2) {} } reject(new Error("AI returned unexpected format. Please try again.")); } }, onError: (err) => reject(new Error(err)), }); }); /* ── Normalise imported data ──────────────────────────────── */ const normalize = (d) => ({ basics: { name: "", title: "", email: "", phone: "", location: "", website: "", linkedin: "", ...(d.basics || {}) }, summary: d.summary || "", experience: (d.experience || []).map((e, i) => ({ id:"e"+(i+1), company:"", role:"", location:"", start:"", end:"", bullets:[], ...e })), projects: (d.projects || []).map((p, i) => ({ id:"p"+(i+1), name:"", tech:"", detail:"", bullets:[], ...p })), education: (d.education || []).map((ed,i) => ({ id:"ed"+(i+1),school:"", degree:"",location:"", start:"", end:"", detail:"", ...ed })), skills: (d.skills || []).map((s, i) => ({ id:"s"+(i+1), group:"", items:[], ...s })), certs: (d.certs || []).map(c => typeof c === "string" ? c : [c.name, c.issuer, c.date].filter(Boolean).join(" · ") ), }); /* ── Extract flow ─────────────────────────────────────────── */ const handleExtract = async () => { setError(""); if (limitHit) { setError("You've reached the free limit. Upgrade to Pro for unlimited imports."); return; } try { setStage("parsing"); let text = ""; if (tab === "paste" || tab === "linkedin") { text = pasteText.trim(); if (!text) { setStage("input"); setError(tab === "linkedin" ? "Please paste your LinkedIn profile text." : "Please paste your CV text."); return; } setStatus(tab === "linkedin" ? "Parsing LinkedIn profile…" : "Analysing your CV…"); } else { if (!file) { setStage("input"); setError("Please select a file."); return; } setStatus("Reading " + file.name + "…"); text = await extractFromFile(file); if (!text) throw new Error("Could not extract text — try pasting your CV instead."); setStatus("Extracting your info with AI…"); } const raw = await parseWithAI(text); setProgress(100); await new Promise(r => setTimeout(r, 320)); setParsed(normalize(raw)); setStage("review"); } catch (e) { const msg = e.message || ""; if (msg === "free_limit_reached") { setLimitHit(true); setError("You've used all 10 free AI imports. Upgrade to Pro for unlimited."); } else { setError(msg || "Something went wrong. Please try again."); } setStage("input"); } }; const handleDrop = (e) => { e.preventDefault(); setDragOver(false); const f = e.dataTransfer.files[0]; if (f) { setFile(f); setTab("file"); } }; /* ── Review summary card ──────────────────────────────────── */ const ReviewCard = ({ data }) => { const b = data.basics || {}; const rows = [ (data.experience || []).length && `${data.experience.length} job${data.experience.length > 1 ? "s" : ""}`, (data.projects || []).length && `${data.projects.length} project${data.projects.length > 1 ? "s" : ""}`, (data.education || []).length && `${data.education.length} education entr${data.education.length > 1 ? "ies" : "y"}`, (data.skills || []).reduce((n,g) => n+(g.items||[]).length, 0) && `${(data.skills||[]).reduce((n,g)=>n+(g.items||[]).length,0)} skills`, (data.certs || []).length && `${data.certs.length} cert${data.certs.length > 1 ? "s" : ""}`, ].filter(Boolean); return (
{(b.name||"?").trim().split(/\s+/).map(w=>w[0]).slice(0,2).join("").toUpperCase()}
{b.name || "Name not found"}
{b.title || b.email || "—"}
{rows.map((r, i) => (
{r}
))}
{data.summary && (
Summary: {data.summary.slice(0, 180)}{data.summary.length > 180 ? "…" : ""}
)}

This will replace your current resume. You can edit everything in the builder after importing.

); }; /* ── Render ───────────────────────────────────────────────── */ return ( <>
{/* Header */}

Import existing CV

AI extracts your info and pre-fills the builder

{/* Body */}
{/* Loading */} {stage === "parsing" && (
{/* Icon */}
{/* Text */}
{progress < 30 ? "Reading your CV…" : progress < 70 ? "Extracting information…" : progress < 99 ? "Finalising details…" : "Done!"}
AI is analysing your resume…
{/* Progress bar */}
{Math.round(progress)}%
)} {/* Review */} {stage === "review" && parsed && } {/* Input */} {stage === "input" && ( <> {/* Tab toggle */}
{[["paste","📋","Paste CV"],["file","📁","Upload file"],["linkedin","in","LinkedIn"]].map(([t,icon,label]) => ( ))}
{/* Paste mode */} {tab === "paste" && (