Fill Blanks changes
This commit is contained in:
@@ -1,187 +0,0 @@
|
||||
import {FillBlanksExercise} from "@/interfaces/exam";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import clsx from "clsx";
|
||||
import {Fragment, useEffect, useState} from "react";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
import {CommonProps} from ".";
|
||||
import Button from "../Low/Button";
|
||||
|
||||
interface WordsDrawerProps {
|
||||
words: {word: string; isDisabled: boolean}[];
|
||||
isOpen: boolean;
|
||||
blankId?: string;
|
||||
previouslySelectedWord?: string;
|
||||
onCancel: () => void;
|
||||
onAnswer: (answer: string) => void;
|
||||
}
|
||||
|
||||
function WordsDrawer({words, isOpen, blankId, previouslySelectedWord, onCancel, onAnswer}: WordsDrawerProps) {
|
||||
const [selectedWord, setSelectedWord] = useState<string | undefined>(previouslySelectedWord);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={clsx(
|
||||
"w-full h-full absolute top-0 left-0 bg-gradient-to-t from-mti-black to-transparent z-10",
|
||||
isOpen ? "visible opacity-10" : "invisible opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute w-full bg-white px-7 py-8 bottom-0 left-0 shadow-2xl rounded-2xl z-20 flex flex-col gap-8 transition-opacity duration-300 ease-in-out",
|
||||
isOpen ? "visible opacity-100" : "invisible opacity-0",
|
||||
)}>
|
||||
<div className="w-full flex gap-2">
|
||||
<div className="rounded-full w-6 h-6 flex items-center justify-center text-white bg-mti-purple-light">{blankId}</div>
|
||||
<span> Choose the correct word:</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-6 gap-6" key="word-array">
|
||||
{words.map(({word, isDisabled}) => (
|
||||
<button
|
||||
key={`${word}_${blankId}`}
|
||||
onClick={() => setSelectedWord((prev) => (prev === word ? undefined : word))}
|
||||
className={clsx(
|
||||
"rounded-full py-3 text-center transition duration-300 ease-in-out",
|
||||
selectedWord === word ? "text-white bg-mti-purple-light" : "bg-mti-purple-ultralight",
|
||||
!isDisabled && "hover:text-white hover:bg-mti-purple",
|
||||
"disabled:cursor-not-allowed disabled:text-mti-gray-dim",
|
||||
)}
|
||||
disabled={isDisabled}>
|
||||
{word}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-between w-full">
|
||||
<Button color="purple" variant="outline" className="max-w-[200px] w-full" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button color="purple" className="max-w-[200px] w-full" onClick={() => onAnswer(selectedWord!)} disabled={!selectedWord}>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FillBlanks({
|
||||
id,
|
||||
allowRepetition,
|
||||
type,
|
||||
prompt,
|
||||
solutions,
|
||||
text,
|
||||
words,
|
||||
userSolutions,
|
||||
onNext,
|
||||
onBack,
|
||||
}: FillBlanksExercise & CommonProps) {
|
||||
const [answers, setAnswers] = useState<{id: string; solution: string}[]>(userSolutions);
|
||||
|
||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hasExamEnded]);
|
||||
|
||||
const calculateScore = () => {
|
||||
const total = text.match(/({{\d+}})/g)?.length || 0;
|
||||
const correct = answers.filter((x) => {
|
||||
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution.toLowerCase();
|
||||
if (!solution) return false;
|
||||
|
||||
const option = words.find((w) =>
|
||||
typeof w === "string" ? w.toLowerCase() === x.solution.toLowerCase() : w.letter.toLowerCase() === x.solution.toLowerCase(),
|
||||
);
|
||||
if (!option) return false;
|
||||
|
||||
return solution === (typeof option === "string" ? option.toLowerCase() : option.word.toLowerCase());
|
||||
}).length;
|
||||
const missing = total - answers.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
||||
|
||||
return {total, correct, missing};
|
||||
};
|
||||
|
||||
const renderLines = (line: string) => {
|
||||
return (
|
||||
<div className="text-base leading-5">
|
||||
{reactStringReplace(line, /({{\d+}})/g, (match) => {
|
||||
const id = match.replaceAll(/[\{\}]/g, "");
|
||||
const userSolution = answers.find((x) => x.id === id);
|
||||
|
||||
return (
|
||||
<input
|
||||
className={clsx(
|
||||
"rounded-full hover:text-white focus:ring-0 focus:outline-none focus:!text-white focus:bg-mti-purple transition duration-300 ease-in-out my-1 px-5 py-2 text-center",
|
||||
!userSolution && "text-center text-mti-purple-light bg-mti-purple-ultralight",
|
||||
userSolution && "px-5 py-2 text-center text-mti-purple-dark bg-mti-purple-ultralight",
|
||||
)}
|
||||
onChange={(e) => setAnswers((prev) => [...prev.filter((x) => x.id !== id), {id, solution: e.target.value}])}
|
||||
value={userSolution?.solution}></input>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
||||
<span className="text-sm w-full leading-6">
|
||||
{prompt.split("\\n").map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
{line}
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
</span>
|
||||
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">
|
||||
{text.split("\\n").map((line, index) => (
|
||||
<p key={index}>
|
||||
{renderLines(line)}
|
||||
<br />
|
||||
</p>
|
||||
))}
|
||||
</span>
|
||||
<div className="bg-mti-gray-smoke rounded-xl px-5 py-6 flex flex-col gap-4">
|
||||
<span className="font-medium text-mti-purple-dark">Options</span>
|
||||
<div className="flex gap-4 flex-wrap">
|
||||
{words.map((v) => {
|
||||
const text = typeof v === "string" ? v : `${v.letter} - ${v.word}`;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
"border border-mti-purple-light rounded-full px-3 py-0.5 transition ease-in-out duration-300",
|
||||
!!answers.find((x) => x.solution.toLowerCase() === (typeof v === "string" ? v : v.letter).toLowerCase()) &&
|
||||
"bg-mti-purple-dark text-white",
|
||||
)}
|
||||
key={text}>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
||||
<Button
|
||||
color="purple"
|
||||
variant="outline"
|
||||
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type})}
|
||||
className="max-w-[200px] w-full">
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="purple"
|
||||
onClick={() => onNext({exercise: id, solutions: answers, score: calculateScore(), type})}
|
||||
className="max-w-[200px] self-end w-full">
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
64
src/components/Exercises/FillBlanks/WordsDrawer.tsx
Normal file
64
src/components/Exercises/FillBlanks/WordsDrawer.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import Button from "@/components/Low/Button";
|
||||
import clsx from "clsx";
|
||||
import { useState } from "react";
|
||||
|
||||
interface WordsDrawerProps {
|
||||
words: {word: string; isDisabled: boolean}[];
|
||||
isOpen: boolean;
|
||||
blankId?: string;
|
||||
previouslySelectedWord?: string;
|
||||
onCancel: () => void;
|
||||
onAnswer: (answer: string) => void;
|
||||
}
|
||||
|
||||
|
||||
const WordsDrawer: React.FC<WordsDrawerProps> = ({words, isOpen, blankId, previouslySelectedWord, onCancel, onAnswer}) => {
|
||||
const [selectedWord, setSelectedWord] = useState<string | undefined>(previouslySelectedWord);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={clsx(
|
||||
"w-full h-full absolute top-0 left-0 bg-gradient-to-t from-mti-black to-transparent z-10",
|
||||
isOpen ? "visible opacity-10" : "invisible opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute w-full bg-white px-7 py-8 bottom-0 left-0 shadow-2xl rounded-2xl z-20 flex flex-col gap-8 transition-opacity duration-300 ease-in-out",
|
||||
isOpen ? "visible opacity-100" : "invisible opacity-0",
|
||||
)}>
|
||||
<div className="w-full flex gap-2">
|
||||
<div className="rounded-full w-6 h-6 flex items-center justify-center text-white bg-mti-purple-light">{blankId}</div>
|
||||
<span> Choose the correct word:</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-6 gap-6" key="word-array">
|
||||
{words.map(({word, isDisabled}) => (
|
||||
<button
|
||||
key={`${word}_${blankId}`}
|
||||
onClick={() => setSelectedWord((prev) => (prev === word ? undefined : word))}
|
||||
className={clsx(
|
||||
"rounded-full py-3 text-center transition duration-300 ease-in-out",
|
||||
selectedWord === word ? "text-white bg-mti-purple-light" : "bg-mti-purple-ultralight",
|
||||
!isDisabled && "hover:text-white hover:bg-mti-purple",
|
||||
"disabled:cursor-not-allowed disabled:text-mti-gray-dim",
|
||||
)}
|
||||
disabled={isDisabled}>
|
||||
{word}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-between w-full">
|
||||
<Button color="purple" variant="outline" className="max-w-[200px] w-full" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button color="purple" className="max-w-[200px] w-full" onClick={() => onAnswer(selectedWord!)} disabled={!selectedWord}>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default WordsDrawer;
|
||||
209
src/components/Exercises/FillBlanks/index.tsx
Normal file
209
src/components/Exercises/FillBlanks/index.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import { FillBlanksExercise, FillBlanksMCOption } from "@/interfaces/exam";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import clsx from "clsx";
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
import { CommonProps } from "..";
|
||||
import Button from "../../Low/Button";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
|
||||
const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
id,
|
||||
type,
|
||||
prompt,
|
||||
solutions,
|
||||
text,
|
||||
words,
|
||||
userSolutions,
|
||||
variant,
|
||||
onNext,
|
||||
onBack,
|
||||
}) => {
|
||||
const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions);
|
||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||
|
||||
const [currentMCSelection, setCurrentMCSelection] = useState<{ id: string, selection: FillBlanksMCOption }>();
|
||||
|
||||
const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
|
||||
return Array.isArray(words) && words.every(
|
||||
word => word && typeof word === 'object' && 'id' in word && 'options' in word
|
||||
);
|
||||
}
|
||||
|
||||
const excludeWordMCType = (x: any) => {
|
||||
return typeof x === "string" ? x : x as { letter: string; word: string };
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hasExamEnded]);
|
||||
|
||||
const calculateScore = () => {
|
||||
const total = text.match(/({{\d+}})/g)?.length || 0;
|
||||
const correct = userSolutions.filter((x) => {
|
||||
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution;
|
||||
if (!solution) return false;
|
||||
|
||||
const option = words.find((w) => {
|
||||
if (typeof w === "string") {
|
||||
return w.toLowerCase() === x.solution.toLowerCase();
|
||||
} else if ('letter' in w) {
|
||||
return w.word.toLowerCase() === x.solution.toLowerCase();
|
||||
} else {
|
||||
return w.id === x.id;
|
||||
}
|
||||
});
|
||||
if (!option) return false;
|
||||
|
||||
if (typeof option === "string") {
|
||||
return solution.toLowerCase() === option.toLowerCase();
|
||||
} else if ('letter' in option) {
|
||||
return solution.toLowerCase() === option.word.toLowerCase();
|
||||
} else if ('options' in option) {
|
||||
return solution.toLowerCase() === (option.options[x.solution as keyof typeof option.options] || '').toLowerCase();
|
||||
}
|
||||
return false;
|
||||
}).length;
|
||||
|
||||
const missing = total - userSolutions.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
||||
|
||||
return { total, correct, missing };
|
||||
};
|
||||
|
||||
const renderLines = (line: string) => {
|
||||
return (
|
||||
<div className="text-base leading-5">
|
||||
{reactStringReplace(line, /({{\d+}})/g, (match) => {
|
||||
const id = match.replaceAll(/[\{\}]/g, "");
|
||||
const userSolution = answers.find((x) => x.id === id);
|
||||
const styles = clsx(
|
||||
"rounded-full hover:text-white focus:ring-0 focus:outline-none focus:!text-white focus:bg-mti-purple transition duration-300 ease-in-out my-1 px-5 py-2 text-center",
|
||||
!userSolution && "text-center text-mti-purple-light bg-mti-purple-ultralight",
|
||||
userSolution && "text-center text-mti-purple-dark bg-mti-purple-ultralight",
|
||||
)
|
||||
return (
|
||||
variant === "mc" ? (
|
||||
<button
|
||||
className={styles}
|
||||
onClick={() => {
|
||||
setCurrentMCSelection(
|
||||
{
|
||||
id: id,
|
||||
selection: words.find((x) => {
|
||||
if (typeof x !== "string" && 'id' in x) {
|
||||
return (x as FillBlanksMCOption).id.toString() == id.toString();
|
||||
}
|
||||
return false;
|
||||
}) as FillBlanksMCOption
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
{userSolution?.solution === undefined ? <span className="text-transparent select-none">placeholder</span> : <span> {userSolution.solution} </span>}
|
||||
</button>
|
||||
) : (
|
||||
<input
|
||||
className={styles}
|
||||
onChange={(e) => setAnswers((prev) => [...prev.filter((x) => x.id !== id), { id, solution: e.target.value }])}
|
||||
value={userSolution?.solution} />
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const onSelection = (id: string, value: string) => {
|
||||
setAnswers((prev) => [...prev.filter((x) => x.id !== id), { id: id, solution: value }]);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
||||
<span className="text-sm w-full leading-6">
|
||||
{prompt.split("\\n").map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
{line}
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
</span>
|
||||
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">
|
||||
{text.split("\\n").map((line, index) => (
|
||||
<p key={index} className={clsx(variant === "mc" && "whitespace-pre-wrap")}>
|
||||
{renderLines(line)}
|
||||
<br />
|
||||
</p>
|
||||
))}
|
||||
</span>
|
||||
{variant === "mc" && typeCheckWordsMC(words) ? (
|
||||
<>
|
||||
{currentMCSelection && (
|
||||
<div className="bg-mti-gray-smoke rounded-xl px-5 py-6 flex flex-col gap-4">
|
||||
<span className="font-medium text-mti-purple-dark">Options</span>
|
||||
<div className="flex gap-4 flex-wrap">
|
||||
{currentMCSelection.selection?.options && Object.entries(currentMCSelection.selection.options).map(([key, value]) => {
|
||||
return <button
|
||||
className={clsx(
|
||||
"border border-mti-purple-light rounded-full px-3 py-0.5 transition ease-in-out duration-300",
|
||||
!!answers.find((x) => x.solution.toLocaleLowerCase() === value.toLocaleLowerCase() && x.id === currentMCSelection.id ) &&
|
||||
"bg-mti-purple-dark text-white",
|
||||
)}
|
||||
key={v4()}
|
||||
onClick={() => onSelection(currentMCSelection.id, value)}
|
||||
>
|
||||
{value}
|
||||
</button>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="bg-mti-gray-smoke rounded-xl px-5 py-6 flex flex-col gap-4">
|
||||
<span className="font-medium text-mti-purple-dark">Options</span>
|
||||
<div className="flex gap-4 flex-wrap">
|
||||
{words.map((v) => {
|
||||
v = excludeWordMCType(v);
|
||||
const text = typeof v === "string" ? v : `${v.letter} - ${v.word}`;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
"border border-mti-purple-light rounded-full px-3 py-0.5 transition ease-in-out duration-300",
|
||||
!!answers.find((x) => x.solution.toLowerCase() === (typeof v === "string" ? v : ("letter" in v ? v.letter : "")).toLowerCase()) &&
|
||||
"bg-mti-purple-dark text-white",
|
||||
)}
|
||||
key={v4()}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div >
|
||||
)}
|
||||
</div>
|
||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
||||
<Button
|
||||
color="purple"
|
||||
variant="outline"
|
||||
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type })}
|
||||
className="max-w-[200px] w-full">
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="purple"
|
||||
onClick={() => onNext({ exercise: id, solutions: answers, score: calculateScore(), type })}
|
||||
className="max-w-[200px] self-end w-full">
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default FillBlanks;
|
||||
@@ -14,22 +14,32 @@ function Question({
|
||||
options,
|
||||
userSolution,
|
||||
onSelectOption,
|
||||
}: MultipleChoiceQuestion & {userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean}) {
|
||||
setContextHighlight
|
||||
}: MultipleChoiceQuestion & {
|
||||
userSolution: string | undefined;
|
||||
onSelectOption?: (option: string) => void;
|
||||
showSolution?: boolean,
|
||||
setContextHighlight?: React.Dispatch<React.SetStateAction<string[]>>
|
||||
}) {
|
||||
|
||||
/*
|
||||
const renderPrompt = (prompt: string) => {
|
||||
return reactStringReplace(prompt, /((<u>)[\w\s']+(<\/u>))/g, (match) => {
|
||||
const word = match.replaceAll("<u>", "").replaceAll("</u>", "");
|
||||
return word.length > 0 ? <u>{word}</u> : null;
|
||||
});
|
||||
};
|
||||
*/
|
||||
|
||||
return (
|
||||
// {renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")}
|
||||
<div className="flex flex-col gap-10">
|
||||
{isNaN(Number(id)) ? (
|
||||
<span>{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")} </span>
|
||||
<span dangerouslySetInnerHTML={{__html: prompt}} />
|
||||
) : (
|
||||
<span className="">
|
||||
<>
|
||||
{id} - <span>{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")} </span>
|
||||
{id} - <span dangerouslySetInnerHTML={{__html: prompt}} />
|
||||
</>
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -63,7 +63,7 @@ const FillBlanksEdit = (props: Props) => {
|
||||
label={`Word ${index + 1}`}
|
||||
name="word"
|
||||
required
|
||||
value={typeof word === "string" ? word : word.word}
|
||||
value={typeof word === "string" ? word : ("word" in word ? word.word : "")}
|
||||
onChange={(value) =>
|
||||
updateExercise({
|
||||
words: exercise.words.map((sol, idx) =>
|
||||
|
||||
39
src/components/HighlightContent.tsx
Normal file
39
src/components/HighlightContent.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
const HighlightContent: React.FC<{
|
||||
html: string;
|
||||
highlightPhrases: string[],
|
||||
firstOccurence?: boolean
|
||||
}> = ({
|
||||
html,
|
||||
highlightPhrases,
|
||||
firstOccurence = false
|
||||
}) => {
|
||||
|
||||
const createHighlightedContent = useCallback(() => {
|
||||
if (highlightPhrases.length === 0) {
|
||||
return { __html: html };
|
||||
}
|
||||
|
||||
const escapeRegExp = (string: string) => {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
};
|
||||
|
||||
const regex = new RegExp(`(${highlightPhrases.map(escapeRegExp).join('|')})`, 'i');
|
||||
const globalRegex = new RegExp(`(${highlightPhrases.map(escapeRegExp).join('|')})`, 'gi');
|
||||
|
||||
let highlightedHtml = html;
|
||||
|
||||
if (firstOccurence) {
|
||||
highlightedHtml = html.replace(regex, (match) => `<span style="background-color: yellow;">${match}</span>`);
|
||||
} else {
|
||||
highlightedHtml = html.replace(globalRegex, (match) => `<span style="background-color: yellow;">${match}</span>`);
|
||||
}
|
||||
|
||||
return { __html: highlightedHtml };
|
||||
}, [html, highlightPhrases, firstOccurence]);
|
||||
|
||||
return <div dangerouslySetInnerHTML={createHighlightedContent()} />;
|
||||
};
|
||||
|
||||
export default HighlightContent;
|
||||
@@ -1,8 +1,8 @@
|
||||
import {FillBlanksExercise} from "@/interfaces/exam";
|
||||
import { FillBlanksExercise, FillBlanksMCOption } from "@/interfaces/exam";
|
||||
import clsx from "clsx";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
import {CommonProps} from ".";
|
||||
import {Fragment} from "react";
|
||||
import { CommonProps } from ".";
|
||||
import { Fragment } from "react";
|
||||
import Button from "../Low/Button";
|
||||
|
||||
export default function FillBlanksSolutions({
|
||||
@@ -19,21 +19,42 @@ export default function FillBlanksSolutions({
|
||||
const calculateScore = () => {
|
||||
const total = text.match(/({{\d+}})/g)?.length || 0;
|
||||
const correct = userSolutions.filter((x) => {
|
||||
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution.toLowerCase();
|
||||
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution;
|
||||
if (!solution) return false;
|
||||
|
||||
const option = words.find((w) =>
|
||||
typeof w === "string" ? w.toLowerCase() === x.solution.toLowerCase() : w.letter.toLowerCase() === x.solution.toLowerCase(),
|
||||
);
|
||||
const option = words.find((w) => {
|
||||
if (typeof w === "string") {
|
||||
return w.toLowerCase() === x.solution.toLowerCase();
|
||||
} else if ('letter' in w) {
|
||||
return w.word.toLowerCase() === x.solution.toLowerCase();
|
||||
} else {
|
||||
return w.id === x.id;
|
||||
}
|
||||
});
|
||||
if (!option) return false;
|
||||
|
||||
return solution === (typeof option === "string" ? option.toLowerCase() : option.word.toLowerCase());
|
||||
if (typeof option === "string") {
|
||||
return solution.toLowerCase() === option.toLowerCase();
|
||||
} else if ('letter' in option) {
|
||||
return solution.toLowerCase() === option.word.toLowerCase();
|
||||
} else if ('options' in option) {
|
||||
return option.options[solution as keyof typeof option.options] == x.solution;
|
||||
}
|
||||
return false;
|
||||
}).length;
|
||||
|
||||
const missing = total - userSolutions.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
||||
|
||||
return {total, correct, missing};
|
||||
return { total, correct, missing };
|
||||
};
|
||||
|
||||
|
||||
const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
|
||||
return Array.isArray(words) && words.every(
|
||||
word => word && typeof word === 'object' && 'id' in word && 'options' in word
|
||||
);
|
||||
}
|
||||
|
||||
const renderLines = (line: string) => {
|
||||
return (
|
||||
<span>
|
||||
@@ -56,23 +77,53 @@ export default function FillBlanksSolutions({
|
||||
const userSolutionWord = words.find((w) =>
|
||||
typeof w === "string"
|
||||
? w.toLowerCase() === userSolution.solution.toLowerCase()
|
||||
: w.letter.toLowerCase() === userSolution.solution.toLowerCase(),
|
||||
: 'letter' in w
|
||||
? w.letter.toLowerCase() === userSolution.solution.toLowerCase()
|
||||
: 'options' in w
|
||||
? w.id === userSolution.id
|
||||
: false
|
||||
);
|
||||
const userSolutionText = typeof userSolutionWord === "string" ? userSolutionWord : userSolutionWord?.word;
|
||||
|
||||
if (userSolutionText === solution.solution) {
|
||||
const userSolutionText =
|
||||
typeof userSolutionWord === "string"
|
||||
? userSolutionWord
|
||||
: userSolutionWord && 'letter' in userSolutionWord
|
||||
? userSolutionWord.word
|
||||
: userSolutionWord && 'options' in userSolutionWord
|
||||
? userSolution.solution
|
||||
: userSolution.solution;
|
||||
|
||||
let correct;
|
||||
let solutionText;
|
||||
if (typeCheckWordsMC(words)) {
|
||||
const options = words.find((x) => x.id === id);
|
||||
if (options) {
|
||||
const correctKey = Object.keys(options.options).find(key =>
|
||||
key.toLowerCase() === solution.solution.toLowerCase()
|
||||
);
|
||||
correct = userSolution.solution == options.options[correctKey as keyof typeof options.options];
|
||||
solutionText = options.options[correctKey as keyof typeof options.options] || solution.solution;
|
||||
} else {
|
||||
correct = false;
|
||||
solutionText = solution.solution;
|
||||
}
|
||||
|
||||
} else {
|
||||
correct = userSolutionText === solution.solution;
|
||||
solutionText = solution.solution;
|
||||
}
|
||||
|
||||
if (correct) {
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
"rounded-full hover:text-white hover:bg-mti-purple transition duration-300 ease-in-out my-1",
|
||||
userSolution && "px-5 py-2 text-center text-white bg-mti-purple-light",
|
||||
)}>
|
||||
{solution.solution}
|
||||
{solutionText}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (userSolutionText !== solution.solution) {
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
@@ -88,7 +139,7 @@ export default function FillBlanksSolutions({
|
||||
"rounded-full hover:text-white hover:bg-mti-purple transition duration-300 ease-in-out my-1",
|
||||
userSolution && "px-5 py-2 text-center text-white bg-mti-purple-light",
|
||||
)}>
|
||||
{solution.solution}
|
||||
{solutionText}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
@@ -138,14 +189,14 @@ export default function FillBlanksSolutions({
|
||||
<Button
|
||||
color="purple"
|
||||
variant="outline"
|
||||
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||
onClick={() => onBack({ exercise: id, solutions: userSolutions, score: calculateScore(), type })}
|
||||
className="max-w-[200px] w-full">
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="purple"
|
||||
onClick={() => onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||
onClick={() => onNext({ exercise: id, solutions: userSolutions, score: calculateScore(), type })}
|
||||
className="max-w-[200px] self-end w-full">
|
||||
Next
|
||||
</Button>
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
const HighlightedContent: React.FC<{ html: string; highlightPhrases: string[] }> = ({ html, highlightPhrases }) => {
|
||||
|
||||
const createHighlightedContent = useCallback(() => {
|
||||
if (highlightPhrases.length === 0) {
|
||||
return { __html: html };
|
||||
}
|
||||
|
||||
const escapeRegExp = (string: string) => {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
};
|
||||
|
||||
const regex = new RegExp(`(${highlightPhrases.map(escapeRegExp).join('|')})`, 'gi');
|
||||
const highlightedHtml = html.replace(regex, (match) => `<span style="background-color: yellow;">${match}</span>`);
|
||||
|
||||
return { __html: highlightedHtml };
|
||||
}, [html, highlightPhrases]);
|
||||
|
||||
return <div dangerouslySetInnerHTML={createHighlightedContent()} />;
|
||||
};
|
||||
|
||||
export default HighlightedContent;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { animated } from '@react-spring/web';
|
||||
import { FaRegCirclePlay, FaRegCircleStop } from "react-icons/fa6";
|
||||
import HighlightedContent from './AnimatedHighlight';
|
||||
import HighlightContent from '../HighlightContent';
|
||||
import { ITrainingTip, SegmentRef, TimelineEvent } from './TrainingInterfaces';
|
||||
|
||||
|
||||
@@ -267,7 +267,7 @@ const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
|
||||
<div className='flex-1 bg-white p-6 rounded-lg shadow'>
|
||||
{/*<h2 className="text-xl font-bold mb-4">Question</h2>*/}
|
||||
<div className="mb-4" dangerouslySetInnerHTML={{ __html: tip.exercise.question }} />
|
||||
<HighlightedContent html={tip.exercise.highlightable} highlightPhrases={highlightedPhrases} />
|
||||
<HighlightContent html={tip.exercise.highlightable} highlightPhrases={highlightedPhrases} />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<div className='bg-gray-50 rounded-lg shadow'>
|
||||
|
||||
@@ -1,55 +1,176 @@
|
||||
import BlankQuestionsModal from "@/components/BlankQuestionsModal";
|
||||
import {renderExercise} from "@/components/Exercises";
|
||||
import { renderExercise } from "@/components/Exercises";
|
||||
import HighlightContent from "@/components/HighlightContent";
|
||||
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 { renderSolution } from "@/components/Solutions";
|
||||
import { infoButtonStyle } from "@/constants/buttonStyles";
|
||||
import { LevelExam, LevelPart, 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 { 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, useEffect, useState} from "react";
|
||||
import {BsChevronDown, BsChevronUp} from "react-icons/bs";
|
||||
import {toast} from "react-toastify";
|
||||
import { Fragment, use, useEffect, useRef, useState } from "react";
|
||||
import { BsChevronDown, BsChevronUp } from "react-icons/bs";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
interface Props {
|
||||
exam: LevelExam;
|
||||
showSolutions?: boolean;
|
||||
onFinish: (userSolutions: UserSolution[]) => void;
|
||||
editing?: boolean;
|
||||
}
|
||||
|
||||
function TextComponent({part}: {part: LevelPart}) {
|
||||
function TextComponent({
|
||||
part, highlightPhrases, contextWord, setContextWordLine
|
||||
}: {
|
||||
part: LevelPart, highlightPhrases: string[], contextWord: string | undefined, setContextWordLine: React.Dispatch<React.SetStateAction<number | undefined>>
|
||||
}) {
|
||||
const textRef = useRef<HTMLDivElement>(null);
|
||||
const [lineNumbers, setLineNumbers] = useState<number[]>([]);
|
||||
const [lineHeight, setLineHeight] = useState<number>(0);
|
||||
part.showContextLines = true;
|
||||
|
||||
const calculateLineNumbers = () => {
|
||||
if (textRef.current) {
|
||||
const computedStyle = window.getComputedStyle(textRef.current);
|
||||
const lineHeightValue = parseFloat(computedStyle.lineHeight);
|
||||
const containerWidth = textRef.current.clientWidth;
|
||||
setLineHeight(lineHeightValue);
|
||||
|
||||
const offscreenElement = document.createElement('div');
|
||||
offscreenElement.style.position = 'absolute';
|
||||
offscreenElement.style.top = '-9999px';
|
||||
offscreenElement.style.left = '-9999px';
|
||||
offscreenElement.style.whiteSpace = 'pre-wrap';
|
||||
offscreenElement.style.width = `${containerWidth}px`;
|
||||
offscreenElement.style.font = computedStyle.font;
|
||||
offscreenElement.style.lineHeight = computedStyle.lineHeight;
|
||||
offscreenElement.style.wordWrap = 'break-word';
|
||||
offscreenElement.style.textAlign = computedStyle.textAlign as CanvasTextAlign;
|
||||
|
||||
const textContent = textRef.current.textContent || '';
|
||||
textContent.split(/(\s+)/).forEach((word: string) => {
|
||||
const span = document.createElement('span');
|
||||
span.textContent = word;
|
||||
offscreenElement.appendChild(span);
|
||||
});
|
||||
|
||||
document.body.appendChild(offscreenElement);
|
||||
|
||||
const lines: string[][] = [[]];
|
||||
let currentLine = 1;
|
||||
let currentLineTop: number | undefined;
|
||||
let contextWordLine: number | null = null;
|
||||
|
||||
const firstChild = offscreenElement.firstChild as HTMLElement;
|
||||
if (firstChild) {
|
||||
currentLineTop = firstChild.getBoundingClientRect().top;
|
||||
}
|
||||
|
||||
const spans = offscreenElement.querySelectorAll<HTMLSpanElement>('span');
|
||||
|
||||
spans.forEach(span => {
|
||||
const rect = span.getBoundingClientRect();
|
||||
const top = rect.top;
|
||||
|
||||
if (currentLineTop !== undefined && top > currentLineTop) {
|
||||
currentLine++;
|
||||
currentLineTop = top;
|
||||
lines.push([]);
|
||||
}
|
||||
|
||||
lines[lines.length - 1].push(span.textContent?.trim() || '');
|
||||
|
||||
|
||||
if (contextWord && contextWordLine === null && span.textContent?.includes(contextWord)) {
|
||||
contextWordLine = currentLine;
|
||||
}
|
||||
});
|
||||
|
||||
setLineNumbers(lines.map((_, index) => index + 1));
|
||||
if (contextWordLine) {
|
||||
setContextWordLine(contextWordLine);
|
||||
}
|
||||
|
||||
document.body.removeChild(offscreenElement);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
calculateLineNumbers();
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
calculateLineNumbers();
|
||||
});
|
||||
|
||||
if (textRef.current) {
|
||||
resizeObserver.observe(textRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (textRef.current) {
|
||||
resizeObserver.unobserve(textRef.current);
|
||||
}
|
||||
};
|
||||
}, [part.context, part.showContextLines, contextWord]);
|
||||
|
||||
if (typeof part.showContextLines === "undefined") {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
|
||||
{!!part.context &&
|
||||
part.context
|
||||
.split(/\n|(\\n)/g)
|
||||
.filter((x) => x && x.length > 0 && x !== "\\n")
|
||||
.map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
<p key={index}>{line}</p>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
|
||||
{!!part.context &&
|
||||
part.context
|
||||
.split(/\n|(\\n)/g)
|
||||
.filter((x) => x && x.length > 0 && x !== "\\n")
|
||||
.map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
<p key={index}>{line}</p>
|
||||
</Fragment>
|
||||
<div className="flex mt-2">
|
||||
<div className="flex-shrink-0 w-8 pr-2">
|
||||
{lineNumbers.map(num => (
|
||||
<div key={num} className="text-gray-400 flex justify-end" style={{ lineHeight: `${lineHeight}px` }}>
|
||||
{num}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div ref={textRef} className="h-fit whitespace-pre-wrap ml-2">
|
||||
<HighlightContent html={part.context!} highlightPhrases={highlightPhrases} firstOccurence={true} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Level({exam, showSolutions = false, onFinish}: Props) {
|
||||
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{id: string; amount: number}[]>([]);
|
||||
|
||||
|
||||
export default function Level({ exam, showSolutions = false, onFinish, editing = false }: Props) {
|
||||
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]);
|
||||
const [showBlankModal, setShowBlankModal] = useState(false);
|
||||
|
||||
const {userSolutions, setUserSolutions} = useExamStore((state) => state);
|
||||
const {hasExamEnded, setHasExamEnded} = useExamStore((state) => state);
|
||||
const {partIndex, setPartIndex} = useExamStore((state) => state);
|
||||
const {exerciseIndex, setExerciseIndex} = useExamStore((state) => state);
|
||||
const { userSolutions, setUserSolutions } = useExamStore((state) => state);
|
||||
const { hasExamEnded, setHasExamEnded } = useExamStore((state) => state);
|
||||
const { partIndex, setPartIndex } = useExamStore((state) => state);
|
||||
const { exerciseIndex, setExerciseIndex } = useExamStore((state) => state);
|
||||
const [storeQuestionIndex, setStoreQuestionIndex] = useExamStore((state) => [state.questionIndex, state.setQuestionIndex]);
|
||||
|
||||
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
|
||||
|
||||
const [highlightPhrases, setContextHighlight] = useState<string[]>([]);
|
||||
const [contextWord, setContextWord] = useState<string | undefined>(undefined);
|
||||
const [contextWordLine, setContextWordLine] = useState<number | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasExamEnded && exerciseIndex === -1) {
|
||||
setExerciseIndex(exerciseIndex + 1);
|
||||
@@ -68,12 +189,12 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
|
||||
const nextExercise = (solution?: UserSolution) => {
|
||||
scrollToTop();
|
||||
if (solution) {
|
||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "level", exam: exam.id}]);
|
||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]);
|
||||
}
|
||||
|
||||
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 !== exercise!.id), { id: exercise!.id, amount: storeQuestionIndex }]);
|
||||
}
|
||||
setStoreQuestionIndex(0);
|
||||
|
||||
@@ -94,6 +215,7 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
|
||||
(x) => x === 0,
|
||||
) &&
|
||||
!showSolutions &&
|
||||
!editing &&
|
||||
!hasExamEnded
|
||||
) {
|
||||
setShowBlankModal(true);
|
||||
@@ -103,7 +225,7 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
|
||||
setHasExamEnded(false);
|
||||
|
||||
if (solution) {
|
||||
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "level", exam: exam.id}]);
|
||||
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]);
|
||||
} else {
|
||||
onFinish(userSolutions);
|
||||
}
|
||||
@@ -112,12 +234,12 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
|
||||
const previousExercise = (solution?: UserSolution) => {
|
||||
scrollToTop();
|
||||
if (solution) {
|
||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "level", exam: exam.id}]);
|
||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]);
|
||||
}
|
||||
|
||||
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 !== exercise!.id), { id: exercise!.id, amount: storeQuestionIndex }]);
|
||||
}
|
||||
setStoreQuestionIndex(0);
|
||||
|
||||
@@ -125,7 +247,10 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
|
||||
};
|
||||
|
||||
const getExercise = () => {
|
||||
const exercise = exam.parts[partIndex].exercises[exerciseIndex];
|
||||
if (exerciseIndex === -1) {
|
||||
return undefined;
|
||||
}
|
||||
const exercise = exam.parts[partIndex]?.exercises[exerciseIndex];
|
||||
return {
|
||||
...exercise,
|
||||
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
|
||||
@@ -157,11 +282,45 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
|
||||
</h4>
|
||||
<span className="text-base">You will be allowed to read the text while doing the exercises</span>
|
||||
</div>
|
||||
<TextComponent part={exam.parts[partIndex]} />
|
||||
<TextComponent
|
||||
part={exam.parts[partIndex]}
|
||||
highlightPhrases={highlightPhrases}
|
||||
contextWord={contextWord}
|
||||
setContextWordLine={setContextWordLine}
|
||||
/>
|
||||
</>
|
||||
</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">
|
||||
@@ -171,7 +330,7 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
|
||||
exerciseIndex={calculateExerciseIndex()}
|
||||
module="level"
|
||||
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
|
||||
disableTimer={showSolutions}
|
||||
disableTimer={showSolutions || editing}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -184,12 +343,13 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
|
||||
partIndex > -1 &&
|
||||
exerciseIndex < exam.parts[partIndex].exercises.length &&
|
||||
!showSolutions &&
|
||||
renderExercise(getExercise(), exam.id, nextExercise, previousExercise)}
|
||||
!editing &&
|
||||
renderExercise(exercise!, exam.id, nextExercise, previousExercise)}
|
||||
|
||||
{exerciseIndex > -1 &&
|
||||
partIndex > -1 &&
|
||||
exerciseIndex < exam.parts[partIndex].exercises.length &&
|
||||
showSolutions &&
|
||||
(showSolutions || editing) &&
|
||||
renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise)}
|
||||
</div>
|
||||
{exerciseIndex === -1 && partIndex > 0 && (
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface LevelExam extends ExamBase {
|
||||
|
||||
export interface LevelPart {
|
||||
context?: string;
|
||||
showContextLines?: boolean;
|
||||
exercises: Exercise[];
|
||||
}
|
||||
|
||||
@@ -187,10 +188,10 @@ export interface InteractiveSpeakingExercise {
|
||||
first_title?: string;
|
||||
second_title?: string;
|
||||
text: string;
|
||||
prompts: {text: string; video_url: string}[];
|
||||
prompts: { text: string; video_url: string }[];
|
||||
userSolutions: {
|
||||
id: string;
|
||||
solution: {questionIndex: number; question: string; answer: string}[];
|
||||
solution: { questionIndex: number; question: string; answer: string }[];
|
||||
evaluation?: InteractiveSpeakingEvaluation;
|
||||
}[];
|
||||
topic?: string;
|
||||
@@ -199,13 +200,23 @@ export interface InteractiveSpeakingExercise {
|
||||
variant?: "initial" | "final";
|
||||
}
|
||||
|
||||
export interface FillBlanksMCOption {
|
||||
id: string;
|
||||
options: {
|
||||
A: string;
|
||||
B: string;
|
||||
C: string;
|
||||
D: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface FillBlanksExercise {
|
||||
prompt: string; // *EXAMPLE: "Complete the summary below. Click a blank to select the corresponding word for it."
|
||||
type: "fillBlanks";
|
||||
id: string;
|
||||
words: (string | {letter: string; word: string})[]; // *EXAMPLE: ["preserve", "unaware"]
|
||||
words: (string | { letter: string; word: string } | FillBlanksMCOption)[]; // *EXAMPLE: ["preserve", "unaware"]
|
||||
text: string; // *EXAMPLE: "They tried to {{1}} burning"
|
||||
allowRepetition: boolean;
|
||||
allowRepetition?: boolean;
|
||||
solutions: {
|
||||
id: string; // *EXAMPLE: "1"
|
||||
solution: string; // *EXAMPLE: "preserve"
|
||||
@@ -214,6 +225,7 @@ export interface FillBlanksExercise {
|
||||
id: string; // *EXAMPLE: "1"
|
||||
solution: string; // *EXAMPLE: "preserve"
|
||||
}[];
|
||||
variant?: string;
|
||||
}
|
||||
|
||||
export interface TrueFalseExercise {
|
||||
@@ -274,6 +286,7 @@ export interface MultipleChoiceExercise {
|
||||
prompt: string; // *EXAMPLE: "Select the appropriate option."
|
||||
questions: MultipleChoiceQuestion[];
|
||||
userSolutions: {question: string; option: string}[];
|
||||
setContextHighlight?: React.Dispatch<React.SetStateAction<string[]>>
|
||||
}
|
||||
|
||||
export interface MultipleChoiceQuestion {
|
||||
|
||||
Reference in New Issue
Block a user