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,137 @@
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import SortableSection from "../../Shared/SortableSection";
import getReadingQuestions from '../SectionExercises/reading';
import { Exercise, LevelPart, ReadingPart, SpeakingExercise, WritingExercise } from "@/interfaces/exam";
import { ReadingExercise } from "./types";
import Dropdown from "@/components/Dropdown";
import useExamEditorStore from "@/stores/examEditor";
import Writing from "../../Exercises/Writing";
import Speaking from "../../Exercises/Speaking";
import { ReactNode, useEffect, useState } from "react";
import {
DndContext,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
closestCenter,
} from '@dnd-kit/core';
import GenLoader from "../../Exercises/Shared/GenLoader";
import { ExamPart } from "@/stores/examEditor/types";
import getListeningItems from "./listening";
import getLevelQuestionItems from "./level";
export interface Props {
sectionId: number;
}
const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
const { currentModule, dispatch } = useExamEditorStore();
const { sections, expandedSections } = useExamEditorStore(
(state) => state.modules[currentModule]
);
const { genResult, generating, state } = useExamEditorStore(
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
);
useEffect(() => {
if (genResult !== undefined && generating === "exercises") {
const newExercises = genResult[0].exercises;
const newState = state as ExamPart;
newState.exercises = [...newState.exercises, ...newExercises]
dispatch({
type: "UPDATE_SECTION_STATE", payload: {
sectionId, update: {
exercises: newExercises
}
}
})
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "genResult", value: undefined } })
}
}, [genResult, dispatch, sectionId, currentModule]);
const currentSection = sections.find((s) => s.sectionId === sectionId)!;
const sensors = useSensors(
useSensor(PointerSensor),
);
const questionItems = () => {
let ids, items;
switch (currentModule) {
case "reading":
items = getReadingQuestions((currentSection.state as ReadingPart).exercises as ReadingExercise[], sectionId);
ids = items.map(q => q.id.toString());
break;
case "listening":
items = getListeningItems((currentSection.state as ReadingPart).exercises as ReadingExercise[], sectionId);
ids = items.map(q => q?.id.toString());
break;
case "level":
items = getLevelQuestionItems((currentSection.state as LevelPart).exercises as Exercise[], sectionId);
ids = items.map(q => q.id.toString());
break;
}
return { ids, items }
}
const questions = questionItems();
const background = (component: ReactNode) => {
return (
<div className="p-8 shadow-inner border border-gray-200 bg-gray-50 rounded-xl">
{component}
</div>
);
}
if (currentModule == "writing") return background(<Writing sectionId={sectionId} exercise={currentSection.state as WritingExercise} />);
if (currentModule == "speaking") return background(<Speaking sectionId={sectionId} exercise={currentSection.state as SpeakingExercise} />);
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={(e) => dispatch({ type: "REORDER_EXERCISES", payload: { event: e, sectionId } })}
>
{(currentModule === "level" && questions.ids?.length === 0) ? (
background(<span className="flex justify-center">Generated exercises will appear here!</span>)
) : (
expandedSections.includes(sectionId) &&
questions.items &&
questions.items.length > 0 &&
questions.ids &&
questions.ids.length > 0 && (
<div className="mt-4 p-6 rounded-xl shadow-inner border bg-gray-50">
<SortableContext
items={questions.ids}
strategy={verticalListSortingStrategy}
>
{questions.items.map(item => (
<SortableSection key={item.id.toString()} id={item.id.toString()}>
<Dropdown
className={`w-full text-left p-4 mb-2 bg-gradient-to-r from-ielts-${currentModule}/60 to-ielts-${currentModule} text-white rounded-lg shadow-lg transition-transform transform hover:scale-102`}
customTitle={item.label}
contentWrapperClassName="rounded-xl"
>
<div className="p-4 shadow-inner border border-gray-200 bg-gray-50 rounded-xl">
{item.content}
</div>
</Dropdown>
</SortableSection>
))}
</SortableContext>
</div>
)
)}
{generating === "exercises" && <GenLoader module={currentModule} className="mt-4" />}
</DndContext >
);
}
export default SectionExercises;

View File

