feat: add redo functionality for failed exam questions and update translations
Test / backend (push) Successful in 14s
Test / frontend (push) Successful in 24s

This commit is contained in:
LaurensBartelds committed 2026-04-27 20:15:28 +02:00
1 parent acbcfde617
commit 028c732716
8 files changed
+86 -31

No files matched your search

+1
View File
@@ -7,3 +7,4 @@ backend/db/brainwave.db-wal
node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json
brainwave.db-shm
brainwave.db-wal
node_modules/
+49 -20
View File
@@ -129,7 +129,7 @@ function AppInner() {
setExamState(withNotes(state));
};
const loadExam = (data: Exam, saveToWorkspace = false, examId?: number | null) => {
const loadExam = (data: Exam, saveToWorkspace = false, examId?: number | null, isRedo = false) => {
if (saveToWorkspace && user && isAuthenticated()) {
examApi.createExam(data)
.then((res) => {
@@ -156,6 +156,7 @@ function AppInner() {
revealed: {},
startedAt: Date.now(),
feedbackMode: fm,
isRedo,
});
});
navigate(`/exam/${getExamUrlId(data)}/q1`);
@@ -195,33 +196,60 @@ function AppInner() {
state: { ...examState, completedAt },
};
if (user && isAuthenticated()) {
void resultApi.submitResult({
exam_id: savedExamIdRef.current,
title: entry.title,
score_points: entry.scorePoints,
total_points: entry.totalPoints,
correct_count: entry.correctCount,
graded_count: entry.gradedCount,
passing_score: entry.passingScore,
elapsed_ms: entry.elapsedMs,
state: entry.state as unknown,
exam: entry.exam as unknown,
}).then((data) => {
sessionStorage.setItem('brainwave.pendingResultId', String(data.id));
}).catch(() => {
// Skip saving to history / API when this is a redo session
if (!examState.isRedo) {
if (user && isAuthenticated()) {
void resultApi.submitResult({
exam_id: savedExamIdRef.current,
title: entry.title,
score_points: entry.scorePoints,
total_points: entry.totalPoints,
correct_count: entry.correctCount,
graded_count: entry.gradedCount,
passing_score: entry.passingScore,
elapsed_ms: entry.elapsedMs,
state: entry.state as unknown,
exam: entry.exam as unknown,
}).then((data) => {
sessionStorage.setItem('brainwave.pendingResultId', String(data.id));
}).catch(() => {
appendHistory(entry);
});
} else {
appendHistory(entry);
});
} else {
appendHistory(entry);
}
setHistory((prev) => [entry, ...prev]);
}
setHistory((prev) => [entry, ...prev]);
clearState();
setExamState((s) => (s ? { ...s, completedAt } : s));
navigate("/results");
}, [exam, examState, user, navigate]);
const redoFailed = useCallback(() => {
if (!exam || !examState) return;
const graded = exam.questions.map((q) =>
gradeQuestion(q, examState.answers[q.id]),
);
const failedQuestions = exam.questions
.map((q, i) => ({ q, result: graded[i] }))
.filter(({ result }) => !result.correct)
.map(({ q }) => q);
if (failedQuestions.length === 0) return;
// Shuffle failed questions using Fisher-Yates
const shuffled = [...failedQuestions];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
const redoExam: Exam = { ...exam, questions: shuffled };
loadExam(redoExam, false, savedExamIdRef.current, true);
}, [exam, examState]);
const exit = () => {
setExam(null);
setExamState(null);
@@ -461,6 +489,7 @@ function AppInner() {
state={examState}
onRestart={restart}
onExit={exit}
onRedoFailed={redoFailed}
reviewOnly={false}
/>
) : user && isAuthenticated() ? (
+13 -2
View File
@@ -12,10 +12,11 @@ interface Props {
state: ExamState
onRestart: () => void
onExit: () => void
onRedoFailed?: () => void
reviewOnly?: boolean
}
export default function Results({ exam, state, onRestart, onExit, reviewOnly }: Props) {
export default function Results({ exam, state, onRestart, onExit, onRedoFailed, reviewOnly }: Props) {
const { t } = useTranslation()
const { answers, startedAt, completedAt } = state
const elapsedMs = (completedAt ?? Date.now()) - startedAt
@@ -30,6 +31,7 @@ export default function Results({ exam, state, onRestart, onExit, reviewOnly }:
const scorePoints = graded.reduce((a, g) => a + g.result.points, 0)
const gradedCount = graded.filter((g) => g.result.status !== 'ungraded').length
const correctCount = graded.filter((g) => g.result.correct).length
const failedCount = gradedCount - correctCount
const pct = totalPoints ? Math.round((scorePoints / totalPoints) * 100) : 0
const passing = exam.passingScore ?? 60
const passed = pct >= passing
@@ -137,7 +139,16 @@ export default function Results({ exam, state, onRestart, onExit, reviewOnly }:
})}
<div style={{ marginTop: 48, display: 'flex', gap: 12 }}>
{!reviewOnly && <button className="btn btn-primary" onClick={onRestart}>{t('results.retakeExam')}</button>}
{!reviewOnly && (
<>
<button className="btn btn-primary" onClick={onRestart}>{t('results.retakeExam')}</button>
{onRedoFailed && failedCount > 0 && (
<button className="btn" onClick={onRedoFailed}>
{t('results.redoFailed')}
</button>
)}
</>
)}
<button className="btn" onClick={onExit}>{t('results.backToHome')}</button>
</div>
</div>
+1
View File
@@ -124,6 +124,7 @@
"explanation": "Explanation",
"rubric": "Rubric (self-grade)",
"retakeExam": "Retake exam",
"redoFailed": "Redo failed questions",
"cantRetake": "Exam no longer saved in workspace",
"backToHome": "Back to home"
},
+1
View File
@@ -124,6 +124,7 @@
"explanation": "Uitleg",
"rubric": "Rubric (zelfbeoordeling)",
"retakeExam": "Examen opnieuw maken",
"redoFailed": "Foute vragen opnieuw maken",
"cantRetake": "Examen niet meer opgeslagen in werkruimte",
"backToHome": "Terug naar home"
},
+1
View File
@@ -90,6 +90,7 @@ export interface ExamState {
startedAt: number;
completedAt?: number;
feedbackMode: FeedbackMode;
isRedo?: boolean;
}
export interface HistoryEntry {
+16 -8
View File
@@ -1,20 +1,28 @@
{
"name": "brainwave",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"helmet": "^8.1.0"
"name": "brainwave",
"version": "0.0.1",
"devDependencies": {
"typescript": "^6.0.3"
}
},
"node_modules/helmet": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
"integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
"license": "MIT",
"node_modules/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=18.0.0"
"node": ">=14.17"
}
}
}
+4 -1
View File
@@ -1,5 +1,8 @@
{
"name": "brainwave",
"private": true,
"version": "0.0.1"
"version": "0.0.1",
"devDependencies": {
"typescript": "^6.0.3"
}
}