262 lines
11 KiB
TypeScript
262 lines
11 KiB
TypeScript
import EXERCISES from "./exercises";
|
|
import clsx from "clsx";
|
|
import { ExerciseGen, GeneratedExercises, GeneratorState } from "./generatedExercises";
|
|
import Modal from "@/components/Modal";
|
|
import { useCallback, useState } from "react";
|
|
import ExerciseWizard, { ExerciseConfig } from "./ExerciseWizard";
|
|
import { generate } from "../SettingsEditor/Shared/Generate";
|
|
import { Module } from "@/interfaces";
|
|
import useExamEditorStore from "@/stores/examEditor";
|
|
import { LevelPart, ListeningPart, Message, ReadingPart } from "@/interfaces/exam";
|
|
import { BsArrowRepeat } from "react-icons/bs";
|
|
|
|
interface ExercisePickerProps {
|
|
module: string;
|
|
sectionId: number;
|
|
difficulty: string;
|
|
extraArgs?: Record<string, any>;
|
|
levelSectionId?: number;
|
|
level?: boolean;
|
|
}
|
|
|
|
const ExercisePicker: React.FC<ExercisePickerProps> = ({
|
|
module,
|
|
sectionId,
|
|
extraArgs = undefined,
|
|
levelSectionId,
|
|
level = false
|
|
}) => {
|
|
const { currentModule, dispatch } = useExamEditorStore();
|
|
const { difficulty, sections } = useExamEditorStore((store) => store.modules[level ? "level" : currentModule]);
|
|
const section = sections.find((s) => s.sectionId === (level ? levelSectionId : sectionId));
|
|
|
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
const [localSelectedExercises, setLocalSelectedExercises] = useState<string[]>([]);
|
|
|
|
const state = section?.state;
|
|
|
|
const getFullExerciseType = (exercise: ExerciseGen): string => {
|
|
if (exercise.extra && exercise.extra.length > 0) {
|
|
const extraValue = exercise.extra.find(e => e.param === 'name')?.value;
|
|
return extraValue ? `${exercise.type}/?name=${extraValue}` : exercise.type;
|
|
}
|
|
return exercise.type;
|
|
};
|
|
|
|
const handleChange = (exercise: ExerciseGen) => {
|
|
const fullType = getFullExerciseType(exercise);
|
|
|
|
setLocalSelectedExercises(prev => {
|
|
const newSelected = prev.includes(fullType)
|
|
? prev.filter(type => type !== fullType)
|
|
: [...prev, fullType];
|
|
return newSelected;
|
|
});
|
|
};
|
|
|
|
const moduleExercises = (sectionId && !["level", "writing", "speaking"].includes(module) ? EXERCISES.filter((ex) => ex.module === module && ex.sectionId == sectionId) : EXERCISES.filter((ex) => ex.module === module));
|
|
|
|
const onModuleSpecific = useCallback((configurations: ExerciseConfig[]) => {
|
|
const exercises = configurations.map(config => {
|
|
const exerciseType = config.type.split('name=')[1];
|
|
return {
|
|
type: exerciseType,
|
|
quantity: Number(config.params.quantity || 1),
|
|
...(config.params.num_random_words !== undefined && {
|
|
num_random_words: Number(config.params.num_random_words)
|
|
}),
|
|
...(config.params.max_words !== undefined && {
|
|
max_words: Number(config.params.max_words)
|
|
})
|
|
};
|
|
});
|
|
|
|
let context = {};
|
|
if (module === 'reading') {
|
|
const readingState = state as ReadingPart | LevelPart;
|
|
context = {
|
|
text: readingState.text!.content
|
|
};
|
|
} else if (module === 'listening') {
|
|
const listeningState = state as ListeningPart | LevelPart;
|
|
const script = listeningState.script;
|
|
if (sectionId === 1 || sectionId === 3) {
|
|
const dialog = script as Message[];
|
|
context = {
|
|
text: dialog.map((d) => `${d.name}: ${d.text}`).join("\n")
|
|
};
|
|
} else if (sectionId === 2 || sectionId === 4) {
|
|
context = {
|
|
text: script as string
|
|
};
|
|
}
|
|
}
|
|
if (!["speaking", "writing"].includes(module)) {
|
|
generate(
|
|
sectionId,
|
|
module as Module,
|
|
level ? `exercises-${module}` : "exercises",
|
|
{
|
|
method: 'POST',
|
|
body: {
|
|
...context,
|
|
exercises: exercises,
|
|
difficulty: difficulty
|
|
}
|
|
},
|
|
(data: any) => [{
|
|
exercises: data.exercises
|
|
}],
|
|
levelSectionId,
|
|
level
|
|
);
|
|
} else if (module === "writing") {
|
|
configurations.forEach((config) => {
|
|
let queryParams = config.params.topic !== '' ? { topic: config.params.topic as string } : undefined;
|
|
generate(
|
|
config.type === 'writing_letter' ? 1 : 2,
|
|
"writing",
|
|
config.type,
|
|
{
|
|
method: 'GET',
|
|
queryParams
|
|
},
|
|
(data: any) => [{
|
|
prompt: data.question
|
|
}],
|
|
levelSectionId,
|
|
level
|
|
);
|
|
});
|
|
} else {
|
|
configurations.forEach((config) => {
|
|
let queryParams = Object.fromEntries(
|
|
Object.entries({
|
|
topic: config.params.topic as string,
|
|
first_topic: config.params.first_topic as string,
|
|
second_topic: config.params.second_topic as string,
|
|
}).filter(([_, value]) => value && value !== '')
|
|
);
|
|
let query = Object.keys(queryParams).length === 0 ? undefined : queryParams;
|
|
generate(
|
|
Number(config.type.split('_')[1]),
|
|
"speaking",
|
|
config.type,
|
|
{
|
|
method: 'GET',
|
|
queryParams: query
|
|
},
|
|
(data: any) => {
|
|
switch (Number(config.type.split('_')[1])) {
|
|
case 1:
|
|
return [{
|
|
prompts: data.questions,
|
|
first_topic: data.first_topic,
|
|
second_topic: data.second_topic
|
|
}];
|
|
case 2:
|
|
return [{
|
|
topic: data.topic,
|
|
question: data.question,
|
|
prompts: data.prompts,
|
|
suffix: data.suffix
|
|
}];
|
|
case 3:
|
|
return [{
|
|
topic: data.topic,
|
|
questions: data.questions
|
|
}];
|
|
default:
|
|
return [data];
|
|
}
|
|
},
|
|
levelSectionId,
|
|
level
|
|
);
|
|
});
|
|
}
|
|
setLocalSelectedExercises([]);
|
|
setPickerOpen(false);
|
|
}, [
|
|
sectionId,
|
|
levelSectionId,
|
|
level,
|
|
module,
|
|
state,
|
|
difficulty,
|
|
setPickerOpen
|
|
]);
|
|
|
|
if (section === undefined) return <></>;
|
|
|
|
return (
|
|
<>
|
|
<Modal isOpen={pickerOpen} onClose={() => setPickerOpen(false)} title="Exercise Wizard"
|
|
titleClassName={clsx(
|
|
"text-2xl font-semibold text-center py-4",
|
|
`bg-ielts-${module} text-white`,
|
|
"shadow-sm",
|
|
"-mx-6 -mt-6",
|
|
"mb-6"
|
|
)}
|
|
>
|
|
<ExerciseWizard
|
|
module={module as Module}
|
|
selectedExercises={localSelectedExercises}
|
|
sectionId={sectionId}
|
|
exercises={moduleExercises}
|
|
onSubmit={onModuleSpecific}
|
|
onDiscard={() => setPickerOpen(false)}
|
|
extraArgs={extraArgs}
|
|
/>
|
|
</Modal>
|
|
<div className="flex flex-col gap-4 px-4" key={sectionId}>
|
|
<div className="space-y-2">
|
|
{moduleExercises.map((exercise) => {
|
|
const fullType = getFullExerciseType(exercise);
|
|
return (
|
|
<label
|
|
key={fullType}
|
|
className={`flex items-center space-x-3 text-white font-semibold cursor-pointer p-2 hover:bg-ielts-${exercise.module}/70 rounded bg-ielts-${exercise.module}/90`}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
name="exercise"
|
|
value={fullType}
|
|
checked={localSelectedExercises.includes(fullType)}
|
|
onChange={() => handleChange(exercise)}
|
|
className="h-5 w-5"
|
|
/>
|
|
<div className="flex items-center space-x-2">
|
|
<exercise.icon className="h-5 w-5 text-white" />
|
|
<span>{exercise.label}</span>
|
|
</div>
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className="flex flex-row justify-center">
|
|
<button
|
|
className={
|
|
clsx("flex items-center justify-center px-4 py-2 text-white rounded-xl transition-colors duration-300 disabled:cursor-not-allowed",
|
|
`bg-ielts-${module}/70 border border-ielts-${module} hover:bg-ielts-${module} disabled:bg-ielts-${module}/40 `,
|
|
)
|
|
}
|
|
onClick={() => setPickerOpen(true)}
|
|
disabled={localSelectedExercises.length === 0}
|
|
>
|
|
{section.generating === "exercises" ? (
|
|
<div key={`section-${sectionId}`} className="flex items-center justify-center">
|
|
<BsArrowRepeat className="text-white animate-spin" size={25} />
|
|
</div>
|
|
) : (
|
|
<>{["speaking", "writing"].includes(module) ? "Add Exercises" : "Set Up Exercises"} ({localSelectedExercises.length}) </>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default ExercisePicker; |