Exam generation rework, batch user tables, fastapi endpoint switch
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import ExamEditorStore from "@/stores/examEditor/types";
|
||||
import Header from "../../Shared/Header";
|
||||
import { Module } from "@/interfaces";
|
||||
|
||||
interface Props {
|
||||
sectionId: number;
|
||||
title: string;
|
||||
description: string;
|
||||
editing: boolean;
|
||||
renderContent: (editing: boolean) => React.ReactNode;
|
||||
mode?: "edit" | "delete";
|
||||
onSave: () => void;
|
||||
onDiscard: () => void;
|
||||
onEdit?: () => void;
|
||||
module?: Module;
|
||||
}
|
||||
|
||||
const SectionContext: React.FC<Props> = ({ sectionId, title, description, renderContent, editing, onSave, onDiscard, onEdit, mode = "edit", module}) => {
|
||||
const { currentModule, dispatch } = useExamEditorStore();
|
||||
const { generating } = useExamEditorStore(
|
||||
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
|
||||
);
|
||||
|
||||
const [loading, setLoading] = useState(generating && generating == "context");
|
||||
|
||||
const updateRoot = useCallback((updates: Partial<ExamEditorStore>) => {
|
||||
dispatch({ type: 'UPDATE_ROOT', payload: { updates } });
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const loading = generating && generating == "context";
|
||||
setLoading(loading);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [generating, updateRoot]);
|
||||
|
||||
return (
|
||||
<div className="p-8 shadow-inner border border-gray-200 bg-gray-50 rounded-xl">
|
||||
<div className='relative pb-4'>
|
||||
<Header
|
||||
title={title}
|
||||
description={description}
|
||||
editing={editing}
|
||||
handleSave={onSave}
|
||||
handleDiscard={onDiscard}
|
||||
modeHandle={onEdit}
|
||||
mode={mode}
|
||||
module={module}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
{loading ? (
|
||||
<div className="w-full cursor-text px-7 py-8 border-2 border-mti-gray-platinum bg-white rounded-3xl">
|
||||
<div className="flex flex-col items-center justify-center animate-pulse">
|
||||
<span className={`loading loading-infinity w-32 bg-ielts-${currentModule}`} />
|
||||
<span className={`font-bold text-2xl text-ielts-${currentModule}`}>Generating...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
renderContent(editing)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionContext;
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ListeningPart } from "@/interfaces/exam";
|
||||
import SectionContext from ".";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import { FaFemale, FaMale } from "react-icons/fa";
|
||||
import useSectionEdit from "../../Hooks/useSectionEdit";
|
||||
import ScriptRender from "../../Exercises/Shared/Script";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
|
||||
const ListeningContext: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
|
||||
const { currentModule, dispatch } = useExamEditorStore();
|
||||
const { genResult, state, generating } = useExamEditorStore(
|
||||
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
|
||||
);
|
||||
const listeningPart = state as ListeningPart;
|
||||
|
||||
const [script, setScript] = useState(listeningPart.script);
|
||||
|
||||
const { editing, handleSave, handleDiscard, modeHandle, setEditing } = useSectionEdit({
|
||||
sectionId,
|
||||
mode: "edit",
|
||||
onSave: () => {
|
||||
const newState = { ...listeningPart };
|
||||
newState.script = script;
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState } })
|
||||
setEditing(false);
|
||||
},
|
||||
onDiscard: () => {
|
||||
setScript(listeningPart.script);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (genResult !== undefined && generating === "context") {
|
||||
setEditing(true);
|
||||
setScript(genResult[0].script)
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "genResult", value: undefined } })
|
||||
}
|
||||
}, [genResult, dispatch, sectionId, setEditing, currentModule]);
|
||||
|
||||
const renderContent = (editing: boolean) => {
|
||||
|
||||
if (script === undefined && !editing) {
|
||||
return (<p className="w-full text-gray-600 px-7 py-8 border-2 bg-white rounded-3xl whitespace-pre-line">
|
||||
Generate or import audio to add exercises!
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="py-10">
|
||||
<ScriptRender
|
||||
script={script}
|
||||
setScript={setScript}
|
||||
editing={editing}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContext
|
||||
sectionId={sectionId}
|
||||
title={(sectionId === 1 || sectionId === 3) ? "Conversation" : "Monologue"}
|
||||
description={`Enter the section's ${(sectionId === 1 || sectionId === 3) ? "conversation" : "monologue"} or import your own`}
|
||||
renderContent={renderContent}
|
||||
editing={editing}
|
||||
onSave={handleSave}
|
||||
onEdit={modeHandle}
|
||||
onDiscard={handleDiscard}
|
||||
module={currentModule}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListeningContext;
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ReadingPart } from "@/interfaces/exam";
|
||||
import Input from "@/components/Low/Input";
|
||||
import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea";
|
||||
import Passage from "../../Shared/Passage";
|
||||
import SectionContext from ".";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import useSectionEdit from "../../Hooks/useSectionEdit";
|
||||
|
||||
|
||||
const ReadingContext: React.FC<{sectionId: number;}> = ({sectionId}) => {
|
||||
const {currentModule, dispatch } = useExamEditorStore();
|
||||
const { genResult, state, generating } = useExamEditorStore(
|
||||
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
|
||||
);
|
||||
const readingPart = state as ReadingPart;
|
||||
|
||||
const [title, setTitle] = useState(readingPart.text.title);
|
||||
const [content, setContent] = useState(readingPart.text.content);
|
||||
const [passageOpen, setPassageOpen] = useState(false);
|
||||
|
||||
const { editing, handleSave, handleDiscard, modeHandle, setEditing } = useSectionEdit({
|
||||
sectionId,
|
||||
mode: "edit",
|
||||
onSave: () => {
|
||||
const newState = {...state} as ReadingPart;
|
||||
newState.text.title = title;
|
||||
newState.text.content = content;
|
||||
dispatch({type: 'UPDATE_SECTION_STATE', payload: {sectionId, update: newState}})
|
||||
setEditing(false);
|
||||
},
|
||||
onDiscard: () => {
|
||||
setTitle(readingPart.text.title);
|
||||
setContent(readingPart.text.content);
|
||||
},
|
||||
onMode: () => {
|
||||
setPassageOpen(false);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(()=> {
|
||||
if (genResult !== undefined && generating === "context") {
|
||||
setEditing(true);
|
||||
setTitle(genResult[0].title);
|
||||
setContent(genResult[0].text)
|
||||
dispatch({type: "UPDATE_SECTION_SINGLE_FIELD", payload: {sectionId, module: currentModule, field: "genResult", value: undefined}})
|
||||
}
|
||||
}, [genResult, dispatch, sectionId, setEditing, currentModule]);
|
||||
|
||||
|
||||
const renderContent = (editing: boolean) => {
|
||||
if (editing) {
|
||||
return (
|
||||
<div className="flex flex-col text-mti-gray-dim p-4 gap-4">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Insert a title here"
|
||||
name="title"
|
||||
label="Title"
|
||||
onChange={setTitle}
|
||||
roundness="xl"
|
||||
defaultValue={title}
|
||||
required
|
||||
/>
|
||||
<div className="flex flex-col gap-3">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Content *</label>
|
||||
<AutoExpandingTextArea
|
||||
value={content}
|
||||
placeholder="Insert a passage here"
|
||||
onChange={(text) => setContent(text)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return content === "" || title === "" ? (
|
||||
<p className="w-full text-gray-600 px-7 py-8 border-2 bg-white rounded-3xl whitespace-pre-line">
|
||||
Generate or edit the passage to add exercises!
|
||||
</p>
|
||||
) : (
|
||||
<Passage
|
||||
title={title}
|
||||
content={content}
|
||||
open={passageOpen}
|
||||
setIsOpen={setPassageOpen}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContext
|
||||
sectionId={sectionId}
|
||||
title="Reading Passage"
|
||||
description="The reading passage that the exercises will refer to."
|
||||
renderContent={renderContent}
|
||||
editing={editing}
|
||||
onSave={handleSave}
|
||||
onEdit={modeHandle}
|
||||
module={currentModule}
|
||||
onDiscard={handleDiscard}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReadingContext;
|
||||
@@ -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;
|
||||
@@ -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={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
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={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
content: <FillBlanksMC exercise={exercise} sectionId={sectionId} />
|
||||
};
|
||||
default:
|
||||
return {} as unknown as ExerciseItem;
|
||||
}
|
||||
}).filter((item) => item !== undefined);
|
||||
|
||||
return items || [];
|
||||
};
|
||||
|
||||
|
||||
export default getLevelQuestionItems;
|
||||
@@ -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={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
content: <WriteBlanksForm exercise={exercise} sectionId={sectionId} />
|
||||
};
|
||||
case 'fill':
|
||||
return {
|
||||
id: index,
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
label={`Write Blanks: Fill #${firstWordId} - #${lastWordId}`}
|
||||
preview={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
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={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
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={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
content: <MultipleChoice exercise={exercise} sectionId={sectionId} />
|
||||
};
|
||||
}
|
||||
}).filter((item) => item !== undefined);
|
||||
|
||||
return items || [];
|
||||
};
|
||||
|
||||
|
||||
export default getListeningItems;
|
||||
@@ -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={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
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={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
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={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
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={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
content: <TrueFalse exercise={exercise} sectionId={sectionId} />
|
||||
};
|
||||
|
||||
}
|
||||
}).filter((item) => item !== undefined);
|
||||
|
||||
return items || [];
|
||||
};
|
||||
|
||||
|
||||
export default getExerciseItems;
|
||||
@@ -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;
|
||||
99
src/components/ExamEditor/SectionRenderer/index.tsx
Executable file
99
src/components/ExamEditor/SectionRenderer/index.tsx
Executable file
@@ -0,0 +1,99 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { toast } from 'react-toastify';
|
||||
import ReadingContext from './SectionContext/reading';
|
||||
import SectionExercises from './SectionExercises';
|
||||
import useExamEditorStore from '@/stores/examEditor';
|
||||
import { ModuleState } from '@/stores/examEditor/types';
|
||||
import ListeningContext from './SectionContext/listening';
|
||||
import SectionDropdown from '../Shared/SectionDropdown';
|
||||
|
||||
|
||||
const SectionRenderer: React.FC = () => {
|
||||
const { currentModule, dispatch } = useExamEditorStore();
|
||||
const {
|
||||
focusedSection,
|
||||
expandedSections,
|
||||
sections,
|
||||
sectionLabels,
|
||||
edit,
|
||||
} = useExamEditorStore(state => state.modules[currentModule]);
|
||||
|
||||
const updateModule = useCallback((updates: Partial<ModuleState>) => {
|
||||
dispatch({ type: 'UPDATE_MODULE', payload: { updates } });
|
||||
}, [dispatch]);
|
||||
|
||||
const toggleSection = (sectionId: number) => {
|
||||
if (edit.includes(sectionId)) {
|
||||
toast.info(`Save or discard your changes first!`);
|
||||
} else {
|
||||
if (!expandedSections.includes(sectionId)) {
|
||||
updateModule({ focusedSection: sectionId });
|
||||
}
|
||||
updateModule({
|
||||
expandedSections:
|
||||
expandedSections.includes(sectionId) ?
|
||||
expandedSections.filter(index => index !== sectionId) :
|
||||
[...expandedSections, sectionId]
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
const ContextMap: Record<string, React.ComponentType<{ sectionId: number; }>> = {
|
||||
reading: ReadingContext,
|
||||
listening: ListeningContext,
|
||||
};
|
||||
|
||||
const SectionContext = ContextMap[currentModule];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-row'>
|
||||
<div className={clsx(
|
||||
"p-4 rounded-xl w-full",
|
||||
currentModule && `bg-ielts-${currentModule}/20`
|
||||
)}>
|
||||
|
||||
{sections.map((state, sectionIndex) => {
|
||||
const id = state.sectionId;
|
||||
const label = sectionLabels.find((sl) => sl.id == id)?.label;
|
||||
|
||||
return (
|
||||
<div key={id}
|
||||
className={
|
||||
clsx("rounded-xl shadow",
|
||||
sectionIndex !== sections.length - 1 && "mb-4"
|
||||
)}>
|
||||
<SectionDropdown
|
||||
toggleOpen={() => toggleSection(id)}
|
||||
open={expandedSections.includes(id)}
|
||||
title={label}
|
||||
className={clsx(
|
||||
"w-full py-4 px-8 text-lg font-semibold leading-6 text-white",
|
||||
"shadow-lg transform transition-all duration-300 hover:scale-102 hover:rounded-lg",
|
||||
expandedSections.includes(id) ? "rounded-t-lg" : "rounded-lg",
|
||||
focusedSection !== id ?
|
||||
`bg-gradient-to-r from-ielts-${currentModule}/30 to-ielts-${currentModule}/60 hover:from-ielts-${currentModule}/60 hover:to-ielts-${currentModule}` :
|
||||
`bg-ielts-${currentModule}`
|
||||
)}
|
||||
>
|
||||
{expandedSections.includes(id) && (
|
||||
<div
|
||||
className="p-6 bg-white rounded-b-xl shadow-inner border-b"
|
||||
onFocus={() => updateModule({ focusedSection: id })}
|
||||
tabIndex={id + 1}
|
||||
>
|
||||
{currentModule in ContextMap && <SectionContext sectionId={id} />}
|
||||
<SectionExercises sectionId={id} />
|
||||
</div>
|
||||
)}
|
||||
</SectionDropdown>
|
||||
</div>);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionRenderer;
|
||||
19
src/components/ExamEditor/SectionRenderer/types.ts
Normal file
19
src/components/ExamEditor/SectionRenderer/types.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Module } from "@/interfaces";
|
||||
import { GeneratedExercises, GeneratorState } from "../Shared/ExercisePicker/generatedExercises";
|
||||
import { SectionState } from "@/stores/examEditor/types";
|
||||
|
||||
|
||||
export interface SectionRendererProps {
|
||||
module: Module;
|
||||
sectionLabel: string;
|
||||
states: SectionState[];
|
||||
globalEdit: number[];
|
||||
generatedExercises: GeneratedExercises | undefined;
|
||||
generating: GeneratorState | undefined;
|
||||
focusedSection: number;
|
||||
setGeneratedExercises: React.Dispatch<React.SetStateAction<GeneratedExercises | undefined>>;
|
||||
setGenerating: React.Dispatch<React.SetStateAction<GeneratorState | undefined>>;
|
||||
setGlobalEdit: React.Dispatch<React.SetStateAction<number[]>>;
|
||||
setSectionStates: React.Dispatch<React.SetStateAction<SectionState[]>>;
|
||||
setFocusedSection: React.Dispatch<React.SetStateAction<number>>;
|
||||
}
|
||||
Reference in New Issue
Block a user