Files
encoach_frontend/src/components/ExamEditor/ExercisePicker/ExerciseWizard.tsx
Carlos-Mesquita 6f9be29cd8 ENCOA-312
2025-01-13 21:02:34 +00:00

307 lines
12 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { Tooltip } from 'react-tooltip';
import { ExerciseGen } from './generatedExercises';
import Image from 'next/image';
import clsx from 'clsx';
import { GiBrain } from 'react-icons/gi';
import { IoTextOutline } from 'react-icons/io5';
import { Switch } from '@headlessui/react';
import useExamEditorStore from '@/stores/examEditor';
import { Module } from '@/interfaces';
import { capitalize } from 'lodash';
import Select from '@/components/Low/Select';
import { Difficulty } from '@/interfaces/exam';
interface Props {
module: Module;
sectionId: number;
exercises: ExerciseGen[];
extraArgs?: Record<string, any>;
onSubmit: (configurations: ExerciseConfig[]) => void;
onDiscard: () => void;
selectedExercises: string[];
}
export interface ExerciseConfig {
type: string;
params: {
[key: string]: string | number | boolean;
};
}
const ExerciseWizard: React.FC<Props> = ({
module,
exercises,
extraArgs,
sectionId,
selectedExercises,
onSubmit,
onDiscard,
}) => {
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(() => {
const initialConfigs = selectedExercises.map(exerciseType => {
const exercise = exercises.find(ex => {
const fullType = ex.extra?.find(e => e.param === 'name')?.value
? `${ex.type}/?name=${ex.extra.find(e => e.param === 'name')?.value}`
: ex.type;
return fullType === exerciseType;
});
const params: { [key: string]: string | number | boolean } = {};
exercise?.extra?.forEach(param => {
if (param.param !== 'name') {
if (exerciseType.includes('paragraphMatch') && param.param === 'quantity') {
params[param.param] = extraArgs?.text.split("\n\n").length || 1;
} else {
params[param.param || ''] = param.value ?? '';
}
}
});
return {
type: exerciseType,
params
};
});
setConfigurations(initialConfigs);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedExercises, exercises]);
const handleParameterChange = (
exerciseIndex: number,
paramName: string,
value: string | number | boolean
) => {
setConfigurations(prev => {
const newConfigs = [...prev];
newConfigs[exerciseIndex] = {
...newConfigs[exerciseIndex],
params: {
...newConfigs[exerciseIndex].params,
[paramName]: value
}
};
return newConfigs;
});
};
const renderParameterInput = (
param: NonNullable<ExerciseGen['extra']>[0],
exerciseIndex: number,
config: ExerciseConfig
) => {
if (typeof param.value === 'boolean') {
const currentValue = Boolean(config.params[param.param || '']);
return (
<div className="flex flex-row items-center ml-auto">
<GiBrain
className="mx-4"
size={28}
color={currentValue ? `#F3F4F6` : `#1F2937`}
/>
<Switch
checked={currentValue}
onChange={(value) => handleParameterChange(
exerciseIndex,
param.param || '',
value
)}
className={clsx(
"relative inline-flex h-[30px] w-[58px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white/75",
currentValue ? `bg-[#F3F4F6]` : `bg-[#1F2937]`
)}
>
<span
aria-hidden="true"
className={clsx(
"pointer-events-none inline-block h-[26px] w-[26px] transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out",
currentValue ? 'translate-x-7' : 'translate-x-0'
)}
/>
</Switch>
<IoTextOutline
className="mx-4"
size={28}
color={!currentValue ? `#F3F4F6` : `#1F2937`}
/>
<Tooltip id={`${exerciseIndex}`} className="z-50 bg-white shadow-md rounded-sm" />
<a data-tooltip-id={`${exerciseIndex}`} data-tooltip-html="Generate or use placeholder?" className='ml-1 flex items-center justify-center'>
<Image src="/mat-icon-info.svg" width={24} height={24} alt={"AI Generated?"} />
</a>
</div>
);
}
if ('type' in param && param.type === 'text') {
return (
<div className="space-y-2">
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-white">
{param.label}
</label>
{param.tooltip && (
<>
<Tooltip id={config.type} className="z-50 bg-white shadow-md rounded-sm" />
<a data-tooltip-id={config.type} data-tooltip-html={param.tooltip} className='ml-1 flex items-center justify-center'>
<Image src="/mat-icon-info.svg" width={24} height={24} alt={param.tooltip} />
</a>
</>
)}
</div>
<input
type="text"
value={config.params[param.param || ''] as string}
onChange={(e) => handleParameterChange(
exerciseIndex,
param.param || '',
e.target.value
)}
className="px-3 py-2 shadow-lg rounded-md text-mti-gray-dim w-full"
placeholder="Enter here..."
/>
</div>
);
}
const inputValue = Number(config.params[param.param || '1'].toString()) || config.params[param.param!];
const isParagraphMatch = config.type.split("?name=")[1] === "paragraphMatch";
const maxParagraphs = isParagraphMatch ? extraArgs!.text.split("\n\n").length : 50;
return (
<div className="space-y-2">
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-white">
{`${param.label}${isParagraphMatch ? ` (out of ${extraArgs!.text.split("\n\n").length} paragraphs)` : ""}`}
</label>
{param.tooltip && (
<>
<Tooltip id={config.type} className="z-50 bg-white shadow-md rounded-sm" />
<a data-tooltip-id={config.type} data-tooltip-html={param.tooltip} className='ml-1 flex items-center justify-center'>
<Image src="/mat-icon-info.svg" width={24} height={24} alt={param.tooltip} />
</a>
</>
)}
</div>
{param.param === "difficulty" ?
<Select
options={DIFFICULTIES.map((x) => ({ value: x, label: x }))}
onChange={(value) => {
handleParameterChange(
exerciseIndex,
param.param || '',
value?.value || ''
);
}}
value={{ value: config.params[param.param] !== "" ? config.params[param.param] as string : randomDiff , label: config.params[param.param] !== "" ? config.params[param.param] as string : randomDiff }}
flat
/>
:
<input
type="number"
value={inputValue as number}
onChange={(e) => handleParameterChange(
exerciseIndex,
param.param || '',
e.target.value ? Number(e.target.value) : ''
)}
className="px-3 py-2 shadow-lg rounded-md text-mti-gray-dim w-full"
min={1}
max={maxParagraphs}
/>
}
</div>
);
};
const renderExerciseHeader = (
exercise: ExerciseGen,
exerciseIndex: number,
config: ExerciseConfig,
extraParams: boolean,
) => {
const generateParam = exercise.extra?.find(param => param.param === 'generate');
return (
<div className={clsx("flex items-center w-full", extraParams ? "mb-4" : "py-4")}>
<div className="flex items-center gap-2">
<exercise.icon className="h-5 w-5" />
<h3 className="font-medium text-lg">{exercise.label}</h3>
</div>
{/* when placeholders are done uncomment this*/}
{/*generateParam && renderParameterInput(generateParam, exerciseIndex, config)*/}
</div>
);
};
return (
<div className="space-y-6 px-4 py-6">
{configurations.map((config, exerciseIndex) => {
const exercise = exercises.find(ex => {
const fullType = ex.extra?.find(e => e.param === 'name')?.value
? `${ex.type}/?name=${ex.extra.find(e => e.param === 'name')?.value}`
: ex.type;
return fullType === config.type;
});
if (!exercise) return null;
const nonGenerateParams = exercise.extra?.filter(
param => param.param !== 'name' && param.param !== 'generate'
);
return (
<div
key={config.type}
className={`bg-ielts-${module}/70 text-white rounded-lg p-4 shadow-xl`}
>
{renderExerciseHeader(exercise, exerciseIndex, config, (exercise.extra || []).length > 2)}
{nonGenerateParams && nonGenerateParams.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{nonGenerateParams.map(param => (
<div key={param.param}>
{renderParameterInput(param, exerciseIndex, config)}
</div>
))}
</div>
)}
</div>
);
})}
<div className="flex justify-between">
<button
onClick={onDiscard}
className={`px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-400 transition-colors`}
>
Cancel
</button>
<button
onClick={() => onSubmit(configurations)}
className={`px-4 py-2 bg-ielts-${module} text-white rounded-md hover:bg-ielts-${module}/80 transition-colors`}
>
Add Exercises
</button>
</div>
</div>
);
};
export default ExerciseWizard;