Merged in ENCOA-57_EditExamsGenerate (pull request #53)

ENCOA-57 EditExamsGenerate

Approved-by: Tiago Ribeiro
This commit is contained in:
João Ramos
2024-07-18 09:32:05 +00:00
committed by Tiago Ribeiro
12 changed files with 2290 additions and 1183 deletions

View File

@@ -0,0 +1,84 @@
import { FillBlanksExercise } from "@/interfaces/exam";
import React from "react";
import Input from "@/components/Low/Input";
interface Props {
exercise: FillBlanksExercise;
updateExercise: (data: any) => void;
}
const FillBlanksEdit = (props: Props) => {
const { exercise, updateExercise } = props;
return (
<>
<Input
type="text"
label="Prompt"
name="prompt"
required
value={exercise.prompt}
onChange={(value) =>
updateExercise({
prompt: value,
})
}
/>
<Input
type="text"
label="Text"
name="text"
required
value={exercise.text}
onChange={(value) =>
updateExercise({
text: value,
})
}
/>
<h1>Solutions</h1>
<div className="w-full flex flex-wrap -mx-2">
{exercise.solutions.map((solution, index) => (
<div key={solution.id} className="flex sm:w-1/2 lg:w-1/4 px-2">
<Input
type="text"
label={`Solution ${index + 1}`}
name="solution"
required
value={solution.solution}
onChange={(value) =>
updateExercise({
solutions: exercise.solutions.map((sol) =>
sol.id === solution.id ? { ...sol, solution: value } : sol
),
})
}
/>
</div>
))}
</div>
<h1>Words</h1>
<div className="w-full flex flex-wrap -mx-2">
{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={word}
onChange={(value) =>
updateExercise({
words: exercise.words.map((sol, idx) =>
index === idx ? value : sol
),
})
}
/>
</div>
))}
</div>
</>
);
};
export default FillBlanksEdit;

View File

@@ -0,0 +1,7 @@
import React from "react";
const InteractiveSpeakingEdit = () => {
return null;
};
export default InteractiveSpeakingEdit;

View File

@@ -0,0 +1,130 @@
import React from "react";
import { MatchSentencesExercise } from "@/interfaces/exam";
import Input from "@/components/Low/Input";
import Select from "@/components/Low/Select";
interface Props {
exercise: MatchSentencesExercise;
updateExercise: (data: any) => void;
}
const MatchSentencesEdit = (props: Props) => {
const { exercise, updateExercise } = props;
const selectOptions = exercise.options.map((option) => ({
value: option.id,
label: option.id,
}));
return (
<>
<Input
type="text"
label="Prompt"
name="prompt"
required
value={exercise.prompt}
onChange={(value) =>
updateExercise({
prompt: value,
})
}
/>
<h1>Solutions</h1>
<div className="w-full flex flex-wrap -mx-2">
{exercise.sentences.map((sentence, index) => (
<div key={sentence.id} className="flex flex-col w-full px-2">
<div className="flex w-full">
<Input
type="text"
label={`Sentence ${index + 1}`}
name="sentence"
required
value={sentence.sentence}
onChange={(value) =>
updateExercise({
sentences: exercise.sentences.map((iSol) =>
iSol.id === sentence.id
? {
...iSol,
sentence: value,
}
: iSol
),
})
}
className="px-2"
/>
<div className="w-48 flex items-end px-2">
<Select
value={selectOptions.find(
(o) => o.value === sentence.solution
)}
options={selectOptions}
onChange={(value) => {
updateExercise({
sentences: exercise.sentences.map((iSol) =>
iSol.id === sentence.id
? {
...iSol,
solution: value?.value,
}
: iSol
),
});
}}
/>
</div>
</div>
</div>
))}
<h1>Options</h1>
<div className="w-full flex flex-wrap -mx-2">
{exercise.options.map((option, index) => (
<div key={option.id} className="flex flex-col w-full px-2">
<div className="flex w-full">
<Input
type="text"
label={`Option ${index + 1}`}
name="option"
required
value={option.sentence}
onChange={(value) =>
updateExercise({
options: exercise.options.map((iSol) =>
iSol.id === option.id
? {
...iSol,
sentence: value,
}
: iSol
),
})
}
className="px-2"
/>
<div className="w-48 flex items-end px-2">
<Select
value={{
value: option.id,
label: option.id,
}}
options={[
{
value: option.id,
label: option.id,
},
]}
disabled
onChange={() => {}}
/>
</div>
</div>
</div>
))}
</div>
</div>
</>
);
};
export default MatchSentencesEdit;

View File

@@ -0,0 +1,137 @@
import React from "react";
import Input from "@/components/Low/Input";
import {
MultipleChoiceExercise,
MultipleChoiceQuestion,
} from "@/interfaces/exam";
import Select from "@/components/Low/Select";
interface Props {
exercise: MultipleChoiceExercise;
updateExercise: (data: any) => void;
}
const variantOptions = [
{ value: "text", label: "Text", key: "text" },
{ value: "image", label: "Image", key: "src" },
];
const MultipleChoiceEdit = (props: Props) => {
const { exercise, updateExercise } = props;
return (
<>
<h1>Questions</h1>
<div className="w-full flex-no-wrap -mx-2">
{exercise.questions.map((question: MultipleChoiceQuestion, index) => {
const variantValue = variantOptions.find(
(o) => o.value === question.variant
);
const solutionsOptions = question.options.map((option) => ({
value: option.id,
label: option.id,
}));
const solutionValue = solutionsOptions.find(
(o) => o.value === question.solution
);
return (
<div key={question.id} className="flex w-full px-2 flex-col">
<span>Question ID: {question.id}</span>
<Input
type="text"
label="Prompt"
name="prompt"
required
value={question.prompt}
onChange={(value) =>
updateExercise({
questions: exercise.questions.map((sol) =>
sol.id === question.id ? { ...sol, prompt: value } : sol
),
})
}
/>
<div className="flex w-full">
<div className="w-48 flex items-end px-2">
<Select
value={solutionValue}
options={solutionsOptions}
onChange={(value) => {
updateExercise({
questions: exercise.questions.map((sol) =>
sol.id === question.id
? { ...sol, solution: value?.value }
: sol
),
});
}}
/>
</div>
<div className="w-48 flex items-end px-2">
<Select
value={variantValue}
options={variantOptions}
onChange={(value) => {
updateExercise({
questions: exercise.questions.map((sol) =>
sol.id === question.id
? { ...sol, variant: value?.value }
: sol
),
});
}}
/>
</div>
</div>
<div className="flex w-full flex-wrap -mx-2">
{question.options.map((option) => (
<div
key={option.id}
className="flex sm:w-1/2 lg:w-1/4 px-2 px-2"
>
<Input
type="text"
label={`Option ${option.id}`}
name="option"
required
value={option.text}
onChange={(value) =>
updateExercise({
questions: exercise.questions.map((sol) =>
sol.id === question.id
? {
...sol,
options: sol.options.map((opt) => {
if (
opt.id === option.id &&
variantValue?.key
) {
return {
...opt,
[variantValue.key]: value,
};
}
return opt;
}),
}
: sol
),
})
}
/>
</div>
))}
</div>
</div>
);
})}
</div>
</>
);
};
export default MultipleChoiceEdit;

View File

@@ -0,0 +1,7 @@
import React from 'react';
const SpeakingEdit = () => {
return null;
}
export default SpeakingEdit;

View File

@@ -0,0 +1,71 @@
import React from "react";
import { TrueFalseExercise } from "@/interfaces/exam";
import Input from "@/components/Low/Input";
import Select from "@/components/Low/Select";
interface Props {
exercise: TrueFalseExercise;
updateExercise: (data: any) => void;
}
const options = [
{ value: "true", label: "True" },
{ value: "false", label: "False" },
{ value: "not_given", label: "Not Given" },
];
const TrueFalseEdit = (props: Props) => {
const { exercise, updateExercise } = props;
return (
<>
<Input
type="text"
label="Prompt"
name="prompt"
required
value={exercise.prompt}
onChange={(value) =>
updateExercise({
prompt: value,
})
}
/>
<h1>Questions</h1>
<div className="w-full flex-no-wrap -mx-2">
{exercise.questions.map((question, index) => (
<div key={question.id} className="flex w-full px-2">
<Input
type="text"
label={`Question ${index + 1}`}
name="question"
required
value={question.prompt}
onChange={(value) =>
updateExercise({
questions: exercise.questions.map((sol) =>
sol.id === question.id ? { ...sol, prompt: value } : sol
),
})
}
/>
<div className="w-48 flex items-end px-2">
<Select
value={options.find((o) => o.value === question.solution)}
options={options}
onChange={(value) => {
updateExercise({
questions: exercise.questions.map((sol) =>
sol.id === question.id ? { ...sol, solution: value?.value } : sol
),
});
}}
className="h-18"
/>
</div>
</div>
))}
</div>
</>
);
};
export default TrueFalseEdit;

View File

@@ -0,0 +1,94 @@
import React from "react";
import Input from "@/components/Low/Input";
import { WriteBlanksExercise } from "@/interfaces/exam";
interface Props {
exercise: WriteBlanksExercise;
updateExercise: (data: any) => void;
}
const WriteBlankEdits = (props: Props) => {
const { exercise, updateExercise } = props;
return (
<>
<Input
type="text"
label="Prompt"
name="prompt"
required
value={exercise.prompt}
onChange={(value) =>
updateExercise({
prompt: value,
})
}
/>
<Input
type="text"
label="Text"
name="text"
required
value={exercise.text}
onChange={(value) =>
updateExercise({
text: value,
})
}
/>
<Input
type="text"
label="Max Words"
name="number"
required
value={exercise.maxWords}
onChange={(value) =>
updateExercise({
maxWords: Number(value),
})
}
/>
<h1>Solutions</h1>
<div className="w-full flex flex-wrap -mx-2">
{exercise.solutions.map((solution) => (
<div key={solution.id} className="flex flex-col w-full px-2">
<span>Solution ID: {solution.id}</span>
{/* TODO: Consider adding an add and delete button */}
<div className="flex flex-wrap">
{solution.solution.map((sol, solIndex) => (
<Input
key={`${sol}-${solIndex}`}
type="text"
label={`Solution ${solIndex + 1}`}
name="solution"
required
value={sol}
onChange={(value) =>
updateExercise({
solutions: exercise.solutions.map((iSol) =>
iSol.id === solution.id
? {
...iSol,
solution: iSol.solution.map((iiSol, iiIndex) => {
if (iiIndex === solIndex) {
return value;
}
return iiSol;
}),
}
: iSol
),
})
}
className="sm:w-1/2 lg:w-1/4 px-2"
/>
))}
</div>
</div>
))}
</div>
</>
);
};
export default WriteBlankEdits;

View File

@@ -0,0 +1,7 @@
import React from 'react';
const WritingEdit = () => {
return null;
}
export default WritingEdit;

View File

