diff --git a/job-tracker-ui/src/profile-page.test.tsx b/job-tracker-ui/src/profile-page.test.tsx index 2b9ace6..13e9424 100644 --- a/job-tracker-ui/src/profile-page.test.tsx +++ b/job-tracker-ui/src/profile-page.test.tsx @@ -87,11 +87,12 @@ function renderWith(Component: React.ComponentType) { ); } -// /profile (account identity) — the CV-editing surface still renders here (display:none) because -// the two components share the same underlying implementation; these tests exercise that surface. +// These exercise the master-CV editing surface, which lives on /career (CareerProfilePage) after +// the Phase 2.2 split. ProfilePage no longer carries it. function renderPage() { - return renderWith(ProfilePage); + return renderWith(CareerProfilePage); } +void ProfilePage; beforeEach(() => { mockedApi.get.mockImplementation((url: string) => { diff --git a/job-tracker-ui/src/views/CareerProfilePage.tsx b/job-tracker-ui/src/views/CareerProfilePage.tsx index 3bc563c..9f3bc32 100644 --- a/job-tracker-ui/src/views/CareerProfilePage.tsx +++ b/job-tracker-ui/src/views/CareerProfilePage.tsx @@ -246,11 +246,6 @@ export default function CareerProfilePage() { const [avatarFile, setAvatarFile] = useState(null); const [cropOpen, setCropOpen] = useState(false); - const [email, setEmail] = useState(""); - const [userName, setUserName] = useState(""); - const [firstName, setFirstName] = useState(""); - const [lastName, setLastName] = useState(""); - const [displayName, setDisplayName] = useState(""); const [headline, setHeadline] = useState(""); const [profileCvText, setProfileCvText] = useState(""); const [rewritingSection, setRewritingSection] = useState(false); @@ -273,8 +268,6 @@ export default function CareerProfilePage() { const [structuredCv, setStructuredCv] = useState(emptyStructuredCv()); const [extractionRuns, setExtractionRuns] = useState([]); const runStatusRef = useRef>({}); - const [currentPassword, setCurrentPassword] = useState(""); - const [newPassword, setNewPassword] = useState(""); // Keep a ref to the latest carousel so the unmount cleanup can revoke the // outstanding preview object URLs without re-running on every change. @@ -308,11 +301,6 @@ export default function CareerProfilePage() { ]); const r = profileResponse; setMe(r.data); - setEmail(r.data?.email ?? ""); - setUserName(r.data?.userName ?? ""); - setFirstName(r.data?.firstName ?? ""); - setLastName(r.data?.lastName ?? ""); - setDisplayName(r.data?.displayName ?? ""); setProfileCvText(r.data?.profileCvText ?? ""); setStructuredCv(parseStructuredCvJson(r.data?.profileCvStructureJson)); setExtractionRuns(runsResponse.data ?? []); @@ -570,36 +558,9 @@ export default function CareerProfilePage() { - {!careerOnly ? <> - - - - : null} - {!careerOnly ? - {t("profileAccountSection")} - {!isLocal ? ( - - {t("profileReadOnlyInfo")} - - ) : null} - : null} - {!careerOnly ? <> - setDisplayName(e.target.value)} disabled={!isLocal} fullWidth /> - setUserName(e.target.value)} disabled={!isLocal} fullWidth /> - setFirstName(e.target.value)} disabled={!isLocal} fullWidth /> - setLastName(e.target.value)} disabled={!isLocal} fullWidth /> - setEmail(e.target.value)} disabled={!isLocal} fullWidth /> - setHeadline(e.target.value)} - helperText={t("profileHeadlineHelp")} - fullWidth - /> - : null} @@ -1307,14 +1268,9 @@ export default function CareerProfilePage() { onClick={async () => { setLoading(true); try { - // Scoped save: /career persists only the master profile, /profile only identity. - // The backend (PUT /auth/profile) does partial updates — omitted fields are left - // unchanged — so neither surface wipes the other's data. - const payload = careerOnly - ? { profileCvText, profileCvStructureJson: JSON.stringify(structuredCv) } - : { email, userName, firstName, lastName, displayName }; - await api.put("/auth/profile", payload); - if (!careerOnly) window.localStorage.setItem("profileHeadline", headline.trim()); + // /career saves only the master profile. The backend does partial updates, so + // omitting identity fields leaves them untouched (they are owned by /profile). + await api.put("/auth/profile", { profileCvText, profileCvStructureJson: JSON.stringify(structuredCv) }); await loadProfile(); toast(t("profileUpdated"), "success"); } catch (e: any) { @@ -1329,43 +1285,9 @@ export default function CareerProfilePage() { - {!careerOnly ? - - {t("profileChangePassword")} - {!isLocal ? {t("profilePasswordLocalOnly")} : null} - : null} - {!careerOnly ? <> - setCurrentPassword(e.target.value)} disabled={!isLocal} fullWidth /> - setNewPassword(e.target.value)} disabled={!isLocal} fullWidth /> - - - - - : null} - {!careerOnly && isLocal ? : null} - {!careerOnly && isLocal ? : null} ); } diff --git a/job-tracker-ui/src/views/ProfilePage.tsx b/job-tracker-ui/src/views/ProfilePage.tsx index 14c61f6..fc02db2 100644 --- a/job-tracker-ui/src/views/ProfilePage.tsx +++ b/job-tracker-ui/src/views/ProfilePage.tsx @@ -234,14 +234,10 @@ export default function ProfilePage() { const careerOnly = false; const { toast } = useToast(); const { t } = useI18n(); - const cvInputRef = useRef(null); const avatarInputRef = useRef(null); const [me, setMe] = useState(null); const [loading, setLoading] = useState(false); const [loadError, setLoadError] = useState(null); - const [uploadingCv, setUploadingCv] = useState(false); - const [improvingCv, setImprovingCv] = useState(false); - const [rebuildingCv, setRebuildingCv] = useState(false); const [uploadingAvatar, setUploadingAvatar] = useState(false); const [avatarFile, setAvatarFile] = useState(null); const [cropOpen, setCropOpen] = useState(false); @@ -253,60 +249,16 @@ export default function ProfilePage() { const [displayName, setDisplayName] = useState(""); const [headline, setHeadline] = useState(""); const [profileCvText, setProfileCvText] = useState(""); - const [rewritingSection, setRewritingSection] = useState(false); - const [cvSection, setCvSection] = useState(""); - const [cvSectionStyle, setCvSectionStyle] = useState("ats-minimal"); - const [cvSectionTargetRole, setCvSectionTargetRole] = useState(""); - const [cvPromptBackground, setCvPromptBackground] = useState(""); - const [cvTone, setCvTone] = useState("Concise and direct"); - const [cvLanguage, setCvLanguage] = useState("English"); - const [selectedRewriteJobId, setSelectedRewriteJobId] = useState(""); - const [rewritePreview, setRewritePreview] = useState(null); - const [rewritePreviewTemplate, setRewritePreviewTemplate] = useState(null); - const [pdfCarousel, setPdfCarousel] = useState([]); - const [activePdfIndex, setActivePdfIndex] = useState(0); - const [buildingPdfDeck, setBuildingPdfDeck] = useState(false); - const [downloadingPdf, setDownloadingPdf] = useState(false); - const [savedJobs, setSavedJobs] = useState([]); - const [parsingCvSections, setParsingCvSections] = useState(false); - const [reprocessingCv, setReprocessingCv] = useState(false); - const [structuredCv, setStructuredCv] = useState(emptyStructuredCv()); - const [extractionRuns, setExtractionRuns] = useState([]); - const runStatusRef = useRef>({}); const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); - // Keep a ref to the latest carousel so the unmount cleanup can revoke the - // outstanding preview object URLs without re-running on every change. - const pdfCarouselRef = useRef([]); - useEffect(() => { - pdfCarouselRef.current = pdfCarousel; - }, [pdfCarousel]); - - useEffect(() => { - // Revoke any remaining preview object URLs only on unmount. Per-change - // revocation is already handled explicitly in savePdfToCarousel (replace) and - // resetPdfCarousel (clear); doing it here on every pdfCarousel change revoked - // URLs that were still referenced by other items in the deck, breaking their - // previews. - return () => { - pdfCarouselRef.current.forEach((item) => { - if (item.pdfUrl) { - window.URL.revokeObjectURL(item.pdfUrl); - } - }); - }; - }, []); const loadProfile = useCallback(async () => { setLoading(true); try { - const [profileResponse, runsResponse, jobsResponse] = await Promise.all([ - api.get("/auth/me"), - api.get("/profile-cv/runs").catch(() => ({ data: [] as ExtractionRun[] } as any)), - api.get("/jobapplications", { params: { page: 1, pageSize: 100, sortBy: "dateApplied", sortDir: "desc" } }).catch(() => ({ data: { items: [], total: 0, page: 1, pageSize: 100 } } as any)), - ]); - const r = profileResponse; + // /profile only needs the account row. The master-profile data (runs, structured CV, + // saved jobs) is loaded by CareerProfilePage on /career. + const r = await api.get("/auth/me"); setMe(r.data); setEmail(r.data?.email ?? ""); setUserName(r.data?.userName ?? ""); @@ -314,15 +266,10 @@ export default function ProfilePage() { setLastName(r.data?.lastName ?? ""); setDisplayName(r.data?.displayName ?? ""); setProfileCvText(r.data?.profileCvText ?? ""); - setStructuredCv(parseStructuredCvJson(r.data?.profileCvStructureJson)); - setExtractionRuns(runsResponse.data ?? []); - setSavedJobs(jobsResponse.data?.items ?? []); setHeadline(window.localStorage.getItem("profileHeadline") ?? ""); setLoadError(null); } catch (error: any) { setMe(null); - setExtractionRuns([]); - setSavedJobs([]); setLoadError(String(error?.response?.data || error?.message || "Unable to load profile right now.")); } finally { setLoading(false); @@ -333,31 +280,6 @@ export default function ProfilePage() { void loadProfile(); }, [loadProfile]); - useEffect(() => { - const activeRuns = extractionRuns.filter((run) => run.status === "queued" || run.status === "running"); - if (activeRuns.length === 0) return; - - const timer = window.setInterval(() => { - void loadProfile(); - }, 4000); - - return () => window.clearInterval(timer); - }, [extractionRuns, loadProfile]); - - useEffect(() => { - const previous = runStatusRef.current; - for (const run of extractionRuns) { - const prior = previous[run.id]; - if ((prior === "queued" || prior === "running") && run.status === "applied") { - toast(`CV ${run.trigger} completed.`, "success"); - } - if ((prior === "queued" || prior === "running") && run.status === "failed") { - toast(run.errorMessage || `CV ${run.trigger} failed.`, "error"); - } - previous[run.id] = run.status; - } - }, [extractionRuns, toast]); - const initials = useMemo(() => initialsFrom([me?.displayName, me?.firstName, me?.lastName, me?.userName, me?.email]), [me]); const isLocal = me?.provider === "local"; const fullName = [me?.firstName, me?.lastName].filter(Boolean).join(" "); @@ -370,107 +292,6 @@ export default function ProfilePage() { : t("profileGoogleLinked") : t("profileGoogleNotLinked"); const cvLabel = profileCvText.trim() ? t("profileCvReady", { count: cvWordCount }) : t("profileCvMissing"); - const latestRun = extractionRuns[0]; - const selectedRewriteTemplate = REWRITE_TEMPLATES.find((option) => option.id === cvSectionStyle) ?? REWRITE_TEMPLATES[0]; - const selectedRewriteJob = savedJobs.find((job) => String(job.id) === selectedRewriteJobId) ?? null; - const rewriteReady = Boolean(rewritePreview?.html && rewritePreview.fullText.trim()); - const activePdfItem = pdfCarousel[activePdfIndex] ?? null; - - const releasePdfCarousel = useCallback((items: PdfCarouselItem[]) => { - items.forEach((item) => { - if (item.pdfUrl) { - window.URL.revokeObjectURL(item.pdfUrl); - } - }); - }, []); - - const buildRewritePayload = useCallback((templateId: CvSectionStyle): RewriteRequestPayload => ({ - sectionName: cvSection || null, - style: templateId, - templateId, - targetRole: cvSectionTargetRole.trim() || null, - jobApplicationId: selectedRewriteJob ? selectedRewriteJob.id : null, - sourceText: profileCvText.trim() || null, - promptBackground: cvPromptBackground.trim() || null, - tone: cvTone, - language: cvLanguage, - }), [cvLanguage, cvPromptBackground, cvSection, cvSectionTargetRole, cvTone, profileCvText, selectedRewriteJob]); - - const resetPdfCarousel = useCallback(() => { - setPdfCarousel((current) => { - releasePdfCarousel(current); - return []; - }); - setActivePdfIndex(0); - }, [releasePdfCarousel]); - - const savePdfToCarousel = useCallback(async (templateId: CvSectionStyle, download = false) => { - const template = REWRITE_TEMPLATES.find((option) => option.id === templateId) ?? REWRITE_TEMPLATES[0]; - const payload = buildRewritePayload(templateId); - const response = await api.post("/profile-cv/export-pdf", payload, { responseType: "blob" }); - const blob = new Blob([response.data], { type: "application/pdf" }); - const url = window.URL.createObjectURL(blob); - const item: PdfCarouselItem = { - templateId, - title: template.title, - fileName: rewritePreview?.suggestedFileName || `${templateId}-cv.pdf`, - pdfUrl: url, - status: "ready", - }; - - setPdfCarousel((current) => { - const existing = current.find((entry) => entry.templateId === templateId); - if (existing?.pdfUrl) { - window.URL.revokeObjectURL(existing.pdfUrl); - } - const next = existing - ? current.map((entry) => (entry.templateId === templateId ? item : entry)) - : [...current, item]; - setActivePdfIndex(next.findIndex((entry) => entry.templateId === templateId)); - return next; - }); - - if (download) { - const link = document.createElement("a"); - link.href = url; - link.download = item.fileName; - document.body.appendChild(link); - link.click(); - link.remove(); - } - - return item; - }, [buildRewritePayload, rewritePreview?.suggestedFileName]); - - const buildPdfCarousel = useCallback(async () => { - setBuildingPdfDeck(true); - resetPdfCarousel(); - const orderedTemplates = [selectedRewriteTemplate.id, ...REWRITE_TEMPLATES.map((option) => option.id).filter((id) => id !== selectedRewriteTemplate.id)]; - const seedItems = orderedTemplates.map((templateId) => ({ - templateId, - title: REWRITE_TEMPLATES.find((option) => option.id === templateId)?.title ?? templateId, - fileName: `${templateId}-cv.pdf`, - status: "loading" as const, - })); - setPdfCarousel(seedItems); - setActivePdfIndex(0); - - for (const templateId of orderedTemplates) { - try { - const item = await savePdfToCarousel(templateId, false); - setPdfCarousel((current) => current.map((entry) => entry.templateId === templateId ? item : entry)); - } catch (error: any) { - const message = getApiErrorMessage(error, `Failed to generate the ${templateId} PDF preview.`); - setPdfCarousel((current) => current.map((entry) => entry.templateId === templateId ? { ...entry, status: "error", error: message } : entry)); - } - } - - setBuildingPdfDeck(false); - }, [resetPdfCarousel, savePdfToCarousel, selectedRewriteTemplate.id]); - - useEffect(() => { - resetPdfCarousel(); - }, [rewritePreview?.fullText, rewritePreview?.templateId, rewritePreview?.targetRole, resetPdfCarousel]); return ( @@ -601,704 +422,6 @@ export default function ProfilePage() { /> : null} - - - - {t("profileMasterCv")} - - {t("profileMasterCvBody")} - - - - { - const file = event.target.files?.[0]; - event.target.value = ""; - if (!file) return; - const formData = new FormData(); - formData.append("file", file); - setUploadingCv(true); - try { - await api.post("/profile-cv/upload", formData, { headers: { "Content-Type": "multipart/form-data" } }); - await loadProfile(); - toast(t("profileCvUploaded"), "success"); - } catch (e: any) { - toast(String(e?.response?.data || e?.message || t("profileCvUploadFailed")), "error"); - } finally { - setUploadingCv(false); - } - }} - /> - - - - - - - {uploadingCv ? : null} - - {t("profileCvStructuredDefaultHint")} - - - }> - - - {t("profileCvRawPanelTitle")} - {t("profileCvRawPanelHelp")} - - - - - - setProfileCvText(e.target.value)} - helperText={t("profileCvTextHelp")} - multiline - minRows={12} - disabled={!isLocal} - fullWidth - /> - - - - - - - - - {t("profileCvExtractionHistory")} - {t("profileCvExtractionHistoryHelp")} - - {structuredCv.metadata.profileVersion ? : null} - - {latestRun ? ( - - {extractionRuns.map((run) => ( - - - {run.trigger} - - - {run.id === structuredCv.metadata.appliedExtractionRunId ? : null} - - - {run.artifactFileName || t("profileCvNoStoredArtifact")} - - {run.parserVersion} · {new Date(run.startedAtUtc).toLocaleString()} - - {run.errorMessage ? ( - - {run.errorMessage} - - ) : null} - - ))} - - ) : ( - {t("profileCvExtractionHistoryEmpty")} - )} - - - - - {t("profileCvStructureOverview")} - {t("profileCvStructureOverviewHelp")} - - - - {structuredCv.sections.length > 0 ? ( - - {structuredCv.sections.map((section) => { - const safeContent = typeof section.content === "string" ? section.content : ""; - const safeWordCount = Number.isFinite(Number(section.wordCount)) ? Number(section.wordCount) : (safeContent.trim() ? safeContent.trim().split(/\s+/).length : 0); - return ( - - - {section.name} - - - {safeContent.slice(0, 280)}{safeContent.length > 280 ? "…" : ""} - - ); - })} - - ) : ( - {t("profileCvStructureEmpty")} - )} - - - - {t("profileCvStructuredEditor")} - {t("profileCvStructuredEditorHelp")} - - - - - setStructuredCv((prev) => ({ ...prev, contact: { ...prev.contact, fullName: e.target.value || undefined } }))} fullWidth /> - - - - setStructuredCv((prev) => ({ ...prev, contact: { ...prev.contact, headline: e.target.value || undefined } }))} fullWidth /> - - - - setStructuredCv((prev) => ({ ...prev, contact: { ...prev.contact, email: e.target.value || undefined } }))} fullWidth /> - - - - setStructuredCv((prev) => ({ ...prev, contact: { ...prev.contact, phone: e.target.value || undefined } }))} fullWidth /> - - - - setStructuredCv((prev) => ({ ...prev, contact: { ...prev.contact, location: e.target.value || undefined } }))} fullWidth /> - - - setStructuredCv((prev) => ({ ...prev, contact: { ...prev.contact, website: e.target.value || undefined } }))} fullWidth /> - setStructuredCv((prev) => ({ ...prev, contact: { ...prev.contact, linkedIn: e.target.value || undefined } }))} fullWidth sx={{ gridColumn: { xs: "1 / -1", md: "1 / -1" } }} /> - - - - - setStructuredCv((prev) => ({ ...prev, summary: splitLines(e.target.value) }))} - helperText={t("profileCvStructuredListHelp")} - multiline - minRows={5} - fullWidth - /> - - - - setStructuredCv((prev) => ({ ...prev, skills: splitLines(e.target.value) }))} - helperText={t("profileCvStructuredListHelp")} - multiline - minRows={5} - fullWidth - /> - - - - setStructuredCv((prev) => ({ ...prev, interests: splitLines(e.target.value) }))} - helperText={t("profileCvStructuredListHelp")} - multiline - minRows={4} - fullWidth - /> - - - - - - - {t("profileCvStructuredLanguages")} - - - - {structuredCv.languages.length === 0 ? {t("profileCvStructuredEmpty")} : null} - {structuredCv.languages.map((language, index) => ( - - - setStructuredCv((prev) => ({ ...prev, languages: prev.languages.map((entry, entryIndex) => entryIndex === index ? { ...entry, name: e.target.value || undefined } : entry) }))} fullWidth /> - setStructuredCv((prev) => ({ ...prev, languages: prev.languages.map((entry, entryIndex) => entryIndex === index ? { ...entry, level: e.target.value || undefined } : entry) }))} fullWidth /> - setStructuredCv((prev) => ({ ...prev, languages: prev.languages.map((entry, entryIndex) => entryIndex === index ? { ...entry, notes: e.target.value || undefined } : entry) }))} fullWidth /> - - - - ))} - - - - - {t("profileCvStructuredJobs")} - - - {structuredCv.jobs.length === 0 ? {t("profileCvStructuredEmpty")} : null} - {structuredCv.jobs.map((job, index) => ( - - - setStructuredCv((prev) => ({ ...prev, jobs: prev.jobs.map((entry, entryIndex) => entryIndex === index ? { ...entry, title: e.target.value || undefined } : entry) }))} fullWidth /> - setStructuredCv((prev) => ({ ...prev, jobs: prev.jobs.map((entry, entryIndex) => entryIndex === index ? { ...entry, company: e.target.value || undefined } : entry) }))} fullWidth /> - setStructuredCv((prev) => ({ ...prev, jobs: prev.jobs.map((entry, entryIndex) => entryIndex === index ? { ...entry, location: e.target.value || undefined } : entry) }))} fullWidth /> - - setStructuredCv((prev) => ({ ...prev, jobs: prev.jobs.map((entry, entryIndex) => entryIndex === index ? { ...entry, start: e.target.value || undefined } : entry) }))} fullWidth /> - setStructuredCv((prev) => ({ ...prev, jobs: prev.jobs.map((entry, entryIndex) => entryIndex === index ? { ...entry, end: e.target.value || undefined, isCurrent: /present|current/i.test(e.target.value) || entry.isCurrent } : entry) }))} fullWidth /> - - setStructuredCv((prev) => ({ ...prev, jobs: prev.jobs.map((entry, entryIndex) => entryIndex === index ? { ...entry, bullets: splitLines(e.target.value) } : entry) }))} helperText={t("profileCvStructuredListHelp")} multiline minRows={5} fullWidth sx={{ gridColumn: { xs: "1 / -1", md: "1 / -1" } }} /> - setStructuredCv((prev) => ({ ...prev, jobs: prev.jobs.map((entry, entryIndex) => entryIndex === index ? { ...entry, skills: splitLines(e.target.value) } : entry) }))} helperText={t("profileCvStructuredListHelp")} multiline minRows={3} fullWidth sx={{ gridColumn: { xs: "1 / -1", md: "1 / -1" } }} /> - - - - - - ))} - - - - - {t("profileCvStructuredEducation")} - - - {structuredCv.education.length === 0 ? {t("profileCvStructuredEmpty")} : null} - {structuredCv.education.map((education, index) => ( - - - setStructuredCv((prev) => ({ ...prev, education: prev.education.map((entry, entryIndex) => entryIndex === index ? { ...entry, qualification: e.target.value || undefined } : entry) }))} fullWidth /> - setStructuredCv((prev) => ({ ...prev, education: prev.education.map((entry, entryIndex) => entryIndex === index ? { ...entry, institution: e.target.value || undefined } : entry) }))} fullWidth /> - setStructuredCv((prev) => ({ ...prev, education: prev.education.map((entry, entryIndex) => entryIndex === index ? { ...entry, location: e.target.value || undefined } : entry) }))} fullWidth /> - - setStructuredCv((prev) => ({ ...prev, education: prev.education.map((entry, entryIndex) => entryIndex === index ? { ...entry, start: e.target.value || undefined } : entry) }))} fullWidth /> - setStructuredCv((prev) => ({ ...prev, education: prev.education.map((entry, entryIndex) => entryIndex === index ? { ...entry, end: e.target.value || undefined } : entry) }))} fullWidth /> - - setStructuredCv((prev) => ({ ...prev, education: prev.education.map((entry, entryIndex) => entryIndex === index ? { ...entry, details: splitLines(e.target.value) } : entry) }))} helperText={t("profileCvStructuredListHelp")} multiline minRows={4} fullWidth sx={{ gridColumn: { xs: "1 / -1", md: "1 / -1" } }} /> - - - - - - ))} - - - - - {t("profileCvStructuredOtherSections")} - - - {structuredCv.otherSections.length === 0 ? {t("profileCvStructuredEmpty")} : null} - {structuredCv.otherSections.map((section, index) => ( - - - setStructuredCv((prev) => ({ ...prev, otherSections: prev.otherSections.map((entry, entryIndex) => entryIndex === index ? { ...entry, title: e.target.value || undefined } : entry) }))} fullWidth /> - - setStructuredCv((prev) => ({ ...prev, otherSections: prev.otherSections.map((entry, entryIndex) => entryIndex === index ? { ...entry, items: splitLines(e.target.value) } : entry) }))} helperText={t("profileCvStructuredListHelp")} multiline minRows={4} fullWidth sx={{ gridColumn: { xs: "1 / -1", md: "1 / -1" } }} /> - - - ))} - - - - - - Template-driven CV builder - - Choose a template, optionally target one section, and tailor the output toward a saved job or free-text role target. The preview below renders the actual PDF layout before you apply it. - - - - - {selectedRewriteJob ? : null} - {rewriteReady ? : null} - - - - - - - - - - {selectedRewriteTemplate.eyebrow} - {selectedRewriteTemplate.title} - {selectedRewriteTemplate.blurb} - - setRewritePreviewTemplate(selectedRewriteTemplate)}> - - - - - - - {selectedRewriteTemplate.eyebrow} - {selectedRewriteTemplate.sampleHeading} - {selectedRewriteTemplate.sampleMeta} - - - Preview of the generated PDF style - {selectedRewriteTemplate.sampleBullets.map((bullet) => ( - • {bullet} - ))} - - - - - - - - - - - Choose a visual direction before generating - - {REWRITE_TEMPLATES.map((option) => { - const selected = option.id === cvSectionStyle; - return ( - setCvSectionStyle(option.id)} - onKeyDown={(event) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - setCvSectionStyle(option.id); - } - }} - sx={{ - p: 1.15, - borderRadius: 3, - cursor: "pointer", - border: "1px solid", - borderColor: selected ? "primary.main" : "divider", - background: selected ? `linear-gradient(180deg, ${option.accent}10 0%, rgba(255,255,255,0.98) 100%)` : "rgba(255,255,255,0.84)", - boxShadow: selected ? "0 0 0 1px rgba(25,118,210,0.16), 0 10px 24px rgba(15,23,42,0.08)" : "0 6px 16px rgba(15,23,42,0.04)", - transition: "transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease", - '&:hover': { transform: 'translateY(-1px)' }, - }} - > - - - {option.eyebrow} - - {option.sampleHeading} - {option.sampleMeta} - - - - - - {option.title} - {option.blurb} - - {selected ? : null} - - - - - ); - })} - - - - - - - - setCvPromptBackground(e.target.value)} - fullWidth - multiline - minRows={4} - helperText="Describe your strengths, preferred emphasis, industry background, or the angle you want the AI to lean into." - sx={{ gridColumn: { xs: "1 / -1", md: "1 / -1" } }} - /> - - {t("profileCvSectionLabel")} - - - setCvSectionTargetRole(e.target.value)} - fullWidth - helperText={selectedRewriteJob ? `Using saved job context: ${selectedRewriteJob.jobTitle}` : "Leave empty to let the selected job drive tailoring."} - /> - - Language - - - - Tone - - - - Saved job context - - - - - - - Builder output - - {selectedRewriteTemplate.title} · {rewritePreview?.targetRole || selectedRewriteJob?.jobTitle || cvSectionTargetRole || "General reuse"} - - - - - - - - - - - - - {rewritePreview?.sectionName || "Full rewritten CV text"} - {rewriteReady ? : null} - - - {rewriteReady ? ( - {rewritePreview?.sectionName ? rewritePreview?.rewrittenText : rewritePreview?.fullText} - ) : ( - Choose a template and generate a live preview. The builder will show rewritten content here and render the PDF layout beside it. - )} - - - - - - - - - - - PDF carousel - - {activePdfItem?.title ? `${activePdfItem.title} · generated PDF` : `${selectedRewriteTemplate.title} · print-ready layout`} - - - {activePdfItem?.fileName ? : rewriteReady ? : null} - - - {pdfCarousel.length > 0 ? ( - <> - - {pdfCarousel.map((item, index) => ( - - ))} - - - {activePdfItem?.status === "ready" && activePdfItem.pdfUrl ? ( -