Add attachment-aware AI drafting and CV section tools
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user