ENCOA-311

This commit is contained in:
Carlos-Mesquita
2025-01-13 01:18:19 +00:00
parent 715a841483
commit ccbbf30058
33 changed files with 824 additions and 194 deletions

View File

@@ -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<Props> = ({
onDiscard,
}) => {
const [configurations, setConfigurations] = useState<ExerciseConfig[]>([]);
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<Props> = ({
);
}
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<Props> = ({
</>
)}
</div>
{param.param === "difficulty" ?
<Select
options={DIFFICULTIES.map((x) => ({ value: x, label: x }))}
onChange={(value) => {
handleParameterChange(
exerciseIndex,
param.param || '',
value?.value || ''
);
}}
value={{ value: config.params[param.param] !== "" ? config.params[param.param] as string : randomDiff , label: config.params[param.param] !== "" ? config.params[param.param] as string : randomDiff }}
flat
/>
:
<input
type="number"
value={inputValue}
value={inputValue as number}
onChange={(e) => handleParameterChange(
exerciseIndex,
param.param || '',
@@ -195,6 +222,8 @@ const ExerciseWizard: React.FC<Props> = ({
min={1}
max={maxParagraphs}
/>
}
</div>
);
};

View File

@@ -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",

View File

@@ -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
}

View File

@@ -13,12 +13,13 @@ import { BsArrowRepeat } from "react-icons/bs";
interface ExercisePickerProps {
module: string;
sectionId: number;
difficulty: string;
extraArgs?: Record<string, any>;
levelSectionId?: number;
level?: boolean;
}
const DIFFICULTIES: string[] = ["A1", "A2", "B1", "B2", "C1", "C2"];
const ExercisePicker: React.FC<ExercisePickerProps> = ({
module,
sectionId,
@@ -26,7 +27,7 @@ const ExercisePicker: React.FC<ExercisePickerProps> = ({
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<ExercisePickerProps> = ({
}),
...(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<ExercisePickerProps> = ({
method: 'POST',
body: {
...context,
exercises: exercises,
difficulty: difficulty
exercises,
difficulty
}
},
(data: any) => [{
@@ -112,7 +116,11 @@ const ExercisePicker: React.FC<ExercisePickerProps> = ({
);
} 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<ExercisePickerProps> = ({
queryParams
},
(data: any) => [{
prompt: data.question
prompt: data.question,
difficulty: data.difficulty
}],
levelSectionId,
level
@@ -135,6 +144,7 @@ const ExercisePicker: React.FC<ExercisePickerProps> = ({
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<ExercisePickerProps> = ({
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];

View File

@@ -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 (
<div className="space-y-4">
<BlanksEditor
alerts={alerts}
editing={editing}
state={blanksState}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
blanksDispatcher={blanksDispatcher}
description="Place blanks and assign words from the word bank"
initialText={local.text}

View File

@@ -1,5 +1,5 @@
import { FillBlanksExercise, FillBlanksMCOption, ReadingPart } from "@/interfaces/exam";
import { useEffect, useReducer, useState } from "react";
import { Difficulty, FillBlanksExercise, FillBlanksMCOption, ReadingPart } from "@/interfaces/exam";
import { useCallback, useEffect, useReducer, useState } from "react";
import BlanksEditor from "..";
import { Card, CardContent } from "@/components/ui/card";
import useExamEditorStore from "@/stores/examEditor";
@@ -15,6 +15,7 @@ import MCOption from "./MCOption";
const FillBlanksMC: 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)!
);
@@ -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 (
<div className="space-y-4">
<BlanksEditor
alerts={alerts}
editing={editing}
state={blanksState}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
blanksDispatcher={blanksDispatcher}
description="Place blanks and select the correct answer from multiple choice options"
initialText={local.text}

View File

@@ -1,8 +1,8 @@
import useSectionEdit from "@/components/ExamEditor/Hooks/useSectionEdit";
import { Card, CardContent } from "@/components/ui/card";
import { WriteBlanksExercise, ReadingPart } from "@/interfaces/exam";
import { WriteBlanksExercise, ReadingPart, Difficulty } from "@/interfaces/exam";
import useExamEditorStore from "@/stores/examEditor";
import { useState, useReducer, useEffect } from "react";
import { useState, useReducer, useEffect, useCallback } from "react";
import { toast } from "react-toastify";
import BlanksEditor from "..";
import { AlertItem } from "../../Shared/Alert";
@@ -13,6 +13,7 @@ import AlternativeSolutions from "./AlternativeSolutions";
const WriteBlanksFill: React.FC<{ exercise: WriteBlanksExercise; 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)!
);
@@ -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 (
<div className="space-y-4">
<BlanksEditor
@@ -171,6 +182,8 @@ const WriteBlanksFill: React.FC<{ exercise: WriteBlanksExercise; sectionId: numb
blanksDispatcher={blanksDispatcher}
description={local.prompt}
initialText={local.text}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
module={currentModule}
showBlankBank={true}
onBlankSelect={(blankId) => setSelectedBlankId(blankId?.toString() || null)}

View File

@@ -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<Props> = ({
title = "Fill Blanks",
initialText,
description,
difficulty,
saveDifficulty,
state,
editing,
module,
@@ -169,6 +174,8 @@ const BlanksEditor: React.FC<Props> = ({
title={title}
description={description}
editing={editing}
difficulty={difficulty}
saveDifficulty={saveDifficulty}
handleSave={onSave}
handleDelete={onDelete}
handleDiscard={onDiscard}

View File

@@ -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 (
<div className="flex flex-col mx-auto p-2">
<Header
title={exercise.variant && exercise.variant == "ideaMatch" ? "Idea Match" : "Paragraph Match"}
description={`Edit ${exercise.variant && exercise.variant == "ideaMatch" ? "ideas/opinions" : "headings"} and their matches`}
editing={editing}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
handleSave={handleSave}
handleDelete={handleDelete}
handleDiscard={handleDiscard}

View File

@@ -5,9 +5,9 @@ import UnderlineQuestion from "./UnderlineQuestion";
import useSectionEdit from "@/components/ExamEditor/Hooks/useSectionEdit";
import { toast } from "react-toastify";
import setEditingAlert from "../../Shared/setEditingAlert";
import { LevelPart, ListeningPart, MultipleChoiceExercise, MultipleChoiceQuestion, ReadingPart } from "@/interfaces/exam";
import { Difficulty, LevelPart, ListeningPart, MultipleChoiceExercise, MultipleChoiceQuestion, ReadingPart } from "@/interfaces/exam";
import useExamEditorStore from "@/stores/examEditor";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { MdAdd } from "react-icons/md";
import Alert, { AlertItem } from "../../Shared/Alert";
import PromptEdit from "../../Shared/PromptEdit";
@@ -18,6 +18,7 @@ const UnderlineMultipleChoice: React.FC<{exercise: MultipleChoiceExercise, secti
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)!
);
@@ -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 (
<div className="p-4">
<Header
title='Underline Multiple Choice Exercise'
description="Edit questions with 4 underline options each"
editing={editing}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
handleSave={handleSave}
handleDelete={handleDelete}
handlePractice={handlePractice}

View File

@@ -1,11 +1,11 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import {
MdAdd,
MdEdit,
MdEditOff,
} from 'react-icons/md';
import { ReadingPart, MultipleChoiceExercise, MultipleChoiceQuestion, LevelPart, ListeningPart } from '@/interfaces/exam';
import { ReadingPart, MultipleChoiceExercise, MultipleChoiceQuestion, LevelPart, ListeningPart, Difficulty } from '@/interfaces/exam';
import clsx from 'clsx';
import useExamEditorStore from '@/stores/examEditor';
import { toast } from 'react-toastify';
@@ -74,7 +74,8 @@ const validateMultipleChoiceQuestions = (
};
const MultipleChoice: React.FC<MultipleChoiceProps> = ({ 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<MultipleChoiceProps> = ({ 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 (
<div className="p-4">
<Header
title='Multiple Choice Exercise'
description={`Edit questions with ${optionsQuantity} options each`}
editing={editing}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
handleSave={handleSave}
handleDelete={handleDelete}
handleDiscard={handleDiscard}

View File

@@ -5,10 +5,10 @@ import { AiOutlineUnorderedList, AiOutlinePlus, AiOutlineDelete } from 'react-ic
import { Tooltip } from "react-tooltip";
import Header from "../../Shared/Header";
import GenLoader from "../Shared/GenLoader";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
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 { FaChevronLeft, FaChevronRight } from "react-icons/fa6";
import { RiVideoLine } from "react-icons/ri";
@@ -21,7 +21,8 @@ interface Props {
}
const InteractiveSpeaking: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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 (
<>
<div className='relative pb-4'>
@@ -271,6 +295,8 @@ const InteractiveSpeaking: React.FC<Props> = ({ 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}

View File

@@ -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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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 (
<>
<div className='relative pb-4'>
@@ -307,6 +331,8 @@ const Speaking1: React.FC<Props> = ({ 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}

View File

@@ -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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ sectionId, exercise, module = "speaking" }
</div>
`;
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 (
<>
<div className='relative pb-4'>
@@ -251,6 +275,8 @@ const Speaking2: React.FC<Props> = ({ 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}

View File

@@ -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 (
<div className="p-4">
<Header
title='True/False/Not Given Exercise'
description='Edit questions and their solutions'
editing={editing}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
handleSave={handleSave}
handleDelete={handleDelete}
handleDiscard={handleDiscard}

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import {
MdAdd,
@@ -13,7 +13,7 @@ import Header from '../../Shared/Header';
import clsx from 'clsx';
import Alert, { AlertItem } from '../Shared/Alert';
import AutoExpandingTextArea from '@/components/Low/AutoExpandingTextarea';
import { ReadingPart, WriteBlanksExercise } from '@/interfaces/exam';
import { Difficulty, ReadingPart, WriteBlanksExercise } from '@/interfaces/exam';
import useExamEditorStore from '@/stores/examEditor';
import useSectionEdit from '../../Hooks/useSectionEdit';
import setEditingAlert from '../Shared/setEditingAlert';
@@ -25,8 +25,8 @@ import PromptEdit from '../Shared/PromptEdit';
const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise; }> = ({ 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 (
<div className="p-4">
<Header
title={"Write Blanks: Questions"}
description="Edit questions and their solutions"
editing={editing}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
handleSave={handleSave}
handleDiscard={handleDiscard}
handleDelete={handleDelete}

View File

@@ -1,10 +1,10 @@
import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea";
import { Card, CardContent } from "@/components/ui/card";
import { WriteBlanksExercise, ReadingPart } from "@/interfaces/exam";
import { WriteBlanksExercise, ReadingPart, Difficulty } from "@/interfaces/exam";
import useExamEditorStore from "@/stores/examEditor";
import { DragEndEvent } from "@dnd-kit/core";
import { arrayMove } from "@dnd-kit/sortable";
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback } from "react";
import { MdEditOff, MdEdit, MdDelete, MdAdd } from "react-icons/md";
import { toast } from "react-toastify";
import useSectionEdit from "../../Hooks/useSectionEdit";
@@ -21,6 +21,7 @@ import PromptEdit from "../Shared/PromptEdit";
const WriteBlanksForm: React.FC<{ sectionId: number; exercise: WriteBlanksExercise }> = ({ 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 (
<div className="p-4">
<Header
title="Write Blanks: Form Exercise"
description="Edit questions and their solutions"
editing={editing}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
handleSave={handleSave}
handleDiscard={handleDiscard}
handleDelete={handleDelete}

View File

@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from "react";
import useExamEditorStore from "@/stores/examEditor";
import ExamEditorStore, { ModuleState } from "@/stores/examEditor/types";
import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea";
import { LevelPart, WritingExercise } from "@/interfaces/exam";
import { Difficulty, LevelPart, WritingExercise } from "@/interfaces/exam";
import Header from "../../Shared/Header";
import Alert, { AlertItem } from "../Shared/Alert";
import clsx from "clsx";
@@ -20,6 +20,7 @@ interface Props {
const Writing: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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 (
<>
<div className={clsx('relative', level ? "px-4 mt-2" : "pb-2")}>
@@ -102,6 +125,8 @@ const Writing: React.FC<Props> = ({ 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}

View File

@@ -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);

View File

@@ -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:

View File

@@ -7,7 +7,7 @@ import { Module } from "@/interfaces";
interface GeneratorConfig {
method: 'GET' | 'POST';
queryParams?: Record<string, string>;
queryParams?: Record<string, string | string[]>;
files?: Record<string, string>;
body?: Record<string, any>;
}
@@ -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, string | string[]>): 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}` : ''}`;

View File

@@ -273,7 +273,6 @@ const LevelSettings: React.FC = () => {
<ExercisePicker
module="level"
sectionId={focusedSection}
difficulty={difficulty}
/>
</Dropdown>
</div>
@@ -335,7 +334,6 @@ const LevelSettings: React.FC = () => {
<ExercisePicker
module="writing"
sectionId={focusedSection}
difficulty={difficulty}
levelSectionId={focusedSection}
level
/>
@@ -370,7 +368,6 @@ const LevelSettings: React.FC = () => {
<ExercisePicker
module="speaking"
sectionId={focusedSection}
difficulty={difficulty}
levelSectionId={focusedSection}
level
/>

View File

@@ -297,7 +297,6 @@ const ListeningComponents: React.FC<Props> = ({ currentSection, localSettings, u
<ExercisePicker
module="listening"
sectionId={levelId !== undefined ? levelId : focusedSection}
difficulty={difficulty}
extraArgs={{ script: currentSection === undefined || currentSection.audio === undefined ? "" : currentSection.script }}
levelSectionId={focusedSection}
level={level}

View File

@@ -95,7 +95,6 @@ const ReadingComponents: React.FC<Props> = ({localSettings, updateLocalAndSchedu
<ExercisePicker
module="reading"
sectionId={levelId !== undefined ? levelId : focusedSection}
difficulty={difficulty}
extraArgs={{ text: currentSection === undefined || currentSection.text === undefined ? "" : currentSection.text.content }}
levelSectionId={focusedSection}
level={level}

View File

@@ -7,11 +7,15 @@ import Input from "@/components/Low/Input";
import GenerateBtn from "../Shared/GenerateBtn";
import clsx from "clsx";
import { FaFemale, FaMale } from "react-icons/fa";
import { InteractiveSpeakingExercise, LevelPart, SpeakingExercise } from "@/interfaces/exam";
import { Difficulty, InteractiveSpeakingExercise, LevelPart, SpeakingExercise } from "@/interfaces/exam";
import { toast } from "react-toastify";
import { generateVideos } from "../Shared/generateVideos";
import { Module } from "@/interfaces";
import useCanGenerate from "./useCanGenerate";
import ReactSelect, { components } from "react-select";
import { capitalize } from "lodash";
import Option from "@/interfaces/option";
import { MdSignalCellularAlt } from "react-icons/md";
export interface Avatar {
name: string;
@@ -36,9 +40,23 @@ const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSche
const [selectedAvatar, setSelectedAvatar] = useState<Avatar | null>(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<Props> = ({ 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<Props> = ({ 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<Props> = ({ localSettings, updateLocalAndSche
contentWrapperClassName={level ? `border border-ielts-speaking` : ''}
>
<div className={clsx("gap-2 px-2 pb-4", secId === 1 ? "flex flex-col w-full" : "flex flex-row items-center")}>
<div className="gap-4 px-2 pb-4 flex flex-col w-full">
<div className="flex flex-col flex-grow gap-4 px-2">
<label className="font-normal text-base text-mti-gray-dim">{`${secId === 1 ? "First Topic" : "Topic"}`} (Optional)</label>
<Input
@@ -201,8 +222,9 @@ const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSche
placeholder="Topic"
name="category"
onChange={onTopicChange}
roundness="full"
roundness="xl"
value={localSettings.speakingTopic}
thin
/>
</div>
{secId === 1 &&
@@ -214,12 +236,69 @@ const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSche
placeholder="Topic"
name="category"
onChange={onSecondTopicChange}
roundness="full"
roundness="xl"
value={localSettings.speakingSecondTopic}
thin
/>
</div>
}
<div className={clsx("flex h-16 mb-1", secId === 1 ? "justify-center mt-4" : "self-end")}>
<div className="flex flex-col gap-2 px-2">
<label className="block font-normal text-base text-mti-gray-dim mb-2">Difficulty (Optional)</label>
<ReactSelect
options={difficultyOptions}
value={difficultyOptions.find(opt => opt.value === specificDiff)}
onChange={(value) => setSpecificDiff(value!.value as Difficulty)}
menuPortalTarget={document?.body}
components={{
IndicatorSeparator: null,
ValueContainer: ({ children, ...props }) => (
<components.ValueContainer {...props}>
<div className="flex flex-row gap-2 items-center pl-4">
<MdSignalCellularAlt size={14} className="text-gray-600" />
{children}
</div>
</components.ValueContainer>
)
}}
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"
/>
</div>
<div className="flex h-16 mb-1 justify-center mt-4">
<GenerateBtn
module="speaking"
genType={`${id ? `${id}-` : ''}speakingScript`}

View File

@@ -5,9 +5,13 @@ import { generate } from "../Shared/Generate";
import GenerateBtn from "../Shared/GenerateBtn";
import { LevelSectionSettings, WritingSectionSettings } from "@/stores/examEditor/types";
import useExamEditorStore from "@/stores/examEditor";
import { WritingExercise } from "@/interfaces/exam";
import { Difficulty, WritingExercise } from "@/interfaces/exam";
import clsx from "clsx";
import { FaFileUpload } from "react-icons/fa";
import ReactSelect, { components } from "react-select";
import Option from "@/interfaces/option"
import { MdSignalCellularAlt } from "react-icons/md";
import { capitalize } from "lodash";
interface Props {
@@ -25,6 +29,21 @@ const WritingComponents: React.FC<Props> = ({ 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<Props> = ({ localSettings, updateLocalAndSched
{
method: 'POST',
queryParams: {
difficulty,
difficulty: specificDiff.length == 2 ? [specificDiff] : difficulty,
type: type!
},
files: {
@@ -42,7 +61,8 @@ const WritingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSched
}
},
(data: any) => [{
prompt: data.question
prompt: data.question,
difficulty: data.difficulty
}]
)
} else {
@@ -53,18 +73,18 @@ const WritingComponents: React.FC<Props> = ({ 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<Props> = ({ localSettings, updateLocalAndSched
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isImageUploadOpen: isOpen }, false)}
contentWrapperClassName={level ? `border border-ielts-writing` : ''}
>
<div className="flex flex-row gap-2 items-center px-2 pb-4">
<div className="flex flex-row p-2 gap-4">
<span className="bg-gray-100 px-3.5 py-2.5 rounded-lg border border-gray-300 text-mti-gray-dim">
Upload a graph, chart or diagram
</span>
<input
type="file"
accept="image/png, image/jpeg"
onChange={handleFileUpload}
style={{ display: 'none' }}
ref={fileInputRef}
/>
<button
key={`section-${focusedSection}`}
className={clsx(
"flex items-center w-[140px] px-4 py-2 text-white rounded-xl transition-colors duration-300 text-lg disabled:cursor-not-allowed",
`bg-ielts-writing/70 border border-ielts-writing hover:bg-ielts-writing disabled:bg-ielts-writing/40`,
)}
onClick={triggerFileInput}
>
<div className="flex flex-row">
<FaFileUpload className="mr-2" size={24} />
<span>Upload</span>
</div>
</button>
<div className="flex w-full flex-row gap-2 items-center px-2 pb-4">
<div className="flex w-full flex-col gap-4">
<div className="flex flex-col gap-2">
<label className="block font-normal text-base text-mti-gray-dim mb-2">Difficulty (Optional)</label>
<ReactSelect
options={difficultyOptions}
value={difficultyOptions.find(opt => opt.value === specificDiff)}
onChange={(value) => setSpecificDiff(value!.value as Difficulty)}
menuPortalTarget={document?.body}
components={{
IndicatorSeparator: null,
ValueContainer: ({ children, ...props }) => (
<components.ValueContainer {...props}>
<div className="flex flex-row gap-2 items-center pl-4">
<MdSignalCellularAlt size={14} className="text-gray-600" />
{children}
</div>
</components.ValueContainer>
)
}}
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"
/>
</div>
<div className="flex flex-row justify-between gap-4">
<span className="bg-gray-100 px-3.5 py-2.5 rounded-lg border border-gray-300 text-mti-gray-dim flex-grow">
Upload a graph, chart or diagram
</span>
<input
type="file"
accept="image/png, image/jpeg"
onChange={handleFileUpload}
style={{ display: 'none' }}
ref={fileInputRef}
/>
<button
key={`section-${focusedSection}`}
className={clsx(
"flex items-center w-[140px] px-4 py-2 text-white rounded-xl transition-colors duration-300 text-lg disabled:cursor-not-allowed",
`bg-ielts-writing/70 border border-ielts-writing hover:bg-ielts-writing disabled:bg-ielts-writing/40`,
)}
onClick={triggerFileInput}
>
<div className="flex flex-row">
<FaFileUpload className="mr-2" size={24} />
<span>Upload</span>
</div>
</button>
</div>
</div>
</div>
</Dropdown>}
{
(type !== "academic" || (type === "academic" && academic_url !== undefined)) && <Dropdown
(type !== "academic" || (type === "academic" && focusedSection == 2)) && <Dropdown
title="Generate Instructions"
module={"writing"}
open={localSettings.isWritingTopicOpen}
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isWritingTopicOpen: isOpen }, false)}
contentWrapperClassName={level ? `border border-ielts-writing` : ''}
>
<div className="flex flex-row gap-2 items-center px-2 pb-4">
{type !== "academic" ?
<div className="flex flex-col flex-grow gap-4 px-2">
<label className="font-normal text-base text-mti-gray-dim">Topic (Optional)</label>
<Input
key={`section-${focusedSection}`}
type="text"
placeholder="Topic"
name="category"
onChange={onTopicChange}
roundness="full"
value={localSettings.writingTopic}
<div className="px-2 pb-4 flex flex-col w-full">
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<label className="block font-normal text-base text-mti-gray-dim mb-2">Topic (Optional)</label>
<div className="flex gap-2 min-w-0">
<Input
key={`section-${focusedSection}`}
type="text"
placeholder="Topic"
name="category"
onChange={onTopicChange}
roundness="xl"
value={localSettings.writingTopic}
thin
/>
</div>
</div>
<div className="flex flex-col gap-2">
<label className="block font-normal text-base text-mti-gray-dim mb-2">Difficulty (Optional)</label>
<ReactSelect
options={difficultyOptions}
value={difficultyOptions.find(opt => opt.value === specificDiff)}
onChange={(value) => setSpecificDiff(value!.value as Difficulty)}
menuPortalTarget={document?.body}
components={{
IndicatorSeparator: null,
ValueContainer: ({ children, ...props }) => (
<components.ValueContainer {...props}>
<div className="flex flex-row gap-2 items-center pl-4">
<MdSignalCellularAlt size={14} className="text-gray-600" />
{children}
</div>
</components.ValueContainer>
)
}}
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"
/>
</div>
:
<div className="flex flex-col flex-grow gap-4 px-2">
<span className="bg-gray-100 px-3.5 py-2.5 rounded-lg border border-gray-300 text-mti-gray-dim">
Generate instructions based on the uploaded image.
</span>
<div className="flex w-full h-full justify-center items-center mt-2">
<GenerateBtn
genType="writing"
module={"writing"}
sectionId={focusedSection}
generateFnc={generatePassage}
/>
</div>
}
<div className="flex self-end h-16 mb-1">
<GenerateBtn
genType="writing"
module={"writing"}
sectionId={focusedSection}
generateFnc={generatePassage}
/>
</div>
</div>
</Dropdown>

View File

@@ -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<Props> = ({
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 (
<div className="flex justify-between items-center mb-6 text-sm">
<div className="flex flex-col md:flex-row items-start md:items-center mb-6 text-sm">
<div>
<h1 className="text-2xl font-bold text-gray-800">{title}</h1>
<p className="text-gray-600 mt-1">{description}</p>
</div>
<div className="flex gap-2">
{children}
<button
onClick={handleSave}
disabled={!editing}
className={
clsx("px-4 py-2 rounded-lg flex items-center gap-2 transition-all duration-200",
editing ? 'bg-green-500 text-white hover:bg-green-600' : 'bg-gray-100 text-gray-400 cursor-not-allowed'
)}
>
<MdSave size={18} />
Save
</button>
<button
onClick={handleDiscard}
disabled={!editing}
className={clsx(
"px-4 py-2 rounded-lg flex items-center gap-2 transition-all duration-200",
editing ? 'bg-gray-500 text-white hover:bg-gray-600' : 'bg-gray-100 text-gray-400 cursor-not-allowed'
)}
>
<MdRefresh size={18} />
Discard
</button>
{handleEdit && (
<div className="flex w-[50%] ml-auto justify-end flex-wrap gap-2">
<div className="flex flex-wrap gap-2 justify-end">
<button
onClick={handleEdit}
className={`px-4 py-2 bg-ielts-${module}/80 text-white hover:bg-ielts-${module} rounded-lg transition-all duration-200 flex items-center gap-2`}
onClick={handleSave}
disabled={!editing}
className={
clsx("px-3 py-2 rounded-lg flex items-center gap-1 transition-all duration-200 min-w-[90px] justify-center",
editing ? 'bg-green-500 text-white hover:bg-green-600' : 'bg-gray-100 text-gray-400 cursor-not-allowed'
)}
>
{editing ? <MdEditOff size={18} /> : <MdEdit size={18} />}
Edit
<MdSave size={18} />
Save
</button>
)}
{handlePractice &&
<button
onClick={handlePractice}
onClick={handleDiscard}
disabled={!editing}
className={clsx(
"px-4 py-2 rounded-lg flex items-center gap-2 transition-all duration-200",
isEvaluationEnabled
? 'bg-amber-500 text-white hover:bg-amber-600'
: 'bg-gray-200 text-gray-600 hover:bg-gray-300'
"px-3 py-2 rounded-lg flex items-center gap-1 transition-all duration-200 min-w-[90px] justify-center",
editing ? 'bg-gray-500 text-white hover:bg-gray-600' : 'bg-gray-100 text-gray-400 cursor-not-allowed'
)}
>
{isEvaluationEnabled ? <HiOutlineClipboardCheck size={18} /> : <HiOutlineClipboardList size={18} />}
{isEvaluationEnabled ? 'Graded' : 'Practice'}
<MdRefresh size={18} />
Discard
</button>
}
{handleDelete && (
<button
onClick={handleDelete}
className="px-4 py-2 bg-white border border-red-500 text-red-500 hover:bg-red-50 rounded-lg transition-all duration-200 flex items-center gap-2"
>
<MdDelete size={18} />
Delete
</button>
)}
{handleEdit && (
<button
onClick={handleEdit}
className={`px-3 py-2 bg-ielts-${module}/80 text-white hover:bg-ielts-${module} rounded-lg transition-all duration-200 flex items-center gap-1 min-w-[90px] justify-center`}
>
{editing ? <MdEditOff size={18} /> : <MdEdit size={18} />}
Edit
</button>
)}
{handlePractice &&
<button
onClick={handlePractice}
className={clsx(
"px-3 py-2 rounded-lg flex items-center gap-1 transition-all duration-200 min-w-[90px] justify-center",
isEvaluationEnabled
? 'bg-amber-500 text-white hover:bg-amber-600'
: 'bg-gray-200 text-gray-600 hover:bg-gray-300'
)}
>
{isEvaluationEnabled ? <HiOutlineClipboardCheck size={18} /> : <HiOutlineClipboardList size={18} />}
{isEvaluationEnabled ? 'Graded' : 'Practice'}
</button>
}
{handleDelete && (
<button
onClick={handleDelete}
className="px-3 py-2 bg-white border border-red-500 text-red-500 hover:bg-red-50 rounded-lg transition-all duration-200 flex items-center gap-1 min-w-[90px] justify-center"
>
<MdDelete size={18} />
Delete
</button>
)}
{difficulty !== undefined && (
<div className="w-[92px]">
<ReactSelect
options={difficultyOptions}
value={difficultyOptions.find(opt => opt.value === difficulty)}
onChange={(value) => saveDifficulty!(value!.value as Difficulty)}
menuPortalTarget={document?.body}
components={{
IndicatorSeparator: null,
ValueContainer: ({ children, ...props }) => (
<components.ValueContainer {...props}>
<div className="flex flex-row gap-2 items-center pl-0.5">
<MdSignalCellularAlt size={14} className="text-gray-600" />
{children}
</div>
</components.ValueContainer>
)
}}
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"
/>
</div>
)}
{children}
</div>
</div>
</div>
);

View File

@@ -148,9 +148,23 @@ const ExamEditor: React.FC<{ levelParts?: number }> = ({ levelParts = 0 }) => {
<div className="flex flex-col gap-3 flex-grow">
<label className="font-normal text-base text-mti-gray-dim">Difficulty</label>
<Select
options={DIFFICULTIES.map((x) => ({ value: x, label: capitalize(x) }))}
onChange={(value) => value && updateModule({ difficulty: value.value as Difficulty })}
value={{ value: difficulty, label: capitalize(difficulty) }}
isMulti={true}
options={DIFFICULTIES.map((x) => ({
value: x,
label: capitalize(x)
}))}
onChange={(values) => {
const selectedDifficulties = values ? values.map(v => v.value as Difficulty) : [];
updateModule({ difficulty: selectedDifficulties });
}}
value={
difficulty
? difficulty.map(d => ({
value: d,
label: capitalize(d)
}))
: null
}
/>
</div>
{(sectionLabels.length != 0 && currentModule !== "level") ? (

View File

@@ -4,8 +4,8 @@ import ReactSelect, { GroupBase, StylesConfig } from "react-select";
import Option from "@/interfaces/option";
interface Props {
defaultValue?: Option;
value?: Option | null;
defaultValue?: Option | Option[];
value?: Option | Option[] | null;
options: Option[];
disabled?: boolean;
placeholder?: string;
@@ -13,6 +13,7 @@ interface Props {
styles?: StylesConfig<Option, boolean, GroupBase<Option>>;
className?: string;
label?: string;
flat?: boolean
}
interface MultiProps {
@@ -25,7 +26,7 @@ interface SingleProps {
onChange: (value: Option | null) => void
}
export default function Select({ value, isMulti, defaultValue, options, placeholder, disabled, onChange, styles, isClearable, label, className }: Props & (MultiProps | SingleProps)) {
export default function Select({ value, isMulti, defaultValue, options, placeholder, disabled, onChange, styles, isClearable, label, className, flat }: Props & (MultiProps | SingleProps)) {
const [target, setTarget] = useState<HTMLElement>();
useEffect(() => {
@@ -41,8 +42,9 @@ export default function Select({ value, isMulti, defaultValue, options, placehol
styles
? undefined
: clsx(
"placeholder:text-mti-gray-cool border-mti-gray-platinum w-full rounded-full border bg-white px-4 py-4 text-sm font-normal focus:outline-none",
"placeholder:text-mti-gray-cool border-mti-gray-platinum w-full border bg-white text-sm font-normal focus:outline-none",
disabled && "!bg-mti-gray-platinum/40 !text-mti-gray-dim cursor-not-allowed",
flat ? "rounded-md" : "px-4 py-4 rounded-full",
className,
)
}

View File

@@ -103,7 +103,6 @@ const Level: React.FC<ExamProps<LevelExam>> = ({ exam, showSolutions = false, pr
const hasPractice = useMemo(() => {
if (partIndex > -1 && partIndex < exam.parts.length && !showPartDivider) {
console.log(exam.parts[partIndex].exercises.some(e => e.isPractice))
return exam.parts[partIndex].exercises.some(e => e.isPractice)
}
return false

View File

@@ -18,7 +18,7 @@ export interface ExamBase {
minTimer: number;
isDiagnostic: boolean;
variant?: Variant;
difficulty?: Difficulty;
difficulty?: Difficulty | Difficulty[];
owners?: string[];
entities?: string[]
shuffle?: boolean;
@@ -177,6 +177,7 @@ export interface WritingExercise extends Section {
topic?: string;
variant?: string;
isPractice?: boolean
difficulty?: Difficulty
}
export interface AIDetectionAttributes {
@@ -212,6 +213,7 @@ export interface SpeakingExercise extends Section {
}[];
topic?: string;
isPractice?: boolean
difficulty?: Difficulty
}
export interface InteractiveSpeakingExercise extends Section {
@@ -232,6 +234,7 @@ export interface InteractiveSpeakingExercise extends Section {
second_topic?: string;
variant?: "initial" | "final";
isPractice?: boolean
difficulty?: Difficulty
}
export interface FillBlanksMCOption {
@@ -261,6 +264,7 @@ export interface FillBlanksExercise {
}[];
variant?: string;
isPractice?: boolean
difficulty?: Difficulty
}
export interface TrueFalseExercise {
@@ -270,6 +274,7 @@ export interface TrueFalseExercise {
questions: TrueFalseQuestion[];
userSolutions: { id: string; solution: "true" | "false" | "not_given" }[];
isPractice?: boolean
difficulty?: Difficulty
}
export interface TrueFalseQuestion {
@@ -294,6 +299,7 @@ export interface WriteBlanksExercise {
}[];
variant?: string;
isPractice?: boolean
difficulty?: Difficulty
}
export interface MatchSentencesExercise {
@@ -306,6 +312,7 @@ export interface MatchSentencesExercise {
options: MatchSentenceExerciseOption[];
variant?: string;
isPractice?: boolean
difficulty?: Difficulty
}
export interface MatchSentenceExerciseSentence {
@@ -331,6 +338,7 @@ export interface MultipleChoiceExercise {
content: string;
}
isPractice?: boolean
difficulty?: Difficulty
}
export interface MultipleChoiceQuestion {

View File

@@ -157,7 +157,7 @@ const defaultModuleSettings = (module: Module, minTimer: number): ModuleState =>
const state: ModuleState = {
examLabel: defaultExamLabel(module),
minTimer,
difficulty: sample(["A1", "A2", "B1", "B2", "C1", "C2"] as Difficulty[])!,
difficulty: [sample(["A1", "A2", "B1", "B2", "C1", "C2"] as Difficulty[])!],
isPrivate: true,
sectionLabels: sectionLabels(module),
expandedSections: [1],

View File

@@ -125,7 +125,7 @@ export interface ModuleState {
examLabel: string;
sections: SectionState[];
minTimer: number;
difficulty: Difficulty;
difficulty: Difficulty[];
isPrivate: boolean;
sectionLabels: {id: number; label: string;}[];
expandedSections: number[];