Add focus plans and stage-aware follow-up drafting
This commit is contained in:
@@ -19,7 +19,7 @@ import {
|
||||
} from "@mui/material";
|
||||
|
||||
import { api, getApiErrorMessage } from "../api";
|
||||
import { ApplicationPackageResponse, CandidateFit, InterviewPrepResponse, JobApplication, ReadinessResponse } from "../types";
|
||||
import { ApplicationPackageResponse, CandidateFit, FocusPlanResponse, InterviewPrepResponse, JobApplication, ReadinessResponse } from "../types";
|
||||
import { useToast } from "../toast";
|
||||
import { useDialogActions } from "../dialogs";
|
||||
|
||||
@@ -42,6 +42,7 @@ interface Props {
|
||||
jobId: number | null;
|
||||
onClose: () => void;
|
||||
initialTab?: number;
|
||||
initialFollowUpMode?: string;
|
||||
}
|
||||
|
||||
function statusChipColor(status: string): "default" | "primary" | "warning" | "error" | "success" {
|
||||
@@ -70,7 +71,7 @@ function copyLines(items: string[]) {
|
||||
return navigator.clipboard.writeText(items.map((item) => `• ${item}`).join("\n"));
|
||||
}
|
||||
|
||||
export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0 }: Props) {
|
||||
export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0, initialFollowUpMode }: Props) {
|
||||
const { toast } = useToast();
|
||||
const { t } = useI18n();
|
||||
const { confirmAction } = useDialogActions();
|
||||
@@ -84,7 +85,9 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0
|
||||
const [sendingDraft, setSendingDraft] = useState(false);
|
||||
const [refreshingAi, setRefreshingAi] = useState(false);
|
||||
const [candidateFit, setCandidateFit] = useState<CandidateFit | null>(null);
|
||||
const [focusPlan, setFocusPlan] = useState<FocusPlanResponse | null>(null);
|
||||
const [loadingCandidateFit, setLoadingCandidateFit] = useState(false);
|
||||
const [loadingFocusPlan, setLoadingFocusPlan] = useState(false);
|
||||
const [interviewPrep, setInterviewPrep] = useState<InterviewPrepResponse | null>(null);
|
||||
const [loadingInterviewPrep, setLoadingInterviewPrep] = useState(false);
|
||||
const [readiness, setReadiness] = useState<ReadinessResponse | null>(null);
|
||||
@@ -96,14 +99,17 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0
|
||||
const [generationMode, setGenerationMode] = useState<GenerationMode>("default");
|
||||
const [tailoredCvText, setTailoredCvText] = useState("");
|
||||
const [draftRecipient, setDraftRecipient] = useState("");
|
||||
const [followUpMode, setFollowUpMode] = useState(initialFollowUpMode || "post-apply");
|
||||
const [draftReloadToken, setDraftReloadToken] = useState(0);
|
||||
const [draftSubject, setDraftSubject] = useState("");
|
||||
const [draftBody, setDraftBody] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !jobId) return;
|
||||
setTab(Math.max(0, Math.min(8, initialTab)));
|
||||
setTab(Math.max(0, Math.min(9, initialTab)));
|
||||
setFollowUpDraft(null);
|
||||
setCandidateFit(null);
|
||||
setFocusPlan(null);
|
||||
setInterviewPrep(null);
|
||||
setReadiness(null);
|
||||
setApplicationPackage(null);
|
||||
@@ -111,20 +117,21 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0
|
||||
setJob(r.data);
|
||||
setTailoredCvText(r.data.tailoredCvText ?? "");
|
||||
setDraftRecipient(r.data.company?.recruiterEmail ?? "");
|
||||
setFollowUpMode(initialFollowUpMode || (r.data.status?.includes("Interview") ? "post-interview" : r.data.status === "Waiting" ? "waiting-update" : r.data.status === "Offer" ? "offer-checkin" : r.data.status === "Rejected" ? "feedback-request" : "post-apply"));
|
||||
});
|
||||
api.get(`/auth/me`).then((r) => setIsAdmin(Boolean(r.data?.roles?.includes("Admin")))).catch(() => setIsAdmin(false));
|
||||
api.get(`/jobapplications/${jobId}/history`).then((r) => setHistory(r.data)).catch(() => setHistory([]));
|
||||
}, [open, jobId, initialTab]);
|
||||
}, [open, jobId, initialTab, initialFollowUpMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !jobId || tab !== 4 || followUpDraft) return;
|
||||
if (!open || !jobId || tab !== 4) return;
|
||||
setLoadingDraft(true);
|
||||
api.get<FollowUpDraft>(`/jobapplications/${jobId}/followup-draft`).then((r) => {
|
||||
api.get<FollowUpDraft>(`/jobapplications/${jobId}/followup-draft`, { params: { mode: followUpMode } }).then((r) => {
|
||||
setFollowUpDraft(r.data);
|
||||
setDraftSubject(r.data.subject);
|
||||
setDraftBody(r.data.body);
|
||||
}).catch(() => setFollowUpDraft(null)).finally(() => setLoadingDraft(false));
|
||||
}, [open, jobId, tab, followUpDraft]);
|
||||
}, [open, jobId, tab, followUpMode, draftReloadToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !jobId || tab !== 5 || candidateFit) return;
|
||||
@@ -133,13 +140,19 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0
|
||||
}, [open, jobId, tab, candidateFit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !jobId || tab !== 6 || interviewPrep) return;
|
||||
if (!open || !jobId || tab !== 6 || focusPlan) return;
|
||||
setLoadingFocusPlan(true);
|
||||
api.get<FocusPlanResponse>(`/jobapplications/${jobId}/focus-plan`).then((r) => setFocusPlan(r.data)).catch(() => setFocusPlan(null)).finally(() => setLoadingFocusPlan(false));
|
||||
}, [open, jobId, tab, focusPlan]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !jobId || tab !== 7 || interviewPrep) return;
|
||||
setLoadingInterviewPrep(true);
|
||||
api.get<InterviewPrepResponse>(`/jobapplications/${jobId}/interview-prep`).then((r) => setInterviewPrep(r.data)).catch(() => setInterviewPrep(null)).finally(() => setLoadingInterviewPrep(false));
|
||||
}, [open, jobId, tab, interviewPrep]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !jobId || tab !== 7 || readiness) return;
|
||||
if (!open || !jobId || tab !== 8 || readiness) return;
|
||||
setLoadingReadiness(true);
|
||||
api.get<ReadinessResponse>(`/jobapplications/${jobId}/readiness`).then((r) => setReadiness(r.data)).catch(() => setReadiness(null)).finally(() => setLoadingReadiness(false));
|
||||
}, [open, jobId, tab, readiness]);
|
||||
@@ -193,6 +206,7 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0
|
||||
<Tab label={t("jobDetailsTabTailoredCv")} />
|
||||
<Tab label={t("jobTableFollowUp")} />
|
||||
<Tab label={t("jobDetailsTabCandidateFit")} />
|
||||
<Tab label={t("jobDetailsTabFocusPlan")} />
|
||||
<Tab label={t("jobDetailsTabInterviewPrep")} />
|
||||
<Tab label={t("jobTableReadiness")} />
|
||||
{isAdmin ? <Tab label={t("jobDetailsTabHistory")} /> : null}
|
||||
@@ -364,8 +378,23 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0
|
||||
<Box>
|
||||
{loadingDraft ? <Box sx={{ py: 4, display: "flex", justifyContent: "center" }}><CircularProgress size={28} /></Box> : followUpDraft ? (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
<Box><Typography variant="overline">{t("jobDetailsReason")}</Typography><Typography>{followUpDraft.reason}</Typography></Box>
|
||||
<Box><Typography variant="overline">{t("jobDetailsSuggestedSendDate")}</Typography><Typography>{new Date(followUpDraft.suggestedSendOn).toLocaleDateString()}</Typography></Box>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2 }}>
|
||||
<Box><Typography variant="overline">{t("jobDetailsReason")}</Typography><Typography>{followUpDraft.reason}</Typography></Box>
|
||||
<Box><Typography variant="overline">{t("jobDetailsSuggestedSendDate")}</Typography><Typography>{new Date(followUpDraft.suggestedSendOn).toLocaleDateString()}</Typography></Box>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<FormControl size="small" sx={{ minWidth: 240 }}>
|
||||
<InputLabel>{t("jobDetailsFollowUpMode")}</InputLabel>
|
||||
<Select value={followUpMode} label={t("jobDetailsFollowUpMode")} onChange={(e) => setFollowUpMode(e.target.value)}>
|
||||
<MenuItem value="post-apply">{t("jobDetailsFollowUpModePostApply")}</MenuItem>
|
||||
<MenuItem value="waiting-update">{t("jobDetailsFollowUpModeWaiting")}</MenuItem>
|
||||
<MenuItem value="post-interview">{t("jobDetailsFollowUpModePostInterview")}</MenuItem>
|
||||
<MenuItem value="offer-checkin">{t("jobDetailsFollowUpModeOffer")}</MenuItem>
|
||||
<MenuItem value="feedback-request">{t("jobDetailsFollowUpModeFeedback")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button variant="outlined" onClick={() => setDraftReloadToken((value) => value + 1)}>{t("jobDetailsRegenerateDraft")}</Button>
|
||||
</Box>
|
||||
<TextField label={t("jobDetailsRecipient")} value={draftRecipient} onChange={(e) => setDraftRecipient(e.target.value)} helperText={t("jobDetailsRecipientHelp")} />
|
||||
<TextField label={t("jobDetailsSubject")} value={draftSubject} onChange={(e) => setDraftSubject(e.target.value)} />
|
||||
<TextField label={t("jobDetailsDraft")} multiline minRows={8} value={draftBody} onChange={(e) => setDraftBody(e.target.value)} />
|
||||
@@ -419,6 +448,19 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0
|
||||
)}
|
||||
|
||||
{tab === 6 && (
|
||||
<Box>
|
||||
{loadingFocusPlan ? <Box sx={{ py: 4, display: "flex", justifyContent: "center" }}><CircularProgress size={28} /></Box> : focusPlan ? (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
<DraftCard title={t("jobDetailsFocusSummary")} content={focusPlan.strategicSummary} />
|
||||
<TwoColumnSection leftTitle={t("jobDetailsImmediatePriorities")} leftItems={focusPlan.immediatePriorities} rightTitle={t("jobDetailsProofPoints")} rightItems={focusPlan.proofPointsToLeadWith} />
|
||||
<TwoColumnSection leftTitle={t("jobDetailsCvBulletIdeas")} leftItems={focusPlan.cvBulletIdeas} rightTitle={t("jobDetailsCoverLetterAngles")} rightItems={focusPlan.coverLetterAngles} />
|
||||
<ListCard title={t("jobDetailsFollowUpApproach")} items={focusPlan.followUpApproach} />
|
||||
</Box>
|
||||
) : <Typography sx={{ color: "text.secondary" }}>{t("jobDetailsNoFocusPlan")}</Typography>}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{tab === 7 && (
|
||||
<Box>
|
||||
{loadingInterviewPrep ? <Box sx={{ py: 4, display: "flex", justifyContent: "center" }}><CircularProgress size={28} /></Box> : interviewPrep ? (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
@@ -430,7 +472,7 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{tab === 7 && (
|
||||
{tab === 8 && (
|
||||
<Box>
|
||||
{loadingReadiness ? <Box sx={{ py: 4, display: "flex", justifyContent: "center" }}><CircularProgress size={28} /></Box> : readiness ? (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
@@ -448,7 +490,7 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{tab === 8 && isAdmin && (
|
||||
{tab === 9 && isAdmin && (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||
{history.length === 0 ? <Typography sx={{ color: "text.secondary" }}>{t("jobDetailsNoHistory")}</Typography> : history.map((entry) => <PaperRow key={entry.id} type={entry.type} oldValue={entry.oldValue} newValue={entry.newValue} at={entry.at} note={entry.note} />)}
|
||||
</Box>
|
||||
|
||||
@@ -148,6 +148,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
const [companyFilterId, setCompanyFilterId] = useState<number | "All">("All");
|
||||
const [detailsJobId, setDetailsJobId] = useState<number | null>(null);
|
||||
const [detailsInitialTab, setDetailsInitialTab] = useState(0);
|
||||
const [detailsFollowUpMode, setDetailsFollowUpMode] = useState<string | undefined>(undefined);
|
||||
const [editJobId, setEditJobId] = useState<number | null>(null);
|
||||
const [reloadToken, setReloadToken] = useState(0);
|
||||
const [statusAnchor, setStatusAnchor] = useState<null | HTMLElement>(null);
|
||||
@@ -181,11 +182,13 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
const paramsSearch = new URLSearchParams(location.search);
|
||||
const openId = Number(paramsSearch.get("open") || 0);
|
||||
const tabIndex = Number(paramsSearch.get("tab") || 0);
|
||||
const followMode = paramsSearch.get("followMode") || undefined;
|
||||
if (!openId || jobs.length === 0) return;
|
||||
const job = jobs.find((j) => j.id === openId);
|
||||
if (!job) return;
|
||||
setDetailsJobId(openId);
|
||||
setDetailsInitialTab(Number.isFinite(tabIndex) ? Math.max(0, Math.min(8, tabIndex)) : 0);
|
||||
setDetailsInitialTab(Number.isFinite(tabIndex) ? Math.max(0, Math.min(9, tabIndex)) : 0);
|
||||
setDetailsFollowUpMode(followMode);
|
||||
paramsSearch.delete("open");
|
||||
paramsSearch.delete("tab");
|
||||
navigate({ pathname: location.pathname, search: paramsSearch.toString() ? `?${paramsSearch.toString()}` : "" }, { replace: true });
|
||||
@@ -413,7 +416,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
<TablePagination component="div" count={total} page={page} onPageChange={(_, next) => setPage(next)} rowsPerPage={pageSize} onRowsPerPageChange={(e) => { onPageSizeChange(Number(e.target.value) as 15 | 20 | 25); setPage(0); }} rowsPerPageOptions={[15, 20, 25]} />
|
||||
</Paper>
|
||||
|
||||
<JobDetailsDialog open={detailsJobId !== null} jobId={detailsJobId} initialTab={detailsInitialTab} onClose={() => { setDetailsJobId(null); setDetailsInitialTab(0); }} />
|
||||
<JobDetailsDialog open={detailsJobId !== null} jobId={detailsJobId} initialTab={detailsInitialTab} initialFollowUpMode={detailsFollowUpMode} onClose={() => { setDetailsJobId(null); setDetailsInitialTab(0); setDetailsFollowUpMode(undefined); }} />
|
||||
<EditJobDialog open={editJobId !== null} jobId={editJobId} onClose={() => setEditJobId(null)} onSaved={() => setReloadToken((t) => t + 1)} />
|
||||
<Menu anchorEl={statusAnchor} open={Boolean(statusAnchor)} onClose={() => { setStatusAnchor(null); setStatusJobId(null); }}>
|
||||
{(["Waiting", "Interview", "Offer", "Rejected", "Ghosted"] as const).map((s) => <MenuItem key={s} onClick={() => { if (statusJobId) void setStatusQuick(statusJobId, s); setStatusAnchor(null); setStatusJobId(null); }}>{t("jobTableSetStatus", { status: s })}</MenuItem>)}
|
||||
|
||||
@@ -176,6 +176,10 @@ export const translations = {
|
||||
profileMasterCvBody: "Upload a PDF, DOCX, plain text file, markdown file, or image scan. The AI service extracts text where possible and falls back to OCR for supported scanned files.",
|
||||
profileUploadCv: "Upload CV",
|
||||
profileCvImprove: "Improve CV text",
|
||||
profileCvRebuild: "Rebuild CV structure",
|
||||
profileCvRebuilding: "Rebuilding CV...",
|
||||
profileCvRebuilt: "CV rebuilt into a cleaner structure.",
|
||||
profileCvRebuildFailed: "Failed to rebuild CV text.",
|
||||
profileCvImproving: "Improving CV...",
|
||||
profileCvImproved: "CV text improved.",
|
||||
profileCvImproveFailed: "Failed to improve CV text.",
|
||||
@@ -617,6 +621,7 @@ export const translations = {
|
||||
jobDetailsTabAttachments: "Attachments",
|
||||
jobDetailsTabTailoredCv: "Tailored CV",
|
||||
jobDetailsTabCandidateFit: "Candidate fit",
|
||||
jobDetailsTabFocusPlan: "Focus plan",
|
||||
jobDetailsTabInterviewPrep: "Interview prep",
|
||||
jobDetailsTabHistory: "History",
|
||||
jobDetailsTailoredCvMode: "Generation mode",
|
||||
@@ -677,6 +682,13 @@ export const translations = {
|
||||
jobDetailsRecruiterMessageSaveFailed: "Failed to save recruiter message.",
|
||||
jobDetailsKeyPoints: "Key points to emphasize",
|
||||
jobDetailsReason: "Reason",
|
||||
jobDetailsFollowUpMode: "Follow-up mode",
|
||||
jobDetailsFollowUpModePostApply: "Post-apply check-in",
|
||||
jobDetailsFollowUpModeWaiting: "Waiting for update",
|
||||
jobDetailsFollowUpModePostInterview: "Post-interview follow-up",
|
||||
jobDetailsFollowUpModeOffer: "Offer / decision check-in",
|
||||
jobDetailsFollowUpModeFeedback: "Feedback request",
|
||||
jobDetailsRegenerateDraft: "Regenerate draft",
|
||||
jobDetailsSuggestedSendDate: "Suggested send date",
|
||||
jobDetailsRecipient: "Recipient",
|
||||
jobDetailsRecipientHelp: "Defaults to the company recruiter email when available.",
|
||||
@@ -701,6 +713,13 @@ export const translations = {
|
||||
jobDetailsRecruiterMessageGuidance: "Recruiter message guidance",
|
||||
jobDetailsNoDraftAvailableYet: "No draft available yet.",
|
||||
jobDetailsCandidateFitEmpty: "Add your profile CV text on the Profile page to generate a candidate fit analysis for this role.",
|
||||
jobDetailsFocusSummary: "Strategy summary",
|
||||
jobDetailsImmediatePriorities: "Immediate priorities",
|
||||
jobDetailsCvBulletIdeas: "CV bullet ideas",
|
||||
jobDetailsProofPoints: "Proof points to lead with",
|
||||
jobDetailsCoverLetterAngles: "Cover-letter angles",
|
||||
jobDetailsFollowUpApproach: "Follow-up approach",
|
||||
jobDetailsNoFocusPlan: "No focus plan available yet.",
|
||||
jobDetailsInterviewPrepBrief: "Interview prep brief",
|
||||
jobDetailsTalkingPoints: "Talking points",
|
||||
jobDetailsLikelyQuestions: "Likely questions",
|
||||
@@ -901,6 +920,10 @@ export const translations = {
|
||||
profileMasterCvBody: "Last opp en PDF, DOCX, ren tekstfil, markdown-fil eller et bildeskann. AI-tjenesten henter ut tekst der det er mulig og faller tilbake til OCR for støttede skannede filer.",
|
||||
profileUploadCv: "Last opp CV",
|
||||
profileCvImprove: "Forbedre CV-tekst",
|
||||
profileCvRebuild: "Bygg opp CV-struktur på nytt",
|
||||
profileCvRebuilding: "Bygger opp CV på nytt...",
|
||||
profileCvRebuilt: "CV bygget opp i en renere struktur.",
|
||||
profileCvRebuildFailed: "Kunne ikke bygge opp CV-tekst på nytt.",
|
||||
profileCvImproving: "Forbedrer CV...",
|
||||
profileCvImproved: "CV-tekst forbedret.",
|
||||
profileCvImproveFailed: "Kunne ikke forbedre CV-tekst.",
|
||||
@@ -1342,6 +1365,7 @@ export const translations = {
|
||||
jobDetailsTabAttachments: "Vedlegg",
|
||||
jobDetailsTabTailoredCv: "Tilpasset CV",
|
||||
jobDetailsTabCandidateFit: "Kandidatmatch",
|
||||
jobDetailsTabFocusPlan: "Fokusplan",
|
||||
jobDetailsTabInterviewPrep: "Intervjuforberedelse",
|
||||
jobDetailsTabHistory: "Historikk",
|
||||
jobDetailsTailoredCvMode: "Genereringsmodus",
|
||||
@@ -1402,6 +1426,13 @@ export const translations = {
|
||||
jobDetailsRecruiterMessageSaveFailed: "Kunne ikke lagre melding til rekrutterer.",
|
||||
jobDetailsKeyPoints: "Nøkkelpunkter å fremheve",
|
||||
jobDetailsReason: "Årsak",
|
||||
jobDetailsFollowUpMode: "Oppfølgingsmodus",
|
||||
jobDetailsFollowUpModePostApply: "Oppfølging etter søknad",
|
||||
jobDetailsFollowUpModeWaiting: "Venter på oppdatering",
|
||||
jobDetailsFollowUpModePostInterview: "Oppfølging etter intervju",
|
||||
jobDetailsFollowUpModeOffer: "Tilbud / beslutningsoppfølging",
|
||||
jobDetailsFollowUpModeFeedback: "Be om tilbakemelding",
|
||||
jobDetailsRegenerateDraft: "Generer utkast på nytt",
|
||||
jobDetailsSuggestedSendDate: "Foreslått sendingsdato",
|
||||
jobDetailsRecipient: "Mottaker",
|
||||
jobDetailsRecipientHelp: "Bruker selskapets rekrutterer-e-post som standard når den finnes.",
|
||||
@@ -1426,6 +1457,13 @@ export const translations = {
|
||||
jobDetailsRecruiterMessageGuidance: "Veiledning for rekrutterermelding",
|
||||
jobDetailsNoDraftAvailableYet: "Ingen utkast tilgjengelig ennå.",
|
||||
jobDetailsCandidateFitEmpty: "Legg til CV-teksten din på profilsiden for å generere en kandidatmatchanalyse for denne rollen.",
|
||||
jobDetailsFocusSummary: "Strategioppsummering",
|
||||
jobDetailsImmediatePriorities: "Viktigste prioriteringer nå",
|
||||
jobDetailsCvBulletIdeas: "Ideer til CV-punkter",
|
||||
jobDetailsProofPoints: "Bevispunkter å lede med",
|
||||
jobDetailsCoverLetterAngles: "Vinkler for søknadsbrev",
|
||||
jobDetailsFollowUpApproach: "Oppfølgingsstrategi",
|
||||
jobDetailsNoFocusPlan: "Ingen fokusplan tilgjengelig ennå.",
|
||||
jobDetailsInterviewPrepBrief: "Kort intervjuforberedelse",
|
||||
jobDetailsTalkingPoints: "Samtalepunkter",
|
||||
jobDetailsLikelyQuestions: "Sannsynlige spørsmål",
|
||||
|
||||
@@ -52,6 +52,7 @@ export default function ProfilePage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [uploadingCv, setUploadingCv] = useState(false);
|
||||
const [improvingCv, setImprovingCv] = useState(false);
|
||||
const [rebuildingCv, setRebuildingCv] = useState(false);
|
||||
const [uploadingAvatar, setUploadingAvatar] = useState(false);
|
||||
const [avatarFile, setAvatarFile] = useState<File | null>(null);
|
||||
const [cropOpen, setCropOpen] = useState(false);
|
||||
@@ -248,12 +249,31 @@ export default function ProfilePage() {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button variant="outlined" disabled={!isLocal || uploadingCv || improvingCv} onClick={() => cvInputRef.current?.click()}>
|
||||
<Button variant="outlined" disabled={!isLocal || uploadingCv || improvingCv || rebuildingCv} onClick={() => cvInputRef.current?.click()}>
|
||||
{uploadingCv ? t("profileUploading") : t("profileUploadCv")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={!isLocal || !profileCvText.trim() || uploadingCv || improvingCv}
|
||||
disabled={!isLocal || !profileCvText.trim() || uploadingCv || improvingCv || rebuildingCv}
|
||||
onClick={async () => {
|
||||
setRebuildingCv(true);
|
||||
try {
|
||||
const res = await api.post<{ text?: string }>("/profile-cv/rebuild");
|
||||
if (res.data?.text) setProfileCvText(res.data.text);
|
||||
await loadProfile();
|
||||
toast(t("profileCvRebuilt"), "success");
|
||||
} catch (e: any) {
|
||||
toast(String(e?.response?.data || e?.message || t("profileCvRebuildFailed")), "error");
|
||||
} finally {
|
||||
setRebuildingCv(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{rebuildingCv ? t("profileCvRebuilding") : t("profileCvRebuild")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={!isLocal || !profileCvText.trim() || uploadingCv || improvingCv || rebuildingCv}
|
||||
onClick={async () => {
|
||||
setImprovingCv(true);
|
||||
try {
|
||||
|
||||
@@ -75,6 +75,15 @@ export interface CandidateFit {
|
||||
recruiterMessageDraft?: string | null;
|
||||
}
|
||||
|
||||
export interface FocusPlanResponse {
|
||||
immediatePriorities: string[];
|
||||
cvBulletIdeas: string[];
|
||||
proofPointsToLeadWith: string[];
|
||||
coverLetterAngles: string[];
|
||||
followUpApproach: string[];
|
||||
strategicSummary: string;
|
||||
}
|
||||
|
||||
export interface InterviewPrepResponse {
|
||||
summary: string;
|
||||
talkingPoints: string[];
|
||||
|
||||
Reference in New Issue
Block a user