import FillBlanksEdit from "@/components/Generation/fill.blanks.edit"; import MultipleChoiceEdit from "@/components/Generation/multiple.choice.edit"; import WriteBlankEdits from "@/components/Generation/write.blanks.edit"; import Checkbox from "@/components/Low/Checkbox"; import Input from "@/components/Low/Input"; import Select from "@/components/Low/Select"; import { Difficulty, LevelExam, MultipleChoiceExercise, MultipleChoiceQuestion, LevelPart, FillBlanksExercise, WriteBlanksExercise, Exercise, } from "@/interfaces/exam"; import useExamStore from "@/stores/examStore"; import {getExamById} from "@/utils/exams"; import {playSound} from "@/utils/sound"; import {Tab} from "@headlessui/react"; import axios from "axios"; import clsx from "clsx"; import {capitalize, sample} from "lodash"; import {useRouter} from "next/router"; import {useEffect, useState} from "react"; import {BsArrowRepeat, BsCheck, BsPencilSquare, BsX} from "react-icons/bs"; import reactStringReplace from "react-string-replace"; import {toast} from "react-toastify"; import {v4} from "uuid"; const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"]; const TYPES: {[key: string]: string} = { multiple_choice_4: "Multiple Choice", multiple_choice_blank_space: "Multiple Choice - Blank Space", multiple_choice_underlined: "Multiple Choice - Underlined", blank_space_text: "Blank Space", reading_passage_utas: "Reading Passage", }; type LevelSection = {type: string; quantity: number; topic?: string; part?: LevelPart}; const QuestionDisplay = ({question, onUpdate}: {question: MultipleChoiceQuestion; onUpdate: (question: MultipleChoiceQuestion) => void}) => { const [isEditing, setIsEditing] = useState(false); const [options, setOptions] = useState(question.options); const [answer, setAnswer] = useState(question.solution); const renderPrompt = (prompt: string) => { return reactStringReplace(prompt, /(()[\w\s']+(<\/u>))/g, (match) => { const word = match.replaceAll("", "").replaceAll("", ""); return word.length > 0 ? {word} : null; }); }; return (
<> {question.id}. {renderPrompt(question.prompt).filter((x) => x?.toString() !== "")}
{question.options.map((option, index) => ( setAnswer(option.id)}> ({option.id}) {" "} {isEditing ? ( setOptions((prev) => prev.map((x, idx) => (idx === index ? {...x, text: e.target.value} : x)))} /> ) : ( {option.text} )} ))}
{!isEditing && ( )} {isEditing && ( <> )}
); }; const TaskTab = ({section, setSection}: {section: LevelSection; setSection: (section: LevelSection) => void}) => { const [isLoading, setIsLoading] = useState(false); const onUpdate = (question: MultipleChoiceQuestion) => { if (!section) return; const updatedExam = { ...section, exercises: section.part?.exercises.map((x) => ({ ...x, questions: (x as MultipleChoiceExercise).questions.map((q) => (q.id === question.id ? question : q)), })), }; setSection(updatedExam as any); }; const renderExercise = (exercise: Exercise) => { if (exercise.type === "multipleChoice") return (
Multiple Choice {exercise.questions.length} questions
setSection({ ...section, part: {...section.part!, exercises: section.part!.exercises.map((x) => (x.id === exercise.id ? {...x, ...data} : x))}, }) } />
); if (exercise.type === "fillBlanks") return (
Fill Blanks
{exercise.prompt} setSection({ ...section, part: {...section.part!, exercises: section.part!.exercises.map((x) => (x.id === exercise.id ? {...x, ...data} : x))}, }) } />
); if (exercise.type === "writeBlanks") return (
Write Blanks
{exercise.prompt} setSection({ ...section, part: {...section.part!, exercises: section.part!.exercises.map((x) => (x.id === exercise.id ? {...x, ...data} : x))}, }) } />
); }; return (
setSection({...section, quantity: parseInt(v)})} value={section?.quantity || 10} />
{section?.type === "reading_passage_utas" && (
setSection({...section, topic: v})} value={section?.topic} />
)}
{isLoading && (
Generating...
)} {section?.part && (
{section.part.context &&
{section.part.context}
} {section.part.exercises.map(renderExercise)}
)}
); }; interface Props { id: string; } const LevelGeneration = ({id}: Props) => { const [generatedExam, setGeneratedExam] = useState(); const [isLoading, setIsLoading] = useState(false); const [resultingExam, setResultingExam] = useState(); const [timer, setTimer] = useState(10); const [difficulty, setDifficulty] = useState(sample(DIFFICULTIES)!); const [numberOfParts, setNumberOfParts] = useState(1); const [parts, setParts] = useState([{quantity: 10, type: "multiple_choice_4"}]); const [isPrivate, setPrivate] = useState(false); useEffect(() => { setParts((prev) => Array.from(Array(numberOfParts)).map((_, i) => (!!prev.at(i) ? prev.at(i)! : {quantity: 10, type: "multiple_choice_4"}))); }, [numberOfParts]); const router = useRouter(); const setExams = useExamStore((state) => state.setExams); const setSelectedModules = useExamStore((state) => state.setSelectedModules); const loadExam = async (examId: string) => { const exam = await getExamById("level", examId.trim()); if (!exam) { toast.error("Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID", { toastId: "invalid-exam-id", }); return; } setExams([exam]); setSelectedModules(["level"]); router.push("/exercises"); }; const generateExam = () => { if (parts.length === 0) return; setIsLoading(true); let body: any = {}; parts.forEach((part, index) => { body[`exercise_${index + 1}_type`] = part.type; body[`exercise_${index + 1}_qty`] = part.quantity; if (part.topic) body[`exercise_${index + 1}_topic`] = part.topic; if (part.type === "reading_passage_utas") { body[`exercise_${index + 1}_sa_qty`] = Math.floor(part.quantity / 2); body[`exercise_${index + 1}_mc_qty`] = Math.ceil(part.quantity / 2); } }); let newParts = [...parts]; axios .post<{exercises: {[key: string]: any}}>("/api/exam/level/generate/level", {nr_exercises: numberOfParts, ...body}) .then((result) => { console.log(result.data); playSound(typeof result.data === "string" ? "error" : "check"); if (typeof result.data === "string") return toast.error("Something went wrong, please try to generate again."); const exam: LevelExam = { id: v4(), minTimer: timer, module: "level", difficulty, variant: "full", isDiagnostic: false, private: isPrivate, parts: parts .map((part, index) => { const currentExercise = result.data.exercises[`exercise_${index + 1}`] as any; if ( part.type === "multiple_choice_4" || part.type === "multiple_choice_blank_space" || part.type === "multiple_choice_underlined" ) { const exercise: MultipleChoiceExercise = { id: v4(), prompt: part.type === "multiple_choice_underlined" ? "Select the wrong part of the sentence." : "Select the appropriate option.", questions: currentExercise.questions.map((x: any) => ({...x, variant: "text"})), type: "multipleChoice", userSolutions: [], }; const item = { exercises: [exercise], }; newParts = newParts.map((p, i) => i === index ? { ...p, part: item, } : p, ); return item; } if (part.type === "blank_space_text") { const exercise: WriteBlanksExercise = { id: v4(), prompt: "Complete the text below.", text: currentExercise.text, maxWords: 3, solutions: currentExercise.words.map((x: any) => ({id: x.id, solution: [x.text]})), type: "writeBlanks", userSolutions: [], }; const item = { exercises: [exercise], }; newParts = newParts.map((p, i) => i === index ? { ...p, part: item, } : p, ); return item; } const mcExercise: MultipleChoiceExercise = { id: v4(), prompt: "Select the appropriate option.", questions: currentExercise.exercises.multipleChoice.questions.map((x: any) => ({...x, variant: "text"})), type: "multipleChoice", userSolutions: [], }; const wbExercise: WriteBlanksExercise = { id: v4(), prompt: "Complete the notes below.", maxWords: 3, text: currentExercise.exercises.shortAnswer.map((x: any) => `${x.question} {{${x.id}}}`).join("\n"), solutions: currentExercise.exercises.shortAnswer.map((x: any) => ({ id: x.id, solution: x.possible_answers, })), type: "writeBlanks", userSolutions: [], }; const item = { context: currentExercise.text.content, exercises: [mcExercise, wbExercise], }; newParts = newParts.map((p, i) => i === index ? { ...p, part: item, } : p, ); return item; }) .filter((x) => !!x) as LevelPart[], }; setParts(newParts); setGeneratedExam(exam); }) .catch((error) => { console.log(error); playSound("error"); toast.error("Something went wrong, please try again later!"); }) .finally(() => setIsLoading(false)); }; const submitExam = () => { if (!generatedExam) { toast.error("Please generate all tasks before submitting"); return; } if (!id) { toast.error("Please insert a title before submitting"); return; } setIsLoading(true); const exam = { ...generatedExam, id, parts: generatedExam.parts.map((p, i) => ({...p, exercises: parts[i].part!.exercises})), }; axios .post(`/api/exam/level`, exam) .then((result) => { playSound("sent"); console.log(`Generated Exam ID: ${result.data.id}`); toast.success("This new exam has been generated successfully! Check the ID in our browser's console."); setResultingExam(result.data); setGeneratedExam(undefined); }) .catch((error) => { console.log(error); toast.error("Something went wrong while generating, please try again later."); }) .finally(() => setIsLoading(false)); }; return ( <>
setNumberOfParts(parseInt(v))} value={numberOfParts} />
setTimer(parseInt(v))} value={timer} />
Privacy (Only available for Assignments)
{Array.from(Array(numberOfParts), (_, index) => index).map((index) => ( clsx( "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", "transition duration-300 ease-in-out", selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-level", ) }> Part {index + 1} ))} {Array.from(Array(numberOfParts), (_, index) => index).map((index) => ( { console.log(part); setParts((prev) => prev.map((x, i) => (i === index ? part : x))); }} /> ))}
{resultingExam && ( )}
); }; export default LevelGeneration;