fix(cv): bind letter proposals to current sources and preserve document choices
This commit is contained in:
@@ -666,6 +666,8 @@ export function ApplicationCoverLetterSection({ jobId, onDirtyChange }: { jobId:
|
||||
|
||||
<CoverLetterAiAssistant
|
||||
jobId={jobId}
|
||||
documentLanguage={settings?.language}
|
||||
onDocumentLanguageChange={settings ? next => setSettingsDraft({ ...settings, language: next }) : undefined}
|
||||
toolbarHost={aiToolbarHost}
|
||||
currentText={text}
|
||||
disabled={busy || confirming || loading || !data}
|
||||
@@ -737,7 +739,7 @@ const COVER_LETTER_ACTIONS = [
|
||||
{ key: "tailor", labelKey: "coverActionTailor" },
|
||||
] as const;
|
||||
|
||||
function CoverLetterAiAssistant({ jobId, currentText, disabled, onApply, toolbarHost }: { jobId: number; currentText: string; disabled: boolean; onApply: (text: string, action: string) => void; toolbarHost: HTMLDivElement | null }) {
|
||||
function CoverLetterAiAssistant({ jobId, currentText, disabled, onApply, toolbarHost, documentLanguage, onDocumentLanguageChange }: { jobId: number; currentText: string; disabled: boolean; onApply: (text: string, action: string) => void; toolbarHost: HTMLDivElement | null; documentLanguage?: "en" | "nb-NO"; onDocumentLanguageChange?: (language: "en" | "nb-NO") => void }) {
|
||||
const labelId = useId();
|
||||
const { canUseAi } = useAccountPlan();
|
||||
const { language: uiLanguage, t } = useI18n();
|
||||
@@ -746,11 +748,14 @@ function CoverLetterAiAssistant({ jobId, currentText, disabled, onApply, toolbar
|
||||
const [mode, setMode] = useState("professional");
|
||||
const [language, setLanguage] = useState<"en" | "nb-NO">(() => uiLanguage === "nb" ? "nb-NO" : "en");
|
||||
const [instructions, setInstructions] = useState("");
|
||||
const [suggestion, setSuggestion] = useState<{ text: string; action: string } | null>(null);
|
||||
const [suggestion, setSuggestion] = useState<{ id: number; text: string; action: string } | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<AssetError | null>(null);
|
||||
const busyRef = useRef(false);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const disabledRef = useRef(disabled);
|
||||
disabledRef.current = disabled;
|
||||
useEffect(() => { if (documentLanguage) setLanguage(documentLanguage); }, [documentLanguage]);
|
||||
|
||||
const requestSequence = useRef(0);
|
||||
useEffect(() => {
|
||||
@@ -779,11 +784,39 @@ function CoverLetterAiAssistant({ jobId, currentText, disabled, onApply, toolbar
|
||||
});
|
||||
if (!isCurrent() || request !== requestSequence.current) return;
|
||||
const text = result.result.text?.trim() ?? "";
|
||||
setSuggestion(text ? { text, action: requestedAction } : null);
|
||||
setSuggestion(text ? { id: result.id, text, action: requestedAction } : null);
|
||||
} catch (err) {
|
||||
if (isCurrent() && request === requestSequence.current) {
|
||||
const sourceRequired = (err as { response?: { data?: unknown } })?.response?.data === "cover_letter_source_required";
|
||||
setError(sourceRequired ? { kind: "error", message: "", messageKey: "coverAiSourceRequired", retryable: false } : assetError(err, "coverAiGenerateFailed"));
|
||||
const sourceChanged = (err as { response?: { data?: { code?: string } } })?.response?.data?.code === "cover_letter_source_changed";
|
||||
setError(sourceRequired || sourceChanged ? { kind: "error", message: "", messageKey: sourceChanged ? "coverAiSourceChanged" : "coverAiSourceRequired", retryable: false } : assetError(err, "coverAiGenerateFailed"));
|
||||
}
|
||||
} finally {
|
||||
if (request === requestSequence.current) { busyRef.current = false; setBusy(false); }
|
||||
}
|
||||
};
|
||||
|
||||
const applySuggestion = async () => {
|
||||
if (!suggestion || busyRef.current || disabledRef.current || loadingCv) return;
|
||||
if (!Number.isSafeInteger(suggestion.id) || suggestion.id <= 0) {
|
||||
setSuggestion(null);
|
||||
setError({ kind: "error", message: "", messageKey: "coverAiSourceChanged", retryable: false });
|
||||
return;
|
||||
}
|
||||
const request = ++requestSequence.current;
|
||||
busyRef.current = true;
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await aiWorkspaceApi.validateSource(jobId, suggestion.id);
|
||||
if (!isCurrent() || request !== requestSequence.current || disabledRef.current) return;
|
||||
onApply(suggestion.text, suggestion.action);
|
||||
setSuggestion(null);
|
||||
} catch (err) {
|
||||
if (isCurrent() && request === requestSequence.current) {
|
||||
setSuggestion(null);
|
||||
const sourceChanged = (err as { response?: { data?: { code?: string } } })?.response?.data?.code === "cover_letter_source_changed";
|
||||
setError(sourceChanged ? { kind: "error", message: "", messageKey: "coverAiSourceChanged", retryable: false } : assetError(err, "coverAiGenerateFailed"));
|
||||
}
|
||||
} finally {
|
||||
if (request === requestSequence.current) { busyRef.current = false; setBusy(false); }
|
||||
@@ -835,7 +868,7 @@ function CoverLetterAiAssistant({ jobId, currentText, disabled, onApply, toolbar
|
||||
</FormControl>
|
||||
<FormControl size="small" sx={{ minWidth: 190 }}>
|
||||
<InputLabel id={`${labelId}-coverAiDocumentLanguage`}>{t("coverAiDocumentLanguage")}</InputLabel>
|
||||
<Select disabled={busy || disabled || loadingCv} labelId={`${labelId}-coverAiDocumentLanguage`} label={t("coverAiDocumentLanguage")} value={language} onChange={(event) => setLanguage(event.target.value as "en" | "nb-NO")}>
|
||||
<Select disabled={busy || disabled || loadingCv} labelId={`${labelId}-coverAiDocumentLanguage`} label={t("coverAiDocumentLanguage")} value={language} onChange={(event) => { const next = event.target.value as "en" | "nb-NO"; setLanguage(next); onDocumentLanguageChange?.(next); }}>
|
||||
<MenuItem value="en">{t("languageEnglish")}</MenuItem>
|
||||
<MenuItem value="nb-NO">{t("languageNorwegianBokmal")}</MenuItem>
|
||||
</Select>
|
||||
@@ -875,7 +908,7 @@ function CoverLetterAiAssistant({ jobId, currentText, disabled, onApply, toolbar
|
||||
<TextField label={t("coverAiSuggestion")} fullWidth multiline minRows={3} value={suggestion.text} disabled={busy || disabled}
|
||||
onChange={event => setSuggestion({ ...suggestion, text: event.target.value })} />
|
||||
<Stack direction="row" spacing={1} sx={{ mt: 2 }}>
|
||||
<Button size="small" variant="contained" disabled={busy || disabled || loadingCv || !suggestion.text.trim()} onClick={() => { if (!busyRef.current && !disabled && !loadingCv) { onApply(suggestion.text, suggestion.action); setSuggestion(null); } }}>{t("coverAiApply")}</Button>
|
||||
<Button size="small" variant="contained" disabled={busy || disabled || loadingCv || !suggestion.text.trim()} onClick={() => void applySuggestion()}>{t("coverAiApply")}</Button>
|
||||
<Button size="small" disabled={busy || disabled || loadingCv} onClick={() => setSuggestion(null)}>{t("coverAiReject")}</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
Reference in New Issue
Block a user