faais-k commited on
Commit
59c3cc3
·
1 Parent(s): 3df0574

fix: improve session recovery and audio recorder reliability

Browse files
frontend/src/contexts/InterviewContext.jsx CHANGED
@@ -134,6 +134,25 @@ export function InterviewProvider({ children }) {
134
  sessionId: sid
135
  }
136
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  }
138
  // If no active question, user stays at their current step
139
  } catch (e) {
@@ -158,8 +177,13 @@ export function InterviewProvider({ children }) {
158
  try {
159
  await api.startInterview(state.sessionId);
160
  const res = await api.nextQuestion(state.sessionId);
161
- dispatch({ type: "SET_QUESTION", v: res.question, total: res.total_questions || 0 });
162
- setStep("interview");
 
 
 
 
 
163
  } catch (e) {
164
  setError(e.message || "Could not start interview.");
165
  }
@@ -226,7 +250,7 @@ export function InterviewProvider({ children }) {
226
  try {
227
  // Use formal skip endpoint that properly updates backend state
228
  const result = await api.skipQuestion(state.sessionId, state.question.id);
229
- if (!result.next_question || result.next_question?.status === "completed") {
230
  setStep("processing");
231
  return;
232
  }
 
134
  sessionId: sid
135
  }
136
  });
137
+ } else if (storedStep === "interview" && status.status === "active") {
138
+ // Last answer was already saved, but no unanswered question is active.
139
+ // This happens after a refresh between scoring and fetching the next question.
140
+ const next = await api.nextQuestion(sid);
141
+ if (next.status === "completed" || next.status === "awaiting_wrapup_answer" || !next.question) {
142
+ dispatch({ type: "CLEAR_QUESTION" });
143
+ dispatch({ type: "SET_STEP", v: "processing" });
144
+ } else {
145
+ dispatch({
146
+ type: "RESTORE_STATE",
147
+ state: {
148
+ step: "interview",
149
+ question: next.question,
150
+ questionNumber: (status.questions_asked_count || 0) + 1,
151
+ totalQuestions: next.total_questions || status.total_questions,
152
+ sessionId: sid
153
+ }
154
+ });
155
+ }
156
  }
157
  // If no active question, user stays at their current step
