ENCOA-228 Now when user navigates between modules the generation items persist. Reading, listening and writing added to level module
This commit is contained in:
@@ -1,40 +1,41 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import ExamEditorStore from "@/stores/examEditor/types";
|
||||
import ExamEditorStore, { Generating } from "@/stores/examEditor/types";
|
||||
import Header from "../../Shared/Header";
|
||||
import { Module } from "@/interfaces";
|
||||
import GenLoader from "../../Exercises/Shared/GenLoader";
|
||||
|
||||
interface Props {
|
||||
sectionId: number;
|
||||
title: string;
|
||||
description: string;
|
||||
editing: boolean;
|
||||
renderContent: (editing: boolean) => React.ReactNode;
|
||||
renderContent: (editing: boolean, listeningSection?: number) => React.ReactNode;
|
||||
mode?: "edit" | "delete";
|
||||
onSave: () => void;
|
||||
onDiscard: () => void;
|
||||
onEdit?: () => void;
|
||||
module?: Module;
|
||||
module: Module;
|
||||
listeningSection?: number;
|
||||
context: Generating;
|
||||
}
|
||||
|
||||
const SectionContext: React.FC<Props> = ({ sectionId, title, description, renderContent, editing, onSave, onDiscard, onEdit, mode = "edit", module}) => {
|
||||
const { currentModule, dispatch } = useExamEditorStore();
|
||||
const { generating } = useExamEditorStore(
|
||||
const SectionContext: React.FC<Props> = ({ sectionId, title, description, renderContent, editing, onSave, onDiscard, onEdit, mode = "edit", module, context, listeningSection }) => {
|
||||
const { currentModule } = useExamEditorStore();
|
||||
const { generating, levelGenerating } = 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]);
|
||||
const [loading, setLoading] = useState(generating && generating === context);
|
||||
|
||||
useEffect(() => {
|
||||
const loading = generating && generating == "context";
|
||||
setLoading(loading);
|
||||
const gen = module === "level" ? levelGenerating.find(g => g === context) !== undefined : generating && generating === context;
|
||||
if (loading !== gen) {
|
||||
setLoading(gen);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [generating, updateRoot]);
|
||||
}, [generating, levelGenerating]);
|
||||
|
||||
return (
|
||||
<div className="p-8 shadow-inner border border-gray-200 bg-gray-50 rounded-xl">
|
||||
@@ -45,21 +46,15 @@ const SectionContext: React.FC<Props> = ({ sectionId, title, description, render
|
||||
editing={editing}
|
||||
handleSave={onSave}
|
||||
handleDiscard={onDiscard}
|
||||
modeHandle={onEdit}
|
||||
mode={mode}
|
||||
handleEdit={onEdit}
|
||||
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>
|
||||
<GenLoader module={module} />
|
||||
) : (
|
||||
renderContent(editing)
|
||||
renderContent(editing, listeningSection)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import ListeningContext from "./listening";
|
||||
import ReadingContext from "./reading";
|
||||
import GenLoader from "../../Exercises/Shared/GenLoader";
|
||||
|
||||
interface Props {
|
||||
sectionId: number;
|
||||
}
|
||||
|
||||
const LevelContext: React.FC<Props> = ({ sectionId }) => {
|
||||
const { currentModule } = useExamEditorStore();
|
||||
const { generating, readingSection, listeningSection } = useExamEditorStore(
|
||||
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{generating && (
|
||||
(generating === "passage" && <GenLoader module="reading" />) ||
|
||||
(generating === "listeningScript" && <GenLoader module="listening" />)
|
||||
)}
|
||||
{(readingSection || listeningSection) && (
|
||||
<div className="space-y-4 mb-4">
|
||||
{readingSection && <ReadingContext sectionId={sectionId} module="level" />}
|
||||
{listeningSection && <ListeningContext sectionId={sectionId} listeningSection={listeningSection} module="level" level={true}/>}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LevelContext;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ListeningPart } from "@/interfaces/exam";
|
||||
import { LevelPart, ListeningPart } from "@/interfaces/exam";
|
||||
import SectionContext from ".";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import useSectionEdit from "../../Hooks/useSectionEdit";
|
||||
@@ -9,25 +9,42 @@ import Dropdown from "@/components/Dropdown";
|
||||
import AudioPlayer from "@/components/Low/AudioPlayer";
|
||||
import { MdHeadphones } from "react-icons/md";
|
||||
import clsx from "clsx";
|
||||
import { Module } from "@/interfaces";
|
||||
import GenLoader from "../../Exercises/Shared/GenLoader";
|
||||
|
||||
interface Props {
|
||||
module: Module;
|
||||
sectionId: number;
|
||||
listeningSection?: number;
|
||||
level?: boolean;
|
||||
}
|
||||
|
||||
|
||||
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 ListeningContext: React.FC<Props> = ({ sectionId, module, listeningSection, level = false }) => {
|
||||
const { dispatch } = useExamEditorStore();
|
||||
const { genResult, state, generating, levelGenResults, levelGenerating } = useExamEditorStore(
|
||||
(state) => state.modules[module].sections.find((section) => section.sectionId === sectionId)!
|
||||
);
|
||||
const listeningPart = state as ListeningPart;
|
||||
const listeningPart = state as ListeningPart | LevelPart;
|
||||
const [isDialogDropdownOpen, setIsDialogDropdownOpen] = useState(false);
|
||||
|
||||
const [scriptLocal, setScriptLocal] = useState(listeningPart.script);
|
||||
|
||||
const { editing, handleSave, handleDiscard, modeHandle, setEditing } = useSectionEdit({
|
||||
const { editing, handleSave, handleDiscard, setEditing, handleEdit } = useSectionEdit({
|
||||
sectionId,
|
||||
mode: "edit",
|
||||
onSave: () => {
|
||||
const newState = { ...listeningPart };
|
||||
newState.script = scriptLocal;
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState } })
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module } })
|
||||
setEditing(false);
|
||||
|
||||
if (genResult) {
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module, field: "genResult", value: undefined } })
|
||||
}
|
||||
|
||||
if (levelGenResults.find((res) => res.generating === "listeningScript")) {
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module, field: "levelGenResults", value: levelGenResults.filter((res) => res.generating !== "listeningScript") } })
|
||||
}
|
||||
},
|
||||
onDiscard: () => {
|
||||
setScriptLocal(listeningPart.script);
|
||||
@@ -35,15 +52,42 @@ const ListeningContext: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (genResult !== undefined && generating === "context") {
|
||||
if (genResult && generating === "listeningScript") {
|
||||
setEditing(true);
|
||||
setScriptLocal(genResult[0].script);
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "genResult", value: undefined } })
|
||||
setScriptLocal(genResult.result[0].script);
|
||||
setIsDialogDropdownOpen(true);
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module, field: "generating", value: undefined } })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [genResult, dispatch, sectionId, setEditing, currentModule]);
|
||||
}, [genResult]);
|
||||
|
||||
const renderContent = (editing: boolean) => {
|
||||
useEffect(() => {
|
||||
if (genResult && generating === "audio") {
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module, field: "generating", value: undefined } })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [genResult]);
|
||||
|
||||
useEffect(() => {
|
||||
const scriptRes = levelGenResults.find((res) => res.generating === "listeningScript");
|
||||
if (levelGenResults && scriptRes) {
|
||||
setEditing(true);
|
||||
setScriptLocal(scriptRes.result[0].script);
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module, field: "levelGenerating", value: levelGenerating.filter(g => g !== "listeningScript") } })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [levelGenResults]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const scriptRes = levelGenResults.find((res) => res.generating === "audio");
|
||||
if (levelGenResults && scriptRes) {
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module, field: "levelGenerating", value: levelGenerating.filter(g => g !== "audio") } })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [levelGenResults]);
|
||||
|
||||
const renderContent = (editing: boolean, listeningSection?: number) => {
|
||||
if (scriptLocal === undefined && !editing) {
|
||||
return (
|
||||
<Card>
|
||||
@@ -53,16 +97,18 @@ const ListeningContext: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
{listeningPart.audio?.source && (
|
||||
<AudioPlayer
|
||||
key={sectionId}
|
||||
src={listeningPart.audio?.source ?? ''}
|
||||
color="listening"
|
||||
/>
|
||||
{generating === "audio" ? (<GenLoader module="listening" custom="Generating audio ..." />) : (
|
||||
<>
|
||||
{listeningPart.audio?.source && (
|
||||
<AudioPlayer
|
||||
key={sectionId}
|
||||
src={listeningPart.audio?.source ?? ''}
|
||||
color="listening"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Dropdown
|
||||
className="mt-8 w-full flex items-center justify-between p-4 bg-white hover:bg-gray-50 transition-colors border rounded-xl border-gray-200"
|
||||
@@ -71,16 +117,22 @@ const ListeningContext: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
|
||||
<div className="flex items-center space-x-3">
|
||||
<MdHeadphones className={clsx(
|
||||
"h-5 w-5",
|
||||
`text-ielts-${currentModule}`
|
||||
`text-ielts-${module}`
|
||||
)} />
|
||||
<span className="font-medium text-gray-900">{(sectionId === 1 || sectionId === 3) ? "Conversation" : "Monologue"}</span>
|
||||
<span className="font-medium text-gray-900">{
|
||||
listeningSection === undefined ?
|
||||
([1, 3].includes(sectionId) ? "Conversation" : "Monologue") :
|
||||
([1, 3].includes(listeningSection) ? "Conversation" : "Monologue")}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
open={isDialogDropdownOpen}
|
||||
setIsOpen={setIsDialogDropdownOpen}
|
||||
>
|
||||
<ScriptRender
|
||||
local={scriptLocal}
|
||||
setLocal={setScriptLocal}
|
||||
section={sectionId}
|
||||
section={level ? listeningSection! : sectionId}
|
||||
editing={editing}
|
||||
/>
|
||||
</Dropdown>
|
||||
@@ -91,14 +143,20 @@ const ListeningContext: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
|
||||
return (
|
||||
<SectionContext
|
||||
sectionId={sectionId}
|
||||
title={(sectionId === 1 || sectionId === 3) ? "Conversation" : "Monologue"}
|
||||
title={
|
||||
listeningSection === undefined ?
|
||||
([1, 3].includes(sectionId) ? "Conversation" : "Monologue") :
|
||||
([1, 3].includes(listeningSection) ? "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}
|
||||
onEdit={handleEdit}
|
||||
onDiscard={handleDiscard}
|
||||
module={currentModule}
|
||||
module={module}
|
||||
context="listeningScript"
|
||||
listeningSection={listeningSection}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,53 +1,81 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ReadingPart } from "@/interfaces/exam";
|
||||
import { LevelPart, 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";
|
||||
import { Module } from "@/interfaces";
|
||||
|
||||
interface Props {
|
||||
module: Module;
|
||||
sectionId: number;
|
||||
level?: boolean;
|
||||
}
|
||||
|
||||
|
||||
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 ReadingContext: React.FC<Props> = ({ sectionId, module, level = false }) => {
|
||||
const { dispatch } = useExamEditorStore();
|
||||
const sectionState = useExamEditorStore(
|
||||
(state) => state.modules[module].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 { genResult, state, levelGenResults, levelGenerating } = sectionState;
|
||||
const readingPart = state as ReadingPart | LevelPart;
|
||||
|
||||
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({
|
||||
const { editing, handleSave, handleDiscard, handleEdit, setEditing } = useSectionEdit({
|
||||
sectionId,
|
||||
mode: "edit",
|
||||
onSave: () => {
|
||||
let newState = {...state} as ReadingPart;
|
||||
newState.text.title = title;
|
||||
newState.text.content = content;
|
||||
dispatch({type: 'UPDATE_SECTION_STATE', payload: {sectionId, update: newState}})
|
||||
let newState = { ...state } as ReadingPart | LevelPart;
|
||||
newState.text = {
|
||||
title, content
|
||||
}
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module } })
|
||||
setEditing(false);
|
||||
|
||||
if (genResult) {
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module, field: "genResult", value: undefined } })
|
||||
}
|
||||
|
||||
if (levelGenResults.find((res) => res.generating === "passage")) {
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module, field: "levelGenResults", value: levelGenResults.filter((res) => res.generating !== "passage") } })
|
||||
}
|
||||
},
|
||||
onDiscard: () => {
|
||||
setTitle(readingPart.text.title);
|
||||
setContent(readingPart.text.content);
|
||||
setTitle(readingPart.text?.title || '');
|
||||
setContent(readingPart.text?.content || '');
|
||||
},
|
||||
onMode: () => {
|
||||
onEdit: () => {
|
||||
setPassageOpen(false);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(()=> {
|
||||
if (genResult !== undefined && generating === "context") {
|
||||
useEffect(() => {
|
||||
if (genResult && genResult.generating === "passage") {
|
||||
setEditing(true);
|
||||
console.log(genResult);
|
||||
setTitle(genResult[0].title);
|
||||
setContent(genResult[0].text);
|
||||
dispatch({type: "UPDATE_SECTION_SINGLE_FIELD", payload: {sectionId, module: currentModule, field: "genResult", value: undefined}})
|
||||
setTitle(genResult.result[0].title);
|
||||
setContent(genResult.result[0].text);
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module, field: "generating", value: undefined } })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [genResult, dispatch, sectionId, setEditing, currentModule]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [genResult]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const passageRes = levelGenResults.find((res) => res.generating === "passage");
|
||||
if (levelGenResults && passageRes) {
|
||||
setEditing(true);
|
||||
setTitle(passageRes.result[0].title);
|
||||
setContent(passageRes.result[0].text);
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module, field: "levelGenerating", value: levelGenerating.filter(g => g !== "passage") } })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [levelGenResults]);
|
||||
|
||||
|
||||
const renderContent = (editing: boolean) => {
|
||||
@@ -98,9 +126,10 @@ const ReadingContext: React.FC<{sectionId: number;}> = ({sectionId}) => {
|
||||
renderContent={renderContent}
|
||||
editing={editing}
|
||||
onSave={handleSave}
|
||||
onEdit={modeHandle}
|
||||
module={currentModule}
|
||||
onEdit={handleEdit}
|
||||
module={module}
|
||||
onDiscard={handleDiscard}
|
||||
context="passage"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Exercise } from "@/interfaces/exam";
|
||||
import ExerciseItem, { isExerciseItem } from "./types";
|
||||
import MultipleChoice from "../../Exercises/MultipleChoice";
|
||||
import ExerciseLabel from "../../Shared/ExerciseLabel";
|
||||
import writeBlanks from "./writeBlanks";
|
||||
import TrueFalse from "../../Exercises/TrueFalse";
|
||||
import fillBlanks from "./fillBlanks";
|
||||
import MatchSentences from "../../Exercises/MatchSentences";
|
||||
import Writing from "../../Exercises/Writing";
|
||||
|
||||
const getExerciseItems = (exercises: Exercise[], sectionId: number): ExerciseItem[] => {
|
||||
const items: ExerciseItem[] = exercises.map((exercise, index) => {
|
||||
let firstQuestionId, lastQuestionId;
|
||||
switch (exercise.type) {
|
||||
case "multipleChoice":
|
||||
firstQuestionId = exercise.questions[0].id;
|
||||
lastQuestionId = exercise.questions[exercise.questions.length - 1].id;
|
||||
return {
|
||||
id: index.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
type='Multiple Choice Questions'
|
||||
firstId={firstQuestionId}
|
||||
lastId={lastQuestionId}
|
||||
prompt={exercise.prompt}
|
||||
/>
|
||||
),
|
||||
content: <MultipleChoice exercise={exercise} sectionId={sectionId} />
|
||||
};
|
||||
case "trueFalse":
|
||||
firstQuestionId = exercise.questions[0].id
|
||||
lastQuestionId = exercise.questions[exercise.questions.length - 1].id;
|
||||
return {
|
||||
id: index.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
type='True/False/Not Given'
|
||||
firstId={firstQuestionId}
|
||||
lastId={lastQuestionId}
|
||||
prompt={exercise.prompt}
|
||||
/>
|
||||
),
|
||||
content: <TrueFalse exercise={exercise} sectionId={sectionId} />
|
||||
};
|
||||
case "matchSentences":
|
||||
firstQuestionId = exercise.sentences[0].id;
|
||||
lastQuestionId = exercise.sentences[exercise.sentences.length - 1].id;
|
||||
return {
|
||||
id: index.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
type={exercise.variant == "ideaMatch" ? "Idea Match" : "Paragraph Match"}
|
||||
firstId={firstQuestionId}
|
||||
lastId={lastQuestionId}
|
||||
prompt={exercise.prompt}
|
||||
/>
|
||||
),
|
||||
content: <MatchSentences exercise={exercise} sectionId={sectionId} />
|
||||
};
|
||||
case "fillBlanks":
|
||||
return fillBlanks(exercise, index, sectionId);
|
||||
case "writeBlanks":
|
||||
return writeBlanks(exercise, index, sectionId);
|
||||
case "writing":
|
||||
return {
|
||||
id: index.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
type={`Writing Task: ${exercise.variant === "letter" ? "Letter" : "Essay"}`}
|
||||
firstId={exercise.sectionId!.toString()}
|
||||
lastId={exercise.sectionId!.toString()}
|
||||
prompt={exercise.prompt}
|
||||
/>
|
||||
),
|
||||
content: <Writing key={exercise.id} exercise={exercise} sectionId={sectionId} index={index} module="level" />
|
||||
};
|
||||
default:
|
||||
return {} as unknown as ExerciseItem;
|
||||
}
|
||||
}).filter(isExerciseItem);
|
||||
/*return mappedItems.filter((item): item is ExerciseItem =>
|
||||
item !== null && isExerciseItem(item)
|
||||
);*/
|
||||
return items;
|
||||
};
|
||||
|
||||
|
||||
export default getExerciseItems;
|
||||
@@ -0,0 +1,78 @@
|
||||
import { FillBlanksExercise, FillBlanksMCOption } from "@/interfaces/exam";
|
||||
import ExerciseItem from "./types";
|
||||
import ExerciseLabel from "../../Shared/ExerciseLabel";
|
||||
import FillBlanksLetters from "../../Exercises/Blanks/Letters";
|
||||
import FillBlanksMC from "../../Exercises/Blanks/MultipleChoice";
|
||||
|
||||
interface LetterWord {
|
||||
letter: string;
|
||||
word: string;
|
||||
}
|
||||
|
||||
function isLetterWordArray(words: (string | LetterWord | FillBlanksMCOption)[]): words is LetterWord[] {
|
||||
return words.length > 0 &&
|
||||
words.every(item =>
|
||||
typeof item === 'object' &&
|
||||
'letter' in item &&
|
||||
'word' in item &&
|
||||
!('options' in item)
|
||||
);
|
||||
}
|
||||
|
||||
function isFillBlanksMCOptionArray(words: (string | LetterWord | FillBlanksMCOption)[]): words is FillBlanksMCOption[] {
|
||||
return words.length > 0 &&
|
||||
words.every(item =>
|
||||
typeof item === 'object' &&
|
||||
'id' in item &&
|
||||
'options' in item &&
|
||||
typeof (item as FillBlanksMCOption).options === 'object' &&
|
||||
'A' in (item as FillBlanksMCOption).options &&
|
||||
'B' in (item as FillBlanksMCOption).options &&
|
||||
'C' in (item as FillBlanksMCOption).options &&
|
||||
'D' in (item as FillBlanksMCOption).options
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const fillBlanks = (exercise: FillBlanksExercise, index: number, sectionId: number): ExerciseItem => {
|
||||
const firstWordId = exercise.solutions[0].id;
|
||||
const lastWordId = exercise.solutions[exercise.solutions.length - 1].id;
|
||||
|
||||
|
||||
if (isLetterWordArray(exercise.words)) {
|
||||
return {
|
||||
id: index.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
type='Fill Blanks Question'
|
||||
firstId={firstWordId}
|
||||
lastId={lastWordId}
|
||||
prompt={exercise.prompt}
|
||||
/>
|
||||
),
|
||||
content: <FillBlanksLetters exercise={exercise} sectionId={sectionId} />
|
||||
};
|
||||
}
|
||||
|
||||
if (isFillBlanksMCOptionArray(exercise.words)) {
|
||||
return {
|
||||
id: index.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
type='Fill Blanks: MC Question'
|
||||
firstId={firstWordId}
|
||||
lastId={lastWordId}
|
||||
prompt={exercise.prompt}
|
||||
/>
|
||||
),
|
||||
content: <FillBlanksMC exercise={exercise} sectionId={sectionId} />
|
||||
};
|
||||
}
|
||||
|
||||
// Don't know where the fillBlanks with words as string fits
|
||||
throw new Error(`Unsupported Exercise`);
|
||||
}
|
||||
|
||||
export default fillBlanks;
|
||||
@@ -1,8 +1,7 @@
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import SortableSection from "../../Shared/SortableSection";
|
||||
import getReadingQuestions from '../SectionExercises/reading';
|
||||
import { Exercise, LevelPart, ListeningPart, ReadingPart, SpeakingExercise, WritingExercise } from "@/interfaces/exam";
|
||||
import ExerciseItem, { ReadingExercise } from "./types";
|
||||
import ExerciseItem from "./types";
|
||||
import Dropdown from "@/components/Dropdown";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import Writing from "../../Exercises/Writing";
|
||||
@@ -13,15 +12,14 @@ import {
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragEndEvent,
|
||||
closestCenter,
|
||||
UniqueIdentifier,
|
||||
} from '@dnd-kit/core';
|
||||
import GenLoader from "../../Exercises/Shared/GenLoader";
|
||||
import { ExamPart } from "@/stores/examEditor/types";
|
||||
import getListeningItems from "./listening";
|
||||
import getLevelQuestionItems from "./level";
|
||||
import { ExamPart, Generating } from "@/stores/examEditor/types";
|
||||
import React from "react";
|
||||
import getExerciseItems from "./exercises";
|
||||
import { Action } from "@/stores/examEditor/reducers";
|
||||
import { writingTask } from "@/stores/examEditor/sections";
|
||||
|
||||
|
||||
interface QuestionItemsResult {
|
||||
@@ -30,31 +28,133 @@ interface QuestionItemsResult {
|
||||
}
|
||||
|
||||
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)!
|
||||
const dispatch = useExamEditorStore(state => state.dispatch);
|
||||
const currentModule = useExamEditorStore(state => state.currentModule);
|
||||
|
||||
const sections = useExamEditorStore(state => state.modules[currentModule].sections);
|
||||
const expandedSections = useExamEditorStore(state => state.modules[currentModule].expandedSections);
|
||||
|
||||
const section = useExamEditorStore(
|
||||
state => state.modules[currentModule].sections.find(
|
||||
section => section.sectionId === sectionId
|
||||
)
|
||||
);
|
||||
|
||||
const genResult = section?.genResult;
|
||||
const generating = section?.generating;
|
||||
const levelGenResults = section?.levelGenResults
|
||||
const levelGenerating = section?.levelGenerating;
|
||||
const sectionState = section?.state;
|
||||
|
||||
useEffect(() => {
|
||||
if (genResult !== undefined && generating === "exercises") {
|
||||
const newExercises = genResult[0].exercises;
|
||||
if (genResult && genResult.generating === "exercises" && genResult.module === currentModule) {
|
||||
const newExercises = genResult.result[0].exercises;
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_STATE", payload: {
|
||||
sectionId, update: {
|
||||
exercises: [...(state as ExamPart).exercises, ...newExercises]
|
||||
sectionId,
|
||||
module: genResult.module,
|
||||
update: {
|
||||
exercises: [...(sectionState as ExamPart).exercises, ...newExercises]
|
||||
}
|
||||
}
|
||||
})
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "generating", value: undefined } })
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "genResult", value: undefined } })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [genResult, dispatch, sectionId, currentModule]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (levelGenResults && levelGenResults.some(res => res.generating.startsWith("exercises"))) {
|
||||
const newExercises = levelGenResults
|
||||
.filter(res => res.generating.startsWith("exercises"))
|
||||
.map(res => res.result[0].exercises)
|
||||
.flat();
|
||||
|
||||
const updates = [
|
||||
{
|
||||
type: "UPDATE_SECTION_STATE",
|
||||
payload: {
|
||||
sectionId,
|
||||
module: "level",
|
||||
update: {
|
||||
exercises: [...(sectionState as ExamPart).exercises, ...newExercises]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||
payload: {
|
||||
sectionId,
|
||||
module: currentModule,
|
||||
field: "levelGenerating",
|
||||
value: levelGenerating?.filter(g => !g?.startsWith("exercises"))
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||
payload: {
|
||||
sectionId,
|
||||
module: currentModule,
|
||||
field: "levelGenResults",
|
||||
value: levelGenResults.filter(res => !res.generating.startsWith("exercises"))
|
||||
}
|
||||
}
|
||||
] as Action[];
|
||||
|
||||
updates.forEach(update => dispatch(update));
|
||||
}
|
||||
}, [levelGenResults, sectionState, levelGenerating, sectionId, currentModule]);
|
||||
|
||||
useEffect(() => {
|
||||
if (levelGenResults && levelGenResults.some(res => res.generating === "writing_letter" || res.generating === "writing_2")) {
|
||||
const results = levelGenResults.filter(res => res.generating === "writing_letter" || res.generating === "writing_2");
|
||||
|
||||
const updates = [
|
||||
{
|
||||
type: "UPDATE_SECTION_STATE",
|
||||
payload: {
|
||||
sectionId,
|
||||
module: "level",
|
||||
update: {
|
||||
exercises: [...(sectionState as ExamPart).exercises,
|
||||
...results.map((res)=> {
|
||||
return {
|
||||
...writingTask(res.generating === "writing_letter" ? 1 : 2),
|
||||
prompt: res.result[0].prompt,
|
||||
variant: res.generating === "writing_letter" ? "letter" : "essay"
|
||||
} as WritingExercise;
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||
payload: {
|
||||
sectionId,
|
||||
module: currentModule,
|
||||
field: "levelGenerating",
|
||||
value: levelGenerating?.filter(g => !results.flatMap(res => res.generating as Generating).includes(g))
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||
payload: {
|
||||
sectionId,
|
||||
module: currentModule,
|
||||
field: "levelGenResults",
|
||||
value: levelGenResults.filter(res => !results.flatMap(res => res.generating as Generating).includes(res.generating))
|
||||
}
|
||||
}
|
||||
] as Action[];
|
||||
|
||||
updates.forEach(update => dispatch(update));
|
||||
}
|
||||
}, [levelGenResults, sectionState, levelGenerating, sectionId, currentModule]);
|
||||
|
||||
|
||||
const currentSection = sections.find((s) => s.sectionId === sectionId)!;
|
||||
|
||||
const sensors = useSensors(
|
||||
@@ -62,42 +162,12 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
|
||||
);
|
||||
|
||||
const questionItems = (): QuestionItemsResult => {
|
||||
let result: QuestionItemsResult = {
|
||||
ids: [],
|
||||
items: []
|
||||
};
|
||||
|
||||
switch (currentModule) {
|
||||
case "reading": {
|
||||
const items = getReadingQuestions(
|
||||
(currentSection.state as ReadingPart).exercises as ReadingExercise[],
|
||||
sectionId
|
||||
);
|
||||
result.items = items.filter((item): item is ExerciseItem => item !== undefined);
|
||||
result.ids = result.items.map(item => item.id);
|
||||
break;
|
||||
}
|
||||
case "listening": {
|
||||
const items = getListeningItems(
|
||||
(currentSection.state as ListeningPart).exercises as Exercise[],
|
||||
sectionId
|
||||
);
|
||||
result.items = items.filter((item): item is ExerciseItem => item !== undefined);
|
||||
result.ids = result.items.map(item => item.id);
|
||||
break;
|
||||
}
|
||||
case "level": {
|
||||
const items = getLevelQuestionItems(
|
||||
(currentSection.state as LevelPart).exercises as Exercise[],
|
||||
sectionId
|
||||
);
|
||||
result.items = items.filter((item): item is ExerciseItem => item !== undefined);
|
||||
result.ids = result.items.map(item => item.id);
|
||||
break;
|
||||
}
|
||||
const part = currentSection.state as ReadingPart | ListeningPart | LevelPart;
|
||||
const items = getExerciseItems(part.exercises, sectionId);
|
||||
return {
|
||||
items,
|
||||
ids: items.map(item => item.id)
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const background = (component: ReactNode) => {
|
||||
@@ -108,33 +178,33 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (currentModule == "writing") return background(<Writing sectionId={sectionId} exercise={currentSection.state as WritingExercise} />);
|
||||
if (currentModule == "writing") return background(<Writing sectionId={sectionId} exercise={currentSection.state as WritingExercise} module="writing" />);
|
||||
if (currentModule == "speaking") return background(<Speaking sectionId={sectionId} exercise={currentSection.state as SpeakingExercise} />);
|
||||
|
||||
const questions = questionItems();
|
||||
|
||||
// #############################################################################
|
||||
// Typescript checks so that the compiler and builder don't freak out
|
||||
const filteredIds = (questions.ids ?? []).filter(Boolean);
|
||||
|
||||
function isValidItem(item: ExerciseItem | undefined): item is ExerciseItem {
|
||||
return item !== undefined &&
|
||||
typeof item.id === 'string' &&
|
||||
typeof item.sectionId === 'number' &&
|
||||
React.isValidElement(item.label) &&
|
||||
return item !== undefined &&
|
||||
typeof item.id === 'string' &&
|
||||
typeof item.sectionId === 'number' &&
|
||||
React.isValidElement(item.label) &&
|
||||
React.isValidElement(item.content);
|
||||
}
|
||||
|
||||
const filteredItems = (questions.items ?? []).filter(isValidItem);
|
||||
// #############################################################################
|
||||
|
||||
console.log(levelGenerating);
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={(e) => dispatch({ type: "REORDER_EXERCISES", payload: { event: e, sectionId } })}
|
||||
onDragEnd={(e) => dispatch({ type: "REORDER_EXERCISES", payload: { event: e, sectionId, module: currentModule } })}
|
||||
>
|
||||
{(currentModule === "level" && questions.ids?.length === 0 && generating === undefined) ? (
|
||||
background(<span className="flex justify-center">Generated exercises will appear here!</span>)
|
||||
) : (
|
||||
expandedSections.includes(sectionId) &&
|
||||
{expandedSections.includes(sectionId) &&
|
||||
questions.items &&
|
||||
questions.items.length > 0 &&
|
||||
questions.ids &&
|
||||
@@ -160,9 +230,16 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
|
||||
</SortableContext>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
}
|
||||
{generating === "exercises" && <GenLoader module={currentModule} className="mt-4" />}
|
||||
{currentModule === "level" && (
|
||||
<>
|
||||
{
|
||||
questions.ids?.length === 0 && !levelGenerating?.some((g) => g?.startsWith("exercises") || g?.startsWith("writing")) && generating !== "exercises"
|
||||
&& background(<span className="flex justify-center">Generated exercises will appear here!</span>)}
|
||||
{levelGenerating?.some((g) => g?.startsWith("exercises") || g?.startsWith("writing")) && <GenLoader module={currentModule} className="mt-4" />}
|
||||
</>)
|
||||
}
|
||||
</DndContext >
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import { Exercise } from "@/interfaces/exam";
|
||||
import ExerciseItem, { isExerciseItem } from "./types";
|
||||
import ExerciseLabel from "../../Shared/ExerciseLabel";
|
||||
import MultipleChoice from "../../Exercises/MultipleChoice";
|
||||
import FillBlanksMC from "../../Exercises/Blanks/MultipleChoice";
|
||||
import Passage from "../../Shared/Passage";
|
||||
|
||||
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":
|
||||
let content = <MultipleChoice exercise={exercise} sectionId={sectionId} />;
|
||||
const isReadingPassage = exercise.mcVariant && exercise.mcVariant === "passageUtas";
|
||||
if (isReadingPassage) {
|
||||
content = (<>
|
||||
<div className="p-4">
|
||||
<Passage
|
||||
title={exercise.passage?.title!}
|
||||
content={exercise.passage?.content!}
|
||||
/>
|
||||
</div>
|
||||
<MultipleChoice exercise={exercise} sectionId={sectionId} /></>
|
||||
);
|
||||
}
|
||||
firstWordId = exercise.questions[0].id;
|
||||
lastWordId = exercise.questions[exercise.questions.length - 1].id;
|
||||
return {
|
||||
id: index.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
label={isReadingPassage ? `Reading Passage: MC Questions #${firstWordId} - #${lastWordId}` : `Multiple Choice Questions #${firstWordId} - #${lastWordId}`}
|
||||
preview={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
content
|
||||
};
|
||||
case "fillBlanks":
|
||||
firstWordId = exercise.solutions[0].id;
|
||||
lastWordId = exercise.solutions[exercise.solutions.length - 1].id;
|
||||
return {
|
||||
id: index.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
label={`Fill Blanks: MC Question #${firstWordId} - #${lastWordId}`}
|
||||
preview={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
content: <FillBlanksMC exercise={exercise} sectionId={sectionId} />
|
||||
};
|
||||
default:
|
||||
return {} as unknown as ExerciseItem;
|
||||
}
|
||||
}).filter(isExerciseItem);
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
|
||||
export default getLevelQuestionItems;
|
||||
@@ -1,127 +0,0 @@
|
||||
import ExerciseItem, { isExerciseItem } 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';
|
||||
import WriteBlanks from '../../Exercises/WriteBlanks';
|
||||
|
||||
const writeBlanks = (exercise: WriteBlanksExercise, index: number, sectionId: number, previewLabel: (text: string) => string): ExerciseItem => {
|
||||
const firstWordId = exercise.solutions[0].id;
|
||||
const lastWordId = exercise.solutions[exercise.solutions.length - 1].id;
|
||||
|
||||
switch (exercise.variant) {
|
||||
case 'form':
|
||||
return {
|
||||
id: index.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
label={`Write Blanks: Form #${firstWordId} - #${lastWordId}`}
|
||||
preview={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
content: <WriteBlanksForm exercise={exercise} sectionId={sectionId} />
|
||||
};
|
||||
case 'fill':
|
||||
return {
|
||||
id: index.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
label={`Write Blanks: Fill #${firstWordId} - #${lastWordId}`}
|
||||
preview={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
content: <WriteBlanksFill exercise={exercise} sectionId={sectionId} />
|
||||
};
|
||||
case 'questions':
|
||||
return {
|
||||
id: index.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
label={`Write Blanks: Questions #${firstWordId} ${firstWordId === lastWordId ? '' : `- #${lastWordId}`}`}
|
||||
preview={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
content: <WriteBlanks exercise={exercise} sectionId={sectionId} title='Write Blanks: Questions' />
|
||||
};
|
||||
}
|
||||
throw new Error(`Just so that typescript doesnt complain`);
|
||||
};
|
||||
|
||||
const getListeningItems = (exercises: Exercise[], sectionId: number): ExerciseItem[] => {
|
||||
const previewLabel = (text: string) => {
|
||||
return text !== undefined ? text.replaceAll('\\n', ' ').split(' ').slice(0, 15).join(' ') : "";
|
||||
};
|
||||
|
||||
const mappedItems = exercises.map((exercise, index): ExerciseItem | null => {
|
||||
let firstWordId, lastWordId;
|
||||
|
||||
switch (exercise.type) {
|
||||
case "fillBlanks":
|
||||
firstWordId = exercise.solutions[0].id;
|
||||
lastWordId = exercise.solutions[exercise.solutions.length - 1].id;
|
||||
return {
|
||||
id: index.toString(),
|
||||
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.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
label={`Multiple Choice Questions #${firstWordId} - #${lastWordId}`}
|
||||
preview={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
content: <MultipleChoice exercise={exercise} sectionId={sectionId} />
|
||||
};
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
return mappedItems.filter((item): item is ExerciseItem =>
|
||||
item !== null && isExerciseItem(item)
|
||||
);
|
||||
};
|
||||
|
||||
export default getListeningItems;
|
||||
@@ -1,117 +0,0 @@
|
||||
import ExerciseItem, { isExerciseItem, 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';
|
||||
import MultipleChoice from '../../Exercises/MultipleChoice';
|
||||
|
||||
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.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
label={`Fill Blanks Question #${firstWordId} ${firstWordId === lastWordId ? '' : `- #${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.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
label={`Write Blanks Question #${firstWordId} ${firstWordId === lastWordId ? '' : `- #${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.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
label={`${exercise.variant == "ideaMatch" ? "Idea Match" : "Paragraph Match"} #${firstWordId} ${firstWordId === lastWordId ? '' : `- #${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.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
label={`True/False/Not Given #${firstWordId} ${firstWordId === lastWordId ? '' : `- #${lastWordId}`}`}
|
||||
preview={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
content: <TrueFalse exercise={exercise} sectionId={sectionId} />
|
||||
};
|
||||
case "multipleChoice":
|
||||
firstWordId = exercise.questions[0].id;
|
||||
lastWordId = exercise.questions[exercise.questions.length - 1].id;
|
||||
return {
|
||||
id: index.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
label={`Multiple Choice Questions #${firstWordId} - #${lastWordId}`}
|
||||
preview={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
content: <MultipleChoice exercise={exercise} sectionId={sectionId} />
|
||||
};
|
||||
|
||||
}
|
||||
}).filter(isExerciseItem);
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
|
||||
export default getExerciseItems;
|
||||
@@ -1,5 +1,3 @@
|
||||
import { FillBlanksExercise, MatchSentencesExercise, MultipleChoiceExercise, TrueFalseExercise, WriteBlanksExercise } from "@/interfaces/exam";
|
||||
|
||||
export default interface ExerciseItem {
|
||||
id: string;
|
||||
sectionId: number;
|
||||
@@ -7,8 +5,6 @@ export default interface ExerciseItem {
|
||||
content: React.ReactNode;
|
||||
}
|
||||
|
||||
export type ReadingExercise = FillBlanksExercise | TrueFalseExercise | MatchSentencesExercise | WriteBlanksExercise | MultipleChoiceExercise;
|
||||
|
||||
export function isExerciseItem(item: unknown): item is ExerciseItem {
|
||||
return item !== undefined &&
|
||||
item !== null &&
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { WriteBlanksExercise } from "@/interfaces/exam";
|
||||
import ExerciseLabel from "../../Shared/ExerciseLabel";
|
||||
import WriteBlanksForm from "../../Exercises/WriteBlanksForm";
|
||||
import WriteBlanksFill from "../../Exercises/Blanks/WriteBlankFill";
|
||||
import WriteBlanks from "../../Exercises/WriteBlanks";
|
||||
import ExerciseItem from "./types";
|
||||
|
||||
const writeBlanks = (exercise: WriteBlanksExercise, index: number, sectionId: number): ExerciseItem => {
|
||||
const firstQuestionId = exercise.solutions[0].id;
|
||||
const lastQuestionId = exercise.solutions[exercise.solutions.length - 1].id;
|
||||
|
||||
switch (exercise.variant) {
|
||||
case 'form':
|
||||
return {
|
||||
id: index.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
type='Write Blanks: Form'
|
||||
firstId={firstQuestionId}
|
||||
lastId={lastQuestionId}
|
||||
prompt={exercise.prompt}
|
||||
/>
|
||||
),
|
||||
content: <WriteBlanksForm exercise={exercise} sectionId={sectionId} />
|
||||
};
|
||||
case 'fill':
|
||||
return {
|
||||
id: index.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
type='Write Blanks: Fill'
|
||||
firstId={firstQuestionId}
|
||||
lastId={lastQuestionId}
|
||||
prompt={exercise.prompt}
|
||||
/>
|
||||
),
|
||||
content: <WriteBlanksFill exercise={exercise} sectionId={sectionId} />
|
||||
};
|
||||
default:
|
||||
return {
|
||||
id: index.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
type='Write Blanks: Questions'
|
||||
firstId={firstQuestionId}
|
||||
lastId={lastQuestionId}
|
||||
prompt={exercise.prompt}
|
||||
/>
|
||||
),
|
||||
content: <WriteBlanks exercise={exercise} sectionId={sectionId} />
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default writeBlanks;
|
||||
@@ -7,6 +7,8 @@ import useExamEditorStore from '@/stores/examEditor';
|
||||
import { ModuleState } from '@/stores/examEditor/types';
|
||||
import ListeningContext from './SectionContext/listening';
|
||||
import SectionDropdown from '../Shared/SectionDropdown';
|
||||
import LevelContext from './SectionContext/level';
|
||||
import { Module } from '@/interfaces';
|
||||
|
||||
|
||||
const SectionRenderer: React.FC = () => {
|
||||
@@ -39,9 +41,10 @@ const SectionRenderer: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const ContextMap: Record<string, React.ComponentType<{ sectionId: number; }>> = {
|
||||
const ContextMap: Record<string, React.ComponentType<{ sectionId: number; module: Module }>> = {
|
||||
reading: ReadingContext,
|
||||
listening: ListeningContext,
|
||||
level: LevelContext,
|
||||
};
|
||||
|
||||
const SectionContext = ContextMap[currentModule];
|
||||
@@ -83,7 +86,7 @@ const SectionRenderer: React.FC = () => {
|
||||
onFocus={() => updateModule({ focusedSection: id })}
|
||||
tabIndex={id + 1}
|
||||
>
|
||||
{currentModule in ContextMap && <SectionContext sectionId={id} />}
|
||||
{currentModule in ContextMap && <SectionContext sectionId={id} module={currentModule} />}
|
||||
<SectionExercises sectionId={id} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Module } from "@/interfaces";
|
||||
import { GeneratedExercises, GeneratorState } from "../Shared/ExercisePicker/generatedExercises";
|
||||
import { GeneratedExercises, GeneratorState } from "../ExercisePicker/generatedExercises";
|
||||
import { SectionState } from "@/stores/examEditor/types";
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user