@@ -1,294 +1,375 @@
import Select from "@/components/Low/Select"; import Select from "@/components/Low/Select";
import {Difficulty, LevelExam, MultipleChoiceExercise, MultipleChoiceQuestion} from "@/interfaces/exam"; import {
Difficulty,
LevelExam,
MultipleChoiceExercise,
MultipleChoiceQuestion,
LevelPart,
} 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 {useState} from "react"; import { useState } from "react";
import {BsArrowRepeat, BsCheck, BsPencilSquare, BsX} from "react-icons/bs"; import { BsArrowRepeat, BsCheck, BsPencilSquare, BsX } from "react-icons/bs";
import {toast} from "react-toastify"; import { toast } from "react-toastify";
import {v4} from "uuid"; import { v4 } from "uuid";
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"]; const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
const QuestionDisplay = ({question, onUpdate}: {question: MultipleChoiceQuestion; onUpdate: (question: MultipleChoiceQuestion) => void}) => { const QuestionDisplay = ({
const [isEditing, setIsEditing] = useState(false); question,
const [options, setOptions] = useState(question.options); onUpdate,
}: {
question: MultipleChoiceQuestion;
onUpdate: (question: MultipleChoiceQuestion) => void;
}) => {
const [isEditing, setIsEditing] = useState(false);
const [options, setOptions] = useState(question.options);
const [answer, setAnswer] = useState(question.solution);
return ( return (
<div key={question.id} className="flex flex-col gap-1"> <div key={question.id} className="flex flex-col gap-1">
<span className="font-semibold"> <span className="font-semibold">
{question.id}. {question.prompt}{" "} {question.id}. {question.prompt}{" "}
</span> </span>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
{question.options.map((option, index) => ( {question.options.map((option, index) => (
<span key={option.id} className={clsx(question.solution === option.id && "font-bold")}> <span
<span className={clsx("font-semibold", question.solution === option.id ? "text-mti-green-light" : "text-ielts-level")}> key={option.id}
({option.id}) className={clsx(answer === option.id && "font-bold")}
</span>{" "} >
{isEditing ? ( <span
<input className={clsx(
defaultValue={option.text} "font-semibold",
className="w-60" answer === option.id
onChange={(e) => setOptions((prev) => prev.map((x, idx) => (idx === index ? {...x, text: e.target.value} : x)))} ? "text-mti-green-light"
/> : "text-ielts-level"
) : ( )}
<span>{option.text}</span> onClick={() => setAnswer(option.id)}
)} >
</span> ({option.id})
))} </span>{" "}
</div> {isEditing ? (
<div className="flex gap-2 mt-2 w-full"> <input
{!isEditing && ( defaultValue={option.text}
<button className="w-60"
onClick={() => setIsEditing(true)} onChange={(e) =>
className="p-2 border border-neutral-300 bg-white rounded-xl hover:drop-shadow transition ease-in-out duration-300"> setOptions((prev) =>
<BsPencilSquare /> prev.map((x, idx) =>
</button> idx === index ? { ...x, text: e.target.value } : x
)} )
{isEditing && ( )
<> }
<button />
onClick={() => { ) : (
onUpdate({...question, options}); <span>{option.text}</span>
setIsEditing(false); )}
}} </span>
className="p-2 border border-neutral-300 bg-white rounded-xl hover:drop-shadow transition ease-in-out duration-300"> ))}
<BsCheck /> </div>
</button> <div className="flex gap-2 mt-2 w-full">
<button {!isEditing && (
onClick={() => setIsEditing(false)} <button
className="p-2 border border-neutral-300 bg-white rounded-xl hover:drop-shadow transition ease-in-out duration-300"> onClick={() => setIsEditing(true)}
<BsX /> className="p-2 border border-neutral-300 bg-white rounded-xl hover:drop-shadow transition ease-in-out duration-300"
</button> >
</> <BsPencilSquare />
)} </button>
</div> )}
</div> {isEditing && (
); <>
<button
onClick={() => {
onUpdate({ ...question, options, solution: answer });
setIsEditing(false);
}}
className="p-2 border border-neutral-300 bg-white rounded-xl hover:drop-shadow transition ease-in-out duration-300"
>
<BsCheck />
</button>
<button
onClick={() => setIsEditing(false)}
className="p-2 border border-neutral-300 bg-white rounded-xl hover:drop-shadow transition ease-in-out duration-300"
>
<BsX />
</button>
</>
)}
</div>
</div>
);
}; };
const TaskTab = ({exam, difficulty, setExam}: {exam?: LevelExam; difficulty: Difficulty; setExam: (exam: LevelExam) => void}) => { const TaskTab = ({
const [isLoading, setIsLoading] = useState(false); exam,
difficulty,
setExam,
}: {
exam?: LevelPart;
difficulty: Difficulty;
setExam: (exam: LevelPart) => void;
}) => {
const [isLoading, setIsLoading] = useState(false);
const generate = () => { const generate = () => {
const url = new URLSearchParams(); const url = new URLSearchParams();
url.append("difficulty", difficulty); url.append("difficulty", difficulty);
setIsLoading(true); setIsLoading(true);
axios axios
.get(`/api/exam/level/generate/level?${url.toString()}`) .get(`/api/exam/level/generate/level?${url.toString()}`)
.then((result) => { .then((result) => {
playSound(typeof result.data === "string" ? "error" : "check"); playSound(typeof result.data === "string" ? "error" : "check");
if (typeof result.data === "string") return toast.error("Something went wrong, please try to generate again."); if (typeof result.data === "string")
setExam(result.data); return toast.error(
}) "Something went wrong, please try to generate again."
.catch((error) => { );
console.log(error); setExam(result.data);
toast.error("Something went wrong!"); })
}) .catch((error) => {
.finally(() => setIsLoading(false)); console.log(error);
}; toast.error("Something went wrong!");
})
.finally(() => setIsLoading(false));
};
const onUpdate = (question: MultipleChoiceQuestion) => { const onUpdate = (question: MultipleChoiceQuestion) => {
if (!exam) return; if (!exam) return;
const updatedExam = { const updatedExam = {
...exam, ...exam,
parts: exam.parts.map((p) => exercises: exam.exercises.map((x) => ({
p.exercises.map((x) => ({ ...x,
...x, questions: (x as MultipleChoiceExercise).questions.map((q) =>
questions: (x as MultipleChoiceExercise).questions.map((q) => (q.id === question.id ? question : q)), q.id === question.id ? question : q
})), ),
), }),
}; ),
console.log(updatedExam); };
setExam(updatedExam as any); console.log(updatedExam);
}; setExam(updatedExam as any);
};
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 gap-4 items-end"> <div className="flex gap-4 items-end">
<button <button
onClick={generate} onClick={generate}
disabled={isLoading} disabled={isLoading}
className={clsx( className={clsx(
"bg-ielts-level/70 border border-ielts-level text-white w-full px-6 py-6 rounded-xl h-[70px]", "bg-ielts-level/70 border border-ielts-level text-white w-full px-6 py-6 rounded-xl h-[70px]",
"hover:bg-ielts-level disabled:bg-ielts-level/40 disabled:cursor-not-allowed", "hover:bg-ielts-level disabled:bg-ielts-level/40 disabled:cursor-not-allowed",
"transition ease-in-out duration-300", "transition ease-in-out duration-300"
)}> )}
{isLoading ? ( >
<div className="flex items-center justify-center"> {isLoading ? (
<BsArrowRepeat className="text-white animate-spin" size={25} /> <div className="flex items-center justify-center">
</div> <BsArrowRepeat className="text-white animate-spin" size={25} />
) : ( </div>
"Generate" ) : (
)} "Generate"
</button> )}
</div> </button>
{isLoading && ( </div>
<div className="w-fit h-fit mt-12 self-center animate-pulse flex flex-col gap-8 items-center"> {isLoading && (
<span className={clsx("loading loading-infinity w-32 text-ielts-level")} /> <div className="w-fit h-fit mt-12 self-center animate-pulse flex flex-col gap-8 items-center">
<span className={clsx("font-bold text-2xl text-ielts-level")}>Generating...</span> <span
</div> className={clsx("loading loading-infinity w-32 text-ielts-level")}
)} />
{exam && ( <span className={clsx("font-bold text-2xl text-ielts-level")}>
<div className="flex flex-col gap-2 w-full overflow-y-scroll scrollbar-hide h-full"> Generating...
{exam.parts </span>
.flatMap((x) => x.exercises) </div>
.filter((x) => x.type === "multipleChoice") )}
.map((ex) => { {exam && (
const exercise = ex as MultipleChoiceExercise; <div className="flex flex-col gap-2 w-full overflow-y-scroll scrollbar-hide h-full">
{exam.exercises
.filter((x) => x.type === "multipleChoice")
.map((ex) => {
const exercise = ex as MultipleChoiceExercise;
return ( return (
<div key={ex.id} className="w-full h-full flex flex-col gap-2"> <div key={ex.id} className="w-full h-full flex flex-col gap-2">
<div className="flex gap-2"> <div className="flex gap-2">
<span className="text-xl font-semibold">Multiple Choice</span> <span className="text-xl font-semibold">
<span className="rounded-xl bg-white border border-ielts-level p-1 px-4 w-fit"> Multiple Choice
{exercise.questions.length} questions </span>
</span> <span className="rounded-xl bg-white border border-ielts-level p-1 px-4 w-fit">
</div> {exercise.questions.length} questions
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> </span>
{exercise.questions.map((question) => ( </div>
<QuestionDisplay question={question} onUpdate={onUpdate} key={question.id} /> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
))} {exercise.questions.map((question) => (
</div> <QuestionDisplay
</div> question={question}
); onUpdate={onUpdate}
})} key={question.id}
</div> />
)} ))}
</Tab.Panel> </div>
); </div>
);
})}
</div>
)}
</Tab.Panel>
);
}; };
const LevelGeneration = () => { const LevelGeneration = () => {
const [generatedExam, setGeneratedExam] = useState<LevelExam>(); const [generatedExam, setGeneratedExam] = useState<LevelPart>();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [resultingExam, setResultingExam] = useState<LevelExam>(); const [resultingExam, setResultingExam] = useState<LevelExam>();
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!); const [difficulty, setDifficulty] = useState<Difficulty>(
sample(DIFFICULTIES)!
);
const router = useRouter(); const router = useRouter();
const setExams = useExamStore((state) => state.setExams); const setExams = useExamStore((state) => state.setExams);
const setSelectedModules = useExamStore((state) => state.setSelectedModules); const setSelectedModules = useExamStore((state) => state.setSelectedModules);
const loadExam = async (examId: string) => { const loadExam = async (examId: string) => {
const exam = await getExamById("level", examId.trim()); const exam = await getExamById("level", examId.trim());
if (!exam) { if (!exam) {
toast.error("Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID", { toast.error(
toastId: "invalid-exam-id", "Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID",
}); {
toastId: "invalid-exam-id",
}
);
return; return;
} }
setExams([exam]); setExams([exam]);
setSelectedModules(["level"]); setSelectedModules(["level"]);
router.push("/exercises"); router.push("/exercises");
}; };
const submitExam = () => { const submitExam = () => {
if (!generatedExam) { if (!generatedExam) {
toast.error("Please generate all tasks before submitting"); toast.error("Please generate all tasks before submitting");
return; return;
} }
setIsLoading(true); setIsLoading(true);
const exam: LevelExam = {
...generatedExam,
isDiagnostic: false,
minTimer: 25,
module: "level",
id: v4(),
};
axios const exam: LevelExam = {
.post(`/api/exam/level`, exam) isDiagnostic: false,
.then((result) => { minTimer: 25,
playSound("sent"); module: "level",
console.log(`Generated Exam ID: ${result.data.id}`); id: v4(),
toast.success("This new exam has been generated successfully! Check the ID in our browser's console."); parts: [generatedExam],
setResultingExam(result.data); };
setGeneratedExam(undefined); axios
}) .post(`/api/exam/level`, exam)
.catch((error) => { .then((result) => {
console.log(error); playSound("sent");
toast.error("Something went wrong while generating, please try again later."); console.log(`Generated Exam ID: ${result.data.id}`);
}) toast.success(
.finally(() => setIsLoading(false)); "This new exam has been generated successfully! Check the ID in our browser's console."
}; );
setResultingExam(result.data);
return ( setGeneratedExam(undefined);
<> })
<div className="flex gap-4 w-1/2"> .catch((error) => {
<div className="flex flex-col gap-3 w-full"> console.log(error);
<label className="font-normal text-base text-mti-gray-dim">Difficulty</label> toast.error(
<Select "Something went wrong while generating, please try again later."
options={DIFFICULTIES.map((x) => ({value: x, label: capitalize(x)}))} );
onChange={(value) => (value ? setDifficulty(value.value as Difficulty) : null)} })
value={{value: difficulty, label: capitalize(difficulty)}} .finally(() => setIsLoading(false));
/> };
</div>
</div> return (
<Tab.Group> <>
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-level/20 p-1"> <div className="flex gap-4 w-1/2">
<Tab <div className="flex flex-col gap-3 w-full">
className={({selected}) => <label className="font-normal text-base text-mti-gray-dim">
clsx( Difficulty
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-level/70", </label>
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-level focus:outline-none focus:ring-2", <Select
"transition duration-300 ease-in-out", options={DIFFICULTIES.map((x) => ({
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-level", value: x,
) label: capitalize(x),
}> }))}
Exam onChange={(value) =>
</Tab> value ? setDifficulty(value.value as Difficulty) : null
</Tab.List> }
<Tab.Panels> value={{ value: difficulty, label: capitalize(difficulty) }}
<TaskTab difficulty={difficulty} exam={generatedExam} setExam={setGeneratedExam} /> />
</Tab.Panels> </div>
</Tab.Group> </div>
<div className="w-full flex justify-end gap-4"> <Tab.Group>
{resultingExam && ( <Tab.List className="flex space-x-1 rounded-xl bg-ielts-level/20 p-1">
<button <Tab
disabled={isLoading} className={({ selected }) =>
onClick={() => loadExam(resultingExam.id)} clsx(
className={clsx( "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-level/70",
"bg-white border border-ielts-level text-ielts-level w-full max-w-[200px] rounded-xl h-[70px] self-end", "ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-level focus:outline-none focus:ring-2",
"hover:bg-ielts-level hover:text-white disabled:bg-ielts-level/40 disabled:cursor-not-allowed", "transition duration-300 ease-in-out",
"transition ease-in-out duration-300", selected
)}> ? "bg-white shadow"
Perform Exam : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-level"
</button> )
)} }
<button >
disabled={!generatedExam || isLoading} Exam
data-tip="Please generate all three passages" </Tab>
onClick={submitExam} </Tab.List>
className={clsx( <Tab.Panels>
"bg-ielts-level/70 border border-ielts-level text-white w-full max-w-[200px] rounded-xl h-[70px] self-end", <TaskTab
"hover:bg-ielts-level disabled:bg-ielts-level/40 disabled:cursor-not-allowed", difficulty={difficulty}
"transition ease-in-out duration-300", exam={generatedExam}
!generatedExam && "tooltip", setExam={setGeneratedExam}
)}> />
{isLoading ? ( </Tab.Panels>
<div className="flex items-center justify-center"> </Tab.Group>
<BsArrowRepeat className="text-white animate-spin" size={25} /> <div className="w-full flex justify-end gap-4">
</div> {resultingExam && (
) : ( <button
"Submit" disabled={isLoading}
)} onClick={() => loadExam(resultingExam.id)}
</button> className={clsx(
</div> "bg-white border border-ielts-level text-ielts-level w-full max-w-[200px] rounded-xl h-[70px] self-end",
</> "hover:bg-ielts-level hover:text-white disabled:bg-ielts-level/40 disabled:cursor-not-allowed",
); "transition ease-in-out duration-300"
)}
>
Perform Exam
</button>
)}
<button
disabled={!generatedExam || isLoading}
data-tip="Please generate all three passages"
onClick={submitExam}
className={clsx(
"bg-ielts-level/70 border border-ielts-level text-white w-full max-w-[200px] rounded-xl h-[70px] self-end",
"hover:bg-ielts-level disabled:bg-ielts-level/40 disabled:cursor-not-allowed",
"transition ease-in-out duration-300",
!generatedExam && "tooltip"
)}
>
{isLoading ? (
<div className="flex items-center justify-center">
<BsArrowRepeat className="text-white animate-spin" size={25} />
</div>
) : (
"Submit"
)}
</button>
</div>
</>
);
}; };
export default LevelGeneration; export default LevelGeneration;

