Public Access
feat: add redo functionality for failed exam questions and update translations
This commit is contained in:
1 parent
acbcfde617
commit
028c732716
8 files changed
+86
-31
No files matched your search
@@ -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
@@ -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() ? (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -90,6 +90,7 @@ export interface ExamState {
|
||||
startedAt: number;
|
||||
completedAt?: number;
|
||||
feedbackMode: FeedbackMode;
|
||||
isRedo?: boolean;
|
||||
}
|
||||
|
||||
export interface HistoryEntry {
|
||||
|
||||
Generated
+16
-8
@@ -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
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"name": "brainwave",
|
||||
"private": true,
|
||||
"version": "0.0.1"
|
||||
"version": "0.0.1",
|
||||
"devDependencies": {
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user