import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Accordion, AccordionDetails, AccordionSummary, Alert, Avatar, Box, Button, Chip, Dialog, DialogContent, DialogTitle, Divider, FormControl, IconButton, InputLabel, LinearProgress, MenuItem, Paper, Select, TextField, Typography } from "@mui/material"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import PhotoCameraOutlinedIcon from "@mui/icons-material/PhotoCameraOutlined"; import ZoomInOutlinedIcon from "@mui/icons-material/ZoomInOutlined"; import { api, getApiErrorMessage } from "../api"; import GoogleAuthCard from "../components/GoogleAuthCard"; import CropImageDialog from "../components/CropImageDialog"; import { useToast } from "../toast"; import { useI18n } from "../i18n/I18nProvider"; import { emptyStructuredCv, getStructuredCvFieldMetadata, joinLines, normalizeStructuredCv, parseStructuredCvJson, splitLines, StructuredCvFieldMetadata, StructuredCvProfile, } from "../profileCv"; import { JobApplication } from "../types"; type CvSectionOption = "" | "Professional Summary" | "Core Skills" | "Experience Highlights" | "Selected Achievements" | "Projects"; type CvSectionStyle = "ats-minimal" | "harvard" | "auckland" | "edinburgh" | "monarch" | "fjord"; type ExtractionRun = { id: number; trigger: string; status: string; artifactFileName?: string; startedAtUtc: string; completedAtUtc?: string; appliedAtUtc?: string; parserVersion: string; normalizerVersion: string; llmPromptVersion: string; errorMessage?: string; }; type QueuedCvRunResponse = { queued: boolean; extractionRunId: number; status: string; }; type JobListResponse = { items: JobApplication[]; total: number; page: number; pageSize: number; }; type RewriteTemplateOption = { id: CvSectionStyle; title: string; eyebrow: string; accent: string; blurb: string; sampleHeading: string; sampleMeta: string; sampleBullets: string[]; }; type CvBuilderPreview = { templateId: CvSectionStyle; html: string; suggestedFileName: string; fullText: string; rewrittenText: string; structuredCv: StructuredCvProfile; sectionName?: string | null; targetRole?: string | null; jobApplicationId?: number | null; }; type PdfCarouselItem = { templateId: CvSectionStyle; title: string; fileName: string; pdfUrl?: string; status: "loading" | "ready" | "error"; error?: string; }; type RewriteRequestPayload = { sectionName: string | null; style: CvSectionStyle; templateId: CvSectionStyle; targetRole: string | null; jobApplicationId: number | null; sourceText: string | null; }; type MeResponse = { provider?: "local" | "google" | "external"; id?: string; email?: string; userName?: string; firstName?: string; lastName?: string; displayName?: string; profileCvText?: string; profileCvStructureJson?: string; avatarImageDataUrl?: string; roles?: string[]; googleLink?: { linked: boolean; email?: string | null; linkedAt?: string | null; } | null; }; const CV_UPLOAD_ACCEPT = ".pdf,.docx,.txt,.md,image/png,image/jpeg,image/webp,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/plain,text/markdown"; const AVATAR_UPLOAD_ACCEPT = "image/png,image/jpeg,image/webp"; const REWRITE_TEMPLATES: RewriteTemplateOption[] = [ { id: "ats-minimal", title: "ATS Minimal", eyebrow: "Scanner-friendly", accent: "#0f172a", blurb: "Compact, direct, and easy for screening systems to parse.", sampleHeading: "Senior Backend Engineer", sampleMeta: "Acme Systems · Oslo · 2021 - Present", sampleBullets: ["Built API workflows with measurable delivery outcomes.", "Kept skills and achievements easy to scan."] }, { id: "harvard", title: "Harvard", eyebrow: "Traditional", accent: "#7f1d1d", blurb: "Formal hierarchy and restrained tone for conservative hiring flows.", sampleHeading: "Professional Summary", sampleMeta: "Clear structure · precise dates · credible language", sampleBullets: ["Emphasizes polished summaries.", "Works well for broad professional roles."] }, { id: "auckland", title: "Auckland", eyebrow: "Modern sidebar", accent: "#0f766e", blurb: "Sharper highlights with a more contemporary, design-forward rhythm.", sampleHeading: "Selected Impact", sampleMeta: "Focused strengths · compact highlights", sampleBullets: ["Pulls skills into stronger highlight clusters.", "Good when you want a fresher feel."] }, { id: "edinburgh", title: "Edinburgh", eyebrow: "Editorial", accent: "#5b21b6", blurb: "More personality and stronger section contrast without losing clarity.", sampleHeading: "Experience Highlights", sampleMeta: "Premium spacing · stronger visual voice", sampleBullets: ["Useful when the CV should feel more distinctive.", "Still keeps wording grounded and factual."] }, { id: "monarch", title: "Monarch", eyebrow: "Executive", accent: "#7c2d12", blurb: "High-contrast premium presentation for leadership-heavy applications.", sampleHeading: "Executive Profile", sampleMeta: "Leadership clarity · premium hierarchy", sampleBullets: ["Adds more top-level summary emphasis.", "Well suited to senior strategic roles."] }, { id: "fjord", title: "Fjord", eyebrow: "Technical", accent: "#0f4c5c", blurb: "Calm, high-density layout for engineering resumes and project-heavy CVs.", sampleHeading: "Projects & Systems", sampleMeta: "Technical depth · practical readability", sampleBullets: ["Gives projects and skills more weight.", "Better for technical detail without chaos."] }, ]; function initialsFrom(values: Array) { const joined = values.map((x) => (x ?? "").trim()).filter(Boolean); if (joined.length === 0) return "?"; if (joined.length === 1) { const parts = joined[0].split(/[\s@._-]+/).filter(Boolean); if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase(); return (parts[0][0] + parts[1][0]).toUpperCase(); } return (joined[0][0] + joined[1][0]).toUpperCase(); } function confidenceTone(confidence?: number) { if (typeof confidence !== "number") return { label: "Review", color: "default" as const }; if (confidence >= 0.8) return { label: `High ${Math.round(confidence * 100)}%`, color: "success" as const }; if (confidence >= 0.65) return { label: `Medium ${Math.round(confidence * 100)}%`, color: "warning" as const }; return { label: `Low ${Math.round(confidence * 100)}%`, color: "error" as const }; } function FieldReviewNote({ metadata }: { metadata?: StructuredCvFieldMetadata }) { if (!metadata) return null; const tone = confidenceTone(metadata.confidence); return ( {metadata.method ? : null} {metadata.sourceBlockId ? : null} {metadata.reviewState ? : null} {metadata.sourceSnippet ? ( {metadata.sourceSnippet} ) : null} ); } export default function ProfilePage() { 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); 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); const [cvSection, setCvSection] = useState(""); const [cvSectionStyle, setCvSectionStyle] = useState("ats-minimal"); const [cvSectionTargetRole, setCvSectionTargetRole] = useState(""); 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(""); useEffect(() => { return () => { pdfCarousel.forEach((item) => { if (item.pdfUrl) { window.URL.revokeObjectURL(item.pdfUrl); } }); }; }, [pdfCarousel]); 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; 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 ?? []); 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); } }, []); useEffect(() => { 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(" "); const cvWordCount = profileCvText.trim() ? profileCvText.trim().split(/\s+/).length : 0; const providerLabel = me?.provider === "local" ? t("profileLocalAccount") : me?.provider === "google" ? t("profileGoogleSession") : t("profileExternalSession"); const googleLabel = me?.googleLink?.linked ? me.googleLink.email ? t("profileGoogleLinkedWithEmail", { email: me.googleLink.email }) : 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, }), [cvSection, cvSectionTargetRole, 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 ( { setCropOpen(false); setAvatarFile(null); }} onSave={async (blob) => { const file = new File([blob], "avatar.png", { type: "image/png" }); const formData = new FormData(); formData.append("file", file); setUploadingAvatar(true); try { const response = await api.post<{ avatarImageDataUrl?: string }>("/auth/avatar", formData, { headers: { "Content-Type": "multipart/form-data" }, }); setMe((prev) => (prev ? { ...prev, avatarImageDataUrl: response.data?.avatarImageDataUrl ?? prev.avatarImageDataUrl } : prev)); setCropOpen(false); setAvatarFile(null); toast(t("profileImageUpdated"), "success"); } catch (e: any) { toast(String(e?.response?.data || e?.message || t("profileImageUploadFailed")), "error"); } finally { setUploadingAvatar(false); } }} /> {loadError ? ( void loadProfile()}>Retry}> Unable to load profile. {loadError} ) : null} {initials} { const file = event.target.files?.[0] ?? null; event.target.value = ""; if (!file) return; setAvatarFile(file); setCropOpen(true); }} /> {me?.avatarImageDataUrl ? ( ) : null} {t("profileTitle")} {me?.userName || me?.displayName || fullName || me?.email || "-"} {headline || t("profileHeadlinePlaceholder")} {t("profileAccountSection")} {!isLocal ? ( {t("profileReadOnlyInfo")} ) : null} 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 /> {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} {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.5, borderRadius: 3.5, cursor: "pointer", border: "1px solid", borderColor: selected ? "primary.main" : "divider", boxShadow: selected ? "0 0 0 1px rgba(25,118,210,0.18), 0 12px 30px rgba(15,23,42,0.08)" : "0 6px 18px rgba(15,23,42,0.04)", background: selected ? `linear-gradient(180deg, ${option.accent}12 0%, rgba(255,255,255,0.96) 100%)` : "background.paper", transition: "transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease", '&:hover': { transform: 'translateY(-2px)' }, }} > {option.eyebrow} {option.title} { event.stopPropagation(); setRewritePreviewTemplate(option); }}> {option.sampleHeading} {option.sampleMeta} {option.sampleBullets.map((bullet) => ( • {bullet} ))} {option.blurb} ); })} {t("profileCvSectionLabel")} setCvSectionTargetRole(e.target.value)} fullWidth helperText={selectedRewriteJob ? `Using saved job context: ${selectedRewriteJob.jobTitle}` : "Leave empty to let the selected job drive tailoring."} /> 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 ? (