191 lines
7.7 KiB
TypeScript
191 lines
7.7 KiB
TypeScript
import EXERCISES from "./exercises";
|
|
import clsx from "clsx";
|
|
import { ExerciseGen, GeneratedExercises, GeneratorState } from "./generatedExercises";
|
|
import Modal from "@/components/Modal";
|
|
import { useState } from "react";
|
|
import ExerciseWizard, { ExerciseConfig } from "./ExerciseWizard";
|
|
import { generate } from "../../SettingsEditor/Shared/Generate";
|
|
import { Module } from "@/interfaces";
|
|
import useExamEditorStore from "@/stores/examEditor";
|
|
import { ListeningPart, Message, ReadingPart } from "@/interfaces/exam";
|
|
import { BsArrowRepeat } from "react-icons/bs";
|
|
|
|
interface ExercisePickerProps {
|
|
module: string;
|
|
sectionId: number;
|
|
difficulty: string;
|
|
extraArgs?: Record<string, any>;
|
|
}
|
|
|
|
const ExercisePicker: React.FC<ExercisePickerProps> = ({
|
|
module,
|
|
sectionId,
|
|
extraArgs = undefined,
|
|
}) => {
|
|
const { currentModule, dispatch } = useExamEditorStore();
|
|
const { difficulty } = useExamEditorStore((store) => store.modules[currentModule]);
|
|
const section = useExamEditorStore((store) => store.modules[currentModule].sections.find((s) => s.sectionId == sectionId));
|
|
|
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
|
|
if (section === undefined) return <></>;
|
|
|
|
const { state, selectedExercises } = section;
|
|
|
|
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);
|
|
|
|
const newSelected = selectedExercises.includes(fullType)
|
|
? selectedExercises.filter(type => type !== fullType)
|
|
: [...selectedExercises, fullType];
|
|
|
|
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { module: currentModule, sectionId, field: "selectedExercises", value: newSelected } })
|
|
};
|
|
|
|
const moduleExercises = (sectionId && module !== "level" ? EXERCISES.filter((ex) => ex.module === module && ex.sectionId == sectionId) : EXERCISES.filter((ex) => ex.module === module));
|
|
|
|
const onModuleSpecific = (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, moduleState;
|
|
|
|
switch (module) {
|
|
case 'reading':
|
|
moduleState = state as ReadingPart;
|
|
context = {
|
|
text: moduleState.text.content
|
|
}
|
|
break;
|
|
case 'listening':
|
|
moduleState = state as ListeningPart;
|
|
let script = moduleState.script;
|
|
let text, dialog;
|
|
switch (sectionId) {
|
|
case 1:
|
|
case 3:
|
|
dialog = script as Message[];
|
|
text = dialog.map((d) => `${d.name}: ${d.text}`).join("\n");
|
|
context = { text: text }
|
|
break;
|
|
case 2:
|
|
case 4:
|
|
text = script as string;
|
|
context = { text: text }
|
|
break;
|
|
}
|
|
break;
|
|
default:
|
|
context = {}
|
|
}
|
|
generate(
|
|
sectionId,
|
|
module as Module,
|
|
"exercises",
|
|
{
|
|
method: 'POST',
|
|
body: {
|
|
...context,
|
|
exercises: exercises,
|
|
difficulty: difficulty
|
|
}
|
|
},
|
|
(data: any) => [{
|
|
exercises: data.exercises
|
|
}]
|
|
);
|
|
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "selectedExercises", value: [] } })
|
|
setPickerOpen(false);
|
|
};
|
|
|
|
|
|
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
|
|
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={selectedExercises.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={selectedExercises.length == 0}
|
|
>
|
|
{section.generating === "exercises" ? (
|
|
<div key={`section-${sectionId}`} className="flex items-center justify-center">
|
|
<BsArrowRepeat className="text-white animate-spin" size={25} />
|
|
</div>
|
|
) : (
|
|
<>Set Up Exercises ({selectedExercises.length}) </>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default ExercisePicker;
|