Files
encoach_frontend/src/components/ExamEditor/Exercises/MatchSentences/index.tsx
Carlos-Mesquita ccbbf30058 ENCOA-311
2025-01-13 01:18:19 +00:00

261 lines
9.7 KiB
TypeScript

import React, { useState, useMemo, useEffect, useCallback } from 'react';
import {
MdAdd,
MdVisibility,
MdVisibilityOff
} from 'react-icons/md';
import { Difficulty, MatchSentencesExercise, ReadingPart } from '@/interfaces/exam';
import Alert, { AlertItem } from '../Shared/Alert';
import ReferenceViewer from './ParagraphViewer';
import Header from '../../Shared/Header';
import SortableQuestion from '../Shared/SortableQuestion';
import QuestionsList from '../Shared/QuestionsList';
import useExamEditorStore from '@/stores/examEditor';
import useSectionEdit from '../../Hooks/useSectionEdit';
import validateMatchSentences from './validation';
import setEditingAlert from '../Shared/setEditingAlert';
import { toast } from 'react-toastify';
import { DragEndEvent } from '@dnd-kit/core';
import { handleMatchSentencesReorder } from '@/stores/examEditor/reorder/local';
import PromptEdit from '../Shared/PromptEdit';
const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: number }> = ({ exercise, sectionId }) => {
const { currentModule, dispatch } = useExamEditorStore();
const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty);
const { state } = useExamEditorStore(
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
);
const section = state as ReadingPart;
const [local, setLocal] = useState(exercise);
const [selectedParagraph, setSelectedParagraph] = useState<string | null>(null);
const [showReference, setShowReference] = useState(false);
const [alerts, setAlerts] = useState<AlertItem[]>([]);
const updateLocal = (exercise: MatchSentencesExercise) => {
setLocal(exercise);
setEditing(true);
};
const { editing, setEditing, handleSave, handleDiscard, handleDelete, handlePractice } = useSectionEdit({
sectionId,
onSave: () => {
const isValid = validateMatchSentences(local.sentences, setAlerts);
if (!isValid) {
toast.error("Please fix the errors before saving!");
return;
}
setEditing(false);
setAlerts([]);
const newState = { ...section };
newState.exercises = newState.exercises.map((ex) => ex.id === exercise.id ? local : ex);
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
},
onDiscard: () => {
setLocal(exercise);
setSelectedParagraph(null);
setShowReference(false);
},
onDelete: () => {
const newSection = {
...section,
exercises: section.exercises.filter((ex) => ex.id !== local.id)
};
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection, module: currentModule } });
},
onPractice: () => {
const updatedExercise = {
...local,
isPractice: !local.isPractice
};
const newState = { ...section };
newState.exercises = newState.exercises.map((ex) =>
ex.id === exercise.id ? updatedExercise : ex
);
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
}
});
const usedOptions = useMemo(() => {
return local.sentences.reduce((acc, sentence) => {
if (sentence.solution) {
acc.add(sentence.solution);
}
return acc;
}, new Set<string>());
}, [local.sentences]);
const addHeading = () => {
const newId = (parseInt(local.sentences[local.sentences.length - 1].id) + 1).toString();
updateLocal({
...local,
sentences: [
...local.sentences,
{
id: newId,
sentence: "",
solution: ""
}
]
});
};
const updateHeading = (index: number, field: string, value: string) => {
const newSentences = [...local.sentences];
if (field === 'solution') {
const oldSolution = newSentences[index].solution;
if (oldSolution) {
usedOptions.delete(oldSolution);
}
}
newSentences[index] = { ...newSentences[index], [field]: value };
updateLocal({ ...local, sentences: newSentences });
};
const deleteHeading = (index: number) => {
if (local.sentences.length <= 1) {
toast.error(`There needs to be at least one ${exercise.variant && exercise.variant == "ideaMatch" ? "idea/opinion" : "heading"}!`);
return;
}
const deletedSolution = local.sentences[index].solution;
if (deletedSolution) {
usedOptions.delete(deletedSolution);
}
const newSentences = local.sentences.filter((_, i) => i !== index);
updateLocal({ ...local, sentences: newSentences });
};
useEffect(() => {
validateMatchSentences(local.sentences, setAlerts);
}, [local.sentences]);
useEffect(() => {
setEditingAlert(editing, setAlerts);
}, [editing]);
const handleDragEnd = (event: DragEndEvent) => {
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 (
<div className="flex flex-col mx-auto p-2">
<Header
title={exercise.variant && exercise.variant == "ideaMatch" ? "Idea Match" : "Paragraph Match"}
description={`Edit ${exercise.variant && exercise.variant == "ideaMatch" ? "ideas/opinions" : "headings"} and their matches`}
editing={editing}
difficulty={exercise.difficulty}
saveDifficulty={saveDifficulty}
handleSave={handleSave}
handleDelete={handleDelete}
handleDiscard={handleDiscard}
handlePractice={handlePractice}
isEvaluationEnabled={!local.isPractice}
>
<button
onClick={() => setShowReference(!showReference)}
className="px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors flex items-center gap-2"
>
{showReference ? <MdVisibilityOff size={18} /> : <MdVisibility size={18} />}
{showReference ? 'Hide Reference' : 'Show Reference'}
</button>
</Header>
<div className="space-y-4">
{alerts.length > 0 && <Alert alerts={alerts} />}
<PromptEdit
value={local.prompt}
onChange={(text) => updateLocal({ ...local, prompt: text })}
/>
<QuestionsList
ids={local.sentences.map(s => s.id)}
handleDragEnd={handleDragEnd}
>
{local.sentences.map((sentence, index) => (
<SortableQuestion
key={sentence.id}
id={sentence.id}
index={index}
deleteQuestion={() => deleteHeading(index)}
onFocus={() => setSelectedParagraph(sentence.solution)}
>
<>
<input
type="text"
value={sentence.sentence}
onChange={(e) => updateHeading(index, 'sentence', e.target.value)}
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none text-mti-gray-dim"
placeholder={`Enter ${exercise.variant && exercise.variant == "ideaMatch" ? "idea/opinion" : "heading"} ...`}
/>
<div className="flex items-center gap-3">
<select
value={sentence.solution}
onChange={(e) => {
updateHeading(index, 'solution', e.target.value);
setSelectedParagraph(e.target.value);
}}
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none bg-white text-mti-gray-dim"
>
<option value="">Select matching {exercise.variant == "ideaMatch" ? "author" : "paragraph"}...</option>
{local.options.map((option) => {
const isUsed = usedOptions.has(option.id);
const isCurrentSelection = sentence.solution === option.id;
return (
<option
key={option.id}
value={option.id}
disabled={isUsed && !isCurrentSelection}
>
{exercise.variant == "ideaMatch" ? "Author" : "Paragraph"} {option.id}
</option>
);
})}
</select>
</div>
</>
</SortableQuestion>
))}
</QuestionsList>
{(section.text !== undefined && section.text.content.split("\n\n").length - 1) === local.sentences.length && (
<button
onClick={addHeading}
className="w-full p-4 border-2 border-dashed border-gray-300 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-colors flex items-center justify-center gap-2 text-gray-600 hover:text-blue-600"
>
<MdAdd size={18} />
Add New Match
</button>
)}
</div>
<ReferenceViewer
headings={exercise.variant !== "ideaMatch"}
showReference={showReference}
selectedReference={selectedParagraph}
options={local.options}
setShowReference={setShowReference}
/>
</div>
);
};
export default MatchSentences;