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
@@ -36,6 +36,7 @@ type FollowUpDraft = {
};
type GenerationMode = "default" | "concise" | "ats" | "achievement" | "interview";
type CoverLetterStyle = "balanced" | "concise" | "formal" | "bold";
interface Props {
open: boolean;
@@ -97,6 +98,7 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
const [generatingPackage, setGeneratingPackage] = useState(false);
const [applicationPackage, setApplicationPackage] = useState<ApplicationPackageResponse | null>(null);
const [generationMode, setGenerationMode] = useState<GenerationMode>("default");
const [coverLetterStyle, setCoverLetterStyle] = useState<CoverLetterStyle>("balanced");
const [tailoredCvText, setTailoredCvText] = useState("");
const [draftRecipient, setDraftRecipient] = useState("");
const [followUpMode, setFollowUpMode] = useState(initialFollowUpMode || "post-apply");
@@ -279,6 +281,15 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
<MenuItem value="interview">{t("jobDetailsGenerationInterview")}</MenuItem>
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 190 }}>
<InputLabel>{t("jobDetailsCoverLetterStyle")}</InputLabel>
<Select value={coverLetterStyle} label={t("jobDetailsCoverLetterStyle")} onChange={(e) => setCoverLetterStyle(e.target.value as CoverLetterStyle)}>
<MenuItem value="balanced">{t("jobDetailsCoverLetterStyleBalanced")}</MenuItem>
<MenuItem value="concise">{t("jobDetailsCoverLetterStyleConcise")}</MenuItem>
<MenuItem value="formal">{t("jobDetailsCoverLetterStyleFormal")}</MenuItem>
<MenuItem value="bold">{t("jobDetailsCoverLetterStyleBold")}</MenuItem>
</Select>
</FormControl>
<Button size="small" variant="outlined" onClick={async () => {
try {
const me = await api.get<{ profileCvText?: string | null }>("/auth/me");
@@ -292,7 +303,7 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
if (!jobId) return;
setGeneratingPackage(true);
try {
const res = await api.post<ApplicationPackageResponse>(`/jobapplications/${jobId}/generate-application-package`, null, { params: { mode: generationMode } });
const res = await api.post<ApplicationPackageResponse>(`/jobapplications/${jobId}/generate-application-package`, null, { params: { mode: generationMode, coverLetterStyle } });
setApplicationPackage(res.data);
setTailoredCvText(res.data.tailoredCvText ?? "");
toast(t("jobDetailsPackageGenerated"), "success");
@@ -369,6 +380,7 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
}
}} saving={savingApplicationDrafts} />
<ListCard title={t("jobDetailsKeyPoints")} items={applicationPackage.keyPoints} />
<ListCard title={t("jobDetailsAttachmentSignals")} items={applicationPackage.attachmentSignals.length > 0 ? applicationPackage.attachmentSignals : [t("jobDetailsNoAttachmentSignals")]} subtitle={applicationPackage.attachmentFilesUsed.length > 0 ? applicationPackage.attachmentFilesUsed.join(", ") : undefined} />
</Box>
) : null}
</Box>
@@ -525,13 +537,16 @@ function TwoColumnSection({ leftTitle, leftItems, rightTitle, rightItems }: { le
);
}
function ListCard({ title, items }: { title: string; items: string[] }) {
function ListCard({ title, items, subtitle }: { title: string; items: string[]; subtitle?: string }) {
const { t } = useI18n();
return (
<Box sx={{ p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.default" }}>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 1, flexWrap: "wrap", mb: 1 }}>
<Typography variant="overline">{title}</Typography>
<Box>
<Typography variant="overline">{title}</Typography>
{subtitle ? <Typography variant="caption" sx={{ display: "block", color: "text.secondary" }}>{subtitle}</Typography> : null}
</Box>
<Button size="small" variant="outlined" onClick={() => copyLines(items)}>{t("jobDetailsCopy")}</Button>
</Box>
<Box sx={{ display: "flex", flexDirection: "column", gap: 0.75 }}>