Added the audio player to the level exam
This commit is contained in:
@@ -18,502 +18,542 @@ import { Tab } from "@headlessui/react";
|
|||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import { typeCheckWordsMC } from "@/utils/type.check";
|
import { typeCheckWordsMC } from "@/utils/type.check";
|
||||||
import SectionNavbar from "../Navigation/SectionNavbar";
|
import SectionNavbar from "../Navigation/SectionNavbar";
|
||||||
|
import AudioPlayer from "@/components/Low/AudioPlayer";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
exam: LevelExam;
|
exam: LevelExam;
|
||||||
showSolutions?: boolean;
|
showSolutions?: boolean;
|
||||||
onFinish: (userSolutions: UserSolution[]) => void;
|
onFinish: (userSolutions: UserSolution[]) => void;
|
||||||
preview?: boolean;
|
preview?: boolean;
|
||||||
partDividers?: boolean;
|
partDividers?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Level({ exam, showSolutions = false, onFinish, preview = false }: Props) {
|
export default function Level({ exam, showSolutions = false, onFinish, preview = false }: Props) {
|
||||||
const levelBgColor = "bg-ielts-level-light";
|
const levelBgColor = "bg-ielts-level-light";
|
||||||
|
|
||||||
const examState = useExamStore((state) => state);
|
const examState = useExamStore((state) => state);
|
||||||
const persistentExamState = usePersistentExamStore((state) => state);
|
const persistentExamState = usePersistentExamStore((state) => state);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
userSolutions,
|
userSolutions,
|
||||||
hasExamEnded,
|
hasExamEnded,
|
||||||
partIndex,
|
partIndex,
|
||||||
exerciseIndex,
|
exerciseIndex,
|
||||||
questionIndex,
|
questionIndex,
|
||||||
shuffles,
|
shuffles,
|
||||||
currentSolution,
|
currentSolution,
|
||||||
setBgColor,
|
setBgColor,
|
||||||
setUserSolutions,
|
setUserSolutions,
|
||||||
setHasExamEnded,
|
setHasExamEnded,
|
||||||
setPartIndex,
|
setPartIndex,
|
||||||
setExerciseIndex,
|
setExerciseIndex,
|
||||||
setQuestionIndex,
|
setQuestionIndex,
|
||||||
setShuffles,
|
setShuffles,
|
||||||
setCurrentSolution
|
setCurrentSolution
|
||||||
} = !preview ? examState : persistentExamState;
|
} = !preview ? examState : persistentExamState;
|
||||||
|
|
||||||
// In case client want to switch back
|
// In case client want to switch back
|
||||||
const textRenderDisabled = true;
|
const textRenderDisabled = true;
|
||||||
|
|
||||||
const [showSubmissionModal, setShowSubmissionModal] = useState(false);
|
const [timesListened, setTimesListened] = useState(0);
|
||||||
const [showQuestionsModal, setShowQuestionsModal] = useState(false);
|
const [showSubmissionModal, setShowSubmissionModal] = useState(false);
|
||||||
const [continueAnyways, setContinueAnyways] = useState(false);
|
const [showQuestionsModal, setShowQuestionsModal] = useState(false);
|
||||||
const [textRender, setTextRender] = useState(false);
|
const [continueAnyways, setContinueAnyways] = useState(false);
|
||||||
const [changedPrompt, setChangedPrompt] = useState(false);
|
const [textRender, setTextRender] = useState(false);
|
||||||
const [nextExerciseCalled, setNextExerciseCalled] = useState(false);
|
const [changedPrompt, setChangedPrompt] = useState(false);
|
||||||
const [currentSolutionSet, setCurrentSolutionSet] = useState(false);
|
const [nextExerciseCalled, setNextExerciseCalled] = useState(false);
|
||||||
|
const [currentSolutionSet, setCurrentSolutionSet] = useState(false);
|
||||||
|
|
||||||
const [seenParts, setSeenParts] = useState<Set<number>>(new Set(showSolutions ? exam.parts.map((_, index) => index) : [0]));
|
const [seenParts, setSeenParts] = useState<Set<number>>(new Set(showSolutions ? exam.parts.map((_, index) => index) : [0]));
|
||||||
|
|
||||||
const [questionModalKwargs, setQuestionModalKwargs] = useState<{
|
const [questionModalKwargs, setQuestionModalKwargs] = useState<{
|
||||||
type?: "module" | "blankQuestions" | "submit"; unanswered?: boolean | undefined; onClose: (next?: boolean) => void | undefined;
|
type?: "module" | "blankQuestions" | "submit"; unanswered?: boolean | undefined; onClose: (next?: boolean) => void | undefined;
|
||||||
}>({
|
}>({
|
||||||
type: "blankQuestions",
|
type: "blankQuestions",
|
||||||
onClose: function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } }
|
onClose: function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } }
|
||||||
});
|
});
|
||||||
|
|
||||||
const [currentExercise, setCurrentExercise] = useState<Exercise | undefined>(undefined);
|
const [currentExercise, setCurrentExercise] = useState<Exercise | undefined>(undefined);
|
||||||
const [showPartDivider, setShowPartDivider] = useState<boolean>(typeof exam.parts[0].intro === "string" && !showSolutions);
|
const [showPartDivider, setShowPartDivider] = useState<boolean>(typeof exam.parts[0].intro === "string" && !showSolutions);
|
||||||
const [startNow, setStartNow] = useState<boolean>(!showSolutions);
|
const [startNow, setStartNow] = useState<boolean>(!showSolutions);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentExercise === undefined && partIndex === 0 && exerciseIndex === 0) {
|
if (currentExercise === undefined && partIndex === 0 && exerciseIndex === 0) {
|
||||||
setCurrentExercise(exam.parts[0].exercises[0]);
|
setCurrentExercise(exam.parts[0].exercises[0]);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [currentExercise, partIndex, exerciseIndex]);
|
}, [currentExercise, partIndex, exerciseIndex]);
|
||||||
|
|
||||||
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
|
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
|
||||||
|
|
||||||
const [contextWords, setContextWords] = useState<{ match: string, originalLine: string }[] | undefined>(undefined);
|
const [contextWords, setContextWords] = useState<{ match: string, originalLine: string }[] | undefined>(undefined);
|
||||||
const [contextWordLines, setContextWordLines] = useState<number[] | undefined>(undefined);
|
const [contextWordLines, setContextWordLines] = useState<number[] | undefined>(undefined);
|
||||||
const [totalLines, setTotalLines] = useState<number>(0);
|
const [totalLines, setTotalLines] = useState<number>(0);
|
||||||
|
|
||||||
const [showSolutionsSave, setShowSolutionsSave] = useState(showSolutions ? userSolutions.filter((x) => x.module === "level") : undefined)
|
const [showSolutionsSave, setShowSolutionsSave] = useState(showSolutions ? userSolutions.filter((x) => x.module === "level") : undefined)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof currentSolution !== "undefined") {
|
if (typeof currentSolution !== "undefined") {
|
||||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== currentSolution.exercise), { ...currentSolution, module: "level" as Module, exam: exam.id, shuffleMaps: exam.shuffle ? [...shuffles.find((x) => x.exerciseID == currentExercise?.id)?.shuffles!] : [] }]);
|
setUserSolutions([...userSolutions.filter((x) => x.exercise !== currentSolution.exercise), { ...currentSolution, module: "level" as Module, exam: exam.id, shuffleMaps: exam.shuffle ? [...shuffles.find((x) => x.exerciseID == currentExercise?.id)?.shuffles!] : [] }]);
|
||||||
setCurrentSolutionSet(true);
|
setCurrentSolutionSet(true);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [currentSolution, exam.id, exam.shuffle, shuffles, currentExercise])
|
}, [currentSolution, exam.id, exam.shuffle, shuffles, currentExercise])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof currentSolution !== "undefined") {
|
if (typeof currentSolution !== "undefined") {
|
||||||
setCurrentSolution(undefined);
|
setCurrentSolution(undefined);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [currentSolution]);
|
}, [currentSolution]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showSolutions) {
|
if (showSolutions) {
|
||||||
const solutionShuffles = userSolutions.map(solution => ({
|
const solutionShuffles = userSolutions.map(solution => ({
|
||||||
exerciseID: solution.exercise,
|
exerciseID: solution.exercise,
|
||||||
shuffles: solution.shuffleMaps || []
|
shuffles: solution.shuffleMaps || []
|
||||||
}));
|
}));
|
||||||
setShuffles(solutionShuffles);
|
setShuffles(solutionShuffles);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getExercise = () => {
|
const getExercise = () => {
|
||||||
let exercise = exam.parts[partIndex]?.exercises[exerciseIndex];
|
let exercise = exam.parts[partIndex]?.exercises[exerciseIndex];
|
||||||
exercise = {
|
exercise = {
|
||||||
...exercise,
|
...exercise,
|
||||||
userSolutions: userSolutions.find((x) => x.exercise == exercise.id)?.solutions || [],
|
userSolutions: userSolutions.find((x) => x.exercise == exercise.id)?.solutions || [],
|
||||||
};
|
};
|
||||||
exercise = shuffleExamExercise(exam.shuffle, exercise, showSolutions, userSolutions, shuffles, setShuffles);
|
exercise = shuffleExamExercise(exam.shuffle, exercise, showSolutions, userSolutions, shuffles, setShuffles);
|
||||||
return exercise;
|
return exercise;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentExercise(getExercise());
|
setCurrentExercise(getExercise());
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [partIndex, exerciseIndex, questionIndex]);
|
}, [partIndex, exerciseIndex, questionIndex]);
|
||||||
|
|
||||||
const next = () => {
|
const next = () => {
|
||||||
setNextExerciseCalled(true);
|
setNextExerciseCalled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextExercise = () => {
|
const nextExercise = () => {
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
|
|
||||||
if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) {
|
if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) {
|
||||||
setExerciseIndex(exerciseIndex + 1);
|
setExerciseIndex(exerciseIndex + 1);
|
||||||
setCurrentSolutionSet(false);
|
setCurrentSolutionSet(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (partIndex + 1 === exam.parts.length && !hasExamEnded && !showQuestionsModal && !showSolutions && !continueAnyways) {
|
if (partIndex + 1 === exam.parts.length && !hasExamEnded && !showQuestionsModal && !showSolutions && !continueAnyways) {
|
||||||
modalKwargs();
|
modalKwargs();
|
||||||
setShowQuestionsModal(true);
|
setShowQuestionsModal(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (partIndex + 1 < exam.parts.length && !hasExamEnded) {
|
if (partIndex + 1 < exam.parts.length && !hasExamEnded) {
|
||||||
if (!answeredEveryQuestion(partIndex) && !continueAnyways && !showSolutions && !seenParts.has(partIndex + 1)) {
|
if (!answeredEveryQuestion(partIndex) && !continueAnyways && !showSolutions && !seenParts.has(partIndex + 1)) {
|
||||||
modalKwargs();
|
modalKwargs();
|
||||||
setShowQuestionsModal(true);
|
setShowQuestionsModal(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!showSolutions && exam.parts[0].intro && !seenParts.has(partIndex + 1)) {
|
if (!showSolutions && exam.parts[0].intro && !seenParts.has(partIndex + 1)) {
|
||||||
setShowPartDivider(true);
|
setShowPartDivider(true);
|
||||||
setBgColor(levelBgColor);
|
setBgColor(levelBgColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
setSeenParts(prev => new Set(prev).add(partIndex + 1));
|
setSeenParts(prev => new Set(prev).add(partIndex + 1));
|
||||||
|
|
||||||
if (partIndex < exam.parts.length - 1 && exam.parts[partIndex + 1].context && !textRenderDisabled) {
|
if (partIndex < exam.parts.length - 1 && exam.parts[partIndex + 1].context && !textRenderDisabled) {
|
||||||
setTextRender(true);
|
setTextRender(true);
|
||||||
}
|
}
|
||||||
setPartIndex(partIndex + 1);
|
|
||||||
setExerciseIndex(0);
|
|
||||||
setQuestionIndex(0);
|
|
||||||
setCurrentSolutionSet(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (partIndex + 1 === exam.parts.length && exerciseIndex === exam.parts[partIndex].exercises.length - 1 && !continueAnyways && !showSolutions) {
|
setTimesListened(0);
|
||||||
modalKwargs();
|
setPartIndex(partIndex + 1);
|
||||||
setShowQuestionsModal(true);
|
setExerciseIndex(0);
|
||||||
}
|
setQuestionIndex(0);
|
||||||
|
setCurrentSolutionSet(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setHasExamEnded(false);
|
if (partIndex + 1 === exam.parts.length && exerciseIndex === exam.parts[partIndex].exercises.length - 1 && !continueAnyways && !showSolutions) {
|
||||||
setCurrentSolutionSet(false);
|
modalKwargs();
|
||||||
if (typeof showSolutionsSave !== "undefined") {
|
setShowQuestionsModal(true);
|
||||||
onFinish(showSolutionsSave);
|
}
|
||||||
} else {
|
|
||||||
onFinish(userSolutions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
setHasExamEnded(false);
|
||||||
if (nextExerciseCalled && currentSolutionSet) {
|
setCurrentSolutionSet(false);
|
||||||
nextExercise();
|
if (typeof showSolutionsSave !== "undefined") {
|
||||||
setNextExerciseCalled(false);
|
onFinish(showSolutionsSave);
|
||||||
}
|
} else {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
onFinish(userSolutions);
|
||||||
}, [nextExerciseCalled, currentSolutionSet])
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const previousExercise = (solution?: UserSolution) => {
|
useEffect(() => {
|
||||||
scrollToTop();
|
if (nextExerciseCalled && currentSolutionSet) {
|
||||||
|
nextExercise();
|
||||||
|
setNextExerciseCalled(false);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [nextExerciseCalled, currentSolutionSet])
|
||||||
|
|
||||||
if (exam.parts[partIndex].context && questionIndex === 0 && !textRender && !textRenderDisabled) {
|
const previousExercise = (solution?: UserSolution) => {
|
||||||
setTextRender(true);
|
scrollToTop();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (questionIndex == 0) {
|
if (exam.parts[partIndex].context && questionIndex === 0 && !textRender && !textRenderDisabled) {
|
||||||
setPartIndex(partIndex - 1);
|
setTextRender(true);
|
||||||
if (!seenParts.has(partIndex - 1)) {
|
return;
|
||||||
setBgColor(levelBgColor);
|
}
|
||||||
setShowPartDivider(true);
|
|
||||||
setQuestionIndex(0);
|
|
||||||
setSeenParts(prev => new Set(prev).add(partIndex - 1));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1;
|
if (questionIndex == 0) {
|
||||||
const lastExercise = exam.parts[partIndex - 1].exercises[lastExerciseIndex];
|
setPartIndex(partIndex - 1);
|
||||||
setExerciseIndex(lastExerciseIndex);
|
if (!seenParts.has(partIndex - 1)) {
|
||||||
|
setBgColor(levelBgColor);
|
||||||
|
setShowPartDivider(true);
|
||||||
|
setQuestionIndex(0);
|
||||||
|
setSeenParts(prev => new Set(prev).add(partIndex - 1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (lastExercise.type === "multipleChoice") {
|
const lastExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1;
|
||||||
setQuestionIndex(lastExercise.questions.length - 1)
|
const lastExercise = exam.parts[partIndex - 1].exercises[lastExerciseIndex];
|
||||||
} else {
|
setExerciseIndex(lastExerciseIndex);
|
||||||
setQuestionIndex(0)
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setExerciseIndex(exerciseIndex - 1);
|
if (lastExercise.type === "multipleChoice") {
|
||||||
if (exerciseIndex - 1 === -1) {
|
setQuestionIndex(lastExercise.questions.length - 1)
|
||||||
setPartIndex(partIndex - 1);
|
} else {
|
||||||
const lastPartExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1;
|
setQuestionIndex(0)
|
||||||
const previousExercise = exam.parts[partIndex - 1].exercises[lastPartExerciseIndex];
|
}
|
||||||
if (previousExercise.type === "multipleChoice") {
|
return;
|
||||||
setQuestionIndex(previousExercise.questions.length - 1)
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
setExerciseIndex(exerciseIndex - 1);
|
||||||
|
if (exerciseIndex - 1 === -1) {
|
||||||
|
setPartIndex(partIndex - 1);
|
||||||
|
const lastPartExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1;
|
||||||
|
const previousExercise = exam.parts[partIndex - 1].exercises[lastPartExerciseIndex];
|
||||||
|
if (previousExercise.type === "multipleChoice") {
|
||||||
|
setQuestionIndex(previousExercise.questions.length - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const calculateExerciseIndex = () => {
|
};
|
||||||
return exam.parts.reduce((acc, curr, index) => {
|
|
||||||
if (index < partIndex) {
|
|
||||||
return acc + countExercises(curr.exercises)
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, 0) + (questionIndex + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderText = () => (
|
const calculateExerciseIndex = () => {
|
||||||
<>
|
return exam.parts.reduce((acc, curr, index) => {
|
||||||
<div className={clsx("flex flex-col gap-6 w-full bg-mti-gray-seasalt rounded-xl mt-4 relative py-8 px-16")}>
|
if (index < partIndex) {
|
||||||
<>
|
return acc + countExercises(curr.exercises)
|
||||||
<div className="flex flex-col w-full gap-2">
|
}
|
||||||
{textRender && !textRenderDisabled ? (
|
return acc;
|
||||||
<>
|
}, 0) + (questionIndex + 1);
|
||||||
<h4 className="text-xl font-semibold">
|
};
|
||||||
Please read the following excerpt attentively, you will then be asked questions about the text you've read.
|
|
||||||
</h4>
|
|
||||||
<span className="text-base">You will be allowed to read the text while doing the exercises</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<h4 className="text-xl font-semibold">
|
|
||||||
Answer the questions on the right based on what you've read.
|
|
||||||
</h4>
|
|
||||||
)}
|
|
||||||
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
|
|
||||||
{exam.parts[partIndex].context &&
|
|
||||||
<TextComponent
|
|
||||||
part={exam.parts[partIndex]}
|
|
||||||
contextWords={contextWords}
|
|
||||||
setContextWordLines={setContextWordLines}
|
|
||||||
setTotalLines={setTotalLines}
|
|
||||||
/>}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</div>
|
|
||||||
{textRender && !textRenderDisabled && (
|
|
||||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
className="max-w-[200px] w-full"
|
|
||||||
onClick={() => { setTextRender(false); previousExercise(); }}
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button color="purple" onClick={() => setTextRender(false)} className="max-w-[200px] self-end w-full">
|
const renderAudioPlayer = () => (
|
||||||
Next
|
<div className="flex flex-col gap-8 w-full bg-mti-gray-seasalt rounded-xl py-8 px-16">
|
||||||
</Button>
|
{exam?.parts[partIndex]?.audio?.source ? (
|
||||||
</div>
|
<>
|
||||||
)}
|
<div className="flex flex-col w-full gap-2">
|
||||||
</>
|
<h4 className="text-xl font-semibold">Please listen to the following audio attentively.</h4>
|
||||||
);
|
<span className="text-base">
|
||||||
|
{(() => {
|
||||||
|
const audioRepeatTimes = exam?.parts[partIndex]?.audio?.repeatableTimes;
|
||||||
|
return audioRepeatTimes && audioRepeatTimes > 0
|
||||||
|
? `You will only be allowed to listen to the audio ${audioRepeatTimes - timesListened} time(s).`
|
||||||
|
: "You may listen to the audio as many times as you would like.";
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl flex flex-col gap-4 items-center w-full h-fit">
|
||||||
|
<AudioPlayer
|
||||||
|
key={partIndex}
|
||||||
|
src={exam?.parts[partIndex]?.audio?.source ?? ''}
|
||||||
|
color="listening"
|
||||||
|
onEnd={() => setTimesListened((prev) => prev + 1)}
|
||||||
|
disabled={exam?.parts[partIndex]?.audio?.repeatableTimes != null &&
|
||||||
|
timesListened === exam.parts[partIndex]?.audio?.repeatableTimes}
|
||||||
|
disablePause
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>This section will be displayed the audio once it has been generated.</span>
|
||||||
|
)}
|
||||||
|
|
||||||
const partLabel = () => {
|
</div>
|
||||||
const partCategory = exam.parts[partIndex].category ? ` (${exam.parts[partIndex].category})` : '';
|
);
|
||||||
if (currentExercise?.type === "fillBlanks" && typeCheckWordsMC(currentExercise.words))
|
|
||||||
return `Part ${partIndex + 1} (Questions ${currentExercise.words[0].id} - ${currentExercise.words[currentExercise.words.length - 1].id})${partCategory}\n\n${currentExercise.prompt}`
|
|
||||||
|
|
||||||
if (currentExercise?.type === "multipleChoice") {
|
const renderText = () => (
|
||||||
return `Part ${partIndex + 1} (Questions ${currentExercise.questions[0].id} - ${currentExercise.questions[currentExercise.questions.length - 1].id})${partCategory}\n\n${currentExercise.prompt}`
|
<>
|
||||||
}
|
<div className={clsx("flex flex-col gap-6 w-full bg-mti-gray-seasalt rounded-xl mt-4 relative py-8 px-16")}>
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col w-full gap-2">
|
||||||
|
{textRender && !textRenderDisabled ? (
|
||||||
|
<>
|
||||||
|
<h4 className="text-xl font-semibold">
|
||||||
|
Please read the following excerpt attentively, you will then be asked questions about the text you've read.
|
||||||
|
</h4>
|
||||||
|
<span className="text-base">You will be allowed to read the text while doing the exercises</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<h4 className="text-xl font-semibold">
|
||||||
|
Answer the questions on the right based on what you've read.
|
||||||
|
</h4>
|
||||||
|
)}
|
||||||
|
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
|
||||||
|
{(exam.parts[partIndex].context || exam.parts[partIndex].text) &&
|
||||||
|
<TextComponent
|
||||||
|
part={exam.parts[partIndex]}
|
||||||
|
contextWords={contextWords}
|
||||||
|
setContextWordLines={setContextWordLines}
|
||||||
|
setTotalLines={setTotalLines}
|
||||||
|
/>}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
{textRender && !textRenderDisabled && (
|
||||||
|
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
className="max-w-[200px] w-full"
|
||||||
|
onClick={() => { setTextRender(false); previousExercise(); }}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
if (typeof exam.parts[partIndex].context === "string") {
|
<Button color="purple" onClick={() => setTextRender(false)} className="max-w-[200px] self-end w-full">
|
||||||
const nextExercise = exam.parts[partIndex].exercises[0] as MultipleChoiceExercise;
|
Next
|
||||||
return `Part ${partIndex + 1} (Questions ${nextExercise.questions[0].id} - ${nextExercise.questions[nextExercise.questions.length - 1].id})${partCategory}\n\n${nextExercise.prompt}`
|
</Button>
|
||||||
}
|
</div>
|
||||||
}
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
const answeredEveryQuestion = (partIndex: number) => {
|
const partLabel = () => {
|
||||||
return exam.parts[partIndex].exercises.every((exercise) => {
|
const partCategory = exam.parts[partIndex].category ? ` (${exam.parts[partIndex].category})` : '';
|
||||||
const userSolution = userSolutions.find(x => x.exercise === exercise.id);
|
if (currentExercise?.type === "fillBlanks" && typeCheckWordsMC(currentExercise.words))
|
||||||
switch (exercise.type) {
|
return `Part ${partIndex + 1} (Questions ${currentExercise.words[0].id} - ${currentExercise.words[currentExercise.words.length - 1].id})${partCategory}\n\n${currentExercise.prompt}`
|
||||||
case 'multipleChoice':
|
|
||||||
return userSolution?.solutions.length === exercise.questions.length;
|
|
||||||
case 'fillBlanks':
|
|
||||||
return userSolution?.solutions.length === exercise.words.length;
|
|
||||||
case 'writeBlanks':
|
|
||||||
return userSolution?.solutions.length === exercise.solutions.length;
|
|
||||||
case 'matchSentences':
|
|
||||||
return userSolution?.solutions.length === exercise.sentences.length;
|
|
||||||
case 'trueFalse':
|
|
||||||
return userSolution?.solutions.length === exercise.questions.length;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
if (currentExercise?.type === "multipleChoice") {
|
||||||
const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
|
return `Part ${partIndex + 1} (Questions ${currentExercise.questions[0].id} - ${currentExercise.questions[currentExercise.questions.length - 1].id})${partCategory}\n\n${currentExercise.prompt}`
|
||||||
|
}
|
||||||
|
|
||||||
const findMatch = (index: number) => {
|
if (typeof exam.parts[partIndex].context === "string") {
|
||||||
if (currentExercise && currentExercise.type === "multipleChoice" && currentExercise!.questions[index]) {
|
const nextExercise = exam.parts[partIndex].exercises[0] as MultipleChoiceExercise;
|
||||||
const match = currentExercise!.questions[index].prompt.match(regex);
|
return `Part ${partIndex + 1} (Questions ${nextExercise.questions[0].id} - ${nextExercise.questions[nextExercise.questions.length - 1].id})${partCategory}\n\n${nextExercise.prompt}`
|
||||||
if (match) {
|
}
|
||||||
return { match: match[1], originalLine: match[2] }
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the client for some whatever random reason decides
|
const answeredEveryQuestion = (partIndex: number) => {
|
||||||
// to add more questions update this
|
return exam.parts[partIndex].exercises.every((exercise) => {
|
||||||
const numberOfQuestions = 2;
|
const userSolution = userSolutions.find(x => x.exercise === exercise.id);
|
||||||
|
switch (exercise.type) {
|
||||||
|
case 'multipleChoice':
|
||||||
|
return userSolution?.solutions.length === exercise.questions.length;
|
||||||
|
case 'fillBlanks':
|
||||||
|
return userSolution?.solutions.length === exercise.words.length;
|
||||||
|
case 'writeBlanks':
|
||||||
|
return userSolution?.solutions.length === exercise.solutions.length;
|
||||||
|
case 'matchSentences':
|
||||||
|
return userSolution?.solutions.length === exercise.sentences.length;
|
||||||
|
case 'trueFalse':
|
||||||
|
return userSolution?.solutions.length === exercise.questions.length;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (exam.parts[partIndex].context) {
|
useEffect(() => {
|
||||||
const hits = Array.from({ length: numberOfQuestions }).reduce<{ match: string, originalLine: string }[]>((acc, _, i) => {
|
const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
|
||||||
const result = findMatch(questionIndex + i);
|
|
||||||
if (!!result) {
|
|
||||||
acc.push(result);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (hits.length > 0) {
|
const findMatch = (index: number) => {
|
||||||
setContextWords(hits)
|
if (currentExercise && currentExercise.type === "multipleChoice" && currentExercise!.questions[index]) {
|
||||||
}
|
const match = currentExercise!.questions[index].prompt.match(regex);
|
||||||
}
|
if (match) {
|
||||||
|
return { match: match[1], originalLine: match[2] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// if the client for some whatever random reason decides
|
||||||
}, [currentExercise, questionIndex, totalLines]);
|
// to add more questions update this
|
||||||
|
const numberOfQuestions = 2;
|
||||||
|
|
||||||
useEffect(() => {
|
if (exam.parts[partIndex].context) {
|
||||||
if (
|
const hits = Array.from({ length: numberOfQuestions }).reduce<{ match: string, originalLine: string }[]>((acc, _, i) => {
|
||||||
exerciseIndex !== -1 && currentExercise &&
|
const result = findMatch(questionIndex + i);
|
||||||
currentExercise.type === "multipleChoice" &&
|
if (!!result) {
|
||||||
exam.parts[partIndex].context && contextWordLines
|
acc.push(result);
|
||||||
) {
|
}
|
||||||
if (contextWordLines.length > 0) {
|
return acc;
|
||||||
contextWordLines.forEach((n, i) => {
|
}, []);
|
||||||
if (contextWords && contextWords[i] && n !== -1) {
|
|
||||||
const updatedPrompt = currentExercise!.questions[questionIndex + i].prompt.replace(
|
if (hits.length > 0) {
|
||||||
`in line ${contextWords[i].originalLine}`,
|
setContextWords(hits)
|
||||||
`in line ${n}`
|
}
|
||||||
);
|
}
|
||||||
currentExercise!.questions[questionIndex + i].prompt = updatedPrompt;
|
|
||||||
}
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
})
|
}, [currentExercise, questionIndex, totalLines]);
|
||||||
setChangedPrompt(true);
|
|
||||||
}
|
useEffect(() => {
|
||||||
}
|
if (
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
exerciseIndex !== -1 && currentExercise &&
|
||||||
}, [contextWordLines]);
|
currentExercise.type === "multipleChoice" &&
|
||||||
|
exam.parts[partIndex].context && contextWordLines
|
||||||
|
) {
|
||||||
|
if (contextWordLines.length > 0) {
|
||||||
|
contextWordLines.forEach((n, i) => {
|
||||||
|
if (contextWords && contextWords[i] && n !== -1) {
|
||||||
|
const updatedPrompt = currentExercise!.questions[questionIndex + i].prompt.replace(
|
||||||
|
`in line ${contextWords[i].originalLine}`,
|
||||||
|
`in line ${n}`
|
||||||
|
);
|
||||||
|
currentExercise!.questions[questionIndex + i].prompt = updatedPrompt;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setChangedPrompt(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [contextWordLines]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (continueAnyways) {
|
if (continueAnyways) {
|
||||||
setContinueAnyways(false);
|
setContinueAnyways(false);
|
||||||
nextExercise();
|
nextExercise();
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [continueAnyways]);
|
}, [continueAnyways]);
|
||||||
|
|
||||||
const modalKwargs = () => {
|
const modalKwargs = () => {
|
||||||
const kwargs: { type: "module" | "blankQuestions" | "submit", unanswered: boolean, onClose: (next?: boolean) => void; } = {
|
const kwargs: { type: "module" | "blankQuestions" | "submit", unanswered: boolean, onClose: (next?: boolean) => void; } = {
|
||||||
type: "blankQuestions",
|
type: "blankQuestions",
|
||||||
unanswered: false,
|
unanswered: false,
|
||||||
onClose: function (x: boolean | undefined) { if (x) { setContinueAnyways(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } }
|
onClose: function (x: boolean | undefined) { if (x) { setContinueAnyways(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } }
|
||||||
};
|
};
|
||||||
|
|
||||||
if (partIndex === exam.parts.length - 1) {
|
if (partIndex === exam.parts.length - 1) {
|
||||||
kwargs.type = "submit"
|
kwargs.type = "submit"
|
||||||
kwargs.unanswered = !exam.parts.every((_, partIndex) => answeredEveryQuestion(partIndex));
|
kwargs.unanswered = !exam.parts.every((_, partIndex) => answeredEveryQuestion(partIndex));
|
||||||
kwargs.onClose = function (x: boolean | undefined) { if (x) { setShowSubmissionModal(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } };
|
kwargs.onClose = function (x: boolean | undefined) { if (x) { setShowSubmissionModal(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } };
|
||||||
}
|
}
|
||||||
setQuestionModalKwargs(kwargs);
|
setQuestionModalKwargs(kwargs);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mcNavKwargs = {
|
const mcNavKwargs = {
|
||||||
userSolutions: userSolutions,
|
userSolutions: userSolutions,
|
||||||
exam: exam,
|
exam: exam,
|
||||||
partIndex: partIndex,
|
partIndex: partIndex,
|
||||||
showSolutions: showSolutions,
|
showSolutions: showSolutions,
|
||||||
setExerciseIndex: setExerciseIndex,
|
setExerciseIndex: setExerciseIndex,
|
||||||
setPartIndex: setPartIndex,
|
setPartIndex: setPartIndex,
|
||||||
runOnClick: setQuestionIndex
|
runOnClick: setQuestionIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const memoizedRender = useMemo(() => {
|
const memoizedRender = useMemo(() => {
|
||||||
setChangedPrompt(false);
|
setChangedPrompt(false);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{textRender && !textRenderDisabled ?
|
{textRender && !textRenderDisabled ?
|
||||||
renderText() :
|
renderText() :
|
||||||
<>
|
<>
|
||||||
{exam.parts[partIndex]?.context && renderText()}
|
{exam.parts[partIndex]?.context && renderText()}
|
||||||
{(showSolutions) ?
|
{exam.parts[partIndex]?.audio && renderAudioPlayer()}
|
||||||
currentExercise && renderSolution(currentExercise, nextExercise, previousExercise) :
|
{(showSolutions) ?
|
||||||
currentExercise && renderExercise(currentExercise, exam.id, next, previousExercise)
|
currentExercise && renderSolution(currentExercise, nextExercise, previousExercise) :
|
||||||
}
|
currentExercise && renderExercise(currentExercise, exam.id, next, previousExercise)
|
||||||
</>
|
}
|
||||||
}
|
</>
|
||||||
</>)
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
</>
|
||||||
}, [textRender, currentExercise, changedPrompt]);
|
)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [textRender, currentExercise, changedPrompt]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={clsx("flex flex-col h-full w-full gap-8 items-center", showPartDivider && "justify-center")}>
|
<div className={clsx("flex flex-col h-full w-full gap-8 items-center", showPartDivider && "justify-center")}>
|
||||||
<Modal
|
<Modal
|
||||||
className={"!w-2/6 !p-8"}
|
className={"!w-2/6 !p-8"}
|
||||||
titleClassName={"font-bold text-3xl text-mti-rose-light"}
|
titleClassName={"font-bold text-3xl text-mti-rose-light"}
|
||||||
isOpen={showSubmissionModal}
|
isOpen={showSubmissionModal}
|
||||||
onClose={() => { }}
|
onClose={() => { }}
|
||||||
title={"Confirm Submission"}
|
title={"Confirm Submission"}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<p className="text-xl mt-8 mb-12">Are you sure you want to proceed with the submission?</p>
|
<p className="text-xl mt-8 mb-12">Are you sure you want to proceed with the submission?</p>
|
||||||
<div className="w-full flex justify-between">
|
<div className="w-full flex justify-between">
|
||||||
<Button color="purple" onClick={() => setShowSubmissionModal(false)} variant="outline" className="max-w-[200px] self-end w-full !text-xl">
|
<Button color="purple" onClick={() => setShowSubmissionModal(false)} variant="outline" className="max-w-[200px] self-end w-full !text-xl">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="rose" onClick={() => { setShowSubmissionModal(false); setContinueAnyways(true) }} className="max-w-[200px] self-end w-full !text-xl">
|
<Button color="rose" onClick={() => { setShowSubmissionModal(false); setContinueAnyways(true) }} className="max-w-[200px] self-end w-full !text-xl">
|
||||||
Confirm
|
Confirm
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
</Modal>
|
</Modal>
|
||||||
<QuestionsModal isOpen={showQuestionsModal} {...questionModalKwargs} />
|
<QuestionsModal isOpen={showQuestionsModal} {...questionModalKwargs} />
|
||||||
{
|
{
|
||||||
!(partIndex === 0 && questionIndex === 0 && (showPartDivider || startNow)) &&
|
!(partIndex === 0 && questionIndex === 0 && (showPartDivider || startNow)) &&
|
||||||
<Timer minTimer={exam.minTimer} disableTimer={showSolutions} standalone={true} />
|
<Timer minTimer={exam.minTimer} disableTimer={showSolutions} standalone={true} />
|
||||||
}
|
}
|
||||||
{(showPartDivider || startNow) ?
|
{(showPartDivider || startNow) ?
|
||||||
<PartDivider
|
<PartDivider
|
||||||
module="level"
|
module="level"
|
||||||
sectionLabel="Part"
|
sectionLabel="Part"
|
||||||
defaultTitle="Placement Test"
|
defaultTitle="Placement Test"
|
||||||
section={exam.parts[partIndex]}
|
section={exam.parts[partIndex]}
|
||||||
sectionIndex={partIndex}
|
sectionIndex={partIndex}
|
||||||
onNext={() => { setShowPartDivider(false); setStartNow(false); setBgColor("bg-white"); }}
|
onNext={() => { setShowPartDivider(false); setStartNow(false); setBgColor("bg-white"); }}
|
||||||
/> : (
|
/> : (
|
||||||
<>
|
<>
|
||||||
{exam.parts[0].intro && (
|
{exam.parts[0].intro && (
|
||||||
<SectionNavbar
|
<SectionNavbar
|
||||||
module="level"
|
module="level"
|
||||||
sections={exam.parts}
|
sections={exam.parts}
|
||||||
sectionLabel="Part"
|
sectionLabel="Part"
|
||||||
sectionIndex={partIndex}
|
sectionIndex={partIndex}
|
||||||
setSectionIndex={setPartIndex}
|
setSectionIndex={setPartIndex}
|
||||||
onClick={
|
onClick={
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
setExerciseIndex(0);
|
setExerciseIndex(0);
|
||||||
setQuestionIndex(0);
|
setQuestionIndex(0);
|
||||||
if (!seenParts.has(index)) {
|
if (!seenParts.has(index)) {
|
||||||
setShowPartDivider(true);
|
setShowPartDivider(true);
|
||||||
setBgColor(levelBgColor);
|
setBgColor(levelBgColor);
|
||||||
setSeenParts(prev => new Set(prev).add(index));
|
setSeenParts(prev => new Set(prev).add(index));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} />
|
} />
|
||||||
)}
|
)}
|
||||||
<ModuleTitle
|
<ModuleTitle
|
||||||
examLabel={exam.label}
|
examLabel={exam.label}
|
||||||
partLabel={partLabel()}
|
partLabel={partLabel()}
|
||||||
minTimer={exam.minTimer}
|
minTimer={exam.minTimer}
|
||||||
exerciseIndex={calculateExerciseIndex()}
|
exerciseIndex={calculateExerciseIndex()}
|
||||||
module="level"
|
module="level"
|
||||||
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
|
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
|
||||||
disableTimer={showSolutions}
|
disableTimer={showSolutions}
|
||||||
showTimer={false}
|
showTimer={false}
|
||||||
{...mcNavKwargs}
|
{...mcNavKwargs}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"mb-20 w-full",
|
"mb-20 w-full",
|
||||||
!!exam.parts[partIndex].context && !textRender && "grid grid-cols-2 gap-4",
|
!!exam.parts[partIndex].context && !textRender && "grid grid-cols-2 gap-4",
|
||||||
)}>
|
)}>
|
||||||
{memoizedRender}
|
{memoizedRender}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user