diff --git a/src/pages/(generation)/SpeakingGeneration.tsx b/src/pages/(generation)/SpeakingGeneration.tsx index e0d1134a..1c8d4bfc 100644 --- a/src/pages/(generation)/SpeakingGeneration.tsx +++ b/src/pages/(generation)/SpeakingGeneration.tsx @@ -1,378 +1,503 @@ import Input from "@/components/Low/Input"; import Select from "@/components/Low/Select"; -import {Difficulty, Exercise, InteractiveSpeakingExercise, SpeakingExam, SpeakingExercise} from "@/interfaces/exam"; -import {AVATARS} from "@/resources/speakingAvatars"; +import { + Difficulty, + Exercise, + InteractiveSpeakingExercise, + SpeakingExam, + SpeakingExercise, +} from "@/interfaces/exam"; +import { AVATARS } from "@/resources/speakingAvatars"; import useExamStore from "@/stores/examStore"; -import {getExamById} from "@/utils/exams"; -import {playSound} from "@/utils/sound"; -import {convertCamelCaseToReadable} from "@/utils/string"; -import {Tab} from "@headlessui/react"; +import { getExamById } from "@/utils/exams"; +import { playSound } from "@/utils/sound"; +import { convertCamelCaseToReadable } from "@/utils/string"; +import { Tab } from "@headlessui/react"; import axios from "axios"; import clsx from "clsx"; -import {capitalize, sample, uniq} from "lodash"; +import { capitalize, sample, uniq } from "lodash"; import moment from "moment"; -import {useRouter} from "next/router"; -import {useEffect, useState} from "react"; -import {BsArrowRepeat, BsCheck} from "react-icons/bs"; -import {toast} from "react-toastify"; -import {v4} from "uuid"; +import { useRouter } from "next/router"; +import { useEffect, useState, Dispatch, SetStateAction } from "react"; +import { BsArrowRepeat, BsCheck } from "react-icons/bs"; +import { toast } from "react-toastify"; +import { v4 } from "uuid"; const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"]; const PartTab = ({ - part, - index, - difficulty, - setPart, + part, + index, + difficulty, + setPart, + updatePart, }: { - part?: SpeakingPart; - difficulty: Difficulty; - index: number; - setPart: (part?: SpeakingPart) => void; + part?: SpeakingPart; + difficulty: Difficulty; + index: number; + setPart: (part?: SpeakingPart) => void; + updatePart: Dispatch>; }) => { - const [gender, setGender] = useState<"male" | "female">("male"); - const [isLoading, setIsLoading] = useState(false); + const [gender, setGender] = useState<"male" | "female">("male"); + const [isLoading, setIsLoading] = useState(false); - const generate = () => { - setPart(undefined); - setIsLoading(true); + const generate = () => { + setPart(undefined); + setIsLoading(true); - const url = new URLSearchParams(); - url.append("difficulty", difficulty); + const url = new URLSearchParams(); + url.append("difficulty", difficulty); - axios - .get(`/api/exam/speaking/generate/speaking_task_${index}?${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."); - console.log(result.data); - setPart(result.data); - }) - .catch((error) => { - console.log(error); - toast.error("Something went wrong!"); - }) - .finally(() => setIsLoading(false)); - }; + axios + .get( + `/api/exam/speaking/generate/speaking_task_${index}?${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." + ); + console.log(result.data); + setPart(result.data); + }) + .catch((error) => { + console.log(error); + toast.error("Something went wrong!"); + }) + .finally(() => setIsLoading(false)); + }; - const generateVideo = async () => { - if (!part) 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 generateVideo = async () => { + if (!part) + 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); - const initialTime = moment(); + setIsLoading(true); + const initialTime = moment(); - axios - .post(`/api/exam/speaking/generate/speaking/generate_video_${index}`, {...part, avatar: avatar?.id}) - .then((result) => { - const isError = typeof result.data === "string" || moment().diff(initialTime, "seconds") < 60; + axios + .post(`/api/exam/speaking/generate/speaking/generate_video_${index}`, { + ...part, + avatar: avatar?.id, + }) + .then((result) => { + const isError = + typeof result.data === "string" || + moment().diff(initialTime, "seconds") < 60; - playSound(isError ? "error" : "check"); - console.log(result.data); - if (isError) return toast.error("Something went wrong, please try to generate the video again."); - setPart({...part, result: {...result.data, topic: part?.topic}, gender, avatar}); - }) - .catch((e) => { - toast.error("Something went wrong!"); - console.log(e); - }) - .finally(() => setIsLoading(false)); - }; + playSound(isError ? "error" : "check"); + console.log(result.data); + if (isError) + return toast.error( + "Something went wrong, please try to generate the video again." + ); + setPart({ + ...part, + result: { ...result.data, topic: part?.topic }, + gender, + avatar, + }); + }) + .catch((e) => { + toast.error("Something went wrong!"); + console.log(e); + }) + .finally(() => setIsLoading(false)); + }; - return ( - -
- - + value ? setGender(value.value as typeof gender) : null + } + disabled={isLoading} + /> +
+
+ + +
+ {isLoading && ( +
+ + + Generating... + +
+ )} + {part && !isLoading && ( +
+

+ {!!part.first_topic && !!part.second_topic + ? `${part.first_topic} & ${part.second_topic}` + : part.topic} +

+ {part.question && {part.question}} + {part.questions && ( +
+ {part.questions.map((question, index) => ( + + - {question} + + ))} +
+ )} + {part.prompts && ( +
+ + You should talk about the following things: + + {part.prompts.map((prompt, index) => ( + + - {prompt} + + ))} +
+ )} + {part.result && ( + Video Generated: ✅ + )} + {part.avatar && part.gender && ( + + Instructor: {part.avatar.name} -{" "} + {capitalize(part.avatar.gender)} + + )} + {part.questions?.map((question, index) => ( + + updatePart((part?: SpeakingPart) => { + if (part) { + return { + ...part, + questions: part.questions?.map((x, xIndex) => + xIndex === index ? value : x + ), + } as SpeakingPart; + } + + return part; + }) + } + /> + ))} +
+ )} +
+ ); }; interface SpeakingPart { - prompts?: string[]; - question?: string; - questions?: string[]; - topic: string; - first_topic?: string; - second_topic?: string; - result?: SpeakingExercise | InteractiveSpeakingExercise; - gender?: "male" | "female"; - avatar?: (typeof AVATARS)[number]; + prompts?: string[]; + question?: string; + questions?: string[]; + topic: string; + first_topic?: string; + second_topic?: string; + result?: SpeakingExercise | InteractiveSpeakingExercise; + gender?: "male" | "female"; + avatar?: (typeof AVATARS)[number]; } const SpeakingGeneration = () => { - const [part1, setPart1] = useState(); - const [part2, setPart2] = useState(); - const [part3, setPart3] = useState(); - const [minTimer, setMinTimer] = useState(14); - const [isLoading, setIsLoading] = useState(false); - const [resultingExam, setResultingExam] = useState(); - const [difficulty, setDifficulty] = useState(sample(DIFFICULTIES)!); + const [part1, setPart1] = useState(); + const [part2, setPart2] = useState(); + const [part3, setPart3] = useState(); + const [minTimer, setMinTimer] = useState(14); + const [isLoading, setIsLoading] = useState(false); + const [resultingExam, setResultingExam] = useState(); + const [difficulty, setDifficulty] = useState( + sample(DIFFICULTIES)! + ); - useEffect(() => { - const parts = [part1, part2, part3].filter((x) => !!x); - setMinTimer(parts.length === 0 ? 5 : parts.length * 5); - }, [part1, part2, part3]); + useEffect(() => { + const parts = [part1, part2, part3].filter((x) => !!x); + setMinTimer(parts.length === 0 ? 5 : parts.length * 5); + }, [part1, part2, part3]); - const router = useRouter(); + const router = useRouter(); - const setExams = useExamStore((state) => state.setExams); - const setSelectedModules = useExamStore((state) => state.setSelectedModules); + const setExams = useExamStore((state) => state.setExams); + const setSelectedModules = useExamStore((state) => state.setSelectedModules); - const submitExam = () => { - if (!part1?.result && !part2?.result && !part3?.result) return toast.error("Please generate at least one task!"); + const submitExam = () => { + 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] - .filter((x) => !!x) - .map((x) => ({ - ...x, - first_title: x?.type === "interactiveSpeaking" ? x.first_topic : undefined, - second_title: x?.type === "interactiveSpeaking" ? x.second_topic : undefined, - })); + const exercises = [part1?.result, part2?.result, part3?.result] + .filter((x) => !!x) + .map((x) => ({ + ...x, + first_title: + x?.type === "interactiveSpeaking" ? x.first_topic : undefined, + second_title: + x?.type === "interactiveSpeaking" ? x.second_topic : undefined, + })); - const exam: SpeakingExam = { - id: v4(), - isDiagnostic: false, - exercises: exercises as (SpeakingExercise | InteractiveSpeakingExercise)[], - minTimer, - variant: minTimer >= 14 ? "full" : "partial", - module: "speaking", - instructorGender: genders.every((x) => x === "male") ? "male" : genders.every((x) => x === "female") ? "female" : "varied", - }; + const exam: SpeakingExam = { + id: v4(), + isDiagnostic: false, + exercises: exercises as ( + | SpeakingExercise + | InteractiveSpeakingExercise + )[], + minTimer, + variant: minTimer >= 14 ? "full" : "partial", + module: "speaking", + instructorGender: genders.every((x) => x === "male") + ? "male" + : genders.every((x) => x === "female") + ? "female" + : "varied", + }; - axios - .post(`/api/exam/speaking`, exam) - .then((result) => { - playSound("sent"); - console.log(`Generated Exam ID: ${result.data.id}`); - toast.success("This new exam has been generated successfully! Check the ID in our browser's console."); - setResultingExam(result.data); + axios + .post(`/api/exam/speaking`, exam) + .then((result) => { + playSound("sent"); + console.log(`Generated Exam ID: ${result.data.id}`); + toast.success( + "This new exam has been generated successfully! Check the ID in our browser's console." + ); + setResultingExam(result.data); - setPart1(undefined); - setPart2(undefined); - setPart3(undefined); - setDifficulty(sample(DIFFICULTIES)!); - setMinTimer(14); - }) - .catch((error) => { - console.log(error); - toast.error("Something went wrong while generating, please try again later."); - }) - .finally(() => setIsLoading(false)); - }; + setPart1(undefined); + setPart2(undefined); + setPart3(undefined); + setDifficulty(sample(DIFFICULTIES)!); + setMinTimer(14); + }) + .catch((error) => { + console.log(error); + toast.error( + "Something went wrong while generating, please try again later." + ); + }) + .finally(() => setIsLoading(false)); + }; - const loadExam = async (examId: string) => { - const exam = await getExamById("speaking", examId.trim()); - if (!exam) { - toast.error("Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID", { - toastId: "invalid-exam-id", - }); + const loadExam = async (examId: string) => { + const exam = await getExamById("speaking", examId.trim()); + if (!exam) { + toast.error( + "Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID", + { + toastId: "invalid-exam-id", + } + ); - return; - } + return; + } - setExams([exam]); - setSelectedModules(["speaking"]); + setExams([exam]); + setSelectedModules(["speaking"]); - router.push("/exercises"); - }; + router.push("/exercises"); + }; - return ( - <> -
-
- - setMinTimer(parseInt(e) < 5 ? 5 : parseInt(e))} - value={minTimer} - className="max-w-[300px]" - /> -
-
- - setMinTimer(parseInt(e) < 5 ? 5 : parseInt(e))} + value={minTimer} + className="max-w-[300px]" + /> +
+
+ +