Merged in develop (pull request #136)

Develop
This commit is contained in:
Tiago Ribeiro
2025-01-13 22:44:25 +00:00
36 changed files with 843 additions and 202 deletions

View File

@@ -8,6 +8,9 @@ import { IoTextOutline } from 'react-icons/io5';
import { Switch } from '@headlessui/react'; import { Switch } from '@headlessui/react';
import useExamEditorStore from '@/stores/examEditor'; import useExamEditorStore from '@/stores/examEditor';
import { Module } from '@/interfaces'; import { Module } from '@/interfaces';
import { capitalize } from 'lodash';
import Select from '@/components/Low/Select';
import { Difficulty } from '@/interfaces/exam';
interface Props { interface Props {
module: Module; module: Module;
@@ -36,6 +39,18 @@ const ExerciseWizard: React.FC<Props> = ({
onDiscard, onDiscard,
}) => { }) => {
const [configurations, setConfigurations] = useState<ExerciseConfig[]>([]); const [configurations, setConfigurations] = useState<ExerciseConfig[]>([]);
const { currentModule } = useExamEditorStore();
const { difficulty } = useExamEditorStore(state => state.modules[currentModule]);
const randomDiff = difficulty.length === 1
? capitalize(difficulty[0])
: difficulty.length == 0 ?
"Random" :
`Selected (${difficulty.sort().map(dif => capitalize(dif)).join(", ")})` as Difficulty;
const DIFFICULTIES = difficulty.length === 1
? ["A1", "A2", "B1", "B2", "C1", "C2", "Random"]
: ["A1", "A2", "B1", "B2", "C1", "C2", randomDiff, "Random"];
useEffect(() => { useEffect(() => {
const initialConfigs = selectedExercises.map(exerciseType => { const initialConfigs = selectedExercises.map(exerciseType => {
@@ -164,7 +179,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 isParagraphMatch = config.type.split("?name=")[1] === "paragraphMatch";
const maxParagraphs = isParagraphMatch ? extraArgs!.text.split("\n\n").length : 50; const maxParagraphs = isParagraphMatch ? extraArgs!.text.split("\n\n").length : 50;
@@ -183,9 +198,23 @@ const ExerciseWizard: React.FC<Props> = ({
</> </>
)} )}
</div> </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 <input
type="number" type="number"
value={inputValue} value={inputValue as number}
onChange={(e) => handleParameterChange( onChange={(e) => handleParameterChange(
exerciseIndex, exerciseIndex,
param.param || '', param.param || '',
@@ -195,6 +224,8 @@ const ExerciseWizard: React.FC<Props> = ({
min={1} min={1}
max={maxParagraphs} max={maxParagraphs}
/> />
}
</div> </div>
); );
}; };

View File

@@ -20,7 +20,6 @@ import {
FaQuestionCircle, FaQuestionCircle,
} from 'react-icons/fa'; } from 'react-icons/fa';
import { ExerciseGen } from './generatedExercises'; import { ExerciseGen } from './generatedExercises';
import { MdRadioButtonChecked } from 'react-icons/md';
import { BsListCheck } from 'react-icons/bs'; import { BsListCheck } from 'react-icons/bs';
const quantity = (quantity: number, tooltip?: string) => { 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 = () => { const generate = () => {
return { return {
param: "generate", param: "generate",
@@ -52,6 +59,7 @@ const reading = (passage: number) => {
value: "multipleChoice" value: "multipleChoice"
}, },
quantity(5, "Quantity of Multiple Choice Questions"), quantity(5, "Quantity of Multiple Choice Questions"),
difficulty(),
generate() generate()
], ],
module: "reading" module: "reading"
@@ -73,6 +81,7 @@ const reading = (passage: number) => {
value: 1 value: 1
}, },
quantity(4, "Quantity of Blanks"), quantity(4, "Quantity of Blanks"),
difficulty(),
generate() generate()
], ],
module: "reading" module: "reading"
@@ -94,6 +103,7 @@ const reading = (passage: number) => {
value: 3 value: 3
}, },
quantity(4, "Quantity of Blanks"), quantity(4, "Quantity of Blanks"),
difficulty(),
generate() generate()
], ],
module: "reading" module: "reading"
@@ -109,6 +119,7 @@ const reading = (passage: number) => {
value: "trueFalse" value: "trueFalse"
}, },
quantity(4, "Quantity of Statements"), quantity(4, "Quantity of Statements"),
difficulty(),
generate() generate()
], ],
module: "reading" module: "reading"
@@ -124,6 +135,7 @@ const reading = (passage: number) => {
value: "paragraphMatch" value: "paragraphMatch"
}, },
quantity(5, "Quantity of Matches"), quantity(5, "Quantity of Matches"),
difficulty(),
generate() generate()
], ],
module: "reading" module: "reading"
@@ -143,6 +155,7 @@ const reading = (passage: number) => {
value: "ideaMatch" value: "ideaMatch"
}, },
quantity(5, "Quantity of Ideas"), quantity(5, "Quantity of Ideas"),
difficulty(),
generate() generate()
], ],
module: "reading" module: "reading"
@@ -165,6 +178,7 @@ const listening = (section: number) => {
value: section == 3 ? "multipleChoice3Options" : "multipleChoice" value: section == 3 ? "multipleChoice3Options" : "multipleChoice"
}, },
quantity(5, "Quantity of Multiple Choice Questions"), quantity(5, "Quantity of Multiple Choice Questions"),
difficulty(),
generate() generate()
], ],
module: "listening" module: "listening"
@@ -180,6 +194,7 @@ const listening = (section: number) => {
value: "writeBlanksQuestions" value: "writeBlanksQuestions"
}, },
quantity(5, "Quantity of Blanks"), quantity(5, "Quantity of Blanks"),
difficulty(),
generate() generate()
], ],
module: "listening" module: "listening"
@@ -195,6 +210,7 @@ const listening = (section: number) => {
value: "trueFalse" value: "trueFalse"
}, },
quantity(4, "Quantity of Statements"), quantity(4, "Quantity of Statements"),
difficulty(),
generate() generate()
], ],
module: "listening" module: "listening"
@@ -214,6 +230,7 @@ const listening = (section: number) => {
value: "writeBlanksFill" value: "writeBlanksFill"
}, },
quantity(5, "Quantity of Blanks"), quantity(5, "Quantity of Blanks"),
difficulty(),
generate() generate()
], ],
module: "listening" module: "listening"
@@ -231,6 +248,7 @@ const listening = (section: number) => {
value: "writeBlanksForm" value: "writeBlanksForm"
}, },
quantity(5, "Quantity of Blanks"), quantity(5, "Quantity of Blanks"),
difficulty(),
generate() generate()
], ],
module: "listening" module: "listening"
@@ -251,6 +269,7 @@ const EXERCISES: ExerciseGen[] = [
value: "multipleChoice" value: "multipleChoice"
}, },
quantity(10, "Amount"), quantity(10, "Amount"),
difficulty(),
generate() generate()
], ],
module: "level" module: "level"
@@ -265,6 +284,7 @@ const EXERCISES: ExerciseGen[] = [
value: "mcBlank" value: "mcBlank"
}, },
quantity(10, "Amount"), quantity(10, "Amount"),
difficulty(),
generate() generate()
], ],
module: "level" module: "level"
@@ -279,6 +299,7 @@ const EXERCISES: ExerciseGen[] = [
value: "mcUnderline" value: "mcUnderline"
}, },
quantity(10, "Amount"), quantity(10, "Amount"),
difficulty(),
generate() generate()
], ],
module: "level" module: "level"
@@ -294,6 +315,7 @@ const EXERCISES: ExerciseGen[] = [
param: "text_size", param: "text_size",
value: "250" value: "250"
}, },
difficulty(),
generate() generate()
], ],
module: "level" module: "level"
@@ -313,6 +335,7 @@ const EXERCISES: ExerciseGen[] = [
param: "text_size", param: "text_size",
value: "250" value: "250"
}, },
difficulty(),
generate() generate()
], ],
module: "level" module: "level"
@@ -345,6 +368,7 @@ const EXERCISES: ExerciseGen[] = [
param: "text_size", param: "text_size",
value: "700" value: "700"
}, },
difficulty(),
generate() generate()
], ],
module: "level" module: "level"
@@ -360,6 +384,7 @@ const EXERCISES: ExerciseGen[] = [
value: "", value: "",
type: "text" type: "text"
}, },
difficulty(),
generate() generate()
], ],
module: "writing" module: "writing"
@@ -375,6 +400,7 @@ const EXERCISES: ExerciseGen[] = [
value: "", value: "",
type: "text" type: "text"
}, },
difficulty(),
generate() generate()
], ],
module: "writing" module: "writing"
@@ -384,6 +410,7 @@ const EXERCISES: ExerciseGen[] = [
type: "speaking_1", type: "speaking_1",
icon: FaComments, icon: FaComments,
extra: [ extra: [
difficulty(),
generate(), generate(),
{ {
label: "First Topic", label: "First Topic",
@@ -405,6 +432,7 @@ const EXERCISES: ExerciseGen[] = [
type: "speaking_2", type: "speaking_2",
icon: FaUserFriends, icon: FaUserFriends,
extra: [ extra: [
difficulty(),
generate(), generate(),
{ {
label: "Topic", label: "Topic",
@@ -420,6 +448,7 @@ const EXERCISES: ExerciseGen[] = [
type: "speaking_3", type: "speaking_3",
icon: FaHandshake, icon: FaHandshake,
extra: [ extra: [
difficulty(),
generate(), generate(),
{ {
label: "Topic", label: "Topic",

View File

@@ -17,6 +17,6 @@ export interface ExerciseGen {
type: string; type: string;
icon: IconType; icon: IconType;
sectionId?: number; 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 module: string
} }

View File

@@ -13,12 +13,13 @@ import { BsArrowRepeat } from "react-icons/bs";
interface ExercisePickerProps { interface ExercisePickerProps {
module: string; module: string;
sectionId: number; sectionId: number;
difficulty: string;
extraArgs?: Record<string, any>; extraArgs?: Record<string, any>;
levelSectionId?: number; levelSectionId?: number;
level?: boolean; level?: boolean;
} }
const DIFFICULTIES: string[] = ["A1", "A2", "B1", "B2", "C1", "C2"];
const ExercisePicker: React.FC<ExercisePickerProps> = ({ const ExercisePicker: React.FC<ExercisePickerProps> = ({
module, module,
sectionId, sectionId,
@@ -26,7 +27,7 @@ const ExercisePicker: React.FC<ExercisePickerProps> = ({
levelSectionId, levelSectionId,
level = false level = false
}) => { }) => {
const { currentModule, dispatch } = useExamEditorStore(); const { currentModule } = useExamEditorStore();
const { difficulty, sections } = useExamEditorStore((store) => store.modules[level ? "level" : currentModule]); const { difficulty, sections } = useExamEditorStore((store) => store.modules[level ? "level" : currentModule]);
const section = sections.find((s) => s.sectionId === (level ? levelSectionId : sectionId)); const section = sections.find((s) => s.sectionId === (level ? levelSectionId : sectionId));
@@ -67,6 +68,9 @@ const ExercisePicker: React.FC<ExercisePickerProps> = ({
}), }),
...(config.params.max_words !== undefined && { ...(config.params.max_words !== undefined && {
max_words: Number(config.params.max_words) max_words: Number(config.params.max_words)
}),
...((DIFFICULTIES.includes(config.params.difficulty as string) || config.params.difficulty === "Random") && {
difficulty: config.params.difficulty
}) })
}; };
}); });
@@ -100,8 +104,8 @@ const ExercisePicker: React.FC<ExercisePickerProps> = ({
method: 'POST', method: 'POST',
body: { body: {
...context, ...context,
exercises: exercises, exercises,
difficulty: difficulty difficulty
} }
}, },
(data: any) => [{ (data: any) => [{
@@ -112,7 +116,11 @@ const ExercisePicker: React.FC<ExercisePickerProps> = ({
); );
} else if (module === "writing") { } else if (module === "writing") {
configurations.forEach((config) => { 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( generate(
config.type === 'writing_letter' ? 1 : 2, config.type === 'writing_letter' ? 1 : 2,
"writing", "writing",
@@ -122,7 +130,8 @@ const ExercisePicker: React.FC<ExercisePickerProps> = ({
queryParams queryParams
}, },
(data: any) => [{ (data: any) => [{
prompt: data.question prompt: data.question,
difficulty: data.difficulty
}], }],
levelSectionId, levelSectionId,
level level
@@ -135,6 +144,7 @@ const ExercisePicker: React.FC<ExercisePickerProps> = ({
topic: config.params.topic as string, topic: config.params.topic as string,
first_topic: config.params.first_topic as string, first_topic: config.params.first_topic as string,
second_topic: config.params.second_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 !== '') }).filter(([_, value]) => value && value !== '')
); );
let query = Object.keys(queryParams).length === 0 ? undefined : queryParams; let query = Object.keys(queryParams).length === 0 ? undefined : queryParams;
@@ -152,19 +162,22 @@ const ExercisePicker: React.FC<ExercisePickerProps> = ({
return [{ return [{
prompts: data.questions, prompts: data.questions,
first_topic: data.first_topic, first_topic: data.first_topic,
second_topic: data.second_topic second_topic: data.second_topic,
difficulty: data.difficulty
}]; }];
case 2: case 2:
return [{ return [{
topic: data.topic, topic: data.topic,
question: data.question, question: data.question,
prompts: data.prompts, prompts: data.prompts,
suffix: data.suffix suffix: data.suffix,
difficulty: data.difficulty
}]; }];
case 3: case 3:
return [{ return [{
topic: data.topic, topic: data.topic,
questions: data.questions questions: data.questions,
difficulty: data.difficulty
}]; }];
default: default:
return [data]; return [data];

View File

@@ -1,5 +1,5 @@
import { FillBlanksExercise, ReadingPart } from "@/interfaces/exam"; import { Difficulty, FillBlanksExercise, ReadingPart } from "@/interfaces/exam";
import { useEffect, useReducer, useState } from "react"; import { useCallback, useEffect, useReducer, useState } from "react";
import BlanksEditor from ".."; import BlanksEditor from "..";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { MdEdit, MdEditOff } from "react-icons/md"; import { MdEdit, MdEditOff } from "react-icons/md";
@@ -21,6 +21,7 @@ interface Word {
const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: number }> = ({ exercise, sectionId }) => { const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: number }> = ({ exercise, sectionId }) => {
const { currentModule, dispatch } = useExamEditorStore(); const { currentModule, dispatch } = useExamEditorStore();
const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty);
const { state } = useExamEditorStore( const { state } = useExamEditorStore(
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)! (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); setEditingAlert(editing, setAlerts);
}, [editing]) }, [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 ( return (
<div className="space-y-4"> <div className="space-y-4">
<BlanksEditor <BlanksEditor
alerts={alerts} alerts={alerts}
editing={editing} editing={editing}
state={blanksState} state={blanksState}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
blanksDispatcher={blanksDispatcher} blanksDispatcher={blanksDispatcher}
description="Place blanks and assign words from the word bank" description="Place blanks and assign words from the word bank"
initialText={local.text} initialText={local.text}

View File

@@ -1,5 +1,5 @@
import { FillBlanksExercise, FillBlanksMCOption, ReadingPart } from "@/interfaces/exam"; import { Difficulty, FillBlanksExercise, FillBlanksMCOption, ReadingPart } from "@/interfaces/exam";
import { useEffect, useReducer, useState } from "react"; import { useCallback, useEffect, useReducer, useState } from "react";
import BlanksEditor from ".."; import BlanksEditor from "..";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import useExamEditorStore from "@/stores/examEditor"; import useExamEditorStore from "@/stores/examEditor";
@@ -15,6 +15,7 @@ import MCOption from "./MCOption";
const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }> = ({ exercise, sectionId }) => { const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }> = ({ exercise, sectionId }) => {
const { currentModule, dispatch } = useExamEditorStore(); const { currentModule, dispatch } = useExamEditorStore();
const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty);
const { state } = useExamEditorStore( const { state } = useExamEditorStore(
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)! (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 // eslint-disable-next-line react-hooks/exhaustive-deps
}, [blanksState.blanks]); }, [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 ( return (
<div className="space-y-4"> <div className="space-y-4">
<BlanksEditor <BlanksEditor
alerts={alerts} alerts={alerts}
editing={editing} editing={editing}
state={blanksState} state={blanksState}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
blanksDispatcher={blanksDispatcher} blanksDispatcher={blanksDispatcher}
description="Place blanks and select the correct answer from multiple choice options" description="Place blanks and select the correct answer from multiple choice options"
initialText={local.text} initialText={local.text}

View File

@@ -1,8 +1,8 @@
import useSectionEdit from "@/components/ExamEditor/Hooks/useSectionEdit"; import useSectionEdit from "@/components/ExamEditor/Hooks/useSectionEdit";
import { Card, CardContent } from "@/components/ui/card"; 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 useExamEditorStore from "@/stores/examEditor";
import { useState, useReducer, useEffect } from "react"; import { useState, useReducer, useEffect, useCallback } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import BlanksEditor from ".."; import BlanksEditor from "..";
import { AlertItem } from "../../Shared/Alert"; import { AlertItem } from "../../Shared/Alert";
@@ -13,6 +13,7 @@ import AlternativeSolutions from "./AlternativeSolutions";
const WriteBlanksFill: React.FC<{ exercise: WriteBlanksExercise; sectionId: number }> = ({ exercise, sectionId }) => { const WriteBlanksFill: React.FC<{ exercise: WriteBlanksExercise; sectionId: number }> = ({ exercise, sectionId }) => {
const { currentModule, dispatch } = useExamEditorStore(); const { currentModule, dispatch } = useExamEditorStore();
const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty);
const { state } = useExamEditorStore( const { state } = useExamEditorStore(
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)! (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); setEditingAlert(editing, setAlerts);
}, [editing]); }, [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 ( return (
<div className="space-y-4"> <div className="space-y-4">
<BlanksEditor <BlanksEditor
@@ -171,6 +182,8 @@ const WriteBlanksFill: React.FC<{ exercise: WriteBlanksExercise; sectionId: numb
blanksDispatcher={blanksDispatcher} blanksDispatcher={blanksDispatcher}
description={local.prompt} description={local.prompt}
initialText={local.text} initialText={local.text}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
module={currentModule} module={currentModule}
showBlankBank={true} showBlankBank={true}
onBlankSelect={(blankId) => setSelectedBlankId(blankId?.toString() || null)} 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 { Blank, DropZone } from "./DragNDrop";
import { getTextSegments, BlankState, BlanksState, BlanksAction, BlankToken } from "./BlanksReducer"; import { getTextSegments, BlankState, BlanksState, BlanksAction, BlankToken } from "./BlanksReducer";
import PromptEdit from "../Shared/PromptEdit"; import PromptEdit from "../Shared/PromptEdit";
import { Difficulty } from "@/interfaces/exam";
interface Props { interface Props {
title?: string; title?: string;
initialText: string; initialText: string;
description: string; description: string;
difficulty?: Difficulty;
saveDifficulty: (difficulty: Difficulty) => void;
state: BlanksState; state: BlanksState;
module: string; module: string;
editing: boolean; editing: boolean;
@@ -49,6 +52,8 @@ const BlanksEditor: React.FC<Props> = ({
title = "Fill Blanks", title = "Fill Blanks",
initialText, initialText,
description, description,
difficulty,
saveDifficulty,
state, state,
editing, editing,
module, module,
@@ -169,6 +174,8 @@ const BlanksEditor: React.FC<Props> = ({
title={title} title={title}
description={description} description={description}
editing={editing} editing={editing}
difficulty={difficulty}
saveDifficulty={saveDifficulty}
handleSave={onSave} handleSave={onSave}
handleDelete={onDelete} handleDelete={onDelete}
handleDiscard={onDiscard} handleDiscard={onDiscard}

View File

@@ -1,10 +1,10 @@
import React, { useState, useMemo, useEffect } from 'react'; import React, { useState, useMemo, useEffect, useCallback } from 'react';
import { import {
MdAdd, MdAdd,
MdVisibility, MdVisibility,
MdVisibilityOff MdVisibilityOff
} from 'react-icons/md'; } from 'react-icons/md';
import { MatchSentencesExercise, ReadingPart } from '@/interfaces/exam'; import { Difficulty, MatchSentencesExercise, ReadingPart } from '@/interfaces/exam';
import Alert, { AlertItem } from '../Shared/Alert'; import Alert, { AlertItem } from '../Shared/Alert';
import ReferenceViewer from './ParagraphViewer'; import ReferenceViewer from './ParagraphViewer';
import Header from '../../Shared/Header'; 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 MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: number }> = ({ exercise, sectionId }) => {
const { currentModule, dispatch } = useExamEditorStore(); const { currentModule, dispatch } = useExamEditorStore();
const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty);
const { state } = useExamEditorStore( const { state } = useExamEditorStore(
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)! (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)); 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 ( return (
<div className="flex flex-col mx-auto p-2"> <div className="flex flex-col mx-auto p-2">
<Header <Header
title={exercise.variant && exercise.variant == "ideaMatch" ? "Idea Match" : "Paragraph Match"} title={exercise.variant && exercise.variant == "ideaMatch" ? "Idea Match" : "Paragraph Match"}
description={`Edit ${exercise.variant && exercise.variant == "ideaMatch" ? "ideas/opinions" : "headings"} and their matches`} description={`Edit ${exercise.variant && exercise.variant == "ideaMatch" ? "ideas/opinions" : "headings"} and their matches`}
editing={editing} editing={editing}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
handleSave={handleSave} handleSave={handleSave}
handleDelete={handleDelete} handleDelete={handleDelete}
handleDiscard={handleDiscard} handleDiscard={handleDiscard}

View File

@@ -5,9 +5,9 @@ import UnderlineQuestion from "./UnderlineQuestion";
import useSectionEdit from "@/components/ExamEditor/Hooks/useSectionEdit"; import useSectionEdit from "@/components/ExamEditor/Hooks/useSectionEdit";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import setEditingAlert from "../../Shared/setEditingAlert"; 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 useExamEditorStore from "@/stores/examEditor";
import { useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { MdAdd } from "react-icons/md"; import { MdAdd } from "react-icons/md";
import Alert, { AlertItem } from "../../Shared/Alert"; import Alert, { AlertItem } from "../../Shared/Alert";
import PromptEdit from "../../Shared/PromptEdit"; import PromptEdit from "../../Shared/PromptEdit";
@@ -18,6 +18,7 @@ const UnderlineMultipleChoice: React.FC<{exercise: MultipleChoiceExercise, secti
sectionId, sectionId,
}) => { }) => {
const { currentModule, dispatch } = useExamEditorStore(); const { currentModule, dispatch } = useExamEditorStore();
const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty);
const { state } = useExamEditorStore( const { state } = useExamEditorStore(
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)! (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 ( return (
<div className="p-4"> <div className="p-4">
<Header <Header
title='Underline Multiple Choice Exercise' title='Underline Multiple Choice Exercise'
description="Edit questions with 4 underline options each" description="Edit questions with 4 underline options each"
editing={editing} editing={editing}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
handleSave={handleSave} handleSave={handleSave}
handleDelete={handleDelete} handleDelete={handleDelete}
handlePractice={handlePractice} 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 { Card, CardContent } from '@/components/ui/card';
import { import {
MdAdd, MdAdd,
MdEdit, MdEdit,
MdEditOff, MdEditOff,
} from 'react-icons/md'; } 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 clsx from 'clsx';
import useExamEditorStore from '@/stores/examEditor'; import useExamEditorStore from '@/stores/examEditor';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -74,7 +74,8 @@ const validateMultipleChoiceQuestions = (
}; };
const MultipleChoice: React.FC<MultipleChoiceProps> = ({ exercise, sectionId, optionsQuantity }) => { 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( const { state } = useExamEditorStore(
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)! (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)); 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 ( return (
<div className="p-4"> <div className="p-4">
<Header <Header
title='Multiple Choice Exercise' title='Multiple Choice Exercise'
description={`Edit questions with ${optionsQuantity} options each`} description={`Edit questions with ${optionsQuantity} options each`}
editing={editing} editing={editing}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
handleSave={handleSave} handleSave={handleSave}
handleDelete={handleDelete} handleDelete={handleDelete}
handleDiscard={handleDiscard} handleDiscard={handleDiscard}

View File

@@ -5,10 +5,10 @@ import { AiOutlineUnorderedList, AiOutlinePlus, AiOutlineDelete } from 'react-ic
import { Tooltip } from "react-tooltip"; import { Tooltip } from "react-tooltip";
import Header from "../../Shared/Header"; import Header from "../../Shared/Header";
import GenLoader from "../Shared/GenLoader"; import GenLoader from "../Shared/GenLoader";
import { useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import useSectionEdit from "../../Hooks/useSectionEdit"; import useSectionEdit from "../../Hooks/useSectionEdit";
import useExamEditorStore from "@/stores/examEditor"; 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 { BsFileText } from "react-icons/bs";
import { FaChevronLeft, FaChevronRight } from "react-icons/fa6"; import { FaChevronLeft, FaChevronRight } from "react-icons/fa6";
import { RiVideoLine } from "react-icons/ri"; import { RiVideoLine } from "react-icons/ri";
@@ -21,7 +21,8 @@ interface Props {
} }
const InteractiveSpeaking: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }) => { 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 [local, setLocal] = useState(exercise);
const [currentVideoIndex, setCurrentVideoIndex] = useState(0); const [currentVideoIndex, setCurrentVideoIndex] = useState(0);
@@ -105,13 +106,17 @@ const InteractiveSpeaking: React.FC<Props> = ({ sectionId, exercise, module = "s
useEffect(() => { useEffect(() => {
if (genResult && generating === "speakingScript") { 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 = { const updatedLocal = {
...local, ...local,
title: genResult.result[0].title, title: genResult.result[0].title,
prompts: genResult.result[0].prompts.map((item: any) => ({ prompts: genResult.result[0].prompts.map((item: any) => ({
text: item || "", text: item || "",
video_url: "" video_url: ""
})) })),
difficulty: genResult.result[0].difficulty
}; };
setEditing(true); setEditing(true);
setLocal(updatedLocal); setLocal(updatedLocal);
@@ -158,13 +163,17 @@ const InteractiveSpeaking: React.FC<Props> = ({ sectionId, exercise, module = "s
const isGenerating = levelGenerating?.includes(`${local.id}-speakingScript`); const isGenerating = levelGenerating?.includes(`${local.id}-speakingScript`);
if (speakingScript && isGenerating) { if (speakingScript && isGenerating) {
if (!difficulty.includes(speakingScript.result[0].difficulty)) {
dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, speakingScript.result[0].difficulty]} } });
}
const updatedLocal = { const updatedLocal = {
...local, ...local,
title: speakingScript.result[0].title, title: speakingScript.result[0].title,
prompts: speakingScript.result[0].prompts.map((item: any) => ({ prompts: speakingScript.result[0].prompts.map((item: any) => ({
text: item || "", text: item || "",
video_url: "" video_url: ""
})) })),
difficulty: speakingScript.result[0].difficulty
}; };
setEditing(true); setEditing(true);
setLocal(updatedLocal); 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 ( return (
<> <>
<div className='relative pb-4'> <div className='relative pb-4'>
@@ -271,6 +295,8 @@ const InteractiveSpeaking: React.FC<Props> = ({ sectionId, exercise, module = "s
title={`Interactive Speaking Script`} title={`Interactive Speaking Script`}
description='Generate or write the scripts for the videos.' description='Generate or write the scripts for the videos.'
editing={editing} editing={editing}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
handleSave={handleSave} handleSave={handleSave}
handleEdit={handleEdit} handleEdit={handleEdit}
handleDiscard={handleDiscard} 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 AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { AiOutlineUnorderedList, AiOutlinePlus, AiOutlineDelete } from 'react-icons/ai'; import { AiOutlineUnorderedList, AiOutlinePlus, AiOutlineDelete } from 'react-icons/ai';
@@ -6,7 +6,7 @@ import Header from "../../Shared/Header";
import GenLoader from "../Shared/GenLoader"; import GenLoader from "../Shared/GenLoader";
import useSectionEdit from "../../Hooks/useSectionEdit"; import useSectionEdit from "../../Hooks/useSectionEdit";
import useExamEditorStore from "@/stores/examEditor"; 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 { BsFileText } from "react-icons/bs";
import { RiVideoLine } from 'react-icons/ri'; import { RiVideoLine } from 'react-icons/ri';
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa6'; import { FaChevronLeft, FaChevronRight } from 'react-icons/fa6';
@@ -19,7 +19,8 @@ interface Props {
} }
const Speaking1: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }) => { 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 [local, setLocal] = useState(() => {
const defaultPrompts = [ const defaultPrompts = [
{ text: "Hello my name is {avatar}, what is yours?", video_url: "" }, { 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(() => { useEffect(() => {
if (genResult && generating === "speakingScript") { 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 = { const updatedLocal = {
...local, ...local,
first_title: genResult.result[0].first_topic, first_title: genResult.result[0].first_topic,
@@ -129,7 +133,8 @@ const Speaking1: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
text: item, text: item,
video_url: "" video_url: ""
})) }))
] ],
difficulty: genResult.result[0].difficulty
}; };
setEditing(true); setEditing(true);
setLocal(updatedLocal); setLocal(updatedLocal);
@@ -176,10 +181,14 @@ const Speaking1: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
const isGenerating = levelGenerating?.includes(`${local.id}-speakingScript`); const isGenerating = levelGenerating?.includes(`${local.id}-speakingScript`);
if (speakingScript && isGenerating) { if (speakingScript && isGenerating) {
if (!difficulty.includes(speakingScript.result[0].difficulty)) {
dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, speakingScript.result[0].difficulty]} } });
}
const updatedLocal = { const updatedLocal = {
...local, ...local,
first_title: speakingScript.result[0].first_topic, first_title: speakingScript.result[0].first_topic,
second_title: speakingScript.result[0].second_topic, second_title: speakingScript.result[0].second_topic,
difficulty: speakingScript.result[0].difficulty,
prompts: [ prompts: [
local.prompts[0], local.prompts[0],
local.prompts[1], 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 ( return (
<> <>
<div className='relative pb-4'> <div className='relative pb-4'>
@@ -307,6 +331,8 @@ const Speaking1: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
title={`Speaking 1 Script`} title={`Speaking 1 Script`}
description='Generate or write the scripts for the videos.' description='Generate or write the scripts for the videos.'
editing={editing} editing={editing}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
handleSave={handleSave} handleSave={handleSave}
handleEdit={handleEdit} handleEdit={handleEdit}
handleDiscard={handleDiscard} handleDiscard={handleDiscard}

View File

@@ -1,9 +1,9 @@
import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea"; import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { AiOutlinePlus, AiOutlineDelete } from 'react-icons/ai'; 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 useExamEditorStore from "@/stores/examEditor";
import { useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import useSectionEdit from "../../Hooks/useSectionEdit"; import useSectionEdit from "../../Hooks/useSectionEdit";
import Header from "../../Shared/Header"; import Header from "../../Shared/Header";
import { Tooltip } from "react-tooltip"; import { Tooltip } from "react-tooltip";
@@ -21,7 +21,8 @@ interface Props {
} }
const Speaking2: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }) => { 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 [local, setLocal] = useState(exercise);
const { sections } = useExamEditorStore((store) => store.modules[module]); const { sections } = useExamEditorStore((store) => store.modules[module]);
@@ -104,11 +105,15 @@ const Speaking2: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
useEffect(() => { useEffect(() => {
if (genResult && generating === "speakingScript") { 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 = { const updatedLocal = {
...local, ...local,
title: genResult.result[0].topic, title: genResult.result[0].topic,
text: genResult.result[0].question, text: genResult.result[0].question,
prompts: genResult.result[0].prompts prompts: genResult.result[0].prompts,
difficulty: genResult.result[0].difficulty
}; };
setEditing(true); setEditing(true);
setLocal(updatedLocal); 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 speakingScript = levelGenResults.find((res) => res.generating === `${local.id}-speakingScript`);
const generating = levelGenerating.find((res) => res === `${local.id}-speakingScript`); const generating = levelGenerating.find((res) => res === `${local.id}-speakingScript`);
if (speakingScript && generating) { if (speakingScript && generating) {
if (!difficulty.includes(speakingScript.result[0].difficulty)) {
dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, speakingScript.result[0].difficulty]} } });
}
const updatedLocal = { const updatedLocal = {
...local, ...local,
title: speakingScript.result[0].topic, title: speakingScript.result[0].topic,
text: speakingScript.result[0].question, text: speakingScript.result[0].question,
prompts: speakingScript.result[0].prompts prompts: speakingScript.result[0].prompts,
difficulty: speakingScript.result[0].difficulty
}; };
setEditing(true); setEditing(true);
setLocal(updatedLocal); setLocal(updatedLocal);
@@ -244,6 +253,21 @@ const Speaking2: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
</div> </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 ( return (
<> <>
<div className='relative pb-4'> <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`} title={`Speaking ${module === "level" ? local.sectionId : sectionId} Script`}
description='Generate or write the script for the video.' description='Generate or write the script for the video.'
editing={editing} editing={editing}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
handleSave={handleSave} handleSave={handleSave}
handleEdit={handleEdit} handleEdit={handleEdit}
handleDiscard={handleDiscard} handleDiscard={handleDiscard}

View File

@@ -1,9 +1,9 @@
import React, { useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { import {
MdAdd, MdAdd,
} from 'react-icons/md'; } from 'react-icons/md';
import Alert, { AlertItem } from '../Shared/Alert'; 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 QuestionsList from '../Shared/QuestionsList';
import Header from '../../Shared/Header'; import Header from '../../Shared/Header';
import SortableQuestion from '../Shared/SortableQuestion'; 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 TrueFalse: React.FC<{ exercise: TrueFalseExercise, sectionId: number }> = ({ exercise, sectionId }) => {
const { currentModule, dispatch } = useExamEditorStore(); const { currentModule, dispatch } = useExamEditorStore();
const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty);
const { state } = useExamEditorStore( const { state } = useExamEditorStore(
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)! (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)); 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 ( return (
<div className="p-4"> <div className="p-4">
<Header <Header
title='True/False/Not Given Exercise' title='True/False/Not Given Exercise'
description='Edit questions and their solutions' description='Edit questions and their solutions'
editing={editing} editing={editing}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
handleSave={handleSave} handleSave={handleSave}
handleDelete={handleDelete} handleDelete={handleDelete}
handleDiscard={handleDiscard} 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 { Card, CardContent } from '@/components/ui/card';
import { import {
MdAdd, MdAdd,
@@ -13,7 +13,7 @@ import Header from '../../Shared/Header';
import clsx from 'clsx'; import clsx from 'clsx';
import Alert, { AlertItem } from '../Shared/Alert'; import Alert, { AlertItem } from '../Shared/Alert';
import AutoExpandingTextArea from '@/components/Low/AutoExpandingTextarea'; 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 useExamEditorStore from '@/stores/examEditor';
import useSectionEdit from '../../Hooks/useSectionEdit'; import useSectionEdit from '../../Hooks/useSectionEdit';
import setEditingAlert from '../Shared/setEditingAlert'; 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 WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise; }> = ({ sectionId, exercise }) => {
const { currentModule, dispatch } = useExamEditorStore(); const { currentModule, dispatch } = useExamEditorStore();
const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty);
const { state } = useExamEditorStore( const { state } = useExamEditorStore(
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)! (state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
); );
@@ -231,12 +231,24 @@ const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise;
}, [local.solutions]); }, [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 ( return (
<div className="p-4"> <div className="p-4">
<Header <Header
title={"Write Blanks: Questions"} title={"Write Blanks: Questions"}
description="Edit questions and their solutions" description="Edit questions and their solutions"
editing={editing} editing={editing}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
handleSave={handleSave} handleSave={handleSave}
handleDiscard={handleDiscard} handleDiscard={handleDiscard}
handleDelete={handleDelete} handleDelete={handleDelete}

View File

@@ -1,10 +1,10 @@
import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea"; import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea";
import { Card, CardContent } from "@/components/ui/card"; 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 useExamEditorStore from "@/stores/examEditor";
import { DragEndEvent } from "@dnd-kit/core"; import { DragEndEvent } from "@dnd-kit/core";
import { arrayMove } from "@dnd-kit/sortable"; 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 { MdEditOff, MdEdit, MdDelete, MdAdd } from "react-icons/md";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import useSectionEdit from "../../Hooks/useSectionEdit"; 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 WriteBlanksForm: React.FC<{ sectionId: number; exercise: WriteBlanksExercise }> = ({ sectionId, exercise }) => {
const { currentModule, dispatch } = useExamEditorStore(); const { currentModule, dispatch } = useExamEditorStore();
const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty);
const { state } = useExamEditorStore( const { state } = useExamEditorStore(
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)! (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 }); 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 ( return (
<div className="p-4"> <div className="p-4">
<Header <Header
title="Write Blanks: Form Exercise" title="Write Blanks: Form Exercise"
description="Edit questions and their solutions" description="Edit questions and their solutions"
editing={editing} editing={editing}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
handleSave={handleSave} handleSave={handleSave}
handleDiscard={handleDiscard} handleDiscard={handleDiscard}
handleDelete={handleDelete} handleDelete={handleDelete}

View File

@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from "react";
import useExamEditorStore from "@/stores/examEditor"; import useExamEditorStore from "@/stores/examEditor";
import ExamEditorStore, { ModuleState } from "@/stores/examEditor/types"; import ExamEditorStore, { ModuleState } from "@/stores/examEditor/types";
import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea"; 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 Header from "../../Shared/Header";
import Alert, { AlertItem } from "../Shared/Alert"; import Alert, { AlertItem } from "../Shared/Alert";
import clsx from "clsx"; import clsx from "clsx";
@@ -20,6 +20,7 @@ interface Props {
const Writing: React.FC<Props> = ({ sectionId, exercise, module, index }) => { const Writing: React.FC<Props> = ({ sectionId, exercise, module, index }) => {
const { currentModule, dispatch } = useExamEditorStore(); const { currentModule, dispatch } = useExamEditorStore();
const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty);
const { type, academic_url } = useExamEditorStore( const { type, academic_url } = useExamEditorStore(
(state) => state.modules[currentModule] (state) => state.modules[currentModule]
); );
@@ -39,6 +40,7 @@ const Writing: React.FC<Props> = ({ sectionId, exercise, module, index }) => {
onSave: () => { onSave: () => {
const newExercise = { ...local } as WritingExercise; const newExercise = { ...local } as WritingExercise;
newExercise.prompt = prompt; newExercise.prompt = prompt;
newExercise.difficulty = exercise.difficulty;
setAlerts([]); setAlerts([]);
setEditing(false); setEditing(false);
if (!level) { if (!level) {
@@ -86,6 +88,12 @@ const Writing: React.FC<Props> = ({ sectionId, exercise, module, index }) => {
if (genResult) { if (genResult) {
setEditing(true); setEditing(true);
setPrompt(genResult.result[0].prompt); 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 } }) dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "generating", value: undefined } })
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // 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); setEditingAlert(prompt !== local.prompt, setAlerts);
}, [prompt, local.prompt]); }, [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 ( return (
<> <>
<div className={clsx('relative', level ? "px-4 mt-2" : "pb-2")}> <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`} title={`${sectionId === 1 ? (type === "academic" ? "Visual Information" : "Letter") : "Essay"} Instructions`}
description='Generate or edit the instructions for the task' description='Generate or edit the instructions for the task'
editing={editing} editing={editing}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
handleSave={handleSave} handleSave={handleSave}
handleDelete={module == "level" ? handleDelete : undefined} handleDelete={module == "level" ? handleDelete : undefined}
handleEdit={handleEdit} handleEdit={handleEdit}

View File

@@ -1,6 +1,6 @@
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import SortableSection from "../../Shared/SortableSection"; 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 ExerciseItem from "./types";
import Dropdown from "@/components/Dropdown"; import Dropdown from "@/components/Dropdown";
import useExamEditorStore from "@/stores/examEditor"; import useExamEditorStore from "@/stores/examEditor";
@@ -32,8 +32,7 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
const dispatch = useExamEditorStore(state => state.dispatch); const dispatch = useExamEditorStore(state => state.dispatch);
const currentModule = useExamEditorStore(state => state.currentModule); const currentModule = useExamEditorStore(state => state.currentModule);
const sections = useExamEditorStore(state => state.modules[currentModule].sections); const {sections, expandedSections, difficulty} = useExamEditorStore(state => state.modules[currentModule]);
const expandedSections = useExamEditorStore(state => state.modules[currentModule].expandedSections);
const section = useExamEditorStore( const section = useExamEditorStore(
state => state.modules[currentModule].sections.find( state => state.modules[currentModule].sections.find(
@@ -50,6 +49,15 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
useEffect(() => { useEffect(() => {
if (genResult && genResult.generating === "exercises" && genResult.module === currentModule) { if (genResult && genResult.generating === "exercises" && genResult.module === currentModule) {
const newExercises = genResult.result[0].exercises; 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({ dispatch({
type: "UPDATE_SECTION_STATE", payload: { type: "UPDATE_SECTION_STATE", payload: {
sectionId, sectionId,
@@ -85,6 +93,18 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
) => { ) => {
const nonWritingOrSpeaking = results[0]?.generating.startsWith("exercises"); 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 = [ const updates = [
{ {
type: "UPDATE_SECTION_STATE", type: "UPDATE_SECTION_STATE",
@@ -94,7 +114,7 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
update: { update: {
exercises: [ exercises: [
...sectionState.exercises, ...sectionState.exercises,
...assignExercisesFn(results) ...newExercises
] ]
} }
} }
@@ -168,6 +188,7 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
results.map(res => ({ results.map(res => ({
...writingTask(res.generating === "writing_letter" ? 1 : 2), ...writingTask(res.generating === "writing_letter" ? 1 : 2),
prompt: res.result[0].prompt, prompt: res.result[0].prompt,
difficulty: res.result[0].difficulty,
variant: res.generating === "writing_letter" ? "letter" : "essay" variant: res.generating === "writing_letter" ? "letter" : "essay"
}) as WritingExercise); }) as WritingExercise);

View File

@@ -22,6 +22,7 @@ const getSpeakingTaskData = (taskNumber: number, data: any) => {
video_url: "" video_url: ""
})) }))
], ],
difficulty: data.difficulty,
sectionId: 1, sectionId: 1,
}; };
case 2: case 2:
@@ -29,6 +30,7 @@ const getSpeakingTaskData = (taskNumber: number, data: any) => {
title: data.topic, title: data.topic,
text: data.question, text: data.question,
prompts: data.prompts, prompts: data.prompts,
difficulty: data.difficulty,
sectionId: 2, sectionId: 2,
type: "speaking" type: "speaking"
}; };
@@ -39,6 +41,7 @@ const getSpeakingTaskData = (taskNumber: number, data: any) => {
text: item || "", text: item || "",
video_url: "" video_url: ""
})), })),
difficulty: data.difficulty,
sectionId: 3, sectionId: 3,
}; };
default: default:

View File

@@ -7,7 +7,7 @@ import { Module } from "@/interfaces";
interface GeneratorConfig { interface GeneratorConfig {
method: 'GET' | 'POST'; method: 'GET' | 'POST';
queryParams?: Record<string, string>; queryParams?: Record<string, string | string[]>;
files?: Record<string, string>; files?: Record<string, string>;
body?: Record<string, any>; body?: Record<string, any>;
} }
@@ -61,9 +61,21 @@ export function generate(
setGenerating(level ? levelSectionId! : sectionId, type, level); setGenerating(level ? levelSectionId! : sectionId, type, level);
const queryString = config.queryParams function buildQueryString(params: Record<string, string | string[]>): string {
? new URLSearchParams(config.queryParams).toString() 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}` : ''}`; const url = `/api/exam/generate/${module}/${sectionId}${queryString ? `?${queryString}` : ''}`;

View File

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

View File

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

View File

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

View File

@@ -7,11 +7,15 @@ import Input from "@/components/Low/Input";
import GenerateBtn from "../Shared/GenerateBtn"; import GenerateBtn from "../Shared/GenerateBtn";
import clsx from "clsx"; import clsx from "clsx";
import { FaFemale, FaMale } from "react-icons/fa"; 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 { toast } from "react-toastify";
import { generateVideos } from "../Shared/generateVideos"; import { generateVideos } from "../Shared/generateVideos";
import { Module } from "@/interfaces"; import { Module } from "@/interfaces";
import useCanGenerate from "./useCanGenerate"; 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 { export interface Avatar {
name: string; name: string;
@@ -36,9 +40,25 @@ const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSche
const [selectedAvatar, setSelectedAvatar] = useState<Avatar | null>(null); const [selectedAvatar, setSelectedAvatar] = useState<Avatar | null>(null);
const randomDiff = difficulty.length === 1
? capitalize(difficulty[0])
: difficulty.length == 0 ?
"Random" :
`Selected (${difficulty.sort().map(dif => capitalize(dif)).join(", ")})` as Difficulty;
const DIFFICULTIES = difficulty.length === 1
? ["A1", "A2", "B1", "B2", "C1", "C2", "Random"]
: ["A1", "A2", "B1", "B2", "C1", "C2", randomDiff, "Random"];
const difficultyOptions: Option[] = DIFFICULTIES.map(level => ({
label: level,
value: level
}));
const [specificDiff, setSpecificDiff] = useState(randomDiff);
const generateScript = useCallback((scriptSectionId: number) => { const generateScript = useCallback((scriptSectionId: number) => {
const queryParams: { const queryParams: {
difficulty: string; difficulty: string[];
first_topic?: string; first_topic?: string;
second_topic?: string; second_topic?: string;
topic?: string; topic?: string;
@@ -70,19 +90,22 @@ const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSche
return [{ return [{
prompts: data.questions, prompts: data.questions,
first_topic: data.first_topic, first_topic: data.first_topic,
second_topic: data.second_topic second_topic: data.second_topic,
difficulty: specificDiff.length == 2 ? specificDiff : difficulty,
}]; }];
case 2: case 2:
return [{ return [{
topic: data.topic, topic: data.topic,
question: data.question, question: data.question,
prompts: data.prompts, prompts: data.prompts,
suffix: data.suffix suffix: data.suffix,
difficulty: specificDiff.length == 2 ? specificDiff : difficulty,
}]; }];
case 3: case 3:
return [{ return [{
title: data.topic, title: data.topic,
prompts: data.questions prompts: data.questions,
difficulty: specificDiff.length == 2 ? specificDiff : difficulty,
}]; }];
default: default:
return [data]; return [data];
@@ -92,7 +115,7 @@ const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSche
level 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) => { const onTopicChange = useCallback((speakingTopic: string) => {
updateLocalAndScheduleGlobal({ speakingTopic }); updateLocalAndScheduleGlobal({ speakingTopic });
@@ -192,7 +215,7 @@ const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSche
contentWrapperClassName={level ? `border border-ielts-speaking` : ''} 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"> <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> <label className="font-normal text-base text-mti-gray-dim">{`${secId === 1 ? "First Topic" : "Topic"}`} (Optional)</label>
<Input <Input
@@ -201,8 +224,9 @@ const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSche
placeholder="Topic" placeholder="Topic"
name="category" name="category"
onChange={onTopicChange} onChange={onTopicChange}
roundness="full" roundness="xl"
value={localSettings.speakingTopic} value={localSettings.speakingTopic}
thin
/> />
</div> </div>
{secId === 1 && {secId === 1 &&
@@ -214,12 +238,69 @@ const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSche
placeholder="Topic" placeholder="Topic"
name="category" name="category"
onChange={onSecondTopicChange} onChange={onSecondTopicChange}
roundness="full" roundness="xl"
value={localSettings.speakingSecondTopic} value={localSettings.speakingSecondTopic}
thin
/> />
</div> </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 <GenerateBtn
module="speaking" module="speaking"
genType={`${id ? `${id}-` : ''}speakingScript`} genType={`${id ? `${id}-` : ''}speakingScript`}

View File

@@ -5,9 +5,13 @@ import { generate } from "../Shared/Generate";
import GenerateBtn from "../Shared/GenerateBtn"; import GenerateBtn from "../Shared/GenerateBtn";
import { LevelSectionSettings, WritingSectionSettings } from "@/stores/examEditor/types"; import { LevelSectionSettings, WritingSectionSettings } from "@/stores/examEditor/types";
import useExamEditorStore from "@/stores/examEditor"; import useExamEditorStore from "@/stores/examEditor";
import { WritingExercise } from "@/interfaces/exam"; import { Difficulty, WritingExercise } from "@/interfaces/exam";
import clsx from "clsx"; import clsx from "clsx";
import { FaFileUpload } from "react-icons/fa"; 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 { interface Props {
@@ -25,6 +29,23 @@ const WritingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSched
type, type,
academic_url academic_url
} = useExamEditorStore((store) => store.modules["writing"]); } = useExamEditorStore((store) => store.modules["writing"]);
const randomDiff = difficulty.length === 1
? capitalize(difficulty[0])
: difficulty.length == 0 ?
"Random" :
`Selected (${difficulty.sort().map(dif => capitalize(dif)).join(", ")})` as Difficulty;
const DIFFICULTIES = difficulty.length === 1
? ["A1", "A2", "B1", "B2", "C1", "C2", "Random"]
: ["A1", "A2", "B1", "B2", "C1", "C2", randomDiff, "Random"];
const difficultyOptions: Option[] = DIFFICULTIES.map(level => ({
label: level,
value: level
}));
const [specificDiff, setSpecificDiff] = useState(randomDiff);
const generatePassage = useCallback((sectionId: number) => { const generatePassage = useCallback((sectionId: number) => {
if (type === "academic" && academic_url !== undefined && sectionId == 1) { if (type === "academic" && academic_url !== undefined && sectionId == 1) {
generate( generate(
@@ -34,7 +55,7 @@ const WritingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSched
{ {
method: 'POST', method: 'POST',
queryParams: { queryParams: {
difficulty, difficulty: specificDiff.length == 2 ? [specificDiff] : difficulty,
type: type! type: type!
}, },
files: { files: {
@@ -42,7 +63,8 @@ const WritingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSched
} }
}, },
(data: any) => [{ (data: any) => [{
prompt: data.question prompt: data.question,
difficulty: data.difficulty
}] }]
) )
} else { } else {
@@ -53,18 +75,18 @@ const WritingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSched
{ {
method: 'GET', method: 'GET',
queryParams: { queryParams: {
difficulty, difficulty: specificDiff.length == 2 ? [specificDiff] : difficulty,
type: type!, type: type!,
...(localSettings.writingTopic && { topic: localSettings.writingTopic }) ...(localSettings.writingTopic && { topic: localSettings.writingTopic })
} }
}, },
(data: any) => [{ (data: any) => [{
prompt: data.question prompt: data.question,
difficulty: data.difficulty
}] }]
); );
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [type, academic_url, currentModule, specificDiff, difficulty, localSettings.writingTopic]);
}, [localSettings.writingTopic, difficulty, academic_url]);
const onTopicChange = useCallback((writingTopic: string) => { const onTopicChange = useCallback((writingTopic: string) => {
updateLocalAndScheduleGlobal({ writingTopic }); updateLocalAndScheduleGlobal({ writingTopic });
@@ -96,10 +118,66 @@ const WritingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSched
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isImageUploadOpen: isOpen }, false)} setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isImageUploadOpen: isOpen }, false)}
contentWrapperClassName={level ? `border border-ielts-writing` : ''} contentWrapperClassName={level ? `border border-ielts-writing` : ''}
> >
<div className="flex flex-row gap-2 items-center px-2 pb-4"> <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-row p-2 gap-4"> <div className="flex flex-col gap-2">
<span className="bg-gray-100 px-3.5 py-2.5 rounded-lg border border-gray-300 text-mti-gray-dim"> <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 Upload a graph, chart or diagram
</span> </span>
<input <input
@@ -124,38 +202,90 @@ const WritingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSched
</button> </button>
</div> </div>
</div> </div>
</div>
</Dropdown>} </Dropdown>}
{ {
(type !== "academic" || (type === "academic" && academic_url !== undefined)) && <Dropdown (type !== "academic" || (type === "academic" && focusedSection == 2)) && <Dropdown
title="Generate Instructions" title="Generate Instructions"
module={"writing"} module={"writing"}
open={localSettings.isWritingTopicOpen} open={localSettings.isWritingTopicOpen}
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isWritingTopicOpen: isOpen }, false)} setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isWritingTopicOpen: isOpen }, false)}
contentWrapperClassName={level ? `border border-ielts-writing` : ''} contentWrapperClassName={level ? `border border-ielts-writing` : ''}
> >
<div className="px-2 pb-4 flex flex-col w-full">
<div className="flex flex-row gap-2 items-center px-2 pb-4"> <div className="flex flex-col gap-4">
{type !== "academic" ? <div className="flex flex-col gap-2">
<div className="flex flex-col flex-grow gap-4 px-2"> <label className="block font-normal text-base text-mti-gray-dim mb-2">Topic (Optional)</label>
<label className="font-normal text-base text-mti-gray-dim">Topic (Optional)</label> <div className="flex gap-2 min-w-0">
<Input <Input
key={`section-${focusedSection}`} key={`section-${focusedSection}`}
type="text" type="text"
placeholder="Topic" placeholder="Topic"
name="category" name="category"
onChange={onTopicChange} onChange={onTopicChange}
roundness="full" roundness="xl"
value={localSettings.writingTopic} value={localSettings.writingTopic}
thin
/> />
</div> </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> </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',
} }
<div className="flex self-end h-16 mb-1"> }),
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 w-full h-full justify-center items-center mt-2">
<GenerateBtn <GenerateBtn
genType="writing" genType="writing"
module={"writing"} module={"writing"}
@@ -164,6 +294,7 @@ const WritingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSched
/> />
</div> </div>
</div> </div>
</div>
</Dropdown> </Dropdown>
} }
</> </>

View File

@@ -1,13 +1,18 @@
import { Module } from "@/interfaces"; import { Module } from "@/interfaces";
import clsx from "clsx"; import clsx from "clsx";
import { ReactNode } from "react"; import { ReactNode, useEffect, useState } from "react";
import { MdDelete, MdEdit, MdEditOff, MdRefresh, MdSave } from "react-icons/md"; import { MdDelete, MdEdit, MdEditOff, MdRefresh, MdSave, MdSignalCellularAlt } from "react-icons/md";
import { HiOutlineClipboardCheck, HiOutlineClipboardList } from "react-icons/hi"; 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 { interface Props {
title: string; title: string;
description: string; description: string;
editing: boolean; editing: boolean;
difficulty?: Difficulty;
saveDifficulty?: (diff: Difficulty) => void;
module?: Module; module?: Module;
handleSave: () => void; handleSave: () => void;
handleDiscard: () => void; handleDiscard: () => void;
@@ -19,20 +24,27 @@ interface Props {
} }
const Header: React.FC<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 ( 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> <div>
<h1 className="text-2xl font-bold text-gray-800">{title}</h1> <h1 className="text-2xl font-bold text-gray-800">{title}</h1>
<p className="text-gray-600 mt-1">{description}</p> <p className="text-gray-600 mt-1">{description}</p>
</div> </div>
<div className="flex gap-2"> <div className="flex w-[50%] ml-auto justify-end flex-wrap gap-2">
{children} <div className="flex flex-wrap gap-2 justify-end">
<button <button
onClick={handleSave} onClick={handleSave}
disabled={!editing} disabled={!editing}
className={ className={
clsx("px-4 py-2 rounded-lg flex items-center gap-2 transition-all duration-200", 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 ? 'bg-green-500 text-white hover:bg-green-600' : 'bg-gray-100 text-gray-400 cursor-not-allowed'
)} )}
> >
@@ -43,7 +55,7 @@ const Header: React.FC<Props> = ({
onClick={handleDiscard} onClick={handleDiscard}
disabled={!editing} disabled={!editing}
className={clsx( className={clsx(
"px-4 py-2 rounded-lg flex items-center gap-2 transition-all duration-200", "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' editing ? 'bg-gray-500 text-white hover:bg-gray-600' : 'bg-gray-100 text-gray-400 cursor-not-allowed'
)} )}
> >
@@ -53,7 +65,7 @@ const Header: React.FC<Props> = ({
{handleEdit && ( {handleEdit && (
<button <button
onClick={handleEdit} 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`} 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} />} {editing ? <MdEditOff size={18} /> : <MdEdit size={18} />}
Edit Edit
@@ -63,7 +75,7 @@ const Header: React.FC<Props> = ({
<button <button
onClick={handlePractice} onClick={handlePractice}
className={clsx( className={clsx(
"px-4 py-2 rounded-lg flex items-center gap-2 transition-all duration-200", "px-3 py-2 rounded-lg flex items-center gap-1 transition-all duration-200 min-w-[90px] justify-center",
isEvaluationEnabled isEvaluationEnabled
? 'bg-amber-500 text-white hover:bg-amber-600' ? 'bg-amber-500 text-white hover:bg-amber-600'
: 'bg-gray-200 text-gray-600 hover:bg-gray-300' : 'bg-gray-200 text-gray-600 hover:bg-gray-300'
@@ -76,12 +88,71 @@ const Header: React.FC<Props> = ({
{handleDelete && ( {handleDelete && (
<button <button
onClick={handleDelete} 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" 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} /> <MdDelete size={18} />
Delete Delete
</button> </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>
</div> </div>
); );

View File

@@ -50,8 +50,10 @@ const ResetModule: React.FC<Props> = ({ module, isOpen, setIsOpen, setNumberOfLe
const handleResetModule = () => { const handleResetModule = () => {
dispatch({ type: 'RESET_MODULE', payload: { module } }); dispatch({ type: 'RESET_MODULE', payload: { module } });
setIsOpen(false); setIsOpen(false);
if (module === "level") {
setNumberOfLevelParts(1); setNumberOfLevelParts(1);
} }
}
return ( return (
<Transition <Transition

View File

@@ -148,9 +148,23 @@ const ExamEditor: React.FC<{ levelParts?: number }> = ({ levelParts = 0 }) => {
<div className="flex flex-col gap-3 flex-grow"> <div className="flex flex-col gap-3 flex-grow">
<label className="font-normal text-base text-mti-gray-dim">Difficulty</label> <label className="font-normal text-base text-mti-gray-dim">Difficulty</label>
<Select <Select
options={DIFFICULTIES.map((x) => ({ value: x, label: capitalize(x) }))} isMulti={true}
onChange={(value) => value && updateModule({ difficulty: value.value as Difficulty })} options={DIFFICULTIES.map((x) => ({
value={{ value: difficulty, label: capitalize(difficulty) }} 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> </div>
{(sectionLabels.length != 0 && currentModule !== "level") ? ( {(sectionLabels.length != 0 && currentModule !== "level") ? (
@@ -202,9 +216,13 @@ const ExamEditor: React.FC<{ levelParts?: number }> = ({ levelParts = 0 }) => {
/> />
</div> </div>
{currentModule === "listening" && <ListeningInstructions />} {currentModule === "listening" && <ListeningInstructions />}
{["reading", "listening", "level"].includes(currentModule) && <Button onClick={() => setIsResetModuleOpen(true)} customColor={`bg-ielts-${currentModule}/70 hover:bg-ielts-${currentModule} border-ielts-${currentModule}`} className={`text-white self-end`}> <Button
onClick={() => setIsResetModuleOpen(true)}
customColor={`bg-ielts-${currentModule}/70 hover:bg-ielts-${currentModule} border-ielts-${currentModule}`}
className={`text-white self-end`}
>
Reset Module Reset Module
</Button>} </Button>
</div> </div>
<div className="flex flex-row gap-8"> <div className="flex flex-row gap-8">
<Settings /> <Settings />

View File

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

View File

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

View File

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

View File

@@ -80,7 +80,6 @@ async function patch(req: NextApiRequest, res: NextApiResponse) {
await Promise.all(newParticipants.map(async (p) => await updateExpiryDateOnGroup(p, group.admin))); await Promise.all(newParticipants.map(async (p) => await updateExpiryDateOnGroup(p, group.admin)));
} }
console.log(req.body);
await db.collection("groups").updateOne({id}, {$set: {id, ...req.body}}, {upsert: true}); await db.collection("groups").updateOne({id}, {$set: {id, ...req.body}}, {upsert: true});
res.status(200).json({ok: true}); res.status(200).json({ok: true});

View File

@@ -153,14 +153,14 @@ export const defaultSectionSettings = (module: Module, sectionId: number, part?:
} }
const defaultModuleSettings = (module: Module, minTimer: number): ModuleState => { const defaultModuleSettings = (module: Module, minTimer: number, reset: boolean = false): ModuleState => {
const state: ModuleState = { const state: ModuleState = {
examLabel: defaultExamLabel(module), examLabel: defaultExamLabel(module),
minTimer, minTimer,
difficulty: sample(["A1", "A2", "B1", "B2", "C1", "C2"] as Difficulty[])!, difficulty: [sample(["A1", "A2", "B1", "B2", "C1", "C2"] as Difficulty[])!],
isPrivate: true, isPrivate: true,
sectionLabels: sectionLabels(module), sectionLabels: sectionLabels(module),
expandedSections: [1], expandedSections: [(reset && (module === "writing" || module === "speaking")) ? 0 : 1],
focusedSection: 1, focusedSection: 1,
sections: [defaultSectionSettings(module, 1)], sections: [defaultSectionSettings(module, 1)],
importModule: true, importModule: true,

View File

@@ -59,7 +59,7 @@ export const rootReducer = (
return { return {
modules: { modules: {
...state.modules, ...state.modules,
[module]: defaultModuleSettings(module, timer), [module]: defaultModuleSettings(module, timer, true),
}, },
}; };
case 'FULL_RESET': case 'FULL_RESET':

View File

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