158
  } catch (e) {
 
177
  try {
178
  await api.startInterview(state.sessionId);
179
  const res = await api.nextQuestion(state.sessionId);
180
+ if (res.status === "completed" || res.status === "awaiting_wrapup_answer" || !res.question) {
181
+ setLoading(false);
182
+ setStep("processing");
183
+ } else {
184
+ dispatch({ type: "SET_QUESTION", v: res.question, total: res.total_questions || 0 });
185
+ setStep("interview");
186
+ }
187
  } catch (e) {
188
  setError(e.message || "Could not start interview.");
189
  }
 
250
  try {
251
  // Use formal skip endpoint that properly updates backend state
252
  const result = await api.skipQuestion(state.sessionId, state.question.id);
253
+ if (!result.next_question) {
254
  setStep("processing");
255
  return;
256
  }
frontend/src/hooks/useAudioRecorder.js CHANGED
@@ -24,10 +24,19 @@ export function useAudioRecorder() {
24
 
25
  const mediaRef = useRef(null);
26
  const chunksRef = useRef([]);
 
 
27
  const audioCtxRef = useRef(null);
28
  const analyserRef = useRef(null);
29
  const rafRef = useRef(null);
30
 
 
 
 
 
 
 
 
31
  const updateVolume = useCallback(() => {
32
  if (!analyserRef.current) return;
33
  const dataArray = new Uint8Array(analyserRef.current.frequencyBinCount);
@@ -50,7 +59,8 @@ export function useAudioRecorder() {
50
  const start = useCallback(async () => {
51
  setMicError(null);
52
 
53
- if (audioURL) URL.revokeObjectURL(audioURL);
 
54
  setAudioBlob(null);
55
  setAudioURL(null);
56
  setVolume(0);
@@ -82,6 +92,9 @@ export function useAudioRecorder() {
82
  // Small delay to ensure all dataavailable events have fired
83
  setTimeout(() => {
84
  try {
 
 
 
85
  if (chunksRef.current.length === 0) {
86
  console.warn("No audio chunks collected.");
87
  setMicError("No audio was captured. Please speak into the microphone.");
@@ -91,6 +104,7 @@ export function useAudioRecorder() {
91
 
92
  const blob = new Blob(chunksRef.current, { type: mimeType || "audio/webm" });
93
  const newURL = URL.createObjectURL(blob);
 
94
  setAudioBlob(blob);
95
  setAudioURL(newURL);
96
  } catch (err) {
@@ -118,7 +132,7 @@ export function useAudioRecorder() {
118
  console.error("Mic access failed:", e);
119
  setMicError("Microphone access denied. Please allow microphone access and try again.");
120
  }
121
- }, [audioURL]);
122
 
123
  useEffect(() => {
124
  if (recording) {
@@ -142,9 +156,10 @@ export function useAudioRecorder() {
142
  const reset = useCallback(() => {
143
  // Stop any active recording first
144
  if (mediaRef.current && mediaRef.current.state !== "inactive") {
 
145
  mediaRef.current.stop();
146
  }
147
- if (audioURL) URL.revokeObjectURL(audioURL);
148
  setAudioBlob(null);
149
  setAudioURL(null);
150
  setRecording(false);
@@ -152,13 +167,13 @@ export function useAudioRecorder() {
152
  setVolume(0);
153
  chunksRef.current = [];
154
  mediaRef.current = null;
155
- }, [audioURL]);
156
 
157
  useEffect(() => {
158
  return () => {
159
- if (audioURL) URL.revokeObjectURL(audioURL);
160
  };
161
- }, [audioURL]);
162
 
163
  return {
164
  recording,
 
24
 
25
  const mediaRef = useRef(null);
26
  const chunksRef = useRef([]);
27
+ const audioURLRef = useRef(null);
28
+ const suppressStopBlobRef = useRef(false);
29
  const audioCtxRef = useRef(null);
30
  const analyserRef = useRef(null);
31
  const rafRef = useRef(null);
32
 
33
+ const revokeAudioURL = useCallback(() => {
34
+ if (audioURLRef.current) {
35
+ URL.revokeObjectURL(audioURLRef.current);
36
+ audioURLRef.current = null;
37
+ }
38
+ }, []);
39
+
40
  const updateVolume = useCallback(() => {
41
  if (!analyserRef.current) return;
42
  const dataArray = new Uint8Array(analyserRef.current.frequencyBinCount);
 
59
  const start = useCallback(async () => {
60
  setMicError(null);
61
 
62
+ suppressStopBlobRef.current = false;
63
+ revokeAudioURL();
64
  setAudioBlob(null);
65
  setAudioURL(null);
66
  setVolume(0);
 
92
  // Small delay to ensure all dataavailable events have fired
93
  setTimeout(() => {
94
  try {
95
+ if (suppressStopBlobRef.current) {
96
+ return;
97
+ }
98
  if (chunksRef.current.length === 0) {
99
  console.warn("No audio chunks collected.");
100
  setMicError("No audio was captured. Please speak into the microphone.");
 
104
 
105
  const blob = new Blob(chunksRef.current, { type: mimeType || "audio/webm" });
106
  const newURL = URL.createObjectURL(blob);
107
+ audioURLRef.current = newURL;
108
  setAudioBlob(blob);
109
  setAudioURL(newURL);
110
  } catch (err) {
 
132
  console.error("Mic access failed:", e);
133
  setMicError("Microphone access denied. Please allow microphone access and try again.");
134
  }
135
+ }, [revokeAudioURL]);
136
 
137
  useEffect(() => {
138
  if (recording) {
 
156
  const reset = useCallback(() => {
157
  // Stop any active recording first
158
  if (mediaRef.current && mediaRef.current.state !== "inactive") {
159
+ suppressStopBlobRef.current = true;
160
  mediaRef.current.stop();
161
  }
162
+ revokeAudioURL();
163
  setAudioBlob(null);
164
  setAudioURL(null);
165
  setRecording(false);
 
167
  setVolume(0);
168
  chunksRef.current = [];
169
  mediaRef.current = null;
170
+ }, [revokeAudioURL]);
171
 
172
  useEffect(() => {
173
  return () => {
174
+ revokeAudioURL();
175
  };
176
+ }, [revokeAudioURL]);
177
 
178
  return {
179
  recording,