307 lines
12 KiB
TypeScript
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; |