Add CV structure analysis groundwork

This commit is contained in:
cesnimda
2026-03-23 23:07:02 +01:00
parent eb4b517d58
commit 8f04637cff
3 changed files with 157 additions and 0 deletions
+16
View File
@@ -191,6 +191,14 @@ export const translations = {
profileCvTextHelp: "Keep this updated and specific. Include recent roles, tools, achievements, measurable outcomes, and the work you want to be hired for next. If extraction misses something, edit it here manually.",
profileCvPreferredUploads: "Supported uploads: PDF, DOCX, TXT, MD, PNG, JPG, JPEG, WEBP.",
profileCvSectionTools: "Section rewrite tools",
profileCvStructureOverview: "CV structure overview",
profileCvStructureOverviewHelp: "Parse your current CV text into reusable sections so you can spot missing structure before tailoring.",
profileCvStructureParse: "Analyze sections",
profileCvStructureParsing: "Analyzing sections...",
profileCvStructureParsed: "CV structure analyzed.",
profileCvStructureParseFailed: "Failed to analyze CV structure.",
profileCvStructureEmpty: "No parsed sections yet.",
profileCvSectionWordCount: "{count} words",
profileCvSectionToolsHelp: "Generate sharper versions of one CV section at a time before pasting them back into your master CV.",
profileCvSectionLabel: "Section",
profileCvSectionSummary: "Professional Summary",
@@ -991,6 +999,14 @@ export const translations = {
profileCvTextHelp: "Hold denne oppdatert og konkret. Ta med nylige roller, verktøy, prestasjoner, målbare resultater og arbeidet du vil bli ansatt for neste gang. Hvis tekstuttrekket mangler noe, kan du redigere manuelt her.",
profileCvPreferredUploads: "Støttede opplastinger: PDF, DOCX, TXT, MD, PNG, JPG, JPEG, WEBP.",
profileCvSectionTools: "Verktøy for CV-seksjoner",
profileCvStructureOverview: "Oversikt over CV-struktur",
profileCvStructureOverviewHelp: "Analyser gjeldende CV-tekst til gjenbrukbare seksjoner slik at du ser manglende struktur før du tilpasser den.",
profileCvStructureParse: "Analyser seksjoner",
profileCvStructureParsing: "Analyserer seksjoner...",
profileCvStructureParsed: "CV-strukturen er analysert.",
profileCvStructureParseFailed: "Kunne ikke analysere CV-strukturen.",
profileCvStructureEmpty: "Ingen analyserte seksjoner ennå.",
profileCvSectionWordCount: "{count} ord",
profileCvSectionToolsHelp: "Generer skarpere versjoner av én CV-seksjon om gangen før du limer dem tilbake i hoved-CV-en.",
profileCvSectionLabel: "Seksjon",
profileCvSectionSummary: "Profesjonell oppsummering",
+49
View File
@@ -11,6 +11,12 @@ import CropImageDialog from "../components/CropImageDialog";
import { useToast } from "../toast";
import { useI18n } from "../i18n/I18nProvider";
type ParsedCvSection = {
name: string;
content: string;
wordCount: number;
};
type CvSectionOption = "Professional Summary" | "Core Skills" | "Experience Highlights" | "Selected Achievements" | "Projects";
type CvSectionStyle = "balanced" | "concise" | "impact" | "ats";
@@ -94,6 +100,8 @@ export default function ProfilePage() {
const [cvSectionStyle, setCvSectionStyle] = useState<CvSectionStyle>("balanced");
const [cvSectionTargetRole, setCvSectionTargetRole] = useState("");
const [cvSectionDraft, setCvSectionDraft] = useState("");
const [parsingCvSections, setParsingCvSections] = useState(false);
const [parsedCvSections, setParsedCvSections] = useState<ParsedCvSection[]>([]);
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
@@ -335,6 +343,47 @@ export default function ProfilePage() {
disabled={!isLocal}
fullWidth
/>
<Box sx={{ mt: 2, p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.paper" }}>
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, flexWrap: "wrap", alignItems: "center", mb: 1.5 }}>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>{t("profileCvStructureOverview")}</Typography>
<Typography variant="body2" sx={{ color: "text.secondary" }}>{t("profileCvStructureOverviewHelp")}</Typography>
</Box>
<Button
variant="outlined"
disabled={!isLocal || !profileCvText.trim() || parsingCvSections}
onClick={async () => {
setParsingCvSections(true);
try {
const res = await api.post<{ sections?: ParsedCvSection[] }>("/profile-cv/parse", { text: profileCvText });
setParsedCvSections(res.data?.sections ?? []);
toast(t("profileCvStructureParsed"), "success");
} catch (e: any) {
toast(String(e?.response?.data || e?.message || t("profileCvStructureParseFailed")), "error");
} finally {
setParsingCvSections(false);
}
}}
>
{parsingCvSections ? t("profileCvStructureParsing") : t("profileCvStructureParse")}
</Button>
</Box>
{parsedCvSections.length > 0 ? (
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 1.5 }}>
{parsedCvSections.map((section) => (
<Box key={section.name} sx={{ p: 1.25, borderRadius: 2.5, border: "1px solid", borderColor: "divider", backgroundColor: "background.default" }}>
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, alignItems: "center", mb: 0.75 }}>
<Typography variant="overline">{section.name}</Typography>
<Chip size="small" label={t("profileCvSectionWordCount", { count: section.wordCount })} />
</Box>
<Typography variant="body2" sx={{ color: "text.secondary", whiteSpace: "pre-wrap" }}>{section.content.slice(0, 280)}{section.content.length > 280 ? "…" : ""}</Typography>
</Box>
))}
</Box>
) : (
<Typography variant="body2" sx={{ color: "text.secondary" }}>{t("profileCvStructureEmpty")}</Typography>
)}
</Box>
<Box sx={{ mt: 2, p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.paper" }}>
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, flexWrap: "wrap", alignItems: "center", mb: 1.5 }}>
<Box>