View File

@@ -1,345 +1,526 @@
import MultipleChoiceEdit from "@/components/Generation/multiple.choice.edit";
import Input from "@/components/Low/Input"; import Input from "@/components/Low/Input";
import Select from "@/components/Low/Select"; import Select from "@/components/Low/Select";
import {Difficulty, Exercise, ListeningExam} from "@/interfaces/exam"; import { Difficulty, Exercise, ListeningExam } 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 {convertCamelCaseToReadable} from "@/utils/string"; import { convertCamelCaseToReadable } from "@/utils/string";
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, Dispatch, SetStateAction } from "react";
import {BsArrowRepeat, BsCheck} from "react-icons/bs"; import { BsArrowRepeat, BsCheck } from "react-icons/bs";
import {toast} from "react-toastify"; import { toast } from "react-toastify";
import WriteBlanksEdit from "@/components/Generation/write.blanks.edit";
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"]; const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
const MULTIPLE_CHOICE = { type: "multipleChoice", label: "Multiple Choice" };
const WRITE_BLANKS_QUESTIONS = {
type: "writeBlanksQuestions",
label: "Write the Blanks: Questions",
};
const WRITE_BLANKS_FILL = {
type: "writeBlanksFill",
label: "Write the Blanks: Fill",
};
const WRITE_BLANKS_FORM = {
type: "writeBlanksForm",
label: "Write the Blanks: Form",
};
const MULTIPLE_CHOICE_3 = {
type: "multipleChoice3Options",
label: "Multiple Choice",
};
const PartTab = ({ const PartTab = ({
part, part,
types, difficulty,
difficulty, availableTypes,
index, index,
setPart, setPart,
updatePart,
}: { }: {
part?: ListeningPart; part?: ListeningPart;
difficulty: Difficulty; difficulty: Difficulty;
types: string[]; availableTypes: { type: string; label: string }[];
index: number; index: number;
setPart: (part?: ListeningPart) => void; setPart: (part?: ListeningPart) => void;
updatePart: Dispatch<SetStateAction<ListeningPart | undefined>>;
}) => { }) => {
const [topic, setTopic] = useState(""); const [topic, setTopic] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [types, setTypes] = useState<string[]>([]);
const generate = () => { const generate = () => {
const url = new URLSearchParams(); const url = new URLSearchParams();
url.append("difficulty", difficulty); url.append("difficulty", difficulty);
if (topic) url.append("topic", topic); if (topic) url.append("topic", topic);
if (types) types.forEach((t) => url.append("exercises", t)); if (types) types.forEach((t) => url.append("exercises", t));
setPart(undefined); setPart(undefined);
setIsLoading(true); setIsLoading(true);
axios axios
.get(`/api/exam/listening/generate/listening_section_${index}${topic || types ? `?${url.toString()}` : ""}`) .get(
.then((result) => { `/api/exam/listening/generate/listening_section_${index}${
playSound(typeof result.data === "string" ? "error" : "check"); topic || types ? `?${url.toString()}` : ""
if (typeof result.data === "string") return toast.error("Something went wrong, please try to generate again."); }`
setPart(result.data); )
}) .then((result) => {
.catch((error) => { playSound(typeof result.data === "string" ? "error" : "check");
console.log(error); if (typeof result.data === "string")
toast.error("Something went wrong!"); return toast.error(
}) "Something went wrong, please try to generate again."
.finally(() => setIsLoading(false)); );
}; setPart(result.data);
})
.catch((error) => {
console.log(error);
toast.error("Something went wrong!");
})
.finally(() => setIsLoading(false));
};
return ( const renderExercises = () => {
<Tab.Panel className="w-full bg-ielts-listening/20 min-h-[600px] h-full rounded-xl p-6 flex flex-col gap-4"> return part?.exercises.map((exercise) => {
<div className="flex gap-4 items-end"> switch (exercise.type) {
<Input type="text" placeholder="Grand Canyon..." name="topic" label="Topic" onChange={setTopic} roundness="xl" defaultValue={topic} /> case "multipleChoice":
<button return (
onClick={generate} <>
disabled={isLoading || types.length === 0} <h1>Exercise: Multiple Choice</h1>
data-tip="The passage is currently being generated" <MultipleChoiceEdit
className={clsx( exercise={exercise}
"bg-ielts-listening/70 border border-ielts-listening text-white w-full max-w-[200px] rounded-xl h-[70px]", key={exercise.id}
"hover:bg-ielts-listening disabled:bg-ielts-listening/40 disabled:cursor-not-allowed", updateExercise={(data: any) =>
"transition ease-in-out duration-300", updatePart((part?: ListeningPart) => {
isLoading && "tooltip", if (part) {
)}> const exercises = part.exercises.map((x) =>
{isLoading ? ( x.id === exercise.id ? { ...x, ...data } : x
<div className="flex items-center justify-center"> ) as Exercise[];
<BsArrowRepeat className="text-white animate-spin" size={25} /> const updatedPart = {
</div> ...part,
) : ( exercises,
"Generate" } as ListeningPart;
)} return updatedPart;
</button> }
</div>
{isLoading && ( return part;
<div className="w-fit h-fit mt-12 self-center animate-pulse flex flex-col gap-8 items-center"> })
<span className={clsx("loading loading-infinity w-32 text-ielts-listening")} /> }
<span className={clsx("font-bold text-2xl text-ielts-listening")}>Generating...</span> />
</div> </>
)} );
{part && ( // TODO: This might be broken as they all returns the same
<div className="flex flex-col gap-2 w-full overflow-y-scroll scrollbar-hide"> case "writeBlanks":
<div className="flex gap-4"> return (
{part.exercises.map((x) => ( <>
<span className="rounded-xl bg-white border border-ielts-listening p-1 px-4" key={x.id}> <h1>Exercise: Write Blanks</h1>
{x.type && convertCamelCaseToReadable(x.type)} <WriteBlanksEdit
</span> exercise={exercise}
))} key={exercise.id}
</div> updateExercise={(data: any) => {
{typeof part.text === "string" && <span className="w-full h-96">{part.text.replaceAll("\n\n", " ")}</span>} updatePart((part?: ListeningPart) => {
{typeof part.text !== "string" && ( if (part) {
<div className="w-full h-96 flex flex-col gap-2"> return {
{part.text.conversation.map((x, index) => ( ...part,
<span key={index} className="flex gap-1"> exercises: part.exercises.map((x) =>
<span className="font-semibold">{x.name}:</span> x.id === exercise.id ? { ...x, ...data } : x
{x.text.replaceAll("\n\n", " ")} ),
</span> } as ListeningPart;
))} }
</div>
)} return part;
</div> });
)} }}
</Tab.Panel> />
); </>
);
default:
return null;
}
});
};
const toggleType = (type: string) =>
setTypes((prev) =>
prev.includes(type)
? [...prev.filter((x) => x !== type)]
: [...prev, type]
);
return (
<Tab.Panel className="w-full bg-ielts-listening/20 min-h-[600px] h-full rounded-xl p-6 flex flex-col gap-4">
<div className="flex flex-col gap-3">
<label className="font-normal text-base text-mti-gray-dim">
Exercises
</label>
<div className="flex flex-row -2xl:flex-wrap w-full gap-4 -md:justify-center justify-between">
{availableTypes.map((x) => (
<span
onClick={() => toggleType(x.type)}
key={x.type}
className={clsx(
"px-6 py-4 w-64 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
"transition duration-300 ease-in-out",
!types.includes(x.type)
? "bg-white border-mti-gray-platinum"
: "bg-ielts-listening/70 border-ielts-listening text-white"
)}
>
{x.label}
</span>
))}
</div>
</div>
<div className="flex gap-4 items-end">
<Input
type="text"
placeholder="Grand Canyon..."
name="topic"
label="Topic"
onChange={setTopic}
roundness="xl"
defaultValue={topic}
/>
<button
onClick={generate}
disabled={isLoading || types.length === 0}
data-tip="The passage is currently being generated"
className={clsx(
"bg-ielts-listening/70 border border-ielts-listening text-white w-full max-w-[200px] rounded-xl h-[70px]",
"hover:bg-ielts-listening disabled:bg-ielts-listening/40 disabled:cursor-not-allowed",
"transition ease-in-out duration-300",
isLoading && "tooltip"
)}
>
{isLoading ? (
<div className="flex items-center justify-center">
<BsArrowRepeat className="text-white animate-spin" size={25} />
</div>
) : (
"Generate"
)}
</button>
</div>
{isLoading && (
<div className="w-fit h-fit mt-12 self-center animate-pulse flex flex-col gap-8 items-center">
<span
className={clsx(
"loading loading-infinity w-32 text-ielts-listening"
)}
/>
<span className={clsx("font-bold text-2xl text-ielts-listening")}>
Generating...
</span>
</div>
)}
{part && (
<>
<div className="flex flex-col gap-2 w-full overflow-y-scroll scrollbar-hide">
<div className="flex gap-4">
{part.exercises.map((x) => (
<span
className="rounded-xl bg-white border border-ielts-listening p-1 px-4"
key={x.id}
>
{x.type && convertCamelCaseToReadable(x.type)}
</span>
))}
</div>
{typeof part.text === "string" && (
<span className="w-full h-96">
{part.text.replaceAll("\n\n", " ")}
</span>
)}
{typeof part.text !== "string" && (
<div className="w-full h-96 flex flex-col gap-2">
{part.text.conversation.map((x, index) => (
<span key={index} className="flex gap-1">
<span className="font-semibold">{x.name}:</span>
{x.text.replaceAll("\n\n", " ")}
</span>
))}
</div>
)}
</div>
{renderExercises()}
</>
)}
</Tab.Panel>
);
}; };
interface ListeningPart { interface ListeningPart {
exercises: Exercise[]; exercises: Exercise[];
text: text:
| { | {
conversation: { conversation: {
gender: string; gender: string;
name: string; name: string;
text: string; text: string;
voice: string; voice: string;
}[]; }[];
} }
| string; | string;
} }
const ListeningGeneration = () => { const ListeningGeneration = () => {
const [part1, setPart1] = useState<ListeningPart>(); const [part1, setPart1] = useState<ListeningPart>();
const [part2, setPart2] = useState<ListeningPart>(); const [part2, setPart2] = useState<ListeningPart>();
const [part3, setPart3] = useState<ListeningPart>(); const [part3, setPart3] = useState<ListeningPart>();
const [part4, setPart4] = useState<ListeningPart>(); const [part4, setPart4] = useState<ListeningPart>();
const [minTimer, setMinTimer] = useState(30); const [minTimer, setMinTimer] = useState(30);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [resultingExam, setResultingExam] = useState<ListeningExam>(); const [resultingExam, setResultingExam] = useState<ListeningExam>();
const [types, setTypes] = useState<string[]>([]); const [difficulty, setDifficulty] = useState<Difficulty>(
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!); sample(DIFFICULTIES)!
);
useEffect(() => { useEffect(() => {
const part1Timer = part1 ? 5 : 0; const part1Timer = part1 ? 5 : 0;
const part2Timer = part2 ? 8 : 0; const part2Timer = part2 ? 8 : 0;
const part3Timer = part3 ? 8 : 0; const part3Timer = part3 ? 8 : 0;
const part4Timer = part4 ? 9 : 0; const part4Timer = part4 ? 9 : 0;
const sum = part1Timer + part2Timer + part3Timer + part4Timer; const sum = part1Timer + part2Timer + part3Timer + part4Timer;
setMinTimer(sum > 0 ? sum : 5); setMinTimer(sum > 0 ? sum : 5);
}, [part1, part2, part3, part4]); }, [part1, part2, part3, part4]);
const availableTypes = [ const router = useRouter();
{type: "multipleChoice", label: "Multiple Choice"},
{type: "writeBlanksQuestions", label: "Write the Blanks: Questions"},
{type: "writeBlanksFill", label: "Write the Blanks: Fill"},
{type: "writeBlanksForm", label: "Write the Blanks: Form"},
];
const router = useRouter(); const setExams = useExamStore((state) => state.setExams);
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
const setExams = useExamStore((state) => state.setExams); const submitExam = () => {
const setSelectedModules = useExamStore((state) => state.setSelectedModules); const parts = [part1, part2, part3, part4].filter((x) => !!x);
console.log({ parts });
if (parts.length === 0)
return toast.error("Please generate at least one section!");
const toggleType = (type: string) => setTypes((prev) => (prev.includes(type) ? [...prev.filter((x) => x !== type)] : [...prev, type])); setIsLoading(true);
const submitExam = () => { axios
const parts = [part1, part2, part3, part4].filter((x) => !!x); .post(`/api/exam/listening/generate/listening`, {
console.log({parts}); parts,
if (parts.length === 0) return toast.error("Please generate at least one section!"); minTimer,
difficulty,
})
.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);
setIsLoading(true); setPart1(undefined);
setPart2(undefined);
setPart3(undefined);
setPart4(undefined);
setDifficulty(sample(DIFFICULTIES)!);
})
.catch((error) => {
console.log(error);
toast.error("Something went wrong!");
})
.finally(() => setIsLoading(false));
};
axios const loadExam = async (examId: string) => {
.post(`/api/exam/listening/generate/listening`, {parts, minTimer, difficulty}) const exam = await getExamById("listening", examId.trim());
.then((result) => { if (!exam) {
playSound("sent"); toast.error(
console.log(`Generated Exam ID: ${result.data.id}`); "Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID",
toast.success("This new exam has been generated successfully! Check the ID in our browser's console."); {
setResultingExam(result.data); toastId: "invalid-exam-id",
}
);
setPart1(undefined); return;
setPart2(undefined); }
setPart3(undefined);
setPart4(undefined);
setDifficulty(sample(DIFFICULTIES)!);
setTypes([]);
})
.catch((error) => {
console.log(error);
toast.error("Something went wrong!");
})
.finally(() => setIsLoading(false));
};
const loadExam = async (examId: string) => { setExams([exam]);
const exam = await getExamById("listening", examId.trim()); setSelectedModules(["listening"]);
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; router.push("/exercises");
} };
setExams([exam]); return (
setSelectedModules(["listening"]); <>
<div className="flex gap-4 w-1/2">
router.push("/exercises"); <div className="flex flex-col gap-3">
}; <label className="font-normal text-base text-mti-gray-dim">
Timer
return ( </label>
<> <Input
<div className="flex gap-4 w-1/2"> type="number"
<div className="flex flex-col gap-3"> name="minTimer"
<label className="font-normal text-base text-mti-gray-dim">Timer</label> onChange={(e) => setMinTimer(parseInt(e) < 15 ? 15 : parseInt(e))}
<Input value={minTimer}
type="number" className="max-w-[300px]"
name="minTimer" />
onChange={(e) => setMinTimer(parseInt(e) < 15 ? 15 : parseInt(e))} </div>
value={minTimer} <div className="flex flex-col gap-3 w-full">
className="max-w-[300px]" <label className="font-normal text-base text-mti-gray-dim">
/> Difficulty
</div> </label>
<div className="flex flex-col gap-3 w-full"> <Select
<label className="font-normal text-base text-mti-gray-dim">Difficulty</label> options={DIFFICULTIES.map((x) => ({
<Select value: x,
options={DIFFICULTIES.map((x) => ({value: x, label: capitalize(x)}))} label: capitalize(x),
onChange={(value) => (value ? setDifficulty(value.value as Difficulty) : null)} }))}
value={{value: difficulty, label: capitalize(difficulty)}} onChange={(value) =>
disabled={!!part1 || !!part2 || !!part3 || !!part4} value ? setDifficulty(value.value as Difficulty) : null
/> }
</div> value={{ value: difficulty, label: capitalize(difficulty) }}
</div> disabled={!!part1 || !!part2 || !!part3 || !!part4}
/>
<div className="flex flex-col gap-3"> </div>
<label className="font-normal text-base text-mti-gray-dim">Exercises</label> </div>
<div className="flex flex-row -2xl:flex-wrap w-full gap-4 -md:justify-center justify-between"> <Tab.Group>
{availableTypes.map((x) => ( <Tab.List className="flex space-x-1 rounded-xl bg-ielts-listening/20 p-1">
<span <Tab
onClick={() => toggleType(x.type)} className={({ selected }) =>
key={x.type} clsx(
className={clsx( "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-listening/70 flex gap-2 items-center justify-center",
"px-6 py-4 w-64 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer", "ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-listening focus:outline-none focus:ring-2",
"transition duration-300 ease-in-out", "transition duration-300 ease-in-out",
!types.includes(x.type) selected
? "bg-white border-mti-gray-platinum" ? "bg-white shadow"
: "bg-ielts-listening/70 border-ielts-listening text-white", : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening"
)}> )
{x.label} }
</span> >
))} Section 1 {part1 && <BsCheck />}
</div> </Tab>
</div> <Tab
className={({ selected }) =>
<Tab.Group> clsx(
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-listening/20 p-1"> "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-listening/70 flex gap-2 items-center justify-center",
<Tab "ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-listening focus:outline-none focus:ring-2",
className={({selected}) => "transition duration-300 ease-in-out",
clsx( selected
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-listening/70 flex gap-2 items-center justify-center", ? "bg-white shadow"
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-listening focus:outline-none focus:ring-2", : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening"
"transition duration-300 ease-in-out", )
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening", }
) >
}> Section 2 {part2 && <BsCheck />}
Section 1 {part1 && <BsCheck />} </Tab>
</Tab> <Tab
<Tab className={({ selected }) =>
className={({selected}) => clsx(
clsx( "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-listening/70 flex gap-2 items-center justify-center",
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-listening/70 flex gap-2 items-center justify-center", "ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-listening focus:outline-none focus:ring-2",
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-listening focus:outline-none focus:ring-2", "transition duration-300 ease-in-out",
"transition duration-300 ease-in-out", selected
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening", ? "bg-white shadow"
) : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening"
}> )
Section 2 {part2 && <BsCheck />} }
</Tab> >
<Tab Section 3 {part3 && <BsCheck />}
className={({selected}) => </Tab>
clsx( <Tab
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-listening/70 flex gap-2 items-center justify-center", className={({ selected }) =>
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-listening focus:outline-none focus:ring-2", clsx(
"transition duration-300 ease-in-out", "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-listening/70 flex gap-2 items-center justify-center",
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening", "ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-listening focus:outline-none focus:ring-2",
) "transition duration-300 ease-in-out",
}> selected
Section 3 {part3 && <BsCheck />} ? "bg-white shadow"
</Tab> : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening"
<Tab )
className={({selected}) => }
clsx( >
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-listening/70 flex gap-2 items-center justify-center", Section 4 {part4 && <BsCheck />}
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-listening focus:outline-none focus:ring-2", </Tab>
"transition duration-300 ease-in-out", </Tab.List>
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening", <Tab.Panels>
) {[
}> {
Section 4 {part4 && <BsCheck />} part: part1,
</Tab> setPart: setPart1,
</Tab.List> types: [
<Tab.Panels> MULTIPLE_CHOICE,
{[ WRITE_BLANKS_QUESTIONS,
{part: part1, setPart: setPart1}, WRITE_BLANKS_FILL,
{part: part2, setPart: setPart2}, WRITE_BLANKS_FORM,
{part: part3, setPart: setPart3}, ],
{part: part4, setPart: setPart4}, },
].map(({part, setPart}, index) => ( {
<PartTab part={part} difficulty={difficulty} types={types} index={index + 1} key={index} setPart={setPart} /> part: part2,
))} setPart: setPart2,
</Tab.Panels> types: [MULTIPLE_CHOICE, WRITE_BLANKS_QUESTIONS],
</Tab.Group> },
<div className="w-full flex justify-end gap-4"> {
{resultingExam && ( part: part3,
<button setPart: setPart3,
disabled={isLoading} types: [MULTIPLE_CHOICE_3, WRITE_BLANKS_QUESTIONS],
onClick={() => loadExam(resultingExam.id)} },
className={clsx( {
"bg-white border border-ielts-listening text-ielts-listening w-full max-w-[200px] rounded-xl h-[70px] self-end", part: part4,
"hover:bg-ielts-listening hover:text-white disabled:bg-ielts-listening/40 disabled:cursor-not-allowed", setPart: setPart4,
"transition ease-in-out duration-300", types: [
)}> MULTIPLE_CHOICE,
Perform Exam WRITE_BLANKS_QUESTIONS,
</button> WRITE_BLANKS_FILL,
)} WRITE_BLANKS_FORM,
<button ],
disabled={(!part1 && !part2 && !part3 && !part4) || isLoading} },
data-tip="Please generate all three passages" ].map(({ part, setPart, types }, index) => (
onClick={submitExam} <PartTab
className={clsx( part={part}
"bg-ielts-listening/70 border border-ielts-listening text-white w-full max-w-[200px] rounded-xl h-[70px] self-end", difficulty={difficulty}
"hover:bg-ielts-listening disabled:bg-ielts-listening/40 disabled:cursor-not-allowed", availableTypes={types}
"transition ease-in-out duration-300", index={index + 1}
!part1 && !part2 && !part3 && !part4 && "tooltip", key={index}
)}> setPart={setPart}
{isLoading ? ( updatePart={setPart}
<div className="flex items-center justify-center"> />
<BsArrowRepeat className="text-white animate-spin" size={25} /> ))}
</div> </Tab.Panels>
) : ( </Tab.Group>
"Submit" <div className="w-full flex justify-end gap-4">
)} {resultingExam && (
</button> <button
</div> disabled={isLoading}
</> onClick={() => loadExam(resultingExam.id)}
); className={clsx(
"bg-white border border-ielts-listening text-ielts-listening w-full max-w-[200px] rounded-xl h-[70px] self-end",
"hover:bg-ielts-listening hover:text-white disabled:bg-ielts-listening/40 disabled:cursor-not-allowed",
"transition ease-in-out duration-300"
)}
>
Perform Exam
</button>
)}
<button
disabled={(!part1 && !part2 && !part3 && !part4) || isLoading}
data-tip="Please generate all three passages"
onClick={submitExam}
className={clsx(
"bg-ielts-listening/70 border border-ielts-listening text-white w-full max-w-[200px] rounded-xl h-[70px] self-end",
"hover:bg-ielts-listening disabled:bg-ielts-listening/40 disabled:cursor-not-allowed",
"transition ease-in-out duration-300",
!part1 && !part2 && !part3 && !part4 && "tooltip"
)}
>
{isLoading ? (
<div className="flex items-center justify-center">
<BsArrowRepeat className="text-white animate-spin" size={25} />
</div>
) : (
"Submit"
)}
</button>
</div>
</>
);
}; };
export default ListeningGeneration; export default ListeningGeneration;

