diff --git a/src/components/ExamEditor/ExercisePicker/ExerciseWizard.tsx b/src/components/ExamEditor/ExercisePicker/ExerciseWizard.tsx index b82c3751..7a3150ef 100644 --- a/src/components/ExamEditor/ExercisePicker/ExerciseWizard.tsx +++ b/src/components/ExamEditor/ExercisePicker/ExerciseWizard.tsx @@ -8,6 +8,9 @@ import { IoTextOutline } from 'react-icons/io5'; import { Switch } from '@headlessui/react'; import useExamEditorStore from '@/stores/examEditor'; import { Module } from '@/interfaces'; +import { capitalize } from 'lodash'; +import Select from '@/components/Low/Select'; +import { Difficulty } from '@/interfaces/exam'; interface Props { module: Module; @@ -36,6 +39,16 @@ const ExerciseWizard: React.FC = ({ onDiscard, }) => { const [configurations, setConfigurations] = useState([]); + const { currentModule } = useExamEditorStore(); + const { difficulty } = useExamEditorStore(state => state.modules[currentModule]); + + const randomDiff = difficulty.length === 1 + ? capitalize(difficulty[0]) + : `Random (${difficulty.map(dif => capitalize(dif)).join(", ")})` as Difficulty; + + const DIFFICULTIES = difficulty.length === 1 + ? ["A1", "A2", "B1", "B2", "C1", "C2"] + : ["A1", "A2", "B1", "B2", "C1", "C2", randomDiff]; useEffect(() => { const initialConfigs = selectedExercises.map(exerciseType => { @@ -164,7 +177,7 @@ const ExerciseWizard: React.FC = ({ ); } - const inputValue = Number(config.params[param.param || '1'].toString()); + const inputValue = Number(config.params[param.param || '1'].toString()) || config.params[param.param!]; const isParagraphMatch = config.type.split("?name=")[1] === "paragraphMatch"; const maxParagraphs = isParagraphMatch ? extraArgs!.text.split("\n\n").length : 50; @@ -183,9 +196,23 @@ const ExerciseWizard: React.FC = ({ )} + {param.param === "difficulty" ? + handleParameterChange( exerciseIndex, param.param || '', @@ -195,6 +222,8 @@ const ExerciseWizard: React.FC = ({ min={1} max={maxParagraphs} /> + } + ); }; diff --git a/src/components/ExamEditor/ExercisePicker/exercises.ts b/src/components/ExamEditor/ExercisePicker/exercises.ts index 3613f519..e0760158 100644 --- a/src/components/ExamEditor/ExercisePicker/exercises.ts +++ b/src/components/ExamEditor/ExercisePicker/exercises.ts @@ -20,7 +20,6 @@ import { FaQuestionCircle, } from 'react-icons/fa'; import { ExerciseGen } from './generatedExercises'; -import { MdRadioButtonChecked } from 'react-icons/md'; import { BsListCheck } from 'react-icons/bs'; const quantity = (quantity: number, tooltip?: string) => { @@ -32,6 +31,14 @@ const quantity = (quantity: number, tooltip?: string) => { } } +const difficulty = () => { + return { + param: "difficulty", + label: "Difficulty", + tooltip: "Exercise difficulty", + } +} + const generate = () => { return { param: "generate", @@ -52,6 +59,7 @@ const reading = (passage: number) => { value: "multipleChoice" }, quantity(5, "Quantity of Multiple Choice Questions"), + difficulty(), generate() ], module: "reading" @@ -73,6 +81,7 @@ const reading = (passage: number) => { value: 1 }, quantity(4, "Quantity of Blanks"), + difficulty(), generate() ], module: "reading" @@ -94,6 +103,7 @@ const reading = (passage: number) => { value: 3 }, quantity(4, "Quantity of Blanks"), + difficulty(), generate() ], module: "reading" @@ -109,6 +119,7 @@ const reading = (passage: number) => { value: "trueFalse" }, quantity(4, "Quantity of Statements"), + difficulty(), generate() ], module: "reading" @@ -124,6 +135,7 @@ const reading = (passage: number) => { value: "paragraphMatch" }, quantity(5, "Quantity of Matches"), + difficulty(), generate() ], module: "reading" @@ -143,6 +155,7 @@ const reading = (passage: number) => { value: "ideaMatch" }, quantity(5, "Quantity of Ideas"), + difficulty(), generate() ], module: "reading" @@ -165,6 +178,7 @@ const listening = (section: number) => { value: section == 3 ? "multipleChoice3Options" : "multipleChoice" }, quantity(5, "Quantity of Multiple Choice Questions"), + difficulty(), generate() ], module: "listening" @@ -180,6 +194,7 @@ const listening = (section: number) => { value: "writeBlanksQuestions" }, quantity(5, "Quantity of Blanks"), + difficulty(), generate() ], module: "listening" @@ -195,6 +210,7 @@ const listening = (section: number) => { value: "trueFalse" }, quantity(4, "Quantity of Statements"), + difficulty(), generate() ], module: "listening" @@ -214,6 +230,7 @@ const listening = (section: number) => { value: "writeBlanksFill" }, quantity(5, "Quantity of Blanks"), + difficulty(), generate() ], module: "listening" @@ -231,6 +248,7 @@ const listening = (section: number) => { value: "writeBlanksForm" }, quantity(5, "Quantity of Blanks"), + difficulty(), generate() ], module: "listening" @@ -251,6 +269,7 @@ const EXERCISES: ExerciseGen[] = [ value: "multipleChoice" }, quantity(10, "Amount"), + difficulty(), generate() ], module: "level" @@ -265,6 +284,7 @@ const EXERCISES: ExerciseGen[] = [ value: "mcBlank" }, quantity(10, "Amount"), + difficulty(), generate() ], module: "level" @@ -279,6 +299,7 @@ const EXERCISES: ExerciseGen[] = [ value: "mcUnderline" }, quantity(10, "Amount"), + difficulty(), generate() ], module: "level" @@ -294,6 +315,7 @@ const EXERCISES: ExerciseGen[] = [ param: "text_size", value: "250" }, + difficulty(), generate() ], module: "level" @@ -313,6 +335,7 @@ const EXERCISES: ExerciseGen[] = [ param: "text_size", value: "250" }, + difficulty(), generate() ], module: "level" @@ -345,6 +368,7 @@ const EXERCISES: ExerciseGen[] = [ param: "text_size", value: "700" }, + difficulty(), generate() ], module: "level" @@ -360,6 +384,7 @@ const EXERCISES: ExerciseGen[] = [ value: "", type: "text" }, + difficulty(), generate() ], module: "writing" @@ -375,6 +400,7 @@ const EXERCISES: ExerciseGen[] = [ value: "", type: "text" }, + difficulty(), generate() ], module: "writing" @@ -384,6 +410,7 @@ const EXERCISES: ExerciseGen[] = [ type: "speaking_1", icon: FaComments, extra: [ + difficulty(), generate(), { label: "First Topic", @@ -405,6 +432,7 @@ const EXERCISES: ExerciseGen[] = [ type: "speaking_2", icon: FaUserFriends, extra: [ + difficulty(), generate(), { label: "Topic", @@ -420,6 +448,7 @@ const EXERCISES: ExerciseGen[] = [ type: "speaking_3", icon: FaHandshake, extra: [ + difficulty(), generate(), { label: "Topic", diff --git a/src/components/ExamEditor/ExercisePicker/generatedExercises.ts b/src/components/ExamEditor/ExercisePicker/generatedExercises.ts index 5e1d38a3..1a78dcf9 100644 --- a/src/components/ExamEditor/ExercisePicker/generatedExercises.ts +++ b/src/components/ExamEditor/ExercisePicker/generatedExercises.ts @@ -17,6 +17,6 @@ export interface ExerciseGen { type: string; icon: IconType; sectionId?: number; - extra?: { param?: string; value?: string | number | boolean; label?: string; tooltip?: string, type?: string}[]; + extra?: { param: string; value?: string | number | boolean; label?: string; tooltip?: string, type?: string}[]; module: string } diff --git a/src/components/ExamEditor/ExercisePicker/index.tsx b/src/components/ExamEditor/ExercisePicker/index.tsx index 48f7cde2..06bce493 100644 --- a/src/components/ExamEditor/ExercisePicker/index.tsx +++ b/src/components/ExamEditor/ExercisePicker/index.tsx @@ -13,12 +13,13 @@ import { BsArrowRepeat } from "react-icons/bs"; interface ExercisePickerProps { module: string; sectionId: number; - difficulty: string; extraArgs?: Record; levelSectionId?: number; level?: boolean; } +const DIFFICULTIES: string[] = ["A1", "A2", "B1", "B2", "C1", "C2"]; + const ExercisePicker: React.FC = ({ module, sectionId, @@ -26,7 +27,7 @@ const ExercisePicker: React.FC = ({ levelSectionId, level = false }) => { - const { currentModule, dispatch } = useExamEditorStore(); + const { currentModule } = useExamEditorStore(); const { difficulty, sections } = useExamEditorStore((store) => store.modules[level ? "level" : currentModule]); const section = sections.find((s) => s.sectionId === (level ? levelSectionId : sectionId)); @@ -67,6 +68,9 @@ const ExercisePicker: React.FC = ({ }), ...(config.params.max_words !== undefined && { max_words: Number(config.params.max_words) + }), + ...(DIFFICULTIES.includes(config.params.difficulty as string) && { + difficulty: config.params.difficulty }) }; }); @@ -100,8 +104,8 @@ const ExercisePicker: React.FC = ({ method: 'POST', body: { ...context, - exercises: exercises, - difficulty: difficulty + exercises, + difficulty } }, (data: any) => [{ @@ -112,7 +116,11 @@ const ExercisePicker: React.FC = ({ ); } else if (module === "writing") { configurations.forEach((config) => { - let queryParams = config.params.topic !== '' ? { topic: config.params.topic as string } : undefined; + let queryParams = { + difficulty: config.params.difficulty ? config.params.difficulty as string: difficulty, + ...(config.params.topic !== '' && { topic: config.params.topic as string }) + }; + generate( config.type === 'writing_letter' ? 1 : 2, "writing", @@ -122,7 +130,8 @@ const ExercisePicker: React.FC = ({ queryParams }, (data: any) => [{ - prompt: data.question + prompt: data.question, + difficulty: data.difficulty }], levelSectionId, level @@ -135,6 +144,7 @@ const ExercisePicker: React.FC = ({ topic: config.params.topic as string, first_topic: config.params.first_topic as string, second_topic: config.params.second_topic as string, + difficulty: config.params.difficulty ? config.params.difficulty as string: difficulty, }).filter(([_, value]) => value && value !== '') ); let query = Object.keys(queryParams).length === 0 ? undefined : queryParams; @@ -152,19 +162,22 @@ const ExercisePicker: React.FC = ({ return [{ prompts: data.questions, first_topic: data.first_topic, - second_topic: data.second_topic + second_topic: data.second_topic, + difficulty: data.difficulty }]; case 2: return [{ topic: data.topic, question: data.question, prompts: data.prompts, - suffix: data.suffix + suffix: data.suffix, + difficulty: data.difficulty }]; case 3: return [{ topic: data.topic, - questions: data.questions + questions: data.questions, + difficulty: data.difficulty }]; default: return [data]; diff --git a/src/components/ExamEditor/Exercises/Blanks/Letters/index.tsx b/src/components/ExamEditor/Exercises/Blanks/Letters/index.tsx index 82bccf04..7ca7433e 100644 --- a/src/components/ExamEditor/Exercises/Blanks/Letters/index.tsx +++ b/src/components/ExamEditor/Exercises/Blanks/Letters/index.tsx @@ -1,5 +1,5 @@ -import { FillBlanksExercise, ReadingPart } from "@/interfaces/exam"; -import { useEffect, useReducer, useState } from "react"; +import { Difficulty, FillBlanksExercise, ReadingPart } from "@/interfaces/exam"; +import { useCallback, useEffect, useReducer, useState } from "react"; import BlanksEditor from ".."; import { Card, CardContent } from "@/components/ui/card"; import { MdEdit, MdEditOff } from "react-icons/md"; @@ -21,6 +21,7 @@ interface Word { const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: number }> = ({ exercise, sectionId }) => { const { currentModule, dispatch } = useExamEditorStore(); + const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty); const { state } = useExamEditorStore( (state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)! ); @@ -249,12 +250,24 @@ const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: num setEditingAlert(editing, setAlerts); }, [editing]) + const saveDifficulty = useCallback((diff: Difficulty) => { + if (!difficulty.includes(diff)) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, diff]} } }); + } + const updatedExercise = { ...exercise, difficulty: diff }; + const newState = { ...section }; + newState.exercises = newState.exercises.map((ex) => ex.id === exercise.id ? updatedExercise : ex ); + dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } }); + }, [currentModule, difficulty, dispatch, exercise, section, sectionId]); + return (
= ({ exercise, sectionId }) => { const { currentModule, dispatch } = useExamEditorStore(); + const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty); const { state } = useExamEditorStore( (state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)! ); @@ -257,12 +258,24 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number } // eslint-disable-next-line react-hooks/exhaustive-deps }, [blanksState.blanks]); + const saveDifficulty = useCallback((diff: Difficulty) => { + if (!difficulty.includes(diff)) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, diff]} } }); + } + const updatedExercise = { ...exercise, difficulty: diff }; + const newState = { ...section }; + newState.exercises = newState.exercises.map((ex) => ex.id === exercise.id ? updatedExercise : ex ); + dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } }); + }, [currentModule, difficulty, dispatch, exercise, section, sectionId]); + return (
= ({ exercise, sectionId }) => { const { currentModule, dispatch } = useExamEditorStore(); + const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty); const { state } = useExamEditorStore( (state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)! ); @@ -161,6 +162,16 @@ const WriteBlanksFill: React.FC<{ exercise: WriteBlanksExercise; sectionId: numb setEditingAlert(editing, setAlerts); }, [editing]); + const saveDifficulty = useCallback((diff: Difficulty) => { + if (!difficulty.includes(diff)) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, diff]} } }); + } + const updatedExercise = { ...exercise, difficulty: diff }; + const newState = { ...section }; + newState.exercises = newState.exercises.map((ex) => ex.id === exercise.id ? updatedExercise : ex ); + dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } }); + }, [currentModule, difficulty, dispatch, exercise, section, sectionId]); + return (
setSelectedBlankId(blankId?.toString() || null)} diff --git a/src/components/ExamEditor/Exercises/Blanks/index.tsx b/src/components/ExamEditor/Exercises/Blanks/index.tsx index 21e40c30..bda25eb7 100644 --- a/src/components/ExamEditor/Exercises/Blanks/index.tsx +++ b/src/components/ExamEditor/Exercises/Blanks/index.tsx @@ -20,12 +20,15 @@ import { Card, CardContent } from "@/components/ui/card"; import { Blank, DropZone } from "./DragNDrop"; import { getTextSegments, BlankState, BlanksState, BlanksAction, BlankToken } from "./BlanksReducer"; import PromptEdit from "../Shared/PromptEdit"; +import { Difficulty } from "@/interfaces/exam"; interface Props { title?: string; initialText: string; description: string; + difficulty?: Difficulty; + saveDifficulty: (difficulty: Difficulty) => void; state: BlanksState; module: string; editing: boolean; @@ -49,6 +52,8 @@ const BlanksEditor: React.FC = ({ title = "Fill Blanks", initialText, description, + difficulty, + saveDifficulty, state, editing, module, @@ -169,6 +174,8 @@ const BlanksEditor: React.FC = ({ title={title} description={description} editing={editing} + difficulty={difficulty} + saveDifficulty={saveDifficulty} handleSave={onSave} handleDelete={onDelete} handleDiscard={onDiscard} diff --git a/src/components/ExamEditor/Exercises/MatchSentences/index.tsx b/src/components/ExamEditor/Exercises/MatchSentences/index.tsx index 52a6ee3f..9bb63867 100644 --- a/src/components/ExamEditor/Exercises/MatchSentences/index.tsx +++ b/src/components/ExamEditor/Exercises/MatchSentences/index.tsx @@ -1,10 +1,10 @@ -import React, { useState, useMemo, useEffect } from 'react'; +import React, { useState, useMemo, useEffect, useCallback } from 'react'; import { MdAdd, MdVisibility, MdVisibilityOff } from 'react-icons/md'; -import { MatchSentencesExercise, ReadingPart } from '@/interfaces/exam'; +import { Difficulty, MatchSentencesExercise, ReadingPart } from '@/interfaces/exam'; import Alert, { AlertItem } from '../Shared/Alert'; import ReferenceViewer from './ParagraphViewer'; import Header from '../../Shared/Header'; @@ -21,6 +21,7 @@ import PromptEdit from '../Shared/PromptEdit'; const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: number }> = ({ exercise, sectionId }) => { const { currentModule, dispatch } = useExamEditorStore(); + const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty); const { state } = useExamEditorStore( (state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)! ); @@ -147,12 +148,24 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu updateLocal(handleMatchSentencesReorder(event, local)); } + const saveDifficulty = useCallback((diff: Difficulty) => { + if (!difficulty.includes(diff)) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, diff]} } }); + } + const updatedExercise = { ...exercise, difficulty: diff }; + const newState = { ...section }; + newState.exercises = newState.exercises.map((ex) => ex.id === exercise.id ? updatedExercise : ex ); + dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } }); +}, [currentModule, difficulty, dispatch, exercise, section, sectionId]); + return (
{ const { currentModule, dispatch } = useExamEditorStore(); + const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty); const { state } = useExamEditorStore( (state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)! ); @@ -113,12 +114,24 @@ const UnderlineMultipleChoice: React.FC<{exercise: MultipleChoiceExercise, secti } }); + const saveDifficulty = useCallback((diff: Difficulty) => { + if (!difficulty.includes(diff)) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, diff]} } }); + } + const updatedExercise = { ...exercise, difficulty: diff }; + const newState = { ...section }; + newState.exercises = newState.exercises.map((ex) => ex.id === exercise.id ? updatedExercise : ex ); + dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } }); + }, [currentModule, difficulty, dispatch, exercise, section, sectionId]); + return (
= ({ exercise, sectionId, optionsQuantity }) => { - const { currentModule, dispatch} = useExamEditorStore(); + const { currentModule, dispatch } = useExamEditorStore(); + const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty); const { state } = useExamEditorStore( (state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)! ); @@ -202,12 +203,24 @@ const MultipleChoice: React.FC = ({ exercise, sectionId, op setLocal(handleMultipleChoiceReorder(event, local)); }; + const saveDifficulty = useCallback((diff: Difficulty) => { + if (!difficulty.includes(diff)) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, diff]} } }); + } + const updatedExercise = { ...exercise, difficulty: diff }; + const newState = { ...section }; + newState.exercises = newState.exercises.map((ex) => ex.id === exercise.id ? updatedExercise : ex ); + dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } }); + }, [currentModule, difficulty, dispatch, exercise, section, sectionId]); + return (
= ({ sectionId, exercise, module = "speaking" }) => { - const { dispatch } = useExamEditorStore(); + const { currentModule, dispatch } = useExamEditorStore(); + const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty); const [local, setLocal] = useState(exercise); const [currentVideoIndex, setCurrentVideoIndex] = useState(0); @@ -105,13 +106,17 @@ const InteractiveSpeaking: React.FC = ({ sectionId, exercise, module = "s useEffect(() => { if (genResult && generating === "speakingScript") { + if (!difficulty.includes(genResult.result[0].difficulty)) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, genResult.result[0].difficulty]} } }); + } const updatedLocal = { ...local, title: genResult.result[0].title, prompts: genResult.result[0].prompts.map((item: any) => ({ text: item || "", video_url: "" - })) + })), + difficulty: genResult.result[0].difficulty }; setEditing(true); setLocal(updatedLocal); @@ -158,13 +163,17 @@ const InteractiveSpeaking: React.FC = ({ sectionId, exercise, module = "s const isGenerating = levelGenerating?.includes(`${local.id}-speakingScript`); if (speakingScript && isGenerating) { + if (!difficulty.includes(speakingScript.result[0].difficulty)) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, speakingScript.result[0].difficulty]} } }); + } const updatedLocal = { ...local, title: speakingScript.result[0].title, prompts: speakingScript.result[0].prompts.map((item: any) => ({ text: item || "", video_url: "" - })) + })), + difficulty: speakingScript.result[0].difficulty }; setEditing(true); setLocal(updatedLocal); @@ -264,6 +273,21 @@ const InteractiveSpeaking: React.FC = ({ sectionId, exercise, module = "s ); }; + const saveDifficulty = useCallback((diff: Difficulty)=> { + if (!difficulty.includes(diff)) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, diff]} } }); + } + if (module !== "level") { + const updatedExercise = { ...exercise, difficulty: diff }; + dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: updatedExercise, module: currentModule } }); + } else { + const updatedExercise = { ...exercise, difficulty: diff }; + const newState = { ...state as LevelPart }; + newState.exercises = (newState as LevelPart).exercises.map((ex) => ex.id === exercise.id ? updatedExercise : ex ); + dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } }); + } + }, [currentModule, difficulty, dispatch, exercise, module, sectionId, state]); + return ( <>
@@ -271,6 +295,8 @@ const InteractiveSpeaking: React.FC = ({ sectionId, exercise, module = "s title={`Interactive Speaking Script`} description='Generate or write the scripts for the videos.' editing={editing} + difficulty={exercise.difficulty} + saveDifficulty={saveDifficulty} handleSave={handleSave} handleEdit={handleEdit} handleDiscard={handleDiscard} diff --git a/src/components/ExamEditor/Exercises/Speaking/Speaking1.tsx b/src/components/ExamEditor/Exercises/Speaking/Speaking1.tsx index 624dafbe..e979525e 100644 --- a/src/components/ExamEditor/Exercises/Speaking/Speaking1.tsx +++ b/src/components/ExamEditor/Exercises/Speaking/Speaking1.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea"; import { Card, CardContent } from "@/components/ui/card"; import { AiOutlineUnorderedList, AiOutlinePlus, AiOutlineDelete } from 'react-icons/ai'; @@ -6,7 +6,7 @@ import Header from "../../Shared/Header"; import GenLoader from "../Shared/GenLoader"; import useSectionEdit from "../../Hooks/useSectionEdit"; import useExamEditorStore from "@/stores/examEditor"; -import { InteractiveSpeakingExercise, LevelPart } from "@/interfaces/exam"; +import { Difficulty, InteractiveSpeakingExercise, LevelPart } from "@/interfaces/exam"; import { BsFileText } from "react-icons/bs"; import { RiVideoLine } from 'react-icons/ri'; import { FaChevronLeft, FaChevronRight } from 'react-icons/fa6'; @@ -19,7 +19,8 @@ interface Props { } const Speaking1: React.FC = ({ sectionId, exercise, module = "speaking" }) => { - const { dispatch } = useExamEditorStore(); + const { currentModule, dispatch } = useExamEditorStore(); + const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty); const [local, setLocal] = useState(() => { const defaultPrompts = [ { text: "Hello my name is {avatar}, what is yours?", video_url: "" }, @@ -118,6 +119,9 @@ const Speaking1: React.FC = ({ sectionId, exercise, module = "speaking" } useEffect(() => { if (genResult && generating === "speakingScript") { + if (!difficulty.includes(genResult.result[0].difficulty)) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, genResult.result[0].difficulty]} } }); + } const updatedLocal = { ...local, first_title: genResult.result[0].first_topic, @@ -129,7 +133,8 @@ const Speaking1: React.FC = ({ sectionId, exercise, module = "speaking" } text: item, video_url: "" })) - ] + ], + difficulty: genResult.result[0].difficulty }; setEditing(true); setLocal(updatedLocal); @@ -176,10 +181,14 @@ const Speaking1: React.FC = ({ sectionId, exercise, module = "speaking" } const isGenerating = levelGenerating?.includes(`${local.id}-speakingScript`); if (speakingScript && isGenerating) { + if (!difficulty.includes(speakingScript.result[0].difficulty)) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, speakingScript.result[0].difficulty]} } }); + } const updatedLocal = { ...local, first_title: speakingScript.result[0].first_topic, second_title: speakingScript.result[0].second_topic, + difficulty: speakingScript.result[0].difficulty, prompts: [ local.prompts[0], local.prompts[1], @@ -300,6 +309,21 @@ const Speaking1: React.FC = ({ sectionId, exercise, module = "speaking" } ); }; + const saveDifficulty = useCallback((diff: Difficulty)=> { + if (!difficulty.includes(diff)) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, diff]} } }); + } + if (module !== "level") { + const updatedExercise = { ...exercise, difficulty: diff }; + dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: updatedExercise, module: currentModule } }); + } else { + const updatedExercise = { ...exercise, difficulty: diff }; + const newState = { ...state as LevelPart }; + newState.exercises = (newState as LevelPart).exercises.map((ex) => ex.id === exercise.id ? updatedExercise : ex ); + dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } }); + } + }, [currentModule, difficulty, dispatch, exercise, module, sectionId, state]); + return ( <>
@@ -307,6 +331,8 @@ const Speaking1: React.FC = ({ sectionId, exercise, module = "speaking" } title={`Speaking 1 Script`} description='Generate or write the scripts for the videos.' editing={editing} + difficulty={exercise.difficulty} + saveDifficulty={saveDifficulty} handleSave={handleSave} handleEdit={handleEdit} handleDiscard={handleDiscard} diff --git a/src/components/ExamEditor/Exercises/Speaking/Speaking2.tsx b/src/components/ExamEditor/Exercises/Speaking/Speaking2.tsx index 5b21fc69..b08988a4 100644 --- a/src/components/ExamEditor/Exercises/Speaking/Speaking2.tsx +++ b/src/components/ExamEditor/Exercises/Speaking/Speaking2.tsx @@ -1,9 +1,9 @@ import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea"; import { Card, CardContent } from "@/components/ui/card"; import { AiOutlinePlus, AiOutlineDelete } from 'react-icons/ai'; -import { LevelPart, SpeakingExercise } from "@/interfaces/exam"; +import { Difficulty, LevelPart, SpeakingExercise } from "@/interfaces/exam"; import useExamEditorStore from "@/stores/examEditor"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import useSectionEdit from "../../Hooks/useSectionEdit"; import Header from "../../Shared/Header"; import { Tooltip } from "react-tooltip"; @@ -21,7 +21,8 @@ interface Props { } const Speaking2: React.FC = ({ sectionId, exercise, module = "speaking" }) => { - const { dispatch } = useExamEditorStore(); + const { currentModule, dispatch } = useExamEditorStore(); + const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty); const [local, setLocal] = useState(exercise); const { sections } = useExamEditorStore((store) => store.modules[module]); @@ -104,11 +105,15 @@ const Speaking2: React.FC = ({ sectionId, exercise, module = "speaking" } useEffect(() => { if (genResult && generating === "speakingScript") { + if (!difficulty.includes(genResult.result[0].difficulty)) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, genResult.result[0].difficulty]} } }); + } const updatedLocal = { ...local, title: genResult.result[0].topic, text: genResult.result[0].question, - prompts: genResult.result[0].prompts + prompts: genResult.result[0].prompts, + difficulty: genResult.result[0].difficulty }; setEditing(true); setLocal(updatedLocal); @@ -153,11 +158,15 @@ const Speaking2: React.FC = ({ sectionId, exercise, module = "speaking" } const speakingScript = levelGenResults.find((res) => res.generating === `${local.id}-speakingScript`); const generating = levelGenerating.find((res) => res === `${local.id}-speakingScript`); if (speakingScript && generating) { + if (!difficulty.includes(speakingScript.result[0].difficulty)) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, speakingScript.result[0].difficulty]} } }); + } const updatedLocal = { ...local, title: speakingScript.result[0].topic, text: speakingScript.result[0].question, - prompts: speakingScript.result[0].prompts + prompts: speakingScript.result[0].prompts, + difficulty: speakingScript.result[0].difficulty }; setEditing(true); setLocal(updatedLocal); @@ -244,6 +253,21 @@ const Speaking2: React.FC = ({ sectionId, exercise, module = "speaking" }
`; + const saveDifficulty = useCallback((diff: Difficulty)=> { + if (!difficulty.includes(diff)) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, diff]} } }); + } + if (module !== "level") { + const updatedExercise = { ...exercise, difficulty: diff }; + dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: updatedExercise, module: currentModule } }); + } else { + const updatedExercise = { ...exercise, difficulty: diff }; + const newState = { ...state as LevelPart }; + newState.exercises = (newState as LevelPart).exercises.map((ex) => ex.id === exercise.id ? updatedExercise : ex ); + dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } }); + } + }, [currentModule, difficulty, dispatch, exercise, module, sectionId, state]); + return ( <>
@@ -251,6 +275,8 @@ const Speaking2: React.FC = ({ sectionId, exercise, module = "speaking" } title={`Speaking ${module === "level" ? local.sectionId : sectionId} Script`} description='Generate or write the script for the video.' editing={editing} + difficulty={exercise.difficulty} + saveDifficulty={saveDifficulty} handleSave={handleSave} handleEdit={handleEdit} handleDiscard={handleDiscard} diff --git a/src/components/ExamEditor/Exercises/TrueFalse/index.tsx b/src/components/ExamEditor/Exercises/TrueFalse/index.tsx index be850a86..068c3e42 100644 --- a/src/components/ExamEditor/Exercises/TrueFalse/index.tsx +++ b/src/components/ExamEditor/Exercises/TrueFalse/index.tsx @@ -1,9 +1,9 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { MdAdd, } from 'react-icons/md'; import Alert, { AlertItem } from '../Shared/Alert'; -import { ReadingPart, TrueFalseExercise } from '@/interfaces/exam'; +import { Difficulty, ReadingPart, TrueFalseExercise } from '@/interfaces/exam'; import QuestionsList from '../Shared/QuestionsList'; import Header from '../../Shared/Header'; import SortableQuestion from '../Shared/SortableQuestion'; @@ -19,6 +19,7 @@ import PromptEdit from '../Shared/PromptEdit'; const TrueFalse: React.FC<{ exercise: TrueFalseExercise, sectionId: number }> = ({ exercise, sectionId }) => { const { currentModule, dispatch } = useExamEditorStore(); + const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty); const { state } = useExamEditorStore( (state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)! ); @@ -131,12 +132,24 @@ const TrueFalse: React.FC<{ exercise: TrueFalseExercise, sectionId: number }> = setLocal(handleTrueFalseReorder(event, local)); } + const saveDifficulty = useCallback((diff: Difficulty) => { + if (!difficulty.includes(diff)) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, diff]} } }); + } + const updatedExercise = { ...exercise, difficulty: diff }; + const newState = { ...section }; + newState.exercises = newState.exercises.map((ex) => ex.id === exercise.id ? updatedExercise : ex ); + dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } }); + }, [currentModule, difficulty, dispatch, exercise, section, sectionId]); + return (
= ({ sectionId, exercise }) => { - const { currentModule, dispatch } = useExamEditorStore(); + const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty); const { state } = useExamEditorStore( (state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)! ); @@ -231,12 +231,24 @@ const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise; }, [local.solutions]); + const saveDifficulty = useCallback((diff: Difficulty) => { + if (!difficulty.includes(diff)) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, diff]} } }); + } + const updatedExercise = { ...exercise, difficulty: diff }; + const newState = { ...section }; + newState.exercises = newState.exercises.map((ex) => ex.id === exercise.id ? updatedExercise : ex ); + dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } }); + }, [currentModule, difficulty, dispatch, exercise, section, sectionId]); + return (
= ({ sectionId, exercise }) => { const { currentModule, dispatch } = useExamEditorStore(); + const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty); const { state } = useExamEditorStore( (state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)! ); @@ -208,12 +209,24 @@ const WriteBlanksForm: React.FC<{ sectionId: number; exercise: WriteBlanksExerci updateLocal({ ...local, text: newText }); }; + const saveDifficulty = useCallback((diff: Difficulty) => { + if (!difficulty.includes(diff)) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, diff]} } }); + } + const updatedExercise = { ...exercise, difficulty: diff }; + const newState = { ...section }; + newState.exercises = newState.exercises.map((ex) => ex.id === exercise.id ? updatedExercise : ex ); + dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } }); + }, [currentModule, difficulty, dispatch, exercise, section, sectionId]); + return (
= ({ sectionId, exercise, module, index }) => { const { currentModule, dispatch } = useExamEditorStore(); + const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty); const { type, academic_url } = useExamEditorStore( (state) => state.modules[currentModule] ); @@ -39,6 +40,7 @@ const Writing: React.FC = ({ sectionId, exercise, module, index }) => { onSave: () => { const newExercise = { ...local } as WritingExercise; newExercise.prompt = prompt; + newExercise.difficulty = exercise.difficulty; setAlerts([]); setEditing(false); if (!level) { @@ -86,6 +88,12 @@ const Writing: React.FC = ({ sectionId, exercise, module, index }) => { if (genResult) { setEditing(true); setPrompt(genResult.result[0].prompt); + if (!difficulty.includes(genResult.result[0].difficulty)) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, genResult.result[0].difficulty]} } }); + } + const updatedExercise = { ...exercise, difficulty: genResult.result[0].difficulty }; + dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: updatedExercise, module: currentModule } }); + dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "generating", value: undefined } }) } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -95,6 +103,21 @@ const Writing: React.FC = ({ sectionId, exercise, module, index }) => { setEditingAlert(prompt !== local.prompt, setAlerts); }, [prompt, local.prompt]); + const saveDifficulty = useCallback((diff: Difficulty)=> { + if (!difficulty.includes(diff)) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, diff]} } }); + } + if (!level) { + const updatedExercise = { ...exercise, difficulty: diff }; + dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: updatedExercise, module: currentModule } }); + } else { + const updatedExercise = { ...exercise, difficulty: diff }; + const newState = { ...state as LevelPart }; + newState.exercises = (newState as LevelPart).exercises.map((ex) => ex.id === exercise.id ? updatedExercise : ex ); + dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } }); + } + }, [currentModule, difficulty, dispatch, exercise, level, sectionId, state]); + return ( <>
@@ -102,6 +125,8 @@ const Writing: React.FC = ({ sectionId, exercise, module, index }) => { title={`${sectionId === 1 ? (type === "academic" ? "Visual Information" : "Letter") : "Essay"} Instructions`} description='Generate or edit the instructions for the task' editing={editing} + difficulty={exercise.difficulty} + saveDifficulty={saveDifficulty} handleSave={handleSave} handleDelete={module == "level" ? handleDelete : undefined} handleEdit={handleEdit} diff --git a/src/components/ExamEditor/SectionRenderer/SectionExercises/index.tsx b/src/components/ExamEditor/SectionRenderer/SectionExercises/index.tsx index c6dc6f03..b86a067d 100644 --- a/src/components/ExamEditor/SectionRenderer/SectionExercises/index.tsx +++ b/src/components/ExamEditor/SectionRenderer/SectionExercises/index.tsx @@ -1,6 +1,6 @@ import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; import SortableSection from "../../Shared/SortableSection"; -import { Exercise, InteractiveSpeakingExercise, LevelPart, ListeningPart, ReadingPart, SpeakingExercise, WritingExercise } from "@/interfaces/exam"; +import { Difficulty, Exercise, InteractiveSpeakingExercise, LevelPart, ListeningPart, ReadingPart, SpeakingExercise, WritingExercise } from "@/interfaces/exam"; import ExerciseItem from "./types"; import Dropdown from "@/components/Dropdown"; import useExamEditorStore from "@/stores/examEditor"; @@ -32,8 +32,7 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => { const dispatch = useExamEditorStore(state => state.dispatch); const currentModule = useExamEditorStore(state => state.currentModule); - const sections = useExamEditorStore(state => state.modules[currentModule].sections); - const expandedSections = useExamEditorStore(state => state.modules[currentModule].expandedSections); + const {sections, expandedSections, difficulty} = useExamEditorStore(state => state.modules[currentModule]); const section = useExamEditorStore( state => state.modules[currentModule].sections.find( @@ -50,6 +49,15 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => { useEffect(() => { if (genResult && genResult.generating === "exercises" && genResult.module === currentModule) { const newExercises = genResult.result[0].exercises; + + const newDifficulties = newExercises + .map((ex: Exercise) => ex.difficulty) + .filter((diff: Difficulty) => !difficulty.includes(diff)); + + if (newDifficulties.length > 0) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, ...newDifficulties]} } }); + } + dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, @@ -85,6 +93,18 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => { ) => { const nonWritingOrSpeaking = results[0]?.generating.startsWith("exercises"); + const newExercises = assignExercisesFn(results); + + const newDifficulties = newExercises + .map((ex: Exercise) => ex.difficulty) + .filter((diff: Difficulty | undefined): diff is Difficulty => + diff !== undefined && !difficulty.includes(diff) + ); + + if (newDifficulties.length > 0) { + dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, ...newDifficulties]} } }); + } + const updates = [ { type: "UPDATE_SECTION_STATE", @@ -94,7 +114,7 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => { update: { exercises: [ ...sectionState.exercises, - ...assignExercisesFn(results) + ...newExercises ] } } @@ -168,6 +188,7 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => { results.map(res => ({ ...writingTask(res.generating === "writing_letter" ? 1 : 2), prompt: res.result[0].prompt, + difficulty: res.result[0].difficulty, variant: res.generating === "writing_letter" ? "letter" : "essay" }) as WritingExercise); diff --git a/src/components/ExamEditor/SectionRenderer/SectionExercises/speaking.ts b/src/components/ExamEditor/SectionRenderer/SectionExercises/speaking.ts index 5be387e9..e6c6e677 100644 --- a/src/components/ExamEditor/SectionRenderer/SectionExercises/speaking.ts +++ b/src/components/ExamEditor/SectionRenderer/SectionExercises/speaking.ts @@ -22,6 +22,7 @@ const getSpeakingTaskData = (taskNumber: number, data: any) => { video_url: "" })) ], + difficulty: data.difficulty, sectionId: 1, }; case 2: @@ -29,6 +30,7 @@ const getSpeakingTaskData = (taskNumber: number, data: any) => { title: data.topic, text: data.question, prompts: data.prompts, + difficulty: data.difficulty, sectionId: 2, type: "speaking" }; @@ -39,6 +41,7 @@ const getSpeakingTaskData = (taskNumber: number, data: any) => { text: item || "", video_url: "" })), + difficulty: data.difficulty, sectionId: 3, }; default: diff --git a/src/components/ExamEditor/SettingsEditor/Shared/Generate.ts b/src/components/ExamEditor/SettingsEditor/Shared/Generate.ts index d4f2a121..e3c9503a 100644 --- a/src/components/ExamEditor/SettingsEditor/Shared/Generate.ts +++ b/src/components/ExamEditor/SettingsEditor/Shared/Generate.ts @@ -7,7 +7,7 @@ import { Module } from "@/interfaces"; interface GeneratorConfig { method: 'GET' | 'POST'; - queryParams?: Record; + queryParams?: Record; files?: Record; body?: Record; } @@ -61,9 +61,21 @@ export function generate( setGenerating(level ? levelSectionId! : sectionId, type, level); - const queryString = config.queryParams - ? new URLSearchParams(config.queryParams).toString() - : ''; + function buildQueryString(params: Record): string { + const searchParams = new URLSearchParams(); + + Object.entries(params).forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach(v => searchParams.append(key, v)); + } else { + searchParams.append(key, value); + } + }); + + return searchParams.toString(); + } + + const queryString = config.queryParams ? buildQueryString(config.queryParams) : ''; const url = `/api/exam/generate/${module}/${sectionId}${queryString ? `?${queryString}` : ''}`; diff --git a/src/components/ExamEditor/SettingsEditor/level.tsx b/src/components/ExamEditor/SettingsEditor/level.tsx index 33eb0c87..c5fc97e1 100644 --- a/src/components/ExamEditor/SettingsEditor/level.tsx +++ b/src/components/ExamEditor/SettingsEditor/level.tsx @@ -273,7 +273,6 @@ const LevelSettings: React.FC = () => {
@@ -335,7 +334,6 @@ const LevelSettings: React.FC = () => { @@ -370,7 +368,6 @@ const LevelSettings: React.FC = () => { diff --git a/src/components/ExamEditor/SettingsEditor/listening/components.tsx b/src/components/ExamEditor/SettingsEditor/listening/components.tsx index d7b03297..f1c846f6 100644 --- a/src/components/ExamEditor/SettingsEditor/listening/components.tsx +++ b/src/components/ExamEditor/SettingsEditor/listening/components.tsx @@ -297,7 +297,6 @@ const ListeningComponents: React.FC = ({ currentSection, localSettings, u = ({localSettings, updateLocalAndSchedu = ({ localSettings, updateLocalAndSche const [selectedAvatar, setSelectedAvatar] = useState(null); + const randomDiff = difficulty.length === 1 + ? capitalize(difficulty[0]) + : `Random (${difficulty.map(dif => capitalize(dif)).join(", ")})` as Difficulty; + + const DIFFICULTIES = difficulty.length === 1 + ? ["A1", "A2", "B1", "B2", "C1", "C2"] + : ["A1", "A2", "B1", "B2", "C1", "C2", randomDiff]; + + const difficultyOptions: Option[] = DIFFICULTIES.map(level => ({ + label: level, + value: level + })); + const [specificDiff, setSpecificDiff] = useState(randomDiff); + const generateScript = useCallback((scriptSectionId: number) => { const queryParams: { - difficulty: string; + difficulty: string[]; first_topic?: string; second_topic?: string; topic?: string; @@ -70,19 +88,22 @@ const SpeakingComponents: React.FC = ({ localSettings, updateLocalAndSche return [{ prompts: data.questions, first_topic: data.first_topic, - second_topic: data.second_topic + second_topic: data.second_topic, + difficulty: specificDiff.length == 2 ? specificDiff : difficulty, }]; case 2: return [{ topic: data.topic, question: data.question, prompts: data.prompts, - suffix: data.suffix + suffix: data.suffix, + difficulty: specificDiff.length == 2 ? specificDiff : difficulty, }]; case 3: return [{ title: data.topic, - prompts: data.questions + prompts: data.questions, + difficulty: specificDiff.length == 2 ? specificDiff : difficulty, }]; default: return [data]; @@ -92,7 +113,7 @@ const SpeakingComponents: React.FC = ({ localSettings, updateLocalAndSche level ); - }, [difficulty, level, section.sectionId, focusedSection, id, sectionId, localSettings.speakingTopic, localSettings.speakingSecondTopic]); + }, [difficulty, level, section.sectionId, focusedSection, id, sectionId, localSettings.speakingTopic, localSettings.speakingSecondTopic, specificDiff]); const onTopicChange = useCallback((speakingTopic: string) => { updateLocalAndScheduleGlobal({ speakingTopic }); @@ -192,7 +213,7 @@ const SpeakingComponents: React.FC = ({ localSettings, updateLocalAndSche contentWrapperClassName={level ? `border border-ielts-speaking` : ''} > -
+
= ({ localSettings, updateLocalAndSche placeholder="Topic" name="category" onChange={onTopicChange} - roundness="full" + roundness="xl" value={localSettings.speakingTopic} + thin />
{secId === 1 && @@ -214,12 +236,69 @@ const SpeakingComponents: React.FC = ({ localSettings, updateLocalAndSche placeholder="Topic" name="category" onChange={onSecondTopicChange} - roundness="full" + roundness="xl" value={localSettings.speakingSecondTopic} + thin />
} -
+
+ + opt.value === specificDiff)} + onChange={(value) => setSpecificDiff(value!.value as Difficulty)} + menuPortalTarget={document?.body} + components={{ + IndicatorSeparator: null, + ValueContainer: ({ children, ...props }) => ( + +
+ + {children} +
+
+ ) + }} + styles={{ + menuPortal: (base) => ({ ...base, zIndex: 9999 }), + control: (styles) => ({ + ...styles, + minHeight: '50px', + border: '1px solid #e5e7eb', + borderRadius: '0.5rem', + boxShadow: 'none', + backgroundColor: 'white', + cursor: 'pointer', + '&:hover': { + border: '1px solid #e5e7eb', + } + }), + valueContainer: (styles) => ({ + ...styles, + padding: '0 8px', + display: 'flex', + alignItems: 'center' + }), + input: (styles) => ({ + ...styles, + margin: '0', + padding: '0' + }), + dropdownIndicator: (styles) => ({ + ...styles, + padding: '8px' + }), + option: (styles, state) => ({ + ...styles, + backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", + color: state.isFocused ? "black" : styles.color, + }), + }} + className="text-sm" + /> +
+
= ({ localSettings, updateLocalAndSched type, academic_url } = useExamEditorStore((store) => store.modules["writing"]); + + const randomDiff = difficulty.length === 1 + ? capitalize(difficulty[0]) + : `Random (${difficulty.map(dif => capitalize(dif)).join(", ")})` as Difficulty; + + const DIFFICULTIES = difficulty.length === 1 + ? ["A1", "A2", "B1", "B2", "C1", "C2"] + : ["A1", "A2", "B1", "B2", "C1", "C2", randomDiff]; + + const difficultyOptions: Option[] = DIFFICULTIES.map(level => ({ + label: level, + value: level + })); + const [specificDiff, setSpecificDiff] = useState(randomDiff); + const generatePassage = useCallback((sectionId: number) => { if (type === "academic" && academic_url !== undefined && sectionId == 1) { generate( @@ -34,7 +53,7 @@ const WritingComponents: React.FC = ({ localSettings, updateLocalAndSched { method: 'POST', queryParams: { - difficulty, + difficulty: specificDiff.length == 2 ? [specificDiff] : difficulty, type: type! }, files: { @@ -42,7 +61,8 @@ const WritingComponents: React.FC = ({ localSettings, updateLocalAndSched } }, (data: any) => [{ - prompt: data.question + prompt: data.question, + difficulty: data.difficulty }] ) } else { @@ -53,18 +73,18 @@ const WritingComponents: React.FC = ({ localSettings, updateLocalAndSched { method: 'GET', queryParams: { - difficulty, + difficulty: specificDiff.length == 2 ? [specificDiff] : difficulty, type: type!, ...(localSettings.writingTopic && { topic: localSettings.writingTopic }) } }, (data: any) => [{ - prompt: data.question + prompt: data.question, + difficulty: data.difficulty }] ); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [localSettings.writingTopic, difficulty, academic_url]); + }, [type, academic_url, currentModule, specificDiff, difficulty, localSettings.writingTopic]); const onTopicChange = useCallback((writingTopic: string) => { updateLocalAndScheduleGlobal({ writingTopic }); @@ -96,72 +116,181 @@ const WritingComponents: React.FC = ({ localSettings, updateLocalAndSched setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isImageUploadOpen: isOpen }, false)} contentWrapperClassName={level ? `border border-ielts-writing` : ''} > -
- -
- - Upload a graph, chart or diagram - - - +
+
+
+ + opt.value === specificDiff)} + onChange={(value) => setSpecificDiff(value!.value as Difficulty)} + menuPortalTarget={document?.body} + components={{ + IndicatorSeparator: null, + ValueContainer: ({ children, ...props }) => ( + +
+ + {children} +
+
+ ) + }} + styles={{ + menuPortal: (base) => ({ ...base, zIndex: 9999 }), + control: (styles) => ({ + ...styles, + minHeight: '50px', + border: '1px solid #e5e7eb', + borderRadius: '0.5rem', + boxShadow: 'none', + backgroundColor: 'white', + cursor: 'pointer', + '&:hover': { + border: '1px solid #e5e7eb', + } + }), + valueContainer: (styles) => ({ + ...styles, + padding: '0 8px', + display: 'flex', + alignItems: 'center' + }), + input: (styles) => ({ + ...styles, + margin: '0', + padding: '0' + }), + dropdownIndicator: (styles) => ({ + ...styles, + padding: '8px' + }), + option: (styles, state) => ({ + ...styles, + backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", + color: state.isFocused ? "black" : styles.color, + }), + }} + className="text-sm" + /> +
+
+ + Upload a graph, chart or diagram + + + +
} { - (type !== "academic" || (type === "academic" && academic_url !== undefined)) && updateLocalAndScheduleGlobal({ isWritingTopicOpen: isOpen }, false)} contentWrapperClassName={level ? `border border-ielts-writing` : ''} > - -
- {type !== "academic" ? -
- - +
+
+ +
+ +
+
+
+ + opt.value === specificDiff)} + onChange={(value) => setSpecificDiff(value!.value as Difficulty)} + menuPortalTarget={document?.body} + components={{ + IndicatorSeparator: null, + ValueContainer: ({ children, ...props }) => ( + +
+ + {children} +
+
+ ) + }} + styles={{ + menuPortal: (base) => ({ ...base, zIndex: 9999 }), + control: (styles) => ({ + ...styles, + minHeight: '50px', + border: '1px solid #e5e7eb', + borderRadius: '0.5rem', + boxShadow: 'none', + backgroundColor: 'white', + cursor: 'pointer', + '&:hover': { + border: '1px solid #e5e7eb', + } + }), + valueContainer: (styles) => ({ + ...styles, + padding: '0 8px', + display: 'flex', + alignItems: 'center' + }), + input: (styles) => ({ + ...styles, + margin: '0', + padding: '0' + }), + dropdownIndicator: (styles) => ({ + ...styles, + padding: '8px' + }), + option: (styles, state) => ({ + ...styles, + backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", + color: state.isFocused ? "black" : styles.color, + }), + }} + className="text-sm" />
- : -
- - Generate instructions based on the uploaded image. - +
+
- } -
-
diff --git a/src/components/ExamEditor/Shared/Header.tsx b/src/components/ExamEditor/Shared/Header.tsx index 6a0412a3..edb2f67a 100644 --- a/src/components/ExamEditor/Shared/Header.tsx +++ b/src/components/ExamEditor/Shared/Header.tsx @@ -1,13 +1,18 @@ import { Module } from "@/interfaces"; import clsx from "clsx"; -import { ReactNode } from "react"; -import { MdDelete, MdEdit, MdEditOff, MdRefresh, MdSave } from "react-icons/md"; +import { ReactNode, useEffect, useState } from "react"; +import { MdDelete, MdEdit, MdEditOff, MdRefresh, MdSave, MdSignalCellularAlt } from "react-icons/md"; import { HiOutlineClipboardCheck, HiOutlineClipboardList } from "react-icons/hi"; +import { Difficulty } from "@/interfaces/exam"; +import Option from "@/interfaces/option"; +import ReactSelect, { components } from "react-select"; interface Props { title: string; description: string; editing: boolean; + difficulty?: Difficulty; + saveDifficulty?: (diff: Difficulty) => void; module?: Module; handleSave: () => void; handleDiscard: () => void; @@ -19,69 +24,135 @@ interface Props { } const Header: React.FC = ({ - title, description, editing, isEvaluationEnabled, handleSave, handleDiscard, handleDelete, handleEdit, handlePractice, children, module }) => { + title, description, editing, difficulty, saveDifficulty, isEvaluationEnabled, handleSave, handleDiscard, handleDelete, handleEdit, handlePractice, children, module +}) => { + const DIFFICULTIES: Difficulty[] = ["A1", "A2", "B1", "B2", "C1", "C2"]; + const difficultyOptions: Option[] = DIFFICULTIES.map(level => ({ + label: level, + value: level + })); + return ( -
+

{title}

{description}

-
- {children} - - - {handleEdit && ( +
+
- )} - {handlePractice && - } - {handleDelete && ( - - )} + {handleEdit && ( + + )} + {handlePractice && + + } + {handleDelete && ( + + )} + {difficulty !== undefined && ( +
+ opt.value === difficulty)} + onChange={(value) => saveDifficulty!(value!.value as Difficulty)} + menuPortalTarget={document?.body} + components={{ + IndicatorSeparator: null, + ValueContainer: ({ children, ...props }) => ( + +
+ + {children} +
+
+ ) + }} + styles={{ + menuPortal: (base) => ({ ...base, zIndex: 9999 }), + control: (styles) => ({ + ...styles, + minHeight: '40px', + border: '1px solid #e5e7eb', + borderRadius: '0.5rem', + boxShadow: 'none', + backgroundColor: '#f3f4f6', + cursor: 'pointer', + '&:hover': { + border: '1px solid #e5e7eb', + } + }), + valueContainer: (styles) => ({ + ...styles, + padding: '0 8px', + display: 'flex', + alignItems: 'center' + }), + input: (styles) => ({ + ...styles, + margin: '0', + padding: '0' + }), + dropdownIndicator: (styles) => ({ + ...styles, + padding: '8px' + }), + option: (styles, state) => ({ + ...styles, + backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", + color: state.isFocused ? "black" : styles.color, + }), + }} + className="text-sm" + /> +
+ )} + {children} +
); diff --git a/src/components/ExamEditor/index.tsx b/src/components/ExamEditor/index.tsx index dcc3ec1e..944e2f5a 100644 --- a/src/components/ExamEditor/index.tsx +++ b/src/components/ExamEditor/index.tsx @@ -148,9 +148,23 @@ const ExamEditor: React.FC<{ levelParts?: number }> = ({ levelParts = 0 }) => {