Skip to content

Commit 55ab1bb

Browse files
committed
feat: add timeout mechanism to prevent indefinite polling in Results
- Add 60-second timeout for results polling - Generate failed results for unprocessed questions after timeout - Show clear timeout warnings and messages to users - Handle race conditions between timeout and API responses - Add English/Chinese translations for timeout messages - Prevent indefinite waiting when processing fails
1 parent 0faf6fd commit 55ab1bb

3 files changed

Lines changed: 133 additions & 12 deletions

File tree

CLAUDE.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,5 +97,4 @@ echo/
9797

9898
## todos
9999

100-
- convert to Chinese interface
101100
- update README with new images of interface with highlights

frontend/src/components/Results.vue

Lines changed: 119 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
<div class="score-label">{{ translate('results.outOf') }} {{ totalQuestions || '?' }}</div>
1313
</div>
1414
<div class="processing-info" v-if="!allProcessed">
15-
<p>{{ translate('results.processing', [formatCount(processedCount), totalQuestionCount]) }}</p>
15+
<p v-if="!timeoutOccurred">{{ translate('results.processing', [formatCount(processedCount), totalQuestionCount]) }}</p>
16+
<p v-else class="timeout-warning">{{ translate('results.timeoutWarning', [Math.floor(POLLING_TIMEOUT / 1000)]) }}</p>
1617
</div>
1718
</div>
1819

@@ -36,6 +37,11 @@
3637
<p><strong>{{ translate('results.timeTaken') }}:</strong> {{ formatDuration(resultsData.duration_seconds) }}</p>
3738
</div>
3839

