From 57bb871e98c8666c06da09ec175cb6132e7008a9 Mon Sep 17 00:00:00 2001 From: dangrabo <193651636+dangrabo@users.noreply.github.com> Date: Sat, 6 Jun 2026 14:15:37 -0700 Subject: [PATCH 1/2] Wired up LLM feedback --- backend/services/llm/router.py | 156 +++++++++++-- backend/services/llm/schemas.py | 23 ++ frontend/app/InterviewClient.module.css | 33 +++ frontend/app/InterviewClient.tsx | 208 ++++++++++++++++-- .../SessionScorecardPanel.module.css | 66 ++++++ .../app/scorecard/SessionScorecardPanel.tsx | 117 ++++++++++ frontend/lib/interview-coach/types.ts | 24 ++ frontend/lib/prompts/questions.ts | 3 + 8 files changed, 590 insertions(+), 40 deletions(-) create mode 100644 frontend/app/scorecard/SessionScorecardPanel.module.css create mode 100644 frontend/app/scorecard/SessionScorecardPanel.tsx diff --git a/backend/services/llm/router.py b/backend/services/llm/router.py index ae60e0f..a07018c 100644 --- a/backend/services/llm/router.py +++ b/backend/services/llm/router.py @@ -16,8 +16,10 @@ from pydantic import ValidationError from services.llm.schemas import ( + FullSessionFeedbackResponse, ReviewContextPayload, SessionFeedbackResponse, + SessionReviewPayload, ) FEEDBACK_MODEL = "openai/gpt-oss-20b" @@ -98,6 +100,68 @@ Be specific and grounded in the provided transcript and segments. Do not invent \ facts the candidate did not say. Output only the JSON object.""" +_SESSION_RESPONSE_SCHEMA = { + "type": "object", + "properties": { + "overallSummary": {"type": "string"}, + "overallStrengths": {"type": "array", "items": {"type": "string"}}, + "overallImprovements": {"type": "array", "items": {"type": "string"}}, + "overallDeliveryNotes": {"type": "string"}, + "questionReviews": { + "type": "array", + "items": { + "type": "object", + "properties": { + "question": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "text": {"type": "string"}, + }, + "required": ["id", "text"], + }, + "transcriptScores": _RESPONSE_SCHEMA["properties"][ + "transcriptScores" + ], + "feedback": _RESPONSE_SCHEMA["properties"]["feedback"], + "modelAnswer": _RESPONSE_SCHEMA["properties"]["modelAnswer"], + }, + "required": ["question", "transcriptScores", "feedback", "modelAnswer"], + }, + }, + }, + "required": [ + "overallSummary", + "overallStrengths", + "overallImprovements", + "overallDeliveryNotes", + "questionReviews", + ], +} + +_SESSION_SYSTEM_PROMPT = """You are an expert interview coach reviewing a \ +candidate's full mock interview session. + +You receive multiple question-and-answer pairs. Each answer includes the \ +question text, full transcript, and timestamped segments with delivery signals \ +(arousal, dominance, valence — each 0..1). These are tone-and-delivery signals \ +only, not clinical labels. + +Produce a single JSON object with: +1. overallSummary: 3-5 sentence overview of how the candidate performed across \ +the whole session (content and delivery trends). +2. overallStrengths: 3-5 session-level strengths. +3. overallImprovements: 3-5 session-level, actionable improvements. +4. overallDeliveryNotes: one paragraph on vocal delivery patterns across answers. +5. questionReviews: one entry per question answered, in the same order provided. \ +Each entry must include: + - question: echo back the exact id and text from the input + - transcriptScores: clarity/structure/relevance/conciseness (0..1) for that answer + - feedback: summary, strengths, improvements, deliveryNotes for that answer + - modelAnswer: a strong example answer for that question + +Be specific and grounded in the transcripts. Do not invent facts. Output only JSON.""" + def _require_groq() -> None: if not os.environ.get("GROQ_API_KEY"): @@ -121,49 +185,52 @@ def _build_user_prompt(payload: ReviewContextPayload) -> str: ) -def _call_groq(user_prompt: str) -> str: +def _call_groq(user_prompt: str, *, session: bool = False) -> str: completion = _groq.chat.completions.create( model=FEEDBACK_MODEL, messages=[ - {"role": "system", "content": _SYSTEM_PROMPT}, + { + "role": "system", + "content": _SESSION_SYSTEM_PROMPT if session else _SYSTEM_PROMPT, + }, {"role": "user", "content": user_prompt}, ], temperature=0.4, response_format={ "type": "json_schema", "json_schema": { - "name": "session_feedback", + "name": "session_feedback" if session else "answer_feedback", "strict": False, - "schema": _RESPONSE_SCHEMA, + "schema": _SESSION_RESPONSE_SCHEMA if session else _RESPONSE_SCHEMA, }, }, ) return completion.choices[0].message.content or "" -@router.post("/generate", response_model=SessionFeedbackResponse) -async def generate(payload: ReviewContextPayload) -> SessionFeedbackResponse: - """Generate transcript scores + qualitative feedback + a model answer for a - recorded interview answer using a single Groq call.""" - _require_groq() - - if not payload.transcript.text.strip(): - raise HTTPException(status_code=422, detail="Transcript is empty.") +def _build_session_user_prompt(payload: SessionReviewPayload) -> str: + blocks = [] + for n, answer in enumerate(payload.answers, start=1): + blocks.append(f"=== ANSWER {n} ===\n{_build_user_prompt(answer)}") + return "\n\n".join(blocks) - user_prompt = _build_user_prompt(payload) +def _generate_with_retry( + user_prompt: str, + *, + session: bool = False, +) -> dict: last_error: Exception | None = None - for _ in range(2): # one retry on transport or validation failure + for _ in range(2): try: - raw = _call_groq(user_prompt) + raw = _call_groq(user_prompt, session=session) except Exception as exc: last_error = exc continue try: - data = json.loads(raw) - return SessionFeedbackResponse.model_validate(data) - except (json.JSONDecodeError, ValidationError) as exc: + return json.loads(raw) + except json.JSONDecodeError as exc: last_error = exc continue @@ -171,3 +238,56 @@ async def generate(payload: ReviewContextPayload) -> SessionFeedbackResponse: status_code=502, detail=f"Feedback generation failed: {last_error}", ) + + +@router.post("/generate", response_model=SessionFeedbackResponse) +async def generate(payload: ReviewContextPayload) -> SessionFeedbackResponse: + """Generate transcript scores + qualitative feedback + a model answer for a + recorded interview answer using a single Groq call.""" + _require_groq() + + if not payload.transcript.text.strip(): + raise HTTPException(status_code=422, detail="Transcript is empty.") + + user_prompt = _build_user_prompt(payload) + + try: + data = _generate_with_retry(user_prompt) + return SessionFeedbackResponse.model_validate(data) + except HTTPException: + raise + except ValidationError as exc: + raise HTTPException( + status_code=502, + detail=f"Feedback generation failed: {exc}", + ) from exc + + +@router.post("/generate-session", response_model=FullSessionFeedbackResponse) +async def generate_session( + payload: SessionReviewPayload, +) -> FullSessionFeedbackResponse: + """Generate holistic session feedback plus per-question reviews in one Groq call.""" + _require_groq() + + if not payload.answers: + raise HTTPException(status_code=422, detail="No answers provided.") + + for answer in payload.answers: + if not answer.transcript.text.strip(): + raise HTTPException( + status_code=422, detail="One or more transcripts are empty." + ) + + user_prompt = _build_session_user_prompt(payload) + + try: + data = _generate_with_retry(user_prompt, session=True) + return FullSessionFeedbackResponse.model_validate(data) + except HTTPException: + raise + except ValidationError as exc: + raise HTTPException( + status_code=502, + detail=f"Session feedback generation failed: {exc}", + ) from exc diff --git a/backend/services/llm/schemas.py b/backend/services/llm/schemas.py index 0e54f29..8749d8f 100644 --- a/backend/services/llm/schemas.py +++ b/backend/services/llm/schemas.py @@ -61,3 +61,26 @@ class SessionFeedbackResponse(BaseModel): transcriptScores: TranscriptScores feedback: QualitativeFeedback modelAnswer: ModelAnswer + + +class SessionReviewPayload(BaseModel): + """Request body for POST /feedback/generate-session — multiple Q&A pairs.""" + + answers: list[ReviewContextPayload] + + +class QuestionReview(BaseModel): + question: Question + transcriptScores: TranscriptScores + feedback: QualitativeFeedback + modelAnswer: ModelAnswer + + +class FullSessionFeedbackResponse(BaseModel): + """Response body for POST /feedback/generate-session.""" + + overallSummary: str + overallStrengths: list[str] + overallImprovements: list[str] + overallDeliveryNotes: str + questionReviews: list[QuestionReview] diff --git a/frontend/app/InterviewClient.module.css b/frontend/app/InterviewClient.module.css index 048da15..df01433 100644 --- a/frontend/app/InterviewClient.module.css +++ b/frontend/app/InterviewClient.module.css @@ -197,6 +197,39 @@ cursor: not-allowed; } +.feedbackModeFieldset { + border: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.55rem; +} + +.feedbackModeFieldset:disabled { + opacity: 0.45; +} + +.feedbackModeLegend { + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.07em; + opacity: 0.5; + margin-bottom: 0.15rem; +} + +.feedbackModeOption { + display: flex; + align-items: center; + gap: 0.55rem; + font-size: 0.88rem; + cursor: pointer; +} + +.feedbackModeOption input { + accent-color: #fff; +} + .questionPlaceholder { font-size: 1rem; opacity: 0.35; diff --git a/frontend/app/InterviewClient.tsx b/frontend/app/InterviewClient.tsx index 4962ef1..31d70ad 100644 --- a/frontend/app/InterviewClient.tsx +++ b/frontend/app/InterviewClient.tsx @@ -3,13 +3,19 @@ import { useCallback, useRef, useState } from "react"; import { INTRO_QUESTION, + MAX_INTERVIEW_QUESTIONS, pickRandomQuestion, type InterviewQuestion, } from "@/lib/prompts/questions"; import { INTERVIEWERS, DEFAULT_INTERVIEWER, type Interviewer } from "@/lib/prompts/interviewers"; import { ScorecardPanel } from "@/app/scorecard/ScorecardPanel"; +import { SessionScorecardPanel } from "@/app/scorecard/SessionScorecardPanel"; import { buildReviewContext } from "@/lib/interview-coach/sessionAdapter"; -import type { ReviewContextPayload } from "@/lib/interview-coach/types"; +import type { + FeedbackMode, + ReviewContextPayload, + SessionReviewPayload, +} from "@/lib/interview-coach/types"; import styles from "./InterviewClient.module.css"; const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000"; @@ -18,7 +24,7 @@ const SILENCE_THRESHOLD = 0.015; const SILENCE_BEFORE_PROMPT_MS = 3_000; const SILENCE_AFTER_PROMPT_MS = 8_000; -type Stage = "idle" | "playing" | "recording" | "processing" | "done"; +type Stage = "idle" | "playing" | "recording" | "processing" | "done" | "finished"; interface Segment { start: number; @@ -29,6 +35,14 @@ interface Segment { valence: number; } +function upsertAnswer( + answers: ReviewContextPayload[], + next: ReviewContextPayload +): ReviewContextPayload[] { + const rest = answers.filter((a) => a.question.id !== next.question.id); + return [...rest, next]; +} + function buildIntro(interviewer: Interviewer): string { return `Hi there, welcome. I'm ${interviewer.name}, ${interviewer.title}. Thank you so much for coming in today — we're really glad to have you. Let's go ahead and get started.`; } @@ -59,15 +73,18 @@ async function speakWithGroq(text: string, voice: string, signal: AbortSignal): export default function InterviewClient() { const [interviewer, setInterviewer] = useState(DEFAULT_INTERVIEWER); + const [feedbackMode, setFeedbackMode] = useState("perQuestion"); const [question, setQuestion] = useState(null); const [questionNumber, setQuestionNumber] = useState(0); const [stage, setStage] = useState("idle"); const [showQuestion, setShowQuestion] = useState(false); const [showDonePrompt, setShowDonePrompt] = useState(false); const [segments, setSegments] = useState([]); + const [savedAnswers, setSavedAnswers] = useState([]); const [reviewContext, setReviewContext] = useState(null); + const [sessionPayload, setSessionPayload] = useState(null); const [statusText, setStatusText] = useState( - "Select your interviewer and press Start Interview." + "Select your interviewer, feedback timing, and press Start Interview." ); const mediaRecorderRef = useRef(null); @@ -79,6 +96,12 @@ export default function InterviewClient() { const rafRef = useRef(null); const abortRef = useRef(null); const usedIdsRef = useRef>(new Set()); + const pendingEndRef = useRef(false); + const questionRef = useRef(null); + const savedAnswersRef = useRef([]); + + questionRef.current = question; + savedAnswersRef.current = savedAnswers; const newAbort = useCallback((): AbortSignal => { abortRef.current?.abort(); @@ -102,6 +125,47 @@ export default function InterviewClient() { } }, []); + const resetInterview = useCallback(() => { + abortRef.current?.abort(); + clearTimers(); + pendingEndRef.current = false; + usedIdsRef.current.clear(); + setQuestion(null); + setQuestionNumber(0); + setSegments([]); + setSavedAnswers([]); + setReviewContext(null); + setSessionPayload(null); + setShowQuestion(false); + setShowDonePrompt(false); + setStage("idle"); + setStatusText("Select your interviewer, feedback timing, and press Start Interview."); + }, [clearTimers]); + + const finishInterview = useCallback( + (answers: ReviewContextPayload[]) => { + abortRef.current?.abort(); + clearTimers(); + pendingEndRef.current = false; + + if (feedbackMode === "endOfInterview" && answers.length > 0) { + setSavedAnswers(answers); + setSessionPayload({ answers }); + setReviewContext(null); + setShowQuestion(false); + setStage("finished"); + setStatusText("Interview complete. Review your session feedback below."); + return; + } + + resetInterview(); + if (answers.length > 0) { + setStatusText("Interview ended."); + } + }, + [clearTimers, feedbackMode, resetInterview] + ); + const stopRecording = useCallback(() => { clearTimers(); setShowDonePrompt(false); @@ -119,6 +183,7 @@ export default function InterviewClient() { const form = new FormData(); form.append("file", blob, `answer${ext}`); const signal = newAbort(); + const activeQuestion = questionRef.current; try { const res = await fetch(`${API_BASE}/speech/transcribe`, { method: "POST", @@ -128,14 +193,25 @@ export default function InterviewClient() { if (!res.ok) throw new Error(`${res.status}`); const data: Segment[] = await res.json(); setSegments(data); + + if (pendingEndRef.current && activeQuestion) { + const answers = upsertAnswer( + savedAnswersRef.current, + buildReviewContext(activeQuestion, data) + ); + finishInterview(answers); + return; + } + setStage("done"); setStatusText("Answer received. Review your response or move to the next question."); } catch (err) { + pendingEndRef.current = false; setStatusText(`Error: ${err}. Try again.`); setStage("idle"); } }, - [newAbort] + [finishInterview, newAbort] ); const startRecording = useCallback(async () => { @@ -205,12 +281,15 @@ export default function InterviewClient() { const startInterview = useCallback(async () => { usedIdsRef.current.clear(); + pendingEndRef.current = false; + setSavedAnswers([]); + setReviewContext(null); + setSessionPayload(null); const signal = newAbort(); const firstQuestion = INTRO_QUESTION; setQuestion(firstQuestion); setQuestionNumber(1); setSegments([]); - setReviewContext(null); setShowQuestion(false); setStage("playing"); setStatusText(`${interviewer.name} is introducing themselves…`); @@ -229,7 +308,14 @@ export default function InterviewClient() { startRecording(); }, [interviewer, newAbort, startRecording]); + const commitCurrentAnswer = useCallback((): ReviewContextPayload[] => { + if (!question || segments.length === 0) return savedAnswers; + return upsertAnswer(savedAnswers, buildReviewContext(question, segments)); + }, [question, segments, savedAnswers]); + const nextQuestion = useCallback(async () => { + const answers = commitCurrentAnswer(); + setSavedAnswers(answers); stopRecording(); const signal = newAbort(); const next = pickRandomQuestion(usedIdsRef.current); @@ -252,19 +338,48 @@ export default function InterviewClient() { setShowQuestion(true); startRecording(); - }, [interviewer, newAbort, stopRecording, startRecording]); + }, [commitCurrentAnswer, interviewer, newAbort, stopRecording, startRecording]); - const hearFeedback = useCallback(() => { + const endInterview = useCallback(() => { + if (stage === "recording") { + pendingEndRef.current = true; + stopRecording(); + setStatusText("Finishing your answer…"); + return; + } + + if (stage === "processing") { + pendingEndRef.current = true; + setStatusText("Finishing your answer…"); + return; + } + + if (stage === "playing") { + abortRef.current?.abort(); + } + + finishInterview(commitCurrentAnswer()); + }, [commitCurrentAnswer, finishInterview, stage, stopRecording]); + + const viewFeedback = useCallback(() => { if (!question || segments.length === 0) return; setReviewContext(buildReviewContext(question, segments)); }, [question, segments]); + const dismissFeedback = useCallback(() => { + setReviewContext(null); + }, []); + + const interviewActive = stage !== "idle" && stage !== "finished"; + const hasMoreQuestions = questionNumber < MAX_INTERVIEW_QUESTIONS; + const showPerQuestionFeedback = feedbackMode === "perQuestion" && stage === "done"; + const dotClass = [ styles.dot, stage === "playing" ? styles.playing : "", stage === "recording" ? styles.recording : "", stage === "processing" ? styles.processing : "", - stage === "done" ? styles.done : "", + stage === "done" || stage === "finished" ? styles.done : "", ] .filter(Boolean) .join(" "); @@ -274,10 +389,12 @@ export default function InterviewClient() {
Interview Coach - {questionNumber > 0 && ( + {questionNumber > 0 && stage !== "finished" && ( <> · - Question {questionNumber} + + Question {questionNumber} of {MAX_INTERVIEW_QUESTIONS} + )}
@@ -310,6 +427,30 @@ export default function InterviewClient() {
+
+ Feedback timing + + +
+
@@ -353,21 +494,37 @@ export default function InterviewClient() { {stage === "done" && ( <> - - + {hasMoreQuestions && ( + + )} + {showPerQuestionFeedback && ( + + )} )} + + {interviewActive && ( + + )} + + {stage === "finished" && ( + + )}
- {segments.length > 0 && ( + {segments.length > 0 && stage !== "finished" && ( <>
@@ -391,7 +548,14 @@ export default function InterviewClient() { {reviewContext && ( <>
- + + + )} + + {sessionPayload && stage === "finished" && ( + <> +
+ )}
diff --git a/frontend/app/scorecard/SessionScorecardPanel.module.css b/frontend/app/scorecard/SessionScorecardPanel.module.css new file mode 100644 index 0000000..15af114 --- /dev/null +++ b/frontend/app/scorecard/SessionScorecardPanel.module.css @@ -0,0 +1,66 @@ +.root { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.loading { + font-size: 0.9rem; + opacity: 0.7; +} + +.error { + font-size: 0.9rem; + color: var(--color-danger); +} + +.overall { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.overallTitle { + font-size: 1rem; + font-weight: 600; + margin: 0; +} + +.summary, +.deliveryNotes { + font-size: 0.9rem; + line-height: 1.6; + margin: 0; +} + +.listBlock { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.listHeading { + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.07em; + opacity: 0.55; + margin: 0; +} + +.list { + margin: 0; + padding-left: 1.2rem; + font-size: 0.88rem; + line-height: 1.55; +} + +.divider { + height: 1px; + background: rgba(255, 255, 255, 0.07); +} + +.questionReview { + display: flex; + flex-direction: column; + gap: 1.5rem; +} diff --git a/frontend/app/scorecard/SessionScorecardPanel.tsx b/frontend/app/scorecard/SessionScorecardPanel.tsx new file mode 100644 index 0000000..4bdab3a --- /dev/null +++ b/frontend/app/scorecard/SessionScorecardPanel.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { useEffect, useState } from "react"; + +import { Scorecard } from "@/app/scorecard/components/Scorecard/Scorecard"; +import type { + FullSessionFeedbackResponse, + SessionReviewPayload, +} from "@/lib/interview-coach/types"; + +import styles from "./SessionScorecardPanel.module.css"; + +const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000"; + +export type SessionScorecardPanelProps = { + sessionInput: SessionReviewPayload; +}; + +async function fetchSessionFeedback( + payload: SessionReviewPayload, + signal: AbortSignal +): Promise { + const res = await fetch(`${API_BASE}/feedback/generate-session`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + signal, + }); + if (!res.ok) throw new Error(`Session feedback request failed (${res.status})`); + return (await res.json()) as FullSessionFeedbackResponse; +} + +export function SessionScorecardPanel({ sessionInput }: SessionScorecardPanelProps) { + const [result, setResult] = useState(null); + const [fetching, setFetching] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const controller = new AbortController(); + + const run = async () => { + setFetching(true); + setError(null); + setResult(null); + try { + const data = await fetchSessionFeedback(sessionInput, controller.signal); + if (!controller.signal.aborted) setResult(data); + } catch (err) { + if (controller.signal.aborted) return; + setError(err instanceof Error ? err.message : "Could not generate session feedback."); + } finally { + if (!controller.signal.aborted) setFetching(false); + } + }; + + void run(); + return () => controller.abort(); + }, [sessionInput]); + + if (error) { + return ( +

+ {error} +

+ ); + } + + if (fetching || !result) { + return

Generating interview feedback…

; + } + + const answerByQuestionId = new Map( + sessionInput.answers.map((answer) => [answer.question.id, answer.transcript]) + ); + + return ( +
+
+

Overall session feedback

+

{result.overallSummary}

+
+

Strengths

+
    + {result.overallStrengths.map((item) => ( +
  • {item}
  • + ))} +
+
+
+

Improvements

+
    + {result.overallImprovements.map((item) => ( +
  • {item}
  • + ))} +
+
+

{result.overallDeliveryNotes}

+
+ +
+ + {result.questionReviews.map((review, index) => ( +
+ + {index < result.questionReviews.length - 1 ?
: null} +
+ ))} +
+ ); +} diff --git a/frontend/lib/interview-coach/types.ts b/frontend/lib/interview-coach/types.ts index 202a3b5..9ab38a4 100644 --- a/frontend/lib/interview-coach/types.ts +++ b/frontend/lib/interview-coach/types.ts @@ -77,6 +77,30 @@ export type SessionFeedbackResponse = { modelAnswer: ModelAnswer; }; +/** One answered question in a multi-question session review. */ +export type QuestionReview = { + question: InterviewQuestion; + transcriptScores: TranscriptFeedbackScores; + feedback: QualitativeFeedback; + modelAnswer: ModelAnswer; +}; + +/** Request body for POST /feedback/generate-session. */ +export type SessionReviewPayload = { + answers: ReviewContextPayload[]; +}; + +/** Response body from POST /feedback/generate-session. */ +export type FullSessionFeedbackResponse = { + overallSummary: string; + overallStrengths: string[]; + overallImprovements: string[]; + overallDeliveryNotes: string; + questionReviews: QuestionReview[]; +}; + +export type FeedbackMode = "perQuestion" | "endOfInterview"; + export type PipelineStageStatus = "idle" | "pending" | "done" | "error"; export type PipelineStageId = diff --git a/frontend/lib/prompts/questions.ts b/frontend/lib/prompts/questions.ts index d76b6f5..2a711bc 100644 --- a/frontend/lib/prompts/questions.ts +++ b/frontend/lib/prompts/questions.ts @@ -92,6 +92,9 @@ export const QUESTIONS: InterviewQuestion[] = [ }, ]; +/** Intro question plus one pass through the full question bank. */ +export const MAX_INTERVIEW_QUESTIONS = 1 + QUESTIONS.length; + export function pickRandomQuestion(usedIds: Set): InterviewQuestion { const available = QUESTIONS.filter((q) => !usedIds.has(q.id)); const pool = available.length > 0 ? available : QUESTIONS; From 0cd0d868d3fd2bc5e17fbcb06f1948739ac95d3e Mon Sep 17 00:00:00 2001 From: dangrabo <193651636+dangrabo@users.noreply.github.com> Date: Sat, 6 Jun 2026 14:17:33 -0700 Subject: [PATCH 2/2] fixed lint errors --- frontend/app/InterviewClient.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/app/InterviewClient.tsx b/frontend/app/InterviewClient.tsx index 31d70ad..a929766 100644 --- a/frontend/app/InterviewClient.tsx +++ b/frontend/app/InterviewClient.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { INTRO_QUESTION, MAX_INTERVIEW_QUESTIONS, @@ -100,8 +100,13 @@ export default function InterviewClient() { const questionRef = useRef(null); const savedAnswersRef = useRef([]); - questionRef.current = question; - savedAnswersRef.current = savedAnswers; + useEffect(() => { + questionRef.current = question; + }, [question]); + + useEffect(() => { + savedAnswersRef.current = savedAnswers; + }, [savedAnswers]); const newAbort = useCallback((): AbortSignal => { abortRef.current?.abort();