Add attachment-aware AI drafting and CV section tools

This commit is contained in:
cesnimda
2026-03-23 22:17:03 +01:00
parent 8db620e45b
commit 0c8258e90f
7 changed files with 316 additions and 17 deletions
+74 -2
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Alert, Avatar, Box, Button, Chip, Divider, LinearProgress, Paper, TextField, Typography } from "@mui/material";
import { Alert, Avatar, Box, Button, Chip, Divider, FormControl, InputLabel, LinearProgress, MenuItem, Paper, Select, TextField, Typography } from "@mui/material";
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import PhotoCameraOutlinedIcon from "@mui/icons-material/PhotoCameraOutlined";
@@ -11,6 +11,9 @@ import CropImageDialog from "../components/CropImageDialog";
import { useToast } from "../toast";
import { useI18n } from "../i18n/I18nProvider";
type CvSectionOption = "Professional Summary" | "Core Skills" | "Experience Highlights" | "Selected Achievements" | "Projects";
type CvSectionStyle = "balanced" | "concise" | "impact" | "ats";
type MeResponse = {
provider?: "local" | "google" | "external";
id?: string;
@@ -64,7 +67,11 @@ 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<CvSectionOption>("Professional Summary");
const [cvSectionStyle, setCvSectionStyle] = useState<CvSectionStyle>("balanced");
const [cvSectionTargetRole, setCvSectionTargetRole] = useState("");
const [cvSectionDraft, setCvSectionDraft] = useState("");
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
@@ -306,6 +313,71 @@ 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("profileCvSectionTools")}</Typography>
<Typography variant="body2" sx={{ color: "text.secondary" }}>{t("profileCvSectionToolsHelp")}</Typography>
</Box>
<Button
variant="outlined"
disabled={!isLocal || !profileCvText.trim() || rewritingSection || uploadingCv || improvingCv || rebuildingCv}
onClick={async () => {
setRewritingSection(true);
try {
const res = await api.post<{ text?: string }>("/profile-cv/rewrite-section", {
sectionName: cvSection,
style: cvSectionStyle,
targetRole: cvSectionTargetRole.trim() || null,
});
setCvSectionDraft(res.data?.text ?? "");
toast(t("profileCvSectionRewritten"), "success");
} catch (e: any) {
toast(String(e?.response?.data || e?.message || t("profileCvSectionRewriteFailed")), "error");
} finally {
setRewritingSection(false);
}
}}
>
{rewritingSection ? t("profileCvSectionRewriting") : t("profileCvSectionRewrite")}
</Button>
</Box>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr 1.2fr" }, gap: 1.5, mb: 1.5 }}>
<FormControl fullWidth size="small">
<InputLabel>{t("profileCvSectionLabel")}</InputLabel>
<Select value={cvSection} label={t("profileCvSectionLabel")} onChange={(e) => setCvSection(e.target.value as CvSectionOption)}>
<MenuItem value="Professional Summary">{t("profileCvSectionSummary")}</MenuItem>
<MenuItem value="Core Skills">{t("profileCvSectionSkills")}</MenuItem>
<MenuItem value="Experience Highlights">{t("profileCvSectionExperience")}</MenuItem>
<MenuItem value="Selected Achievements">{t("profileCvSectionAchievements")}</MenuItem>
<MenuItem value="Projects">{t("profileCvSectionProjects")}</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth size="small">
<InputLabel>{t("profileCvSectionStyle")}</InputLabel>
<Select value={cvSectionStyle} label={t("profileCvSectionStyle")} onChange={(e) => setCvSectionStyle(e.target.value as CvSectionStyle)}>
<MenuItem value="balanced">{t("profileCvSectionStyleBalanced")}</MenuItem>
<MenuItem value="concise">{t("profileCvSectionStyleConcise")}</MenuItem>
<MenuItem value="impact">{t("profileCvSectionStyleImpact")}</MenuItem>
<MenuItem value="ats">{t("profileCvSectionStyleAts")}</MenuItem>
</Select>
</FormControl>
<TextField label={t("profileCvSectionTargetRole")} value={cvSectionTargetRole} onChange={(e) => setCvSectionTargetRole(e.target.value)} fullWidth />
</Box>
<TextField
label={t("profileCvSectionDraft")}
value={cvSectionDraft}
onChange={(e) => setCvSectionDraft(e.target.value)}
multiline
minRows={6}
fullWidth
placeholder={t("profileCvSectionDraftPlaceholder")}
/>
<Box sx={{ mt: 1, display: "flex", justifyContent: "flex-end", gap: 1, flexWrap: "wrap" }}>
<Button variant="text" disabled={!cvSectionDraft.trim()} onClick={() => navigator.clipboard.writeText(cvSectionDraft)}>{t("profileCopyCvText")}</Button>
<Button variant="outlined" disabled={!cvSectionDraft.trim()} onClick={() => setProfileCvText((prev) => `${prev.trim()}\n\n${cvSection}\n${cvSectionDraft.trim()}`.trim())}>{t("profileCvSectionAppend")}</Button>
</Box>
</Box>
<Box sx={{ mt: 1, display: "flex", justifyContent: "space-between", gap: 1, flexWrap: "wrap" }}>
<Typography variant="caption" sx={{ color: "text.secondary" }}>
{cvWordCount} words