40+
<!-- Timeout Warning -->
41+
<div v-if="timeoutOccurred" class="timeout-warning-box">
42+
<p>⚠️ <strong>{{ translate('results.timeoutOccurred') }}</strong> {{ translate('results.timeoutOccurredMessage') }}</p>
43+
</div>
44+
3945
<!-- AI Disclaimer -->
4046
<div class="ai-disclaimer">
4147
<p>⚠️ <strong>{{ translate('results.aiDisclaimer') }}</strong> {{ translate('results.aiDisclaimerText') }}</p>
@@ -114,10 +120,15 @@ export default {
114120
const processedCount = ref(0)
115121
const isCheckingStatus = ref(false)
116122
const isPlayingAudio = ref(null) // Track which question's audio is playing
123+
const timeoutOccurred = ref(false) // Track if timeout has occurred
124+
const pollingStartTime = ref(null) // Track when polling started
117125
118126
// Audio player element
119127
const audioPlayer = ref(null)
120128
129+
// Timeout configuration (60 seconds)
130+
const POLLING_TIMEOUT = 60000
131+
121132
// Start checking status when component mounts
122133
onMounted(async () => {
123134
await loadResults()
@@ -133,16 +144,36 @@ export default {
133144
}
134145
})
135146
136-
// Load results from backend (polling until all processed)
147+
// Load results from backend (polling until all processed or timeout)
137148
const loadResults = async () => {
138149
if (isCheckingStatus.value) return
139150
151+
// Initialize polling start time if not set
152+
if (!pollingStartTime.value) {
153+
pollingStartTime.value = Date.now()
154+
}
155+
156+
// Check for timeout
157+
const elapsedTime = Date.now() - pollingStartTime.value
158+
if (elapsedTime > POLLING_TIMEOUT && !timeoutOccurred.value) {
159+
console.log('Polling timeout reached, forcing results display')
160+
timeoutOccurred.value = true
161+
await handleTimeout()
162+
return
163+
}
164+
140165
try {
141166
isCheckingStatus.value = true
142167
console.log('Loading results for session:', props.sessionId)
143168
const response = await fetch(`/session/${props.sessionId}/results`)
144169
console.log('Response status:', response.status)
145170
171+
// Check if timeout occurred while waiting for response
172+
if (timeoutOccurred.value) {
173+
console.log('Timeout occurred during request, stopping polling')
174+
return
175+
}
176+
146177
if (response.ok) {
147178
const data = await response.json()
148179
console.log('Results loaded:', data)
@@ -156,25 +187,79 @@ export default {
156187
// Store total question count separately for display
157188
totalQuestionCount.value = data.total_questions
158189
159-
// If not all processed, continue polling
160-
if (!data.all_processed) {
190+
// If not all processed and no timeout, continue polling
191+
if (!data.all_processed && !timeoutOccurred.value) {
161192
setTimeout(loadResults, 2000)
162193
}
163194
} else {
164195
console.error('Failed to load results:', response.status)
165-
// Continue polling on error
166-
setTimeout(loadResults, 2000)
196+
// Continue polling on error if no timeout
197+
if (!timeoutOccurred.value) {
198+
setTimeout(loadResults, 2000)
199+
}
167200
}
168201
} catch (error) {
169202
console.error('Error loading results:', error)
170-
// Continue polling on error
171-
setTimeout(loadResults, 2000)
203+
// Continue polling on error if no timeout
204+
if (!timeoutOccurred.value) {
205+
setTimeout(loadResults, 2000)
206+
}
172207
} finally {
173208
isCheckingStatus.value = false
174209
}
175210
}
176211
177-
212+
// Handle timeout by generating failed results for unprocessed questions
213+
const handleTimeout = async () => {
214+
if (!resultsData.value) return
215+
216+
console.log('Handling timeout, generating failed question results')
217+
218+
// Create a copy of the results data
219+
const modifiedResults = { ...resultsData.value }
220+
221+
// Get indices of questions that haven't been processed
222+
const processedIndices = new Set(modifiedResults.question_results.map(q => q.question_index))
223+
const totalQuestions = modifiedResults.total_questions
224+
225+
// Generate failed results for unprocessed questions
226+
for (let i = 0; i < totalQuestions; i++) {
227+
if (!processedIndices.has(i)) {
228+
const failedResult = generateFailedQuestionResult(i)
229+
modifiedResults.question_results.push(failedResult)
230+
}
231+
}
232+
233+
// Sort questions by index
234+
modifiedResults.question_results.sort((a, b) => a.question_index - b.question_index)
235+
236+
// Update the results data
237+
modifiedResults.all_processed = true
238+
modifiedResults.processed_count = totalQuestions
239+
resultsData.value = modifiedResults
240+
allProcessed.value = true
241+
}
242+
243+
// Generate a failed question result
244+
const generateFailedQuestionResult = (questionIndex) => {
245+
// Try to get question info from the original exam data
246+
// Since we don't have the full exam data, we'll create a generic failed result
247+
return {
248+
question_index: questionIndex,
249+
question_id: `question_${questionIndex}`,
250+
question_type: "unknown",
251+
question_text: translate('results.questionProcessingFailed'),
252+
score: 0,
253+
feedback: translate('results.processingTimeout'),
254+
explanation: translate('results.timeoutExplanation'),
255+
suggested_answer: null,
256+
student_answer: null,
257+
reference_answer: null,
258+
student_audio_path: null
259+
}
260+
}
261+
262+
178263
// Start a new exam
179264
const startNewExam = () => {
180265
emit('new-exam')
@@ -261,6 +346,7 @@ export default {
261346
allProcessed,
262347
processedCount,
263348
isPlayingAudio,
349+
timeoutOccurred,
264350
startNewExam,
265351
goHome,
266352
formatDuration,
@@ -517,6 +603,30 @@ export default {
517603
color: #78350f;
518604
}
519605
606+
.timeout-warning {
607+
color: #dc2626;
608+
font-weight: 600;
609+
}
610+
611+
.timeout-warning-box {
612+
background: #fef2f2;
613+
border: 1px solid #dc2626;
614+
border-radius: 8px;
615+
padding: 1rem;
616+
margin-bottom: 2rem;
617+
}
618+
619+
.timeout-warning-box p {
620+
margin: 0;
621+
color: #991b1b;
622+
font-size: 0.9rem;
623+
line-height: 1.5;
624+
}
625+
626+
.timeout-warning-box strong {
627+
color: #7f1d1d;
628+
}
629+
520630
.btn {
521631
padding: 1rem 2rem;
522632
border: none;

frontend/src/translations.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,13 @@ export const translations = {
213213
finalizingResults: "Finalizing your exam results...",
214214
startNewExam: "Start New Exam",
215215
backToHome: "Back to Home",
216-
notAnswered: "Not answered"
216+
notAnswered: "Not answered",
217+
timeoutWarning: "Taking longer than expected... Showing results after {0} seconds",
218+
timeoutOccurred: "Timeout Occurred",
219+
timeoutOccurredMessage: "Some questions took too long to process and have been marked as failed. This could be due to network issues or server overload.",
220+
questionProcessingFailed: "Question processing failed",
221+
processingTimeout: "Processing timeout - answer not received",
222+
timeoutExplanation: "This question could not be processed due to a timeout. Please check your internet connection and try again."
217223
},
218224

219225
// Common
@@ -454,7 +460,13 @@ export const translations = {
454460
finalizingResults: "正在完成您的考试结果...",
455461
startNewExam: "开始新考试",
456462
backToHome: "返回首页",
457-
notAnswered: "未作答"
463+
notAnswered: "未作答",
464+
timeoutWarning: "处理时间超过预期...将在{0}秒后显示结果",
465+
timeoutOccurred: "处理超时",
466+
timeoutOccurredMessage: "部分题目处理时间过长,已被标记为失败。这可能是由于网络问题或服务器过载导致的。",
467+
questionProcessingFailed: "题目处理失败",
468+
processingTimeout: "处理超时 - 未收到答案",
469+
timeoutExplanation: "由于处理超时,无法处理此题。请检查网络连接后重试。"
458470
},
459471

460472
// Common

0 commit comments

Comments
 (0)