@@ -0,0 +1,61 @@
import { Exercise } from "@/interfaces/exam";
import ExerciseItem from "./types";
import ExerciseLabel from "../../Shared/ExerciseLabel";
import MultipleChoice from "../../Exercises/MultipleChoice";
import FillBlanksMC from "../../Exercises/Blanks/MultipleChoice";
const getLevelQuestionItems = (exercises: Exercise[], sectionId: number): ExerciseItem[] => {
const previewLabel = (text: string) => {
return text !== undefined ? text.replaceAll('\\n', ' ').split(' ').slice(0, 15).join(' ') : "";
}
const items: ExerciseItem[] = exercises.map((exercise, index) => {
let firstWordId, lastWordId;
switch (exercise.type) {
case "multipleChoice":
firstWordId = exercise.questions[0].id;
lastWordId = exercise.questions[exercise.questions.length - 1].id;
return {
id: index,
sectionId,
label: (
<ExerciseLabel
label={`Multiple Choice Questions #${firstWordId} - #${lastWordId}`}
preview={
<>
&quot;{previewLabel(exercise.prompt)}...&quot;
</>
}
/>
),
content: <MultipleChoice exercise={exercise} sectionId={sectionId} />
};
case "fillBlanks":
firstWordId = exercise.solutions[0].id;
lastWordId = exercise.solutions[exercise.solutions.length - 1].id;
return {
id: index,
sectionId,
label: (
<ExerciseLabel
label={`Fill Blanks Question #${firstWordId} - #${lastWordId}`}
preview={
<>
&quot;{previewLabel(exercise.prompt)}...&quot;
</>
}
/>
),
content: <FillBlanksMC exercise={exercise} sectionId={sectionId} />
};
default:
return {} as unknown as ExerciseItem;
}
}).filter((item) => item !== undefined);
return items || [];
};
export default getLevelQuestionItems;

View File

@@ -0,0 +1,106 @@
import ExerciseItem from './types';
import ExerciseLabel from '../../Shared/ExerciseLabel';
import FillBlanksLetters from '../../Exercises/Blanks/Letters';
import { Exercise, WriteBlanksExercise } from '@/interfaces/exam';
import MultipleChoice from '../../Exercises/MultipleChoice';
import WriteBlanksForm from '../../Exercises/WriteBlanksForm';
import WriteBlanksFill from '../../Exercises/Blanks/WriteBlankFill';
const writeBlanks = (exercise: WriteBlanksExercise, index: number, sectionId: number, previewLabel: (text: string) => string) => {
const firstWordId = exercise.solutions[0].id;
const lastWordId = exercise.solutions[exercise.solutions.length - 1].id;
switch (exercise.variant) {
case 'form':
return {
id: index,
sectionId,
label: (
<ExerciseLabel
label={`Write Blanks: Form #${firstWordId} - #${lastWordId}`}
preview={
<>
&quot;{previewLabel(exercise.prompt)}...&quot;
</>
}
/>
),
content: <WriteBlanksForm exercise={exercise} sectionId={sectionId} />
};
case 'fill':
return {
id: index,
sectionId,
label: (
<ExerciseLabel
label={`Write Blanks: Fill #${firstWordId} - #${lastWordId}`}
preview={
<>
&quot;{previewLabel(exercise.prompt)}...&quot;
</>
}
/>
),
content: <WriteBlanksFill exercise={exercise} sectionId={sectionId} />
};
}
}
const getListeningItems = (exercises: Exercise[], sectionId: number) => {
const previewLabel = (text: string) => {
return text !== undefined ? text.replaceAll('\\n', ' ').split(' ').slice(0, 15).join(' ') : "";
}
const items = exercises.map((exercise, index) => {
let firstWordId, lastWordId;
switch (exercise.type) {
case "fillBlanks":
firstWordId = exercise.solutions[0].id;
lastWordId = exercise.solutions[exercise.solutions.length - 1].id;
return {
id: index,
sectionId,
label: (
<ExerciseLabel
label={`Fill Blanks Question #${firstWordId} - #${lastWordId}`}
preview={
<>
&quot;{previewLabel(exercise.prompt)}...&quot;
</>
}
/>
),
content: <FillBlanksLetters exercise={exercise} sectionId={sectionId} />
};
case "writeBlanks":
return writeBlanks(exercise, index, sectionId, previewLabel);
case "multipleChoice":
firstWordId = exercise.questions[0].id;
lastWordId = exercise.questions[exercise.questions.length - 1].id;
return {
id: index,
sectionId,
label: (
<ExerciseLabel
label={`Multiple Choice Questions #${firstWordId} - #${lastWordId}`}
preview={
<>
&quot;{previewLabel(exercise.prompt)}...&quot;
</>
}
/>
),
content: <MultipleChoice exercise={exercise} sectionId={sectionId} />
};
}
}).filter((item) => item !== undefined);
return items || [];
};
export default getListeningItems;

