If someone else wants to join in on the fun be my guest

This commit is contained in:
Carlos Mesquita
2024-08-19 01:24:55 +01:00
parent edc9d4de2a
commit bcb1a0f914
10 changed files with 319 additions and 122 deletions

View File

@@ -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>

View File

@@ -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[]>>
}) { }) {
/* /*
@@ -78,6 +76,7 @@ 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 { shuffleMaps } = useExamStore((state) => state);
const { questionIndex, setQuestionIndex } = useExamStore((state) => state); const { questionIndex, setQuestionIndex } = useExamStore((state) => state);
const { userSolutions: storeUserSolutions, setUserSolutions } = useExamStore((state) => state); const { userSolutions: storeUserSolutions, setUserSolutions } = useExamStore((state) => state);
const hasExamEnded = useExamStore((state) => state.hasExamEnded); const hasExamEnded = useExamStore((state) => state.hasExamEnded);
@@ -85,7 +84,10 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio
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]);
@@ -101,27 +103,49 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio
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; });
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 }; 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);
} }

View File

@@ -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 {

View File

@@ -1,5 +1,5 @@
/* 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";
@@ -15,6 +15,29 @@ function Question({
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>

View File

@@ -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";
@@ -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 &&

View File

@@ -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 {
@@ -113,8 +115,7 @@ 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 {
@@ -286,7 +287,6 @@ export interface MultipleChoiceExercise {
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;
}
} }

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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),
})); }));

View File

@@ -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
})); }));
}; };