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
+67
-12
No files matched your search
@@ -7,3 +7,4 @@ backend/db/brainwave.db-wal
|
|||||||
node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json
|
node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json
|
||||||
brainwave.db-shm
|
brainwave.db-shm
|
||||||
brainwave.db-wal
|
brainwave.db-wal
|
||||||
|
node_modules/
|
||||||
+30
-1
@@ -129,7 +129,7 @@ function AppInner() {
|
|||||||
setExamState(withNotes(state));
|
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()) {
|
if (saveToWorkspace && user && isAuthenticated()) {
|
||||||
examApi.createExam(data)
|
examApi.createExam(data)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@@ -156,6 +156,7 @@ function AppInner() {
|
|||||||
revealed: {},
|
revealed: {},
|
||||||
startedAt: Date.now(),
|
startedAt: Date.now(),
|
||||||
feedbackMode: fm,
|
feedbackMode: fm,
|
||||||
|
isRedo,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
navigate(`/exam/${getExamUrlId(data)}/q1`);
|
navigate(`/exam/${getExamUrlId(data)}/q1`);
|
||||||
@@ -195,6 +196,8 @@ function AppInner() {
|
|||||||
state: { ...examState, completedAt },
|
state: { ...examState, completedAt },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Skip saving to history / API when this is a redo session
|
||||||
|
if (!examState.isRedo) {
|
||||||
if (user && isAuthenticated()) {
|
if (user && isAuthenticated()) {
|
||||||
void resultApi.submitResult({
|
void resultApi.submitResult({
|
||||||
exam_id: savedExamIdRef.current,
|
exam_id: savedExamIdRef.current,
|
||||||
@@ -217,11 +220,36 @@ function AppInner() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setHistory((prev) => [entry, ...prev]);
|
setHistory((prev) => [entry, ...prev]);
|
||||||
|
}
|
||||||
|
|
||||||
clearState();
|
clearState();
|
||||||
setExamState((s) => (s ? { ...s, completedAt } : s));
|
setExamState((s) => (s ? { ...s, completedAt } : s));
|
||||||
navigate("/results");
|
navigate("/results");
|
||||||
}, [exam, examState, user, navigate]);
|
}, [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 = () => {
|
const exit = () => {
|
||||||
setExam(null);
|
setExam(null);
|
||||||
setExamState(null);
|
setExamState(null);
|
||||||
@@ -461,6 +489,7 @@ function AppInner() {
|
|||||||
state={examState}
|
state={examState}
|
||||||
onRestart={restart}
|
onRestart={restart}
|
||||||
onExit={exit}
|
onExit={exit}
|
||||||
|
onRedoFailed={redoFailed}
|
||||||
reviewOnly={false}
|
reviewOnly={false}
|
||||||
/>
|
/>
|
||||||
) : user && isAuthenticated() ? (
|
) : user && isAuthenticated() ? (
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ interface Props {
|
|||||||
state: ExamState
|
state: ExamState
|
||||||
onRestart: () => void
|
onRestart: () => void
|
||||||
onExit: () => void
|
onExit: () => void
|
||||||
|
onRedoFailed?: () => void
|
||||||
reviewOnly?: boolean
|
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 { t } = useTranslation()
|
||||||
const { answers, startedAt, completedAt } = state
|
const { answers, startedAt, completedAt } = state
|
||||||
const elapsedMs = (completedAt ?? Date.now()) - startedAt
|
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 scorePoints = graded.reduce((a, g) => a + g.result.points, 0)
|
||||||
const gradedCount = graded.filter((g) => g.result.status !== 'ungraded').length
|
const gradedCount = graded.filter((g) => g.result.status !== 'ungraded').length
|
||||||
const correctCount = graded.filter((g) => g.result.correct).length
|
const correctCount = graded.filter((g) => g.result.correct).length
|
||||||
|
const failedCount = gradedCount - correctCount
|
||||||
const pct = totalPoints ? Math.round((scorePoints / totalPoints) * 100) : 0
|
const pct = totalPoints ? Math.round((scorePoints / totalPoints) * 100) : 0
|
||||||
const passing = exam.passingScore ?? 60
|
const passing = exam.passingScore ?? 60
|
||||||
const passed = pct >= passing
|
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 }}>
|
<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>
|
<button className="btn" onClick={onExit}>{t('results.backToHome')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -124,6 +124,7 @@
|
|||||||
"explanation": "Explanation",
|
"explanation": "Explanation",
|
||||||
"rubric": "Rubric (self-grade)",
|
"rubric": "Rubric (self-grade)",
|
||||||
"retakeExam": "Retake exam",
|
"retakeExam": "Retake exam",
|
||||||
|
"redoFailed": "Redo failed questions",
|
||||||
"cantRetake": "Exam no longer saved in workspace",
|
"cantRetake": "Exam no longer saved in workspace",
|
||||||
"backToHome": "Back to home"
|
"backToHome": "Back to home"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -124,6 +124,7 @@
|
|||||||
"explanation": "Uitleg",
|
"explanation": "Uitleg",
|
||||||
"rubric": "Rubric (zelfbeoordeling)",
|
"rubric": "Rubric (zelfbeoordeling)",
|
||||||
"retakeExam": "Examen opnieuw maken",
|
"retakeExam": "Examen opnieuw maken",
|
||||||
|
"redoFailed": "Foute vragen opnieuw maken",
|
||||||
"cantRetake": "Examen niet meer opgeslagen in werkruimte",
|
"cantRetake": "Examen niet meer opgeslagen in werkruimte",
|
||||||
"backToHome": "Terug naar home"
|
"backToHome": "Terug naar home"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export interface ExamState {
|
|||||||
startedAt: number;
|
startedAt: number;
|
||||||
completedAt?: number;
|
completedAt?: number;
|
||||||
feedbackMode: FeedbackMode;
|
feedbackMode: FeedbackMode;
|
||||||
|
isRedo?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HistoryEntry {
|
export interface HistoryEntry {
|
||||||
|
|||||||
Generated
+16
-8
@@ -1,20 +1,28 @@
|
|||||||
{
|
{
|
||||||
"name": "brainwave",
|
"name": "brainwave",
|
||||||
|
"version": "0.0.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"name": "brainwave",
|
||||||
"helmet": "^8.1.0"
|
"version": "0.0.1",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/helmet": {
|
"node_modules/typescript": {
|
||||||
"version": "8.1.0",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||||
"integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
|
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||||
"license": "MIT",
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "brainwave",
|
"name": "brainwave",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1"
|
"version": "0.0.1",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^6.0.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user