View File

@@ -0,0 +1,98 @@
import ExerciseItem, { ReadingExercise } from './types';
import WriteBlanks from "@/editor/Exercises/WriteBlanks";
import ExerciseLabel from '../../Shared/ExerciseLabel';
import MatchSentences from '../../Exercises/MatchSentences';
import TrueFalse from '../../Exercises/TrueFalse';
import FillBlanksLetters from '../../Exercises/Blanks/Letters';
const getExerciseItems = (exercises: ReadingExercise[], sectionId: number): ExerciseItem[] => {
const previewLabel = (text: string) => {
return text !== undefined ? text.replaceAll('\\n', ' ').split(' ').slice(0, 15).join(' ') : ""
}
const items: ExerciseItem[] = exercises.map((exercise, index) => {
let firstWordId, lastWordId;
switch (exercise.type) {
case "fillBlanks":
firstWordId = exercise.solutions[0].id;
lastWordId = exercise.solutions[exercise.solutions.length - 1].id;
return {
id: index,
sectionId,
label: (
<ExerciseLabel
label={`Fill Blanks Question #${firstWordId} - #${lastWordId}`}
preview={
<>
&quot;{previewLabel(exercise.prompt)}...&quot;
</>
}
/>
),
content: <FillBlanksLetters exercise={exercise} sectionId={sectionId} />
};
case "writeBlanks":
firstWordId = exercise.solutions[0].id;
lastWordId = exercise.solutions[exercise.solutions.length - 1].id;
return {
id: index,
sectionId,
label: (
<ExerciseLabel
label={`Write Blanks Question #${firstWordId} - #${lastWordId}`}
preview={
<>
&quot;{previewLabel(exercise.prompt)}...&quot;
</>
}
/>
),
content: <WriteBlanks exercise={exercise} sectionId={sectionId} />
};
case "matchSentences":
firstWordId = exercise.sentences[0].id;
lastWordId = exercise.sentences[exercise.sentences.length - 1].id;
return {
id: index,
sectionId,
label: (
<ExerciseLabel
label={`${exercise.variant == "ideaMatch" ? "Idea Match" : "Paragraph Match"} ${firstWordId == lastWordId ? `#${firstWordId}` : `#${firstWordId} - #${lastWordId}`}`}
preview={
<>
&quot;{previewLabel(exercise.prompt)}...&quot;
</>
}
/>
),
content: <MatchSentences exercise={exercise} sectionId={sectionId}/>
};
case "trueFalse":
firstWordId = exercise.questions[0].id
lastWordId = exercise.questions[exercise.questions.length - 1].id;
return {
id: index,
sectionId,
label: (
<ExerciseLabel
label={`True/False/Not Given #${firstWordId} - #${lastWordId}`}
preview={
<>
&quot;{previewLabel(exercise.prompt)}...&quot;
</>
}
/>
),
content: <TrueFalse exercise={exercise} sectionId={sectionId} />
};
}
}).filter((item) => item !== undefined);
return items || [];
};
export default getExerciseItems;

View File

@@ -0,0 +1,10 @@
import { FillBlanksExercise, MatchSentencesExercise, TrueFalseExercise, WriteBlanksExercise } from "@/interfaces/exam";
export default interface ExerciseItem {
id: number;
sectionId: number;
label: React.ReactNode;
content: React.ReactNode;
}
export type ReadingExercise = FillBlanksExercise | TrueFalseExercise | MatchSentencesExercise | WriteBlanksExercise;