import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Accordion, AccordionDetails, AccordionSummary, Alert, Avatar, Box, Button, Checkbox, Chip, FormControl, FormControlLabel, InputLabel, LinearProgress, MenuItem, Paper, Select, Tab, Tabs, 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 UploadFileOutlinedIcon from "@mui/icons-material/UploadFileOutlined"; import { api, getApiErrorMessage } from "../api"; import CropImageDialog from "../components/CropImageDialog"; import ProfileCompleteness from "./career/ProfileCompleteness"; import CareerWorkspaceOverview from "./career/CareerWorkspaceOverview"; import CareerEvidenceBank from "./career/CareerEvidenceBank"; import { EducationSection, InterestsSection, LanguagesSection, LongTailSections, OtherSectionsSection, PersonalInformationSection, ProfessionalSummarySection, SkillsSection, WorkExperienceSection, } from "./career/CareerProfileSections"; import { useToast } from "../toast"; import { useI18n } from "../i18n/I18nProvider"; import { useAccountPlan } from "../accountPlan"; import type { TranslationKey } from "../i18n/translations"; import ProFeatureNotice from "../components/ProFeatureNotice"; import { PageSection } from "../layout/PageLayout"; import type { UserOperation } from "../types"; import { emptyStructuredCv, getStructuredCvFieldMetadata, normalizeStructuredCv, StructuredCvProfile, } from "../profileCv"; type ExtractionRun = { id: number; trigger: string; status: string; artifactFileName?: string; startedAtUtc: string; completedAtUtc?: string; appliedAtUtc?: string; parserVersion: string; normalizerVersion: string; llmPromptVersion: string; errorMessage?: string; operation?: UserOperation | null; }; type CvImportChange = { id: string; label: string; confidence: string; fieldChanges?: Array<{ field: string; oldValue?: string | null; newValue?: string | null }> }; type CvImportDiff = { totalAdded: number; totalUpdated: number; totalLowConfidence: number; hasChanges: boolean; categories: Array<{ category: string; added: CvImportChange[]; updated: CvImportChange[]; unchangedCount: number; lowConfidenceCount: number; }>; }; type CvRunDiffResponse = { runId: number; status: string; diff: CvImportDiff }; type QueuedCvRunResponse = { queued: boolean; extractionRunId: number; status: string; operation?: UserOperation | null; statusUrl?: string | null; created: boolean; }; const activeOperationStatuses = new Set(["queued", "running", "waiting_for_retry", "waiting_for_external_fallback"]); const activeRunLabels = new Set(["queued", "running", "processing locally", "waiting to retry", "waiting for approved fallback"]); function cvRunStatus(run: ExtractionRun) { const operation = run.operation; if (!operation || operation.status === "succeeded") return run.status; if (operation.cancellationRequestedAtUtc) return "cancellation requested"; switch (operation.status) { case "running": return "processing locally"; case "waiting_for_retry": return "waiting to retry"; case "waiting_for_external_fallback": return "waiting for approved fallback"; default: return operation.status; } } const importLabelKeys: Record = { Contact: "careerProfileContactSection", Summary: "profileCvStructuredSummary", "Professional summary": "profileCvStructuredSummary", Experience: "cvSectionExperience", Education: "cvSectionEducation", Projects: "cvSectionProjects", Certifications: "cvSectionCertifications", Languages: "cvSectionLanguages", Skills: "cvSectionSkills", Interests: "cvSectionInterests", Awards: "cvSectionAwards", Publications: "cvSectionPublications", Organisations: "cvSectionOrganisations", References: "cvSectionReferences", "Full name": "profileCvContactFullName", Headline: "profileCvContactHeadline", Email: "profileCvContactEmail", Phone: "profileCvContactPhone", Location: "profileCvContactLocation", Website: "profileCvContactWebsite", LinkedIn: "profileCvContactLinkedIn", Level: "profileCvLanguageLevel", Dates: "careerImportDates", Date: "careerImportDates", Bullets: "careerImportDetails", Details: "careerImportDetails", Issuer: "careerImportIssuer", queued: "careerImportQueued", running: "careerImportProcessing", "processing locally": "careerImportProcessing", "waiting to retry": "careerImportWaitingRetry", "waiting for approved fallback": "careerImportWaitingFallback", "cancellation requested": "operationsCancellationRequested", pending_review: "careerImportPendingReview", applied: "careerImportApplied", discarded: "careerImportDiscarded", failed: "careerImportFailed", cancelled: "careerImportCancelled", upload: "careerImportUpload", rebuild: "careerImportRebuild", improve: "careerImportImprove", reprocess: "careerImportReprocess", }; 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"; 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(); } // Phase 3: the master profile now comes from the relational source of truth via /career/profile. type CareerSectionStatus = { key: string; label: string; complete: boolean; count: number }; type CareerCompleteness = { percent: number; missing: string[]; sections: CareerSectionStatus[] }; type CareerProfileResponse = { profile: StructuredCvProfile; completeness: CareerCompleteness; cvText?: string | null }; type CareerVersion = { version: number; source: string; createdAtUtc: string; isCurrent: boolean }; type CareerWorkspaceSection = "overview" | "profile" | "evidence" | "import"; function initialWorkspaceSection(): CareerWorkspaceSection { if (typeof window === "undefined") return "overview"; const section = new URLSearchParams(window.location.search).get("section"); return section === "profile" || section === "evidence" || section === "import" ? section : "overview"; } // CareerProfilePage backs /career: the master career profile — the single editable source of // truth for all future generated documents. Split out from ProfilePage in Phase 2.2; wired to the // relational /career/profile API in Phase 3. export default function CareerProfilePage() { const { canUseAi } = useAccountPlan(); // Retained so the shared JSX (copied from ProfilePage) reads identically; hardcoded for /career. const careerOnly = true; const { toast } = useToast(); const { language, t } = useI18n(); const translationRef = useRef(t); useEffect(() => { translationRef.current = t; }, [t]); const cvInputRef = useRef(null); const avatarInputRef = useRef(null); const [me, setMe] = useState(null); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(null); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); const profileMutationPending = useRef(false); 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 [headline, setHeadline] = useState(""); const [profileCvText, setProfileCvText] = useState(""); const [parsingCvSections, setParsingCvSections] = useState(false); const [reprocessingCv, setReprocessingCv] = useState(false); const [structuredCv, setStructuredCv] = useState(emptyStructuredCv()); const [profileDirty, setProfileDirty] = useState(false); const [completeness, setCompleteness] = useState(null); const [versions, setVersions] = useState([]); const [versionsLoading, setVersionsLoading] = useState(true); const [versionsError, setVersionsError] = useState(null); const versionsReadId = useRef(0); const [restoreError, setRestoreError] = useState(null); // The raw import/section parser remains available as an advanced recovery tool. const [showAdvancedCvTools, setShowAdvancedCvTools] = useState(false); const [workspaceSection, setWorkspaceSection] = useState(initialWorkspaceSection); const [evidenceVisited, setEvidenceVisited] = useState(() => initialWorkspaceSection() === "evidence"); const navigateWorkspace = useCallback((next: CareerWorkspaceSection) => { setWorkspaceSection(next); if (next === "evidence") setEvidenceVisited(true); if (typeof window === "undefined") return; const url = new URL(window.location.href); if (next === "overview") url.searchParams.delete("section"); else url.searchParams.set("section", next); window.history.replaceState(window.history.state, "", `${url.pathname}${url.search}${url.hash}`); }, []); const loadVersions = useCallback(async () => { const readId = ++versionsReadId.current; setVersionsLoading(true); try { const r = await api.get("/career/profile/versions"); if (readId === versionsReadId.current) { setVersions(r.data ?? []); setVersionsError(null); } } catch (error) { if (readId === versionsReadId.current) setVersionsError(getApiErrorMessage(error, "")); } finally { if (readId === versionsReadId.current) setVersionsLoading(false); } }, []); const restoreVersion = useCallback(async (version: number) => { if (profileMutationPending.current || me?.provider !== "local" || loading || profileDirty) return; profileMutationPending.current = true; setLoading(true); setRestoreError(null); try { const r = await api.post(`/career/profile/versions/${version}/restore`); setStructuredCv(normalizeStructuredCv(r.data?.profile ?? emptyStructuredCv())); setCompleteness(r.data?.completeness ?? null); setProfileDirty(false); invalidateImportComparisons(); await loadVersions(); toast(t("profileUpdated"), "success"); } catch (error) { const message = getApiErrorMessage(error, ""); setRestoreError(message); toast(message || translationRef.current("careerRestoreFailed"), "error"); } finally { profileMutationPending.current = false; setLoading(false); } }, [loadVersions, loading, me?.provider, profileDirty, t, toast]); const [extractionRuns, setExtractionRuns] = useState([]); const [runsLoading, setRunsLoading] = useState(true); const [runsLoadError, setRunsLoadError] = useState(null); const runsReadId = useRef(0); const [runDiffs, setRunDiffs] = useState>({}); const [runDiffErrors, setRunDiffErrors] = useState>({}); const [runDiffLoading, setRunDiffLoading] = useState>({}); const diffReadGeneration = useRef(0); const diffReadIds = useRef>({}); const [reviewingRunId, setReviewingRunId] = useState(null); const [reviewAction, setReviewAction] = useState<"accept" | "discard" | null>(null); const reviewPending = useRef(false); const [runActionErrors, setRunActionErrors] = useState>({}); const operationPending = useRef(new Set()); const [pendingOperations, setPendingOperations] = useState([]); const [acceptedLowConfidenceIds, setAcceptedLowConfidenceIds] = useState>({}); const runStatusRef = useRef>({}); function invalidateImportComparisons() { // A comparison and its choices refer to the source that was current when it was read. ++diffReadGeneration.current; setRunDiffs({}); setRunDiffErrors({}); setRunDiffLoading({}); setAcceptedLowConfidenceIds({}); } const loadRunDiff = useCallback(async (runId: number) => { const generation = diffReadGeneration.current; const readId = (diffReadIds.current[runId] ?? 0) + 1; diffReadIds.current[runId] = readId; const isCurrent = () => generation === diffReadGeneration.current && readId === diffReadIds.current[runId]; setRunDiffLoading((current) => ({ ...current, [runId]: true })); setRunDiffErrors((current) => { const next = { ...current }; delete next[runId]; return next; }); try { const response = await api.get(`/profile-cv/runs/${runId}/diff`); if (isCurrent()) setRunDiffs((current) => ({ ...current, [runId]: response.data.diff })); } catch (error) { if (isCurrent()) setRunDiffErrors((current) => ({ ...current, [runId]: getApiErrorMessage(error, "") })); } finally { if (isCurrent()) setRunDiffLoading((current) => ({ ...current, [runId]: false })); } }, []); const editStructuredCv = useCallback((next: React.SetStateAction) => { setStructuredCv(next); setProfileDirty(true); }, []); const loadExtractionRuns = useCallback(async () => { const readId = ++runsReadId.current; setRunsLoading(true); try { const response = await api.get("/profile-cv/runs"); if (readId === runsReadId.current) { setExtractionRuns(response.data ?? []); setRunsLoadError(null); } } catch (error) { // Polling failure must not clear existing run state or touch unsaved profile edits. if (readId === runsReadId.current) setRunsLoadError(String(error)); } finally { if (readId === runsReadId.current) setRunsLoading(false); } }, []); const queueCvUpload = useCallback(async (file: File) => { const formData = new FormData(); formData.append("file", file); setUploadingCv(true); try { const res = await api.post("/profile-cv/upload", formData, { headers: { "Content-Type": "multipart/form-data" } }); await loadExtractionRuns(); toast(t("profileCvQueuedUpload", { id: res.data.extractionRunId }), "info"); } catch (error) { toast(getApiErrorMessage(error, t("profileCvUploadFailed")), "error"); } finally { setUploadingCv(false); } }, [loadExtractionRuns, t, toast]); const loadProfile = useCallback(async () => { setLoading(true); try { // /career reads the structured profile from the relational source of truth (/career/profile); // /auth/me still provides the account row (avatar, provider chips) shown in the header. const [careerResponse, meResponse] = await Promise.all([ api.get("/career/profile"), api.get("/auth/me"), ]); setMe(meResponse.data); setProfileCvText(careerResponse.data?.cvText ?? ""); const profile = normalizeStructuredCv(careerResponse.data?.profile ?? emptyStructuredCv()); setStructuredCv(profile); setCompleteness(careerResponse.data?.completeness ?? null); setProfileDirty(false); setHeadline(profile.contact.headline ?? ""); setLoadError(null); } catch (error) { setMe(null); setLoadError(getApiErrorMessage(error, "")); } finally { setLoading(false); } }, []); useEffect(() => { void loadProfile(); void loadExtractionRuns(); void loadVersions(); }, [loadExtractionRuns, loadProfile, loadVersions]); useEffect(() => { const activeRuns = extractionRuns.filter((run) => run.operation ? activeOperationStatuses.has(run.operation.status) : run.status === "queued" || run.status === "running"); if (activeRuns.length === 0) return; const timer = window.setInterval(() => { void loadExtractionRuns(); }, 4000); return () => window.clearInterval(timer); }, [extractionRuns, loadExtractionRuns]); useEffect(() => { for (const run of extractionRuns) { if (run.status === "pending_review" && !runDiffs[run.id] && !runDiffLoading[run.id] && runDiffErrors[run.id] === undefined) void loadRunDiff(run.id); } }, [extractionRuns, loadRunDiff, runDiffErrors, runDiffLoading, runDiffs]); const changeRunOperation = async (run: ExtractionRun, action: "cancel" | "retry") => { const operation = run.operation; if (!isLocal || !operation || operationPending.current.has(operation.id)) return; const runRegion = document.getElementById(`career-import-run-${run.id}`); if (runRegion?.contains(document.activeElement)) runRegion.focus(); operationPending.current.add(operation.id); setPendingOperations([...operationPending.current]); setRunActionErrors((current) => { const next = { ...current }; delete next[run.id]; return next; }); try { await api.post(`/operations/${operation.id}/${action}`); await loadExtractionRuns(); toast(translationRef.current(action === "cancel" ? "profileCvCancelRequested" : "profileCvRetryQueued"), "info"); } catch (error) { const key = action === "cancel" ? "profileCvCancelFailed" : "profileCvRetryFailed"; const message = getApiErrorMessage(error, ""); setRunActionErrors((current) => ({ ...current, [run.id]: { key, message } })); toast(message || translationRef.current(key), "error"); } finally { operationPending.current.delete(operation.id); setPendingOperations([...operationPending.current]); } }; const reviewRun = async (runId: number, action: "accept" | "discard") => { if (!isLocal || loading || saving || reviewPending.current || profileMutationPending.current || (action === "accept" && (profileDirty || !runDiffs[runId]))) return; const runRegion = document.getElementById(`career-import-run-${runId}`); if (runRegion?.contains(document.activeElement)) runRegion.focus(); reviewPending.current = true; if (action === "accept") profileMutationPending.current = true; setReviewingRunId(runId); setReviewAction(action); setRunActionErrors((current) => { const next = { ...current }; delete next[runId]; return next; }); try { if (action === "accept") await api.post(`/profile-cv/runs/${runId}/accept`, { acceptedLowConfidenceIds: acceptedLowConfidenceIds[runId] ?? [] }); else await api.post(`/profile-cv/runs/${runId}/discard`); setExtractionRuns((current) => current.map((run) => run.id === runId ? { ...run, status: action === "accept" ? "applied" : "discarded" } : run)); if (action === "accept") { invalidateImportComparisons(); await Promise.all([loadProfile(), loadExtractionRuns(), loadVersions()]); } else await loadExtractionRuns(); toast(translationRef.current(action === "accept" ? "profileCvChangesApplied" : "profileCvExtractionDiscarded"), action === "accept" ? "success" : "info"); } catch (error) { const key = action === "accept" ? "profileCvApplyChangesFailed" : "profileCvDiscardFailed"; const message = getApiErrorMessage(error, ""); setRunActionErrors((current) => ({ ...current, [runId]: { key, message } })); toast(message || translationRef.current(key), "error"); } finally { if (action === "accept") profileMutationPending.current = false; reviewPending.current = false; setReviewingRunId(null); setReviewAction(null); } }; useEffect(() => { const previous = runStatusRef.current; for (const run of extractionRuns) { const status = cvRunStatus(run); const prior = previous[run.id]; if (activeRunLabels.has(prior) && status === "pending_review") { toast(t("profileCvRunReady", { trigger: run.trigger }), "info"); } if (activeRunLabels.has(prior) && status === "failed") { toast(run.errorMessage || t("profileCvRunFailed", { trigger: run.trigger }), "error"); } previous[run.id] = status; } }, [extractionRuns, t, toast]); // Field-review lookup passed to the extracted sections. They stay decoupled from the full profile // shape; the parent still owns structuredCv and the metadata source. const metaFor = useCallback((path: string) => getStructuredCvFieldMetadata(structuredCv, path), [structuredCv]); const initials = useMemo(() => initialsFrom([me?.displayName, me?.firstName, me?.lastName, me?.userName, me?.email]), [me]); const isLocal = me?.provider === "local"; const editorDisabled = !isLocal || loading || saving || parsingCvSections || (reviewingRunId !== null && reviewAction === "accept"); const loadErrorMessage = loadError === null ? null : loadError || t("profileLoadFailed"); 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 activeCvRun = extractionRuns.find((run) => run.operation ? activeOperationStatuses.has(run.operation.status) : run.status === "queued" || run.status === "running"); const pendingCvRun = extractionRuns.find((run) => run.status === "pending_review"); const extractionProgress = activeCvRun?.operation?.progressPercent; const importLabel = (value: string) => Object.hasOwn(importLabelKeys, value) ? t(importLabelKeys[value]) : value; const importChangeLabel = (change: CvImportChange, category: string) => category === "Professional summary" ? importLabel(category) : change.id === "Contact" && change.confidence === "Low" && change.label === "Contact details" ? t("careerImportContactDetails") : change.label; return ( {t("careerWorkspaceNavigation")} navigateWorkspace(value)} aria-label={t("careerWorkspaceNavigation")} sx={{ display: { xs: "none", sm: "flex" }, px: 1 }} > {workspaceSection === "overview" ? ( void loadExtractionRuns()} onRetryProfile={() => { if (!profileDirty && !profileMutationPending.current) void loadProfile(); }} profileDirty={profileDirty} onNavigate={navigateWorkspace} /> ) : null} {evidenceVisited ? : null} {workspaceSection === "profile" || workspaceSection === "import" ? { const file = event.target.files?.[0]; event.target.value = ""; if (file) void queueCvUpload(file); }} /> {workspaceSection === "profile" ? void restoreVersion(version)} showSummary={false} versionsLoading={versionsLoading} versionsError={versionsError} onRetryVersions={() => void loadVersions()} restoreDisabled={profileDirty} restoreError={restoreError} /> : null} {workspaceSection === "profile" ? { 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); } }} /> : null} {loadErrorMessage ? ( { if (!profileDirty && !profileMutationPending.current) void loadProfile(); }}>{t("retry")}}> {t("profileLoadFailed")} {loadError ? {loadError} : null} ) : null} {workspaceSection === "profile" ? {t("careerProfileImportTitle")} {t("careerProfileImportBody")} {uploadingCv || activeCvRun ? {typeof extractionProgress === "number" ? t("careerProfileImportPercent", { percent: extractionProgress }) : t("careerProfileImportProcessing")} : null} {pendingCvRun ? navigateWorkspace("import")}>{t("careerProfileImportReview")}}> {t("careerProfileImportReady")} : null} {!canUseAi ? {t("profileProImportBody")} : null} : null} {workspaceSection === "profile" ? {initials} { const file = event.target.files?.[0] ?? null; event.target.value = ""; if (!file) return; setAvatarFile(file); setCropOpen(true); }} /> {me?.avatarImageDataUrl ? ( ) : null} {careerOnly ? t("careerWorkspaceProfileTab") : t("profileTitle")} {me?.userName || me?.displayName || fullName || me?.email || "-"} {headline || t("profileHeadlinePlaceholder")} : null} {!canUseAi && ( {t("profileProImportBody")} )} {profileDirty ? {t("profileUnsavedBeforeActions")} : null} {t("profileMasterCv")} {t("profileMasterCvBody")} {uploadingCv ? : null} {t("profileCvStructuredDefaultHint")} }> {t("profileCvRawPanelTitle")} {t("profileCvRawPanelHelp")} {t("careerImportRawSourceHelp")} { setProfileCvText(e.target.value); setProfileDirty(true); }} helperText={t("profileCvTextHelp")} multiline minRows={12} disabled={editorDisabled} fullWidth /> {runsLoading ? : null} {runsLoadError !== null ? void loadExtractionRuns()}>{t("retry")}}>{t("careerOverviewImportUnavailable")} : null} {latestRun ? ( {extractionRuns.map((run) => ( {importLabel(run.trigger)} · {run.id} {run.id === structuredCv.metadata.appliedExtractionRunId ? : null} {run.artifactFileName || t("profileCvNoStoredArtifact")} {run.parserVersion} · {new Date(run.startedAtUtc).toLocaleString(language === "nb" ? "nb-NO" : "en")} {run.errorMessage ? ( {run.errorMessage} ) : null} {run.operation?.canCancel ? ( ) : null} {run.operation?.canRetry ? ( ) : null} {runActionErrors[run.id] ? {t(runActionErrors[run.id].key)} {runActionErrors[run.id].message ? {runActionErrors[run.id].message} : null} : null} {run.status === "pending_review" ? ( {runDiffs[run.id] ? ( <> {t("profileCvDiffSummary", { added: runDiffs[run.id].totalAdded, updated: runDiffs[run.id].totalUpdated })} {runDiffs[run.id].totalLowConfidence ? ` | ${t("profileCvNeedAttention", { count: runDiffs[run.id].totalLowConfidence })}` : ""} {t("careerImportComparisonScope")} {!runDiffs[run.id].hasChanges ? {t("careerImportNoComparedChanges")} : null} {runDiffs[run.id].categories.filter((category) => category.added.length || category.updated.length).map((category) => ( {importLabel(category.category)} {[...category.added.map((change) => ({ ...change, added: true })), ...category.updated.map((change) => ({ ...change, added: false }))].map((change) => ( {importChangeLabel(change, category.category)} · {t(change.added ? "careerImportAdded" : "careerImportUpdated")} {(change.fieldChanges ?? []).map((field) => {importLabel(field.field)} {t("careerImportCurrentValue", { value: field.oldValue || t("careerImportEmptyValue") })} {t("careerImportIncomingValue", { value: field.newValue || t("careerImportEmptyValue") })} )} ))} ))} ) : runDiffErrors[run.id] !== undefined ? { document.getElementById(`career-import-run-${run.id}`)?.focus(); void loadRunDiff(run.id); }}>{t("retry")}}> {t("careerImportComparisonFailed")} {runDiffErrors[run.id] ? {runDiffErrors[run.id]} : null} : } {runDiffs[run.id]?.categories.flatMap((category) => [...category.added, ...category.updated].map((change) => ({ ...change, category: category.category }))).filter((change) => change.confidence === "Low").map((change) => ( setAcceptedLowConfidenceIds((current) => ({ ...current, [run.id]: event.target.checked ? [...(current[run.id] ?? []), change.id] : (current[run.id] ?? []).filter((id) => id !== change.id), }))} />} label={t("profileCvIncludeLowConfidence", { category: importLabel(change.category), label: importChangeLabel(change, change.category) })} /> ))} {profileDirty ? ( {t("profileCvSaveBeforeImport")} ) : null} ) : null} ))} ) : !runsLoading && runsLoadError === null ? ( {t("profileCvExtractionHistoryEmpty")} ) : null} {t("careerImportParseWarning")} {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")} )} editStructuredCv((prev) => ({ ...prev, contact: next }))} getMetadata={metaFor} disabled={editorDisabled} /> editStructuredCv((prev) => ({ ...prev, summary: next }))} getMetadata={metaFor} disabled={editorDisabled} /> editStructuredCv((prev) => ({ ...prev, skills: next }))} getMetadata={metaFor} disabled={editorDisabled} /> editStructuredCv((prev) => ({ ...prev, interests: next }))} getMetadata={metaFor} disabled={editorDisabled} /> editStructuredCv((prev) => ({ ...prev, [key]: next }))} disabled={editorDisabled} /> editStructuredCv((prev) => ({ ...prev, languages: next }))} getMetadata={metaFor} disabled={editorDisabled} /> editStructuredCv((prev) => ({ ...prev, jobs: next }))} disabled={editorDisabled} /> editStructuredCv((prev) => ({ ...prev, education: next }))} disabled={editorDisabled} /> editStructuredCv((prev) => ({ ...prev, otherSections: next }))} disabled={editorDisabled} /> {t("profileCvSectionWordCount", { count: cvWordCount })} {t("profileCvPreferredUploads")} {t("profileCvStructuredEditorHelp")} {saveError !== null ? {t("profileUpdateFailed")} {saveError ? {saveError} : null} : null} {profileDirty ? : null} : null} ); }