Exam generation rework, batch user tables, fastapi endpoint switch

This commit is contained in:
Carlos-Mesquita
2024-11-04 23:29:14 +00:00
parent a2bc997e8f
commit 15c9c4d4bd
148 changed files with 11348 additions and 3901 deletions

View File

@@ -0,0 +1,301 @@
import { FillBlanksExercise, ReadingPart } from "@/interfaces/exam";
import { useEffect, useReducer, useState } from "react";
import BlanksEditor from "..";
import { Card, CardContent } from "@/components/ui/card";
import { MdEdit, MdEditOff } from "react-icons/md";
import FillBlanksWord from "./FillBlanksWord";
import { FaPlus } from "react-icons/fa";
import useExamEditorStore from "@/stores/examEditor";
import { blanksReducer, BlankState, getTextSegments } from "../FillBlanksReducer";
import useSectionEdit from "@/components/ExamEditor/Hooks/useSectionEdit";
import { AlertItem } from "../../Shared/Alert";
import validateBlanks from "../validateBlanks";
import { toast } from "react-toastify";
import setEditingAlert from "../../Shared/setEditingAlert";
interface Word {
letter: string;
word: string;
}
const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: number }> = ({ exercise, sectionId }) => {
const { currentModule, dispatch } = useExamEditorStore();
const { state } = useExamEditorStore(
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
);
const section = state as ReadingPart;
const [alerts, setAlerts] = useState<AlertItem[]>([]);
const [local, setLocal] = useState(exercise);
const [selectedBlankId, setSelectedBlankId] = useState<string | null>(null);
const [answers, setAnswers] = useState<Map<string, string>>(
new Map(exercise.solutions.map(({ id, solution }) => [id, solution]))
);
const [isEditMode, setIsEditMode] = useState(false);
const [newWord, setNewWord] = useState('');
const [editing, setEditing] = useState(false);
const [blanksState, blanksDispatcher] = useReducer(blanksReducer, {
text: exercise.text || "",
blanks: [],
selectedBlankId: null,
draggedItemId: null,
textMode: false,
setEditing,
});
const { handleSave, handleDiscard, modeHandle } = useSectionEdit({
sectionId,
editing,
setEditing,
onSave: () => {
if (!validateBlanks(blanksState.blanks, answers, alerts, setAlerts)) {
toast.error("Please fix the errors before saving!");
return;
}
setEditing(false);
setAlerts([]);
const updatedExercise = {
...local,
text: blanksState.text,
solutions: Array.from(answers.entries()).map(([id, solution]) => ({
id,
solution
}))
};
const newState = { ...section };
newState.exercises = newState.exercises.map((ex) =>
ex.id === exercise.id ? updatedExercise : ex
);
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState } });
dispatch({ type: "REORDER_EXERCISES" });
},
onDiscard: () => {
setSelectedBlankId(null);
setAnswers(new Map(exercise.solutions.map(({ id, solution }) => [id, solution])));
setIsEditMode(false);
setNewWord('');
setLocal(exercise);
blanksDispatcher({ type: "RESET", payload: { text: exercise.text } });
blanksDispatcher({ type: "SET_TEXT", payload: exercise.text || "" });
const tokens = getTextSegments(exercise.text || "");
const initialBlanks = tokens.reduce((acc, token, idx) => {
if (token.type === 'blank') {
acc.push({ id: token.id, position: idx });
}
return acc;
}, [] as BlankState[]);
blanksDispatcher({ type: "SET_BLANKS", payload: initialBlanks });
},
onMode: () => {
const newSection = {
...section,
exercises: section.exercises.filter((ex) => ex.id !== local.id)
};
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
dispatch({ type: "REORDER_EXERCISES" });
}
});
useEffect(() => {
if (!editing) {
setLocal(exercise);
setAnswers(new Map(exercise.solutions.map(({ id, solution }) => [id, solution])));
}
}, [exercise, editing]);
const handleWordSelect = (word: string) => {
if (!selectedBlankId) return;
if (!editing) setEditing(true);
const newAnswers = new Map(answers);
newAnswers.set(selectedBlankId, word);
setAnswers(newAnswers);
setLocal(prev => ({
...prev,
solutions: Array.from(newAnswers.entries()).map(([id, solution]) => ({
id,
solution
}))
}));
};
const handleAddWord = () => {
const word = newWord.trim();
if (!word) return;
setLocal(prev => {
const nextLetter = String.fromCharCode(65 + prev.words.length);
return {
...prev,
words: [...prev.words, { letter: nextLetter, word }]
};
});
setNewWord('');
};
const handleRemoveWord = (index: number) => {
if (!editing) setEditing(true);
if (answers.size === 1) {
toast.error("There needs to be at least 1 word!");
return;
}
setLocal(prev => {
const newWords = prev.words.filter((_, i) => i !== index) as Word[];
const removedWord = prev.words[index] as Word;
const newAnswers = new Map(answers);
for (const [blankId, answer] of newAnswers.entries()) {
if (answer === removedWord.word) {
newAnswers.delete(blankId);
}
}
setAnswers(newAnswers);
return {
...prev,
words: newWords,
solutions: Array.from(newAnswers.entries()).map(([id, solution]) => ({
id,
solution
}))
};
});
};
const isWordUsed = (word: string): boolean => {
if (local.allowRepetition) return false;
return Array.from(answers.values()).includes(word);
};
const handleEditWord = (index: number, newWord: string) => {
if (!editing) setEditing(true);
setLocal(prev => {
const newWords = [...prev.words] as Word[];
const oldWord = newWords[index].word;
newWords[index] = { ...newWords[index], word: newWord };
const newAnswers = new Map(answers);
for (const [blankId, answer] of newAnswers.entries()) {
if (answer === oldWord) {
newAnswers.set(blankId, newWord);
}
}
setAnswers(newAnswers);
return {
...prev,
words: newWords,
solutions: Array.from(newAnswers.entries()).map(([id, solution]) => ({
id,
solution
}))
};
});
};
useEffect(() => {
validateBlanks(blanksState.blanks, answers, alerts, setAlerts);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers, blanksState.blanks, blanksState.textMode])
useEffect(()=> {
setEditingAlert(editing, setAlerts);
}, [editing])
return (
<div className="space-y-4">
<BlanksEditor
alerts={alerts}
editing={editing}
state={blanksState}
blanksDispatcher={blanksDispatcher}
description="Place blanks and assign words from the word bank"
initialText={local.text}
module={currentModule}
showBlankBank={true}
onBlankSelect={(blankId) => setSelectedBlankId(blankId?.toString() || null)}
onSave={handleSave}
onDiscard={handleDiscard}
onDelete={modeHandle}
setEditing={setEditing}
>
<>
{!blanksState.textMode && <Card className="p-4">
<CardContent>
<div className="flex justify-between items-center mb-4">
<div className="text-lg font-semibold">Word Bank</div>
<button
onClick={() => setIsEditMode(!isEditMode)}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
{isEditMode ?
<MdEditOff size={20} className="text-gray-500" /> :
<MdEdit size={20} className="text-gray-500" />
}
</button>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2">
{(local.words as Word[]).map((wordItem, index) => (
<FillBlanksWord
key={wordItem.letter}
letter={wordItem.letter}
word={wordItem.word}
isSelected={answers.get(selectedBlankId || '') === wordItem.word}
isUsed={isWordUsed(wordItem.word)}
onClick={() => handleWordSelect(wordItem.word)}
onRemove={isEditMode ? () => handleRemoveWord(index) : undefined}
onEdit={isEditMode ? (newWord) => handleEditWord(index, newWord) : undefined}
isEditMode={isEditMode}
/>
))}
</div>
{isEditMode && (
<div className="flex flex-row mt-8">
<input
type="text"
value={newWord}
onChange={(e) => setNewWord(e.target.value)}
placeholder="Enter new word"
className="flex-1 px-3 py-2 border border-r-0 rounded-l-md focus:outline-none"
name=""
/>
<button
onClick={handleAddWord}
disabled={!isEditMode || newWord === ""}
className="px-4 bg-blue-500 text-white rounded-r-md border border-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
>
<FaPlus className="h-4 w-4" />
</button>
</div>
)}
</CardContent>
</Card>
}
</>
</BlanksEditor>
</div>
);
};
export default FillBlanksLetters;