If someone else wants to join in on the fun be my guest
This commit is contained in:
@@ -20,6 +20,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { shuffleMaps } = useExamStore((state) => state);
|
||||||
const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions);
|
const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions);
|
||||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||||
|
|
||||||
@@ -62,6 +63,15 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
} else if ('letter' in option) {
|
} else if ('letter' in option) {
|
||||||
return solution.toLowerCase() === option.word.toLowerCase();
|
return solution.toLowerCase() === option.word.toLowerCase();
|
||||||
} else if ('options' in option) {
|
} else if ('options' in option) {
|
||||||
|
if (shuffleMaps.length !== 0) {
|
||||||
|
const shuffleMap = shuffleMaps.find((map) => map.id == x.id)
|
||||||
|
if (!shuffleMap) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const original = shuffleMap[x.solution as keyof typeof shuffleMap];
|
||||||
|
return solution.toLowerCase() === (option.options[original as keyof typeof option.options] || '').toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
return solution.toLowerCase() === (option.options[x.solution as keyof typeof option.options] || '').toLowerCase();
|
return solution.toLowerCase() === (option.options[x.solution as keyof typeof option.options] || '').toLowerCase();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -119,6 +129,18 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
setAnswers((prev) => [...prev.filter((x) => x.id !== id), { id: id, solution: value }]);
|
setAnswers((prev) => [...prev.filter((x) => x.id !== id), { id: id, solution: value }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getShuffles = () => {
|
||||||
|
let shuffle = {};
|
||||||
|
if (shuffleMaps.length !== 0) {
|
||||||
|
shuffle = {
|
||||||
|
shuffleMaps: shuffleMaps.filter((map) =>
|
||||||
|
answers.some(answer => answer.id === map.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shuffle;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
||||||
@@ -190,14 +212,14 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type })}
|
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, ...getShuffles() })}
|
||||||
className="max-w-[200px] w-full">
|
className="max-w-[200px] w-full">
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
onClick={() => onNext({ exercise: id, solutions: answers, score: calculateScore(), type })}
|
onClick={() => onNext({ exercise: id, solutions: answers, score: calculateScore(), type, ...getShuffles() })}
|
||||||
className="max-w-[200px] self-end w-full">
|
className="max-w-[200px] self-end w-full">
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import {MultipleChoiceExercise, MultipleChoiceQuestion} from "@/interfaces/exam";
|
import { MultipleChoiceExercise, MultipleChoiceQuestion } from "@/interfaces/exam";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import {CommonProps} from ".";
|
import { CommonProps } from ".";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
|
|
||||||
function Question({
|
function Question({
|
||||||
@@ -14,12 +14,10 @@ function Question({
|
|||||||
options,
|
options,
|
||||||
userSolution,
|
userSolution,
|
||||||
onSelectOption,
|
onSelectOption,
|
||||||
setContextHighlight
|
|
||||||
}: MultipleChoiceQuestion & {
|
}: MultipleChoiceQuestion & {
|
||||||
userSolution: string | undefined;
|
userSolution: string | undefined;
|
||||||
onSelectOption?: (option: string) => void;
|
onSelectOption?: (option: string) => void;
|
||||||
showSolution?: boolean,
|
showSolution?: boolean,
|
||||||
setContextHighlight?: React.Dispatch<React.SetStateAction<string[]>>
|
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -35,11 +33,11 @@ function Question({
|
|||||||
// {renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")}
|
// {renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")}
|
||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col gap-10">
|
||||||
{isNaN(Number(id)) ? (
|
{isNaN(Number(id)) ? (
|
||||||
<span dangerouslySetInnerHTML={{__html: prompt}} />
|
<span dangerouslySetInnerHTML={{ __html: prompt }} />
|
||||||
) : (
|
) : (
|
||||||
<span className="">
|
<span className="">
|
||||||
<>
|
<>
|
||||||
{id} - <span dangerouslySetInnerHTML={{__html: prompt}} />
|
{id} - <span dangerouslySetInnerHTML={{ __html: prompt }} />
|
||||||
</>
|
</>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -75,53 +73,79 @@ function Question({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MultipleChoice({id, prompt, type, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) {
|
export default function MultipleChoice({ id, prompt, type, questions, userSolutions, onNext, onBack }: MultipleChoiceExercise & CommonProps) {
|
||||||
const [answers, setAnswers] = useState<{question: string; option: string}[]>(userSolutions);
|
const [answers, setAnswers] = useState<{ question: string; option: string }[]>(userSolutions);
|
||||||
|
|
||||||
const {questionIndex, setQuestionIndex} = useExamStore((state) => state);
|
const { shuffleMaps } = useExamStore((state) => state);
|
||||||
const {userSolutions: storeUserSolutions, setUserSolutions} = useExamStore((state) => state);
|
const { questionIndex, setQuestionIndex } = useExamStore((state) => state);
|
||||||
|
const { userSolutions: storeUserSolutions, setUserSolutions } = useExamStore((state) => state);
|
||||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||||
|
|
||||||
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));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUserSolutions([...storeUserSolutions.filter((x) => x.exercise !== id), {exercise: id, solutions: answers, score: calculateScore(), type}]);
|
setUserSolutions(
|
||||||
|
[...storeUserSolutions.filter((x) => x.exercise !== id), {
|
||||||
|
exercise: id, solutions: answers, score: calculateScore(), type
|
||||||
|
}]);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers]);
|
}, [answers]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
|
if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [hasExamEnded]);
|
}, [hasExamEnded]);
|
||||||
|
|
||||||
const onSelectOption = (option: string) => {
|
const onSelectOption = (option: string) => {
|
||||||
const question = questions[questionIndex];
|
const question = questions[questionIndex];
|
||||||
setAnswers((prev) => [...prev.filter((x) => x.question !== question.id), {option, question: question.id}]);
|
setAnswers((prev) => [...prev.filter((x) => x.question !== question.id), { option, question: question.id }]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateScore = () => {
|
const calculateScore = () => {
|
||||||
const total = questions.length;
|
const total = questions.length;
|
||||||
const correct = answers.filter(
|
const correct = answers.filter((x) => {
|
||||||
(x) => questions.find((y) => y.id.toString() === x.question.toString())?.solution === x.option || false,
|
const matchingQuestion = questions.find((y) => {
|
||||||
).length;
|
return y.id.toString() === x.question.toString();
|
||||||
const missing = total - answers.filter((x) => questions.find((y) => y.id.toString() === x.question.toString())).length;
|
});
|
||||||
|
|
||||||
return {total, correct, missing};
|
let isSolutionCorrect;
|
||||||
|
if (shuffleMaps.length == 0) {
|
||||||
|
isSolutionCorrect = matchingQuestion?.solution === x.option;
|
||||||
|
} else {
|
||||||
|
const shuffleMap = shuffleMaps.find((map) => map.id == x.question)
|
||||||
|
isSolutionCorrect = shuffleMap?.map[x.option] == matchingQuestion?.solution;
|
||||||
|
}
|
||||||
|
return isSolutionCorrect || false;
|
||||||
|
}).length;
|
||||||
|
const missing = total - correct;
|
||||||
|
|
||||||
|
return { total, correct, missing };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getShuffles = () => {
|
||||||
|
let shuffle = {};
|
||||||
|
if (shuffleMaps.length !== 0) {
|
||||||
|
shuffle = {
|
||||||
|
shuffleMaps: shuffleMaps.filter((map) =>
|
||||||
|
answers.some(answer => answer.question === map.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shuffle;
|
||||||
|
}
|
||||||
|
|
||||||
const next = () => {
|
const next = () => {
|
||||||
if (questionIndex === questions.length - 1) {
|
if (questionIndex === questions.length - 1) {
|
||||||
onNext({exercise: id, solutions: answers, score: calculateScore(), type});
|
onNext({ exercise: id, solutions: answers, score: calculateScore(), type, ...getShuffles() });
|
||||||
} else {
|
} else {
|
||||||
setQuestionIndex(questionIndex + 1);
|
setQuestionIndex(questionIndex + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
};
|
};
|
||||||
|
|
||||||
const back = () => {
|
const back = () => {
|
||||||
if (questionIndex === 0) {
|
if (questionIndex === 0) {
|
||||||
onBack({exercise: id, solutions: answers, score: calculateScore(), type});
|
onBack({ exercise: id, solutions: answers, score: calculateScore(), type, ...getShuffles() });
|
||||||
} else {
|
} else {
|
||||||
setQuestionIndex(questionIndex - 1);
|
setQuestionIndex(questionIndex - 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export default function FillBlanksSolutions({
|
|||||||
solutionText = options.options[correctKey as keyof typeof options.options] || solution.solution;
|
solutionText = options.options[correctKey as keyof typeof options.options] || solution.solution;
|
||||||
} else {
|
} else {
|
||||||
correct = false;
|
correct = false;
|
||||||
solutionText = solution.solution;
|
solutionText = solution?.solution;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import {MultipleChoiceExercise, MultipleChoiceQuestion} from "@/interfaces/exam";
|
import { MultipleChoiceExercise, MultipleChoiceQuestion, ShuffleMap } from "@/interfaces/exam";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import {CommonProps} from ".";
|
import { CommonProps } from ".";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
|
|
||||||
function Question({
|
function Question({
|
||||||
@@ -14,7 +14,30 @@ function Question({
|
|||||||
solution,
|
solution,
|
||||||
options,
|
options,
|
||||||
userSolution,
|
userSolution,
|
||||||
}: MultipleChoiceQuestion & {userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean}) {
|
}: MultipleChoiceQuestion & { userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean }) {
|
||||||
|
const { userSolutions } = useExamStore((state) => state);
|
||||||
|
|
||||||
|
const questionShuffleMap = userSolutions.reduce((foundMap, userSolution) => {
|
||||||
|
if (foundMap) return foundMap;
|
||||||
|
return userSolution.shuffleMaps?.find(map => map.id === id) || null;
|
||||||
|
}, null as ShuffleMap | null);
|
||||||
|
|
||||||
|
const shuffledOptions = new Array(options.length);
|
||||||
|
options.forEach(option => {
|
||||||
|
const newId = questionShuffleMap?.map[option.id];
|
||||||
|
const newIndex = options.findIndex(opt => opt.id === newId);
|
||||||
|
shuffledOptions[newIndex] = option;
|
||||||
|
});
|
||||||
|
|
||||||
|
const lettersMap = ['A', 'B', 'C', 'D'];
|
||||||
|
const optionsWithLetters = shuffledOptions.map((option, index) => ({
|
||||||
|
...option,
|
||||||
|
id: lettersMap[index]
|
||||||
|
}));
|
||||||
|
|
||||||
|
const questionOptions = questionShuffleMap ? optionsWithLetters : options;
|
||||||
|
const newQuestionSolution = questionShuffleMap ? questionShuffleMap.map[solution] : solution;
|
||||||
|
|
||||||
const renderPrompt = (prompt: string) => {
|
const renderPrompt = (prompt: string) => {
|
||||||
return reactStringReplace(prompt, /((<u>)[\w\s']+(<\/u>))/g, (match) => {
|
return reactStringReplace(prompt, /((<u>)[\w\s']+(<\/u>))/g, (match) => {
|
||||||
const word = match.replaceAll("<u>", "").replaceAll("</u>", "");
|
const word = match.replaceAll("<u>", "").replaceAll("</u>", "");
|
||||||
@@ -23,11 +46,11 @@ function Question({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const optionColor = (option: string) => {
|
const optionColor = (option: string) => {
|
||||||
if (option === solution && !userSolution) {
|
if (option === newQuestionSolution && !userSolution) {
|
||||||
return "!border-mti-gray-davy !text-mti-gray-davy";
|
return "!border-mti-gray-davy !text-mti-gray-davy";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (option === solution) {
|
if (option === newQuestionSolution) {
|
||||||
return "!border-mti-purple-light !text-mti-purple-light";
|
return "!border-mti-purple-light !text-mti-purple-light";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,24 +70,24 @@ function Question({
|
|||||||
)}
|
)}
|
||||||
<div className="grid grid-cols-4 gap-4 place-items-center">
|
<div className="grid grid-cols-4 gap-4 place-items-center">
|
||||||
{variant === "image" &&
|
{variant === "image" &&
|
||||||
options.map((option) => (
|
questionOptions.map((option) => (
|
||||||
<div
|
<div
|
||||||
key={option.id}
|
key={option?.id}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex flex-col items-center border border-mti-gray-platinum p-4 px-8 rounded-xl gap-4 cursor-pointer bg-white relative",
|
"flex flex-col items-center border border-mti-gray-platinum p-4 px-8 rounded-xl gap-4 cursor-pointer bg-white relative",
|
||||||
optionColor(option.id),
|
optionColor(option!.id),
|
||||||
)}>
|
)}>
|
||||||
<span className={clsx("text-sm", solution !== option.id && userSolution !== option.id && "opacity-50")}>{option.id}</span>
|
<span className={clsx("text-sm", newQuestionSolution !== option?.id && userSolution !== option?.id && "opacity-50")}>{option?.id}</span>
|
||||||
<img src={option.src!} alt={`Option ${option.id}`} />
|
<img src={option?.src!} alt={`Option ${option?.id}`} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{variant === "text" &&
|
{variant === "text" &&
|
||||||
options.map((option) => (
|
questionOptions.map((option) => (
|
||||||
<div
|
<div
|
||||||
key={option.id}
|
key={option?.id}
|
||||||
className={clsx("flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-sm", optionColor(option.id))}>
|
className={clsx("flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-sm", optionColor(option!.id))}>
|
||||||
<span className="font-semibold">{option.id}.</span>
|
<span className="font-semibold">{option?.id}.</span>
|
||||||
<span>{option.text}</span>
|
<span>{option?.text}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -72,8 +95,8 @@ function Question({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MultipleChoice({id, type, prompt, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) {
|
export default function MultipleChoice({ id, type, prompt, questions, userSolutions, onNext, onBack }: MultipleChoiceExercise & CommonProps) {
|
||||||
const {questionIndex, setQuestionIndex} = useExamStore((state) => state);
|
const { questionIndex, setQuestionIndex } = useExamStore((state) => state);
|
||||||
|
|
||||||
const calculateScore = () => {
|
const calculateScore = () => {
|
||||||
const total = questions.length;
|
const total = questions.length;
|
||||||
@@ -82,12 +105,12 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
|
|||||||
).length;
|
).length;
|
||||||
const missing = total - userSolutions.filter((x) => questions.find((y) => y.id.toString() === x.question.toString())).length;
|
const missing = total - userSolutions.filter((x) => questions.find((y) => y.id.toString() === x.question.toString())).length;
|
||||||
|
|
||||||
return {total, correct, missing};
|
return { total, correct, missing };
|
||||||
};
|
};
|
||||||
|
|
||||||
const next = () => {
|
const next = () => {
|
||||||
if (questionIndex === questions.length - 1) {
|
if (questionIndex === questions.length - 1) {
|
||||||
onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type});
|
onNext({ exercise: id, solutions: userSolutions, score: calculateScore(), type });
|
||||||
} else {
|
} else {
|
||||||
setQuestionIndex(questionIndex + 1);
|
setQuestionIndex(questionIndex + 1);
|
||||||
}
|
}
|
||||||
@@ -95,7 +118,7 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
|
|||||||
|
|
||||||
const back = () => {
|
const back = () => {
|
||||||
if (questionIndex === 0) {
|
if (questionIndex === 0) {
|
||||||
onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type});
|
onBack({ exercise: id, solutions: userSolutions, score: calculateScore(), type });
|
||||||
} else {
|
} else {
|
||||||
setQuestionIndex(questionIndex - 1);
|
setQuestionIndex(questionIndex - 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import Button from "@/components/Low/Button";
|
|||||||
import ModuleTitle from "@/components/Medium/ModuleTitle";
|
import ModuleTitle from "@/components/Medium/ModuleTitle";
|
||||||
import { renderSolution } from "@/components/Solutions";
|
import { renderSolution } from "@/components/Solutions";
|
||||||
import { infoButtonStyle } from "@/constants/buttonStyles";
|
import { infoButtonStyle } from "@/constants/buttonStyles";
|
||||||
import { LevelExam, LevelPart, UserSolution, WritingExam } from "@/interfaces/exam";
|
import { Module } from "@/interfaces";
|
||||||
|
import { Exercise, FillBlanksExercise, FillBlanksMCOption, LevelExam, LevelPart, MultipleChoiceExercise, ShuffleMap, UserSolution, WritingExam } from "@/interfaces/exam";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import { defaultUserSolutions } from "@/utils/exams";
|
import { defaultUserSolutions } from "@/utils/exams";
|
||||||
import { countExercises } from "@/utils/moduleUtils";
|
import { countExercises } from "@/utils/moduleUtils";
|
||||||
import { mdiArrowRight } from "@mdi/js";
|
import { mdiArrowRight } from "@mdi/js";
|
||||||
import Icon from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Fragment, use, useEffect, useRef, useState } from "react";
|
import { Dispatch, Fragment, SetStateAction, use, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { BsChevronDown, BsChevronUp } from "react-icons/bs";
|
import { BsChevronDown, BsChevronUp } from "react-icons/bs";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ function TextComponent({
|
|||||||
const lineHeightValue = parseFloat(computedStyle.lineHeight);
|
const lineHeightValue = parseFloat(computedStyle.lineHeight);
|
||||||
const containerWidth = textRef.current.clientWidth;
|
const containerWidth = textRef.current.clientWidth;
|
||||||
setLineHeight(lineHeightValue);
|
setLineHeight(lineHeightValue);
|
||||||
|
|
||||||
const offscreenElement = document.createElement('div');
|
const offscreenElement = document.createElement('div');
|
||||||
offscreenElement.style.position = 'absolute';
|
offscreenElement.style.position = 'absolute';
|
||||||
offscreenElement.style.top = '-9999px';
|
offscreenElement.style.top = '-9999px';
|
||||||
@@ -50,51 +51,51 @@ function TextComponent({
|
|||||||
offscreenElement.style.lineHeight = computedStyle.lineHeight;
|
offscreenElement.style.lineHeight = computedStyle.lineHeight;
|
||||||
offscreenElement.style.wordWrap = 'break-word';
|
offscreenElement.style.wordWrap = 'break-word';
|
||||||
offscreenElement.style.textAlign = computedStyle.textAlign as CanvasTextAlign;
|
offscreenElement.style.textAlign = computedStyle.textAlign as CanvasTextAlign;
|
||||||
|
|
||||||
const textContent = textRef.current.textContent || '';
|
const textContent = textRef.current.textContent || '';
|
||||||
textContent.split(/(\s+)/).forEach((word: string) => {
|
textContent.split(/(\s+)/).forEach((word: string) => {
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
span.textContent = word;
|
span.textContent = word;
|
||||||
offscreenElement.appendChild(span);
|
offscreenElement.appendChild(span);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.appendChild(offscreenElement);
|
document.body.appendChild(offscreenElement);
|
||||||
|
|
||||||
const lines: string[][] = [[]];
|
const lines: string[][] = [[]];
|
||||||
let currentLine = 1;
|
let currentLine = 1;
|
||||||
let currentLineTop: number | undefined;
|
let currentLineTop: number | undefined;
|
||||||
let contextWordLine: number | null = null;
|
let contextWordLine: number | null = null;
|
||||||
|
|
||||||
const firstChild = offscreenElement.firstChild as HTMLElement;
|
const firstChild = offscreenElement.firstChild as HTMLElement;
|
||||||
if (firstChild) {
|
if (firstChild) {
|
||||||
currentLineTop = firstChild.getBoundingClientRect().top;
|
currentLineTop = firstChild.getBoundingClientRect().top;
|
||||||
}
|
}
|
||||||
|
|
||||||
const spans = offscreenElement.querySelectorAll<HTMLSpanElement>('span');
|
const spans = offscreenElement.querySelectorAll<HTMLSpanElement>('span');
|
||||||
|
|
||||||
spans.forEach(span => {
|
spans.forEach(span => {
|
||||||
const rect = span.getBoundingClientRect();
|
const rect = span.getBoundingClientRect();
|
||||||
const top = rect.top;
|
const top = rect.top;
|
||||||
|
|
||||||
if (currentLineTop !== undefined && top > currentLineTop) {
|
if (currentLineTop !== undefined && top > currentLineTop) {
|
||||||
currentLine++;
|
currentLine++;
|
||||||
currentLineTop = top;
|
currentLineTop = top;
|
||||||
lines.push([]);
|
lines.push([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
lines[lines.length - 1].push(span.textContent?.trim() || '');
|
lines[lines.length - 1].push(span.textContent?.trim() || '');
|
||||||
|
|
||||||
|
|
||||||
if (contextWord && contextWordLine === null && span.textContent?.includes(contextWord)) {
|
if (contextWord && contextWordLine === null && span.textContent?.includes(contextWord)) {
|
||||||
contextWordLine = currentLine;
|
contextWordLine = currentLine;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setLineNumbers(lines.map((_, index) => index + 1));
|
setLineNumbers(lines.map((_, index) => index + 1));
|
||||||
if (contextWordLine) {
|
if (contextWordLine) {
|
||||||
setContextWordLine(contextWordLine);
|
setContextWordLine(contextWordLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.body.removeChild(offscreenElement);
|
document.body.removeChild(offscreenElement);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -154,6 +155,12 @@ function TextComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
|
||||||
|
return Array.isArray(words) && words.every(
|
||||||
|
word => word && typeof word === 'object' && 'id' in word && 'options' in word
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function Level({ exam, showSolutions = false, onFinish, editing = false }: Props) {
|
export default function Level({ exam, showSolutions = false, onFinish, editing = false }: Props) {
|
||||||
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]);
|
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]);
|
||||||
@@ -164,6 +171,8 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
const { partIndex, setPartIndex } = useExamStore((state) => state);
|
const { partIndex, setPartIndex } = useExamStore((state) => state);
|
||||||
const { exerciseIndex, setExerciseIndex } = useExamStore((state) => state);
|
const { exerciseIndex, setExerciseIndex } = useExamStore((state) => state);
|
||||||
const [storeQuestionIndex, setStoreQuestionIndex] = useExamStore((state) => [state.questionIndex, state.setQuestionIndex]);
|
const [storeQuestionIndex, setStoreQuestionIndex] = useExamStore((state) => [state.questionIndex, state.setQuestionIndex]);
|
||||||
|
const [shuffleMaps, setShuffleMaps] = useExamStore((state) => [state.shuffleMaps, state.setShuffleMaps])
|
||||||
|
const [currentExercise, setCurrentExercise] = useState<Exercise>();
|
||||||
|
|
||||||
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));
|
||||||
|
|
||||||
@@ -171,6 +180,12 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
const [contextWord, setContextWord] = useState<string | undefined>(undefined);
|
const [contextWord, setContextWord] = useState<string | undefined>(undefined);
|
||||||
const [contextWordLine, setContextWordLine] = useState<number | undefined>(undefined);
|
const [contextWordLine, setContextWordLine] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showSolutions && userSolutions[exerciseIndex].shuffleMaps) {
|
||||||
|
setShuffleMaps(userSolutions[exerciseIndex].shuffleMaps as ShuffleMap[])
|
||||||
|
}
|
||||||
|
}, [showSolutions])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasExamEnded && exerciseIndex === -1) {
|
if (hasExamEnded && exerciseIndex === -1) {
|
||||||
setExerciseIndex(exerciseIndex + 1);
|
setExerciseIndex(exerciseIndex + 1);
|
||||||
@@ -186,6 +201,128 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
onFinish(userSolutions);
|
onFinish(userSolutions);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const getExercise = () => {
|
||||||
|
if (exerciseIndex === -1) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let exercise = exam.parts[partIndex]?.exercises[exerciseIndex];
|
||||||
|
if (!exercise) return undefined;
|
||||||
|
|
||||||
|
exercise = {
|
||||||
|
...exercise,
|
||||||
|
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (exam.shuffle && exercise.type === "multipleChoice") {
|
||||||
|
if (shuffleMaps.length == 0 && !showSolutions) {
|
||||||
|
const newShuffleMaps: ShuffleMap[] = [];
|
||||||
|
|
||||||
|
exercise.questions = exercise.questions.map(question => {
|
||||||
|
const options = [...question.options];
|
||||||
|
let shuffledOptions = [...options].sort(() => Math.random() - 0.5);
|
||||||
|
|
||||||
|
const newOptions = options.map((option, index) => ({
|
||||||
|
id: option.id,
|
||||||
|
text: shuffledOptions[index].text
|
||||||
|
}));
|
||||||
|
|
||||||
|
const optionMapping = options.reduce<{ [key: string]: string }>((acc, originalOption) => {
|
||||||
|
const shuffledPosition = newOptions.find(newOpt => newOpt.text === originalOption.text)?.id;
|
||||||
|
if (shuffledPosition) {
|
||||||
|
acc[shuffledPosition] = originalOption.id;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
newShuffleMaps.push({ id: question.id, map: optionMapping });
|
||||||
|
|
||||||
|
return { ...question, options: newOptions };
|
||||||
|
});
|
||||||
|
|
||||||
|
setShuffleMaps(newShuffleMaps);
|
||||||
|
} else {
|
||||||
|
exercise.questions = exercise.questions.map(question => {
|
||||||
|
const questionShuffleMap = shuffleMaps.find(map => map.id === question.id);
|
||||||
|
if (questionShuffleMap) {
|
||||||
|
const newOptions = question.options.map(option => ({
|
||||||
|
id: option.id,
|
||||||
|
text: question.options.find(o => questionShuffleMap.map[o.id] === option.id)?.text || option.text
|
||||||
|
}));
|
||||||
|
return { ...question, options: newOptions };
|
||||||
|
}
|
||||||
|
return question;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (exam.shuffle && exercise.type === "fillBlanks" && typeCheckWordsMC(exercise.words)) {
|
||||||
|
if (shuffleMaps.length === 0 && !showSolutions) {
|
||||||
|
const newShuffleMaps: ShuffleMap[] = [];
|
||||||
|
|
||||||
|
exercise.words = exercise.words.map(word => {
|
||||||
|
if ('options' in word) {
|
||||||
|
const options = { ...word.options };
|
||||||
|
const shuffledKeys = Object.keys(options).sort(() => Math.random() - 0.5);
|
||||||
|
|
||||||
|
const newOptions = shuffledKeys.reduce((acc, key, index) => {
|
||||||
|
acc[key as keyof typeof options] = options[shuffledKeys[index] as keyof typeof options];
|
||||||
|
return acc;
|
||||||
|
}, {} as { [key in keyof typeof options]: string });
|
||||||
|
|
||||||
|
const optionMapping = shuffledKeys.reduce((acc, key, index) => {
|
||||||
|
acc[key as keyof typeof options] = Object.keys(options)[index] as keyof typeof options;
|
||||||
|
return acc;
|
||||||
|
}, {} as { [key in keyof typeof options]: string });
|
||||||
|
|
||||||
|
newShuffleMaps.push({ id: word.id, map: optionMapping });
|
||||||
|
|
||||||
|
return { ...word, options: newOptions };
|
||||||
|
}
|
||||||
|
return word;
|
||||||
|
});
|
||||||
|
|
||||||
|
setShuffleMaps(newShuffleMaps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return exercise;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newExercise = getExercise();
|
||||||
|
setCurrentExercise(newExercise);
|
||||||
|
}, [partIndex, exerciseIndex]);
|
||||||
|
|
||||||
|
|
||||||
|
//useShuffledMultipleChoiceOptions(currentExercise, exam.shuffle, storeQuestionIndex, shuffleMaps, setShuffleMaps, setCurrentExercise);
|
||||||
|
//useShuffledFillBlanksOptions(currentExercise, exam.shuffle, storeQuestionIndex, shuffleMaps, setShuffleMaps, setCurrentExercise);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
|
||||||
|
if (currentExercise && currentExercise.type === "multipleChoice") {
|
||||||
|
const match = currentExercise.questions[storeQuestionIndex].prompt.match(regex);
|
||||||
|
if (match) {
|
||||||
|
const word = match[1];
|
||||||
|
const originalLineNumber = match[2];
|
||||||
|
setContextHighlight([word]);
|
||||||
|
|
||||||
|
if (word !== contextWord) {
|
||||||
|
setContextWord(word);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedPrompt = currentExercise.questions[storeQuestionIndex].prompt.replace(
|
||||||
|
`in line ${originalLineNumber}`,
|
||||||
|
`in line ${contextWordLine || originalLineNumber}`
|
||||||
|
);
|
||||||
|
|
||||||
|
currentExercise.questions[storeQuestionIndex].prompt = updatedPrompt;
|
||||||
|
} else {
|
||||||
|
setContextHighlight([]);
|
||||||
|
setContextWord(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [storeQuestionIndex, contextWordLine, exerciseIndex, partIndex, shuffleMaps]);
|
||||||
|
|
||||||
const nextExercise = (solution?: UserSolution) => {
|
const nextExercise = (solution?: UserSolution) => {
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
if (solution) {
|
if (solution) {
|
||||||
@@ -193,8 +330,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (storeQuestionIndex > 0) {
|
if (storeQuestionIndex > 0) {
|
||||||
const exercise = getExercise();
|
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: storeQuestionIndex }]);
|
||||||
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise!.id), { id: exercise!.id, amount: storeQuestionIndex }]);
|
|
||||||
}
|
}
|
||||||
setStoreQuestionIndex(0);
|
setStoreQuestionIndex(0);
|
||||||
|
|
||||||
@@ -225,7 +361,11 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
setHasExamEnded(false);
|
setHasExamEnded(false);
|
||||||
|
|
||||||
if (solution) {
|
if (solution) {
|
||||||
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]);
|
let stat = { ...solution, module: "level" as Module, exam: exam.id }
|
||||||
|
if (exam.shuffle) {
|
||||||
|
stat.shuffleMaps = shuffleMaps
|
||||||
|
}
|
||||||
|
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...stat }]);
|
||||||
} else {
|
} else {
|
||||||
onFinish(userSolutions);
|
onFinish(userSolutions);
|
||||||
}
|
}
|
||||||
@@ -238,25 +378,13 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (storeQuestionIndex > 0) {
|
if (storeQuestionIndex > 0) {
|
||||||
const exercise = getExercise();
|
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: storeQuestionIndex }]);
|
||||||
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise!.id), { id: exercise!.id, amount: storeQuestionIndex }]);
|
|
||||||
}
|
}
|
||||||
setStoreQuestionIndex(0);
|
setStoreQuestionIndex(0);
|
||||||
|
|
||||||
setExerciseIndex(exerciseIndex - 1);
|
setExerciseIndex(exerciseIndex - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getExercise = () => {
|
|
||||||
if (exerciseIndex === -1) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const exercise = exam.parts[partIndex]?.exercises[exerciseIndex];
|
|
||||||
return {
|
|
||||||
...exercise,
|
|
||||||
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const calculateExerciseIndex = () => {
|
const calculateExerciseIndex = () => {
|
||||||
if (partIndex === 0)
|
if (partIndex === 0)
|
||||||
return (
|
return (
|
||||||
@@ -292,35 +420,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const exercise = getExercise();
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
|
|
||||||
if (exercise && exercise.type === "multipleChoice") {
|
|
||||||
const match = exercise.questions[storeQuestionIndex].prompt.match(regex);
|
|
||||||
if (match) {
|
|
||||||
const word = match[1];
|
|
||||||
const originalLineNumber = match[2];
|
|
||||||
setContextHighlight([word]);
|
|
||||||
|
|
||||||
if (word !== contextWord) {
|
|
||||||
setContextWord(word);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedPrompt = exercise.questions[storeQuestionIndex].prompt.replace(
|
|
||||||
`in line ${originalLineNumber}`,
|
|
||||||
`in line ${contextWordLine || originalLineNumber}`
|
|
||||||
);
|
|
||||||
|
|
||||||
exercise.questions[storeQuestionIndex].prompt = updatedPrompt;
|
|
||||||
} else {
|
|
||||||
setContextHighlight([]);
|
|
||||||
setContextWord(undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [storeQuestionIndex, contextWordLine]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col h-full w-full gap-8 items-center">
|
<div className="flex flex-col h-full w-full gap-8 items-center">
|
||||||
@@ -344,7 +443,8 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
exerciseIndex < exam.parts[partIndex].exercises.length &&
|
exerciseIndex < exam.parts[partIndex].exercises.length &&
|
||||||
!showSolutions &&
|
!showSolutions &&
|
||||||
!editing &&
|
!editing &&
|
||||||
renderExercise(exercise!, exam.id, nextExercise, previousExercise)}
|
currentExercise &&
|
||||||
|
renderExercise(currentExercise, exam.id, nextExercise, previousExercise)}
|
||||||
|
|
||||||
{exerciseIndex > -1 &&
|
{exerciseIndex > -1 &&
|
||||||
partIndex > -1 &&
|
partIndex > -1 &&
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ interface ExamBase {
|
|||||||
isDiagnostic: boolean;
|
isDiagnostic: boolean;
|
||||||
variant?: Variant;
|
variant?: Variant;
|
||||||
difficulty?: Difficulty;
|
difficulty?: Difficulty;
|
||||||
|
shuffle?: boolean;
|
||||||
createdBy?: string; // option as it has been added later
|
createdBy?: string; // option as it has been added later
|
||||||
createdAt?: string; // option as it has been added later
|
createdAt?: string; // option as it has been added later
|
||||||
}
|
}
|
||||||
@@ -66,6 +67,7 @@ export interface UserSolution {
|
|||||||
};
|
};
|
||||||
exercise: string;
|
exercise: string;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
|
shuffleMaps?: ShuffleMap[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WritingExam extends ExamBase {
|
export interface WritingExam extends ExamBase {
|
||||||
@@ -78,7 +80,7 @@ interface WordCounter {
|
|||||||
limit: number;
|
limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpeakingExam extends ExamBase {
|
export interface SpeakingExam extends ExamBase {
|
||||||
module: "speaking";
|
module: "speaking";
|
||||||
exercises: (SpeakingExercise | InteractiveSpeakingExercise)[];
|
exercises: (SpeakingExercise | InteractiveSpeakingExercise)[];
|
||||||
instructorGender: InstructorGender;
|
instructorGender: InstructorGender;
|
||||||
@@ -97,8 +99,8 @@ export type Exercise =
|
|||||||
export interface Evaluation {
|
export interface Evaluation {
|
||||||
comment: string;
|
comment: string;
|
||||||
overall: number;
|
overall: number;
|
||||||
task_response: {[key: string]: number | {grade: number; comment: string}};
|
task_response: { [key: string]: number | { grade: number; comment: string } };
|
||||||
misspelled_pairs?: {correction: string | null; misspelled: string}[];
|
misspelled_pairs?: { correction: string | null; misspelled: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -111,10 +113,9 @@ type InteractiveTranscriptType = { [key in InteractiveTranscriptKey]?: string };
|
|||||||
type InteractiveFixedTextType = { [key in InteractiveFixedTextKey]?: string };
|
type InteractiveFixedTextType = { [key in InteractiveFixedTextKey]?: string };
|
||||||
|
|
||||||
interface InteractiveSpeakingEvaluation extends Evaluation,
|
interface InteractiveSpeakingEvaluation extends Evaluation,
|
||||||
InteractivePerfectAnswerType,
|
InteractivePerfectAnswerType,
|
||||||
InteractiveTranscriptType,
|
InteractiveTranscriptType,
|
||||||
InteractiveFixedTextType
|
InteractiveFixedTextType { }
|
||||||
{}
|
|
||||||
|
|
||||||
|
|
||||||
interface SpeakingEvaluation extends CommonEvaluation {
|
interface SpeakingEvaluation extends CommonEvaluation {
|
||||||
@@ -233,7 +234,7 @@ export interface TrueFalseExercise {
|
|||||||
id: string;
|
id: string;
|
||||||
prompt: string; // *EXAMPLE: "Select the appropriate option."
|
prompt: string; // *EXAMPLE: "Select the appropriate option."
|
||||||
questions: TrueFalseQuestion[];
|
questions: TrueFalseQuestion[];
|
||||||
userSolutions: {id: string; solution: "true" | "false" | "not_given"}[];
|
userSolutions: { id: string; solution: "true" | "false" | "not_given" }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrueFalseQuestion {
|
export interface TrueFalseQuestion {
|
||||||
@@ -262,7 +263,7 @@ export interface MatchSentencesExercise {
|
|||||||
type: "matchSentences";
|
type: "matchSentences";
|
||||||
id: string;
|
id: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
userSolutions: {question: string; option: string}[];
|
userSolutions: { question: string; option: string }[];
|
||||||
sentences: MatchSentenceExerciseSentence[];
|
sentences: MatchSentenceExerciseSentence[];
|
||||||
allowRepetition: boolean;
|
allowRepetition: boolean;
|
||||||
options: MatchSentenceExerciseOption[];
|
options: MatchSentenceExerciseOption[];
|
||||||
@@ -285,8 +286,7 @@ export interface MultipleChoiceExercise {
|
|||||||
id: string;
|
id: string;
|
||||||
prompt: string; // *EXAMPLE: "Select the appropriate option."
|
prompt: string; // *EXAMPLE: "Select the appropriate option."
|
||||||
questions: MultipleChoiceQuestion[];
|
questions: MultipleChoiceQuestion[];
|
||||||
userSolutions: {question: string; option: string}[];
|
userSolutions: { question: string; option: string }[];
|
||||||
setContextHighlight?: React.Dispatch<React.SetStateAction<string[]>>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MultipleChoiceQuestion {
|
export interface MultipleChoiceQuestion {
|
||||||
@@ -299,4 +299,12 @@ export interface MultipleChoiceQuestion {
|
|||||||
src?: string; // *EXAMPLE: "https://i.imgur.com/rEbrSqA.png" (only used if the variant is "image")
|
src?: string; // *EXAMPLE: "https://i.imgur.com/rEbrSqA.png" (only used if the variant is "image")
|
||||||
text?: string; // *EXAMPLE: "wallet, pens and novel" (only used if the variant is "text")
|
text?: string; // *EXAMPLE: "wallet, pens and novel" (only used if the variant is "text")
|
||||||
}[];
|
}[];
|
||||||
|
shuffleMap?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShuffleMap {
|
||||||
|
id: string;
|
||||||
|
map: {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Module } from ".";
|
import { Module } from ".";
|
||||||
import { InstructorGender } from "./exam";
|
import { InstructorGender, ShuffleMap } from "./exam";
|
||||||
import { PermissionType } from "./permissions";
|
import { PermissionType } from "./permissions";
|
||||||
|
|
||||||
export type User =
|
export type User =
|
||||||
@@ -148,6 +148,7 @@ export interface Stat {
|
|||||||
missing: number;
|
missing: number;
|
||||||
};
|
};
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
|
shuffleMaps?: ShuffleMap[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Group {
|
export interface Group {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import Selection from "@/exams/Selection";
|
|||||||
import Speaking from "@/exams/Speaking";
|
import Speaking from "@/exams/Speaking";
|
||||||
import Writing from "@/exams/Writing";
|
import Writing from "@/exams/Writing";
|
||||||
import useUser from "@/hooks/useUser";
|
import useUser from "@/hooks/useUser";
|
||||||
import {Exam, UserSolution, Variant} from "@/interfaces/exam";
|
import {Exam, LevelExam, UserSolution, Variant} from "@/interfaces/exam";
|
||||||
import {Stat} from "@/interfaces/user";
|
import {Stat} from "@/interfaces/user";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation";
|
import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation";
|
||||||
@@ -257,6 +257,7 @@ export default function ExamPage({page}: Props) {
|
|||||||
user: user?.id || "",
|
user: user?.id || "",
|
||||||
date: new Date().getTime(),
|
date: new Date().getTime(),
|
||||||
isDisabled: solution.isDisabled,
|
isDisabled: solution.isDisabled,
|
||||||
|
shuffleMaps: solution.shuffleMaps,
|
||||||
...(assignment ? {assignment: assignment.id} : {}),
|
...(assignment ? {assignment: assignment.id} : {}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -459,6 +460,19 @@ export default function ExamPage({page}: Props) {
|
|||||||
inactivity: totalInactivity,
|
inactivity: totalInactivity,
|
||||||
}}
|
}}
|
||||||
onViewResults={(index?: number) => {
|
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);
|
||||||
|
}
|
||||||
setShowSolutions(true);
|
setShowSolutions(true);
|
||||||
setModuleIndex(index || 0);
|
setModuleIndex(index || 0);
|
||||||
setExerciseIndex(["reading", "listening"].includes(exams[0].module) ? -1 : 0);
|
setExerciseIndex(["reading", "listening"].includes(exams[0].module) ? -1 : 0);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {Module} from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import {Exam, UserSolution} from "@/interfaces/exam";
|
import {Exam, ShuffleMap, UserSolution} from "@/interfaces/exam";
|
||||||
import {Assignment} from "@/interfaces/results";
|
import {Assignment} from "@/interfaces/results";
|
||||||
import {create} from "zustand";
|
import {create} from "zustand";
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ export interface ExamState {
|
|||||||
exerciseIndex: number;
|
exerciseIndex: number;
|
||||||
questionIndex: number;
|
questionIndex: number;
|
||||||
inactivity: number;
|
inactivity: number;
|
||||||
|
shuffleMaps: ShuffleMap[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExamFunctions {
|
export interface ExamFunctions {
|
||||||
@@ -35,6 +36,7 @@ export interface ExamFunctions {
|
|||||||
setExerciseIndex: (exerciseIndex: number) => void;
|
setExerciseIndex: (exerciseIndex: number) => void;
|
||||||
setQuestionIndex: (questionIndex: number) => void;
|
setQuestionIndex: (questionIndex: number) => void;
|
||||||
setInactivity: (inactivity: number) => void;
|
setInactivity: (inactivity: number) => void;
|
||||||
|
setShuffleMaps: (shuffleMaps: ShuffleMap[]) => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +55,7 @@ export const initialState: ExamState = {
|
|||||||
exerciseIndex: -1,
|
exerciseIndex: -1,
|
||||||
questionIndex: 0,
|
questionIndex: 0,
|
||||||
inactivity: 0,
|
inactivity: 0,
|
||||||
|
shuffleMaps: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const useExamStore = create<ExamState & ExamFunctions>((set) => ({
|
const useExamStore = create<ExamState & ExamFunctions>((set) => ({
|
||||||
@@ -72,6 +75,7 @@ const useExamStore = create<ExamState & ExamFunctions>((set) => ({
|
|||||||
setExerciseIndex: (exerciseIndex: number) => set(() => ({exerciseIndex})),
|
setExerciseIndex: (exerciseIndex: number) => set(() => ({exerciseIndex})),
|
||||||
setQuestionIndex: (questionIndex: number) => set(() => ({questionIndex})),
|
setQuestionIndex: (questionIndex: number) => set(() => ({questionIndex})),
|
||||||
setInactivity: (inactivity: number) => set(() => ({inactivity})),
|
setInactivity: (inactivity: number) => set(() => ({inactivity})),
|
||||||
|
setShuffleMaps: (shuffleMaps) => set(() => ({shuffleMaps})),
|
||||||
|
|
||||||
reset: () => set(() => initialState),
|
reset: () => set(() => initialState),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -137,5 +137,6 @@ export const convertToUserSolutions = (stats: Stat[]): UserSolution[] => {
|
|||||||
solutions: stat.solutions,
|
solutions: stat.solutions,
|
||||||
type: stat.type,
|
type: stat.type,
|
||||||
module: stat.module,
|
module: stat.module,
|
||||||
|
shuffleMaps: stat.shuffleMaps
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user