Merge branch 'develop' of https://bitbucket.org/ecropdev/ielts-ui into ENCOA-316-ENCOA-317
This commit is contained in:
@@ -43,7 +43,7 @@ export default function ExamPage({
|
||||
const [variant, setVariant] = useState<Variant>("full");
|
||||
const [avoidRepeated, setAvoidRepeated] = useState(false);
|
||||
const [showAbandonPopup, setShowAbandonPopup] = useState(false);
|
||||
const [pendingExercises, setPendingExercises] = useState<string[]>([]);
|
||||
const [moduleLock, setModuleLock] = useState(false);
|
||||
|
||||
const {
|
||||
exam,
|
||||
@@ -74,7 +74,6 @@ export default function ExamPage({
|
||||
saveSession,
|
||||
setFlags,
|
||||
setShuffles,
|
||||
evaluated,
|
||||
} = useExamStore();
|
||||
|
||||
const [isFetchingExams, setIsFetchingExams] = useState(false);
|
||||
@@ -139,96 +138,107 @@ export default function ExamPage({
|
||||
setShowAbandonPopup(false);
|
||||
};
|
||||
|
||||
useEvaluationPolling(sessionId ? [sessionId] : [], "exam", user?.id);
|
||||
|
||||
useEffect(() => {
|
||||
if (flags.finalizeModule && !showSolutions && flags.pendingEvaluation) {
|
||||
setModuleLock(true);
|
||||
}, [flags.finalizeModule]);
|
||||
|
||||
useEffect(() => {
|
||||
if (flags.finalizeModule && !showSolutions) {
|
||||
if (
|
||||
exam &&
|
||||
(exam.module === "writing" || exam.module === "speaking") &&
|
||||
userSolutions.length > 0 &&
|
||||
!showSolutions
|
||||
userSolutions.length > 0
|
||||
) {
|
||||
const exercisesToEvaluate = exam.exercises.map(
|
||||
(exercise) => exercise.id
|
||||
);
|
||||
|
||||
setPendingExercises(exercisesToEvaluate);
|
||||
(async () => {
|
||||
await Promise.all(
|
||||
exam.exercises.map(async (exercise, index) => {
|
||||
if (exercise.type === "writing")
|
||||
await evaluateWritingAnswer(
|
||||
user.id,
|
||||
sessionId,
|
||||
exercise,
|
||||
index + 1,
|
||||
userSolutions.find((x) => x.exercise === exercise.id)!,
|
||||
exercise.attachment?.url
|
||||
);
|
||||
|
||||
if (
|
||||
exercise.type === "interactiveSpeaking" ||
|
||||
exercise.type === "speaking"
|
||||
) {
|
||||
await evaluateSpeakingAnswer(
|
||||
user.id,
|
||||
sessionId,
|
||||
exercise,
|
||||
userSolutions.find((x) => x.exercise === exercise.id)!,
|
||||
index + 1
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
exam.exercises.map(async (exercise, index) => {
|
||||
if (exercise.type === "writing") {
|
||||
const sol = await evaluateWritingAnswer(
|
||||
user.id,
|
||||
sessionId,
|
||||
exercise,
|
||||
index + 1,
|
||||
userSolutions.find((x) => x.exercise === exercise.id)!,
|
||||
exercise.attachment?.url
|
||||
);
|
||||
return sol;
|
||||
}
|
||||
if (
|
||||
exercise.type === "interactiveSpeaking" ||
|
||||
exercise.type === "speaking"
|
||||
) {
|
||||
const sol = await evaluateSpeakingAnswer(
|
||||
user.id,
|
||||
sessionId,
|
||||
exercise,
|
||||
userSolutions.find((x) => x.exercise === exercise.id)!,
|
||||
index + 1
|
||||
);
|
||||
return sol;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
);
|
||||
const updatedSolutions = userSolutions.map((solution) => {
|
||||
const completed = results
|
||||
.filter((r) => r !== null)
|
||||
.find((c: any) => c.exercise === solution.exercise);
|
||||
return completed || solution;
|
||||
});
|
||||
setUserSolutions(updatedSolutions);
|
||||
} catch (error) {
|
||||
console.error("Error during module evaluation:", error);
|
||||
} finally {
|
||||
setModuleLock(false);
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
setModuleLock(false);
|
||||
}
|
||||
}
|
||||
}, [exam, showSolutions, userSolutions, sessionId, user?.id, flags]);
|
||||
|
||||
useEvaluationPolling({ pendingExercises, setPendingExercises });
|
||||
}, [
|
||||
exam,
|
||||
showSolutions,
|
||||
userSolutions,
|
||||
sessionId,
|
||||
user.id,
|
||||
flags.finalizeModule,
|
||||
setUserSolutions,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (flags.finalizeExam && moduleIndex !== -1) {
|
||||
setModuleIndex(-1);
|
||||
if (flags.finalizeExam && moduleIndex !== -1 && !moduleLock) {
|
||||
(async () => {
|
||||
setModuleIndex(-1);
|
||||
await saveStats();
|
||||
await axios.get("/api/stats/update");
|
||||
})();
|
||||
}
|
||||
}, [flags.finalizeExam, moduleIndex, setModuleIndex]);
|
||||
}, [
|
||||
flags.finalizeExam,
|
||||
moduleIndex,
|
||||
saveStats,
|
||||
setModuleIndex,
|
||||
userSolutions,
|
||||
moduleLock,
|
||||
flags.finalizeModule,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
flags.finalizeExam &&
|
||||
!flags.pendingEvaluation &&
|
||||
pendingExercises.length === 0
|
||||
!userSolutions.some((s) => s.isDisabled) &&
|
||||
!moduleLock
|
||||
) {
|
||||
(async () => {
|
||||
if (evaluated.length !== 0) {
|
||||
setUserSolutions(
|
||||
userSolutions.map((solution) => {
|
||||
const evaluatedSolution = evaluated.find(
|
||||
(e) => e.exercise === solution.exercise
|
||||
);
|
||||
if (evaluatedSolution) {
|
||||
return { ...solution, ...evaluatedSolution };
|
||||
}
|
||||
return solution;
|
||||
})
|
||||
);
|
||||
}
|
||||
await saveStats();
|
||||
await axios.get("/api/stats/update");
|
||||
setShowSolutions(true);
|
||||
setFlags({ finalizeExam: false });
|
||||
dispatch({ type: "UPDATE_EXAMS" });
|
||||
})();
|
||||
setShowSolutions(true);
|
||||
setFlags({ finalizeExam: false });
|
||||
dispatch({ type: "UPDATE_EXAMS" });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
saveStats,
|
||||
setFlags,
|
||||
setModuleIndex,
|
||||
evaluated,
|
||||
pendingExercises,
|
||||
setUserSolutions,
|
||||
flags,
|
||||
]);
|
||||
}, [flags.finalizeExam, userSolutions, showSolutions, moduleLock]);
|
||||
|
||||
const aggregateScoresByModule = (
|
||||
isPractice?: boolean
|
||||
@@ -269,7 +279,7 @@ export default function ExamPage({
|
||||
};
|
||||
|
||||
userSolutions.forEach((x) => {
|
||||
if ((isPractice && x.isPractice) || (!isPractice && !x.isPractice)) {
|
||||
if (x.isPractice === isPractice) {
|
||||
const examModule =
|
||||
x.module ||
|
||||
(x.type === "writing"
|
||||
@@ -286,12 +296,13 @@ export default function ExamPage({
|
||||
}
|
||||
});
|
||||
|
||||
return Object.keys(scores).reduce((acc, x) => {
|
||||
if (scores[x as Module].total > 0) {
|
||||
acc.push({ module: x as Module, ...scores[x as Module] });
|
||||
}
|
||||
return acc;
|
||||
}, [] as any[]);
|
||||
return Object.keys(scores).reduce<
|
||||
{ module: Module; total: number; missing: number; correct: number }[]
|
||||
>((accm, x) => {
|
||||
if (scores[x as Module].total > 0)
|
||||
accm.push({ module: x as Module, ...scores[x as Module] });
|
||||
return accm;
|
||||
}, []);
|
||||
};
|
||||
|
||||
const ModuleExamMap: Record<Module, React.ComponentType<ExamProps<Exam>>> = {
|
||||
@@ -318,8 +329,7 @@ export default function ExamPage({
|
||||
|
||||
useEffect(() => {
|
||||
setOnFocusLayerMouseEnter(() => () => setShowAbandonPopup(true));
|
||||
}, [
|
||||
]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setBgColor(bgColor);
|
||||
@@ -339,111 +349,112 @@ export default function ExamPage({
|
||||
setHideSidebar,
|
||||
showSolutions,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToastContainer />
|
||||
{user && (
|
||||
<>
|
||||
{/* Modules weren't yet set by an INIT_EXAM or INIT_SOLUTIONS dispatch, show Selection component*/}
|
||||
{selectedModules.length === 0 && (
|
||||
<Selection
|
||||
page={page}
|
||||
user={user!}
|
||||
onStart={(
|
||||
modules: Module[],
|
||||
avoid: boolean,
|
||||
variant: Variant
|
||||
) => {
|
||||
setModuleIndex(0);
|
||||
setAvoidRepeated(avoid);
|
||||
setSelectedModules(modules);
|
||||
setVariant(variant);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isFetchingExams && (
|
||||
<div className="flex flex-grow flex-col items-center justify-center animate-pulse">
|
||||
<span
|
||||
className={`loading loading-infinity w-32 bg-ielts-${selectedModules[0]}`}
|
||||
/>
|
||||
<span
|
||||
className={`font-bold text-2xl text-ielts-${selectedModules[0]}`}
|
||||
>
|
||||
Loading Exam ...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{moduleIndex === -1 && selectedModules.length !== 0 && (
|
||||
<Finish
|
||||
isLoading={flags.pendingEvaluation}
|
||||
user={user!}
|
||||
modules={selectedModules}
|
||||
solutions={userSolutions}
|
||||
assignment={assignment}
|
||||
information={{
|
||||
timeSpent,
|
||||
inactivity,
|
||||
}}
|
||||
destination={destination}
|
||||
onViewResults={(index?: number) => {
|
||||
if (exams[0].module === "level") {
|
||||
const levelExam = exams[0] as LevelExam;
|
||||
const allExercises = levelExam.parts.flatMap(
|
||||
(part) => part.exercises
|
||||
);
|
||||
const exerciseOrderMap = new Map(
|
||||
allExercises.map((ex, index) => [ex.id, index])
|
||||
);
|
||||
const orderedSolutions = userSolutions
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
const indexA =
|
||||
exerciseOrderMap.get(a.exercise) ?? Infinity;
|
||||
const indexB =
|
||||
exerciseOrderMap.get(b.exercise) ?? Infinity;
|
||||
return indexA - indexB;
|
||||
});
|
||||
setUserSolutions(orderedSolutions);
|
||||
} else {
|
||||
setUserSolutions(userSolutions);
|
||||
}
|
||||
setShuffles([]);
|
||||
if (index === undefined) {
|
||||
setFlags({ reviewAll: true });
|
||||
<>
|
||||
{/* Modules weren't yet set by an INIT_EXAM or INIT_SOLUTIONS dispatch, show Selection component*/}
|
||||
{selectedModules.length === 0 && (
|
||||
<Selection
|
||||
page={page}
|
||||
user={user!}
|
||||
onStart={(
|
||||
modules: Module[],
|
||||
avoid: boolean,
|
||||
variant: Variant
|
||||
) => {
|
||||
setModuleIndex(0);
|
||||
setExam(exams[0]);
|
||||
} else {
|
||||
setModuleIndex(index);
|
||||
setExam(exams[index]);
|
||||
}
|
||||
setShowSolutions(true);
|
||||
setQuestionIndex(0);
|
||||
setExerciseIndex(0);
|
||||
setPartIndex(0);
|
||||
}}
|
||||
scores={aggregateScoresByModule()}
|
||||
practiceScores={aggregateScoresByModule(true)}
|
||||
/>
|
||||
)}
|
||||
{/* Exam is on going, display it and the abandon modal */}
|
||||
{isExamLoaded && moduleIndex !== -1 && (
|
||||
<>
|
||||
{exam && CurrentExam && (
|
||||
<CurrentExam exam={exam} showSolutions={showSolutions} />
|
||||
)}
|
||||
{!showSolutions && (
|
||||
<AbandonPopup
|
||||
isOpen={showAbandonPopup}
|
||||
abandonPopupTitle="Leave Exercise"
|
||||
abandonPopupDescription="Are you sure you want to leave the exercise? Your progress will be saved and this exam can be resumed on the Dashboard."
|
||||
abandonConfirmButtonText="Confirm"
|
||||
onAbandon={onAbandon}
|
||||
onCancel={() => setShowAbandonPopup(false)}
|
||||
setAvoidRepeated(avoid);
|
||||
setSelectedModules(modules);
|
||||
setVariant(variant);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isFetchingExams && (
|
||||
<div className="flex flex-grow flex-col items-center justify-center animate-pulse">
|
||||
<span
|
||||
className={`loading loading-infinity w-32 bg-ielts-${selectedModules[0]}`}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
<span
|
||||
className={`font-bold text-2xl text-ielts-${selectedModules[0]}`}
|
||||
>
|
||||
Loading Exam ...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{moduleIndex === -1 && selectedModules.length !== 0 && (
|
||||
<Finish
|
||||
isLoading={userSolutions.some((s) => s.isDisabled)}
|
||||
user={user!}
|
||||
modules={selectedModules}
|
||||
solutions={userSolutions}
|
||||
assignment={assignment}
|
||||
information={{
|
||||
timeSpent,
|
||||
inactivity,
|
||||
}}
|
||||
destination={destination}
|
||||
onViewResults={(index?: number) => {
|
||||
if (exams[0].module === "level") {
|
||||
const levelExam = exams[0] as LevelExam;
|
||||
const allExercises = levelExam.parts.flatMap(
|
||||
(part) => part.exercises
|
||||
);
|
||||
const exerciseOrderMap = new Map(
|
||||
allExercises.map((ex, index) => [ex.id, index])
|
||||
);
|
||||
const orderedSolutions = userSolutions
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
const indexA =
|
||||
exerciseOrderMap.get(a.exercise) ?? Infinity;
|
||||
const indexB =
|
||||
exerciseOrderMap.get(b.exercise) ?? Infinity;
|
||||
return indexA - indexB;
|
||||
});
|
||||
setUserSolutions(orderedSolutions);
|
||||
} else {
|
||||
setUserSolutions(userSolutions);
|
||||
}
|
||||
setShuffles([]);
|
||||
if (index === undefined) {
|
||||
setFlags({ reviewAll: true });
|
||||
setModuleIndex(0);
|
||||
setExam(exams[0]);
|
||||
} else {
|
||||
setModuleIndex(index);
|
||||
setExam(exams[index]);
|
||||
}
|
||||
setShowSolutions(true);
|
||||
setQuestionIndex(0);
|
||||
setExerciseIndex(0);
|
||||
setPartIndex(0);
|
||||
}}
|
||||
scores={aggregateScoresByModule()}
|
||||
practiceScores={aggregateScoresByModule(true)}
|
||||
/>
|
||||
)}
|
||||
{/* Exam is on going, display it and the abandon modal */}
|
||||
{isExamLoaded && moduleIndex !== -1 && (
|
||||
<>
|
||||
{exam && CurrentExam && (
|
||||
<CurrentExam exam={exam} showSolutions={showSolutions} />
|
||||
)}
|
||||
{!showSolutions && (
|
||||
<AbandonPopup
|
||||
isOpen={showAbandonPopup}
|
||||
abandonPopupTitle="Leave Exercise"
|
||||
abandonPopupDescription="Are you sure you want to leave the exercise? Your progress will be saved and this exam can be resumed on the Dashboard."
|
||||
abandonConfirmButtonText="Confirm"
|
||||
onAbandon={onAbandon}
|
||||
onCancel={() => setShowAbandonPopup(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user