feat(interviews): add durable debrief loop

This commit is contained in:
cesnimda
2026-08-31 17:20:58 +02:00
parent c2181afaea
commit 7d5a78ad5d
8 changed files with 60 additions and 3 deletions
@@ -337,6 +337,7 @@ export const INTERVIEW_PREP_CATEGORIES: { key: string; label: string }[] = [
{ key: "star", label: "STAR examples" },
{ key: "question", label: "Questions to ask them" },
{ key: "note", label: "Notes" },
{ key: "debrief", label: "Interview debrief" },
];
export const interviewPrepApi = {
@@ -91,6 +91,14 @@ export function ApplicationInterviewPrep({ jobId }: { jobId: number }) {
return mutate(() => interviewPrepApi.add(jobId, { category, title: value }));
};
const startDebrief = () => mutate(() => interviewPrepApi.add(jobId, {
category: "debrief",
title: t("interviewDebriefDefaultTitle"),
content: t("interviewDebriefTemplate"),
}));
const hasDebrief = board?.groups.some((group) => group.category === "debrief") ?? false;
return (
<Stack spacing={2}>
<Shell
@@ -151,6 +159,18 @@ export function ApplicationInterviewPrep({ jobId }: { jobId: number }) {
))
)}
{board?.isInterviewStage && !hasDebrief ? (
<Paper variant="outlined" sx={{ p: 2, bgcolor: "action.hover" }}>
<Stack direction={{ xs: "column", sm: "row" }} spacing={1.5} alignItems={{ sm: "center" }} justifyContent="space-between">
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>{t("interviewDebriefTitle")}</Typography>
<Typography variant="body2" color="text.secondary">{t("interviewDebriefHelp")}</Typography>
</Box>
<Button variant="outlined" disabled={busy} onClick={() => void startDebrief()}>{t("interviewDebriefStart")}</Button>
</Stack>
</Paper>
) : null}
<Box component="form" onSubmit={add}>
<Stack direction={{ xs: "column", sm: "row" }} spacing={1}>
<TextField
@@ -485,6 +505,7 @@ function interviewCategoryLabel(t: (key: any) => string, category: string) {
star: "interviewCategoryStar",
question: "interviewCategoryQuestions",
note: "interviewCategoryNotes",
debrief: "interviewCategoryDebrief",
};
return keys[category] ? t(keys[category]) : category;
}
+12
View File
@@ -360,6 +360,12 @@ export const translations = {
interviewCategoryStar: "STAR examples",
interviewCategoryQuestions: "Questions to ask them",
interviewCategoryNotes: "Notes",
interviewCategoryDebrief: "Interview debrief",
interviewDebriefTitle: "Capture the interview while it is fresh",
interviewDebriefHelp: "Record the questions, what went well, what you would improve, and any feedback or next steps.",
interviewDebriefStart: "Start debrief",
interviewDebriefDefaultTitle: "Interview debrief",
interviewDebriefTemplate: "Questions asked:\n\nWhat went well:\n\nWhat I would improve:\n\nFeedback and next steps:",
interviewAiTitle: "AI interview coach",
interviewAiSubtitle: "Uses the linked CV, job advert and application analysis. Suggestions never overwrite your notes.",
interviewAiUsing: "Using",
@@ -2735,6 +2741,12 @@ export const translations = {
interviewCategoryStar: "STAR-eksempler",
interviewCategoryQuestions: "Spørsmål du kan stille",
interviewCategoryNotes: "Notater",
interviewCategoryDebrief: "Intervjuoppsummering",
interviewDebriefTitle: "Noter intervjuet mens det er ferskt",
interviewDebriefHelp: "Skriv ned spørsmålene, hva som gikk bra, hva du ville gjort bedre, og eventuelle tilbakemeldinger eller neste steg.",
interviewDebriefStart: "Start oppsummering",
interviewDebriefDefaultTitle: "Intervjuoppsummering",
interviewDebriefTemplate: "Spørsmål som ble stilt:\n\nHva gikk bra:\n\nHva jeg ville gjort bedre:\n\nTilbakemeldinger og neste steg:",
interviewAiTitle: "AI-intervjutrening",
interviewAiSubtitle: "Bruker tilknyttet CV, stillingsannonse og søknadsanalyse. Forslag overskriver aldri notatene dine.",
interviewAiUsing: "Bruker",
@@ -102,6 +102,24 @@ test("adding a prep item posts the chosen category and title", async () => {
));
});
test("starts a structured interview debrief without inventing answers", async () => {
routeGet();
mockedApi.post.mockResolvedValue({ data: board.groups[0].items[0] } as any);
render(<ApplicationInterviewPrep jobId={7} />);
fireEvent.click(await screen.findByRole("button", { name: "Start debrief" }));
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith(
"/jobapplications/7/interview-prep",
{
category: "debrief",
title: "Interview debrief",
content: expect.stringContaining("Questions asked:"),
},
));
expect(mockedApi.post.mock.calls[0][1].content).not.toMatch(/Acme|achievement|metric/i);
});
test("marking an item ready patches it", async () => {
routeGet();
mockedApi.patch.mockResolvedValue({ data: board.groups[1].items[0] } as any);