View File

@@ -1,315 +1,498 @@
import Input from "@/components/Low/Input"; import Input from "@/components/Low/Input";
import Select from "@/components/Low/Select"; import Select from "@/components/Low/Select";
import {Difficulty, ReadingExam, ReadingPart} from "@/interfaces/exam"; import {
Difficulty,
Exercise,
ReadingExam,
ReadingPart,
} 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 {convertCamelCaseToReadable} from "@/utils/string"; import { convertCamelCaseToReadable } from "@/utils/string";
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, Dispatch, SetStateAction } from "react";
import {BsArrowRepeat, BsCheck} from "react-icons/bs"; import { BsArrowRepeat, BsCheck } from "react-icons/bs";
import {toast} from "react-toastify"; import { toast } from "react-toastify";
import {v4} from "uuid"; import { v4 } from "uuid";
import FillBlanksEdit from "@/components/Generation/fill.blanks.edit";
import TrueFalseEdit from "@/components/Generation/true.false.edit";
import WriteBlanksEdit from "@/components/Generation/write.blanks.edit";
import MatchSentencesEdit from "@/components/Generation/match.sentences.edit";
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"]; const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
const availableTypes = [
{ type: "fillBlanks", label: "Fill the Blanks" },
{ type: "trueFalse", label: "True or False" },
{ type: "writeBlanks", label: "Write the Blanks" },
{ type: "paragraphMatch", label: "Match Sentences" },
];
const PartTab = ({ const PartTab = ({
part, part,
types, difficulty,
difficulty, index,
index, setPart,
setPart, updatePart,
}: { }: {
part?: ReadingPart; part?: ReadingPart;
types: string[]; index: number;
index: number; difficulty: Difficulty;
difficulty: Difficulty; setPart: (part?: ReadingPart) => void;
setPart: (part?: ReadingPart) => void; updatePart: Dispatch<SetStateAction<ReadingPart | undefined>>;
// updatePart: (updater: (part: ReadingPart) => ReadingPart) => void;
}) => { }) => {
const [topic, setTopic] = useState(""); const [topic, setTopic] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [types, setTypes] = useState<string[]>([]);
const generate = () => { const toggleType = (type: string) =>
const url = new URLSearchParams(); setTypes((prev) =>
url.append("difficulty", difficulty); prev.includes(type)
? [...prev.filter((x) => x !== type)]
: [...prev, type]
);
if (topic) url.append("topic", topic); const generate = () => {
if (types) types.forEach((t) => url.append("exercises", t)); const url = new URLSearchParams();
url.append("difficulty", difficulty);
setPart(undefined); if (topic) url.append("topic", topic);
setIsLoading(true); if (types) types.forEach((t) => url.append("exercises", t));
axios
.get(`/api/exam/reading/generate/reading_passage_${index}${topic || types ? `?${url.toString()}` : ""}`)
.then((result) => {
playSound(typeof result.data === "string" ? "error" : "check");
if (typeof result.data === "string") return toast.error("Something went wrong, please try to generate again.");
setPart(result.data);
})
.catch((error) => {
console.log(error);
toast.error("Something went wrong!");
})
.finally(() => setIsLoading(false));
};
return ( setPart(undefined);
<Tab.Panel className="w-full bg-ielts-reading/20 min-h-[600px] h-full rounded-xl p-6 flex flex-col gap-4"> setIsLoading(true);
<div className="flex gap-4 items-end"> axios
<Input type="text" placeholder="Grand Canyon..." name="topic" label="Topic" onChange={setTopic} roundness="xl" defaultValue={topic} /> .get(
<button `/api/exam/reading/generate/reading_passage_${index}${
onClick={generate} topic || types ? `?${url.toString()}` : ""
disabled={isLoading || types.length === 0} }`
data-tip="The passage is currently being generated" )
className={clsx( .then((result) => {
"bg-ielts-reading/70 border border-ielts-reading text-white w-full max-w-[200px] rounded-xl h-[70px]", playSound(typeof result.data === "string" ? "error" : "check");
"hover:bg-ielts-reading disabled:bg-ielts-reading/40 disabled:cursor-not-allowed", if (typeof result.data === "string")
"transition ease-in-out duration-300", return toast.error(
isLoading && "tooltip", "Something went wrong, please try to generate again."
)}> );
{isLoading ? ( setPart(result.data);
<div className="flex items-center justify-center"> })
<BsArrowRepeat className="text-white animate-spin" size={25} /> .catch((error) => {
</div> console.log(error);
) : ( toast.error("Something went wrong!");
"Generate" })
)} .finally(() => setIsLoading(false));
</button> };
</div>
{isLoading && ( const renderExercises = () => {
<div className="w-fit h-fit mt-12 self-center animate-pulse flex flex-col gap-8 items-center"> return part?.exercises.map((exercise) => {
<span className={clsx("loading loading-infinity w-32 text-ielts-reading")} /> switch (exercise.type) {
<span className={clsx("font-bold text-2xl text-ielts-reading")}>Generating...</span> case "fillBlanks":
</div> return (
)} <>
{part && ( <h1>Exercise: Fill Blanks</h1>
<div className="flex flex-col gap-2 w-full overflow-y-scroll scrollbar-hide"> <FillBlanksEdit
<div className="flex gap-4"> exercise={exercise}
{part.exercises.map((x) => ( key={exercise.id}
<span className="rounded-xl bg-white border border-ielts-reading p-1 px-4" key={x.id}> updateExercise={(data: any) =>
{x.type && convertCamelCaseToReadable(x.type)} updatePart((part?: ReadingPart) => {
</span> if (part) {
))} const exercises = part.exercises.map((x) =>
</div> x.id === exercise.id ? { ...x, ...data } : x
<h3 className="text-xl font-semibold">{part.text.title}</h3> ) as Exercise[];
<span className="w-full h-96">{part.text.content}</span> const updatedPart = { ...part, exercises } as ReadingPart;
</div> return updatedPart;
)} }
</Tab.Panel>
); return part;
})
}
/>
</>
);
case "trueFalse":
return (
<>
<h1>Exercise: True or False</h1>
<TrueFalseEdit
exercise={exercise}
key={exercise.id}
updateExercise={(data: any) => {
updatePart((part?: ReadingPart) => {
if (part) {
return {
...part,
exercises: part.exercises.map((x) =>
x.id === exercise.id ? { ...x, ...data } : x
),
} as ReadingPart;
}
return part;
});
}}
/>
</>
);
case "writeBlanks":
return (
<>
<h1>Exercise: Write Blanks</h1>
<WriteBlanksEdit
exercise={exercise}
key={exercise.id}
updateExercise={(data: any) => {
updatePart((part?: ReadingPart) => {
if (part) {
return {
...part,
exercises: part.exercises.map((x) =>
x.id === exercise.id ? { ...x, ...data } : x
),
} as ReadingPart;
}
return part;
});
}}
/>
</>
);
case "matchSentences":
return (
<>
<h1>Exercise: Match Sentences</h1>
<MatchSentencesEdit
exercise={exercise}
key={exercise.id}
updateExercise={(data: any) => {
updatePart((part?: ReadingPart) => {
if (part) {
return {
...part,
exercises: part.exercises.map((x) =>
x.id === exercise.id ? { ...x, ...data } : x
),
} as ReadingPart;
}
return part;
});
}}
/>
</>
);
default:
return null;
}
});
};
return (
<Tab.Panel className="w-full bg-ielts-reading/20 min-h-[600px] h-full rounded-xl p-6 flex flex-col gap-4">
<div className="flex flex-col gap-3">
<label className="font-normal text-base text-mti-gray-dim">
Exercises
</label>
<div className="flex flex-row -2xl:flex-wrap w-full gap-4 -md:justify-center justify-between">
{availableTypes.map((x) => (
<span
onClick={() => toggleType(x.type)}
key={x.type}
className={clsx(
"px-6 py-4 w-64 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
"transition duration-300 ease-in-out",
!types.includes(x.type)
? "bg-white border-mti-gray-platinum"
: "bg-ielts-reading/70 border-ielts-reading text-white"
)}
>
{x.label}
</span>
))}
</div>
</div>
<div className="flex gap-4 items-end">
<Input
type="text"
placeholder="Grand Canyon..."
name="topic"
label="Topic"
onChange={setTopic}
roundness="xl"
defaultValue={topic}
/>
<button
onClick={generate}
disabled={isLoading || types.length === 0}
data-tip="The passage is currently being generated"
className={clsx(
"bg-ielts-reading/70 border border-ielts-reading text-white w-full max-w-[200px] rounded-xl h-[70px]",
"hover:bg-ielts-reading disabled:bg-ielts-reading/40 disabled:cursor-not-allowed",
"transition ease-in-out duration-300",
isLoading && "tooltip"
)}
>
{isLoading ? (
<div className="flex items-center justify-center">
<BsArrowRepeat className="text-white animate-spin" size={25} />
</div>
) : (
"Generate"
)}
</button>
</div>
{isLoading && (
<div className="w-fit h-fit mt-12 self-center animate-pulse flex flex-col gap-8 items-center">
<span
className={clsx("loading loading-infinity w-32 text-ielts-reading")}
/>
<span className={clsx("font-bold text-2xl text-ielts-reading")}>
Generating...
</span>
</div>
)}
{part && (
<>
<div className="flex flex-col gap-2 w-full overflow-y-scroll scrollbar-hide">
<div className="flex gap-4">
{part.exercises.map((x) => (
<span
className="rounded-xl bg-white border border-ielts-reading p-1 px-4"
key={x.id}
>
{x.type && convertCamelCaseToReadable(x.type)}
</span>
))}
</div>
<h3 className="text-xl font-semibold">{part.text.title}</h3>
<span className="w-full h-96">{part.text.content}</span>
</div>
{renderExercises()}
</>
)}
</Tab.Panel>
);
}; };
const ReadingGeneration = () => { const ReadingGeneration = () => {
const [part1, setPart1] = useState<ReadingPart>(); const [part1, setPart1] = useState<ReadingPart>();
const [part2, setPart2] = useState<ReadingPart>(); const [part2, setPart2] = useState<ReadingPart>();
const [part3, setPart3] = useState<ReadingPart>(); const [part3, setPart3] = useState<ReadingPart>();
const [minTimer, setMinTimer] = useState(60); const [minTimer, setMinTimer] = useState(60);
const [types, setTypes] = useState<string[]>([]); const [isLoading, setIsLoading] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [resultingExam, setResultingExam] = useState<ReadingExam>();
const [resultingExam, setResultingExam] = useState<ReadingExam>(); const [difficulty, setDifficulty] = useState<Difficulty>(
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!); sample(DIFFICULTIES)!
);
useEffect(() => { useEffect(() => {
const parts = [part1, part2, part3].filter((x) => !!x); const parts = [part1, part2, part3].filter((x) => !!x);
setMinTimer(parts.length === 0 ? 60 : parts.length * 20); setMinTimer(parts.length === 0 ? 60 : parts.length * 20);
}, [part1, part2, part3]); }, [part1, part2, part3]);
const router = useRouter(); const router = useRouter();
const setExams = useExamStore((state) => state.setExams); const setExams = useExamStore((state) => state.setExams);
const setSelectedModules = useExamStore((state) => state.setSelectedModules); const setSelectedModules = useExamStore((state) => state.setSelectedModules);
const availableTypes = [ const loadExam = async (examId: string) => {
{type: "fillBlanks", label: "Fill the Blanks"}, const exam = await getExamById("reading", examId.trim());
{type: "trueFalse", label: "True or False"}, if (!exam) {
{type: "writeBlanks", label: "Write the Blanks"}, toast.error(
{type: "matchSentences", label: "Match Sentences"}, "Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID",
]; {
toastId: "invalid-exam-id",
}
);
const toggleType = (type: string) => setTypes((prev) => (prev.includes(type) ? [...prev.filter((x) => x !== type)] : [...prev, type])); return;
}
const loadExam = async (examId: string) => { setExams([exam]);
const exam = await getExamById("reading", examId.trim()); setSelectedModules(["reading"]);
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; router.push("/exercises");
} };
setExams([exam]); const submitExam = () => {
setSelectedModules(["reading"]); const parts = [part1, part2, part3].filter((x) => !!x) as ReadingPart[];
if (parts.length === 0) {
toast.error("Please generate at least one passage before submitting");
return;
}
router.push("/exercises"); setIsLoading(true);
}; const exam: ReadingExam = {
parts,
isDiagnostic: false,
minTimer,
module: "reading",
id: v4(),
type: "academic",
variant: parts.length === 3 ? "full" : "partial",
difficulty,
};
const submitExam = () => { axios
const parts = [part1, part2, part3].filter((x) => !!x) as ReadingPart[]; .post(`/api/exam/reading`, exam)
if (parts.length === 0) { .then((result) => {
toast.error("Please generate at least one passage before submitting"); playSound("sent");
return; 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);
setIsLoading(true); setPart1(undefined);
const exam: ReadingExam = { setPart2(undefined);
parts, setPart3(undefined);
isDiagnostic: false, setDifficulty(sample(DIFFICULTIES)!);
minTimer, setMinTimer(60);
module: "reading", })
id: v4(), .catch((error) => {
type: "academic", console.log(error);
variant: parts.length === 3 ? "full" : "partial", toast.error(
difficulty, "Something went wrong while generating, please try again later."
}; );
})
.finally(() => setIsLoading(false));
};
axios return (
.post(`/api/exam/reading`, exam) <>
.then((result) => { <div className="flex gap-4 w-1/2">
playSound("sent"); <div className="flex flex-col gap-3">
console.log(`Generated Exam ID: ${result.data.id}`); <label className="font-normal text-base text-mti-gray-dim">
toast.success("This new exam has been generated successfully! Check the ID in our browser's console."); Timer
setResultingExam(result.data); </label>
<Input
setPart1(undefined); type="number"
setPart2(undefined); name="minTimer"
setPart3(undefined); onChange={(e) => setMinTimer(parseInt(e) < 15 ? 15 : parseInt(e))}
setDifficulty(sample(DIFFICULTIES)!); value={minTimer}
setMinTimer(60); className="max-w-[300px]"
setTypes([]); />
}) </div>
.catch((error) => { <div className="flex flex-col gap-3 w-full">
console.log(error); <label className="font-normal text-base text-mti-gray-dim">
toast.error("Something went wrong while generating, please try again later."); Difficulty
}) </label>
.finally(() => setIsLoading(false)); <Select
}; options={DIFFICULTIES.map((x) => ({
value: x,
return ( label: capitalize(x),
<> }))}
<div className="flex gap-4 w-1/2"> onChange={(value) =>
<div className="flex flex-col gap-3"> value ? setDifficulty(value.value as Difficulty) : null
<label className="font-normal text-base text-mti-gray-dim">Timer</label> }
<Input value={{ value: difficulty, label: capitalize(difficulty) }}
type="number" disabled={!!part1 || !!part2 || !!part3}
name="minTimer" />
onChange={(e) => setMinTimer(parseInt(e) < 15 ? 15 : parseInt(e))} </div>
value={minTimer} </div>
className="max-w-[300px]" <Tab.Group>
/> <Tab.List className="flex space-x-1 rounded-xl bg-ielts-reading/20 p-1">
</div> <Tab
<div className="flex flex-col gap-3 w-full"> className={({ selected }) =>
<label className="font-normal text-base text-mti-gray-dim">Difficulty</label> clsx(
<Select "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-reading/70 flex gap-2 items-center justify-center",
options={DIFFICULTIES.map((x) => ({value: x, label: capitalize(x)}))} "ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-reading focus:outline-none focus:ring-2",
onChange={(value) => (value ? setDifficulty(value.value as Difficulty) : null)} "transition duration-300 ease-in-out",
value={{value: difficulty, label: capitalize(difficulty)}} selected
disabled={!!part1 || !!part2 || !!part3} ? "bg-white shadow"
/> : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-reading"
</div> )
</div> }
>
<div className="flex flex-col gap-3"> Passage 1 {part1 && <BsCheck />}
<label className="font-normal text-base text-mti-gray-dim">Exercises</label> </Tab>
<div className="flex flex-row -2xl:flex-wrap w-full gap-4 -md:justify-center justify-between"> <Tab
{availableTypes.map((x) => ( className={({ selected }) =>
<span clsx(
onClick={() => toggleType(x.type)} "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-reading/70 flex gap-2 items-center justify-center",
key={x.type} "ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-reading focus:outline-none focus:ring-2",
className={clsx( "transition duration-300 ease-in-out",
"px-6 py-4 w-64 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer", selected
"transition duration-300 ease-in-out", ? "bg-white shadow"
!types.includes(x.type) ? "bg-white border-mti-gray-platinum" : "bg-ielts-reading/70 border-ielts-reading text-white", : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-reading"
)}> )
{x.label} }
</span> >
))} Passage 2 {part2 && <BsCheck />}
</div> </Tab>
</div> <Tab
className={({ selected }) =>
<Tab.Group> clsx(
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-reading/20 p-1"> "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-reading/70 flex gap-2 items-center justify-center",
<Tab "ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-reading focus:outline-none focus:ring-2",
className={({selected}) => "transition duration-300 ease-in-out",
clsx( selected
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-reading/70 flex gap-2 items-center justify-center", ? "bg-white shadow"
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-reading focus:outline-none focus:ring-2", : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-reading"
"transition duration-300 ease-in-out", )
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-reading", }
) >
}> Passage 3 {part3 && <BsCheck />}
Passage 1 {part1 && <BsCheck />} </Tab>
</Tab> </Tab.List>
<Tab <Tab.Panels>
className={({selected}) => {[
clsx( { part: part1, setPart: setPart1 },
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-reading/70 flex gap-2 items-center justify-center", { part: part2, setPart: setPart2 },
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-reading focus:outline-none focus:ring-2", { part: part3, setPart: setPart3 },
"transition duration-300 ease-in-out", ].map(({ part, setPart }, index) => (
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-reading", <PartTab
) part={part}
}> difficulty={difficulty}
Passage 2 {part2 && <BsCheck />} index={index + 1}
</Tab> key={index}
<Tab setPart={setPart}
className={({selected}) => updatePart={setPart}
clsx( />
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-reading/70 flex gap-2 items-center justify-center", ))}
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-reading focus:outline-none focus:ring-2", </Tab.Panels>
"transition duration-300 ease-in-out", </Tab.Group>
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-reading", <div className="w-full flex justify-end gap-4">
) {resultingExam && (
}> <button
Passage 3 {part3 && <BsCheck />} disabled={isLoading}
</Tab> onClick={() => loadExam(resultingExam.id)}
</Tab.List> className={clsx(
<Tab.Panels> "bg-white border border-ielts-reading text-ielts-reading w-full max-w-[200px] rounded-xl h-[70px] self-end",
{[ "hover:bg-ielts-reading hover:text-white disabled:bg-ielts-reading/40 disabled:cursor-not-allowed",
{part: part1, setPart: setPart1}, "transition ease-in-out duration-300"
{part: part2, setPart: setPart2}, )}
{part: part3, setPart: setPart3}, >
].map(({part, setPart}, index) => ( Perform Exam
<PartTab part={part} types={types} difficulty={difficulty} index={index + 1} key={index} setPart={setPart} /> </button>
))} )}
</Tab.Panels> <button
</Tab.Group> disabled={(!part1 && !part2 && !part3) || isLoading}
<div className="w-full flex justify-end gap-4"> data-tip="Please generate all three passages"
{resultingExam && ( onClick={submitExam}
<button className={clsx(
disabled={isLoading} "bg-ielts-reading/70 border border-ielts-reading text-white w-full max-w-[200px] rounded-xl h-[70px] self-end",
onClick={() => loadExam(resultingExam.id)} "hover:bg-ielts-reading disabled:bg-ielts-reading/40 disabled:cursor-not-allowed",
className={clsx( "transition ease-in-out duration-300",
"bg-white border border-ielts-reading text-ielts-reading w-full max-w-[200px] rounded-xl h-[70px] self-end", !part1 && !part2 && !part3 && "tooltip"
"hover:bg-ielts-reading hover:text-white disabled:bg-ielts-reading/40 disabled:cursor-not-allowed", )}
"transition ease-in-out duration-300", >
)}> {isLoading ? (
Perform Exam <div className="flex items-center justify-center">
</button> <BsArrowRepeat className="text-white animate-spin" size={25} />
)} </div>
<button ) : (
disabled={(!part1 && !part2 && !part3) || isLoading} "Submit"
data-tip="Please generate all three passages" )}
onClick={submitExam} </button>
className={clsx( </div>
"bg-ielts-reading/70 border border-ielts-reading text-white w-full max-w-[200px] rounded-xl h-[70px] self-end", </>
"hover:bg-ielts-reading disabled:bg-ielts-reading/40 disabled:cursor-not-allowed", );
"transition ease-in-out duration-300",
!part1 && !part2 && !part3 && "tooltip",
)}>
{isLoading ? (
<div className="flex items-center justify-center">
<BsArrowRepeat className="text-white animate-spin" size={25} />
</div>
) : (
"Submit"
)}
</button>
</div>
</>
);
}; };
export default ReadingGeneration; export default ReadingGeneration;

View File

@@ -1,378 +1,503 @@
import Input from "@/components/Low/Input"; import Input from "@/components/Low/Input";
import Select from "@/components/Low/Select"; import Select from "@/components/Low/Select";
import {Difficulty, Exercise, InteractiveSpeakingExercise, SpeakingExam, SpeakingExercise} from "@/interfaces/exam"; import {
import {AVATARS} from "@/resources/speakingAvatars"; Difficulty,
Exercise,
InteractiveSpeakingExercise,
SpeakingExam,
SpeakingExercise,
} from "@/interfaces/exam";
import { AVATARS } from "@/resources/speakingAvatars";
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 {convertCamelCaseToReadable} from "@/utils/string"; import { convertCamelCaseToReadable } from "@/utils/string";
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, uniq} from "lodash"; import { capitalize, sample, uniq } from "lodash";
import moment from "moment"; import moment from "moment";
import {useRouter} from "next/router"; import { useRouter } from "next/router";
import {useEffect, useState} from "react"; import { useEffect, useState, Dispatch, SetStateAction } from "react";
import {BsArrowRepeat, BsCheck} from "react-icons/bs"; import { BsArrowRepeat, BsCheck } from "react-icons/bs";
import {toast} from "react-toastify"; import { toast } from "react-toastify";
import {v4} from "uuid"; import { v4 } from "uuid";
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"]; const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
const PartTab = ({ const PartTab = ({
part, part,
index, index,
difficulty, difficulty,
setPart, setPart,
updatePart,
}: { }: {
part?: SpeakingPart; part?: SpeakingPart;
difficulty: Difficulty; difficulty: Difficulty;
index: number; index: number;
setPart: (part?: SpeakingPart) => void; setPart: (part?: SpeakingPart) => void;
updatePart: Dispatch<SetStateAction<SpeakingPart | undefined>>;
}) => { }) => {
const [gender, setGender] = useState<"male" | "female">("male"); const [gender, setGender] = useState<"male" | "female">("male");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const generate = () => { const generate = () => {
setPart(undefined); setPart(undefined);
setIsLoading(true); setIsLoading(true);
const url = new URLSearchParams(); const url = new URLSearchParams();
url.append("difficulty", difficulty); url.append("difficulty", difficulty);
axios axios
.get(`/api/exam/speaking/generate/speaking_task_${index}?${url.toString()}`) .get(
.then((result) => { `/api/exam/speaking/generate/speaking_task_${index}?${url.toString()}`
playSound(typeof result.data === "string" ? "error" : "check"); )
if (typeof result.data === "string") return toast.error("Something went wrong, please try to generate again."); .then((result) => {
console.log(result.data); playSound(typeof result.data === "string" ? "error" : "check");
setPart(result.data); if (typeof result.data === "string")
}) return toast.error(
.catch((error) => { "Something went wrong, please try to generate again."
console.log(error); );
toast.error("Something went wrong!"); console.log(result.data);
}) setPart(result.data);
.finally(() => setIsLoading(false)); })
}; .catch((error) => {
console.log(error);
toast.error("Something went wrong!");
})
.finally(() => setIsLoading(false));
};
const generateVideo = async () => { const generateVideo = async () => {
if (!part) return toast.error("Please generate the first part before generating the video!"); if (!part)
toast.info("This will take quite a while, please do not leave this page or close the tab/window."); return toast.error(
"Please generate the first part before generating the video!"
);
toast.info(
"This will take quite a while, please do not leave this page or close the tab/window."
);
const avatar = sample(AVATARS.filter((x) => x.gender === gender)); const avatar = sample(AVATARS.filter((x) => x.gender === gender));
setIsLoading(true); setIsLoading(true);
const initialTime = moment(); const initialTime = moment();
axios axios
.post(`/api/exam/speaking/generate/speaking/generate_video_${index}`, {...part, avatar: avatar?.id}) .post(`/api/exam/speaking/generate/speaking/generate_video_${index}`, {
.then((result) => { ...part,
const isError = typeof result.data === "string" || moment().diff(initialTime, "seconds") < 60; avatar: avatar?.id,
})
.then((result) => {
const isError =
typeof result.data === "string" ||
moment().diff(initialTime, "seconds") < 60;
playSound(isError ? "error" : "check"); playSound(isError ? "error" : "check");
console.log(result.data); console.log(result.data);
if (isError) return toast.error("Something went wrong, please try to generate the video again."); if (isError)
setPart({...part, result: {...result.data, topic: part?.topic}, gender, avatar}); return toast.error(
}) "Something went wrong, please try to generate the video again."
.catch((e) => { );
toast.error("Something went wrong!"); setPart({
console.log(e); ...part,
}) result: { ...result.data, topic: part?.topic },
.finally(() => setIsLoading(false)); gender,
}; avatar,
});
})
.catch((e) => {
toast.error("Something went wrong!");
console.log(e);
})
.finally(() => setIsLoading(false));
};
return ( return (
<Tab.Panel className="w-full bg-ielts-speaking/20 min-h-[600px] h-full rounded-xl p-6 flex flex-col gap-4"> <Tab.Panel className="w-full bg-ielts-speaking/20 min-h-[600px] h-full rounded-xl p-6 flex flex-col gap-4">
<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">Gender</label> <label className="font-normal text-base text-mti-gray-dim">
<Select Gender
options={[ </label>
{value: "male", label: "Male"}, <Select
{value: "female", label: "Female"}, options={[
]} { value: "male", label: "Male" },
value={{value: gender, label: capitalize(gender)}} { value: "female", label: "Female" },
onChange={(value) => (value ? setGender(value.value as typeof gender) : null)} ]}
disabled={isLoading} value={{ value: gender, label: capitalize(gender) }}
/> onChange={(value) =>
</div> value ? setGender(value.value as typeof gender) : null
<div className="flex gap-4 items-end"> }
<button disabled={isLoading}
onClick={generate} />
disabled={isLoading} </div>
data-tip="The passage is currently being generated" <div className="flex gap-4 items-end">
className={clsx( <button
"bg-ielts-speaking/70 border border-ielts-speaking text-white w-full rounded-xl h-[70px]", onClick={generate}
"hover:bg-ielts-speaking disabled:bg-ielts-speaking/40 disabled:cursor-not-allowed", disabled={isLoading}
"transition ease-in-out duration-300", data-tip="The passage is currently being generated"
isLoading && "tooltip", className={clsx(
)}> "bg-ielts-speaking/70 border border-ielts-speaking text-white w-full rounded-xl h-[70px]",
{isLoading ? ( "hover:bg-ielts-speaking disabled:bg-ielts-speaking/40 disabled:cursor-not-allowed",
<div className="flex items-center justify-center"> "transition ease-in-out duration-300",
<BsArrowRepeat className="text-white animate-spin" size={25} /> isLoading && "tooltip"
</div> )}
) : ( >
"Generate" {isLoading ? (
)} <div className="flex items-center justify-center">
</button> <BsArrowRepeat className="text-white animate-spin" size={25} />
<button </div>
onClick={generateVideo} ) : (
disabled={isLoading || !part} "Generate"
data-tip="The passage is currently being generated" )}
className={clsx( </button>
"bg-ielts-speaking/70 border border-ielts-speaking text-white w-full rounded-xl h-[70px]", <button
"hover:bg-ielts-speaking disabled:bg-ielts-speaking/40 disabled:cursor-not-allowed", onClick={generateVideo}
"transition ease-in-out duration-300", disabled={isLoading || !part}
isLoading && "tooltip", data-tip="The passage is currently being generated"
)}> className={clsx(
{isLoading ? ( "bg-ielts-speaking/70 border border-ielts-speaking text-white w-full rounded-xl h-[70px]",
<div className="flex items-center justify-center"> "hover:bg-ielts-speaking disabled:bg-ielts-speaking/40 disabled:cursor-not-allowed",
<BsArrowRepeat className="text-white animate-spin" size={25} /> "transition ease-in-out duration-300",
</div> isLoading && "tooltip"
) : ( )}
"Generate Video" >
)} {isLoading ? (
</button> <div className="flex items-center justify-center">
</div> <BsArrowRepeat className="text-white animate-spin" size={25} />
{isLoading && ( </div>
<div className="w-fit h-fit mt-12 self-center animate-pulse flex flex-col gap-8 items-center"> ) : (
<span className={clsx("loading loading-infinity w-32 text-ielts-speaking")} /> "Generate Video"
<span className={clsx("font-bold text-2xl text-ielts-speaking")}>Generating...</span> )}
</div> </button>
)} </div>
{part && !isLoading && ( {isLoading && (
<div className="flex flex-col gap-2 w-full overflow-y-scroll scrollbar-hide h-96"> <div className="w-fit h-fit mt-12 self-center animate-pulse flex flex-col gap-8 items-center">
<h3 className="text-xl font-semibold"> <span
{!!part.first_topic && !!part.second_topic ? `${part.first_topic} & ${part.second_topic}` : part.topic} className={clsx(
</h3> "loading loading-infinity w-32 text-ielts-speaking"
{part.question && <span className="w-full">{part.question}</span>} )}
{part.questions && ( />
<div className="flex flex-col gap-1"> <span className={clsx("font-bold text-2xl text-ielts-speaking")}>
{part.questions.map((question, index) => ( Generating...
<span className="w-full" key={index}> </span>
- {question} </div>
</span> )}
))} {part && !isLoading && (
</div> <div className="flex flex-col gap-2 w-full overflow-y-scroll scrollbar-hide h-96">
)} <h3 className="text-xl font-semibold">
{part.prompts && ( {!!part.first_topic && !!part.second_topic
<div className="flex flex-col gap-1"> ? `${part.first_topic} & ${part.second_topic}`
<span className="font-medium">You should talk about the following things:</span> : part.topic}
{part.prompts.map((prompt, index) => ( </h3>
<span className="w-full" key={index}> {part.question && <span className="w-full">{part.question}</span>}
- {prompt} {part.questions && (
</span> <div className="flex flex-col gap-1">
))} {part.questions.map((question, index) => (
</div> <span className="w-full" key={index}>
)} - {question}
{part.result && <span className="font-bold mt-4">Video Generated: </span>} </span>
{part.avatar && part.gender && ( ))}
<span> </div>
<b>Instructor:</b> {part.avatar.name} - {capitalize(part.avatar.gender)} )}
</span> {part.prompts && (
)} <div className="flex flex-col gap-1">
</div> <span className="font-medium">
)} You should talk about the following things:
</Tab.Panel> </span>
); {part.prompts.map((prompt, index) => (
<span className="w-full" key={index}>
- {prompt}
</span>
))}
</div>
)}
{part.result && (
<span className="font-bold mt-4">Video Generated: </span>
)}
{part.avatar && part.gender && (
<span>
<b>Instructor:</b> {part.avatar.name} -{" "}
{capitalize(part.avatar.gender)}
</span>
)}
{part.questions?.map((question, index) => (
<Input
key={index}
type="text"
label="Question"
name="question"
required
value={question}
onChange={
(value) =>
updatePart((part?: SpeakingPart) => {
if (part) {
return {
...part,
questions: part.questions?.map((x, xIndex) =>
xIndex === index ? value : x
),
} as SpeakingPart;
}
return part;
})
}
/>
))}
</div>
)}
</Tab.Panel>
);
}; };
interface SpeakingPart { interface SpeakingPart {
prompts?: string[]; prompts?: string[];
question?: string; question?: string;
questions?: string[]; questions?: string[];
topic: string; topic: string;
first_topic?: string; first_topic?: string;
second_topic?: string; second_topic?: string;
result?: SpeakingExercise | InteractiveSpeakingExercise; result?: SpeakingExercise | InteractiveSpeakingExercise;
gender?: "male" | "female"; gender?: "male" | "female";
avatar?: (typeof AVATARS)[number]; avatar?: (typeof AVATARS)[number];
} }
const SpeakingGeneration = () => { const SpeakingGeneration = () => {
const [part1, setPart1] = useState<SpeakingPart>(); const [part1, setPart1] = useState<SpeakingPart>();
const [part2, setPart2] = useState<SpeakingPart>(); const [part2, setPart2] = useState<SpeakingPart>();
const [part3, setPart3] = useState<SpeakingPart>(); const [part3, setPart3] = useState<SpeakingPart>();
const [minTimer, setMinTimer] = useState(14); const [minTimer, setMinTimer] = useState(14);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [resultingExam, setResultingExam] = useState<SpeakingExam>(); const [resultingExam, setResultingExam] = useState<SpeakingExam>();
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!); const [difficulty, setDifficulty] = useState<Difficulty>(
sample(DIFFICULTIES)!
);
useEffect(() => { useEffect(() => {
const parts = [part1, part2, part3].filter((x) => !!x); const parts = [part1, part2, part3].filter((x) => !!x);
setMinTimer(parts.length === 0 ? 5 : parts.length * 5); setMinTimer(parts.length === 0 ? 5 : parts.length * 5);
}, [part1, part2, part3]); }, [part1, part2, part3]);
const router = useRouter(); const router = useRouter();
const setExams = useExamStore((state) => state.setExams); const setExams = useExamStore((state) => state.setExams);
const setSelectedModules = useExamStore((state) => state.setSelectedModules); const setSelectedModules = useExamStore((state) => state.setSelectedModules);
const submitExam = () => { const submitExam = () => {
if (!part1?.result && !part2?.result && !part3?.result) return toast.error("Please generate at least one task!"); if (!part1?.result && !part2?.result && !part3?.result)
return toast.error("Please generate at least one task!");
setIsLoading(true); setIsLoading(true);
const genders = [part1?.gender, part2?.gender, part3?.gender].filter((x) => !!x); const genders = [part1?.gender, part2?.gender, part3?.gender].filter(
(x) => !!x
);
const exercises = [part1?.result, part2?.result, part3?.result] const exercises = [part1?.result, part2?.result, part3?.result]
.filter((x) => !!x) .filter((x) => !!x)
.map((x) => ({ .map((x) => ({
...x, ...x,
first_title: x?.type === "interactiveSpeaking" ? x.first_topic : undefined, first_title:
second_title: x?.type === "interactiveSpeaking" ? x.second_topic : undefined, x?.type === "interactiveSpeaking" ? x.first_topic : undefined,
})); second_title:
x?.type === "interactiveSpeaking" ? x.second_topic : undefined,
}));
const exam: SpeakingExam = { const exam: SpeakingExam = {
id: v4(), id: v4(),
isDiagnostic: false, isDiagnostic: false,
exercises: exercises as (SpeakingExercise | InteractiveSpeakingExercise)[], exercises: exercises as (
minTimer, | SpeakingExercise
variant: minTimer >= 14 ? "full" : "partial", | InteractiveSpeakingExercise
module: "speaking", )[],
instructorGender: genders.every((x) => x === "male") ? "male" : genders.every((x) => x === "female") ? "female" : "varied", minTimer,
}; variant: minTimer >= 14 ? "full" : "partial",
module: "speaking",
instructorGender: genders.every((x) => x === "male")
? "male"
: genders.every((x) => x === "female")
? "female"
: "varied",
};
axios axios
.post(`/api/exam/speaking`, exam) .post(`/api/exam/speaking`, exam)
.then((result) => { .then((result) => {
playSound("sent"); playSound("sent");
console.log(`Generated Exam ID: ${result.data.id}`); 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."); toast.success(
setResultingExam(result.data); "This new exam has been generated successfully! Check the ID in our browser's console."
);
setResultingExam(result.data);
setPart1(undefined); setPart1(undefined);
setPart2(undefined); setPart2(undefined);
setPart3(undefined); setPart3(undefined);
setDifficulty(sample(DIFFICULTIES)!); setDifficulty(sample(DIFFICULTIES)!);
setMinTimer(14); setMinTimer(14);
}) })
.catch((error) => { .catch((error) => {
console.log(error); console.log(error);
toast.error("Something went wrong while generating, please try again later."); toast.error(
}) "Something went wrong while generating, please try again later."
.finally(() => setIsLoading(false)); );
}; })
.finally(() => setIsLoading(false));
};
const loadExam = async (examId: string) => { const loadExam = async (examId: string) => {
const exam = await getExamById("speaking", examId.trim()); const exam = await getExamById("speaking", examId.trim());
if (!exam) { if (!exam) {
toast.error("Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID", { toast.error(
toastId: "invalid-exam-id", "Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID",
}); {
toastId: "invalid-exam-id",
}
);
return; return;
} }
setExams([exam]); setExams([exam]);
setSelectedModules(["speaking"]); setSelectedModules(["speaking"]);
router.push("/exercises"); router.push("/exercises");
}; };
return ( return (
<> <>
<div className="flex gap-4 w-1/2"> <div className="flex gap-4 w-1/2">
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<label className="font-normal text-base text-mti-gray-dim">Timer</label> <label className="font-normal text-base text-mti-gray-dim">
<Input Timer
type="number" </label>
name="minTimer" <Input
onChange={(e) => setMinTimer(parseInt(e) < 5 ? 5 : parseInt(e))} type="number"
value={minTimer} name="minTimer"
className="max-w-[300px]" onChange={(e) => setMinTimer(parseInt(e) < 5 ? 5 : parseInt(e))}
/> value={minTimer}
</div> className="max-w-[300px]"
<div className="flex flex-col gap-3 w-full"> />
<label className="font-normal text-base text-mti-gray-dim">Difficulty</label> </div>
<Select <div className="flex flex-col gap-3 w-full">
options={DIFFICULTIES.map((x) => ({value: x, label: capitalize(x)}))} <label className="font-normal text-base text-mti-gray-dim">
onChange={(value) => (value ? setDifficulty(value.value as Difficulty) : null)} Difficulty
value={{value: difficulty, label: capitalize(difficulty)}} </label>
disabled={!!part1 || !!part2 || !!part3} <Select
/> options={DIFFICULTIES.map((x) => ({
</div> value: x,
</div> label: capitalize(x),
}))}
onChange={(value) =>
value ? setDifficulty(value.value as Difficulty) : null
}
value={{ value: difficulty, label: capitalize(difficulty) }}
disabled={!!part1 || !!part2 || !!part3}
/>
</div>
</div>
<Tab.Group> <Tab.Group>
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-speaking/20 p-1"> <Tab.List className="flex space-x-1 rounded-xl bg-ielts-speaking/20 p-1">
<Tab <Tab
className={({selected}) => className={({ selected }) =>
clsx( clsx(
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/70 flex gap-2 items-center justify-center", "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/70 flex gap-2 items-center justify-center",
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2", "ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
"transition duration-300 ease-in-out", "transition duration-300 ease-in-out",
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking", selected
) ? "bg-white shadow"
}> : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking"
Exercise 1 {part1 && part1.result && <BsCheck />} )
</Tab> }
<Tab >
className={({selected}) => Exercise 1 {part1 && part1.result && <BsCheck />}
clsx( </Tab>
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/70 flex gap-2 items-center justify-center", <Tab
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2", className={({ selected }) =>
"transition duration-300 ease-in-out", clsx(
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking", "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/70 flex gap-2 items-center justify-center",
) "ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
}> "transition duration-300 ease-in-out",
Exercise 2 {part2 && part2.result && <BsCheck />} selected
</Tab> ? "bg-white shadow"
<Tab : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking"
className={({selected}) => )
clsx( }
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/70 flex gap-2 items-center justify-center", >
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2", Exercise 2 {part2 && part2.result && <BsCheck />}
"transition duration-300 ease-in-out", </Tab>
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking", <Tab
) className={({ selected }) =>
}> clsx(
Interactive {part3 && part3.result && <BsCheck />} "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/70 flex gap-2 items-center justify-center",
</Tab> "ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
</Tab.List> "transition duration-300 ease-in-out",
<Tab.Panels> selected
{[ ? "bg-white shadow"
{part: part1, setPart: setPart1}, : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking"
{part: part2, setPart: setPart2}, )
{part: part3, setPart: setPart3}, }
].map(({part, setPart}, index) => ( >
<PartTab difficulty={difficulty} part={part} index={index + 1} key={index} setPart={setPart} /> Interactive {part3 && part3.result && <BsCheck />}
))} </Tab>
</Tab.Panels> </Tab.List>
</Tab.Group> <Tab.Panels>
<div className="w-full flex justify-end gap-4"> {[
{resultingExam && ( { part: part1, setPart: setPart1 },
<button { part: part2, setPart: setPart2 },
disabled={isLoading} { part: part3, setPart: setPart3 },
onClick={() => loadExam(resultingExam.id)} ].map(({ part, setPart }, index) => (
className={clsx( <PartTab
"bg-white border border-ielts-speaking text-ielts-speaking w-full max-w-[200px] rounded-xl h-[70px] self-end", difficulty={difficulty}
"hover:bg-ielts-speaking hover:text-white disabled:bg-ielts-speaking/40 disabled:cursor-not-allowed", part={part}
"transition ease-in-out duration-300", index={index + 1}
)}> key={index}
Perform Exam setPart={setPart}
</button> updatePart={setPart}
)} />
<button ))}
disabled={(!part1?.result && !part2?.result && !part3?.result) || isLoading} </Tab.Panels>
data-tip="Please generate all three passages" </Tab.Group>
onClick={submitExam} <div className="w-full flex justify-end gap-4">
className={clsx( {resultingExam && (
"bg-ielts-speaking/70 border border-ielts-speaking text-white w-full max-w-[200px] rounded-xl h-[70px] self-end", <button
"hover:bg-ielts-speaking disabled:bg-ielts-speaking/40 disabled:cursor-not-allowed", disabled={isLoading}
"transition ease-in-out duration-300", onClick={() => loadExam(resultingExam.id)}
!part1 && !part2 && !part3 && "tooltip", className={clsx(
)}> "bg-white border border-ielts-speaking text-ielts-speaking w-full max-w-[200px] rounded-xl h-[70px] self-end",
{isLoading ? ( "hover:bg-ielts-speaking hover:text-white disabled:bg-ielts-speaking/40 disabled:cursor-not-allowed",
<div className="flex items-center justify-center"> "transition ease-in-out duration-300"
<BsArrowRepeat className="text-white animate-spin" size={25} /> )}
</div> >
) : ( Perform Exam
"Submit" </button>
)} )}
</button> <button
</div> disabled={
</> (!part1?.result && !part2?.result && !part3?.result) || isLoading
); }
data-tip="Please generate all three passages"
onClick={submitExam}
className={clsx(
"bg-ielts-speaking/70 border border-ielts-speaking text-white w-full max-w-[200px] rounded-xl h-[70px] self-end",
"hover:bg-ielts-speaking disabled:bg-ielts-speaking/40 disabled:cursor-not-allowed",
"transition ease-in-out duration-300",
!part1 && !part2 && !part3 && "tooltip"
)}
>
{isLoading ? (
<div className="flex items-center justify-center">
<BsArrowRepeat className="text-white animate-spin" size={25} />
</div>
) : (
"Submit"
)}
</button>
</div>
</>
);
}; };
export default SpeakingGeneration; export default SpeakingGeneration;