Merged in feature/level-file-upload (pull request #92)
ENCOA-182, ENCOA-185, ENCOA-177, ENCOA-168, ENCOA-186, ENCOA-176, ENCOA-189, ENCOA-167 Approved-by: Tiago Ribeiro
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const websiteUrl = process.env.NODE_ENV === 'production' ? "https://encoach.com" : "http://localhost:3000";
|
const websiteUrl = process.env.NODE_ENV === 'production' ? "https://encoach.com" : "http://localhost:3000";
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: false,
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default function DemographicInformationInput({user, mutateUser}: Props) {
|
|||||||
<label className="font-normal text-base text-mti-gray-dim">Country *</label>
|
<label className="font-normal text-base text-mti-gray-dim">Country *</label>
|
||||||
<CountrySelect value={country} onChange={setCountry} />
|
<CountrySelect value={country} onChange={setCountry} />
|
||||||
</div>
|
</div>
|
||||||
<Input type="tel" name="phone" label="Phone number" onChange={(e) => setPhone(e)} placeholder="Enter phone number" required />
|
<Input type="tel" name="phone" label="Phone number" onChange={(e) => setPhone(e)} value={phone} placeholder="Enter phone number" required />
|
||||||
</div>
|
</div>
|
||||||
{user.type === "student" && (
|
{user.type === "student" && (
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
84
src/components/Exercises/FillBlanks/MCDropdown.tsx
Normal file
84
src/components/Exercises/FillBlanks/MCDropdown.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import React, { useRef, useEffect, useState } from 'react';
|
||||||
|
import { animated, useSpring } from '@react-spring/web';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
interface MCDropdownProps {
|
||||||
|
id: string;
|
||||||
|
options: { [key: string]: string };
|
||||||
|
onSelect: (value: string) => void;
|
||||||
|
selectedValue?: string;
|
||||||
|
className?: string;
|
||||||
|
width: number;
|
||||||
|
isOpen: boolean;
|
||||||
|
onToggle: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MCDropdown: React.FC<MCDropdownProps> = ({
|
||||||
|
id,
|
||||||
|
options,
|
||||||
|
onSelect,
|
||||||
|
selectedValue,
|
||||||
|
className = "relative",
|
||||||
|
width,
|
||||||
|
isOpen,
|
||||||
|
onToggle,
|
||||||
|
}) => {
|
||||||
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [contentHeight, setContentHeight] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (contentRef.current) {
|
||||||
|
setContentHeight(contentRef.current.scrollHeight);
|
||||||
|
}
|
||||||
|
}, [options]);
|
||||||
|
|
||||||
|
const springProps = useSpring({
|
||||||
|
height: isOpen ? contentHeight : 0,
|
||||||
|
opacity: isOpen ? 1 : 0,
|
||||||
|
config: { tension: 300, friction: 30 }
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${className} inline-block`} style={{ width: `${width}px` }}>
|
||||||
|
<button
|
||||||
|
onClick={() => onToggle(id)}
|
||||||
|
className={
|
||||||
|
clsx("rounded-full hover:text-white transition duration-300 ease-in-out px-5 py-2 text-center w-full flex items-center justify-between",
|
||||||
|
selectedValue ? "bg-mti-purple text-white" : "bg-mti-purple-ultralight text-mti-purple-light"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="truncate p-1">{selectedValue || 'Select an option'}</span>
|
||||||
|
<svg
|
||||||
|
className={`w-4 h-4 transform transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<animated.div
|
||||||
|
style={{ ...springProps, width: `${width}px` }}
|
||||||
|
className="absolute z-10 mt-1 overflow-hidden bg-white rounded-md shadow-lg"
|
||||||
|
>
|
||||||
|
<div ref={contentRef}>
|
||||||
|
{Object.entries(options).sort((a, b) => a[0].localeCompare(b[0])).map(([key, value]) => (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
onClick={() => {
|
||||||
|
onSelect(value);
|
||||||
|
onToggle(id);
|
||||||
|
}}
|
||||||
|
className="p-4 hover:bg-mti-purple-ultralight cursor-pointer whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<span>{value}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</animated.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MCDropdown;
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import {FillBlanksExercise, FillBlanksMCOption} from "@/interfaces/exam";
|
import { FillBlanksExercise, FillBlanksMCOption } from "@/interfaces/exam";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {Fragment, useCallback, useEffect, useMemo, useState} from "react";
|
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import {CommonProps} from "..";
|
import { CommonProps } from "..";
|
||||||
import Button from "../../Low/Button";
|
import Button from "../../Low/Button";
|
||||||
import {v4} from "uuid";
|
import { v4 } from "uuid";
|
||||||
|
import MCDropdown from "./MCDropdown";
|
||||||
|
|
||||||
const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||||
id,
|
id,
|
||||||
@@ -19,24 +20,19 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
}) => {
|
}) => {
|
||||||
const {shuffles, exam, partIndex, questionIndex, exerciseIndex} = useExamStore((state) => state);
|
const { shuffles, exam, partIndex, questionIndex, exerciseIndex } = 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);
|
||||||
const setCurrentSolution = useExamStore((state) => state.setCurrentSolution);
|
const setCurrentSolution = useExamStore((state) => state.setCurrentSolution);
|
||||||
const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles;
|
const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles;
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
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) => {
|
const excludeWordMCType = (x: any) => {
|
||||||
return typeof x === "string" ? x : (x as {letter: string; word: string});
|
return typeof x === "string" ? x : (x as { letter: string; word: string });
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
|
if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [hasExamEnded]);
|
}, [hasExamEnded]);
|
||||||
|
|
||||||
@@ -45,6 +41,19 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
correctWords = (exam.parts[partIndex].exercises[exerciseIndex] as FillBlanksExercise).words;
|
correctWords = (exam.parts[partIndex].exercises[exerciseIndex] as FillBlanksExercise).words;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setOpenDropdownId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const calculateScore = () => {
|
const calculateScore = () => {
|
||||||
const total = text.match(/({{\d+}})/g)?.length || 0;
|
const total = text.match(/({{\d+}})/g)?.length || 0;
|
||||||
const correct = answers!.filter((x) => {
|
const correct = answers!.filter((x) => {
|
||||||
@@ -71,56 +80,55 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
return false;
|
return false;
|
||||||
}).length;
|
}).length;
|
||||||
const missing = total - answers!.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
const missing = total - answers!.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
||||||
return {total, correct, missing};
|
return { total, correct, missing };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [openDropdownId, setOpenDropdownId] = useState<string | null>(null);
|
||||||
|
|
||||||
const renderLines = useCallback(
|
const renderLines = useCallback(
|
||||||
(line: string) => {
|
(line: string) => {
|
||||||
return (
|
return (
|
||||||
<div className="text-base leading-5" key={v4()}>
|
<div className="text-xl leading-5" key={v4()} ref={dropdownRef}>
|
||||||
{reactStringReplace(line, /({{\d+}})/g, (match) => {
|
{reactStringReplace(line, /({{\d+}})/g, (match) => {
|
||||||
const id = match.replaceAll(/[\{\}]/g, "");
|
const id = match.replaceAll(/[\{\}]/g, "");
|
||||||
const userSolution = answers.find((x) => x.id === id);
|
const userSolution = answers.find((x) => x.id === id);
|
||||||
const styles = clsx(
|
const styles = clsx(
|
||||||
"rounded-full hover:text-white transition duration-300 ease-in-out my-1 px-5 py-2 text-center",
|
"rounded-full hover:text-white transition duration-300 ease-in-out my-1 px-5 py-2 text-center w-fit",
|
||||||
currentMCSelection?.id == id && "!bg-mti-purple !text-white !outline-none !ring-0",
|
|
||||||
!userSolution && "text-center text-mti-purple-light bg-mti-purple-ultralight",
|
!userSolution && "text-center text-mti-purple-light bg-mti-purple-ultralight",
|
||||||
userSolution && "text-center text-mti-purple-dark bg-mti-purple-ultralight",
|
userSolution && "text-center text-mti-purple-dark bg-mti-purple-ultralight",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const currentSelection = words.find((x) => {
|
||||||
|
if (typeof x !== "string" && "id" in x) {
|
||||||
|
return (x as FillBlanksMCOption).id.toString() == id.toString();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}) as FillBlanksMCOption;
|
||||||
|
|
||||||
return variant === "mc" ? (
|
return variant === "mc" ? (
|
||||||
<>
|
<MCDropdown
|
||||||
{/*<span className="mr-2">{`(${id})`}</span>*/}
|
id={id}
|
||||||
<button
|
options={currentSelection.options}
|
||||||
className={styles}
|
onSelect={(value) => onSelection(id, value)}
|
||||||
onClick={() => {
|
selectedValue={userSolution?.solution}
|
||||||
setCurrentMCSelection({
|
className="inline-block py-2 px-1"
|
||||||
id: id,
|
width={220}
|
||||||
selection: words.find((x) => {
|
isOpen={openDropdownId === id}
|
||||||
if (typeof x !== "string" && "id" in x) {
|
onToggle={()=> setOpenDropdownId(prevId => prevId === id ? null : id)}
|
||||||
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
|
<input
|
||||||
className={styles}
|
className={styles}
|
||||||
onChange={(e) => setAnswers((prev) => [...prev.filter((x) => x.id !== id), {id, solution: e.target.value}])}
|
onChange={(e) => setAnswers((prev) => [...prev.filter((x) => x.id !== id), { id, solution: e.target.value }])}
|
||||||
value={userSolution?.solution}
|
value={userSolution?.solution}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})
|
||||||
</div>
|
}
|
||||||
|
</div >
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[variant, words, setCurrentMCSelection, answers, currentMCSelection],
|
[variant, words, answers, openDropdownId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const memoizedLines = useMemo(() => {
|
const memoizedLines = useMemo(() => {
|
||||||
@@ -131,15 +139,15 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
));
|
));
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [text, variant, renderLines, currentMCSelection]);
|
}, [text, variant, renderLines]);
|
||||||
|
|
||||||
const onSelection = (questionID: string, value: string) => {
|
const onSelection = (questionID: string, value: string) => {
|
||||||
setAnswers((prev) => [...prev.filter((x) => x.id !== questionID), {id: questionID, solution: value}]);
|
setAnswers((prev) => [...prev.filter((x) => x.id !== questionID), { id: questionID, solution: value }]);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (variant === "mc") {
|
if (variant === "mc") {
|
||||||
setCurrentSolution({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps});
|
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers]);
|
}, [answers]);
|
||||||
@@ -150,19 +158,19 @@ 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, shuffleMaps: shuffleMaps})}
|
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps })}
|
||||||
className="max-w-[200px] w-full"
|
className="max-w-[200px] w-full"
|
||||||
disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
|
disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
|
||||||
Back
|
Previous Page
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onNext({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps});
|
onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
||||||
}}
|
}}
|
||||||
className="max-w-[200px] self-end w-full">
|
className="max-w-[200px] self-end w-full">
|
||||||
Next
|
Next Page
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -178,38 +186,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">{memoizedLines}</span>
|
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">{memoizedLines}</span>
|
||||||
{variant === "mc" && typeCheckWordsMC(words) ? (
|
{variant !== "mc" && (
|
||||||
<>
|
|
||||||
{currentMCSelection && (
|
|
||||||
<div className="bg-mti-gray-smoke rounded-xl flex flex-col gap-4 px-16 py-8">
|
|
||||||
<span className="font-medium text-lg text-mti-purple-dark mb-4 px-2">{`${currentMCSelection.id} - Select the appropriate word.`}</span>
|
|
||||||
<div className="flex gap-4 flex-wrap justify-between">
|
|
||||||
{currentMCSelection.selection?.options &&
|
|
||||||
Object.entries(currentMCSelection.selection.options)
|
|
||||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
||||||
.map(([key, value]) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={v4()}
|
|
||||||
onClick={() => onSelection(currentMCSelection.id, value)}
|
|
||||||
className={clsx(
|
|
||||||
"flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-base",
|
|
||||||
!!answers.find(
|
|
||||||
(x) =>
|
|
||||||
x.solution.toLocaleLowerCase() === value.toLocaleLowerCase() &&
|
|
||||||
x.id === currentMCSelection.id,
|
|
||||||
) && "!bg-mti-purple-light !text-white",
|
|
||||||
)}>
|
|
||||||
<span className="font-semibold">{key}.</span>
|
|
||||||
<span>{value}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="bg-mti-gray-smoke rounded-xl px-5 py-6 flex flex-col gap-4">
|
<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>
|
<span className="font-medium text-mti-purple-dark">Options</span>
|
||||||
<div className="flex gap-4 flex-wrap">
|
<div className="flex gap-4 flex-wrap">
|
||||||
@@ -240,19 +217,19 @@ 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, shuffleMaps: shuffleMaps})}
|
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps })}
|
||||||
className="max-w-[200px] w-full"
|
className="max-w-[200px] w-full"
|
||||||
disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
|
disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
|
||||||
Back
|
Previous Page
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onNext({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps});
|
onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
||||||
}}
|
}}
|
||||||
className="max-w-[200px] self-end w-full">
|
className="max-w-[200px] self-end w-full">
|
||||||
Next
|
Next Page
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {FillBlanksExercise} from "@/interfaces/exam";
|
import { FillBlanksExercise, FillBlanksMCOption } from "@/interfaces/exam";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
exercise: FillBlanksExercise;
|
exercise: FillBlanksExercise;
|
||||||
@@ -8,11 +9,16 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FillBlanksEdit = (props: Props) => {
|
const FillBlanksEdit = (props: Props) => {
|
||||||
const {exercise, updateExercise} = props;
|
const { exercise, updateExercise } = props;
|
||||||
|
|
||||||
|
const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
|
||||||
|
return Array.isArray(words) && words.every((word) => word && typeof word === "object" && "id" in word && "options" in word);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type={exercise?.variant && exercise.variant === "mc" ? "textarea" : "text"}
|
||||||
label="Prompt"
|
label="Prompt"
|
||||||
name="prompt"
|
name="prompt"
|
||||||
required
|
required
|
||||||
@@ -24,18 +30,18 @@ const FillBlanksEdit = (props: Props) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type={exercise?.variant && exercise.variant === "mc" ? "textarea" : "text"}
|
||||||
label="Text"
|
label="Text"
|
||||||
name="text"
|
name="text"
|
||||||
required
|
required
|
||||||
value={exercise.text}
|
value={exercise.text}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
updateExercise({
|
updateExercise({
|
||||||
text: value,
|
text: exercise?.variant && exercise.variant === "mc" ? value : value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<h1>Solutions</h1>
|
<h1 className="mt-4">Solutions</h1>
|
||||||
<div className="w-full flex flex-wrap -mx-2">
|
<div className="w-full flex flex-wrap -mx-2">
|
||||||
{exercise.solutions.map((solution, index) => (
|
{exercise.solutions.map((solution, index) => (
|
||||||
<div key={solution.id} className="flex sm:w-1/2 lg:w-1/4 px-2">
|
<div key={solution.id} className="flex sm:w-1/2 lg:w-1/4 px-2">
|
||||||
@@ -47,33 +53,75 @@ const FillBlanksEdit = (props: Props) => {
|
|||||||
value={solution.solution}
|
value={solution.solution}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
updateExercise({
|
updateExercise({
|
||||||
solutions: exercise.solutions.map((sol) => (sol.id === solution.id ? {...sol, solution: value} : sol)),
|
solutions: exercise.solutions.map((sol) => (sol.id === solution.id ? { ...sol, solution: value } : sol)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<h1>Words</h1>
|
<h1 className="mt-4">Words</h1>
|
||||||
<div className="w-full flex flex-wrap -mx-2">
|
<div className={clsx(exercise?.variant && exercise.variant === "mc" ? "w-full flex flex-row" : "w-full flex flex-wrap -mx-2")}>
|
||||||
{exercise.words.map((word, index) => (
|
{exercise?.variant && exercise.variant === "mc" && typeCheckWordsMC(exercise.words) ?
|
||||||
<div key={index} className="flex sm:w-1/2 lg:w-1/4 px-2">
|
(
|
||||||
<Input
|
<div className="flex flex-col w-full">
|
||||||
type="text"
|
{exercise.words.flatMap((mcOptions, wordIndex) =>
|
||||||
label={`Word ${index + 1}`}
|
<>
|
||||||
name="word"
|
<label className="font-semibold">{`Word ${wordIndex + 1}`}</label>
|
||||||
required
|
<div className="flex flex-row">
|
||||||
value={typeof word === "string" ? word : ("word" in word ? word.word : "")}
|
{Object.entries(mcOptions.options).map(([key, value], optionIndex) => (
|
||||||
onChange={(value) =>
|
<div key={`${wordIndex}-${optionIndex}-${key}`} className="flex sm:w-1/2 lg:w-1/4 px-2 mb-4">
|
||||||
updateExercise({
|
<Input
|
||||||
words: exercise.words.map((sol, idx) =>
|
type="text"
|
||||||
index === idx ? (typeof word === "string" ? value : {...word, word: value}) : sol,
|
label={`Option ${key}`}
|
||||||
),
|
name="word"
|
||||||
})
|
required
|
||||||
}
|
value={value}
|
||||||
/>
|
onChange={(newValue) =>
|
||||||
</div>
|
updateExercise({
|
||||||
))}
|
words: exercise.words.map((word, idx) =>
|
||||||
|
idx === wordIndex
|
||||||
|
? {
|
||||||
|
...(word as FillBlanksMCOption),
|
||||||
|
options: {
|
||||||
|
...(word as FillBlanksMCOption).options,
|
||||||
|
[key]: newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: word
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
:
|
||||||
|
(
|
||||||
|
exercise.words.map((word, index) => (
|
||||||
|
<div key={index} className="flex sm:w-1/2 lg:w-1/4 px-2">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
label={`Word ${index + 1}`}
|
||||||
|
name="word"
|
||||||
|
required
|
||||||
|
value={typeof word === "string" ? word : ("word" in word ? word.word : "")}
|
||||||
|
onChange={(value) =>
|
||||||
|
updateExercise({
|
||||||
|
words: exercise.words.map((sol, idx) =>
|
||||||
|
index === idx ? (typeof word === "string" ? value : { ...word, word: value }) : sol,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {useState} from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type: "email" | "text" | "password" | "tel" | "number";
|
type: "email" | "text" | "password" | "tel" | "number" | "textarea";
|
||||||
roundness?: "full" | "xl";
|
roundness?: "full" | "xl";
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -32,6 +32,20 @@ export default function Input({
|
|||||||
}: Props) {
|
}: Props) {
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
|
if (type === "textarea") {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
|
className="w-full h-full cursor-text px-7 py-8 input border-2 border-mti-gray-platinum bg-white rounded-3xl min-h-[200px]"
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
value={value}
|
||||||
|
placeholder={placeholder}
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (type === "password") {
|
if (type === "password") {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col gap-3 w-full">
|
<div className="relative flex flex-col gap-3 w-full">
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import React from "react";
|
|||||||
interface Props {
|
interface Props {
|
||||||
minTimer: number;
|
minTimer: number;
|
||||||
module: Module;
|
module: Module;
|
||||||
|
examLabel?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
exerciseIndex: number;
|
exerciseIndex: number;
|
||||||
totalExercises: number;
|
totalExercises: number;
|
||||||
@@ -30,6 +31,7 @@ export default function ModuleTitle({
|
|||||||
minTimer,
|
minTimer,
|
||||||
module,
|
module,
|
||||||
label,
|
label,
|
||||||
|
examLabel,
|
||||||
exerciseIndex,
|
exerciseIndex,
|
||||||
totalExercises,
|
totalExercises,
|
||||||
disableTimer = false,
|
disableTimer = false,
|
||||||
@@ -156,7 +158,7 @@ export default function ModuleTitle({
|
|||||||
<div className="w-full flex justify-between">
|
<div className="w-full flex justify-between">
|
||||||
<span className="text-base font-semibold">
|
<span className="text-base font-semibold">
|
||||||
{module === "level"
|
{module === "level"
|
||||||
? "Placement Test"
|
? (examLabel ? examLabel : "Placement Test")
|
||||||
: `${moduleLabels[module]} exam${label ? ` - ${label}` : ''}`
|
: `${moduleLabels[module]} exam${label ? ` - ${label}` : ''}`
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -3,16 +3,32 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
part: LevelPart,
|
part: LevelPart,
|
||||||
contextWord: string | undefined,
|
contextWords: { match: string, originalLine: string }[] | undefined,
|
||||||
setContextWordLine: React.Dispatch<React.SetStateAction<number | undefined>>
|
setContextWordLines: React.Dispatch<React.SetStateAction<number[] | undefined>>
|
||||||
|
setTotalLines: React.Dispatch<React.SetStateAction<number>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine }) => {
|
const TextComponent: React.FC<Props> = ({ part, contextWords, setContextWordLines, setTotalLines }) => {
|
||||||
const textRef = useRef<HTMLDivElement>(null);
|
const textRef = useRef<HTMLDivElement>(null);
|
||||||
const [lineNumbers, setLineNumbers] = useState<number[]>([]);
|
const [lineNumbers, setLineNumbers] = useState<number[]>([]);
|
||||||
const [lineHeight, setLineHeight] = useState<number>(0);
|
const [lineHeight, setLineHeight] = useState<number>(0);
|
||||||
const [addBreaksTo, setAddBreaksTo] = useState<number[]>([]);
|
const [addBreaksTo, setAddBreaksTo] = useState<number[]>([]);
|
||||||
|
|
||||||
|
const getBoldTag = (context: string) => {
|
||||||
|
const regex = /<b\s+class=['"]([^'"]+)['"]>(\d+)<\/b>/;
|
||||||
|
const match = context.match(regex);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
className: match[1],
|
||||||
|
number: match[2],
|
||||||
|
fullTag: match[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bTag = getBoldTag(part.context!);
|
||||||
|
|
||||||
const calculateLineNumbers = () => {
|
const calculateLineNumbers = () => {
|
||||||
if (textRef.current) {
|
if (textRef.current) {
|
||||||
const computedStyle = window.getComputedStyle(textRef.current);
|
const computedStyle = window.getComputedStyle(textRef.current);
|
||||||
@@ -39,7 +55,6 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
|||||||
const lines = paragraphs.map((line, lineIndex) => {
|
const lines = paragraphs.map((line, lineIndex) => {
|
||||||
const paragraphWords = line.split(/(\s+)/);
|
const paragraphWords = line.split(/(\s+)/);
|
||||||
return paragraphWords.map((word, wordIndex) => {
|
return paragraphWords.map((word, wordIndex) => {
|
||||||
|
|
||||||
if (lineIndex !== 0 && wordIndex == 0 && lineIndex < paragraphs.length) {
|
if (lineIndex !== 0 && wordIndex == 0 && lineIndex < paragraphs.length) {
|
||||||
betweenParagraphs[lineIndex - 1][1] = word;
|
betweenParagraphs[lineIndex - 1][1] = word;
|
||||||
}
|
}
|
||||||
@@ -47,9 +62,17 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
|||||||
if (wordIndex == paragraphWords.length - 1 && lineIndex < paragraphs.length) {
|
if (wordIndex == paragraphWords.length - 1 && lineIndex < paragraphs.length) {
|
||||||
betweenParagraphs[lineIndex][0] = word;
|
betweenParagraphs[lineIndex][0] = word;
|
||||||
}
|
}
|
||||||
|
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
span.textContent = word;
|
if (wordIndex === 0 && bTag) {
|
||||||
|
const b = document.createElement('b');
|
||||||
|
b.classList.add(bTag.className);
|
||||||
|
b.textContent = `${lineIndex + 1}`;
|
||||||
|
span.appendChild(b);
|
||||||
|
span.appendChild(document.createTextNode(word.substring(1)));
|
||||||
|
}else {
|
||||||
|
span.appendChild(document.createTextNode(word));
|
||||||
|
}
|
||||||
return span;
|
return span;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -67,8 +90,11 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
|||||||
const processedLines: string[][] = [[]];
|
const processedLines: string[][] = [[]];
|
||||||
let currentLine = 1;
|
let currentLine = 1;
|
||||||
let currentLineTop: number | undefined;
|
let currentLineTop: number | undefined;
|
||||||
let contextWordLine: number | null = null;
|
|
||||||
|
|
||||||
|
let contextWordLines: number[] = [];
|
||||||
|
if (contextWords) {
|
||||||
|
contextWordLines = Array(contextWords.length).fill(-1);
|
||||||
|
}
|
||||||
const firstChild = offscreenElement.firstChild as HTMLElement;
|
const firstChild = offscreenElement.firstChild as HTMLElement;
|
||||||
if (firstChild) {
|
if (firstChild) {
|
||||||
currentLineTop = firstChild.getBoundingClientRect().top;
|
currentLineTop = firstChild.getBoundingClientRect().top;
|
||||||
@@ -78,7 +104,7 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
|||||||
|
|
||||||
let betweenIndex = 0;
|
let betweenIndex = 0;
|
||||||
const addBreaksTo: number[] = [];
|
const addBreaksTo: number[] = [];
|
||||||
spans.forEach((span, index)=> {
|
spans.forEach((span, index) => {
|
||||||
const rect = span.getBoundingClientRect();
|
const rect = span.getBoundingClientRect();
|
||||||
const top = rect.top;
|
const top = rect.top;
|
||||||
|
|
||||||
@@ -98,17 +124,22 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
|||||||
}
|
}
|
||||||
|
|
||||||
processedLines[processedLines.length - 1].push(span.textContent?.trim() || '');
|
processedLines[processedLines.length - 1].push(span.textContent?.trim() || '');
|
||||||
|
if (contextWords && contextWordLines.some(element => element === -1)) {
|
||||||
if (contextWord && contextWordLine === null && span.textContent?.includes(contextWord)) {
|
contextWords.forEach((w, index) => {
|
||||||
contextWordLine = currentLine;
|
if (span.textContent?.includes(w.match) && contextWordLines[index] == -1) {
|
||||||
|
contextWordLines[index] = currentLine;
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setAddBreaksTo(addBreaksTo);
|
setAddBreaksTo(addBreaksTo);
|
||||||
|
|
||||||
setLineNumbers(processedLines.map((_, index) => index + 1));
|
setLineNumbers(processedLines.map((_, index) => index + 1));
|
||||||
if (contextWordLine) {
|
setTotalLines(currentLine);
|
||||||
setContextWordLine(contextWordLine);
|
|
||||||
|
if (contextWordLines.length > 0) {
|
||||||
|
setContextWordLines(contextWordLines);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.body.removeChild(offscreenElement);
|
document.body.removeChild(offscreenElement);
|
||||||
@@ -135,18 +166,18 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [part.context, contextWord]);
|
}, [part.context, contextWords]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex mt-2">
|
<div className="flex mt-2">
|
||||||
<div className="flex-shrink-0 w-8 pr-2">
|
<div className="flex-shrink-0 w-8 pr-2">
|
||||||
{lineNumbers.map(num => (
|
{lineNumbers.map(num => (
|
||||||
<>
|
<>
|
||||||
<div key={num} className="text-gray-400 flex justify-end" style={{ lineHeight: `${lineHeight}px` }}>
|
<div key={num} className="text-gray-400 flex justify-end" style={{ lineHeight: `${lineHeight}px` }}>
|
||||||
{num}
|
{num}
|
||||||
</div>
|
</div>
|
||||||
{/* Do not delete the space between the span or else the lines get messed up */}
|
{/* Do not delete the space between the span or else the lines get messed up */}
|
||||||
{addBreaksTo.includes(num) && <span className={`h-[${lineHeight}px] whitespace-pre-wrap`}> </span>}
|
{addBreaksTo.includes(num) && <span className={`h-[${lineHeight}px] whitespace-pre-wrap`}> </span>}
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -85,8 +85,9 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
|
|
||||||
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
|
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
|
||||||
|
|
||||||
const [contextWord, setContextWord] = useState<string | undefined>(undefined);
|
const [contextWords, setContextWords] = useState<{ match: string, originalLine: string }[] | undefined>(undefined);
|
||||||
const [contextWordLine, setContextWordLine] = useState<number | undefined>(undefined);
|
const [contextWordLines, setContextWordLines] = useState<number[] | undefined>(undefined);
|
||||||
|
const [totalLines, setTotalLines] = useState<number>(0);
|
||||||
|
|
||||||
const [showSolutionsSave, setShowSolutionsSave] = useState(showSolutions ? userSolutions.filter((x) => x.module === "level") : undefined)
|
const [showSolutionsSave, setShowSolutionsSave] = useState(showSolutions ? userSolutions.filter((x) => x.module === "level") : undefined)
|
||||||
|
|
||||||
@@ -206,6 +207,14 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
|
|
||||||
if (questionIndex == 0) {
|
if (questionIndex == 0) {
|
||||||
setPartIndex(partIndex - 1);
|
setPartIndex(partIndex - 1);
|
||||||
|
if (!seenParts.has(partIndex - 1)) {
|
||||||
|
setBgColor(levelBgColor);
|
||||||
|
setShowPartDivider(true);
|
||||||
|
setQuestionIndex(0);
|
||||||
|
setSeenParts(prev => new Set(prev).add(partIndex - 1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const lastExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1;
|
const lastExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1;
|
||||||
const lastExercise = exam.parts[partIndex - 1].exercises[lastExerciseIndex];
|
const lastExercise = exam.parts[partIndex - 1].exercises[lastExerciseIndex];
|
||||||
setExerciseIndex(lastExerciseIndex);
|
setExerciseIndex(lastExerciseIndex);
|
||||||
@@ -260,8 +269,9 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
{exam.parts[partIndex].context &&
|
{exam.parts[partIndex].context &&
|
||||||
<TextComponent
|
<TextComponent
|
||||||
part={exam.parts[partIndex]}
|
part={exam.parts[partIndex]}
|
||||||
contextWord={contextWord}
|
contextWords={contextWords}
|
||||||
setContextWordLine={setContextWordLine}
|
setContextWordLines={setContextWordLines}
|
||||||
|
setTotalLines={setTotalLines}
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -321,35 +331,59 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
|
const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
|
||||||
|
|
||||||
|
const findMatch = (index: number) => {
|
||||||
|
if (currentExercise && currentExercise.type === "multipleChoice" && currentExercise!.questions[index]) {
|
||||||
|
const match = currentExercise!.questions[index].prompt.match(regex);
|
||||||
|
if (match) {
|
||||||
|
return { match: match[1], originalLine: match[2] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the client for some whatever random reason decides
|
||||||
|
// to add more questions update this
|
||||||
|
const numberOfQuestions = 2;
|
||||||
|
|
||||||
|
if (exam.parts[partIndex].context) {
|
||||||
|
const hits = Array.from({ length: numberOfQuestions }).reduce<{ match: string, originalLine: string }[]>((acc, _, i) => {
|
||||||
|
const result = findMatch(questionIndex + i);
|
||||||
|
if (!!result) {
|
||||||
|
acc.push(result);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (hits.length > 0) {
|
||||||
|
setContextWords(hits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [currentExercise, questionIndex, totalLines]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
exerciseIndex !== -1 && currentExercise &&
|
exerciseIndex !== -1 && currentExercise &&
|
||||||
currentExercise.type === "multipleChoice" &&
|
currentExercise.type === "multipleChoice" &&
|
||||||
currentExercise.questions[questionIndex] &&
|
exam.parts[partIndex].context && contextWordLines
|
||||||
currentExercise.questions[questionIndex].prompt &&
|
|
||||||
exam.parts[partIndex].context
|
|
||||||
) {
|
) {
|
||||||
const match = currentExercise.questions[questionIndex].prompt.match(regex);
|
if (contextWordLines.length > 0) {
|
||||||
if (match) {
|
contextWordLines.forEach((n, i) => {
|
||||||
const word = match[1];
|
if (contextWords && contextWords[i] && n !== -1) {
|
||||||
const originalLineNumber = match[2];
|
const updatedPrompt = currentExercise!.questions[questionIndex + i].prompt.replace(
|
||||||
|
`in line ${contextWords[i].originalLine}`,
|
||||||
if (word !== contextWord) {
|
`in line ${n}`
|
||||||
setContextWord(word);
|
);
|
||||||
}
|
currentExercise!.questions[questionIndex + i].prompt = updatedPrompt;
|
||||||
|
}
|
||||||
const updatedPrompt = currentExercise.questions[questionIndex].prompt.replace(
|
})
|
||||||
`in line ${originalLineNumber}`,
|
|
||||||
`in line ${contextWordLine || originalLineNumber}`
|
|
||||||
);
|
|
||||||
|
|
||||||
currentExercise.questions[questionIndex].prompt = updatedPrompt;
|
|
||||||
setChangedPrompt(true);
|
setChangedPrompt(true);
|
||||||
} else {
|
|
||||||
setContextWord(undefined);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [contextWordLines]);
|
||||||
}, [currentExercise, questionIndex, contextWordLine]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (continueAnyways) {
|
if (continueAnyways) {
|
||||||
@@ -419,7 +453,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
<Button color="purple" onClick={() => setShowSubmissionModal(false)} variant="outline" className="max-w-[200px] self-end w-full !text-xl">
|
<Button color="purple" onClick={() => setShowSubmissionModal(false)} variant="outline" className="max-w-[200px] self-end w-full !text-xl">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="rose" onClick={() => { setShowSubmissionModal(false); setContinueAnyways(true)}} className="max-w-[200px] self-end w-full !text-xl">
|
<Button color="rose" onClick={() => { setShowSubmissionModal(false); setContinueAnyways(true) }} className="max-w-[200px] self-end w-full !text-xl">
|
||||||
Confirm
|
Confirm
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -469,6 +503,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ModuleTitle
|
<ModuleTitle
|
||||||
|
examLabel={exam.label}
|
||||||
partLabel={partLabel()}
|
partLabel={partLabel()}
|
||||||
minTimer={exam.minTimer}
|
minTimer={exam.minTimer}
|
||||||
exerciseIndex={calculateExerciseIndex()}
|
exerciseIndex={calculateExerciseIndex()}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ interface ExamBase {
|
|||||||
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
|
||||||
private?: boolean;
|
private?: boolean;
|
||||||
|
label?: string;
|
||||||
}
|
}
|
||||||
export interface ReadingExam extends ExamBase {
|
export interface ReadingExam extends ExamBase {
|
||||||
module: "reading";
|
module: "reading";
|
||||||
|
|||||||
@@ -15,31 +15,38 @@ import {
|
|||||||
Exercise,
|
Exercise,
|
||||||
} from "@/interfaces/exam";
|
} from "@/interfaces/exam";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import {getExamById} from "@/utils/exams";
|
import { getExamById } from "@/utils/exams";
|
||||||
import {playSound} from "@/utils/sound";
|
import { playSound } from "@/utils/sound";
|
||||||
import {Tab} from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {capitalize, sample} from "lodash";
|
import { capitalize, sample } from "lodash";
|
||||||
import {useRouter} from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {BsArrowRepeat, BsCheck, BsPencilSquare, BsX} from "react-icons/bs";
|
import { BsArrowRepeat, BsCheck, BsPencilSquare, BsX } from "react-icons/bs";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import {toast} from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import {v4} from "uuid";
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
[key: string]: any;
|
||||||
|
value: string | null;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
|
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
|
||||||
const TYPES: {[key: string]: string} = {
|
const TYPES: { [key: string]: string } = {
|
||||||
multiple_choice_4: "Multiple Choice",
|
multiple_choice_4: "Multiple Choice",
|
||||||
multiple_choice_blank_space: "Multiple Choice - Blank Space",
|
multiple_choice_blank_space: "Multiple Choice - Blank Space",
|
||||||
multiple_choice_underlined: "Multiple Choice - Underlined",
|
multiple_choice_underlined: "Multiple Choice - Underlined",
|
||||||
blank_space_text: "Blank Space",
|
blank_space_text: "Blank Space",
|
||||||
reading_passage_utas: "Reading Passage",
|
reading_passage_utas: "Reading Passage",
|
||||||
|
fill_blanks_mc: "Multiple Choice - Fill Blanks",
|
||||||
};
|
};
|
||||||
|
|
||||||
type LevelSection = {type: string; quantity: number; topic?: string; part?: LevelPart};
|
type LevelSection = { type: string; quantity: number; topic?: string; part?: LevelPart };
|
||||||
|
|
||||||
const QuestionDisplay = ({question, onUpdate}: {question: MultipleChoiceQuestion; onUpdate: (question: MultipleChoiceQuestion) => void}) => {
|
const QuestionDisplay = ({ question, onUpdate }: { question: MultipleChoiceQuestion; onUpdate: (question: MultipleChoiceQuestion) => void }) => {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [options, setOptions] = useState(question.options);
|
const [options, setOptions] = useState(question.options);
|
||||||
const [answer, setAnswer] = useState(question.solution);
|
const [answer, setAnswer] = useState(question.solution);
|
||||||
@@ -70,7 +77,7 @@ const QuestionDisplay = ({question, onUpdate}: {question: MultipleChoiceQuestion
|
|||||||
<input
|
<input
|
||||||
defaultValue={option.text}
|
defaultValue={option.text}
|
||||||
className="w-60"
|
className="w-60"
|
||||||
onChange={(e) => setOptions((prev) => prev.map((x, idx) => (idx === index ? {...x, text: e.target.value} : x)))}
|
onChange={(e) => setOptions((prev) => prev.map((x, idx) => (idx === index ? { ...x, text: e.target.value } : x)))}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span>{option.text}</span>
|
<span>{option.text}</span>
|
||||||
@@ -90,7 +97,7 @@ const QuestionDisplay = ({question, onUpdate}: {question: MultipleChoiceQuestion
|
|||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onUpdate({...question, options, solution: answer});
|
onUpdate({ ...question, options, solution: answer });
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
}}
|
}}
|
||||||
className="p-2 border border-neutral-300 bg-white rounded-xl hover:drop-shadow transition ease-in-out duration-300">
|
className="p-2 border border-neutral-300 bg-white rounded-xl hover:drop-shadow transition ease-in-out duration-300">
|
||||||
@@ -108,9 +115,16 @@ const QuestionDisplay = ({question, onUpdate}: {question: MultipleChoiceQuestion
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskTab = ({section, setSection}: {section: LevelSection; setSection: (section: LevelSection) => void}) => {
|
const TaskTab = ({ section, label, index, setSection }: { section: LevelSection; label: string, index: number, setSection: (section: LevelSection) => void }) => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const [category, setCategory] = useState<string>("");
|
||||||
|
const [description, setDescription] = useState<string>("");
|
||||||
|
const [customDescription, setCustomDescription] = useState<string>("");
|
||||||
|
const [previousOption, setPreviousOption] = useState<Option>({ value: "None", label: "None" });
|
||||||
|
const [descriptionOption, setDescriptionOption] = useState<Option>({ value: "None", label: "None" });
|
||||||
|
const [updateIntro, setUpdateIntro] = useState<boolean>(false);
|
||||||
|
|
||||||
const onUpdate = (question: MultipleChoiceQuestion) => {
|
const onUpdate = (question: MultipleChoiceQuestion) => {
|
||||||
if (!section) return;
|
if (!section) return;
|
||||||
|
|
||||||
@@ -124,6 +138,66 @@ const TaskTab = ({section, setSection}: {section: LevelSection; setSection: (sec
|
|||||||
setSection(updatedExam as any);
|
setSection(updatedExam as any);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultPresets: any = {
|
||||||
|
multiple_choice_4: "Welcome to {part} of the {label}. In this section, you'll be asked to select the correct word or group of words that best completes each sentence.\n\nFor each question, carefully read the sentence and click on the option (A, B, C, or D) that you believe is correct. After making your selection, you can proceed to the next question by clicking \"Next\". If you need to review or change your previous answers, you can go back at any time by clicking \"Back\".",
|
||||||
|
multiple_choice_blank_space: undefined,
|
||||||
|
multiple_choice_underlined: "Welcome to {part} of the {label}. In this section, you'll be asked to identify the underlined word or group of words that is not correct in each sentence.\n\nFor each question, carefully review the sentence and click on the option (A, B, C, or D) that you believe contains the incorrect word or group of words. After making your selection, you can proceed to the next question by clicking \"Next\". If needed, you can go back to previous questions by clicking \"Back\".",
|
||||||
|
blank_space_text: undefined,
|
||||||
|
reading_passage_utas: "Welcome to {part} of the {label}. In this section, you will read a text and answer the questions that follow.\n\nCarefully read the provided text, then select the correct answer (A, B, C, or D) for each question. After making your selection, you can proceed to the next question by clicking \"Next\". If you need to review or change your answers, you can go back at any time by clicking \"Back\".",
|
||||||
|
fill_blanks_mc: "Welcome to {part} of the {label}. In this section, you will read a text and choose the correct word to fill in each blank space.\n\nFor each question, carefully read the text and click on the option (A, B, C, or D) that you believe best fits the context. Once you've made your choice, proceed to the next question by clicking \"Next\". If needed, you can go back to review or change your answers by clicking \"Back\"."
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDefaultPreset = () => {
|
||||||
|
return defaultPresets[section.type] ? defaultPresets[section.type].replace('{part}', `Part ${index + 1}`).replace('{label}', label) :
|
||||||
|
"No default preset is yet available for this type of exercise."
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (descriptionOption.value === "Default" && section?.type) {
|
||||||
|
setDescription(getDefaultPreset())
|
||||||
|
}
|
||||||
|
if (descriptionOption.value === "Custom" && customDescription !== "") {
|
||||||
|
setDescription(customDescription);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [descriptionOption, section?.type, label])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (section?.type) {
|
||||||
|
const defaultPreset = getDefaultPreset();
|
||||||
|
if (descriptionOption.value === "Default" && previousOption.value === "Default" && description !== defaultPreset) {
|
||||||
|
setDescriptionOption({ value: "Custom", label: "Custom" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [descriptionOption, description, label])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPreviousOption(descriptionOption);
|
||||||
|
}, [descriptionOption])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (section.part && ((descriptionOption.value === "Custom" || descriptionOption.value === "Default") && !section.part.intro)) {
|
||||||
|
setUpdateIntro(true);
|
||||||
|
}
|
||||||
|
}, [section?.part, descriptionOption, category])
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (updateIntro && section.part) {
|
||||||
|
setSection({
|
||||||
|
...section,
|
||||||
|
part: {
|
||||||
|
...section.part!,
|
||||||
|
intro: descriptionOption.value === "Default" ? getDefaultPreset() : (descriptionOption.value === "Custom" ? customDescription : undefined),
|
||||||
|
category: category === "" ? undefined : category
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setUpdateIntro(false);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [updateIntro, section?.part])
|
||||||
|
|
||||||
const renderExercise = (exercise: Exercise) => {
|
const renderExercise = (exercise: Exercise) => {
|
||||||
if (exercise.type === "multipleChoice")
|
if (exercise.type === "multipleChoice")
|
||||||
return (
|
return (
|
||||||
@@ -138,7 +212,12 @@ const TaskTab = ({section, setSection}: {section: LevelSection; setSection: (sec
|
|||||||
updateExercise={(data: any) =>
|
updateExercise={(data: any) =>
|
||||||
setSection({
|
setSection({
|
||||||
...section,
|
...section,
|
||||||
part: {...section.part!, exercises: section.part!.exercises.map((x) => (x.id === exercise.id ? {...x, ...data} : x))},
|
part: {
|
||||||
|
...section.part!,
|
||||||
|
exercises: section.part!.exercises.map((x) => (x.id === exercise.id ? { ...x, ...data } : x)),
|
||||||
|
intro: descriptionOption.value === "Default" ? getDefaultPreset() : (descriptionOption.value === "Custom" ? customDescription : undefined),
|
||||||
|
category: category === "" ? undefined : category
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -158,7 +237,12 @@ const TaskTab = ({section, setSection}: {section: LevelSection; setSection: (sec
|
|||||||
updateExercise={(data: any) =>
|
updateExercise={(data: any) =>
|
||||||
setSection({
|
setSection({
|
||||||
...section,
|
...section,
|
||||||
part: {...section.part!, exercises: section.part!.exercises.map((x) => (x.id === exercise.id ? {...x, ...data} : x))},
|
part: {
|
||||||
|
...section.part!,
|
||||||
|
exercises: section.part!.exercises.map((x) => (x.id === exercise.id ? { ...x, ...data } : x)),
|
||||||
|
intro: descriptionOption.value === "Default" ? getDefaultPreset() : (descriptionOption.value === "Custom" ? customDescription : undefined),
|
||||||
|
category: category === "" ? undefined : category
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -178,7 +262,12 @@ const TaskTab = ({section, setSection}: {section: LevelSection; setSection: (sec
|
|||||||
updateExercise={(data: any) =>
|
updateExercise={(data: any) =>
|
||||||
setSection({
|
setSection({
|
||||||
...section,
|
...section,
|
||||||
part: {...section.part!, exercises: section.part!.exercises.map((x) => (x.id === exercise.id ? {...x, ...data} : x))},
|
part: {
|
||||||
|
...section.part!,
|
||||||
|
exercises: section.part!.exercises.map((x) => (x.id === exercise.id ? { ...x, ...data } : x)),
|
||||||
|
intro: descriptionOption.value === "Default" ? getDefaultPreset() : (descriptionOption.value === "Custom" ? customDescription : undefined),
|
||||||
|
category: category === "" ? undefined : category
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -188,30 +277,61 @@ const TaskTab = ({section, setSection}: {section: LevelSection; setSection: (sec
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab.Panel className="w-full bg-ielts-level/20 min-h-[600px] h-full rounded-xl p-6 flex flex-col gap-4">
|
<Tab.Panel className="w-full bg-ielts-level/20 min-h-[600px] h-full rounded-xl p-6 flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-8">
|
||||||
|
<div className="flex flex-row w-full gap-4">
|
||||||
|
<div className="flex flex-col gap-3 w-1/2">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">Description</label>
|
||||||
|
<Select
|
||||||
|
options={["None", "Default", "Custom"].map((descriptionOption) => ({ value: descriptionOption, label: descriptionOption }))}
|
||||||
|
onChange={(o) => setDescriptionOption({ value: o!.value, label: o!.label })}
|
||||||
|
value={descriptionOption}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3 w-1/2">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">Category</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Category"
|
||||||
|
name="category"
|
||||||
|
onChange={(e) => setCategory(e)}
|
||||||
|
roundness="full"
|
||||||
|
defaultValue={category}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{descriptionOption.value !== "None" && (
|
||||||
|
<Input
|
||||||
|
type="textarea"
|
||||||
|
placeholder="Part Description"
|
||||||
|
name="category"
|
||||||
|
onChange={(e) => { setDescription(e); setCustomDescription(e); }}
|
||||||
|
roundness="full"
|
||||||
|
value={descriptionOption.value === "Default" ? description : customDescription}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="flex gap-4 w-full">
|
<div className="flex gap-4 w-full">
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Exercise Type</label>
|
<label className="font-normal text-base text-mti-gray-dim">Exercise Type</label>
|
||||||
<Select
|
<Select
|
||||||
options={Object.keys(TYPES).map((key) => ({value: key, label: TYPES[key]}))}
|
options={Object.keys(TYPES).map((key) => ({ value: key, label: TYPES[key] }))}
|
||||||
onChange={(e) => setSection({...section, type: e!.value!})}
|
onChange={(e) => setSection({ ...section, type: e!.value! })}
|
||||||
value={{value: section?.type || "multiple_choice_4", label: TYPES[section?.type || "multiple_choice_4"]}}
|
value={{ value: section?.type || "multiple_choice_4", label: TYPES[section?.type || "multiple_choice_4"] }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Number of Questions</label>
|
<label className="font-normal text-base text-mti-gray-dim">{section?.type && section.type === "fill_blanks_mc" ? "Number of Words" : "Number of Questions"}</label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
name="Number of Questions"
|
name="Number of Questions"
|
||||||
onChange={(v) => setSection({...section, quantity: parseInt(v)})}
|
onChange={(v) => setSection({ ...section, quantity: parseInt(v) })}
|
||||||
value={section?.quantity || 10}
|
value={section?.quantity || 10}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{section?.type === "reading_passage_utas" && (
|
{section?.type === "reading_passage_utas" || section?.type === "fill_blanks_mc" && (
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Topic</label>
|
<label className="font-normal text-base text-mti-gray-dim">Topic</label>
|
||||||
<Input type="text" name="Topic" onChange={(v) => setSection({...section, topic: v})} value={section?.topic} />
|
<Input type="text" name="Topic" onChange={(v) => setSection({ ...section, topic: v })} value={section?.topic} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -235,18 +355,19 @@ interface Props {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LevelGeneration = ({id}: Props) => {
|
const LevelGeneration = ({ id }: Props) => {
|
||||||
const [generatedExam, setGeneratedExam] = useState<LevelExam>();
|
const [generatedExam, setGeneratedExam] = useState<LevelExam>();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [resultingExam, setResultingExam] = useState<LevelExam>();
|
const [resultingExam, setResultingExam] = useState<LevelExam>();
|
||||||
const [timer, setTimer] = useState(10);
|
const [timer, setTimer] = useState(10);
|
||||||
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!);
|
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!);
|
||||||
const [numberOfParts, setNumberOfParts] = useState(1);
|
const [numberOfParts, setNumberOfParts] = useState(1);
|
||||||
const [parts, setParts] = useState<LevelSection[]>([{quantity: 10, type: "multiple_choice_4"}]);
|
const [parts, setParts] = useState<LevelSection[]>([{ quantity: 10, type: "multiple_choice_4" }]);
|
||||||
const [isPrivate, setPrivate] = useState<boolean>(false);
|
const [isPrivate, setPrivate] = useState<boolean>(false);
|
||||||
|
const [label, setLabel] = useState<string>("Placement Test");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setParts((prev) => Array.from(Array(numberOfParts)).map((_, i) => (!!prev.at(i) ? prev.at(i)! : {quantity: 10, type: "multiple_choice_4"})));
|
setParts((prev) => Array.from(Array(numberOfParts)).map((_, i) => (!!prev.at(i) ? prev.at(i)! : { quantity: 10, type: "multiple_choice_4" })));
|
||||||
}, [numberOfParts]);
|
}, [numberOfParts]);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -289,7 +410,7 @@ const LevelGeneration = ({id}: Props) => {
|
|||||||
let newParts = [...parts];
|
let newParts = [...parts];
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post<{exercises: {[key: string]: any}}>("/api/exam/level/generate/level", {nr_exercises: numberOfParts, ...body})
|
.post<{ exercises: { [key: string]: any } }>("/api/exam/level/generate/level", { nr_exercises: numberOfParts, ...body })
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
console.log(result.data);
|
console.log(result.data);
|
||||||
|
|
||||||
@@ -304,6 +425,7 @@ const LevelGeneration = ({id}: Props) => {
|
|||||||
variant: "full",
|
variant: "full",
|
||||||
isDiagnostic: false,
|
isDiagnostic: false,
|
||||||
private: isPrivate,
|
private: isPrivate,
|
||||||
|
label: label,
|
||||||
parts: parts
|
parts: parts
|
||||||
.map((part, index) => {
|
.map((part, index) => {
|
||||||
const currentExercise = result.data.exercises[`exercise_${index + 1}`] as any;
|
const currentExercise = result.data.exercises[`exercise_${index + 1}`] as any;
|
||||||
@@ -317,23 +439,55 @@ const LevelGeneration = ({id}: Props) => {
|
|||||||
id: v4(),
|
id: v4(),
|
||||||
prompt:
|
prompt:
|
||||||
part.type === "multiple_choice_underlined"
|
part.type === "multiple_choice_underlined"
|
||||||
? "Select the wrong part of the sentence."
|
? "Choose the underlined word or group of words that is not correct.\nFor each question, select your choice (A, B, C or D)."
|
||||||
: "Select the appropriate option.",
|
: "Choose the correct word or group of words that completes the sentences below.\nFor each question, select the correct letter (A, B, C or D).",
|
||||||
questions: currentExercise.questions.map((x: any) => ({...x, variant: "text"})),
|
questions: currentExercise.questions.map((x: any) => ({ ...x, variant: "text" })),
|
||||||
type: "multipleChoice",
|
type: "multipleChoice",
|
||||||
userSolutions: [],
|
userSolutions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const item = {
|
const item = {
|
||||||
exercises: [exercise],
|
exercises: [exercise],
|
||||||
|
intro: parts[index].part?.intro,
|
||||||
|
category: parts[index].part?.category
|
||||||
};
|
};
|
||||||
|
|
||||||
newParts = newParts.map((p, i) =>
|
newParts = newParts.map((p, i) =>
|
||||||
i === index
|
i === index
|
||||||
? {
|
? {
|
||||||
...p,
|
...p,
|
||||||
part: item,
|
part: item,
|
||||||
}
|
}
|
||||||
|
: p,
|
||||||
|
);
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.type === "fill_blanks_mc") {
|
||||||
|
const exercise: FillBlanksExercise = {
|
||||||
|
id: v4(),
|
||||||
|
prompt: "Read the text below and choose the correct word for each space.\nFor each question, select your choice (A, B, C or D). ",
|
||||||
|
text: currentExercise.text,
|
||||||
|
words: currentExercise.words,
|
||||||
|
solutions: currentExercise.solutions,
|
||||||
|
type: "fillBlanks",
|
||||||
|
variant: "mc",
|
||||||
|
userSolutions: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
exercises: [exercise],
|
||||||
|
intro: parts[index].part?.intro,
|
||||||
|
category: parts[index].part?.category
|
||||||
|
};
|
||||||
|
|
||||||
|
newParts = newParts.map((p, i) =>
|
||||||
|
i === index
|
||||||
|
? {
|
||||||
|
...p,
|
||||||
|
part: item,
|
||||||
|
}
|
||||||
: p,
|
: p,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -346,21 +500,23 @@ const LevelGeneration = ({id}: Props) => {
|
|||||||
prompt: "Complete the text below.",
|
prompt: "Complete the text below.",
|
||||||
text: currentExercise.text,
|
text: currentExercise.text,
|
||||||
maxWords: 3,
|
maxWords: 3,
|
||||||
solutions: currentExercise.words.map((x: any) => ({id: x.id, solution: [x.text]})),
|
solutions: currentExercise.words.map((x: any) => ({ id: x.id, solution: [x.text] })),
|
||||||
type: "writeBlanks",
|
type: "writeBlanks",
|
||||||
userSolutions: [],
|
userSolutions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const item = {
|
const item = {
|
||||||
exercises: [exercise],
|
exercises: [exercise],
|
||||||
|
intro: parts[index].part?.intro,
|
||||||
|
category: parts[index].part?.category
|
||||||
};
|
};
|
||||||
|
|
||||||
newParts = newParts.map((p, i) =>
|
newParts = newParts.map((p, i) =>
|
||||||
i === index
|
i === index
|
||||||
? {
|
? {
|
||||||
...p,
|
...p,
|
||||||
part: item,
|
part: item,
|
||||||
}
|
}
|
||||||
: p,
|
: p,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -370,7 +526,7 @@ const LevelGeneration = ({id}: Props) => {
|
|||||||
const mcExercise: MultipleChoiceExercise = {
|
const mcExercise: MultipleChoiceExercise = {
|
||||||
id: v4(),
|
id: v4(),
|
||||||
prompt: "Select the appropriate option.",
|
prompt: "Select the appropriate option.",
|
||||||
questions: currentExercise.exercises.multipleChoice.questions.map((x: any) => ({...x, variant: "text"})),
|
questions: currentExercise.exercises.multipleChoice.questions.map((x: any) => ({ ...x, variant: "text" })),
|
||||||
type: "multipleChoice",
|
type: "multipleChoice",
|
||||||
userSolutions: [],
|
userSolutions: [],
|
||||||
};
|
};
|
||||||
@@ -391,14 +547,16 @@ const LevelGeneration = ({id}: Props) => {
|
|||||||
const item = {
|
const item = {
|
||||||
context: currentExercise.text.content,
|
context: currentExercise.text.content,
|
||||||
exercises: [mcExercise, wbExercise],
|
exercises: [mcExercise, wbExercise],
|
||||||
|
intro: parts[index].part?.intro,
|
||||||
|
category: parts[index].part?.category
|
||||||
};
|
};
|
||||||
|
|
||||||
newParts = newParts.map((p, i) =>
|
newParts = newParts.map((p, i) =>
|
||||||
i === index
|
i === index
|
||||||
? {
|
? {
|
||||||
...p,
|
...p,
|
||||||
part: item,
|
part: item,
|
||||||
}
|
}
|
||||||
: p,
|
: p,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -435,7 +593,8 @@ const LevelGeneration = ({id}: Props) => {
|
|||||||
const exam = {
|
const exam = {
|
||||||
...generatedExam,
|
...generatedExam,
|
||||||
id,
|
id,
|
||||||
parts: generatedExam.parts.map((p, i) => ({...p, exercises: parts[i].part!.exercises})),
|
label: label,
|
||||||
|
parts: generatedExam.parts.map((p, i) => ({ ...p, exercises: parts[i].part!.exercises, category: parts[i].part?.category, intro: parts[i].part?.intro })),
|
||||||
};
|
};
|
||||||
|
|
||||||
axios
|
axios
|
||||||
@@ -466,7 +625,7 @@ const LevelGeneration = ({id}: Props) => {
|
|||||||
label: capitalize(x),
|
label: capitalize(x),
|
||||||
}))}
|
}))}
|
||||||
onChange={(value) => (value ? setDifficulty(value.value as Difficulty) : null)}
|
onChange={(value) => (value ? setDifficulty(value.value as Difficulty) : null)}
|
||||||
value={{value: difficulty, label: capitalize(difficulty)}}
|
value={{ value: difficulty, label: capitalize(difficulty) }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 w-1/3">
|
<div className="flex flex-col gap-3 w-1/3">
|
||||||
@@ -484,12 +643,24 @@ const LevelGeneration = ({id}: Props) => {
|
|||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col gap-3 w-full">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">Exam Label *</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Label"
|
||||||
|
name="label"
|
||||||
|
onChange={(e) => setLabel(e)}
|
||||||
|
roundness="xl"
|
||||||
|
defaultValue={label}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-level/20 p-1">
|
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-level/20 p-1">
|
||||||
{Array.from(Array(numberOfParts), (_, index) => index).map((index) => (
|
{Array.from(Array(numberOfParts), (_, index) => index).map((index) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={index}
|
key={index}
|
||||||
className={({selected}) =>
|
className={({ selected }) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-level/70",
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-level/70",
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-level focus:outline-none focus:ring-2",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-level focus:outline-none focus:ring-2",
|
||||||
@@ -505,6 +676,8 @@ const LevelGeneration = ({id}: Props) => {
|
|||||||
{Array.from(Array(numberOfParts), (_, index) => index).map((index) => (
|
{Array.from(Array(numberOfParts), (_, index) => index).map((index) => (
|
||||||
<TaskTab
|
<TaskTab
|
||||||
key={index}
|
key={index}
|
||||||
|
label={label}
|
||||||
|
index={index}
|
||||||
section={parts[index]}
|
section={parts[index]}
|
||||||
setSection={(part) => {
|
setSection={(part) => {
|
||||||
console.log(part);
|
console.log(part);
|
||||||
|
|||||||
Reference in New Issue
Block a user