Add CV structure analysis groundwork
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user