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,
onBack,
}) => {
const { shuffleMaps } = useExamStore((state) => state);
const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions);
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
@@ -62,6 +63,15 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
} else if ('letter' in option) {
return solution.toLowerCase() === option.word.toLowerCase();
} 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 false;
@@ -119,6 +129,18 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
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 (
<>
<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
color="purple"
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">
Back
</Button>
<Button
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">
Next
</Button>

View File

@@ -1,10 +1,10 @@
/* 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 clsx from "clsx";
import {useEffect, useState} from "react";
import { useEffect, useState } from "react";
import reactStringReplace from "react-string-replace";
import {CommonProps} from ".";
import { CommonProps } from ".";
import Button from "../Low/Button";
function Question({
@@ -14,12 +14,10 @@ function Question({
options,
userSolution,
onSelectOption,
setContextHighlight
}: MultipleChoiceQuestion & {
userSolution: string | undefined;
onSelectOption?: (option: string) => void;
showSolution?: boolean,
setContextHighlight?: React.Dispatch<React.SetStateAction<string[]>>
}) {
/*
@@ -35,11 +33,11 @@ function Question({
// {renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")}
<div className="flex flex-col gap-10">
{isNaN(Number(id)) ? (
<span dangerouslySetInnerHTML={{__html: prompt}} />
<span dangerouslySetInnerHTML={{ __html: prompt }} />
) : (
<span className="">
<>
{id} - <span dangerouslySetInnerHTML={{__html: prompt}} />
{id} - <span dangerouslySetInnerHTML={{ __html: prompt }} />
</>
</span>
)}
@@ -75,53 +73,79 @@ function Question({
);
}
export default function MultipleChoice({id, prompt, type, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) {
const [answers, setAnswers] = useState<{question: string; option: string}[]>(userSolutions);
export default function MultipleChoice({ id, prompt, type, questions, userSolutions, onNext, onBack }: MultipleChoiceExercise & CommonProps) {
const [answers, setAnswers] = useState<{ question: string; option: string }[]>(userSolutions);
const {questionIndex, setQuestionIndex} = useExamStore((state) => state);
const {userSolutions: storeUserSolutions, setUserSolutions} = useExamStore((state) => state);
const { shuffleMaps } = useExamStore((state) => state);
const { questionIndex, setQuestionIndex } = useExamStore((state) => state);
const { userSolutions: storeUserSolutions, setUserSolutions } = useExamStore((state) => state);
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
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
}, [answers]);
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
}, [hasExamEnded]);
const onSelectOption = (option: string) => {
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 total = questions.length;
const correct = answers.filter(
(x) => questions.find((y) => y.id.toString() === x.question.toString())?.solution === x.option || false,
).length;
const missing = total - answers.filter((x) => questions.find((y) => y.id.toString() === x.question.toString())).length;
const correct = answers.filter((x) => {
const matchingQuestion = questions.find((y) => {
return y.id.toString() === x.question.toString();
});
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 = () => {
if (questionIndex === questions.length - 1) {
onNext({exercise: id, solutions: answers, score: calculateScore(), type});
onNext({ exercise: id, solutions: answers, score: calculateScore(), type, ...getShuffles() });
} else {
setQuestionIndex(questionIndex + 1);
}
scrollToTop();
};
const back = () => {
if (questionIndex === 0) {
onBack({exercise: id, solutions: answers, score: calculateScore(), type});
onBack({ exercise: id, solutions: answers, score: calculateScore(), type, ...getShuffles() });
} else {
setQuestionIndex(questionIndex - 1);
}

View File

@@ -105,7 +105,7 @@ export default function FillBlanksSolutions({
solutionText = options.options[correctKey as keyof typeof options.options] || solution.solution;
} else {
correct = false;
solutionText = solution.solution;
solutionText = solution?.solution;
}
} else {

View File

@@ -1,10 +1,10 @@
/* 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 clsx from "clsx";
import {useEffect, useState} from "react";
import { useEffect, useState } from "react";
import reactStringReplace from "react-string-replace";
import {CommonProps} from ".";
import { CommonProps } from ".";
import Button from "../Low/Button";
function Question({
@@ -14,7 +14,30 @@ function Question({
solution,
options,
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) => {
return reactStringReplace(prompt, /((<u>)[\w\s']+(<\/u>))/g, (match) => {
const word = match.replaceAll("<u>", "").replaceAll("</u>", "");
@@ -23,11 +46,11 @@ function Question({
};
const optionColor = (option: string) => {
if (option === solution && !userSolution) {
if (option === newQuestionSolution && !userSolution) {
return "!border-mti-gray-davy !text-mti-gray-davy";
}
if (option === solution) {
if (option === newQuestionSolution) {
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">
{variant === "image" &&
options.map((option) => (
questionOptions.map((option) => (
<div
key={option.id}
key={option?.id}
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",
optionColor(option.id),
optionColor(option!.id),
)}>
<span className={clsx("text-sm", solution !== option.id && userSolution !== option.id && "opacity-50")}>{option.id}</span>
<img src={option.src!} alt={`Option ${option.id}`} />
<span className={clsx("text-sm", newQuestionSolution !== option?.id && userSolution !== option?.id && "opacity-50")}>{option?.id}</span>
<img src={option?.src!} alt={`Option ${option?.id}`} />
</div>
))}
{variant === "text" &&
options.map((option) => (
questionOptions.map((option) => (
<div
key={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>{option.text}</span>
key={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>{option?.text}</span>
</div>
))}
</div>
@@ -72,8 +95,8 @@ function Question({
);
}
export default function MultipleChoice({id, type, prompt, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) {
const {questionIndex, setQuestionIndex} = useExamStore((state) => state);
export default function MultipleChoice({ id, type, prompt, questions, userSolutions, onNext, onBack }: MultipleChoiceExercise & CommonProps) {
const { questionIndex, setQuestionIndex } = useExamStore((state) => state);
const calculateScore = () => {
const total = questions.length;
@@ -82,12 +105,12 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
).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 = () => {
if (questionIndex === questions.length - 1) {
onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type});
onNext({ exercise: id, solutions: userSolutions, score: calculateScore(), type });
} else {
setQuestionIndex(questionIndex + 1);
}
@@ -95,7 +118,7 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
const back = () => {
if (questionIndex === 0) {
onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type});
onBack({ exercise: id, solutions: userSolutions, score: calculateScore(), type });
} else {
setQuestionIndex(questionIndex - 1);
}

View File

@@ -5,14 +5,15 @@ import Button from "@/components/Low/Button";
import ModuleTitle from "@/components/Medium/ModuleTitle";
import { renderSolution } from "@/components/Solutions";
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 { defaultUserSolutions } from "@/utils/exams";
import { countExercises } from "@/utils/moduleUtils";
import { mdiArrowRight } from "@mdi/js";
import Icon from "@mdi/react";
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 { 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) {
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 { exerciseIndex, setExerciseIndex } = useExamStore((state) => state);
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));
@@ -171,6 +180,12 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
const [contextWord, setContextWord] = useState<string | undefined>(undefined);
const [contextWordLine, setContextWordLine] = useState<number | undefined>(undefined);
useEffect(() => {
if (showSolutions && userSolutions[exerciseIndex].shuffleMaps) {
setShuffleMaps(userSolutions[exerciseIndex].shuffleMaps as ShuffleMap[])
}
}, [showSolutions])
useEffect(() => {
if (hasExamEnded && exerciseIndex === -1) {
setExerciseIndex(exerciseIndex + 1);
@@ -186,6 +201,128 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
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) => {
scrollToTop();
if (solution) {
@@ -193,8 +330,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
}
if (storeQuestionIndex > 0) {
const exercise = getExercise();
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise!.id), { id: exercise!.id, amount: storeQuestionIndex }]);
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: storeQuestionIndex }]);
}
setStoreQuestionIndex(0);
@@ -225,7 +361,11 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
setHasExamEnded(false);
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 {
onFinish(userSolutions);
}
@@ -238,25 +378,13 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
}
if (storeQuestionIndex > 0) {
const exercise = getExercise();
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise!.id), { id: exercise!.id, amount: storeQuestionIndex }]);
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: storeQuestionIndex }]);
}
setStoreQuestionIndex(0);
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 = () => {
if (partIndex === 0)
return (
@@ -292,35 +420,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
</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 (
<>
<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 &&
!showSolutions &&
!editing &&
renderExercise(exercise!, exam.id, nextExercise, previousExercise)}
currentExercise &&
renderExercise(currentExercise, exam.id, nextExercise, previousExercise)}
{exerciseIndex > -1 &&
partIndex > -1 &&

View File

@@ -12,6 +12,7 @@ interface ExamBase {
isDiagnostic: boolean;
variant?: Variant;
difficulty?: Difficulty;
shuffle?: boolean;
createdBy?: 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;
isDisabled?: boolean;
shuffleMaps?: ShuffleMap[]
}
export interface WritingExam extends ExamBase {
@@ -78,7 +80,7 @@ interface WordCounter {
limit: number;
}
export interface SpeakingExam extends ExamBase {
export interface SpeakingExam extends ExamBase {
module: "speaking";
exercises: (SpeakingExercise | InteractiveSpeakingExercise)[];
instructorGender: InstructorGender;
@@ -97,8 +99,8 @@ export type Exercise =
export interface Evaluation {
comment: string;
overall: number;
task_response: {[key: string]: number | {grade: number; comment: string}};
misspelled_pairs?: {correction: string | null; misspelled: string}[];
task_response: { [key: string]: number | { grade: number; comment: string } };
misspelled_pairs?: { correction: string | null; misspelled: string }[];
}
@@ -111,10 +113,9 @@ type InteractiveTranscriptType = { [key in InteractiveTranscriptKey]?: string };
type InteractiveFixedTextType = { [key in InteractiveFixedTextKey]?: string };
interface InteractiveSpeakingEvaluation extends Evaluation,
InteractivePerfectAnswerType,
InteractiveTranscriptType,
InteractiveFixedTextType
{}
InteractivePerfectAnswerType,
InteractiveTranscriptType,
InteractiveFixedTextType { }
interface SpeakingEvaluation extends CommonEvaluation {
@@ -233,7 +234,7 @@ export interface TrueFalseExercise {
id: string;
prompt: string; // *EXAMPLE: "Select the appropriate option."
questions: TrueFalseQuestion[];
userSolutions: {id: string; solution: "true" | "false" | "not_given"}[];
userSolutions: { id: string; solution: "true" | "false" | "not_given" }[];
}
export interface TrueFalseQuestion {
@@ -262,7 +263,7 @@ export interface MatchSentencesExercise {
type: "matchSentences";
id: string;
prompt: string;
userSolutions: {question: string; option: string}[];
userSolutions: { question: string; option: string }[];
sentences: MatchSentenceExerciseSentence[];
allowRepetition: boolean;
options: MatchSentenceExerciseOption[];
@@ -285,8 +286,7 @@ export interface MultipleChoiceExercise {
id: string;
prompt: string; // *EXAMPLE: "Select the appropriate option."
questions: MultipleChoiceQuestion[];
userSolutions: {question: string; option: string}[];
setContextHighlight?: React.Dispatch<React.SetStateAction<string[]>>
userSolutions: { question: string; option: string }[];
}
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")
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 { InstructorGender } from "./exam";
import { InstructorGender, ShuffleMap } from "./exam";
import { PermissionType } from "./permissions";
export type User =
@@ -148,6 +148,7 @@ export interface Stat {
missing: number;
};
isDisabled?: boolean;
shuffleMaps?: ShuffleMap[];
}
export interface Group {

View File

@@ -12,7 +12,7 @@ import Selection from "@/exams/Selection";
import Speaking from "@/exams/Speaking";
import Writing from "@/exams/Writing";
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 useExamStore from "@/stores/examStore";
import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation";
@@ -257,6 +257,7 @@ export default function ExamPage({page}: Props) {
user: user?.id || "",
date: new Date().getTime(),
isDisabled: solution.isDisabled,
shuffleMaps: solution.shuffleMaps,
...(assignment ? {assignment: assignment.id} : {}),
}));
@@ -459,6 +460,19 @@ export default function ExamPage({page}: Props) {
inactivity: totalInactivity,
}}
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);
setModuleIndex(index || 0);
setExerciseIndex(["reading", "listening"].includes(exams[0].module) ? -1 : 0);

View File

@@ -1,5 +1,5 @@
import {Module} from "@/interfaces";
import {Exam, UserSolution} from "@/interfaces/exam";
import {Exam, ShuffleMap, UserSolution} from "@/interfaces/exam";
import {Assignment} from "@/interfaces/results";
import {create} from "zustand";
@@ -18,6 +18,7 @@ export interface ExamState {
exerciseIndex: number;
questionIndex: number;
inactivity: number;
shuffleMaps: ShuffleMap[];
}
export interface ExamFunctions {
@@ -35,6 +36,7 @@ export interface ExamFunctions {
setExerciseIndex: (exerciseIndex: number) => void;
setQuestionIndex: (questionIndex: number) => void;
setInactivity: (inactivity: number) => void;
setShuffleMaps: (shuffleMaps: ShuffleMap[]) => void;
reset: () => void;
}
@@ -53,6 +55,7 @@ export const initialState: ExamState = {
exerciseIndex: -1,
questionIndex: 0,
inactivity: 0,
shuffleMaps: []
};
const useExamStore = create<ExamState & ExamFunctions>((set) => ({
@@ -72,6 +75,7 @@ const useExamStore = create<ExamState & ExamFunctions>((set) => ({
setExerciseIndex: (exerciseIndex: number) => set(() => ({exerciseIndex})),
setQuestionIndex: (questionIndex: number) => set(() => ({questionIndex})),
setInactivity: (inactivity: number) => set(() => ({inactivity})),
setShuffleMaps: (shuffleMaps) => set(() => ({shuffleMaps})),
reset: () => set(() => initialState),
}));

View File

@@ -137,5 +137,6 @@ export const convertToUserSolutions = (stats: Stat[]): UserSolution[] => {
solutions: stat.solutions,
type: stat.type,
module: stat.module,
shuffleMaps: stat.shuffleMaps
}));
};