From 0403773b8e99d62bfe1551b5e16c6e0ca8228001 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Tue, 30 Jul 2024 23:18:50 +0100 Subject: [PATCH 01/12] Changed the IDs to now be words and allows the assignment to be like chosen --- src/components/Low/Select.tsx | 2 +- src/dashboards/AssignmentCreator.tsx | 80 +- src/pages/(generation)/LevelGeneration.tsx | 2 +- .../(generation)/ListeningGeneration.tsx | 871 ++++++++---------- src/pages/(generation)/ReadingGeneration.tsx | 28 +- src/pages/(generation)/SpeakingGeneration.tsx | 823 ++++++++--------- src/pages/(generation)/WritingGeneration.tsx | 4 +- src/pages/api/assignments/index.ts | 6 +- src/pages/record.tsx | 21 +- src/pages/tickets.tsx | 58 +- 10 files changed, 906 insertions(+), 989 deletions(-) diff --git a/src/components/Low/Select.tsx b/src/components/Low/Select.tsx index 4381455c..186f6a2c 100644 --- a/src/components/Low/Select.tsx +++ b/src/components/Low/Select.tsx @@ -4,7 +4,7 @@ import ReactSelect, {GroupBase, StylesConfig} from "react-select"; interface Option { [key: string]: any; - value: string; + value: string | null; label: string; } diff --git a/src/dashboards/AssignmentCreator.tsx b/src/dashboards/AssignmentCreator.tsx index e293fc94..7da5523e 100644 --- a/src/dashboards/AssignmentCreator.tsx +++ b/src/dashboards/AssignmentCreator.tsx @@ -2,7 +2,7 @@ import Input from "@/components/Low/Input"; import Modal from "@/components/Modal"; import {Module} from "@/interfaces"; import clsx from "clsx"; -import {useState} from "react"; +import {useEffect, useState} from "react"; import {BsBook, BsCheckCircle, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle} from "react-icons/bs"; import {generate} from "random-words"; import {capitalize} from "lodash"; @@ -21,6 +21,7 @@ import {Assignment} from "@/interfaces/results"; import Checkbox from "@/components/Low/Checkbox"; import {InstructorGender, Variant} from "@/interfaces/exam"; import Select from "@/components/Low/Select"; +import useExams from "@/hooks/useExams"; interface Props { isCreating: boolean; @@ -44,6 +45,14 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro const [instructorGender, setInstructorGender] = useState(assignment?.instructorGender || "varied"); // creates a new exam for each assignee or just one exam for all assignees const [generateMultiple, setGenerateMultiple] = useState(false); + const [useRandomExams, setUseRandomExams] = useState(true); + const [examIDs, setExamIDs] = useState<{id: string; module: Module}[]>([]); + + const {exams} = useExams(); + + useEffect(() => { + setExamIDs((prev) => prev.filter((x) => selectedModules.includes(x.module))); + }, [selectedModules]); const toggleModule = (module: Module) => { const modules = selectedModules.filter((x) => x !== module); @@ -61,6 +70,7 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro assignees, name, startDate, + examIDs: !useRandomExams ? examIDs : undefined, endDate, selectedModules, generateMultiple, @@ -229,19 +239,52 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro -
- - (value ? setInstructorGender(value.value as InstructorGender) : null)} + disabled={!selectedModules.includes("speaking") || !!assignment} + options={[ + {value: "male", label: "Male"}, + {value: "female", label: "Female"}, + {value: "varied", label: "Varied"}, + ]} + /> +
+ )} + + {selectedModules.length > 0 && ( +
+ + Random Exams + + {!useRandomExams && ( +
+ {selectedModules.map((module) => ( +
+ + ({value: key, label: TYPES[key]}))} - onChange={(e) => setSection({...section, type: e!.value})} + onChange={(e) => setSection({...section, type: e!.value!})} value={{value: section?.type || "multiple_choice_4", label: TYPES[section?.type || "multiple_choice_4"]}} />
diff --git a/src/pages/(generation)/ListeningGeneration.tsx b/src/pages/(generation)/ListeningGeneration.tsx index fdbf36a7..85fd1064 100644 --- a/src/pages/(generation)/ListeningGeneration.tsx +++ b/src/pages/(generation)/ListeningGeneration.tsx @@ -1,526 +1,449 @@ import MultipleChoiceEdit from "@/components/Generation/multiple.choice.edit"; import Input from "@/components/Low/Input"; 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 { 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 } from "lodash"; -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 {capitalize, sample} from "lodash"; +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 WriteBlanksEdit from "@/components/Generation/write.blanks.edit"; +import {generate} from "random-words"; const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"]; -const MULTIPLE_CHOICE = { type: "multipleChoice", label: "Multiple Choice" }; +const MULTIPLE_CHOICE = {type: "multipleChoice", label: "Multiple Choice"}; const WRITE_BLANKS_QUESTIONS = { - type: "writeBlanksQuestions", - label: "Write the Blanks: Questions", + type: "writeBlanksQuestions", + label: "Write the Blanks: Questions", }; const WRITE_BLANKS_FILL = { - type: "writeBlanksFill", - label: "Write the Blanks: Fill", + type: "writeBlanksFill", + label: "Write the Blanks: Fill", }; const WRITE_BLANKS_FORM = { - type: "writeBlanksForm", - label: "Write the Blanks: Form", + type: "writeBlanksForm", + label: "Write the Blanks: Form", }; const MULTIPLE_CHOICE_3 = { - type: "multipleChoice3Options", - label: "Multiple Choice", + type: "multipleChoice3Options", + label: "Multiple Choice", }; const PartTab = ({ - part, - difficulty, - availableTypes, - index, - setPart, - updatePart, + part, + difficulty, + availableTypes, + index, + setPart, + updatePart, }: { - part?: ListeningPart; - difficulty: Difficulty; - availableTypes: { type: string; label: string }[]; - index: number; - setPart: (part?: ListeningPart) => void; - updatePart: Dispatch>; + part?: ListeningPart; + difficulty: Difficulty; + availableTypes: {type: string; label: string}[]; + index: number; + setPart: (part?: ListeningPart) => void; + updatePart: Dispatch>; }) => { - const [topic, setTopic] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [types, setTypes] = useState([]); + const [topic, setTopic] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [types, setTypes] = useState([]); - const generate = () => { - const url = new URLSearchParams(); - url.append("difficulty", difficulty); + const generate = () => { + const url = new URLSearchParams(); + url.append("difficulty", difficulty); - if (topic) url.append("topic", topic); - if (types) types.forEach((t) => url.append("exercises", t)); + if (topic) url.append("topic", topic); + if (types) types.forEach((t) => url.append("exercises", t)); - setPart(undefined); - setIsLoading(true); - axios - .get( - `/api/exam/listening/generate/listening_section_${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)); - }; + setPart(undefined); + setIsLoading(true); + axios + .get(`/api/exam/listening/generate/listening_section_${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)); + }; - const renderExercises = () => { - return part?.exercises.map((exercise) => { - switch (exercise.type) { - case "multipleChoice": - return ( - <> -

Exercise: Multiple Choice

- - updatePart((part?: ListeningPart) => { - if (part) { - const exercises = part.exercises.map((x) => - x.id === exercise.id ? { ...x, ...data } : x - ) as Exercise[]; - const updatedPart = { - ...part, - exercises, - } as ListeningPart; - return updatedPart; - } + const renderExercises = () => { + return part?.exercises.map((exercise) => { + switch (exercise.type) { + case "multipleChoice": + return ( + <> +

Exercise: Multiple Choice

+ + updatePart((part?: ListeningPart) => { + if (part) { + const exercises = part.exercises.map((x) => (x.id === exercise.id ? {...x, ...data} : x)) as Exercise[]; + const updatedPart = { + ...part, + exercises, + } as ListeningPart; + return updatedPart; + } - return part; - }) - } - /> - - ); - // TODO: This might be broken as they all returns the same - case "writeBlanks": - return ( - <> -

Exercise: Write Blanks

- { - updatePart((part?: ListeningPart) => { - if (part) { - return { - ...part, - exercises: part.exercises.map((x) => - x.id === exercise.id ? { ...x, ...data } : x - ), - } as ListeningPart; - } + return part; + }) + } + /> + + ); + // TODO: This might be broken as they all returns the same + case "writeBlanks": + return ( + <> +

Exercise: Write Blanks

+ { + updatePart((part?: ListeningPart) => { + if (part) { + return { + ...part, + exercises: part.exercises.map((x) => (x.id === exercise.id ? {...x, ...data} : x)), + } as ListeningPart; + } - return part; - }); - }} - /> - - ); - default: - return null; - } - }); - }; + return part; + }); + }} + /> + + ); + default: + return null; + } + }); + }; - const toggleType = (type: string) => - setTypes((prev) => - prev.includes(type) - ? [...prev.filter((x) => x !== type)] - : [...prev, type] - ); + const toggleType = (type: string) => setTypes((prev) => (prev.includes(type) ? [...prev.filter((x) => x !== type)] : [...prev, type])); - return ( - -
- -
- {availableTypes.map((x) => ( - 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} - - ))} -
-
-
- - -
- {isLoading && ( -
- - - Generating... - -
- )} - {part && ( - <> -
-
- {part.exercises.map((x) => ( - - {x.type && convertCamelCaseToReadable(x.type)} - - ))} -
- {typeof part.text === "string" && ( - - {part.text.replaceAll("\n\n", " ")} - - )} - {typeof part.text !== "string" && ( -
- {part.text.conversation.map((x, index) => ( - - {x.name}: - {x.text.replaceAll("\n\n", " ")} - - ))} -
- )} -
- {renderExercises()} - - )} -
- ); + return ( + +
+ +
+ {availableTypes.map((x) => ( + 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} + + ))} +
+
+
+ + +
+ {isLoading && ( +
+ + Generating... +
+ )} + {part && ( + <> +
+
+ {part.exercises.map((x) => ( + + {x.type && convertCamelCaseToReadable(x.type)} + + ))} +
+ {typeof part.text === "string" && {part.text.replaceAll("\n\n", " ")}} + {typeof part.text !== "string" && ( +
+ {part.text.conversation.map((x, index) => ( + + {x.name}: + {x.text.replaceAll("\n\n", " ")} + + ))} +
+ )} +
+ {renderExercises()} + + )} +
+ ); }; interface ListeningPart { - exercises: Exercise[]; - text: - | { - conversation: { - gender: string; - name: string; - text: string; - voice: string; - }[]; - } - | string; + exercises: Exercise[]; + text: + | { + conversation: { + gender: string; + name: string; + text: string; + voice: string; + }[]; + } + | string; } const ListeningGeneration = () => { - const [part1, setPart1] = useState(); - const [part2, setPart2] = useState(); - const [part3, setPart3] = useState(); - const [part4, setPart4] = useState(); - const [minTimer, setMinTimer] = useState(30); - 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 [part4, setPart4] = useState(); + const [minTimer, setMinTimer] = useState(30); + const [isLoading, setIsLoading] = useState(false); + const [resultingExam, setResultingExam] = useState(); + const [difficulty, setDifficulty] = useState(sample(DIFFICULTIES)!); - useEffect(() => { - const part1Timer = part1 ? 5 : 0; - const part2Timer = part2 ? 8 : 0; - const part3Timer = part3 ? 8 : 0; - const part4Timer = part4 ? 9 : 0; + useEffect(() => { + const part1Timer = part1 ? 5 : 0; + const part2Timer = part2 ? 8 : 0; + const part3Timer = part3 ? 8 : 0; + const part4Timer = part4 ? 9 : 0; - const sum = part1Timer + part2Timer + part3Timer + part4Timer; - setMinTimer(sum > 0 ? sum : 5); - }, [part1, part2, part3, part4]); + const sum = part1Timer + part2Timer + part3Timer + part4Timer; + setMinTimer(sum > 0 ? sum : 5); + }, [part1, part2, part3, part4]); - 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 = () => { - 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 submitExam = () => { + 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!"); - setIsLoading(true); + setIsLoading(true); - axios - .post(`/api/exam/listening/generate/listening`, { - parts, - 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); + axios + .post(`/api/exam/listening/generate/listening`, { + id: generate({minLength: 4, maxLength: 8, min: 3, max: 5, join: " ", formatter: capitalize}), + parts, + minTimer, + difficulty, + }) + .then((result) => { + playSound("sent"); + console.log(`Generated Exam ID: ${result.data.id}`); + toast.success(`Generated Exam ID: ${result.data.id}`); + setResultingExam(result.data); - setPart1(undefined); - setPart2(undefined); - setPart3(undefined); - setPart4(undefined); - setDifficulty(sample(DIFFICULTIES)!); - }) - .catch((error) => { - console.log(error); - toast.error("Something went wrong!"); - }) - .finally(() => setIsLoading(false)); - }; + setPart1(undefined); + setPart2(undefined); + setPart3(undefined); + setPart4(undefined); + setDifficulty(sample(DIFFICULTIES)!); + }) + .catch((error) => { + console.log(error); + toast.error("Something went wrong!"); + }) + .finally(() => setIsLoading(false)); + }; - const loadExam = async (examId: string) => { - const exam = await getExamById("listening", 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("listening", 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(["listening"]); + setExams([exam]); + setSelectedModules(["listening"]); - router.push("/exercises"); - }; + router.push("/exercises"); + }; - return ( - <> -
-
- - setMinTimer(parseInt(e) < 15 ? 15 : parseInt(e))} - value={minTimer} - className="max-w-[300px]" - /> -
-
- - setMinTimer(parseInt(e) < 15 ? 15 : parseInt(e))} + value={minTimer} + className="max-w-[300px]" + /> +
+
+ + - 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 ( + +
+ + + updatePart((part?: SpeakingPart) => { + if (part) { + return { + ...part, + questions: part.questions?.map((x, xIndex) => (xIndex === index ? value : x)), + } as SpeakingPart; + } - return part; - }) - } - /> - ))} -
- )} -
- ); + 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: generate({minLength: 4, maxLength: 8, min: 3, max: 5, join: " ", formatter: capitalize}), + 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(`Generated Exam ID: ${result.data.id}`); + 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]" + /> +
+
+ + x.value === statusFilter) - } + value={STATUS_OPTIONS.find((x) => x.value === statusFilter)} onChange={(value) => setStatusFilter((value?.value as TicketStatus) ?? undefined)} isClearable placeholder="Status..." @@ -278,7 +282,7 @@ export default function Tickets() { disabled={user.type === "agent"} value={getAssigneeValue()} onChange={(value) => - value ? setAssigneeFilter(value.value === "me" ? user.id : value.value) : setAssigneeFilter(undefined) + value ? setAssigneeFilter(value.value === "me" ? user.id : value.value!) : setAssigneeFilter(undefined) } placeholder="Assignee..." isClearable From 9b37b60be0de3461c1692ef1571d75c92ea660bb Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Tue, 30 Jul 2024 23:22:30 +0100 Subject: [PATCH 02/12] Improved the table --- src/components/PermissionList.tsx | 117 +++++++++++++----------------- 1 file changed, 51 insertions(+), 66 deletions(-) diff --git a/src/components/PermissionList.tsx b/src/components/PermissionList.tsx index a54b8541..828c6aac 100644 --- a/src/components/PermissionList.tsx +++ b/src/components/PermissionList.tsx @@ -1,77 +1,62 @@ -import { Permission } from "@/interfaces/permissions"; -import { - createColumnHelper, - flexRender, - getCoreRowModel, - useReactTable, -} from "@tanstack/react-table"; +import {Permission} from "@/interfaces/permissions"; +import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table"; import Link from "next/link"; +import {convertCamelCaseToReadable} from "@/utils/string"; - -interface Props{ - permissions: Permission[] +interface Props { + permissions: Permission[]; } const columnHelper = createColumnHelper(); const defaultColumns = [ - columnHelper.accessor('type', { - header: () => Type, - cell: ({row, getValue}) => ( - - {getValue() as string} - - ) - }) + columnHelper.accessor("type", { + header: () => Type, + cell: ({row, getValue}) => ( + + {convertCamelCaseToReadable(getValue() as string)} + + ), + }), ]; export default function PermissionList({permissions}: Props) { - - const table = useReactTable({ - data: permissions, - columns: defaultColumns, - getCoreRowModel: getCoreRowModel(), - - }) - return ( -
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - ))} - -
- {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} -
- {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} -
-
-
- ) + const table = useReactTable({ + data: permissions, + columns: defaultColumns, + getCoreRowModel: getCoreRowModel(), + }); + return ( +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+
+ ); } From 663b1aae4facebcc7dd15e260291685f72b30c9b Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 31 Jul 2024 10:42:47 +0100 Subject: [PATCH 03/12] Updated the yarn.lock --- yarn.lock | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/yarn.lock b/yarn.lock index 0fc9bc5b..619dadef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -696,6 +696,13 @@ dependencies: tslib "^2.1.0" +"@firebase/util@^1.9.7": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.9.7.tgz#c03b0ae065b3bba22800da0bd5314ef030848038" + integrity sha512-fBVNH/8bRbYjqlbIhZ+lBtdAAS4WqZumx03K06/u7fJSpz1TGjEMm1ImvKD47w+xaFKIP2ori6z8BrbakRfjJA== + dependencies: + tslib "^2.1.0" + "@firebase/webchannel-wrapper@0.9.0": version "0.9.0" resolved "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.9.0.tgz" @@ -6272,6 +6279,7 @@ wordwrap@^1.0.0: integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + name wrap-ansi-cjs version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 752a46b247fc26b15cc9c0335d5c638db97fd932 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 31 Jul 2024 19:15:36 +0100 Subject: [PATCH 04/12] Updated the LevelGeneration to be enabled by default --- src/pages/(generation)/LevelGeneration.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/(generation)/LevelGeneration.tsx b/src/pages/(generation)/LevelGeneration.tsx index 440099ec..3ebf73c0 100644 --- a/src/pages/(generation)/LevelGeneration.tsx +++ b/src/pages/(generation)/LevelGeneration.tsx @@ -296,7 +296,7 @@ const LevelGeneration = () => { module: "level", difficulty, variant: "full", - isDiagnostic: true, + isDiagnostic: false, parts: parts .map((part, index) => { const currentExercise = result.data.exercises[`exercise_${index + 1}`] as any; From a534126c61f7a523dac104cc793064c39df601dd Mon Sep 17 00:00:00 2001 From: Carlos Mesquita Date: Wed, 31 Jul 2024 20:44:46 +0100 Subject: [PATCH 05/12] training.tsx still a bit messy, all that is left is to retrieve data from firestore /training and /walkthrough and render it --- package-lock.json | 116 ++++ package.json | 5 +- src/components/AIDetection.tsx | 24 +- src/components/Sidebar.tsx | 4 + .../TrainingContent/AnimatedHighlight.tsx | 23 + src/components/TrainingContent/Exercise.tsx | 84 +++ .../TrainingContent/ExerciseWalkthrough.tsx | 287 +++++++++ .../TrainingContent/TrainingInterfaces.ts | 50 ++ src/pages/api/training/index.ts | 50 ++ src/pages/record.tsx | 189 +++--- src/pages/training.tsx | 553 ++++++++++++++++++ src/stores/recordStore.ts | 4 + src/stores/trainingContentStore.ts | 18 + tailwind.config.js | 5 +- tsconfig.json | 5 +- yarn.lock | 468 ++++++++------- 16 files changed, 1600 insertions(+), 285 deletions(-) create mode 100644 src/components/TrainingContent/AnimatedHighlight.tsx create mode 100644 src/components/TrainingContent/Exercise.tsx create mode 100644 src/components/TrainingContent/ExerciseWalkthrough.tsx create mode 100644 src/components/TrainingContent/TrainingInterfaces.ts create mode 100644 src/pages/api/training/index.ts create mode 100644 src/pages/training.tsx create mode 100644 src/stores/trainingContentStore.ts diff --git a/package-lock.json b/package-lock.json index 1f18f099..8bde9dc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@paypal/paypal-js": "^7.1.0", "@paypal/react-paypal-js": "^8.1.3", "@react-pdf/renderer": "^3.1.14", + "@react-spring/web": "^9.7.4", "@tanstack/react-table": "^8.10.1", "@types/node": "18.13.0", "@types/react": "18.0.27", @@ -2219,6 +2220,72 @@ "resolved": "https://registry.npmjs.org/@react-pdf/types/-/types-2.5.0.tgz", "integrity": "sha512-XsVRkt0hQ60I4e3leAVt+aZR3KJCaJd179BfJHAv4F4x6Vq3yqkry8lcbUWKGKDw1j3/8sW4FsgGR41SFvsG9A==" }, + "node_modules/@react-spring/animated": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.4.tgz", + "integrity": "sha512-7As+8Pty2QlemJ9O5ecsuPKjmO0NKvmVkRR1n6mEotFgWar8FKuQt2xgxz3RTgxcccghpx1YdS1FCdElQNexmQ==", + "dependencies": { + "@react-spring/shared": "~9.7.4", + "@react-spring/types": "~9.7.4" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.4.tgz", + "integrity": "sha512-GzjA44niEJBFUe9jN3zubRDDDP2E4tBlhNlSIkTChiNf9p4ZQlgXBg50qbXfSXHQPHak/ExYxwhipKVsQ/sUTw==", + "dependencies": { + "@react-spring/animated": "~9.7.4", + "@react-spring/shared": "~9.7.4", + "@react-spring/types": "~9.7.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.4.tgz", + "integrity": "sha512-mqDI6rW0Ca8IdryOMiXRhMtVGiEGLIO89vIOyFQXRIwwIMX30HLya24g9z4olDvFyeDW3+kibiKwtZnA4xhldA==" + }, + "node_modules/@react-spring/shared": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.4.tgz", + "integrity": "sha512-bEPI7cQp94dOtCFSEYpxvLxj0+xQfB5r9Ru1h8OMycsIq7zFZon1G0sHrBLaLQIWeMCllc4tVDYRTLIRv70C8w==", + "dependencies": { + "@react-spring/rafz": "~9.7.4", + "@react-spring/types": "~9.7.4" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.4.tgz", + "integrity": "sha512-iQVztO09ZVfsletMiY+DpT/JRiBntdsdJ4uqk3UJFhrhS8mIC9ZOZbmfGSRs/kdbNPQkVyzucceDicQ/3Mlj9g==" + }, + "node_modules/@react-spring/web": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.4.tgz", + "integrity": "sha512-UMvCZp7I5HCVIleSa4BwbNxynqvj+mJjG2m20VO2yPoi2pnCYANy58flvz9v/YcXTAvsmL655FV3pm5fbr6akA==", + "dependencies": { + "@react-spring/animated": "~9.7.4", + "@react-spring/core": "~9.7.4", + "@react-spring/shared": "~9.7.4", + "@react-spring/types": "~9.7.4" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", @@ -11551,6 +11618,55 @@ "resolved": "https://registry.npmjs.org/@react-pdf/types/-/types-2.5.0.tgz", "integrity": "sha512-XsVRkt0hQ60I4e3leAVt+aZR3KJCaJd179BfJHAv4F4x6Vq3yqkry8lcbUWKGKDw1j3/8sW4FsgGR41SFvsG9A==" }, + "@react-spring/animated": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.4.tgz", + "integrity": "sha512-7As+8Pty2QlemJ9O5ecsuPKjmO0NKvmVkRR1n6mEotFgWar8FKuQt2xgxz3RTgxcccghpx1YdS1FCdElQNexmQ==", + "requires": { + "@react-spring/shared": "~9.7.4", + "@react-spring/types": "~9.7.4" + } + }, + "@react-spring/core": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.4.tgz", + "integrity": "sha512-GzjA44niEJBFUe9jN3zubRDDDP2E4tBlhNlSIkTChiNf9p4ZQlgXBg50qbXfSXHQPHak/ExYxwhipKVsQ/sUTw==", + "requires": { + "@react-spring/animated": "~9.7.4", + "@react-spring/shared": "~9.7.4", + "@react-spring/types": "~9.7.4" + } + }, + "@react-spring/rafz": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.4.tgz", + "integrity": "sha512-mqDI6rW0Ca8IdryOMiXRhMtVGiEGLIO89vIOyFQXRIwwIMX30HLya24g9z4olDvFyeDW3+kibiKwtZnA4xhldA==" + }, + "@react-spring/shared": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.4.tgz", + "integrity": "sha512-bEPI7cQp94dOtCFSEYpxvLxj0+xQfB5r9Ru1h8OMycsIq7zFZon1G0sHrBLaLQIWeMCllc4tVDYRTLIRv70C8w==", + "requires": { + "@react-spring/rafz": "~9.7.4", + "@react-spring/types": "~9.7.4" + } + }, + "@react-spring/types": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.4.tgz", + "integrity": "sha512-iQVztO09ZVfsletMiY+DpT/JRiBntdsdJ4uqk3UJFhrhS8mIC9ZOZbmfGSRs/kdbNPQkVyzucceDicQ/3Mlj9g==" + }, + "@react-spring/web": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.4.tgz", + "integrity": "sha512-UMvCZp7I5HCVIleSa4BwbNxynqvj+mJjG2m20VO2yPoi2pnCYANy58flvz9v/YcXTAvsmL655FV3pm5fbr6akA==", + "requires": { + "@react-spring/animated": "~9.7.4", + "@react-spring/core": "~9.7.4", + "@react-spring/shared": "~9.7.4", + "@react-spring/types": "~9.7.4" + } + }, "@rushstack/eslint-patch": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", diff --git a/package.json b/package.json index 74a71187..a4d592bd 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@paypal/paypal-js": "^7.1.0", "@paypal/react-paypal-js": "^8.1.3", "@react-pdf/renderer": "^3.1.14", + "@react-spring/web": "^9.7.4", "@tanstack/react-table": "^8.10.1", "@types/node": "18.13.0", "@types/react": "18.0.27", @@ -67,6 +68,7 @@ "react-select": "^5.7.5", "react-string-replace": "^1.1.0", "react-toastify": "^9.1.2", + "react-tooltip": "^5.27.1", "react-xarrows": "^2.0.2", "read-excel-file": "^5.7.1", "short-unique-id": "5.0.2", @@ -77,8 +79,7 @@ "use-file-picker": "^2.1.0", "uuid": "^9.0.0", "wavesurfer.js": "^6.6.4", - "zustand": "^4.3.6", - "react-tooltip": "^5.27.1" + "zustand": "^4.3.6" }, "devDependencies": { "@types/blob-stream": "^0.1.33", diff --git a/src/components/AIDetection.tsx b/src/components/AIDetection.tsx index 4860ccde..29496800 100644 --- a/src/components/AIDetection.tsx +++ b/src/components/AIDetection.tsx @@ -9,7 +9,7 @@ import SegmentedProgressBar from "./SegmentedProgressBar"; // Colors and texts scrapped from gpt's zero react bundle const AIDetection: React.FC = ({ predicted_class, confidence_category, class_probabilities, sentences }) => { const probabilityTooltipContent = ` - GTP's Zero deep learning model predicts the
+ Encoach's deep learning model predicts the
probability this text has been entirely
generated by AI. For instance, a 40% AI
probability does not indicate that the text
@@ -19,7 +19,7 @@ const AIDetection: React.FC = ({ predicted_class, confide `; const confidenceTooltipContent = ` Confidence scores are a safeguard to better
- understand AI identification results. GTP Zero
+ understand AI identification results. Encoach
trained it's deep learning model on a diverse
dataset of millions of human and AI-written
documents. Green scores indicate that you can scan
@@ -32,19 +32,19 @@ const AIDetection: React.FC = ({ predicted_class, confide const confidenceKeywords = ["moderately", "highly", "confident", "uncertain"]; var confidence = { low: { - ai: "GPT Zero is uncertain about this text. If GPT Zero had to classify it, it would be considered", - human: "GPT Zero is uncertain about this text. If GPT Zero had to classify it, it would likely be considered", - mixed: "GPT Zero is uncertain about this text. If GPT Zero had to classify it, it would likely be a" + ai: "Encoach is uncertain about this text. If Encoach had to classify it, it would be considered", + human: "Encoach is uncertain about this text. If Encoach had to classify it, it would likely be considered", + mixed: "Encoach is uncertain about this text. If Encoach had to classify it, it would likely be a" }, medium: { - ai: "GPT Zero is moderately confident this text was", - human: "GPT Zero is moderately confident this text is entirely", - mixed: "GPT Zero is moderately confident this text is a" + ai: "Encoach is moderately confident this text was", + human: "Encoach is moderately confident this text is entirely", + mixed: "Encoach is moderately confident this text is a" }, high: { - ai: "GPT Zero is highly confident this text was", - human: "GPT Zero is highly confident this text is entirely", - mixed: "GPT Zero is highly confident this text is a" + ai: "Encoach is highly confident this text was", + human: "Encoach is highly confident this text is entirely", + mixed: "Encoach is highly confident this text is a" } } var classPrediction = { @@ -107,7 +107,7 @@ const AIDetection: React.FC = ({ predicted_class, confide
-

GPT Zero AI Detection Results

+

Encoach Detection Results

diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 42f3b2c8..15852575 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -14,6 +14,7 @@ import { BsClipboardData, BsFileLock, } from "react-icons/bs"; +import { CiDumbbell } from "react-icons/ci"; import {RiLogoutBoxFill} from "react-icons/ri"; import {SlPencil} from "react-icons/sl"; import {FaAward} from "react-icons/fa"; @@ -109,6 +110,9 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u {checkAccess(user, getTypesOfUser(["agent"]), "viewRecords") && (