Merge branch 'develop' of bitbucket.org:ecropdev/ielts-ui into develop
This commit is contained in:
@@ -7,13 +7,16 @@ import { GiBrain } from 'react-icons/gi';
|
||||
import { IoTextOutline } from 'react-icons/io5';
|
||||
import { Switch } from '@headlessui/react';
|
||||
import useExamEditorStore from '@/stores/examEditor';
|
||||
import { Module } from '@/interfaces';
|
||||
|
||||
interface Props {
|
||||
module: Module;
|
||||
sectionId: number;
|
||||
exercises: ExerciseGen[];
|
||||
extraArgs?: Record<string, any>;
|
||||
onSubmit: (configurations: ExerciseConfig[]) => void;
|
||||
onDiscard: () => void;
|
||||
selectedExercises: string[];
|
||||
}
|
||||
|
||||
export interface ExerciseConfig {
|
||||
@@ -24,15 +27,14 @@ export interface ExerciseConfig {
|
||||
}
|
||||
|
||||
const ExerciseWizard: React.FC<Props> = ({
|
||||
module,
|
||||
exercises,
|
||||
extraArgs,
|
||||
sectionId,
|
||||
selectedExercises,
|
||||
onSubmit,
|
||||
onDiscard,
|
||||
}) => {
|
||||
const {currentModule} = useExamEditorStore();
|
||||
const { selectedExercises } = useExamEditorStore((store) => store.modules[currentModule].sections.find((s) => s.sectionId === sectionId))!;
|
||||
|
||||
const [configurations, setConfigurations] = useState<ExerciseConfig[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -236,7 +238,7 @@ const ExerciseWizard: React.FC<Props> = ({
|
||||
return (
|
||||
<div
|
||||
key={config.type}
|
||||
className={`bg-ielts-${currentModule}/70 text-white rounded-lg p-4 shadow-xl`}
|
||||
className={`bg-ielts-${module}/70 text-white rounded-lg p-4 shadow-xl`}
|
||||
>
|
||||
{renderExerciseHeader(exercise, exerciseIndex, config, (exercise.extra || []).length > 2)}
|
||||
|
||||
@@ -262,7 +264,7 @@ const ExerciseWizard: React.FC<Props> = ({
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onSubmit(configurations)}
|
||||
className={`px-4 py-2 bg-ielts-${currentModule} text-white rounded-md hover:bg-ielts-${currentModule}/80 transition-colors`}
|
||||
className={`px-4 py-2 bg-ielts-${module} text-white rounded-md hover:bg-ielts-${module}/80 transition-colors`}
|
||||
>
|
||||
Add Exercises
|
||||
</button>
|
||||
@@ -321,6 +321,12 @@ const EXERCISES: ExerciseGen[] = [
|
||||
type: "writing_letter",
|
||||
icon: FaEnvelope,
|
||||
extra: [
|
||||
{
|
||||
label: "Letter Topic",
|
||||
param: "topic",
|
||||
value: "",
|
||||
type: "text"
|
||||
},
|
||||
generate()
|
||||
],
|
||||
module: "writing"
|
||||
@@ -330,6 +336,12 @@ const EXERCISES: ExerciseGen[] = [
|
||||
type: "writing_2",
|
||||
icon: FaFileAlt,
|
||||
extra: [
|
||||
{
|
||||
label: "Essay Topic",
|
||||
param: "topic",
|
||||
value: "",
|
||||
type: "text"
|
||||
},
|
||||
generate()
|
||||
],
|
||||
module: "writing"
|
||||
@@ -339,7 +351,19 @@ const EXERCISES: ExerciseGen[] = [
|
||||
type: "speaking_1",
|
||||
icon: FaComments,
|
||||
extra: [
|
||||
generate()
|
||||
generate(),
|
||||
{
|
||||
label: "First Topic",
|
||||
param: "first_topic",
|
||||
value: "",
|
||||
type: "text"
|
||||
},
|
||||
{
|
||||
label: "Second Topic",
|
||||
param: "second_topic",
|
||||
value: "",
|
||||
type: "text"
|
||||
},
|
||||
],
|
||||
module: "speaking"
|
||||
},
|
||||
@@ -348,7 +372,13 @@ const EXERCISES: ExerciseGen[] = [
|
||||
type: "speaking_2",
|
||||
icon: FaUserFriends,
|
||||
extra: [
|
||||
generate()
|
||||
generate(),
|
||||
{
|
||||
label: "Topic",
|
||||
param: "topic",
|
||||
value: "",
|
||||
type: "text"
|
||||
},
|
||||
],
|
||||
module: "speaking"
|
||||
},
|
||||
@@ -357,7 +387,13 @@ const EXERCISES: ExerciseGen[] = [
|
||||
type: "speaking_3",
|
||||
icon: FaHandshake,
|
||||
extra: [
|
||||
generate()
|
||||
generate(),
|
||||
{
|
||||
label: "Topic",
|
||||
param: "topic",
|
||||
value: "",
|
||||
type: "text"
|
||||
},
|
||||
],
|
||||
module: "speaking"
|
||||
},
|
||||
@@ -2,35 +2,39 @@ import EXERCISES from "./exercises";
|
||||
import clsx from "clsx";
|
||||
import { ExerciseGen, GeneratedExercises, GeneratorState } from "./generatedExercises";
|
||||
import Modal from "@/components/Modal";
|
||||
import { useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import ExerciseWizard, { ExerciseConfig } from "./ExerciseWizard";
|
||||
import { generate } from "../../SettingsEditor/Shared/Generate";
|
||||
import { generate } from "../SettingsEditor/Shared/Generate";
|
||||
import { Module } from "@/interfaces";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import { ListeningPart, Message, ReadingPart } from "@/interfaces/exam";
|
||||
import { LevelPart, ListeningPart, Message, ReadingPart } from "@/interfaces/exam";
|
||||
import { BsArrowRepeat } from "react-icons/bs";
|
||||
import { writingTask } from "@/stores/examEditor/sections";
|
||||
|
||||
interface ExercisePickerProps {
|
||||
module: string;
|
||||
sectionId: number;
|
||||
difficulty: string;
|
||||
extraArgs?: Record<string, any>;
|
||||
levelSectionId?: number;
|
||||
level?: boolean;
|
||||
}
|
||||
|
||||
const ExercisePicker: React.FC<ExercisePickerProps> = ({
|
||||
module,
|
||||
sectionId,
|
||||
extraArgs = undefined,
|
||||
levelSectionId,
|
||||
level = false
|
||||
}) => {
|
||||
const { currentModule, dispatch } = useExamEditorStore();
|
||||
const { difficulty } = useExamEditorStore((store) => store.modules[currentModule]);
|
||||
const section = useExamEditorStore((store) => store.modules[currentModule].sections.find((s) => s.sectionId == sectionId));
|
||||
const { difficulty, sections } = useExamEditorStore((store) => store.modules[level ? "level" : currentModule]);
|
||||
const section = sections.find((s) => s.sectionId === (level ? levelSectionId : sectionId));
|
||||
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
const [localSelectedExercises, setLocalSelectedExercises] = useState<string[]>([]);
|
||||
|
||||
if (section === undefined) return <></>;
|
||||
|
||||
const { state, selectedExercises } = section;
|
||||
const state = section?.state;
|
||||
|
||||
const getFullExerciseType = (exercise: ExerciseGen): string => {
|
||||
if (exercise.extra && exercise.extra.length > 0) {
|
||||
@@ -43,19 +47,19 @@ const ExercisePicker: React.FC<ExercisePickerProps> = ({
|
||||
const handleChange = (exercise: ExerciseGen) => {
|
||||
const fullType = getFullExerciseType(exercise);
|
||||
|
||||
const newSelected = selectedExercises.includes(fullType)
|
||||
? selectedExercises.filter(type => type !== fullType)
|
||||
: [...selectedExercises, fullType];
|
||||
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { module: currentModule, sectionId, field: "selectedExercises", value: newSelected } })
|
||||
setLocalSelectedExercises(prev => {
|
||||
const newSelected = prev.includes(fullType)
|
||||
? prev.filter(type => type !== fullType)
|
||||
: [...prev, fullType];
|
||||
return newSelected;
|
||||
});
|
||||
};
|
||||
|
||||
const moduleExercises = (sectionId && module !== "level" ? EXERCISES.filter((ex) => ex.module === module && ex.sectionId == sectionId) : EXERCISES.filter((ex) => ex.module === module));
|
||||
const moduleExercises = (sectionId && !["level", "writing", "speaking"].includes(module) ? EXERCISES.filter((ex) => ex.module === module && ex.sectionId == sectionId) : EXERCISES.filter((ex) => ex.module === module));
|
||||
|
||||
const onModuleSpecific = (configurations: ExerciseConfig[]) => {
|
||||
const onModuleSpecific = useCallback((configurations: ExerciseConfig[]) => {
|
||||
const exercises = configurations.map(config => {
|
||||
const exerciseType = config.type.split('name=')[1];
|
||||
|
||||
return {
|
||||
type: exerciseType,
|
||||
quantity: Number(config.params.quantity || 1),
|
||||
@@ -68,56 +72,97 @@ const ExercisePicker: React.FC<ExercisePickerProps> = ({
|
||||
};
|
||||
});
|
||||
|
||||
let context, moduleState;
|
||||
|
||||
switch (module) {
|
||||
case 'reading':
|
||||
moduleState = state as ReadingPart;
|
||||
let context = {};
|
||||
if (module === 'reading') {
|
||||
const readingState = state as ReadingPart | LevelPart;
|
||||
context = {
|
||||
text: readingState.text!.content
|
||||
};
|
||||
} else if (module === 'listening') {
|
||||
const listeningState = state as ListeningPart | LevelPart;
|
||||
const script = listeningState.script;
|
||||
if (sectionId === 1 || sectionId === 3) {
|
||||
const dialog = script as Message[];
|
||||
context = {
|
||||
text: moduleState.text.content
|
||||
}
|
||||
break;
|
||||
case 'listening':
|
||||
moduleState = state as ListeningPart;
|
||||
let script = moduleState.script;
|
||||
let text, dialog;
|
||||
switch (sectionId) {
|
||||
case 1:
|
||||
case 3:
|
||||
dialog = script as Message[];
|
||||
text = dialog.map((d) => `${d.name}: ${d.text}`).join("\n");
|
||||
context = { text: text }
|
||||
break;
|
||||
case 2:
|
||||
case 4:
|
||||
text = script as string;
|
||||
context = { text: text }
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
context = {}
|
||||
text: dialog.map((d) => `${d.name}: ${d.text}`).join("\n")
|
||||
};
|
||||
} else if (sectionId === 2 || sectionId === 4) {
|
||||
context = {
|
||||
text: script as string
|
||||
};
|
||||
}
|
||||
}
|
||||
generate(
|
||||
sectionId,
|
||||
module as Module,
|
||||
"exercises",
|
||||
{
|
||||
method: 'POST',
|
||||
body: {
|
||||
...context,
|
||||
exercises: exercises,
|
||||
difficulty: difficulty
|
||||
if (!["speaking", "writing"].includes(module)) {
|
||||
generate(
|
||||
sectionId,
|
||||
module as Module,
|
||||
level ? `exercises-${module}` : "exercises",
|
||||
{
|
||||
method: 'POST',
|
||||
body: {
|
||||
...context,
|
||||
exercises: exercises,
|
||||
difficulty: difficulty
|
||||
}
|
||||
},
|
||||
(data: any) => [{
|
||||
exercises: data.exercises
|
||||
}],
|
||||
levelSectionId,
|
||||
level
|
||||
);
|
||||
} else if (module === "writing") {
|
||||
configurations.forEach((config) => {
|
||||
let queryParams = config.params.topic !== '' ? { topic: config.params.topic as string } : undefined;
|
||||
generate(
|
||||
config.type === 'writing_letter' ? 1 : 2,
|
||||
"writing",
|
||||
config.type,
|
||||
{
|
||||
method: 'GET',
|
||||
queryParams
|
||||
},
|
||||
(data: any) => [{
|
||||
prompt: data.question
|
||||
}],
|
||||
levelSectionId,
|
||||
level
|
||||
);
|
||||
});
|
||||
} else {
|
||||
/*const newExercises = configurations.map((config) => {
|
||||
switch (config.type) {
|
||||
case 'writing_letter':
|
||||
return { ...writingTask(1), level: true };
|
||||
case 'writing_2':
|
||||
return { ...writingTask(2), level: true };
|
||||
}
|
||||
},
|
||||
(data: any) => [{
|
||||
exercises: data.exercises
|
||||
}]
|
||||
);
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "selectedExercises", value: [] } })
|
||||
return undefined;
|
||||
}).filter((ex) => ex !== undefined);
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_STATE", payload: {
|
||||
module: level ? "level" : module as Module, sectionId, update: {
|
||||
exercises: [
|
||||
...(sections.find((s) => s.sectionId = sectionId)?.state as LevelPart).exercises,
|
||||
...newExercises
|
||||
]
|
||||
}
|
||||
}
|
||||
})*/
|
||||
}
|
||||
setLocalSelectedExercises([]);
|
||||
setPickerOpen(false);
|
||||
};
|
||||
}, [
|
||||
sectionId,
|
||||
levelSectionId,
|
||||
level,
|
||||
module,
|
||||
state,
|
||||
difficulty,
|
||||
setPickerOpen
|
||||
]);
|
||||
|
||||
if (section === undefined) return <></>;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -131,6 +176,8 @@ const ExercisePicker: React.FC<ExercisePickerProps> = ({
|
||||
)}
|
||||
>
|
||||
<ExerciseWizard
|
||||
module={module as Module}
|
||||
selectedExercises={localSelectedExercises}
|
||||
sectionId={sectionId}
|
||||
exercises={moduleExercises}
|
||||
onSubmit={onModuleSpecific}
|
||||
@@ -151,7 +198,7 @@ const ExercisePicker: React.FC<ExercisePickerProps> = ({
|
||||
type="checkbox"
|
||||
name="exercise"
|
||||
value={fullType}
|
||||
checked={selectedExercises.includes(fullType)}
|
||||
checked={localSelectedExercises.includes(fullType)}
|
||||
onChange={() => handleChange(exercise)}
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
@@ -171,14 +218,14 @@ const ExercisePicker: React.FC<ExercisePickerProps> = ({
|
||||
)
|
||||
}
|
||||
onClick={() => setPickerOpen(true)}
|
||||
disabled={selectedExercises.length == 0}
|
||||
disabled={localSelectedExercises.length === 0}
|
||||
>
|
||||
{section.generating === "exercises" ? (
|
||||
<div key={`section-${sectionId}`} className="flex items-center justify-center">
|
||||
<BsArrowRepeat className="text-white animate-spin" size={25} />
|
||||
</div>
|
||||
) : (
|
||||
<>Set Up Exercises ({selectedExercises.length}) </>
|
||||
<>{["speaking", "writing"].includes(module) ? "Add Exercises" : "Set Up Exercises"} ({localSelectedExercises.length}) </>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
@@ -187,4 +234,4 @@ const ExercisePicker: React.FC<ExercisePickerProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ExercisePicker;
|
||||
export default ExercisePicker;
|
||||
@@ -47,7 +47,7 @@ const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: num
|
||||
setEditing,
|
||||
});
|
||||
|
||||
const { handleSave, handleDiscard, modeHandle } = useSectionEdit({
|
||||
const { handleSave, handleDiscard, handleDelete, handlePractice } = useSectionEdit({
|
||||
sectionId,
|
||||
editing,
|
||||
setEditing,
|
||||
@@ -74,7 +74,7 @@ const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: num
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState } });
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
},
|
||||
onDiscard: () => {
|
||||
setSelectedBlankId(null);
|
||||
@@ -96,12 +96,24 @@ const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: num
|
||||
blanksDispatcher({ type: "SET_BLANKS", payload: initialBlanks });
|
||||
|
||||
},
|
||||
onMode: () => {
|
||||
onDelete: () => {
|
||||
const newSection = {
|
||||
...section,
|
||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection, module: currentModule } });
|
||||
},
|
||||
onPractice: () => {
|
||||
const updatedExercise = {
|
||||
...local,
|
||||
isPractice: !local.isPractice,
|
||||
};
|
||||
const newState = { ...section };
|
||||
newState.exercises = newState.exercises.map((ex) =>
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -231,8 +243,6 @@ const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: num
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [answers, blanksState.blanks, blanksState.textMode])
|
||||
|
||||
|
||||
|
||||
useEffect(()=> {
|
||||
setEditingAlert(editing, setAlerts);
|
||||
}, [editing])
|
||||
@@ -252,8 +262,10 @@ const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: num
|
||||
onBlankRemove={handleBlankRemove}
|
||||
onSave={handleSave}
|
||||
onDiscard={handleDiscard}
|
||||
onDelete={modeHandle}
|
||||
onDelete={handleDelete}
|
||||
setEditing={setEditing}
|
||||
onPractice={handlePractice}
|
||||
isEvaluationEnabled={!local.isPractice}
|
||||
>
|
||||
<>
|
||||
{!blanksState.textMode && <Card className="p-4">
|
||||
|
||||
@@ -45,7 +45,7 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }
|
||||
setEditing,
|
||||
});
|
||||
|
||||
const { handleSave, handleDiscard, modeHandle } = useSectionEdit({
|
||||
const { handleSave, handleDiscard, handleDelete, handlePractice } = useSectionEdit({
|
||||
sectionId,
|
||||
editing,
|
||||
setEditing,
|
||||
@@ -72,7 +72,7 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState } });
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
},
|
||||
onDiscard: () => {
|
||||
setSelectedBlankId(null);
|
||||
@@ -92,12 +92,24 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }
|
||||
}, [] as BlankState[]);
|
||||
blanksDispatcher({ type: "SET_BLANKS", payload: initialBlanks });
|
||||
},
|
||||
onMode: () => {
|
||||
onDelete: () => {
|
||||
const newSection = {
|
||||
...section,
|
||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection, module: currentModule } });
|
||||
},
|
||||
onPractice: () => {
|
||||
const updatedExercise = {
|
||||
...local,
|
||||
isPractice: !local.isPractice
|
||||
};
|
||||
const newState = { ...section };
|
||||
newState.exercises = newState.exercises.map((ex) =>
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -189,7 +201,7 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }
|
||||
|
||||
useEffect(() => {
|
||||
validateBlanks(blanksState.blanks, answers, alerts, setAlerts);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [answers, blanksState.blanks, blanksState.textMode]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -215,16 +227,16 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }
|
||||
solution
|
||||
])
|
||||
));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleBlankRemove = (blankId: number) => {
|
||||
if (!editing) setEditing(true);
|
||||
|
||||
|
||||
const newAnswers = new Map(answers);
|
||||
newAnswers.delete(blankId.toString());
|
||||
setAnswers(newAnswers);
|
||||
|
||||
|
||||
setLocal(prev => ({
|
||||
...prev,
|
||||
words: (prev.words as FillBlanksMCOption[]).filter(w => w.id !== blankId.toString()),
|
||||
@@ -233,7 +245,7 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }
|
||||
solution
|
||||
}))
|
||||
}));
|
||||
|
||||
|
||||
blanksDispatcher({ type: "REMOVE_BLANK", payload: blankId });
|
||||
};
|
||||
|
||||
@@ -251,9 +263,11 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }
|
||||
onBlankSelect={(blankId) => setSelectedBlankId(blankId?.toString() || null)}
|
||||
onSave={handleSave}
|
||||
onDiscard={handleDiscard}
|
||||
onDelete={modeHandle}
|
||||
onDelete={handleDelete}
|
||||
onPractice={handlePractice}
|
||||
setEditing={setEditing}
|
||||
onBlankRemove={handleBlankRemove}
|
||||
isEvaluationEnabled={!local.isPractice}
|
||||
>
|
||||
{!blanksState.textMode && selectedBlankId && (
|
||||
<Card className="p-4">
|
||||
|
||||
@@ -10,7 +10,6 @@ import setEditingAlert from "../../Shared/setEditingAlert";
|
||||
import { blanksReducer } from "../BlanksReducer";
|
||||
import { validateWriteBlanks } from "./validation";
|
||||
import AlternativeSolutions from "./AlternativeSolutions";
|
||||
import clsx from "clsx";
|
||||
|
||||
const WriteBlanksFill: React.FC<{ exercise: WriteBlanksExercise; sectionId: number }> = ({ exercise, sectionId }) => {
|
||||
const { currentModule, dispatch } = useExamEditorStore();
|
||||
@@ -34,7 +33,7 @@ const WriteBlanksFill: React.FC<{ exercise: WriteBlanksExercise; sectionId: numb
|
||||
setEditing,
|
||||
});
|
||||
|
||||
const { handleSave, handleDiscard, modeHandle } = useSectionEdit({
|
||||
const { handleSave, handleDiscard, handleDelete, handlePractice } = useSectionEdit({
|
||||
sectionId,
|
||||
editing,
|
||||
setEditing,
|
||||
@@ -57,19 +56,31 @@ const WriteBlanksFill: React.FC<{ exercise: WriteBlanksExercise; sectionId: numb
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState } });
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
},
|
||||
onDiscard: () => {
|
||||
setSelectedBlankId(null);
|
||||
setLocal(exercise);
|
||||
blanksDispatcher({ type: "RESET", payload: { text: exercise.text } });
|
||||
},
|
||||
onMode: () => {
|
||||
onDelete: () => {
|
||||
const newSection = {
|
||||
...section,
|
||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection, module: currentModule } });
|
||||
},
|
||||
onPractice: () => {
|
||||
const updatedExercise = {
|
||||
...local,
|
||||
isPractice: !local.isPractice
|
||||
};
|
||||
const newState = { ...section };
|
||||
newState.exercises = newState.exercises.map((ex) =>
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -160,8 +171,10 @@ const WriteBlanksFill: React.FC<{ exercise: WriteBlanksExercise; sectionId: numb
|
||||
onBlankRemove={handleBlankRemove}
|
||||
onSave={handleSave}
|
||||
onDiscard={handleDiscard}
|
||||
onDelete={modeHandle}
|
||||
onDelete={handleDelete}
|
||||
onPractice={handlePractice}
|
||||
setEditing={setEditing}
|
||||
isEvaluationEnabled={!local.isPractice}
|
||||
>
|
||||
{!blanksState.textMode && (
|
||||
<Card>
|
||||
|
||||
@@ -37,6 +37,8 @@ interface Props {
|
||||
onSave: () => void;
|
||||
onDiscard: () => void;
|
||||
onDelete: () => void;
|
||||
onPractice: () => void;
|
||||
isEvaluationEnabled?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
@@ -56,6 +58,8 @@ const BlanksEditor: React.FC<Props> = ({
|
||||
onSave,
|
||||
onDiscard,
|
||||
onDelete,
|
||||
onPractice,
|
||||
isEvaluationEnabled,
|
||||
setEditing
|
||||
}) => {
|
||||
|
||||
@@ -161,8 +165,10 @@ const BlanksEditor: React.FC<Props> = ({
|
||||
description={description}
|
||||
editing={editing}
|
||||
handleSave={onSave}
|
||||
modeHandle={onDelete}
|
||||
handleDelete={onDelete}
|
||||
handleDiscard={onDiscard}
|
||||
handlePractice={onPractice}
|
||||
isEvaluationEnabled={isEvaluationEnabled}
|
||||
/>
|
||||
{alerts.length > 0 && <Alert alerts={alerts} />}
|
||||
<Card>
|
||||
|
||||
@@ -37,7 +37,7 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu
|
||||
setEditing(true);
|
||||
};
|
||||
|
||||
const { editing, setEditing, handleSave, handleDiscard, modeHandle } = useSectionEdit({
|
||||
const { editing, setEditing, handleSave, handleDiscard, handleDelete, handlePractice } = useSectionEdit({
|
||||
sectionId,
|
||||
onSave: () => {
|
||||
|
||||
@@ -53,19 +53,31 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu
|
||||
|
||||
const newState = { ...section };
|
||||
newState.exercises = newState.exercises.map((ex) => ex.id === exercise.id ? local : ex);
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState } });
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
},
|
||||
onDiscard: () => {
|
||||
setLocal(exercise);
|
||||
setSelectedParagraph(null);
|
||||
setShowReference(false);
|
||||
},
|
||||
onMode: () => {
|
||||
onDelete: () => {
|
||||
const newSection = {
|
||||
...section,
|
||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection, module: currentModule } });
|
||||
},
|
||||
onPractice: () => {
|
||||
const updatedExercise = {
|
||||
...local,
|
||||
isPractice: !local.isPractice
|
||||
};
|
||||
const newState = { ...section };
|
||||
newState.exercises = newState.exercises.map((ex) =>
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -142,8 +154,10 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu
|
||||
description={`Edit ${exercise.variant && exercise.variant == "ideaMatch" ? "ideas/opinions" : "headings"} and their matches`}
|
||||
editing={editing}
|
||||
handleSave={handleSave}
|
||||
modeHandle={modeHandle}
|
||||
handleDelete={handleDelete}
|
||||
handleDiscard={handleDiscard}
|
||||
handlePractice={handlePractice}
|
||||
isEvaluationEnabled={!local.isPractice}
|
||||
>
|
||||
<button
|
||||
onClick={() => setShowReference(!showReference)}
|
||||
@@ -210,7 +224,7 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu
|
||||
</SortableQuestion>
|
||||
))}
|
||||
</QuestionsList>
|
||||
{(section.text.content.split("\n\n").length - 1) === local.sentences.length && (
|
||||
{(section.text !== undefined && section.text.content.split("\n\n").length - 1) === local.sentences.length && (
|
||||
<button
|
||||
onClick={addHeading}
|
||||
className="w-full p-4 border-2 border-dashed border-gray-300 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-colors flex items-center justify-center gap-2 text-gray-600 hover:text-blue-600"
|
||||
|
||||
@@ -73,9 +73,8 @@ const UnderlineMultipleChoice: React.FC<{exercise: MultipleChoiceExercise, secti
|
||||
updateLocal({ ...local, questions: newQuestions });
|
||||
};
|
||||
|
||||
const { editing, handleSave, handleDiscard, modeHandle, setEditing } = useSectionEdit({
|
||||
const { editing, handleSave, handleDiscard, handleDelete, handlePractice, setEditing } = useSectionEdit({
|
||||
sectionId,
|
||||
mode: "edit",
|
||||
onSave: () => {
|
||||
setEditing(false);
|
||||
setAlerts([]);
|
||||
@@ -85,20 +84,32 @@ const UnderlineMultipleChoice: React.FC<{exercise: MultipleChoiceExercise, secti
|
||||
ex.id === local.id ? local : ex
|
||||
)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection, module: currentModule } });
|
||||
},
|
||||
onDiscard: () => {
|
||||
setAlerts([]);
|
||||
setLocal(exercise);
|
||||
setEditing(false);
|
||||
},
|
||||
onMode: () => {
|
||||
onDelete: () => {
|
||||
const newSection = {
|
||||
...section,
|
||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection, module: currentModule } });
|
||||
},
|
||||
onPractice: () => {
|
||||
const updatedExercise = {
|
||||
...local,
|
||||
isPractice: !local.isPractice
|
||||
};
|
||||
const newState = { ...section };
|
||||
newState.exercises = newState.exercises.map((ex) =>
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -108,8 +119,10 @@ const UnderlineMultipleChoice: React.FC<{exercise: MultipleChoiceExercise, secti
|
||||
description="Edit questions with 4 underline options each"
|
||||
editing={editing}
|
||||
handleSave={handleSave}
|
||||
modeHandle={modeHandle}
|
||||
handleDelete={handleDelete}
|
||||
handlePractice={handlePractice}
|
||||
handleDiscard={handleDiscard}
|
||||
isEvaluationEnabled={!local.isPractice}
|
||||
/>
|
||||
{alerts.length > 0 && <Alert className="mb-6" alerts={alerts} />}
|
||||
|
||||
|
||||
@@ -143,9 +143,8 @@ const MultipleChoice: React.FC<MultipleChoiceProps> = ({ exercise, sectionId, op
|
||||
updateLocal({ ...local, questions: updatedQuestions });
|
||||
};
|
||||
|
||||
const { editing, handleSave, handleDiscard, modeHandle, setEditing } = useSectionEdit({
|
||||
const { editing, handleSave, handleDiscard, handleDelete, handlePractice, setEditing } = useSectionEdit({
|
||||
sectionId,
|
||||
mode: "edit",
|
||||
onSave: () => {
|
||||
const isValid = validateMultipleChoiceQuestions(
|
||||
local.questions,
|
||||
@@ -164,20 +163,32 @@ const MultipleChoice: React.FC<MultipleChoiceProps> = ({ exercise, sectionId, op
|
||||
...section,
|
||||
exercises: section.exercises.map((ex) => ex.id === local.id ? local : ex)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection, module: currentModule } });
|
||||
},
|
||||
onDiscard: () => {
|
||||
setEditing(false);
|
||||
setAlerts([]);
|
||||
setLocal(exercise);
|
||||
},
|
||||
onMode: () => {
|
||||
onDelete: () => {
|
||||
const newSection = {
|
||||
...section,
|
||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection, module: currentModule } });
|
||||
},
|
||||
onPractice: () => {
|
||||
const updatedExercise = {
|
||||
...local,
|
||||
isPractice: !local.isPractice
|
||||
};
|
||||
const newState = { ...section };
|
||||
newState.exercises = newState.exercises.map((ex) =>
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -197,8 +208,10 @@ const MultipleChoice: React.FC<MultipleChoiceProps> = ({ exercise, sectionId, op
|
||||
description={`Edit questions with ${optionsQuantity} options each`}
|
||||
editing={editing}
|
||||
handleSave={handleSave}
|
||||
modeHandle={modeHandle}
|
||||
handleDelete={handleDelete}
|
||||
handleDiscard={handleDiscard}
|
||||
handlePractice={handlePractice}
|
||||
isEvaluationEnabled={!local.isPractice}
|
||||
/>
|
||||
{alerts.length > 0 && <Alert className="mb-6" alerts={alerts} />}
|
||||
<Card className="mb-6">
|
||||
|
||||
@@ -41,7 +41,6 @@ const colorOptions = [
|
||||
];
|
||||
|
||||
const ScriptEditor: React.FC<Props> = ({ section, editing = false, local, setLocal }) => {
|
||||
|
||||
const isConversation = [1, 3].includes(section);
|
||||
const speakerCount = section === 1 ? 2 : 4;
|
||||
|
||||
|
||||
@@ -12,53 +12,71 @@ import { InteractiveSpeakingExercise } from "@/interfaces/exam";
|
||||
import { BsFileText } from "react-icons/bs";
|
||||
import { FaChevronLeft, FaChevronRight } from "react-icons/fa6";
|
||||
import { RiVideoLine } from "react-icons/ri";
|
||||
import { Module } from "@/interfaces";
|
||||
|
||||
interface Props {
|
||||
sectionId: number;
|
||||
exercise: InteractiveSpeakingExercise;
|
||||
module?: Module;
|
||||
}
|
||||
|
||||
const InteractiveSpeaking: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
const { currentModule, dispatch } = useExamEditorStore();
|
||||
const InteractiveSpeaking: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }) => {
|
||||
const { dispatch } = useExamEditorStore();
|
||||
const [local, setLocal] = useState(exercise);
|
||||
|
||||
const [currentVideoIndex, setCurrentVideoIndex] = useState(0);
|
||||
|
||||
const { generating, genResult } = useExamEditorStore(
|
||||
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
|
||||
const { generating, genResult, state } = useExamEditorStore(
|
||||
(state) => state.modules[module].sections.find((section) => section.sectionId === sectionId)!
|
||||
);
|
||||
|
||||
const { editing, setEditing, handleSave, handleDiscard, modeHandle } = useSectionEdit({
|
||||
const { editing, setEditing, handleSave, handleDiscard, handleEdit, handlePractice } = useSectionEdit({
|
||||
sectionId,
|
||||
mode: "edit",
|
||||
onSave: () => {
|
||||
setEditing(false);
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId: sectionId, update: local } });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId: sectionId, update: local, module: module } });
|
||||
if (genResult) {
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||
payload: {
|
||||
sectionId,
|
||||
module: module,
|
||||
field: "genResult",
|
||||
value: undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onDiscard: () => {
|
||||
setLocal(exercise);
|
||||
},
|
||||
onMode: () => { },
|
||||
onPractice: () => {
|
||||
const updatedExercise = {
|
||||
...state,
|
||||
isPractice: !local.isPractice
|
||||
};
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: updatedExercise, module: module } });
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (genResult && generating === "context") {
|
||||
if (genResult && generating === "speakingScript") {
|
||||
setEditing(true);
|
||||
setLocal({
|
||||
...local,
|
||||
title: genResult[0].title,
|
||||
prompts: genResult[0].prompts.map((item: any) => ({
|
||||
title: genResult.result[0].title,
|
||||
prompts: genResult.result[0].prompts.map((item: any) => ({
|
||||
text: item || "",
|
||||
video_url: ""
|
||||
}))
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||
payload: {
|
||||
sectionId,
|
||||
module: currentModule,
|
||||
field: "genResult",
|
||||
module: module,
|
||||
field: "generating",
|
||||
value: undefined
|
||||
}
|
||||
});
|
||||
@@ -91,27 +109,18 @@ const InteractiveSpeaking: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
const isUnedited = local.prompts.length === 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (genResult && generating === "media") {
|
||||
setLocal({ ...local, prompts: genResult[0].prompts });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: { ...local, prompts: genResult[0].prompts } } });
|
||||
if (genResult && generating === "video") {
|
||||
setLocal({ ...local, prompts: genResult.result[0].prompts });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: { ...local, prompts: genResult.result[0].prompts }, module: module } });
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||
payload: {
|
||||
sectionId,
|
||||
module: currentModule,
|
||||
module: module,
|
||||
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, generating]);
|
||||
@@ -121,7 +130,7 @@ const InteractiveSpeaking: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
};
|
||||
|
||||
const handleNextVideo = () => {
|
||||
setCurrentVideoIndex((prev) =>
|
||||
setCurrentVideoIndex((prev) =>
|
||||
(prev < local.prompts.length - 1 ? prev + 1 : prev)
|
||||
);
|
||||
};
|
||||
@@ -134,14 +143,15 @@ const InteractiveSpeaking: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
description='Generate or write the scripts for the videos.'
|
||||
editing={editing}
|
||||
handleSave={handleSave}
|
||||
modeHandle={modeHandle}
|
||||
handleEdit={handleEdit}
|
||||
handleDiscard={handleDiscard}
|
||||
mode="edit"
|
||||
handlePractice={handlePractice}
|
||||
isEvaluationEnabled={!local.isPractice}
|
||||
module="speaking"
|
||||
/>
|
||||
</div>
|
||||
{generating && generating === "context" ? (
|
||||
<GenLoader module={currentModule} />
|
||||
{generating && generating === "speakingScript" ? (
|
||||
<GenLoader module={module} />
|
||||
) : (
|
||||
<>
|
||||
{editing ? (
|
||||
@@ -160,8 +170,8 @@ const InteractiveSpeaking: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
onClick={handlePrevVideo}
|
||||
disabled={currentVideoIndex === 0}
|
||||
className={`p-2 rounded-full ${currentVideoIndex === 0
|
||||
? 'text-gray-400 cursor-not-allowed'
|
||||
: 'text-gray-600 hover:bg-gray-100'
|
||||
? 'text-gray-400 cursor-not-allowed'
|
||||
: 'text-gray-600 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<FaChevronLeft className="w-4 h-4" />
|
||||
@@ -173,8 +183,8 @@ const InteractiveSpeaking: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
onClick={handleNextVideo}
|
||||
disabled={currentVideoIndex === local.prompts.length - 1}
|
||||
className={`p-2 rounded-full ${currentVideoIndex === local.prompts.length - 1
|
||||
? 'text-gray-400 cursor-not-allowed'
|
||||
: 'text-gray-600 hover:bg-gray-100'
|
||||
? 'text-gray-400 cursor-not-allowed'
|
||||
: 'text-gray-600 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<FaChevronRight className="w-4 h-4" />
|
||||
@@ -196,8 +206,8 @@ const InteractiveSpeaking: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{generating && generating === "media" &&
|
||||
<GenLoader module={currentModule} custom="Generating the videos ... This may take a while ..." />
|
||||
{generating && generating === "video" &&
|
||||
<GenLoader module={module} custom="Generating the videos ... This may take a while ..." />
|
||||
}
|
||||
<Card>
|
||||
<CardContent>
|
||||
|
||||
@@ -10,14 +10,16 @@ import { InteractiveSpeakingExercise } from "@/interfaces/exam";
|
||||
import { BsFileText } from "react-icons/bs";
|
||||
import { RiVideoLine } from 'react-icons/ri';
|
||||
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa6';
|
||||
import { Module } from '@/interfaces';
|
||||
|
||||
interface Props {
|
||||
sectionId: number;
|
||||
exercise: InteractiveSpeakingExercise;
|
||||
module?: Module;
|
||||
}
|
||||
|
||||
const Speaking1: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
const { currentModule, dispatch } = useExamEditorStore();
|
||||
const Speaking1: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }) => {
|
||||
const {dispatch} = useExamEditorStore();
|
||||
const [local, setLocal] = useState(() => {
|
||||
const defaultPrompts = [
|
||||
{ text: "Hello my name is {avatar}, what is yours?", video_url: "" },
|
||||
@@ -29,16 +31,26 @@ const Speaking1: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
|
||||
const [currentVideoIndex, setCurrentVideoIndex] = useState(0);
|
||||
|
||||
const { generating, genResult } = useExamEditorStore(
|
||||
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
|
||||
const { generating, genResult , state} = useExamEditorStore(
|
||||
(state) => state.modules[module].sections.find((section) => section.sectionId === sectionId)!
|
||||
);
|
||||
|
||||
const { editing, setEditing, handleSave, handleDiscard, modeHandle } = useSectionEdit({
|
||||
const { editing, setEditing, handleSave, handleDiscard, handleEdit, handlePractice } = useSectionEdit({
|
||||
sectionId,
|
||||
mode: "edit",
|
||||
onSave: () => {
|
||||
setEditing(false);
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId: sectionId, update: local } });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId: sectionId, update: local, module } });
|
||||
if (genResult) {
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||
payload: {
|
||||
sectionId,
|
||||
module: module,
|
||||
field: "genResult",
|
||||
value: undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onDiscard: () => {
|
||||
setLocal({
|
||||
@@ -50,32 +62,38 @@ const Speaking1: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
]
|
||||
});
|
||||
},
|
||||
onMode: () => { },
|
||||
onPractice: () => {
|
||||
const updatedExercise = {
|
||||
...state,
|
||||
isPractice: !local.isPractice
|
||||
};
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: updatedExercise, module: module } });
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (genResult && generating === "context") {
|
||||
if (genResult && generating === "speakingScript") {
|
||||
setEditing(true);
|
||||
setLocal(prev => ({
|
||||
...prev,
|
||||
first_title: genResult[0].first_topic,
|
||||
second_title: genResult[0].second_topic,
|
||||
first_title: genResult.result[0].first_topic,
|
||||
second_title: genResult.result[0].second_topic,
|
||||
prompts: [
|
||||
prev.prompts[0],
|
||||
prev.prompts[1],
|
||||
...genResult[0].prompts.map((item: any) => ({
|
||||
...genResult.result[0].prompts.map((item: any) => ({
|
||||
text: item,
|
||||
video_url: ""
|
||||
}))
|
||||
]
|
||||
}));
|
||||
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||
payload: {
|
||||
sectionId,
|
||||
module: currentModule,
|
||||
field: "genResult",
|
||||
module: module,
|
||||
field: "generating",
|
||||
value: undefined
|
||||
}
|
||||
});
|
||||
@@ -111,15 +129,14 @@ const Speaking1: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (genResult && generating === "media") {
|
||||
console.log(genResult[0].prompts);
|
||||
setLocal({ ...local, prompts: genResult[0].prompts });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: { ...local, prompts: genResult[0].prompts } } });
|
||||
if (genResult && generating === "video") {
|
||||
setLocal({ ...local, prompts: genResult.result[0].prompts });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: { ...local, prompts: genResult.result[0].prompts }, module } });
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||
payload: {
|
||||
sectionId,
|
||||
module: currentModule,
|
||||
module: module,
|
||||
field: "generating",
|
||||
value: undefined
|
||||
}
|
||||
@@ -128,7 +145,7 @@ const Speaking1: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||
payload: {
|
||||
sectionId,
|
||||
module: currentModule,
|
||||
module: module,
|
||||
field: "genResult",
|
||||
value: undefined
|
||||
}
|
||||
@@ -155,14 +172,15 @@ const Speaking1: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
description='Generate or write the scripts for the videos.'
|
||||
editing={editing}
|
||||
handleSave={handleSave}
|
||||
modeHandle={modeHandle}
|
||||
handleEdit={handleEdit}
|
||||
handleDiscard={handleDiscard}
|
||||
mode="edit"
|
||||
handlePractice={handlePractice}
|
||||
isEvaluationEnabled={!local.isPractice}
|
||||
module="speaking"
|
||||
/>
|
||||
</div>
|
||||
{generating && generating === "context" ? (
|
||||
<GenLoader module={currentModule} />
|
||||
{generating && generating === "speakingScript" ? (
|
||||
<GenLoader module={module} />
|
||||
) : (
|
||||
<>
|
||||
{editing ? (
|
||||
@@ -305,8 +323,8 @@ const Speaking1: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{generating && generating === "media" &&
|
||||
<GenLoader module={currentModule} custom="Generating the videos ... This may take a while ..." />
|
||||
{generating && generating === "video" &&
|
||||
<GenLoader module={module} custom="Generating the videos ... This may take a while ..." />
|
||||
}
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
|
||||
@@ -12,52 +12,70 @@ import { AiOutlineUnorderedList } from 'react-icons/ai';
|
||||
import { BiQuestionMark, BiMessageRoundedDetail } from "react-icons/bi";
|
||||
import GenLoader from "../Shared/GenLoader";
|
||||
import { RiVideoLine } from 'react-icons/ri';
|
||||
import { Module } from "@/interfaces";
|
||||
|
||||
interface Props {
|
||||
sectionId: number;
|
||||
exercise: SpeakingExercise;
|
||||
module?: Module;
|
||||
}
|
||||
|
||||
|
||||
const Speaking2: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
const { currentModule, dispatch } = useExamEditorStore();
|
||||
const Speaking2: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }) => {
|
||||
const { dispatch } = useExamEditorStore();
|
||||
const [local, setLocal] = useState(exercise);
|
||||
|
||||
const { generating, genResult } = useExamEditorStore(
|
||||
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
|
||||
const { generating, genResult, state } = useExamEditorStore(
|
||||
(state) => state.modules[module].sections.find((section) => section.sectionId === sectionId)!
|
||||
);
|
||||
|
||||
const { editing, setEditing, handleSave, handleDiscard, modeHandle } = useSectionEdit({
|
||||
const { editing, setEditing, handleSave, handleDiscard, handleEdit, handlePractice } = useSectionEdit({
|
||||
sectionId,
|
||||
mode: "edit",
|
||||
onSave: () => {
|
||||
setEditing(false);
|
||||
console.log(local);
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId: sectionId, update: local } });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId: sectionId, update: local , module} });
|
||||
if (genResult) {
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||
payload: {
|
||||
sectionId,
|
||||
module: module,
|
||||
field: "genResult",
|
||||
value: undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onDiscard: () => {
|
||||
setLocal(exercise);
|
||||
},
|
||||
onMode: () => { },
|
||||
onPractice: () => {
|
||||
const updatedExercise = {
|
||||
...state,
|
||||
isPractice: !local.isPractice
|
||||
};
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: updatedExercise, module: module } });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (genResult && generating === "context") {
|
||||
if (genResult && generating === "speakingScript") {
|
||||
setEditing(true);
|
||||
setLocal({
|
||||
...local,
|
||||
title: genResult[0].topic,
|
||||
text: genResult[0].question,
|
||||
prompts: genResult[0].prompts
|
||||
title: genResult.result[0].topic,
|
||||
text: genResult.result[0].question,
|
||||
prompts: genResult.result[0].prompts
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||
payload: {
|
||||
sectionId,
|
||||
module: currentModule,
|
||||
field: "genResult",
|
||||
module: module,
|
||||
field: "generating",
|
||||
value: undefined
|
||||
}
|
||||
});
|
||||
@@ -66,27 +84,18 @@ const Speaking2: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
}, [genResult, generating]);
|
||||
|
||||
useEffect(() => {
|
||||
if (genResult && generating === "media") {
|
||||
setLocal({...local, video_url: genResult[0].video_url});
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: {...local, video_url: genResult[0].video_url} } });
|
||||
if (genResult && generating === "video") {
|
||||
setLocal({...local, video_url: genResult.result[0].video_url});
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: {...local, video_url: genResult.result[0].video_url} , module} });
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||
payload: {
|
||||
sectionId,
|
||||
module: currentModule,
|
||||
module: module,
|
||||
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, generating]);
|
||||
@@ -138,14 +147,15 @@ const Speaking2: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
description='Generate or write the script for the video.'
|
||||
editing={editing}
|
||||
handleSave={handleSave}
|
||||
modeHandle={modeHandle}
|
||||
handleEdit={handleEdit}
|
||||
handleDiscard={handleDiscard}
|
||||
mode="edit"
|
||||
handlePractice={handlePractice}
|
||||
isEvaluationEnabled={!local.isPractice}
|
||||
module="speaking"
|
||||
/>
|
||||
</div>
|
||||
{generating && generating === "context" ? (
|
||||
<GenLoader module={currentModule} />
|
||||
{generating && generating === "speakingScript" ? (
|
||||
<GenLoader module={module} />
|
||||
) : (
|
||||
<>
|
||||
{editing ? (
|
||||
@@ -260,8 +270,8 @@ const Speaking2: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
}
|
||||
{generating && generating === "media" &&
|
||||
<GenLoader module={currentModule} custom="Generating the video ... This may take a while ..." />
|
||||
{generating && generating === "video" &&
|
||||
<GenLoader module={module} custom="Generating the video ... This may take a while ..." />
|
||||
}
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import { ModuleState } from "@/stores/examEditor/types";
|
||||
import { SpeakingExercise, InteractiveSpeakingExercise } from "@/interfaces/exam";
|
||||
import useSectionEdit from "../../Hooks/useSectionEdit";
|
||||
import Header from "../../Shared/Header";
|
||||
import GenLoader from "../Shared/GenLoader";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea";
|
||||
import Speaking2 from "./Speaking2";
|
||||
import InteractiveSpeaking from "./InteractiveSpeaking";
|
||||
import Speaking1 from "./Speaking1";
|
||||
import { Module } from "@/interfaces";
|
||||
|
||||
interface Props {
|
||||
sectionId: number;
|
||||
exercise: SpeakingExercise | InteractiveSpeakingExercise;
|
||||
qId?: number;
|
||||
module: Module;
|
||||
}
|
||||
|
||||
const Speaking: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
const { currentModule } = useExamEditorStore();
|
||||
const Speaking: React.FC<Props> = ({ sectionId, exercise, qId, module = "speaking" }) => {
|
||||
const { dispatch } = useExamEditorStore();
|
||||
const { state } = useExamEditorStore(
|
||||
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
|
||||
(state) => state.modules[module].sections.find((section) => section.sectionId === sectionId)!
|
||||
);
|
||||
|
||||
const onFocus = () => {
|
||||
if (qId) {
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { module, sectionId, field: "focusedExercise", value: { questionId: qId, id: exercise.id } } })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto p-3 space-y-6">
|
||||
<div tabIndex={0} className="mx-auto p-3 space-y-6" onFocus={onFocus}>
|
||||
<div className="p-4">
|
||||
<div className="flex flex-col space-y-6">
|
||||
{sectionId === 1 && <Speaking1 sectionId={sectionId} exercise={state as InteractiveSpeakingExercise} />}
|
||||
|
||||
@@ -72,9 +72,8 @@ const TrueFalse: React.FC<{ exercise: TrueFalseExercise, sectionId: number }> =
|
||||
updateLocal({ ...local, questions: updatedQuestions });
|
||||
};
|
||||
|
||||
const { editing, handleSave, handleDiscard, modeHandle, setEditing } = useSectionEdit({
|
||||
const { editing, handleSave, handleDiscard, handleDelete, handlePractice, setEditing } = useSectionEdit({
|
||||
sectionId,
|
||||
mode: "edit",
|
||||
onSave: () => {
|
||||
const isValid = validateTrueFalseQuestions(
|
||||
local.questions,
|
||||
@@ -93,18 +92,30 @@ const TrueFalse: React.FC<{ exercise: TrueFalseExercise, sectionId: number }> =
|
||||
...section,
|
||||
exercises: section.exercises.map((ex) => ex.id === local.id ? local : ex)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection, module: currentModule } });
|
||||
},
|
||||
onDiscard: () => {
|
||||
setLocal(exercise);
|
||||
},
|
||||
onMode: () => {
|
||||
onDelete: () => {
|
||||
const newSection = {
|
||||
...section,
|
||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection, module: currentModule } });
|
||||
},
|
||||
onPractice: () => {
|
||||
const updatedExercise = {
|
||||
...local,
|
||||
isPractice: false
|
||||
};
|
||||
const newState = { ...section };
|
||||
newState.exercises = newState.exercises.map((ex) =>
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
updateLocal({...local, isPractice: !local.isPractice})
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -127,8 +138,10 @@ const TrueFalse: React.FC<{ exercise: TrueFalseExercise, sectionId: number }> =
|
||||
description='Edit questions and their solutions'
|
||||
editing={editing}
|
||||
handleSave={handleSave}
|
||||
modeHandle={modeHandle}
|
||||
handleDelete={handleDelete}
|
||||
handleDiscard={handleDiscard}
|
||||
handlePractice={handlePractice}
|
||||
isEvaluationEnabled={!local.isPractice}
|
||||
/>
|
||||
{alerts.length > 0 && <Alert className="mb-6" alerts={alerts} />}
|
||||
<PromptEdit
|
||||
|
||||
@@ -23,7 +23,7 @@ import { handleWriteBlanksReorder } from '@/stores/examEditor/reorder/local';
|
||||
import { ParsedQuestion, parseText, reconstructText } from './parsing';
|
||||
|
||||
|
||||
const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise; title?: string; }> = ({ sectionId, exercise, title = "Write Blanks Exercise" }) => {
|
||||
const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise; }> = ({ sectionId, exercise }) => {
|
||||
|
||||
const { currentModule, dispatch } = useExamEditorStore();
|
||||
const { state } = useExamEditorStore(
|
||||
@@ -38,9 +38,8 @@ const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise;
|
||||
const [errors, setErrors] = useState<{ [key: string]: string[] }>({});
|
||||
const [parsedQuestions, setParsedQuestions] = useState<ParsedQuestion[]>([]);
|
||||
|
||||
const { editing, handleSave, handleDiscard, modeHandle, setEditing } = useSectionEdit({
|
||||
const { editing, handleSave, handleDiscard, handleDelete, handlePractice, setEditing } = useSectionEdit({
|
||||
sectionId,
|
||||
mode: "edit",
|
||||
onSave: () => {
|
||||
const isQuestionTextValid = validateQuestionText(
|
||||
parsedQuestions,
|
||||
@@ -65,18 +64,30 @@ const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise;
|
||||
...section,
|
||||
exercises: section.exercises.map((ex) => ex.id === local.id ? local : ex)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection, module: currentModule } });
|
||||
},
|
||||
onDiscard: () => {
|
||||
setLocal(exercise);
|
||||
},
|
||||
onMode: () => {
|
||||
onDelete: () => {
|
||||
const newSection = {
|
||||
...section,
|
||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection, module: currentModule } });
|
||||
},
|
||||
onPractice: () => {
|
||||
const updatedExercise = {
|
||||
...local,
|
||||
isPractice: !local.isPractice
|
||||
};
|
||||
const newState = { ...section };
|
||||
newState.exercises = newState.exercises.map((ex) =>
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -222,12 +233,14 @@ const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise;
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Header
|
||||
title={title}
|
||||
title={"Write Blanks: Questions"}
|
||||
description="Edit questions and their solutions"
|
||||
editing={editing}
|
||||
handleSave={handleSave}
|
||||
handleDiscard={handleDiscard}
|
||||
modeHandle={modeHandle}
|
||||
handleDelete={handleDelete}
|
||||
handlePractice={handlePractice}
|
||||
isEvaluationEnabled={!local.isPractice}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
{alerts.length > 0 && <Alert alerts={alerts} />}
|
||||
|
||||
@@ -31,9 +31,8 @@ const WriteBlanksForm: React.FC<{ sectionId: number; exercise: WriteBlanksExerci
|
||||
const [editingPrompt, setEditingPrompt] = useState(false);
|
||||
const [parsedQuestions, setParsedQuestions] = useState<ParsedQuestion[]>([]);
|
||||
|
||||
const { editing, handleSave, handleDiscard, modeHandle, setEditing } = useSectionEdit({
|
||||
const { editing, handleSave, handleDiscard, handleDelete, handlePractice, setEditing } = useSectionEdit({
|
||||
sectionId,
|
||||
mode: "edit",
|
||||
onSave: () => {
|
||||
const isQuestionsValid = validateQuestions(parsedQuestions, setAlerts);
|
||||
const isSolutionsValid = validateEmptySolutions(local.solutions, setAlerts);
|
||||
@@ -50,19 +49,31 @@ const WriteBlanksForm: React.FC<{ sectionId: number; exercise: WriteBlanksExerci
|
||||
...section,
|
||||
exercises: section.exercises.map((ex) => ex.id === local.id ? local : ex)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection, module: currentModule } });
|
||||
},
|
||||
onDiscard: () => {
|
||||
setLocal(exercise);
|
||||
setParsedQuestions([]);
|
||||
},
|
||||
onMode: () => {
|
||||
onDelete: () => {
|
||||
const newSection = {
|
||||
...section,
|
||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection, module: currentModule } });
|
||||
},
|
||||
onPractice: () => {
|
||||
const updatedExercise = {
|
||||
...local,
|
||||
isPractice: !local.isPractice
|
||||
};
|
||||
const newState = { ...section };
|
||||
newState.exercises = newState.exercises.map((ex) =>
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -204,7 +215,8 @@ const WriteBlanksForm: React.FC<{ sectionId: number; exercise: WriteBlanksExerci
|
||||
editing={editing}
|
||||
handleSave={handleSave}
|
||||
handleDiscard={handleDiscard}
|
||||
modeHandle={modeHandle}
|
||||
handleDelete={handleDelete}
|
||||
handlePractice={handlePractice}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
{alerts.length > 0 && <Alert alerts={alerts} />}
|
||||
|
||||
@@ -2,67 +2,88 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import ExamEditorStore, { ModuleState } from "@/stores/examEditor/types";
|
||||
import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea";
|
||||
import { WritingExercise } from "@/interfaces/exam";
|
||||
import { LevelPart, WritingExercise } from "@/interfaces/exam";
|
||||
import Header from "../../Shared/Header";
|
||||
import Alert, { AlertItem } from "../Shared/Alert";
|
||||
import clsx from "clsx";
|
||||
import useSectionEdit from "../../Hooks/useSectionEdit";
|
||||
import GenLoader from "../Shared/GenLoader";
|
||||
import setEditingAlert from "../Shared/setEditingAlert";
|
||||
import { Module } from "@/interfaces";
|
||||
|
||||
interface Props {
|
||||
sectionId: number;
|
||||
exercise: WritingExercise;
|
||||
module: Module;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
const Writing: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
const Writing: React.FC<Props> = ({ sectionId, exercise, module, index }) => {
|
||||
const { currentModule, dispatch } = useExamEditorStore();
|
||||
const { edit } = useExamEditorStore((store) => store.modules[currentModule]);
|
||||
const { generating, genResult, state } = useExamEditorStore(
|
||||
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
|
||||
);
|
||||
|
||||
|
||||
const [local, setLocal] = useState(exercise);
|
||||
const [prompt, setPrompt] = useState(exercise.prompt);
|
||||
const [loading, setLoading] = useState(generating && generating == "exercises");
|
||||
const [alerts, setAlerts] = useState<AlertItem[]>([]);
|
||||
|
||||
const updateModule = useCallback((updates: Partial<ModuleState>) => {
|
||||
dispatch({ type: 'UPDATE_MODULE', payload: { updates } });
|
||||
}, [dispatch]);
|
||||
const level = module === "level";
|
||||
|
||||
const { editing, handleSave, handleDiscard, modeHandle, setEditing } = useSectionEdit({
|
||||
const { editing, handleSave, handleDiscard, handleDelete, handlePractice, handleEdit, setEditing } = useSectionEdit({
|
||||
sectionId,
|
||||
onSave: () => {
|
||||
const newExercise = { ...local } as WritingExercise;
|
||||
newExercise.prompt = prompt;
|
||||
setAlerts([]);
|
||||
setEditing(false);
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId: sectionId, update: newExercise } });
|
||||
if (!level) {
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId: sectionId, update: newExercise, module } });
|
||||
}
|
||||
if (genResult) {
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "genResult", value: undefined } })
|
||||
}
|
||||
},
|
||||
onDiscard: () => {
|
||||
setEditing(false);
|
||||
setLocal(exercise);
|
||||
setPrompt(exercise.prompt);
|
||||
},
|
||||
onDelete: () => {
|
||||
if (level) {
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_STATE", payload: {
|
||||
sectionId: sectionId,
|
||||
update: {
|
||||
exercises: (state as LevelPart).exercises.filter((_, i) => i !== index)
|
||||
},
|
||||
module
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onPractice: () => {
|
||||
const newState = {
|
||||
...state,
|
||||
isPractice: !local.isPractice
|
||||
};
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const loading = generating && generating == "context";
|
||||
const loading = generating && generating == "writing";
|
||||
setLoading(loading);
|
||||
|
||||
if (loading) {
|
||||
updateModule({ edit: Array.from(new Set(edit).add(sectionId)) });
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [generating, updateModule]);
|
||||
}, [generating]);
|
||||
|
||||
useEffect(() => {
|
||||
if (genResult !== undefined && generating === "context") {
|
||||
if (genResult) {
|
||||
setEditing(true);
|
||||
setPrompt(genResult[0].prompt);
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "genResult", value: undefined } })
|
||||
setPrompt(genResult.result[0].prompt);
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "generating", value: undefined } })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [genResult, dispatch, sectionId, setEditing, currentModule]);
|
||||
@@ -73,20 +94,22 @@ const Writing: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='relative pb-4'>
|
||||
<div className={clsx('relative', level ? "px-4 mt-2" : "pb-2")}>
|
||||
<Header
|
||||
title={`Task ${sectionId} Instructions`}
|
||||
title={`${sectionId === 1 ? "Letter" : "Essay"} Instructions`}
|
||||
description='Generate or edit the instructions for the task'
|
||||
editing={editing}
|
||||
handleSave={handleSave}
|
||||
modeHandle={modeHandle}
|
||||
handleDelete={handleDelete}
|
||||
handleEdit={handleEdit}
|
||||
handleDiscard={handleDiscard}
|
||||
mode="edit"
|
||||
handlePractice={handlePractice}
|
||||
isEvaluationEnabled={!local.isPractice}
|
||||
module={"writing"}
|
||||
/>
|
||||
{alerts.length !== 0 && <Alert alerts={alerts} />}
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className={clsx(level ? "mt-2 px-4" : "mt-4")}>
|
||||
{loading ?
|
||||
<GenLoader module={currentModule} /> :
|
||||
(
|
||||
|
||||
@@ -4,12 +4,13 @@ import { useCallback, useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
sectionId: number;
|
||||
mode?: "delete" | "edit";
|
||||
editing?: boolean;
|
||||
setEditing?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
onSave?: () => void;
|
||||
onDiscard?: () => void;
|
||||
onMode?: () => void;
|
||||
onDelete?: () => void;
|
||||
onPractice?: () => void;
|
||||
onEdit?: () => void;
|
||||
}
|
||||
|
||||
const useSectionEdit = ({
|
||||
@@ -18,7 +19,9 @@ const useSectionEdit = ({
|
||||
setEditing: externalSetEditing,
|
||||
onSave,
|
||||
onDiscard,
|
||||
onMode
|
||||
onDelete,
|
||||
onPractice,
|
||||
onEdit
|
||||
}: Props) => {
|
||||
const { dispatch } = useExamEditorStore();
|
||||
const [internalEditing, setInternalEditing] = useState<boolean>(externalEditing);
|
||||
@@ -31,9 +34,12 @@ const useSectionEdit = ({
|
||||
}, [dispatch]);
|
||||
|
||||
const handleEdit = useCallback(() => {
|
||||
setEditing(true);
|
||||
setEditing(!editing);
|
||||
if (onEdit) {
|
||||
onEdit();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sectionId, setEditing, updateRoot]);
|
||||
}, [sectionId, editing, setEditing, updateRoot]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
if (onSave) {
|
||||
@@ -51,12 +57,18 @@ const useSectionEdit = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [setEditing, updateRoot, onDiscard, sectionId]);
|
||||
|
||||
const modeHandle = useCallback(() => {
|
||||
const handleDelete = useCallback(() => {
|
||||
setEditing(!editing);
|
||||
onMode?.();
|
||||
onDelete?.();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [setEditing, editing, updateRoot, onMode, sectionId]);
|
||||
}, [setEditing, editing, updateRoot, onDelete, sectionId]);
|
||||
|
||||
const handlePractice = useCallback(() => {
|
||||
onPractice?.();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [setEditing, editing, updateRoot, onPractice, sectionId]);
|
||||
|
||||
return {
|
||||
editing,
|
||||
@@ -64,7 +76,8 @@ const useSectionEdit = ({
|
||||
handleEdit,
|
||||
handleSave,
|
||||
handleDiscard,
|
||||
modeHandle,
|
||||
handleDelete,
|
||||
handlePractice,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -38,13 +38,14 @@ const useSettingsState = <T extends SectionSettings>(
|
||||
if (Object.keys(pendingUpdatesRef.current).length > 0) {
|
||||
dispatch({
|
||||
type: 'UPDATE_SECTION_SETTINGS',
|
||||
payload: { sectionId, update: pendingUpdatesRef.current}
|
||||
payload: { sectionId, update: pendingUpdatesRef.current, module}
|
||||
});
|
||||
pendingUpdatesRef.current = {};
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return debouncedFn;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dispatch, sectionId]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -52,7 +53,7 @@ const useSettingsState = <T extends SectionSettings>(
|
||||
if (Object.keys(pendingUpdatesRef.current).length > 0) {
|
||||
dispatch({
|
||||
type: 'UPDATE_SECTION_SETTINGS',
|
||||
payload: {sectionId, update: pendingUpdatesRef.current}
|
||||
payload: {sectionId, update: pendingUpdatesRef.current, module}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FaPencilAlt } from 'react-icons/fa';
|
||||
import { Module } from '@/interfaces';
|
||||
import clsx from 'clsx';
|
||||
import WordUploader from './WordUploader';
|
||||
import GenLoader from '../../Exercises/Shared/GenLoader';
|
||||
import GenLoader from '../Exercises/Shared/GenLoader';
|
||||
import useExamEditorStore from '@/stores/examEditor';
|
||||
|
||||
const ImportOrFromScratch: React.FC<{ module: Module; }> = ({ module }) => {
|
||||
@@ -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,121 @@
|
||||
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";
|
||||
import Speaking from "../../Exercises/Speaking";
|
||||
|
||||
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" />
|
||||
};
|
||||
case "speaking":
|
||||
return {
|
||||
id: index.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
type={`Speaking Section 2`}
|
||||
firstId={exercise.sectionId!.toString()}
|
||||
lastId={exercise.sectionId!.toString()}
|
||||
prompt={exercise.prompts[2]}
|
||||
/>
|
||||
),
|
||||
content: <Speaking key={exercise.id} exercise={exercise} sectionId={sectionId} qId={index} module="level" />
|
||||
};
|
||||
case "interactiveSpeaking":
|
||||
return {
|
||||
id: index.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
type={`Speaking Section 2`}
|
||||
firstId={exercise.sectionId!.toString()}
|
||||
lastId={exercise.sectionId!.toString()}
|
||||
prompt={exercise.prompts[2].text}
|
||||
/>
|
||||
),
|
||||
content: <Speaking key={exercise.id} exercise={exercise} sectionId={sectionId} qId={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,135 @@ 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));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [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));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [levelGenResults, sectionState, levelGenerating, sectionId, currentModule]);
|
||||
|
||||
|
||||
const currentSection = sections.find((s) => s.sectionId === sectionId)!;
|
||||
|
||||
const sensors = useSensors(
|
||||
@@ -62,42 +164,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 +180,33 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
|
||||
);
|
||||
}
|
||||
|
||||
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} />);
|
||||
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} module="speaking" />);
|
||||
|
||||
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 +232,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";
|
||||
|
||||
|
||||
|
||||
@@ -14,27 +14,51 @@ interface GeneratorConfig {
|
||||
export function generate(
|
||||
sectionId: number,
|
||||
module: Module,
|
||||
type: "context" | "exercises",
|
||||
type: Generating,
|
||||
config: GeneratorConfig,
|
||||
mapData: (data: any) => Record<string, any>[]
|
||||
mapData: (data: any) => Record<string, any>[],
|
||||
levelSectionId?: number,
|
||||
level: boolean = false
|
||||
) {
|
||||
const dispatch = useExamEditorStore.getState().dispatch;
|
||||
const setGenerating = (sectionId: number, generating: Generating, level: boolean, remove?: boolean) => {
|
||||
const state = useExamEditorStore.getState();
|
||||
const dispatch = state.dispatch;
|
||||
let generatingUpdate;
|
||||
if (level) {
|
||||
if (remove) {
|
||||
generatingUpdate = state.modules["level"].sections.find((s) => s.sectionId === levelSectionId)!.levelGenerating.filter(g => g === generating)
|
||||
}
|
||||
else {
|
||||
generatingUpdate = [...state.modules["level"].sections.find((s) => s.sectionId === levelSectionId)!.levelGenerating, generating];
|
||||
}
|
||||
} else {
|
||||
generatingUpdate = generating;
|
||||
}
|
||||
|
||||
const setGenerating = (sectionId: number, generating: Generating) => {
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||
payload: { sectionId, module, field: "generating", value: generating }
|
||||
payload: { sectionId : sectionId, module: level ? "level" : module, field: level ? "levelGenerating" : "generating", value: generatingUpdate }
|
||||
});
|
||||
};
|
||||
|
||||
const setGeneratedExercises = (sectionId: number, exercises: Record<string, any>[] | undefined) => {
|
||||
const setGeneratedResult = (sectionId: number, generating: Generating, result: Record<string, any>[] | undefined, level: boolean) => {
|
||||
const state = useExamEditorStore.getState();
|
||||
const dispatch = state.dispatch;
|
||||
|
||||
let genResults;
|
||||
if (level) {
|
||||
genResults = [...state.modules["level"].sections.find((s) => s.sectionId === levelSectionId)!.levelGenResults, { generating, result, module }];
|
||||
} else {
|
||||
genResults = { generating, result, module };
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||
payload: { sectionId, module, field: "genResult", value: exercises }
|
||||
payload: { sectionId, module: level ? "level" : module, field: level ? "levelGenResults" : "genResult", value: genResults }
|
||||
});
|
||||
};
|
||||
|
||||
setGenerating(sectionId, type);
|
||||
setGenerating(level ? levelSectionId! : sectionId, type, level);
|
||||
|
||||
const queryString = config.queryParams
|
||||
? new URLSearchParams(config.queryParams).toString()
|
||||
@@ -49,13 +73,11 @@ export function generate(
|
||||
request
|
||||
.then((result) => {
|
||||
playSound("check");
|
||||
setGeneratedExercises(sectionId, mapData(result.data));
|
||||
setGeneratedResult(level ? levelSectionId! : sectionId, type, mapData(result.data), level);
|
||||
})
|
||||
.catch((error) => {
|
||||
setGenerating(sectionId, undefined, level, true);
|
||||
playSound("error");
|
||||
toast.error("Something went wrong! Try to generate again.");
|
||||
})
|
||||
.finally(() => {
|
||||
setGenerating(sectionId, undefined);
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Module } from "@/interfaces";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import { Generating } from "@/stores/examEditor/types";
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useState } from "react";
|
||||
import { BsArrowRepeat } from "react-icons/bs";
|
||||
import { GiBrain } from "react-icons/gi";
|
||||
|
||||
@@ -11,25 +12,37 @@ interface Props {
|
||||
genType: Generating;
|
||||
generateFnc: (sectionId: number) => void
|
||||
className?: string;
|
||||
levelId?: number;
|
||||
level?: boolean;
|
||||
}
|
||||
|
||||
const GenerateBtn: React.FC<Props> = ({module, sectionId, genType, generateFnc, className}) => {
|
||||
const section = useExamEditorStore((store) => store.modules[module].sections.find((s)=> s.sectionId == sectionId));
|
||||
const GenerateBtn: React.FC<Props> = ({ module, sectionId, genType, generateFnc, className, level = false, levelId }) => {
|
||||
const section = useExamEditorStore((store) => store.modules[level ? "level" : module].sections.find((s) => s.sectionId == levelId ? levelId : sectionId));
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const generating = section?.generating;
|
||||
const levelGenerating = section?.levelGenerating;
|
||||
|
||||
useEffect(()=> {
|
||||
const gen = level ? levelGenerating?.find(g => g === genType) !== undefined : (generating !== undefined && generating === genType);
|
||||
if (loading !== gen) {
|
||||
setLoading(gen);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [generating, levelGenerating])
|
||||
|
||||
if (section === undefined) return <></>;
|
||||
|
||||
const {generating} = section;
|
||||
|
||||
|
||||
const loading = generating && generating === genType;
|
||||
return (
|
||||
<button
|
||||
key={`section-${sectionId}`}
|
||||
className={clsx(
|
||||
"flex items-center w-[140px] justify-center px-4 py-2 text-white rounded-xl transition-colors duration-300 text-lg",
|
||||
`bg-ielts-${module}/70 border border-ielts-${module} hover:bg-ielts-${module}`,
|
||||
"flex items-center w-[140px] justify-center px-4 py-2 text-white rounded-xl transition-colors duration-300 text-lg disabled:cursor-not-allowed",
|
||||
`bg-ielts-${module}/70 border border-ielts-${module} hover:bg-ielts-${module} disabled:bg-ielts-${module}/40`,
|
||||
className
|
||||
)}
|
||||
onClick={loading ? () => { } : () => generateFnc(sectionId)}
|
||||
disabled={loading}
|
||||
onClick={loading ? () => { } : () => generateFnc(levelId ? levelId : sectionId)}
|
||||
>
|
||||
{loading ? (
|
||||
<div key={`section-${sectionId}`} className="flex items-center justify-center">
|
||||
@@ -43,6 +56,6 @@ const GenerateBtn: React.FC<Props> = ({module, sectionId, genType, generateFnc,
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default GenerateBtn;
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
import { Module } from "@/interfaces";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import Dropdown from "./SettingsDropdown";
|
||||
import { LevelSectionSettings } from "@/stores/examEditor/types";
|
||||
|
||||
interface Props {
|
||||
module: Module;
|
||||
sectionId: number;
|
||||
localSettings: LevelSectionSettings;
|
||||
updateLocalAndScheduleGlobal: (updates: Partial<LevelSectionSettings>, schedule?: boolean) => void;
|
||||
}
|
||||
|
||||
const SectionPicker: React.FC<Props> = ({
|
||||
module,
|
||||
sectionId,
|
||||
localSettings,
|
||||
updateLocalAndScheduleGlobal
|
||||
}) => {
|
||||
const { dispatch } = useExamEditorStore();
|
||||
const [selectedValue, setSelectedValue] = React.useState<number | undefined>(undefined);
|
||||
|
||||
const sectionState = useExamEditorStore(state =>
|
||||
state.modules["level"].sections.find((s) => s.sectionId === sectionId)
|
||||
);
|
||||
|
||||
if (sectionState === undefined) return null;
|
||||
|
||||
const { readingSection, listeningSection } = sectionState;
|
||||
const currentValue = selectedValue ?? (module === "reading" ? readingSection : listeningSection);
|
||||
const options = module === "reading" ? [1, 2, 3] : [1, 2, 3, 4];
|
||||
const openPicker = module === "reading" ? "isReadingPickerOpen" : "isListeningPickerOpen";
|
||||
|
||||
const handleSectionChange = (value: number) => {
|
||||
const newValue = currentValue === value ? undefined : value;
|
||||
setSelectedValue(newValue);
|
||||
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||
payload: {
|
||||
sectionId,
|
||||
module: "level",
|
||||
field: module === "reading" ? "readingSection" : "listeningSection",
|
||||
value: newValue
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getTitle = () => {
|
||||
const section = module === "reading" ? "Passage" : "Section";
|
||||
if (!currentValue) return `Choose a ${section}`;
|
||||
return `${section} ${currentValue}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
title={getTitle()}
|
||||
module={module}
|
||||
open={localSettings[openPicker]}
|
||||
setIsOpen={(isOpen: boolean) =>
|
||||
updateLocalAndScheduleGlobal({ [openPicker]: isOpen }, false)
|
||||
}
|
||||
contentWrapperClassName={`pt-6 px-4 bg-gray-200 rounded-b-lg shadow-md transition-all duration-300 ease-in-out border border-ielts-${module}`}
|
||||
>
|
||||
<div className="space-y-2 pt-3 pb-3 px-2 border border-gray-200 rounded-lg shadow-inner">
|
||||
{options.map((num) => (
|
||||
<label
|
||||
key={num}
|
||||
className={`
|
||||
flex items-center space-x-3 font-semibold cursor-pointer p-2 rounded
|
||||
transition-colors duration-200
|
||||
${currentValue === num
|
||||
? `bg-ielts-${module}/90 text-white`
|
||||
: `hover:bg-ielts-${module}/70 text-gray-700`}
|
||||
`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleSectionChange(num);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={currentValue === num}
|
||||
onChange={() => {}}
|
||||
className={`
|
||||
h-5 w-5 cursor-pointer
|
||||
accent-ielts-${module}
|
||||
`}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>
|
||||
{module === "reading" ? `Passage ${num}` : `Section ${num}`}
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionPicker;
|
||||
@@ -10,9 +10,10 @@ interface Props {
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
children: ReactNode;
|
||||
center?: boolean;
|
||||
contentWrapperClassName?: string;
|
||||
}
|
||||
|
||||
const SettingsDropdown: React.FC<Props> = ({ module, title, open, setIsOpen, children, disabled = false, center = false}) => {
|
||||
const SettingsDropdown: React.FC<Props> = ({ module, title, open, setIsOpen, children, contentWrapperClassName = '', disabled = false, center = false}) => {
|
||||
return (
|
||||
<Dropdown
|
||||
title={title}
|
||||
@@ -21,7 +22,7 @@ const SettingsDropdown: React.FC<Props> = ({ module, title, open, setIsOpen, chi
|
||||
`bg-ielts-${module}/70 border border-ielts-${module} hover:bg-ielts-${module} disabled:bg-ielts-${module}/30`,
|
||||
open ? "rounded-t-lg" : "rounded-lg"
|
||||
)}
|
||||
contentWrapperClassName={`pt-6 px-2 bg-white rounded-b-lg shadow-md transition-all duration-300 ease-in-out ${center ? "flex justify-center" : ""}`}
|
||||
contentWrapperClassName={`pt-6 px-2 bg-white rounded-b-lg shadow-md transition-all duration-300 ease-in-out ${center ? "flex justify-center" : ""} ${contentWrapperClassName}`}
|
||||
open={open}
|
||||
setIsOpen={setIsOpen}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import { Exercise, LevelExam, LevelPart } from "@/interfaces/exam";
|
||||
import { Exercise, InteractiveSpeakingExercise, LevelExam, LevelPart, SpeakingExercise } from "@/interfaces/exam";
|
||||
import SettingsEditor from ".";
|
||||
import Option from "@/interfaces/option";
|
||||
import Dropdown from "@/components/Dropdown";
|
||||
import clsx from "clsx";
|
||||
import ExercisePicker from "../Shared/ExercisePicker";
|
||||
import ExercisePicker from "../ExercisePicker";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import useSettingsState from "../Hooks/useSettingsState";
|
||||
import { SectionSettings } from "@/stores/examEditor/types";
|
||||
import { LevelSectionSettings, SectionSettings } from "@/stores/examEditor/types";
|
||||
import { toast } from "react-toastify";
|
||||
import axios from "axios";
|
||||
import { playSound } from "@/utils/sound";
|
||||
import { useRouter } from "next/router";
|
||||
import { usePersistentExamStore } from "@/stores/examStore";
|
||||
import openDetachedTab from "@/utils/popout";
|
||||
import ListeningComponents from "./listening/components";
|
||||
import ReadingComponents from "./reading/components";
|
||||
import WritingComponents from "./writing/components";
|
||||
import SpeakingComponents from "./speaking/components";
|
||||
import SectionPicker from "./Shared/SectionPicker";
|
||||
import SettingsDropdown from "./Shared/SettingsDropdown";
|
||||
|
||||
|
||||
const LevelSettings: React.FC = () => {
|
||||
@@ -26,8 +32,8 @@ const LevelSettings: React.FC = () => {
|
||||
setQuestionIndex,
|
||||
setBgColor,
|
||||
} = usePersistentExamStore();
|
||||
|
||||
const {currentModule, title } = useExamEditorStore();
|
||||
|
||||
const { currentModule, title } = useExamEditorStore();
|
||||
const {
|
||||
focusedSection,
|
||||
difficulty,
|
||||
@@ -36,15 +42,18 @@ const LevelSettings: React.FC = () => {
|
||||
isPrivate,
|
||||
} = useExamEditorStore(state => state.modules[currentModule]);
|
||||
|
||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<SectionSettings>(
|
||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<LevelSectionSettings>(
|
||||
currentModule,
|
||||
focusedSection
|
||||
);
|
||||
|
||||
const section = sections.find((section) => section.sectionId == focusedSection);
|
||||
const focusedExercise = section?.focusedExercise;
|
||||
if (section === undefined) return <></>;
|
||||
|
||||
const currentSection = section.state as LevelPart;
|
||||
const readingSection = section.readingSection;
|
||||
const listeningSection = section.listeningSection;
|
||||
|
||||
const canPreview = currentSection.exercises.length > 0;
|
||||
|
||||
@@ -105,6 +114,8 @@ const LevelSettings: React.FC = () => {
|
||||
openDetachedTab("popout?type=Exam&module=level", router)
|
||||
}
|
||||
|
||||
const speakingExercise = focusedExercise === undefined ? undefined : currentSection.exercises.find((ex) => ex.id === focusedExercise.id) as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
|
||||
return (
|
||||
<SettingsEditor
|
||||
sectionLabel={`Part ${focusedSection}`}
|
||||
@@ -117,26 +128,149 @@ const LevelSettings: React.FC = () => {
|
||||
submitModule={submitLevel}
|
||||
>
|
||||
<div>
|
||||
<Dropdown title="Add Exercises" className={
|
||||
<Dropdown title="Add Level Exercises" className={
|
||||
clsx(
|
||||
"w-full font-semibold flex justify-between items-center p-4 bg-gradient-to-r border",
|
||||
"bg-ielts-level/70 border-ielts-level hover:bg-ielts-level",
|
||||
"text-white shadow-md transition-all duration-300",
|
||||
localSettings.isExerciseDropdownOpen ? "rounded-t-lg" : "rounded-lg"
|
||||
localSettings.isLevelDropdownOpen ? "rounded-t-lg" : "rounded-lg"
|
||||
)
|
||||
}
|
||||
contentWrapperClassName={"pt-4 px-2 bg-white rounded-b-lg shadow-md transition-all duration-300 ease-in-out"}
|
||||
open={localSettings.isExerciseDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen }, false)}
|
||||
open={localSettings.isLevelDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isLevelDropdownOpen: isOpen }, false)}
|
||||
>
|
||||
<ExercisePicker
|
||||
module="level"
|
||||
sectionId={focusedSection}
|
||||
difficulty={difficulty}
|
||||
/>
|
||||
module="level"
|
||||
sectionId={focusedSection}
|
||||
difficulty={difficulty}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</SettingsEditor>
|
||||
<div>
|
||||
<Dropdown title="Add Reading Exercises" className={
|
||||
clsx(
|
||||
"w-full font-semibold flex justify-between items-center p-4 bg-gradient-to-r border",
|
||||
"bg-ielts-reading/70 border-ielts-reading hover:bg-ielts-reading",
|
||||
"text-white shadow-md transition-all duration-300",
|
||||
localSettings.isReadingDropdownOpen ? "rounded-t-lg" : "rounded-lg"
|
||||
)
|
||||
}
|
||||
contentWrapperClassName={"pt-4 px-2 bg-white rounded-b-lg shadow-md transition-all duration-300 ease-in-out"}
|
||||
open={localSettings.isReadingDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isReadingDropdownOpen: isOpen }, false)}
|
||||
>
|
||||
<div className="space-y-2 px-2 pb-2">
|
||||
<SectionPicker {...{ module: "reading", sectionId: focusedSection, localSettings, updateLocalAndScheduleGlobal }} />
|
||||
<ReadingComponents
|
||||
{...{ localSettings, updateLocalAndScheduleGlobal, currentSection, generatePassageDisabled: readingSection === undefined, levelId: readingSection, level: true }}
|
||||
/>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div>
|
||||
<Dropdown title="Add Listening Exercises" className={
|
||||
clsx(
|
||||
"w-full font-semibold flex justify-between items-center p-4 bg-gradient-to-r border",
|
||||
"bg-ielts-listening/70 border-ielts-listening hover:bg-ielts-listening",
|
||||
"text-white shadow-md transition-all duration-300",
|
||||
localSettings.isListeningDropdownOpen ? "rounded-t-lg" : "rounded-lg"
|
||||
)
|
||||
}
|
||||
contentWrapperClassName={"pt-4 px-2 bg-white rounded-b-lg shadow-md transition-all duration-300 ease-in-out"}
|
||||
open={localSettings.isListeningDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isListeningDropdownOpen: isOpen }, false)}
|
||||
>
|
||||
<div className="space-y-2 px-2 pb-2">
|
||||
<SectionPicker {...{ module: "listening", sectionId: focusedSection, localSettings, updateLocalAndScheduleGlobal }} />
|
||||
<ListeningComponents
|
||||
{...{ localSettings, updateLocalAndScheduleGlobal, currentSection, audioContextDisabled: listeningSection === undefined, levelId: listeningSection, level: true }}
|
||||
/>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div>
|
||||
<Dropdown title="Add Writing Exercises" className={
|
||||
clsx(
|
||||
"w-full font-semibold flex justify-between items-center p-4 bg-gradient-to-r border",
|
||||
"bg-ielts-writing/70 border-ielts-writing hover:bg-ielts-writing",
|
||||
"text-white shadow-md transition-all duration-300",
|
||||
localSettings.isWritingDropdownOpen ? "rounded-t-lg" : "rounded-lg"
|
||||
)
|
||||
}
|
||||
contentWrapperClassName={"pt-4 px-2 bg-white rounded-b-lg shadow-md transition-all duration-300 ease-in-out"}
|
||||
open={localSettings.isWritingDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isWritingDropdownOpen: isOpen }, false)}
|
||||
>
|
||||
<ExercisePicker
|
||||
module="writing"
|
||||
sectionId={focusedSection}
|
||||
difficulty={difficulty}
|
||||
levelSectionId={focusedSection}
|
||||
level
|
||||
/>
|
||||
</Dropdown>
|
||||
</div >
|
||||
{/*
|
||||
<div>
|
||||
<Dropdown title="Add Speaking Exercises" className={
|
||||
clsx(
|
||||
"w-full font-semibold flex justify-between items-center p-4 bg-gradient-to-r border",
|
||||
"bg-ielts-speaking/70 border-ielts-speaking hover:bg-ielts-speaking",
|
||||
"text-white shadow-md transition-all duration-300",
|
||||
localSettings.isSpeakingDropdownOpen ? "rounded-t-lg" : "rounded-lg"
|
||||
)
|
||||
}
|
||||
contentWrapperClassName={"pt-4 px-2 bg-white rounded-b-lg shadow-md transition-all duration-300 ease-in-out"}
|
||||
open={localSettings.isSpeakingDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isSpeakingDropdownOpen: isOpen }, false)}
|
||||
>
|
||||
<Dropdown title="Exercises" className={
|
||||
clsx(
|
||||
"w-full font-semibold flex justify-between items-center p-4 bg-gradient-to-r border",
|
||||
"bg-ielts-speaking/70 border-ielts-speaking hover:bg-ielts-speaking",
|
||||
"text-white shadow-md transition-all duration-300",
|
||||
localSettings.isSpeakingDropdownOpen ? "rounded-t-lg" : "rounded-lg"
|
||||
)
|
||||
}
|
||||
contentWrapperClassName={"pt-4 px-2 bg-white rounded-b-lg shadow-md transition-all duration-300 ease-in-out border border-ielts-speaking"}
|
||||
open={localSettings.isSpeakingDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isSpeakingDropdownOpen: isOpen }, false)}
|
||||
>
|
||||
<div className="space-y-2 px-2 pb-2">
|
||||
<ExercisePicker
|
||||
module="speaking"
|
||||
sectionId={focusedSection}
|
||||
difficulty={difficulty}
|
||||
levelSectionId={focusedSection}
|
||||
level
|
||||
/>
|
||||
</div>
|
||||
</Dropdown>
|
||||
|
||||
{speakingExercise !== undefined &&
|
||||
<Dropdown title={`Configure Speaking Exercise ${focusedExercise?.questionId}`} className={
|
||||
clsx(
|
||||
"w-full font-semibold flex justify-between items-center p-4 bg-gradient-to-r border",
|
||||
"bg-ielts-speaking/70 border-ielts-speaking hover:bg-ielts-speaking",
|
||||
"text-white shadow-md transition-all duration-300",
|
||||
localSettings.isSpeakingDropdownOpen ? "rounded-t-lg" : "rounded-lg"
|
||||
)
|
||||
}
|
||||
contentWrapperClassName={"pt-4 px-2 bg-white rounded-b-lg shadow-md transition-all duration-300 ease-in-out border border-ielts-speaking"}
|
||||
open={localSettings.isSpeakingDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isSpeakingDropdownOpen: isOpen }, false)}
|
||||
>
|
||||
<SpeakingComponents
|
||||
{...{ localSettings, updateLocalAndScheduleGlobal, section: speakingExercise }}
|
||||
level
|
||||
/>
|
||||
</Dropdown>
|
||||
}
|
||||
</Dropdown>
|
||||
</div>
|
||||
*/}
|
||||
</SettingsEditor >
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
import Dropdown from "../Shared/SettingsDropdown";
|
||||
import ExercisePicker from "../../ExercisePicker";
|
||||
import GenerateBtn from "../Shared/GenerateBtn";
|
||||
import { useCallback } from "react";
|
||||
import { generate } from "../Shared/Generate";
|
||||
import { LevelSectionSettings, ListeningSectionSettings } from "@/stores/examEditor/types";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import { LevelPart, ListeningPart } from "@/interfaces/exam";
|
||||
import Input from "@/components/Low/Input";
|
||||
import axios from "axios";
|
||||
import { toast } from "react-toastify";
|
||||
import { playSound } from "@/utils/sound";
|
||||
|
||||
interface Props {
|
||||
localSettings: ListeningSectionSettings | LevelSectionSettings;
|
||||
updateLocalAndScheduleGlobal: (updates: Partial<ListeningSectionSettings | LevelSectionSettings>, schedule?: boolean) => void;
|
||||
currentSection: ListeningPart | LevelPart;
|
||||
audioContextDisabled?: boolean;
|
||||
levelId?: number;
|
||||
level?: boolean;
|
||||
}
|
||||
|
||||
const ListeningComponents: React.FC<Props> = ({ currentSection, localSettings, updateLocalAndScheduleGlobal, levelId, level = false, audioContextDisabled = false }) => {
|
||||
const { currentModule, dispatch, modules } = useExamEditorStore();
|
||||
const {
|
||||
focusedSection,
|
||||
difficulty,
|
||||
} = useExamEditorStore(state => state.modules[currentModule]);
|
||||
|
||||
const generateScript = useCallback(() => {
|
||||
generate(
|
||||
levelId ? levelId : focusedSection,
|
||||
"listening",
|
||||
"listeningScript",
|
||||
{
|
||||
method: 'GET',
|
||||
queryParams: {
|
||||
difficulty,
|
||||
...(localSettings.listeningTopic && { topic: localSettings.listeningTopic })
|
||||
}
|
||||
},
|
||||
(data: any) => [{
|
||||
script: data.dialog
|
||||
}],
|
||||
level ? focusedSection : undefined,
|
||||
level
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localSettings.listeningTopic, difficulty, focusedSection, levelId, level]);
|
||||
|
||||
const onTopicChange = useCallback((listeningTopic: string) => {
|
||||
updateLocalAndScheduleGlobal({ listeningTopic });
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
|
||||
const generateAudio = useCallback(async (sectionId: number) => {
|
||||
let body: any;
|
||||
if ([1, 3].includes(levelId ? levelId : focusedSection)) {
|
||||
body = { conversation: currentSection.script }
|
||||
} else {
|
||||
body = { monologue: currentSection.script }
|
||||
}
|
||||
|
||||
try {
|
||||
if (level) {
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
||||
sectionId, module: "level", field: "levelGenerating", value:
|
||||
[...modules["level"].sections.find((s) => s.sectionId === sectionId)!.levelGenerating, "audio"]
|
||||
}
|
||||
});
|
||||
} else {
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "generating", value: "audio" } });
|
||||
}
|
||||
|
||||
const response = await axios.post(
|
||||
'/api/exam/media/listening',
|
||||
body,
|
||||
{
|
||||
responseType: 'arraybuffer',
|
||||
headers: {
|
||||
'Accept': 'audio/mpeg'
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const blob = new Blob([response.data], { type: 'audio/mpeg' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
if (currentSection.audio?.source) {
|
||||
URL.revokeObjectURL(currentSection.audio?.source)
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_STATE",
|
||||
payload: {
|
||||
sectionId,
|
||||
module: level ? "level" : "listening",
|
||||
update: {
|
||||
audio: {
|
||||
source: url,
|
||||
repeatableTimes: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
playSound("check");
|
||||
toast.success('Audio generated successfully!');
|
||||
} catch (error: any) {
|
||||
toast.error('Failed to generate audio');
|
||||
} finally {
|
||||
if (level) {
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
||||
sectionId, module: "level", field: "levelGenerating", value:
|
||||
[...modules["level"].sections.find((s) => s.sectionId === sectionId)!.levelGenerating.filter(g => g !== "audio")]
|
||||
}
|
||||
});
|
||||
} else {
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "generating", value: undefined } });
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentSection?.script, dispatch, level, levelId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
title="Audio Context"
|
||||
module="listening"
|
||||
open={localSettings.isAudioContextOpen}
|
||||
disabled={audioContextDisabled}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isAudioContextOpen: isOpen }, false)}
|
||||
contentWrapperClassName={level ? `border border-ielts-listening` : ''}
|
||||
>
|
||||
<div className="flex flex-row gap-2 items-center px-2 pb-4">
|
||||
<div className="flex flex-col flex-grow gap-4 px-2">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Topic (Optional)</label>
|
||||
<Input
|
||||
key={`section-${focusedSection}`}
|
||||
type="text"
|
||||
placeholder="Topic"
|
||||
name="category"
|
||||
onChange={onTopicChange}
|
||||
roundness="full"
|
||||
value={localSettings.listeningTopic}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex self-end h-16 mb-1">
|
||||
<GenerateBtn
|
||||
module="listening"
|
||||
genType="listeningScript"
|
||||
sectionId={focusedSection}
|
||||
generateFnc={generateScript}
|
||||
level={level}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
title="Add Exercises"
|
||||
module="listening"
|
||||
open={localSettings.isListeningTopicOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isListeningTopicOpen: isOpen }, false)}
|
||||
disabled={currentSection === undefined || currentSection.script === undefined && currentSection.audio === undefined}
|
||||
contentWrapperClassName={level ? `border border-ielts-listening` : ''}
|
||||
>
|
||||
<ExercisePicker
|
||||
module="listening"
|
||||
sectionId={levelId !== undefined ? levelId : focusedSection}
|
||||
difficulty={difficulty}
|
||||
extraArgs={{ script: currentSection === undefined || currentSection.audio === undefined ? "" : currentSection.script }}
|
||||
levelSectionId={focusedSection}
|
||||
level={level}
|
||||
/>
|
||||
</Dropdown>
|
||||
|
||||
<Dropdown
|
||||
title="Generate Audio"
|
||||
module="listening"
|
||||
open={localSettings.isAudioGenerationOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isAudioGenerationOpen: isOpen }, false)}
|
||||
disabled={currentSection === undefined || currentSection.script === undefined && currentSection.audio === undefined || currentSection.exercises.length === 0}
|
||||
contentWrapperClassName={level ? `border border-ielts-listening` : ''}
|
||||
>
|
||||
<div className="flex flex-row items-center text-mti-gray-dim justify-center mb-4">
|
||||
<span className="bg-gray-100 px-3.5 py-2.5 rounded-l-lg border border-r-0 border-gray-300">
|
||||
Generate audio recording for this section
|
||||
</span>
|
||||
<div className="-ml-2.5">
|
||||
<GenerateBtn
|
||||
module="listening"
|
||||
genType="audio"
|
||||
sectionId={levelId ? levelId : focusedSection}
|
||||
generateFnc={generateAudio}
|
||||
levelId={focusedSection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListeningComponents;
|
||||
@@ -1,13 +1,13 @@
|
||||
import Dropdown from "./Shared/SettingsDropdown";
|
||||
import ExercisePicker from "../Shared/ExercisePicker";
|
||||
import SettingsEditor from ".";
|
||||
import GenerateBtn from "./Shared/GenerateBtn";
|
||||
import Dropdown from "../Shared/SettingsDropdown";
|
||||
import ExercisePicker from "../../ExercisePicker";
|
||||
import SettingsEditor from "..";
|
||||
import GenerateBtn from "../Shared/GenerateBtn";
|
||||
import { useCallback, useState } from "react";
|
||||
import { generate } from "./Shared/Generate";
|
||||
import { Generating, ListeningSectionSettings } from "@/stores/examEditor/types";
|
||||
import { generate } from "../Shared/Generate";
|
||||
import { Generating, LevelSectionSettings, ListeningSectionSettings } from "@/stores/examEditor/types";
|
||||
import Option from "@/interfaces/option";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import useSettingsState from "../Hooks/useSettingsState";
|
||||
import useSettingsState from "../../Hooks/useSettingsState";
|
||||
import { ListeningExam, ListeningPart } from "@/interfaces/exam";
|
||||
import Input from "@/components/Low/Input";
|
||||
import openDetachedTab from "@/utils/popout";
|
||||
@@ -16,10 +16,11 @@ import axios from "axios";
|
||||
import { usePersistentExamStore } from "@/stores/examStore";
|
||||
import { playSound } from "@/utils/sound";
|
||||
import { toast } from "react-toastify";
|
||||
import ListeningComponents from "./components";
|
||||
|
||||
const ListeningSettings: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { currentModule, title, dispatch } = useExamEditorStore();
|
||||
const { currentModule, title } = useExamEditorStore();
|
||||
const {
|
||||
focusedSection,
|
||||
difficulty,
|
||||
@@ -47,45 +48,21 @@ const ListeningSettings: React.FC = () => {
|
||||
{
|
||||
label: "Preset: Listening Section 1",
|
||||
value: "Welcome to {part} of the {label}. You will hear a conversation between two people in an everyday social context. This may include topics such as making arrangements or bookings, inquiring about services, or handling basic transactions."
|
||||
},
|
||||
{
|
||||
},
|
||||
{
|
||||
label: "Preset: Listening Section 2",
|
||||
value: "Welcome to {part} of the {label}. You will hear a monologue set in an everyday social context. This may include a speech about local facilities, arrangements for social occasions, or general announcements."
|
||||
},
|
||||
{
|
||||
},
|
||||
{
|
||||
label: "Preset: Listening Section 3",
|
||||
value: "Welcome to {part} of the {label}. You will hear a conversation between up to four people in an educational or training context. This may include discussions about assignments, research projects, or course requirements."
|
||||
},
|
||||
{
|
||||
},
|
||||
{
|
||||
label: "Preset: Listening Section 4",
|
||||
value: "Welcome to {part} of the {label}. You will hear an academic lecture or talk on a specific subject."
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
const generateScript = useCallback(() => {
|
||||
generate(
|
||||
focusedSection,
|
||||
currentModule,
|
||||
"context",
|
||||
{
|
||||
method: 'GET',
|
||||
queryParams: {
|
||||
difficulty,
|
||||
...(localSettings.topic && { topic: localSettings.topic })
|
||||
}
|
||||
},
|
||||
(data: any) => [{
|
||||
script: data.dialog
|
||||
}]
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localSettings.topic, difficulty, focusedSection]);
|
||||
|
||||
const onTopicChange = useCallback((topic: string) => {
|
||||
updateLocalAndScheduleGlobal({ topic });
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
const submitListening = async () => {
|
||||
if (title === "") {
|
||||
toast.error("Enter a title for the exam!");
|
||||
@@ -187,56 +164,6 @@ const ListeningSettings: React.FC = () => {
|
||||
openDetachedTab("popout?type=Exam&module=listening", router)
|
||||
}
|
||||
|
||||
const generateAudio = useCallback(async (sectionId: number) => {
|
||||
let body: any;
|
||||
if ([1, 3].includes(sectionId)) {
|
||||
body = { conversation: currentSection.script }
|
||||
} else {
|
||||
body = { monologue: currentSection.script }
|
||||
}
|
||||
|
||||
try {
|
||||
dispatch({type: "UPDATE_SECTION_SINGLE_FIELD", payload: {sectionId, module: currentModule, field: "generating", value: "media"}});
|
||||
const response = await axios.post(
|
||||
'/api/exam/media/listening',
|
||||
body,
|
||||
{
|
||||
responseType: 'arraybuffer',
|
||||
headers: {
|
||||
'Accept': 'audio/mpeg'
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const blob = new Blob([response.data], { type: 'audio/mpeg' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
if (currentSection.audio?.source) {
|
||||
URL.revokeObjectURL(currentSection.audio?.source)
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_STATE",
|
||||
payload: {
|
||||
sectionId,
|
||||
update: {
|
||||
audio: {
|
||||
source: url,
|
||||
repeatableTimes: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
toast.success('Audio generated successfully!');
|
||||
} catch (error: any) {
|
||||
toast.error('Failed to generate audio');
|
||||
} finally {
|
||||
dispatch({type: "UPDATE_SECTION_SINGLE_FIELD", payload: {sectionId, module: currentModule, field: "generating", value: undefined}});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentSection?.script, dispatch]);
|
||||
|
||||
const canPreview = sections.some(
|
||||
(s) => (s.state as ListeningPart).exercises && (s.state as ListeningPart).exercises.length > 0
|
||||
@@ -259,65 +186,9 @@ const ListeningSettings: React.FC = () => {
|
||||
preview={preview}
|
||||
submitModule={submitListening}
|
||||
>
|
||||
<Dropdown
|
||||
title="Audio Context"
|
||||
module={currentModule}
|
||||
open={localSettings.isAudioContextOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isAudioContextOpen: isOpen }, false)}
|
||||
>
|
||||
<div className="flex flex-row gap-2 items-center px-2 pb-4">
|
||||
<div className="flex flex-col flex-grow gap-4 px-2">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Topic (Optional)</label>
|
||||
<Input
|
||||
key={`section-${focusedSection}`}
|
||||
type="text"
|
||||
placeholder="Topic"
|
||||
name="category"
|
||||
onChange={onTopicChange}
|
||||
roundness="full"
|
||||
value={localSettings.topic}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex self-end h-16 mb-1">
|
||||
<GenerateBtn
|
||||
module={currentModule}
|
||||
genType="context"
|
||||
sectionId={focusedSection}
|
||||
generateFnc={generateScript}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
title="Add Exercises"
|
||||
module={currentModule}
|
||||
open={localSettings.isExerciseDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen }, false)}
|
||||
disabled={currentSection === undefined || currentSection.script === undefined && currentSection.audio === undefined}
|
||||
>
|
||||
<ExercisePicker
|
||||
module="listening"
|
||||
sectionId={focusedSection}
|
||||
difficulty={difficulty}
|
||||
/>
|
||||
</Dropdown>
|
||||
|
||||
<Dropdown
|
||||
title="Generate Audio"
|
||||
module={currentModule}
|
||||
open={localSettings.isAudioGenerationOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isAudioGenerationOpen: isOpen }, false)}
|
||||
disabled={currentSection === undefined || currentSection.script === undefined && currentSection.audio === undefined || currentSection.exercises.length === 0}
|
||||
center
|
||||
>
|
||||
<GenerateBtn
|
||||
module={currentModule}
|
||||
genType="media"
|
||||
sectionId={focusedSection}
|
||||
generateFnc={generateAudio}
|
||||
className="mb-4"
|
||||
/>
|
||||
</Dropdown>
|
||||
<ListeningComponents
|
||||
{...{ localSettings, updateLocalAndScheduleGlobal, currentSection }}
|
||||
/>
|
||||
</SettingsEditor>
|
||||
);
|
||||
};
|
||||
108
src/components/ExamEditor/SettingsEditor/reading/components.tsx
Normal file
108
src/components/ExamEditor/SettingsEditor/reading/components.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import React, { useCallback } from "react";
|
||||
import Dropdown from "../Shared/SettingsDropdown";
|
||||
import Input from "@/components/Low/Input";
|
||||
import ExercisePicker from "../../ExercisePicker";
|
||||
import { generate } from "../Shared/Generate";
|
||||
import GenerateBtn from "../Shared/GenerateBtn";
|
||||
import { LevelPart, ReadingPart } from "@/interfaces/exam";
|
||||
import { LevelSectionSettings, ReadingSectionSettings } from "@/stores/examEditor/types";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
|
||||
interface Props {
|
||||
localSettings: ReadingSectionSettings | LevelSectionSettings;
|
||||
updateLocalAndScheduleGlobal: (updates: Partial<ReadingSectionSettings | LevelSectionSettings>, schedule?: boolean) => void;
|
||||
currentSection: ReadingPart | LevelPart;
|
||||
generatePassageDisabled?: boolean;
|
||||
levelId?: number;
|
||||
level?: boolean;
|
||||
}
|
||||
|
||||
const ReadingComponents: React.FC<Props> = ({localSettings, updateLocalAndScheduleGlobal, currentSection, levelId, level = false, generatePassageDisabled = false}) => {
|
||||
const { currentModule } = useExamEditorStore();
|
||||
const {
|
||||
focusedSection,
|
||||
difficulty,
|
||||
} = useExamEditorStore(state => state.modules[currentModule]);
|
||||
|
||||
const generatePassage = useCallback(() => {
|
||||
generate(
|
||||
levelId ? levelId : focusedSection,
|
||||
"reading",
|
||||
"passage",
|
||||
{
|
||||
method: 'GET',
|
||||
queryParams: {
|
||||
difficulty,
|
||||
...(localSettings.readingTopic && { topic: localSettings.readingTopic })
|
||||
}
|
||||
},
|
||||
(data: any) => [{
|
||||
title: data.title,
|
||||
text: data.text
|
||||
}],
|
||||
level ? focusedSection : undefined,
|
||||
level
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localSettings.readingTopic, difficulty, focusedSection, levelId]);
|
||||
|
||||
const onTopicChange = useCallback((readingTopic: string) => {
|
||||
updateLocalAndScheduleGlobal({ readingTopic });
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
title="Generate Passage"
|
||||
module="reading"
|
||||
open={localSettings.isPassageOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isPassageOpen: isOpen }, false)}
|
||||
contentWrapperClassName={level ? `border border-ielts-reading`: ''}
|
||||
disabled={generatePassageDisabled}
|
||||
>
|
||||
<div className="flex flex-row gap-2 items-center px-2 pb-4">
|
||||
<div className="flex flex-col flex-grow gap-4 px-2">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Topic (Optional)</label>
|
||||
<Input
|
||||
key={`section-${focusedSection}`}
|
||||
type="text"
|
||||
placeholder="Topic"
|
||||
name="category"
|
||||
onChange={onTopicChange}
|
||||
roundness="full"
|
||||
value={localSettings.readingTopic}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex self-end h-16 mb-1">
|
||||
<GenerateBtn
|
||||
module="reading"
|
||||
genType="passage"
|
||||
sectionId={focusedSection}
|
||||
generateFnc={generatePassage}
|
||||
level={level}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
title="Add Exercises"
|
||||
module="reading"
|
||||
open={localSettings.isReadingTopicOpean}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isReadingTopicOpean: isOpen })}
|
||||
contentWrapperClassName={level ? `border border-ielts-reading`: ''}
|
||||
disabled={currentSection === undefined || currentSection.text === undefined || currentSection.text.content === "" || currentSection.text.title === ""}
|
||||
>
|
||||
<ExercisePicker
|
||||
module="reading"
|
||||
sectionId={levelId !== undefined ? levelId : focusedSection}
|
||||
difficulty={difficulty}
|
||||
extraArgs={{ text: currentSection === undefined || currentSection.text === undefined ? "" : currentSection.text.content }}
|
||||
levelSectionId={focusedSection}
|
||||
level={level}
|
||||
/>
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReadingComponents;
|
||||
@@ -1,12 +1,7 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import SettingsEditor from ".";
|
||||
import React from "react";
|
||||
import SettingsEditor from "..";
|
||||
import Option from "@/interfaces/option";
|
||||
import Dropdown from "./Shared/SettingsDropdown";
|
||||
import Input from "@/components/Low/Input";
|
||||
import ExercisePicker from "../Shared/ExercisePicker";
|
||||
import { generate } from "./Shared/Generate";
|
||||
import GenerateBtn from "./Shared/GenerateBtn";
|
||||
import useSettingsState from "../Hooks/useSettingsState";
|
||||
import useSettingsState from "../../Hooks/useSettingsState";
|
||||
import { ReadingExam, ReadingPart } from "@/interfaces/exam";
|
||||
import { ReadingSectionSettings } from "@/stores/examEditor/types";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
@@ -16,6 +11,7 @@ import { usePersistentExamStore } from "@/stores/examStore";
|
||||
import axios from "axios";
|
||||
import { playSound } from "@/utils/sound";
|
||||
import { toast } from "react-toastify";
|
||||
import ReadingComponents from "./components";
|
||||
|
||||
const ReadingSettings: React.FC = () => {
|
||||
const router = useRouter();
|
||||
@@ -60,32 +56,6 @@ const ReadingSettings: React.FC = () => {
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
const generatePassage = useCallback(() => {
|
||||
generate(
|
||||
focusedSection,
|
||||
currentModule,
|
||||
"context",
|
||||
{
|
||||
method: 'GET',
|
||||
queryParams: {
|
||||
difficulty,
|
||||
...(localSettings.topic && { topic: localSettings.topic })
|
||||
}
|
||||
},
|
||||
(data: any) => [{
|
||||
title: data.title,
|
||||
text: data.text
|
||||
}]
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localSettings.topic, difficulty, focusedSection]);
|
||||
|
||||
const onTopicChange = useCallback((topic: string) => {
|
||||
updateLocalAndScheduleGlobal({ topic });
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
|
||||
const canPreviewOrSubmit = sections.some(
|
||||
(s) => (s.state as ReadingPart).exercises && (s.state as ReadingPart).exercises.length > 0
|
||||
);
|
||||
@@ -161,49 +131,9 @@ const ReadingSettings: React.FC = () => {
|
||||
canSubmit={canPreviewOrSubmit}
|
||||
submitModule={submitReading}
|
||||
>
|
||||
<Dropdown
|
||||
title="Generate Passage"
|
||||
module={currentModule}
|
||||
open={localSettings.isPassageOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isPassageOpen: isOpen }, false)}
|
||||
>
|
||||
<div className="flex flex-row gap-2 items-center px-2 pb-4">
|
||||
<div className="flex flex-col flex-grow gap-4 px-2">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Topic (Optional)</label>
|
||||
<Input
|
||||
key={`section-${focusedSection}`}
|
||||
type="text"
|
||||
placeholder="Topic"
|
||||
name="category"
|
||||
onChange={onTopicChange}
|
||||
roundness="full"
|
||||
value={localSettings.topic}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex self-end h-16 mb-1">
|
||||
<GenerateBtn
|
||||
module={currentModule}
|
||||
genType="context"
|
||||
sectionId={focusedSection}
|
||||
generateFnc={generatePassage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
title="Add Exercises"
|
||||
module={currentModule}
|
||||
open={localSettings.isExerciseDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen })}
|
||||
disabled={currentSection === undefined || currentSection.text === undefined || currentSection.text.content === "" || currentSection.text.title === ""}
|
||||
>
|
||||
<ExercisePicker
|
||||
module="reading"
|
||||
sectionId={focusedSection}
|
||||
difficulty={difficulty}
|
||||
extraArgs={{ text: currentSection === undefined ? "" : currentSection.text.content }}
|
||||
/>
|
||||
</Dropdown>
|
||||
<ReadingComponents
|
||||
{...{ localSettings, updateLocalAndScheduleGlobal, currentSection }}
|
||||
/>
|
||||
</SettingsEditor>
|
||||
);
|
||||
};
|
||||
@@ -1,481 +0,0 @@
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import useSettingsState from "../Hooks/useSettingsState";
|
||||
import { SpeakingSectionSettings } from "@/stores/examEditor/types";
|
||||
import Option from "@/interfaces/option";
|
||||
import { useCallback, useState } from "react";
|
||||
import { generate } from "./Shared/Generate";
|
||||
import SettingsEditor from ".";
|
||||
import Dropdown from "./Shared/SettingsDropdown";
|
||||
import Input from "@/components/Low/Input";
|
||||
import GenerateBtn from "./Shared/GenerateBtn";
|
||||
import clsx from "clsx";
|
||||
import { FaFemale, FaMale, FaChevronDown } from "react-icons/fa";
|
||||
import { InteractiveSpeakingExercise, SpeakingExam, SpeakingExercise } from "@/interfaces/exam";
|
||||
import { toast } from "react-toastify";
|
||||
import { generateVideos } from "./Shared/generateVideos";
|
||||
import { usePersistentExamStore } from "@/stores/examStore";
|
||||
import { useRouter } from "next/router";
|
||||
import openDetachedTab from "@/utils/popout";
|
||||
import axios from "axios";
|
||||
import { playSound } from "@/utils/sound";
|
||||
|
||||
export interface Avatar {
|
||||
name: string;
|
||||
gender: string;
|
||||
}
|
||||
|
||||
const SpeakingSettings: React.FC = () => {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
setExam,
|
||||
setExerciseIndex,
|
||||
setQuestionIndex,
|
||||
setBgColor,
|
||||
} = usePersistentExamStore();
|
||||
|
||||
const { title, currentModule, speakingAvatars, dispatch } = useExamEditorStore();
|
||||
const { focusedSection, difficulty, sections, minTimer, isPrivate } = useExamEditorStore((store) => store.modules[currentModule])
|
||||
|
||||
const section = sections.find((section) => section.sectionId == focusedSection)?.state;
|
||||
|
||||
|
||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<SpeakingSectionSettings>(
|
||||
currentModule,
|
||||
focusedSection,
|
||||
);
|
||||
|
||||
const [selectedAvatar, setSelectedAvatar] = useState<Avatar | null>(null);
|
||||
|
||||
const defaultPresets: Option[] = [
|
||||
{
|
||||
label: "Preset: Speaking Part 1",
|
||||
value: "Welcome to {part} of the {label}. You will engage in a conversation about yourself and familiar topics such as your home, family, work, studies, and interests. General questions will be asked."
|
||||
},
|
||||
{
|
||||
label: "Preset: Speaking Part 2",
|
||||
value: "Welcome to {part} of the {label}. You will be given a topic card describing a particular person, object, event, or experience."
|
||||
},
|
||||
{
|
||||
label: "Preset: Speaking Part 3",
|
||||
value: "Welcome to {part} of the {label}. You will engage in an in-depth discussion about abstract ideas and issues. The examiner will ask questions that require you to explain, analyze, and speculate about various aspects of the topic."
|
||||
}
|
||||
];
|
||||
|
||||
const generateScript = useCallback((sectionId: number) => {
|
||||
const queryParams: {
|
||||
difficulty: string;
|
||||
first_topic?: string;
|
||||
second_topic?: string;
|
||||
topic?: string;
|
||||
} = { difficulty };
|
||||
|
||||
if (sectionId === 1) {
|
||||
if (localSettings.topic) {
|
||||
queryParams['first_topic'] = localSettings.topic;
|
||||
}
|
||||
if (localSettings.secondTopic) {
|
||||
queryParams['second_topic'] = localSettings.secondTopic;
|
||||
}
|
||||
} else {
|
||||
if (localSettings.topic) {
|
||||
queryParams['topic'] = localSettings.topic;
|
||||
}
|
||||
}
|
||||
|
||||
generate(
|
||||
sectionId,
|
||||
currentModule,
|
||||
"context", // <- not really context but exercises is reserved for reading, listening and level
|
||||
{
|
||||
method: 'GET',
|
||||
queryParams
|
||||
},
|
||||
(data: any) => {
|
||||
switch (sectionId) {
|
||||
case 1:
|
||||
return [{
|
||||
prompts: data.questions,
|
||||
first_topic: data.first_topic,
|
||||
second_topic: data.second_topic
|
||||
}];
|
||||
case 2:
|
||||
return [{
|
||||
topic: data.topic,
|
||||
question: data.question,
|
||||
prompts: data.prompts,
|
||||
suffix: data.suffix
|
||||
}];
|
||||
case 3:
|
||||
return [{
|
||||
title: data.topic,
|
||||
prompts: data.questions
|
||||
}];
|
||||
default:
|
||||
return [data];
|
||||
}
|
||||
}
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localSettings, difficulty]);
|
||||
|
||||
const onTopicChange = useCallback((topic: string) => {
|
||||
updateLocalAndScheduleGlobal({ topic });
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
const onSecondTopicChange = useCallback((topic: string) => {
|
||||
updateLocalAndScheduleGlobal({ secondTopic: topic });
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
const canPreviewOrSubmit = (() => {
|
||||
return sections.every((s) => {
|
||||
const section = s.state as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
switch (section.type) {
|
||||
case 'speaking':
|
||||
return section.title !== '' &&
|
||||
section.text !== '' &&
|
||||
section.video_url !== '' &&
|
||||
section.prompts.every(prompt => prompt !== '');
|
||||
|
||||
case 'interactiveSpeaking':
|
||||
if ('first_title' in section && 'second_title' in section) {
|
||||
return section.first_title !== '' &&
|
||||
section.second_title !== '' &&
|
||||
section.prompts.every(prompt => prompt.video_url !== '') &&
|
||||
section.prompts.length > 2;
|
||||
}
|
||||
return section.title !== '' &&
|
||||
section.prompts.every(prompt => prompt.video_url !== '');
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
const canGenerate = section && (() => {
|
||||
switch (focusedSection) {
|
||||
case 1: {
|
||||
const currentSection = section as InteractiveSpeakingExercise;
|
||||
return currentSection.first_title !== "" &&
|
||||
currentSection.second_title !== "" &&
|
||||
currentSection.prompts.every(prompt => prompt.text !== "") && currentSection.prompts.length > 2;
|
||||
}
|
||||
case 2: {
|
||||
const currentSection = section as SpeakingExercise;
|
||||
return currentSection.title !== "" &&
|
||||
currentSection.text !== "" &&
|
||||
currentSection.prompts.every(prompt => prompt !== "");
|
||||
}
|
||||
case 3: {
|
||||
const currentSection = section as InteractiveSpeakingExercise;
|
||||
return currentSection.title !== "" &&
|
||||
currentSection.prompts.every(prompt => prompt.text !== "");
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
const generateVideoCallback = useCallback((sectionId: number) => {
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "generating", value: "media" } })
|
||||
generateVideos(
|
||||
section as InteractiveSpeakingExercise | SpeakingExercise,
|
||||
sectionId,
|
||||
selectedAvatar,
|
||||
speakingAvatars
|
||||
).then((results) => {
|
||||
switch (sectionId) {
|
||||
case 1:
|
||||
case 3: {
|
||||
const interactiveSection = section as InteractiveSpeakingExercise;
|
||||
const updatedPrompts = interactiveSection.prompts.map((prompt, index) => ({
|
||||
...prompt,
|
||||
video_url: results[index].url || ''
|
||||
}));
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "genResult", value: [{ prompts: updatedPrompts }] } })
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
if (results[0]?.url) {
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "genResult", value: [{ video_url: results[0].url }] } })
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}).catch((error) => {
|
||||
toast.error("Failed to generate the video, try again later!")
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedAvatar, section]);
|
||||
|
||||
|
||||
const submitSpeaking = async () => {
|
||||
if (title === "") {
|
||||
toast.error("Enter a title for the exam!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
const urlMap = new Map<string, string>();
|
||||
|
||||
const sectionsWithVideos = sections.filter(s => {
|
||||
const exercise = s.state as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
if (exercise.type === "speaking") {
|
||||
return exercise.video_url !== "";
|
||||
}
|
||||
if (exercise.type === "interactiveSpeaking") {
|
||||
return exercise.prompts?.some(prompt => prompt.video_url !== "");
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (sectionsWithVideos.length === 0) {
|
||||
toast.error('No video sections found in the exam! Please record or import videos.');
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
sectionsWithVideos.map(async (section) => {
|
||||
const exercise = section.state as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
|
||||
if (exercise.type === "speaking") {
|
||||
const response = await fetch(exercise.video_url);
|
||||
const blob = await response.blob();
|
||||
formData.append('file', blob, 'video.mp4');
|
||||
urlMap.set(`${section.sectionId}`, exercise.video_url);
|
||||
} else {
|
||||
await Promise.all(
|
||||
exercise.prompts.map(async (prompt, promptIndex) => {
|
||||
if (prompt.video_url) {
|
||||
const response = await fetch(prompt.video_url);
|
||||
const blob = await response.blob();
|
||||
formData.append('file', blob, 'video.mp4');
|
||||
urlMap.set(`${section.sectionId}-${promptIndex}`, prompt.video_url);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const response = await axios.post('/api/storage', formData, {
|
||||
params: {
|
||||
directory: 'speaking_videos'
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
|
||||
const { urls } = response.data;
|
||||
|
||||
const exam: SpeakingExam = {
|
||||
exercises: sections.map((s) => {
|
||||
const exercise = s.state as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
|
||||
if (exercise.type === "speaking") {
|
||||
const videoIndex = Array.from(urlMap.entries())
|
||||
.findIndex(([key]) => key === `${s.sectionId}`);
|
||||
|
||||
return {
|
||||
...exercise,
|
||||
video_url: videoIndex !== -1 ? urls[videoIndex] : exercise.video_url,
|
||||
intro: s.settings.currentIntro,
|
||||
category: s.settings.category
|
||||
};
|
||||
} else {
|
||||
const updatedPrompts = exercise.prompts.map((prompt, promptIndex) => {
|
||||
const videoIndex = Array.from(urlMap.entries())
|
||||
.findIndex(([key]) => key === `${s.sectionId}-${promptIndex}`);
|
||||
|
||||
return {
|
||||
...prompt,
|
||||
video_url: videoIndex !== -1 ? urls[videoIndex] : prompt.video_url
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...exercise,
|
||||
prompts: updatedPrompts,
|
||||
intro: s.settings.currentIntro,
|
||||
category: s.settings.category
|
||||
};
|
||||
}
|
||||
}),
|
||||
minTimer,
|
||||
module: "speaking",
|
||||
id: title,
|
||||
isDiagnostic: false,
|
||||
variant: undefined,
|
||||
difficulty,
|
||||
instructorGender: "varied",
|
||||
private: isPrivate,
|
||||
};
|
||||
|
||||
const result = await axios.post('/api/exam/speaking', exam);
|
||||
playSound("sent");
|
||||
toast.success(`Submitted Exam ID: ${result.data.id}`);
|
||||
|
||||
Array.from(urlMap.values()).forEach(url => {
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
toast.error(
|
||||
"Something went wrong while submitting, please try again later."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const preview = () => {
|
||||
setExam({
|
||||
exercises: sections
|
||||
.filter((s) => {
|
||||
const exercise = s.state as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
|
||||
if (exercise.type === "speaking") {
|
||||
return exercise.video_url !== "";
|
||||
}
|
||||
|
||||
if (exercise.type === "interactiveSpeaking") {
|
||||
return exercise.prompts?.every(prompt => prompt.video_url !== "");
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.map((s) => {
|
||||
const exercise = s.state as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
return {
|
||||
...exercise,
|
||||
intro: s.settings.currentIntro,
|
||||
category: s.settings.category
|
||||
};
|
||||
}),
|
||||
minTimer,
|
||||
module: "speaking",
|
||||
id: title,
|
||||
isDiagnostic: false,
|
||||
variant: undefined,
|
||||
difficulty,
|
||||
private: isPrivate,
|
||||
} as SpeakingExam);
|
||||
setExerciseIndex(0);
|
||||
setQuestionIndex(0);
|
||||
setBgColor("bg-white");
|
||||
openDetachedTab("popout?type=Exam&module=speaking", router)
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsEditor
|
||||
sectionLabel={`Speaking ${focusedSection}`}
|
||||
sectionId={focusedSection}
|
||||
module="speaking"
|
||||
introPresets={[defaultPresets[focusedSection - 1]]}
|
||||
preview={preview}
|
||||
canPreview={canPreviewOrSubmit}
|
||||
canSubmit={canPreviewOrSubmit}
|
||||
submitModule={submitSpeaking}
|
||||
>
|
||||
<Dropdown
|
||||
title="Generate Script"
|
||||
module={currentModule}
|
||||
open={localSettings.isExerciseDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen }, false)}
|
||||
>
|
||||
|
||||
<div className={clsx("gap-2 px-2 pb-4", focusedSection === 1 ? "flex flex-col w-full" : "flex flex-row items-center")}>
|
||||
<div className="flex flex-col flex-grow gap-4 px-2">
|
||||
<label className="font-normal text-base text-mti-gray-dim">{`${focusedSection === 1 ? "First Topic" : "Topic"}`} (Optional)</label>
|
||||
<Input
|
||||
key={`section-${focusedSection}`}
|
||||
type="text"
|
||||
placeholder="Topic"
|
||||
name="category"
|
||||
onChange={onTopicChange}
|
||||
roundness="full"
|
||||
value={localSettings.topic}
|
||||
/>
|
||||
</div>
|
||||
{focusedSection === 1 &&
|
||||
<div className="flex flex-col flex-grow gap-4 px-2">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Second Topic (Optional)</label>
|
||||
<Input
|
||||
key={`section-${focusedSection}`}
|
||||
type="text"
|
||||
placeholder="Topic"
|
||||
name="category"
|
||||
onChange={onSecondTopicChange}
|
||||
roundness="full"
|
||||
value={localSettings.secondTopic}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<div className={clsx("flex h-16 mb-1", focusedSection === 1 ? "justify-center mt-4" : "self-end")}>
|
||||
<GenerateBtn
|
||||
module={currentModule}
|
||||
genType="context"
|
||||
sectionId={focusedSection}
|
||||
generateFnc={generateScript}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
title="Generate Video"
|
||||
module={currentModule}
|
||||
open={localSettings.isGenerateAudioOpen}
|
||||
disabled={!canGenerate}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isGenerateAudioOpen: isOpen }, false)}
|
||||
>
|
||||
<div className={clsx("flex items-center justify-between gap-4 px-2 pb-4")}>
|
||||
<div className="relative flex-1 max-w-xs">
|
||||
<select
|
||||
value={selectedAvatar ? `${selectedAvatar.name}-${selectedAvatar.gender}` : ""}
|
||||
onChange={(e) => {
|
||||
if (e.target.value === "") {
|
||||
setSelectedAvatar(null);
|
||||
} else {
|
||||
const [name, gender] = e.target.value.split("-");
|
||||
const avatar = speakingAvatars.find(a => a.name === name && a.gender === gender);
|
||||
if (avatar) setSelectedAvatar(avatar);
|
||||
}
|
||||
}}
|
||||
className="w-full appearance-none px-4 py-2 border border-gray-200 rounded-full text-base bg-white focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
>
|
||||
<option value="">Select an avatar</option>
|
||||
{speakingAvatars.map((avatar) => (
|
||||
<option
|
||||
key={`${avatar.name}-${avatar.gender}`}
|
||||
value={`${avatar.name}-${avatar.gender}`}
|
||||
>
|
||||
{avatar.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="absolute right-2.5 top-2.5 pointer-events-none">
|
||||
{selectedAvatar && (
|
||||
selectedAvatar.gender === 'male' ? (
|
||||
<FaMale className="w-5 h-5 text-blue-500" />
|
||||
) : (
|
||||
<FaFemale className="w-5 h-5 text-pink-500" />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GenerateBtn
|
||||
module={currentModule}
|
||||
genType="media"
|
||||
sectionId={focusedSection}
|
||||
generateFnc={generateVideoCallback}
|
||||
/>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</SettingsEditor>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpeakingSettings;
|
||||
268
src/components/ExamEditor/SettingsEditor/speaking/components.tsx
Normal file
268
src/components/ExamEditor/SettingsEditor/speaking/components.tsx
Normal file
@@ -0,0 +1,268 @@
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import { LevelSectionSettings, SpeakingSectionSettings } from "@/stores/examEditor/types";
|
||||
import { useCallback, useState } from "react";
|
||||
import { generate } from "../Shared/Generate";
|
||||
import Dropdown from "../Shared/SettingsDropdown";
|
||||
import Input from "@/components/Low/Input";
|
||||
import GenerateBtn from "../Shared/GenerateBtn";
|
||||
import clsx from "clsx";
|
||||
import { FaFemale, FaMale } from "react-icons/fa";
|
||||
import { InteractiveSpeakingExercise, LevelPart, SpeakingExercise } from "@/interfaces/exam";
|
||||
import { toast } from "react-toastify";
|
||||
import { generateVideos } from "../Shared/generateVideos";
|
||||
import { Module } from "@/interfaces";
|
||||
|
||||
export interface Avatar {
|
||||
name: string;
|
||||
gender: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
localSettings: SpeakingSectionSettings | LevelSectionSettings;
|
||||
updateLocalAndScheduleGlobal: (updates: Partial<SpeakingSectionSettings | LevelSectionSettings>, schedule?: boolean) => void;
|
||||
section: SpeakingExercise | InteractiveSpeakingExercise | LevelPart;
|
||||
level?: boolean;
|
||||
module?: Module;
|
||||
}
|
||||
|
||||
const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndScheduleGlobal, section, level, module = "speaking" }) => {
|
||||
|
||||
const { currentModule, speakingAvatars, dispatch } = useExamEditorStore();
|
||||
const { focusedSection, difficulty } = useExamEditorStore((store) => store.modules[currentModule])
|
||||
|
||||
const [selectedAvatar, setSelectedAvatar] = useState<Avatar | null>(null);
|
||||
|
||||
const generateScript = useCallback((sectionId: number) => {
|
||||
const queryParams: {
|
||||
difficulty: string;
|
||||
first_topic?: string;
|
||||
second_topic?: string;
|
||||
topic?: string;
|
||||
} = { difficulty };
|
||||
|
||||
if (sectionId === 1) {
|
||||
if (localSettings.speakingTopic) {
|
||||
queryParams['first_topic'] = localSettings.speakingTopic;
|
||||
}
|
||||
if (localSettings.speakingSecondTopic) {
|
||||
queryParams['second_topic'] = localSettings.speakingSecondTopic;
|
||||
}
|
||||
} else {
|
||||
if (localSettings.speakingTopic) {
|
||||
queryParams['topic'] = localSettings.speakingTopic;
|
||||
}
|
||||
}
|
||||
|
||||
generate(
|
||||
sectionId,
|
||||
currentModule,
|
||||
"speakingScript",
|
||||
{
|
||||
method: 'GET',
|
||||
queryParams
|
||||
},
|
||||
(data: any) => {
|
||||
switch (sectionId) {
|
||||
case 1:
|
||||
return [{
|
||||
prompts: data.questions,
|
||||
first_topic: data.first_topic,
|
||||
second_topic: data.second_topic
|
||||
}];
|
||||
case 2:
|
||||
return [{
|
||||
topic: data.topic,
|
||||
question: data.question,
|
||||
prompts: data.prompts,
|
||||
suffix: data.suffix
|
||||
}];
|
||||
case 3:
|
||||
return [{
|
||||
title: data.topic,
|
||||
prompts: data.questions
|
||||
}];
|
||||
default:
|
||||
return [data];
|
||||
}
|
||||
}
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localSettings, difficulty]);
|
||||
|
||||
const onTopicChange = useCallback((speakingTopic: string) => {
|
||||
updateLocalAndScheduleGlobal({ speakingTopic });
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
const onSecondTopicChange = useCallback((speakingSecondTopic: string) => {
|
||||
updateLocalAndScheduleGlobal({ speakingSecondTopic });
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
const canGenerate = section && (() => {
|
||||
switch (focusedSection) {
|
||||
case 1: {
|
||||
const currentSection = section as InteractiveSpeakingExercise;
|
||||
return currentSection.first_title !== "" &&
|
||||
currentSection.second_title !== "" &&
|
||||
currentSection.prompts.every(prompt => prompt.text !== "") && currentSection.prompts.length > 2;
|
||||
}
|
||||
case 2: {
|
||||
const currentSection = section as SpeakingExercise;
|
||||
return currentSection.title !== "" &&
|
||||
currentSection.text !== "" &&
|
||||
currentSection.prompts.every(prompt => prompt !== "");
|
||||
}
|
||||
case 3: {
|
||||
const currentSection = section as InteractiveSpeakingExercise;
|
||||
return currentSection.title !== "" &&
|
||||
currentSection.prompts.every(prompt => prompt.text !== "");
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
const generateVideoCallback = useCallback((sectionId: number) => {
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "generating", value: "video" } })
|
||||
generateVideos(
|
||||
section as InteractiveSpeakingExercise | SpeakingExercise,
|
||||
sectionId,
|
||||
selectedAvatar,
|
||||
speakingAvatars
|
||||
).then((results) => {
|
||||
switch (sectionId) {
|
||||
case 1:
|
||||
case 3: {
|
||||
const interactiveSection = section as InteractiveSpeakingExercise;
|
||||
const updatedPrompts = interactiveSection.prompts.map((prompt, index) => ({
|
||||
...prompt,
|
||||
video_url: results[index].url || ''
|
||||
}));
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
||||
sectionId, module: currentModule, field: "genResult", value:
|
||||
{ generating: "video", result: [{ prompts: updatedPrompts }], module: module }
|
||||
}
|
||||
})
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
if (results[0]?.url) {
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
||||
sectionId, module: currentModule, field: "genResult", value:
|
||||
{ generating: "video", result: [{ video_url: results[0].url }], module: module }
|
||||
}
|
||||
})
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}).catch((error) => {
|
||||
toast.error("Failed to generate the video, try again later!")
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedAvatar, section]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
title="Generate Script"
|
||||
module="speaking"
|
||||
open={localSettings.isSpeakingTopicOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isSpeakingTopicOpen: isOpen }, false)}
|
||||
contentWrapperClassName={level ? `border border-ielts-speaking` : ''}
|
||||
>
|
||||
|
||||
<div className={clsx("gap-2 px-2 pb-4", focusedSection === 1 ? "flex flex-col w-full" : "flex flex-row items-center")}>
|
||||
<div className="flex flex-col flex-grow gap-4 px-2">
|
||||
<label className="font-normal text-base text-mti-gray-dim">{`${focusedSection === 1 ? "First Topic" : "Topic"}`} (Optional)</label>
|
||||
<Input
|
||||
key={`section-${focusedSection}`}
|
||||
type="text"
|
||||
placeholder="Topic"
|
||||
name="category"
|
||||
onChange={onTopicChange}
|
||||
roundness="full"
|
||||
value={localSettings.speakingTopic}
|
||||
/>
|
||||
</div>
|
||||
{focusedSection === 1 &&
|
||||
<div className="flex flex-col flex-grow gap-4 px-2">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Second Topic (Optional)</label>
|
||||
<Input
|
||||
key={`section-${focusedSection}`}
|
||||
type="text"
|
||||
placeholder="Topic"
|
||||
name="category"
|
||||
onChange={onSecondTopicChange}
|
||||
roundness="full"
|
||||
value={localSettings.speakingSecondTopic}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<div className={clsx("flex h-16 mb-1", focusedSection === 1 ? "justify-center mt-4" : "self-end")}>
|
||||
<GenerateBtn
|
||||
module="speaking"
|
||||
genType="speakingScript"
|
||||
sectionId={focusedSection}
|
||||
generateFnc={generateScript}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
title="Generate Video"
|
||||
module="speaking"
|
||||
open={localSettings.isGenerateVideoOpen}
|
||||
disabled={!canGenerate}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isGenerateVideoOpen: isOpen }, false)}
|
||||
>
|
||||
<div className={clsx("flex items-center justify-between gap-4 px-2 pb-4")}>
|
||||
<div className="relative flex-1 max-w-xs">
|
||||
<select
|
||||
value={selectedAvatar ? `${selectedAvatar.name}-${selectedAvatar.gender}` : ""}
|
||||
onChange={(e) => {
|
||||
if (e.target.value === "") {
|
||||
setSelectedAvatar(null);
|
||||
} else {
|
||||
const [name, gender] = e.target.value.split("-");
|
||||
const avatar = speakingAvatars.find(a => a.name === name && a.gender === gender);
|
||||
if (avatar) setSelectedAvatar(avatar);
|
||||
}
|
||||
}}
|
||||
className="w-full appearance-none px-4 py-2 border border-gray-200 rounded-full text-base bg-white focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
>
|
||||
<option value="">Select an avatar (Optional)</option>
|
||||
{speakingAvatars.map((avatar) => (
|
||||
<option
|
||||
key={`${avatar.name}-${avatar.gender}`}
|
||||
value={`${avatar.name}-${avatar.gender}`}
|
||||
>
|
||||
{avatar.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="absolute right-2.5 top-2.5 pointer-events-none">
|
||||
{selectedAvatar && (
|
||||
selectedAvatar.gender === 'male' ? (
|
||||
<FaMale className="w-5 h-5 text-blue-500" />
|
||||
) : (
|
||||
<FaFemale className="w-5 h-5 text-pink-500" />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GenerateBtn
|
||||
module="speaking"
|
||||
genType="video"
|
||||
sectionId={focusedSection}
|
||||
generateFnc={generateVideoCallback}
|
||||
/>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpeakingComponents;
|
||||
261
src/components/ExamEditor/SettingsEditor/speaking/index.tsx
Normal file
261
src/components/ExamEditor/SettingsEditor/speaking/index.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import useSettingsState from "../../Hooks/useSettingsState";
|
||||
import { SpeakingSectionSettings } from "@/stores/examEditor/types";
|
||||
import Option from "@/interfaces/option";
|
||||
import SettingsEditor from "..";
|
||||
import { InteractiveSpeakingExercise, SpeakingExam, SpeakingExercise } from "@/interfaces/exam";
|
||||
import { toast } from "react-toastify";
|
||||
import { usePersistentExamStore } from "@/stores/examStore";
|
||||
import { useRouter } from "next/router";
|
||||
import openDetachedTab from "@/utils/popout";
|
||||
import axios from "axios";
|
||||
import { playSound } from "@/utils/sound";
|
||||
import SpeakingComponents from "./components";
|
||||
|
||||
export interface Avatar {
|
||||
name: string;
|
||||
gender: string;
|
||||
}
|
||||
|
||||
const SpeakingSettings: React.FC = () => {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
setExam,
|
||||
setExerciseIndex,
|
||||
setQuestionIndex,
|
||||
setBgColor,
|
||||
} = usePersistentExamStore();
|
||||
|
||||
const { title, currentModule } = useExamEditorStore();
|
||||
const { focusedSection, difficulty, sections, minTimer, isPrivate } = useExamEditorStore((store) => store.modules[currentModule])
|
||||
|
||||
const section = sections.find((section) => section.sectionId == focusedSection)?.state;
|
||||
|
||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<SpeakingSectionSettings>(
|
||||
currentModule,
|
||||
focusedSection,
|
||||
);
|
||||
|
||||
if (section === undefined) return <></>;
|
||||
|
||||
const currentSection = section as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
|
||||
const defaultPresets: Option[] = [
|
||||
{
|
||||
label: "Preset: Speaking Part 1",
|
||||
value: "Welcome to {part} of the {label}. You will engage in a conversation about yourself and familiar topics such as your home, family, work, studies, and interests. General questions will be asked."
|
||||
},
|
||||
{
|
||||
label: "Preset: Speaking Part 2",
|
||||
value: "Welcome to {part} of the {label}. You will be given a topic card describing a particular person, object, event, or experience."
|
||||
},
|
||||
{
|
||||
label: "Preset: Speaking Part 3",
|
||||
value: "Welcome to {part} of the {label}. You will engage in an in-depth discussion about abstract ideas and issues. The examiner will ask questions that require you to explain, analyze, and speculate about various aspects of the topic."
|
||||
}
|
||||
];
|
||||
|
||||
const canPreviewOrSubmit = (() => {
|
||||
return sections.every((s) => {
|
||||
const section = s.state as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
switch (section.type) {
|
||||
case 'speaking':
|
||||
return section.title !== '' &&
|
||||
section.text !== '' &&
|
||||
section.video_url !== '' &&
|
||||
section.prompts.every(prompt => prompt !== '');
|
||||
|
||||
case 'interactiveSpeaking':
|
||||
if ('first_title' in section && 'second_title' in section) {
|
||||
return section.first_title !== '' &&
|
||||
section.second_title !== '' &&
|
||||
section.prompts.every(prompt => prompt.video_url !== '') &&
|
||||
section.prompts.length > 2;
|
||||
}
|
||||
return section.title !== '' &&
|
||||
section.prompts.every(prompt => prompt.video_url !== '');
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
const submitSpeaking = async () => {
|
||||
if (title === "") {
|
||||
toast.error("Enter a title for the exam!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
const urlMap = new Map<string, string>();
|
||||
|
||||
const sectionsWithVideos = sections.filter(s => {
|
||||
const exercise = s.state as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
if (exercise.type === "speaking") {
|
||||
return exercise.video_url !== "";
|
||||
}
|
||||
if (exercise.type === "interactiveSpeaking") {
|
||||
return exercise.prompts?.some(prompt => prompt.video_url !== "");
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (sectionsWithVideos.length === 0) {
|
||||
toast.error('No video sections found in the exam! Please record or import videos.');
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
sectionsWithVideos.map(async (section) => {
|
||||
const exercise = section.state as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
|
||||
if (exercise.type === "speaking") {
|
||||
const response = await fetch(exercise.video_url);
|
||||
const blob = await response.blob();
|
||||
formData.append('file', blob, 'video.mp4');
|
||||
urlMap.set(`${section.sectionId}`, exercise.video_url);
|
||||
} else {
|
||||
await Promise.all(
|
||||
exercise.prompts.map(async (prompt, promptIndex) => {
|
||||
if (prompt.video_url) {
|
||||
const response = await fetch(prompt.video_url);
|
||||
const blob = await response.blob();
|
||||
formData.append('file', blob, 'video.mp4');
|
||||
urlMap.set(`${section.sectionId}-${promptIndex}`, prompt.video_url);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const response = await axios.post('/api/storage', formData, {
|
||||
params: {
|
||||
directory: 'speaking_videos'
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
|
||||
const { urls } = response.data;
|
||||
|
||||
const exam: SpeakingExam = {
|
||||
exercises: sections.map((s) => {
|
||||
const exercise = s.state as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
|
||||
if (exercise.type === "speaking") {
|
||||
const videoIndex = Array.from(urlMap.entries())
|
||||
.findIndex(([key]) => key === `${s.sectionId}`);
|
||||
|
||||
return {
|
||||
...exercise,
|
||||
video_url: videoIndex !== -1 ? urls[videoIndex] : exercise.video_url,
|
||||
intro: s.settings.currentIntro,
|
||||
category: s.settings.category
|
||||
};
|
||||
} else {
|
||||
const updatedPrompts = exercise.prompts.map((prompt, promptIndex) => {
|
||||
const videoIndex = Array.from(urlMap.entries())
|
||||
.findIndex(([key]) => key === `${s.sectionId}-${promptIndex}`);
|
||||
|
||||
return {
|
||||
...prompt,
|
||||
video_url: videoIndex !== -1 ? urls[videoIndex] : prompt.video_url
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...exercise,
|
||||
prompts: updatedPrompts,
|
||||
intro: s.settings.currentIntro,
|
||||
category: s.settings.category
|
||||
};
|
||||
}
|
||||
}),
|
||||
minTimer,
|
||||
module: "speaking",
|
||||
id: title,
|
||||
isDiagnostic: false,
|
||||
variant: undefined,
|
||||
difficulty,
|
||||
instructorGender: "varied",
|
||||
private: isPrivate,
|
||||
};
|
||||
|
||||
const result = await axios.post('/api/exam/speaking', exam);
|
||||
playSound("sent");
|
||||
toast.success(`Submitted Exam ID: ${result.data.id}`);
|
||||
|
||||
Array.from(urlMap.values()).forEach(url => {
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
toast.error(
|
||||
"Something went wrong while submitting, please try again later."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const preview = () => {
|
||||
setExam({
|
||||
exercises: sections
|
||||
.filter((s) => {
|
||||
const exercise = s.state as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
|
||||
if (exercise.type === "speaking") {
|
||||
return exercise.video_url !== "";
|
||||
}
|
||||
|
||||
if (exercise.type === "interactiveSpeaking") {
|
||||
return exercise.prompts?.every(prompt => prompt.video_url !== "");
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.map((s) => {
|
||||
const exercise = s.state as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
return {
|
||||
...exercise,
|
||||
intro: s.settings.currentIntro,
|
||||
category: s.settings.category
|
||||
};
|
||||
}),
|
||||
minTimer,
|
||||
module: "speaking",
|
||||
id: title,
|
||||
isDiagnostic: false,
|
||||
variant: undefined,
|
||||
difficulty,
|
||||
private: isPrivate,
|
||||
} as SpeakingExam);
|
||||
setExerciseIndex(0);
|
||||
setQuestionIndex(0);
|
||||
setBgColor("bg-white");
|
||||
openDetachedTab("popout?type=Exam&module=speaking", router)
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsEditor
|
||||
sectionLabel={`Speaking ${focusedSection}`}
|
||||
sectionId={focusedSection}
|
||||
module="speaking"
|
||||
introPresets={[defaultPresets[focusedSection - 1]]}
|
||||
preview={preview}
|
||||
canPreview={canPreviewOrSubmit}
|
||||
canSubmit={canPreviewOrSubmit}
|
||||
submitModule={submitSpeaking}
|
||||
>
|
||||
<SpeakingComponents
|
||||
{...{ localSettings, updateLocalAndScheduleGlobal, section: currentSection }}
|
||||
/>
|
||||
</SettingsEditor>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpeakingSettings;
|
||||
@@ -0,0 +1,85 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import Dropdown from "../Shared/SettingsDropdown";
|
||||
import Input from "@/components/Low/Input";
|
||||
import { generate } from "../Shared/Generate";
|
||||
import GenerateBtn from "../Shared/GenerateBtn";
|
||||
import { LevelSectionSettings, WritingSectionSettings } from "@/stores/examEditor/types";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import { WritingExercise } from "@/interfaces/exam";
|
||||
|
||||
|
||||
interface Props {
|
||||
localSettings: WritingSectionSettings | LevelSectionSettings;
|
||||
updateLocalAndScheduleGlobal: (updates: Partial<WritingSectionSettings | LevelSectionSettings>, schedule?: boolean) => void;
|
||||
currentSection?: WritingExercise;
|
||||
level?: boolean;
|
||||
}
|
||||
|
||||
const WritingComponents: React.FC<Props> = ({localSettings, updateLocalAndScheduleGlobal, level}) => {
|
||||
const { currentModule } = useExamEditorStore();
|
||||
const {
|
||||
difficulty,
|
||||
focusedSection,
|
||||
} = useExamEditorStore((store) => store.modules["writing"]);
|
||||
|
||||
const generatePassage = useCallback((sectionId: number) => {
|
||||
generate(
|
||||
sectionId,
|
||||
currentModule,
|
||||
"writing",
|
||||
{
|
||||
method: 'GET',
|
||||
queryParams: {
|
||||
difficulty,
|
||||
...(localSettings.writingTopic && { topic: localSettings.writingTopic })
|
||||
}
|
||||
},
|
||||
(data: any) => [{
|
||||
prompt: data.question
|
||||
}]
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localSettings.writingTopic, difficulty]);
|
||||
|
||||
const onTopicChange = useCallback((writingTopic: string) => {
|
||||
updateLocalAndScheduleGlobal({ writingTopic });
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
title="Generate Instructions"
|
||||
module={"writing"}
|
||||
open={localSettings.isWritingTopicOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isWritingTopicOpen: isOpen }, false)}
|
||||
contentWrapperClassName={level ? `border border-ielts-writing`: ''}
|
||||
>
|
||||
|
||||
<div className="flex flex-row gap-2 items-center px-2 pb-4">
|
||||
<div className="flex flex-col flex-grow gap-4 px-2">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Topic (Optional)</label>
|
||||
<Input
|
||||
key={`section-${focusedSection}`}
|
||||
type="text"
|
||||
placeholder="Topic"
|
||||
name="category"
|
||||
onChange={onTopicChange}
|
||||
roundness="full"
|
||||
value={localSettings.writingTopic}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex self-end h-16 mb-1">
|
||||
<GenerateBtn
|
||||
genType="writing"
|
||||
module={"writing"}
|
||||
sectionId={focusedSection}
|
||||
generateFnc={generatePassage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WritingComponents;
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import SettingsEditor from ".";
|
||||
import SettingsEditor from "..";
|
||||
import Option from "@/interfaces/option";
|
||||
import Dropdown from "./Shared/SettingsDropdown";
|
||||
import Dropdown from "../Shared/SettingsDropdown";
|
||||
import Input from "@/components/Low/Input";
|
||||
import { generate } from "./Shared/Generate";
|
||||
import useSettingsState from "../Hooks/useSettingsState";
|
||||
import GenerateBtn from "./Shared/GenerateBtn";
|
||||
import { SectionSettings } from "@/stores/examEditor/types";
|
||||
import { generate } from "../Shared/Generate";
|
||||
import useSettingsState from "../../Hooks/useSettingsState";
|
||||
import GenerateBtn from "../Shared/GenerateBtn";
|
||||
import { WritingSectionSettings } from "@/stores/examEditor/types";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import { useRouter } from "next/router";
|
||||
import { usePersistentExamStore } from "@/stores/examStore";
|
||||
@@ -16,6 +16,7 @@ import { v4 } from "uuid";
|
||||
import axios from "axios";
|
||||
import { playSound } from "@/utils/sound";
|
||||
import { toast } from "react-toastify";
|
||||
import WritingComponents from "./components";
|
||||
|
||||
const WritingSettings: React.FC = () => {
|
||||
const router = useRouter();
|
||||
@@ -37,7 +38,7 @@ const WritingSettings: React.FC = () => {
|
||||
setExerciseIndex,
|
||||
} = usePersistentExamStore();
|
||||
|
||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<SectionSettings>(
|
||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<WritingSectionSettings>(
|
||||
currentModule,
|
||||
focusedSection,
|
||||
);
|
||||
@@ -53,29 +54,6 @@ const WritingSettings: React.FC = () => {
|
||||
}
|
||||
];
|
||||
|
||||
const generatePassage = useCallback((sectionId: number) => {
|
||||
generate(
|
||||
sectionId,
|
||||
currentModule,
|
||||
"context",
|
||||
{
|
||||
method: 'GET',
|
||||
queryParams: {
|
||||
difficulty,
|
||||
...(localSettings.topic && { topic: localSettings.topic })
|
||||
}
|
||||
},
|
||||
(data: any) => [{
|
||||
prompt: data.question
|
||||
}]
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localSettings.topic, difficulty]);
|
||||
|
||||
const onTopicChange = useCallback((topic: string) => {
|
||||
updateLocalAndScheduleGlobal({ topic });
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
useEffect(() => {
|
||||
setCanPreviewOrSubmit(states.some((s) => s.prompt !== ""))
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -145,36 +123,9 @@ const WritingSettings: React.FC = () => {
|
||||
canSubmit={canPreviewOrSubmit}
|
||||
submitModule={submitWriting}
|
||||
>
|
||||
<Dropdown
|
||||
title="Generate Instructions"
|
||||
module={currentModule}
|
||||
open={localSettings.isExerciseDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen }, false)}
|
||||
>
|
||||
|
||||
<div className="flex flex-row gap-2 items-center px-2 pb-4">
|
||||
<div className="flex flex-col flex-grow gap-4 px-2">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Topic (Optional)</label>
|
||||
<Input
|
||||
key={`section-${focusedSection}`}
|
||||
type="text"
|
||||
placeholder="Topic"
|
||||
name="category"
|
||||
onChange={onTopicChange}
|
||||
roundness="full"
|
||||
value={localSettings.topic}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex self-end h-16 mb-1">
|
||||
<GenerateBtn
|
||||
genType="context"
|
||||
module={currentModule}
|
||||
sectionId={focusedSection}
|
||||
generateFnc={generatePassage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
<WritingComponents
|
||||
{...{ localSettings, updateLocalAndScheduleGlobal }}
|
||||
/>
|
||||
</SettingsEditor>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +1,26 @@
|
||||
interface Props {
|
||||
label: string;
|
||||
preview?: React.ReactNode;
|
||||
type: string;
|
||||
firstId: string;
|
||||
lastId: string;
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
const ExerciseLabel: React.FC<Props> = ({label, preview}) => {
|
||||
const previewLabel = (text: string) => {
|
||||
return <>
|
||||
"{text !== undefined ? text.replaceAll('\\n', ' ').split(' ').slice(0, 15).join(' ') : ""}..."
|
||||
</>
|
||||
}
|
||||
|
||||
const label = (type: string, firstId: string, lastId: string) => {
|
||||
return `${type} #${firstId} ${firstId === lastId ? '' : `- #${lastId}`}`;
|
||||
}
|
||||
|
||||
|
||||
const ExerciseLabel: React.FC<Props> = ({type, firstId, lastId, prompt}) => {
|
||||
return (
|
||||
<div className="flex w-full justify-between items-center mr-4">
|
||||
<span className="font-semibold">{label}</span>
|
||||
{preview && <div className="text-sm font-light italic">{preview}</div>}
|
||||
<span className="font-semibold">{label(type, firstId, lastId)}</span>
|
||||
<div className="text-sm font-light italic">{previewLabel(prompt)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Module } from "@/interfaces";
|
||||
import clsx from "clsx";
|
||||
import { ReactNode } from "react";
|
||||
import { MdDelete, MdEdit, MdEditOff, MdRefresh, MdSave, MdGrade, MdOutlineGrade } from "react-icons/md";
|
||||
import { MdDelete, MdEdit, MdEditOff, MdRefresh, MdSave } from "react-icons/md";
|
||||
import { HiOutlineClipboardCheck, HiOutlineClipboardList } from "react-icons/hi";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
@@ -10,14 +11,15 @@ interface Props {
|
||||
module?: Module;
|
||||
handleSave: () => void;
|
||||
handleDiscard: () => void;
|
||||
modeHandle?: () => void;
|
||||
evaluationHandle?: () => void;
|
||||
handleDelete?: () => void;
|
||||
handlePractice?: () => void;
|
||||
handleEdit?: () => void;
|
||||
isEvaluationEnabled?: boolean;
|
||||
mode?: "delete" | "edit";
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const Header: React.FC<Props> = ({ title, description, editing, isEvaluationEnabled, handleSave, handleDiscard, modeHandle, evaluationHandle, children, mode = "delete", module }) => {
|
||||
const Header: React.FC<Props> = ({
|
||||
title, description, editing, isEvaluationEnabled, handleSave, handleDiscard, handleDelete, handleEdit, handlePractice, children, module }) => {
|
||||
return (
|
||||
<div className="flex justify-between items-center mb-6 text-sm">
|
||||
<div>
|
||||
@@ -48,26 +50,18 @@ const Header: React.FC<Props> = ({ title, description, editing, isEvaluationEnab
|
||||
<MdRefresh size={18} />
|
||||
Discard
|
||||
</button>
|
||||
{mode === "delete" ? (
|
||||
{handleEdit && (
|
||||
<button
|
||||
onClick={modeHandle}
|
||||
className="px-4 py-2 bg-white border border-red-500 text-red-500 hover:bg-red-50 rounded-lg transition-all duration-200 flex items-center gap-2"
|
||||
>
|
||||
<MdDelete size={18} />
|
||||
Delete
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={modeHandle}
|
||||
onClick={handleEdit}
|
||||
className={`px-4 py-2 bg-ielts-${module}/80 text-white hover:bg-ielts-${module} rounded-lg transition-all duration-200 flex items-center gap-2`}
|
||||
>
|
||||
{editing ? <MdEditOff size={18} /> : <MdEdit size={18} />}
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
{mode === "delete" &&
|
||||
{handlePractice &&
|
||||
<button
|
||||
onClick={evaluationHandle}
|
||||
onClick={handlePractice}
|
||||
className={clsx(
|
||||
"px-4 py-2 rounded-lg flex items-center gap-2 transition-all duration-200",
|
||||
isEvaluationEnabled
|
||||
@@ -75,10 +69,19 @@ const Header: React.FC<Props> = ({ title, description, editing, isEvaluationEnab
|
||||
: 'bg-gray-200 text-gray-600 hover:bg-gray-300'
|
||||
)}
|
||||
>
|
||||
{isEvaluationEnabled ? <MdGrade size={18} /> : <MdOutlineGrade size={18} />}
|
||||
{isEvaluationEnabled ? 'Graded Exercise' : 'Practice Only'}
|
||||
{isEvaluationEnabled ? <HiOutlineClipboardCheck size={18} /> : <HiOutlineClipboardList size={18} />}
|
||||
{isEvaluationEnabled ? 'Graded' : 'Practice'}
|
||||
</button>
|
||||
}
|
||||
{handleDelete && (
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="px-4 py-2 bg-white border border-red-500 text-red-500 hover:bg-red-50 rounded-lg transition-all duration-200 flex items-center gap-2"
|
||||
>
|
||||
<MdDelete size={18} />
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ import ReadingSettings from "./SettingsEditor/reading";
|
||||
import LevelSettings from "./SettingsEditor/level";
|
||||
import ListeningSettings from "./SettingsEditor/listening";
|
||||
import SpeakingSettings from "./SettingsEditor/speaking";
|
||||
import ImportOrStartFromScratch from "./Shared/ImportExam/ImportOrFromScratch";
|
||||
import ImportOrStartFromScratch from "./ImportExam/ImportOrFromScratch";
|
||||
import { defaultSectionSettings } from "@/stores/examEditor/defaults";
|
||||
|
||||
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
|
||||
|
||||
@@ -305,7 +305,7 @@ export default function Listening({ exam, showSolutions = false, preview = false
|
||||
minTimer={exam.minTimer}
|
||||
module="listening"
|
||||
totalExercises={exam.parts.length}
|
||||
disableTimer={showSolutions}
|
||||
disableTimer={showSolutions || preview}
|
||||
indexLabel="Part"
|
||||
/>
|
||||
|
||||
|
||||
@@ -216,6 +216,7 @@ export default function Reading({ exam, showSolutions = false, preview = false,
|
||||
(x) => x === 0,
|
||||
) &&
|
||||
!showSolutions &&
|
||||
!preview &&
|
||||
!hasExamEnded
|
||||
) {
|
||||
setShowBlankModal(true);
|
||||
@@ -324,7 +325,7 @@ export default function Reading({ exam, showSolutions = false, preview = false,
|
||||
exerciseIndex={calculateExerciseIndex()}
|
||||
module="reading"
|
||||
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
|
||||
disableTimer={showSolutions}
|
||||
disableTimer={showSolutions || preview}
|
||||
label={exerciseIndex === -1 ? undefined : convertCamelCaseToReadable(exam.parts[partIndex].exercises[exerciseIndex].type)}
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -45,12 +45,14 @@ export interface LevelExam extends ExamBase {
|
||||
}
|
||||
|
||||
export interface LevelPart extends Section {
|
||||
// to support old exams that have reading passage mc on context
|
||||
context?: string;
|
||||
exercises: Exercise[];
|
||||
audio?: {
|
||||
source: string;
|
||||
repeatableTimes: number; // *The amount of times the user is allowed to repeat the audio, 0 for unlimited
|
||||
};
|
||||
script?: Script;
|
||||
text?: {
|
||||
title: string;
|
||||
content: string;
|
||||
@@ -163,6 +165,7 @@ export interface WritingExercise extends Section {
|
||||
evaluation?: WritingEvaluation;
|
||||
}[];
|
||||
topic?: string;
|
||||
variant?: string;
|
||||
isPractice?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -32,8 +32,13 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
||||
const queryParams = queryToURLSearchParams(req);
|
||||
let endpoint = queryParams.getAll('module').join("/");
|
||||
|
||||
if (endpoint.startsWith("level")) {
|
||||
endpoint = "level/"
|
||||
} else if (endpoint.startsWith("listening")) {
|
||||
endpoint = "listening/"
|
||||
} else if (endpoint.startsWith("reading")) {
|
||||
endpoint = "reading/"
|
||||
}
|
||||
|
||||
const result = await axios.post(`${process.env.BACKEND_URL}/${endpoint}`,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Module } from "@/interfaces"
|
||||
import { Difficulty} from "@/interfaces/exam"
|
||||
import { Difficulty } from "@/interfaces/exam"
|
||||
import { sample } from "lodash"
|
||||
import { ExamPart, ModuleState, SectionState } from "./types"
|
||||
import { levelPart, listeningSection, readingPart, speakingTask, writingTask } from "@/stores/examEditor/sections"
|
||||
@@ -18,21 +18,59 @@ const defaultSettings = (module: Module) => {
|
||||
}
|
||||
|
||||
switch (module) {
|
||||
case 'writing':
|
||||
return {
|
||||
...baseSettings,
|
||||
writingTopic: '',
|
||||
isWritingTopicOpen: false
|
||||
}
|
||||
case 'reading':
|
||||
return {
|
||||
...baseSettings,
|
||||
isPassageOpen: false,
|
||||
readingTopic: '',
|
||||
isReadingTopicOpean: false,
|
||||
}
|
||||
case 'listening':
|
||||
case 'listening':
|
||||
return {
|
||||
...baseSettings,
|
||||
isAudioContextOpen: false,
|
||||
isAudioGenerationOpen: false,
|
||||
listeningTopic: '',
|
||||
isListeningTopicOpen: false,
|
||||
}
|
||||
case 'speaking':
|
||||
return {
|
||||
...baseSettings,
|
||||
isGenerateAudioOpen: false
|
||||
speakingTopic: '',
|
||||
speakingSecondTopic: '',
|
||||
isSpeakingTopicOpen: false,
|
||||
isGenerateVideoOpen: false,
|
||||
}
|
||||
case 'level':
|
||||
return {
|
||||
...baseSettings,
|
||||
isReadingDropdownOpen: false,
|
||||
isWritingDropdownOpen: false,
|
||||
isSpeakingDropdownOpen: false,
|
||||
isListeningDropdownOpen: false,
|
||||
|
||||
isWritingTopicOpen: false,
|
||||
writingTopic: '',
|
||||
|
||||
isPassageOpen: false,
|
||||
readingTopic: '',
|
||||
isReadingTopicOpean: false,
|
||||
|
||||
isAudioContextOpen: false,
|
||||
isAudioGenerationOpen: false,
|
||||
listeningTopic: '',
|
||||
isListeningTopicOpen: false,
|
||||
|
||||
speakingTopic: '',
|
||||
speakingSecondTopic: '',
|
||||
isSpeakingTopicOpen: false,
|
||||
isGenerateVideoOpen: false,
|
||||
}
|
||||
default:
|
||||
return baseSettings;
|
||||
@@ -47,16 +85,16 @@ const sectionLabels = (module: Module) => {
|
||||
label: `Passage ${index + 1}`
|
||||
}));
|
||||
case 'writing':
|
||||
return [{id: 1, label: "Task 1"}, {id: 2, label: "Task 2"}];
|
||||
return [{ id: 1, label: "Task 1" }, { id: 2, label: "Task 2" }];
|
||||
case 'speaking':
|
||||
return [{id: 1, label: "Speaking 1"}, {id: 2, label: "Speaking 2"}, {id: 3, label: "Interactive Speaking"}];
|
||||
return [{ id: 1, label: "Speaking 1" }, { id: 2, label: "Speaking 2" }, { id: 3, label: "Interactive Speaking" }];
|
||||
case 'listening':
|
||||
return Array.from({ length: 4 }, (_, index) => ({
|
||||
id: index + 1,
|
||||
label: `Section ${index + 1}`
|
||||
}));
|
||||
case 'level':
|
||||
return [{id: 1, label: "Part 1"}];
|
||||
return [{ id: 1, label: "Part 1" }];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +123,7 @@ const defaultSection = (module: Module, sectionId: number) => {
|
||||
return listeningSection(sectionId)
|
||||
case 'speaking':
|
||||
return speakingTask(sectionId)
|
||||
case 'level':
|
||||
case 'level':
|
||||
return levelPart(sectionId)
|
||||
}
|
||||
}
|
||||
@@ -98,14 +136,15 @@ export const defaultSectionSettings = (module: Module, sectionId: number, part?:
|
||||
state: part !== undefined ? part : defaultSection(module, sectionId),
|
||||
generating: undefined,
|
||||
genResult: undefined,
|
||||
focusedExercise: undefined,
|
||||
expandedSubSections: [],
|
||||
exercisePickerState: [],
|
||||
selectedExercises: [],
|
||||
levelGenerating: [],
|
||||
levelGenResults: []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const defaultModuleSettings = (module: Module, minTimer: number, states?: SectionState[]): ModuleState => {
|
||||
const defaultModuleSettings = (module: Module, minTimer: number): ModuleState => {
|
||||
const state: ModuleState = {
|
||||
examLabel: defaultExamLabel(module),
|
||||
minTimer,
|
||||
|
||||
@@ -6,9 +6,9 @@ import { reorderSection } from "../reorder/global";
|
||||
|
||||
export type SectionActions =
|
||||
| { type: 'UPDATE_SECTION_SINGLE_FIELD'; payload: { module: Module; sectionId: number; field: string; value: any } }
|
||||
| { type: 'UPDATE_SECTION_SETTINGS'; payload: { sectionId: number; update: Partial<SectionSettings | ReadingSectionSettings>; } }
|
||||
| { type: 'UPDATE_SECTION_STATE'; payload: { sectionId: number; update: Partial<Section>; } }
|
||||
| { type: 'REORDER_EXERCISES'; payload: { event: DragEndEvent, sectionId: number; } };
|
||||
| { type: 'UPDATE_SECTION_SETTINGS'; payload: { sectionId: number; module: Module; update: Partial<SectionSettings | ReadingSectionSettings>; } }
|
||||
| { type: 'UPDATE_SECTION_STATE'; payload: { sectionId: number; module: Module; update: Partial<Section>; } }
|
||||
| { type: 'REORDER_EXERCISES'; payload: { event: DragEndEvent, module: Module; sectionId: number; } };
|
||||
|
||||
export const SECTION_ACTIONS = [
|
||||
'UPDATE_SECTION_SETTINGS',
|
||||
@@ -20,24 +20,15 @@ export const sectionReducer = (
|
||||
state: ExamEditorStore,
|
||||
action: SectionActions
|
||||
): Partial<ExamEditorStore> => {
|
||||
const currentModule = state.currentModule;
|
||||
const modules = state.modules;
|
||||
const sections = state.modules[currentModule].sections;
|
||||
let sectionId: number;
|
||||
|
||||
if (action.payload && 'sectionId' in action.payload) {
|
||||
sectionId = action.payload.sectionId;
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case 'UPDATE_SECTION_SINGLE_FIELD':
|
||||
const { module, field, value } = action.payload;
|
||||
case 'UPDATE_SECTION_SINGLE_FIELD':{
|
||||
const { module, field, value, sectionId } = action.payload;
|
||||
return {
|
||||
modules: {
|
||||
...modules,
|
||||
...state.modules,
|
||||
[module]: {
|
||||
...modules[module],
|
||||
sections: sections.map((section: SectionState) =>
|
||||
...state.modules[module],
|
||||
sections: state.modules[module].sections.map((section: SectionState) =>
|
||||
section.sectionId === sectionId
|
||||
? {
|
||||
...section,
|
||||
@@ -48,22 +39,21 @@ export const sectionReducer = (
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
case 'UPDATE_SECTION_SETTINGS':
|
||||
let updatedSettings = action.payload.update;
|
||||
}
|
||||
case 'UPDATE_SECTION_SETTINGS':{
|
||||
const {module, sectionId, update} = action.payload;
|
||||
return {
|
||||
modules: {
|
||||
...modules,
|
||||
[currentModule]: {
|
||||
...modules[currentModule],
|
||||
sections: sections.map((section: SectionState) =>
|
||||
...state.modules,
|
||||
[module]: {
|
||||
...state.modules[module],
|
||||
sections: state.modules[module].sections.map((section: SectionState) =>
|
||||
section.sectionId === sectionId
|
||||
? {
|
||||
...section,
|
||||
settings: {
|
||||
...section.settings,
|
||||
...updatedSettings
|
||||
...update
|
||||
}
|
||||
}
|
||||
: section
|
||||
@@ -71,30 +61,32 @@ export const sectionReducer = (
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
case 'UPDATE_SECTION_STATE':
|
||||
const updatedState = action.payload.update;
|
||||
}
|
||||
case 'UPDATE_SECTION_STATE': {
|
||||
const { update, module, sectionId }= action.payload;
|
||||
return {
|
||||
modules: {
|
||||
...modules,
|
||||
[currentModule]: {
|
||||
...modules[currentModule],
|
||||
sections: sections.map(section =>
|
||||
...state.modules,
|
||||
[module]: {
|
||||
...state.modules[module],
|
||||
sections: state.modules[module].sections.map(section =>
|
||||
section.sectionId === sectionId
|
||||
? { ...section, state: { ...section.state, ...updatedState } }
|
||||
? { ...section, state: { ...section.state, ...update } }
|
||||
: section
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
case 'REORDER_EXERCISES': {
|
||||
const { active, over } = action.payload.event;
|
||||
const { event, sectionId, module } = action.payload;
|
||||
const {over, active} = event;
|
||||
if (!over) return state;
|
||||
|
||||
const oldIndex = active.id as number;
|
||||
const newIndex = over.id as number;
|
||||
|
||||
const currentSectionState = sections.find((s) => s.sectionId === sectionId)!.state as ReadingPart | ListeningPart | LevelPart;
|
||||
const currentSectionState = state.modules[module].sections.find((s) => s.sectionId === sectionId)!.state as ReadingPart | ListeningPart | LevelPart;
|
||||
const exercises = [...currentSectionState.exercises];
|
||||
const [removed] = exercises.splice(oldIndex, 1);
|
||||
exercises.splice(newIndex, 0, removed);
|
||||
@@ -110,9 +102,9 @@ export const sectionReducer = (
|
||||
...state,
|
||||
modules: {
|
||||
...state.modules,
|
||||
[currentModule]: {
|
||||
...modules[currentModule],
|
||||
sections: sections.map(section =>
|
||||
[module]: {
|
||||
...state.modules[module],
|
||||
sections: state.modules[module].sections.map(section =>
|
||||
section.sectionId === sectionId
|
||||
? { ...section, state: newSectionState }
|
||||
: section
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Exercise, FillBlanksExercise, LevelPart, ListeningPart, MatchSentencesExercise, MultipleChoiceExercise, ReadingPart, TrueFalseExercise, WriteBlanksExercise } from "@/interfaces/exam";
|
||||
import { Exercise, FillBlanksExercise, LevelPart, ListeningPart, MatchSentencesExercise, MultipleChoiceExercise, ReadingPart, TrueFalseExercise, WriteBlanksExercise, WritingExercise } from "@/interfaces/exam";
|
||||
import { ModuleState } from "../types";
|
||||
import ReorderResult from "./types";
|
||||
|
||||
@@ -143,7 +143,6 @@ const reorderSection = (exercises: Exercise[], startId: number): { exercises: Ex
|
||||
|
||||
switch (exercise.type) {
|
||||
case 'fillBlanks':
|
||||
console.log("Reordering FillBlanks");
|
||||
result = reorderFillBlanks(exercise, currentId);
|
||||
currentId = result.lastId;
|
||||
return result.exercise;
|
||||
@@ -168,6 +167,10 @@ const reorderSection = (exercises: Exercise[], startId: number): { exercises: Ex
|
||||
currentId = result.lastId;
|
||||
return result.exercise;
|
||||
|
||||
case 'writing':
|
||||
exercise = { ...exercise, sectionId: currentId };
|
||||
currentId += 1;
|
||||
return exercise;
|
||||
default:
|
||||
return exercise;
|
||||
}
|
||||
@@ -184,7 +187,6 @@ const reorderModule = (moduleState: ModuleState) => {
|
||||
let currentId = 1;
|
||||
let reorderedSections = moduleState.sections.map(section => {
|
||||
let currentSection = section.state as ReadingPart | ListeningPart | LevelPart;
|
||||
console.log(currentSection.exercises);
|
||||
let result = reorderSection(currentSection.exercises, currentId);
|
||||
currentId = result.lastId;
|
||||
return {
|
||||
|
||||
@@ -11,7 +11,8 @@ export const writingTask = (task: number) => ({
|
||||
wordCounter: {
|
||||
limit: task == 1 ? 150 : 250,
|
||||
type: "min",
|
||||
}
|
||||
},
|
||||
order: 1
|
||||
} as WritingExercise);
|
||||
|
||||
export const readingPart = (task: number) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Difficulty, InteractiveSpeakingExercise, LevelPart, ListeningPart, ReadingPart, SpeakingExercise, WritingExercise } from "@/interfaces/exam";
|
||||
import { Module } from "@/interfaces";
|
||||
import Option from "@/interfaces/option";
|
||||
import { ExerciseConfig } from "@/components/ExamEditor/Shared/ExercisePicker/ExerciseWizard";
|
||||
import { ExerciseConfig } from "@/components/ExamEditor/ExercisePicker/ExerciseWizard";
|
||||
|
||||
export interface GeneratedExercises {
|
||||
exercises: Record<string, string>[];
|
||||
@@ -16,32 +16,70 @@ export interface SectionSettings {
|
||||
currentIntro: string | undefined;
|
||||
isCategoryDropdownOpen: boolean;
|
||||
isIntroDropdownOpen: boolean;
|
||||
isExerciseDropdownOpen: boolean;
|
||||
topic?: string;
|
||||
}
|
||||
|
||||
export interface SpeakingSectionSettings extends SectionSettings {
|
||||
secondTopic?: string;
|
||||
isGenerateAudioOpen: boolean;
|
||||
speakingTopic: string;
|
||||
speakingSecondTopic?: string;
|
||||
isSpeakingTopicOpen: boolean;
|
||||
isGenerateVideoOpen: boolean;
|
||||
}
|
||||
|
||||
export interface ReadingSectionSettings extends SectionSettings {
|
||||
isPassageOpen: boolean;
|
||||
readingTopic: string;
|
||||
isReadingTopicOpean: boolean;
|
||||
}
|
||||
|
||||
export interface ListeningSectionSettings extends SectionSettings {
|
||||
isAudioContextOpen: boolean;
|
||||
isAudioGenerationOpen: boolean;
|
||||
listeningTopic: string;
|
||||
isListeningTopicOpen: boolean;
|
||||
}
|
||||
|
||||
export interface WritingSectionSettings extends SectionSettings {
|
||||
isWritingTopicOpen: boolean;
|
||||
writingTopic: string;
|
||||
}
|
||||
|
||||
export interface LevelSectionSettings extends SectionSettings {
|
||||
readingDropdownOpen: boolean;
|
||||
writingDropdownOpen: boolean;
|
||||
speakingDropdownOpen: boolean;
|
||||
listeningDropdownOpen: boolean;
|
||||
isReadingDropdownOpen: boolean;
|
||||
isWritingDropdownOpen: boolean;
|
||||
isSpeakingDropdownOpen: boolean;
|
||||
isListeningDropdownOpen: boolean;
|
||||
isLevelDropdownOpen: boolean;
|
||||
readingSection?: number;
|
||||
listeningSection?: number;
|
||||
|
||||
// writing
|
||||
isWritingTopicOpen: boolean;
|
||||
writingTopic: string;
|
||||
|
||||
// reading
|
||||
isPassageOpen: boolean;
|
||||
readingTopic: string;
|
||||
isReadingTopicOpean: boolean;
|
||||
|
||||
// listening
|
||||
isAudioContextOpen: boolean;
|
||||
isAudioGenerationOpen: boolean;
|
||||
listeningTopic: string;
|
||||
isListeningTopicOpen: boolean;
|
||||
|
||||
// speaking
|
||||
speakingTopic?: string;
|
||||
speakingSecondTopic?: string;
|
||||
isSpeakingTopicOpen: boolean;
|
||||
isGenerateVideoOpen: boolean;
|
||||
|
||||
// section picker
|
||||
isReadingPickerOpen: boolean;
|
||||
isListeningPickerOpen: boolean;
|
||||
}
|
||||
|
||||
export type Generating = "context" | "exercises" | "media" | undefined;
|
||||
export type Context = "passage" | "video" | "audio" | "listeningScript" | "speakingScript" | "writing";
|
||||
export type Generating = Context | "exercises" | string | undefined;
|
||||
export type Section = LevelPart | ReadingPart | ListeningPart | WritingExercise | SpeakingExercise | InteractiveSpeakingExercise;
|
||||
export type ExamPart = ListeningPart | ReadingPart | LevelPart;
|
||||
|
||||
@@ -51,9 +89,14 @@ export interface SectionState {
|
||||
state: Section;
|
||||
expandedSubSections: number[];
|
||||
generating: Generating;
|
||||
genResult: Record<string, any>[] | undefined;
|
||||
exercisePickerState: ExerciseConfig[];
|
||||
selectedExercises: string[];
|
||||
genResult: {generating: string, result: Record<string, any>[], module: Module} | undefined;
|
||||
levelGenerating: Generating[];
|
||||
levelGenResults: {generating: string, result: Record<string, any>[], module: Module}[];
|
||||
focusedExercise?: {questionId: number; id: string} | undefined;
|
||||
writingSection?: number;
|
||||
speakingSection?: number;
|
||||
readingSection?: number;
|
||||
listeningSection?: number;
|
||||
}
|
||||
|
||||
export interface ModuleState {
|
||||
|
||||
Reference in New Issue
Block a user