Spaces:
Sleeping
Sleeping
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 |
-
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
|
|
|
| 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 |
-
}, [
|
| 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 |
-
|
| 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 |
-
}, [
|
| 156 |
|
| 157 |
useEffect(() => {
|
| 158 |
return () => {
|
| 159 |
-
|
| 160 |
};
|
| 161 |
-
}, [
|
| 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,
|