Added Speaking to level, fixed a bug where it was causing level to crash if the listening was already created and the section was switched, added true false exercises to listening
This commit is contained in:
@@ -166,7 +166,22 @@ const listening = (section: number) => {
|
|||||||
generate()
|
generate()
|
||||||
],
|
],
|
||||||
module: "listening"
|
module: "listening"
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
label: `Section ${section} - True False`,
|
||||||
|
type: `listening_${section}`,
|
||||||
|
icon: FaCheckSquare,
|
||||||
|
sectionId: section,
|
||||||
|
extra: [
|
||||||
|
{
|
||||||
|
param: "name",
|
||||||
|
value: "trueFalse"
|
||||||
|
},
|
||||||
|
quantity(4, "Quantity of Statements"),
|
||||||
|
generate()
|
||||||
|
],
|
||||||
|
module: "listening"
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (section === 1 || section === 4) {
|
if (section === 1 || section === 4) {
|
||||||
@@ -285,7 +300,8 @@ const EXERCISES: ExerciseGen[] = [
|
|||||||
],
|
],
|
||||||
module: "level"
|
module: "level"
|
||||||
},
|
},
|
||||||
{
|
// Removing this since level supports reading aswell
|
||||||
|
/*{
|
||||||
label: "Reading Passage: Multiple Choice",
|
label: "Reading Passage: Multiple Choice",
|
||||||
type: "passageUtas",
|
type: "passageUtas",
|
||||||
icon: FaBookOpen,
|
icon: FaBookOpen,
|
||||||
@@ -295,11 +311,11 @@ const EXERCISES: ExerciseGen[] = [
|
|||||||
value: "passageUtas"
|
value: "passageUtas"
|
||||||
},
|
},
|
||||||
// in the utas exam there was only mc so I'm assuming short answers are deprecated
|
// in the utas exam there was only mc so I'm assuming short answers are deprecated
|
||||||
/*{
|
//{
|
||||||
label: "Short Answers",
|
// label: "Short Answers",
|
||||||
param: "sa_qty",
|
// param: "sa_qty",
|
||||||
value: "10"
|
// value: "10"
|
||||||
},*/
|
//},
|
||||||
quantity(10, "Multiple Choice Quantity"),
|
quantity(10, "Multiple Choice Quantity"),
|
||||||
{
|
{
|
||||||
label: "Reading Passage Topic",
|
label: "Reading Passage Topic",
|
||||||
@@ -315,7 +331,7 @@ const EXERCISES: ExerciseGen[] = [
|
|||||||
generate()
|
generate()
|
||||||
],
|
],
|
||||||
module: "level"
|
module: "level"
|
||||||
},
|
},*/
|
||||||
{
|
{
|
||||||
label: "Task 1 - Letter",
|
label: "Task 1 - Letter",
|
||||||
type: "writing_letter",
|
type: "writing_letter",
|
||||||
|
|||||||
@@ -130,25 +130,51 @@ const ExercisePicker: React.FC<ExercisePickerProps> = ({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
/*const newExercises = configurations.map((config) => {
|
configurations.forEach((config) => {
|
||||||
switch (config.type) {
|
let queryParams = Object.fromEntries(
|
||||||
case 'writing_letter':
|
Object.entries({
|
||||||
return { ...writingTask(1), level: true };
|
topic: config.params.topic as string,
|
||||||
case 'writing_2':
|
first_topic: config.params.first_topic as string,
|
||||||
return { ...writingTask(2), level: true };
|
second_topic: config.params.second_topic as string,
|
||||||
}
|
}).filter(([_, value]) => value && value !== '')
|
||||||
return undefined;
|
);
|
||||||
}).filter((ex) => ex !== undefined);
|
let query = Object.keys(queryParams).length === 0 ? undefined : queryParams;
|
||||||
dispatch({
|
generate(
|
||||||
type: "UPDATE_SECTION_STATE", payload: {
|
Number(config.type.split('_')[1]),
|
||||||
module: level ? "level" : module as Module, sectionId, update: {
|
"speaking",
|
||||||
exercises: [
|
config.type,
|
||||||
...(sections.find((s) => s.sectionId = sectionId)?.state as LevelPart).exercises,
|
{
|
||||||
...newExercises
|
method: 'GET',
|
||||||
]
|
queryParams: query
|
||||||
}
|
},
|
||||||
}
|
(data: any) => {
|
||||||
})*/
|
switch (Number(config.type.split('_')[1])) {
|
||||||
|
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 [{
|
||||||
|
topic: data.topic,
|
||||||
|
questions: data.questions
|
||||||
|
}];
|
||||||
|
default:
|
||||||
|
return [data];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
levelSectionId,
|
||||||
|
level
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
setLocalSelectedExercises([]);
|
setLocalSelectedExercises([]);
|
||||||
setPickerOpen(false);
|
setPickerOpen(false);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import GenLoader from "../Shared/GenLoader";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import useSectionEdit from "../../Hooks/useSectionEdit";
|
import useSectionEdit from "../../Hooks/useSectionEdit";
|
||||||
import useExamEditorStore from "@/stores/examEditor";
|
import useExamEditorStore from "@/stores/examEditor";
|
||||||
import { InteractiveSpeakingExercise } from "@/interfaces/exam";
|
import { InteractiveSpeakingExercise, LevelPart } from "@/interfaces/exam";
|
||||||
import { BsFileText } from "react-icons/bs";
|
import { BsFileText } from "react-icons/bs";
|
||||||
import { FaChevronLeft, FaChevronRight } from "react-icons/fa6";
|
import { FaChevronLeft, FaChevronRight } from "react-icons/fa6";
|
||||||
import { RiVideoLine } from "react-icons/ri";
|
import { RiVideoLine } from "react-icons/ri";
|
||||||
@@ -23,10 +23,9 @@ interface Props {
|
|||||||
const InteractiveSpeaking: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }) => {
|
const InteractiveSpeaking: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }) => {
|
||||||
const { dispatch } = useExamEditorStore();
|
const { dispatch } = useExamEditorStore();
|
||||||
const [local, setLocal] = useState(exercise);
|
const [local, setLocal] = useState(exercise);
|
||||||
|
|
||||||
const [currentVideoIndex, setCurrentVideoIndex] = useState(0);
|
const [currentVideoIndex, setCurrentVideoIndex] = useState(0);
|
||||||
|
|
||||||
const { generating, genResult, state } = useExamEditorStore(
|
const { generating, genResult, state, levelGenResults, levelGenerating } = useExamEditorStore(
|
||||||
(state) => state.modules[module].sections.find((section) => section.sectionId === sectionId)!
|
(state) => state.modules[module].sections.find((section) => section.sectionId === sectionId)!
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -34,48 +33,94 @@ const InteractiveSpeaking: React.FC<Props> = ({ sectionId, exercise, module = "s
|
|||||||
sectionId,
|
sectionId,
|
||||||
onSave: () => {
|
onSave: () => {
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId: sectionId, update: local, module: module } });
|
|
||||||
|
if (module === "level") {
|
||||||
|
const updatedState = {
|
||||||
|
...state,
|
||||||
|
exercises: (state as LevelPart).exercises.map((ex) =>
|
||||||
|
ex.id === local.id ? local : ex
|
||||||
|
)
|
||||||
|
};
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_STATE",
|
||||||
|
payload: { sectionId, update: updatedState, module }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_STATE",
|
||||||
|
payload: { sectionId, update: local, module }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (genResult) {
|
if (genResult) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||||
payload: {
|
payload: {
|
||||||
sectionId,
|
sectionId,
|
||||||
module: module,
|
module,
|
||||||
field: "genResult",
|
field: "genResult",
|
||||||
value: undefined
|
value: undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const speakingScript = levelGenResults?.find((res) => res.generating === `${local.id}-speakingScript`);
|
||||||
|
if (module === "level" && speakingScript) {
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
||||||
|
sectionId,
|
||||||
|
field: "levelGenResults",
|
||||||
|
value: levelGenResults.filter((res) => res.generating !== `${local.id}-speakingScript`),
|
||||||
|
module
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onDiscard: () => {
|
onDiscard: () => {
|
||||||
setLocal(exercise);
|
setLocal(exercise);
|
||||||
},
|
},
|
||||||
onPractice: () => {
|
onPractice: () => {
|
||||||
const updatedExercise = {
|
const updatedLocal = { ...local, isPractice: !local.isPractice };
|
||||||
...state,
|
setLocal(updatedLocal);
|
||||||
isPractice: !local.isPractice
|
|
||||||
};
|
if (module === "level") {
|
||||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
const updatedState = {
|
||||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: updatedExercise, module: module } });
|
...state,
|
||||||
|
exercises: (state as LevelPart).exercises.map((ex) =>
|
||||||
|
ex.id === local.id ? updatedLocal : ex
|
||||||
|
)
|
||||||
|
};
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_STATE",
|
||||||
|
payload: { sectionId, update: updatedState, module }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_STATE",
|
||||||
|
payload: { sectionId, update: updatedLocal, module }
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (genResult && generating === "speakingScript") {
|
if (genResult && generating === "speakingScript") {
|
||||||
setEditing(true);
|
const updatedLocal = {
|
||||||
setLocal({
|
|
||||||
...local,
|
...local,
|
||||||
title: genResult.result[0].title,
|
title: genResult.result[0].title,
|
||||||
prompts: genResult.result[0].prompts.map((item: any) => ({
|
prompts: genResult.result[0].prompts.map((item: any) => ({
|
||||||
text: item || "",
|
text: item || "",
|
||||||
video_url: ""
|
video_url: ""
|
||||||
}))
|
}))
|
||||||
});
|
};
|
||||||
|
setEditing(true);
|
||||||
|
setLocal(updatedLocal);
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||||
payload: {
|
payload: {
|
||||||
sectionId,
|
sectionId,
|
||||||
module: module,
|
module,
|
||||||
field: "generating",
|
field: "generating",
|
||||||
value: undefined
|
value: undefined
|
||||||
}
|
}
|
||||||
@@ -84,6 +129,90 @@ const InteractiveSpeaking: React.FC<Props> = ({ sectionId, exercise, module = "s
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [genResult, generating]);
|
}, [genResult, generating]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (genResult && generating === "video") {
|
||||||
|
const updatedLocal = { ...local, prompts: genResult.result[0].prompts };
|
||||||
|
setLocal(updatedLocal);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_STATE",
|
||||||
|
payload: { sectionId, update: updatedLocal, module }
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||||
|
payload: {
|
||||||
|
sectionId,
|
||||||
|
module,
|
||||||
|
field: "generating",
|
||||||
|
value: undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [genResult, generating]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const speakingScript = levelGenResults?.find((res) => res.generating === `${local.id}-speakingScript`);
|
||||||
|
const isGenerating = levelGenerating?.includes(`${local.id}-speakingScript`);
|
||||||
|
|
||||||
|
if (speakingScript && isGenerating) {
|
||||||
|
const updatedLocal = {
|
||||||
|
...local,
|
||||||
|
title: speakingScript.result[0].title,
|
||||||
|
prompts: speakingScript.result[0].prompts.map((item: any) => ({
|
||||||
|
text: item || "",
|
||||||
|
video_url: ""
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
setEditing(true);
|
||||||
|
setLocal(updatedLocal);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
||||||
|
sectionId,
|
||||||
|
field: "levelGenerating",
|
||||||
|
value: levelGenerating.filter((g) => g !== `${local.id}-speakingScript`),
|
||||||
|
module
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [levelGenResults, levelGenerating]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const speakingVideo = levelGenResults?.find((res) => res.generating === `${local.id}-video`);
|
||||||
|
const isGenerating = levelGenerating?.includes(`${local.id}-video`);
|
||||||
|
|
||||||
|
if (speakingVideo && isGenerating) {
|
||||||
|
const updatedLocal = { ...local, prompts: speakingVideo.result[0].prompts };
|
||||||
|
setLocal(updatedLocal);
|
||||||
|
|
||||||
|
const updatedState = {
|
||||||
|
...state,
|
||||||
|
exercises: (state as LevelPart).exercises.map((ex) =>
|
||||||
|
ex.id === local.id ? updatedLocal : ex
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_STATE",
|
||||||
|
payload: { sectionId, update: updatedState, module }
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
||||||
|
sectionId,
|
||||||
|
field: "levelGenerating",
|
||||||
|
value: levelGenerating.filter((g) => g !== `${local.id}-video`),
|
||||||
|
module
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [levelGenResults, levelGenerating]);
|
||||||
|
|
||||||
const addPrompt = () => {
|
const addPrompt = () => {
|
||||||
setLocal(prev => ({
|
setLocal(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -150,7 +279,7 @@ const InteractiveSpeaking: React.FC<Props> = ({ sectionId, exercise, module = "s
|
|||||||
module="speaking"
|
module="speaking"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{generating && generating === "speakingScript" ? (
|
{(generating && generating === "speakingScript") || (levelGenerating.find((g) => g === `${local.id}-speakingScript`)) ? (
|
||||||
<GenLoader module={module} />
|
<GenLoader module={module} />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -206,7 +335,7 @@ const InteractiveSpeaking: React.FC<Props> = ({ sectionId, exercise, module = "s
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
{generating && generating === "video" &&
|
{(generating && generating === "video") || levelGenerating.find((g) => g === `${local.id}-video`) &&
|
||||||
<GenLoader module={module} custom="Generating the videos ... This may take a while ..." />
|
<GenLoader module={module} custom="Generating the videos ... This may take a while ..." />
|
||||||
}
|
}
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Header from "../../Shared/Header";
|
|||||||
import GenLoader from "../Shared/GenLoader";
|
import GenLoader from "../Shared/GenLoader";
|
||||||
import useSectionEdit from "../../Hooks/useSectionEdit";
|
import useSectionEdit from "../../Hooks/useSectionEdit";
|
||||||
import useExamEditorStore from "@/stores/examEditor";
|
import useExamEditorStore from "@/stores/examEditor";
|
||||||
import { InteractiveSpeakingExercise } from "@/interfaces/exam";
|
import { InteractiveSpeakingExercise, LevelPart } from "@/interfaces/exam";
|
||||||
import { BsFileText } from "react-icons/bs";
|
import { BsFileText } from "react-icons/bs";
|
||||||
import { RiVideoLine } from 'react-icons/ri';
|
import { RiVideoLine } from 'react-icons/ri';
|
||||||
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa6';
|
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa6';
|
||||||
@@ -19,7 +19,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Speaking1: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }) => {
|
const Speaking1: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }) => {
|
||||||
const {dispatch} = useExamEditorStore();
|
const { dispatch } = useExamEditorStore();
|
||||||
const [local, setLocal] = useState(() => {
|
const [local, setLocal] = useState(() => {
|
||||||
const defaultPrompts = [
|
const defaultPrompts = [
|
||||||
{ text: "Hello my name is {avatar}, what is yours?", video_url: "" },
|
{ text: "Hello my name is {avatar}, what is yours?", video_url: "" },
|
||||||
@@ -31,7 +31,7 @@ const Speaking1: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
|
|||||||
|
|
||||||
const [currentVideoIndex, setCurrentVideoIndex] = useState(0);
|
const [currentVideoIndex, setCurrentVideoIndex] = useState(0);
|
||||||
|
|
||||||
const { generating, genResult , state} = useExamEditorStore(
|
const { generating, genResult, state, levelGenResults, levelGenerating } = useExamEditorStore(
|
||||||
(state) => state.modules[module].sections.find((section) => section.sectionId === sectionId)!
|
(state) => state.modules[module].sections.find((section) => section.sectionId === sectionId)!
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -39,18 +39,48 @@ const Speaking1: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
|
|||||||
sectionId,
|
sectionId,
|
||||||
onSave: () => {
|
onSave: () => {
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId: sectionId, update: local, module } });
|
|
||||||
|
if (module === "level") {
|
||||||
|
const updatedState = {
|
||||||
|
...state,
|
||||||
|
exercises: (state as LevelPart).exercises.map((ex) =>
|
||||||
|
ex.id === local.id ? local : ex
|
||||||
|
)
|
||||||
|
};
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_STATE",
|
||||||
|
payload: { sectionId, update: updatedState, module }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_STATE",
|
||||||
|
payload: { sectionId, update: local, module }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (genResult) {
|
if (genResult) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||||
payload: {
|
payload: {
|
||||||
sectionId,
|
sectionId,
|
||||||
module: module,
|
module,
|
||||||
field: "genResult",
|
field: "genResult",
|
||||||
value: undefined
|
value: undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const speakingScript = levelGenResults?.find((res) => res.generating === `${local.id}-speakingScript`);
|
||||||
|
if (module === "level" && speakingScript) {
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
||||||
|
sectionId,
|
||||||
|
field: "levelGenResults",
|
||||||
|
value: levelGenResults.filter((res) => res.generating !== `${local.id}-speakingScript`),
|
||||||
|
module
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onDiscard: () => {
|
onDiscard: () => {
|
||||||
setLocal({
|
setLocal({
|
||||||
@@ -63,36 +93,52 @@ const Speaking1: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onPractice: () => {
|
onPractice: () => {
|
||||||
const updatedExercise = {
|
const updatedLocal = { ...local, isPractice: !local.isPractice };
|
||||||
...state,
|
setLocal(updatedLocal);
|
||||||
isPractice: !local.isPractice
|
|
||||||
};
|
if (module === "level") {
|
||||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
const updatedState = {
|
||||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: updatedExercise, module: module } });
|
...state,
|
||||||
|
exercises: (state as LevelPart).exercises.map((ex) =>
|
||||||
|
ex.id === local.id ? updatedLocal : ex
|
||||||
|
)
|
||||||
|
};
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_STATE",
|
||||||
|
payload: { sectionId, update: updatedState, module }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_STATE",
|
||||||
|
payload: { sectionId, update: updatedLocal, module }
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (genResult && generating === "speakingScript") {
|
if (genResult && generating === "speakingScript") {
|
||||||
setEditing(true);
|
const updatedLocal = {
|
||||||
setLocal(prev => ({
|
...local,
|
||||||
...prev,
|
|
||||||
first_title: genResult.result[0].first_topic,
|
first_title: genResult.result[0].first_topic,
|
||||||
second_title: genResult.result[0].second_topic,
|
second_title: genResult.result[0].second_topic,
|
||||||
prompts: [
|
prompts: [
|
||||||
prev.prompts[0],
|
local.prompts[0],
|
||||||
prev.prompts[1],
|
local.prompts[1],
|
||||||
...genResult.result[0].prompts.map((item: any) => ({
|
...genResult.result[0].prompts.map((item: any) => ({
|
||||||
text: item,
|
text: item,
|
||||||
video_url: ""
|
video_url: ""
|
||||||
}))
|
}))
|
||||||
]
|
]
|
||||||
}));
|
};
|
||||||
|
setEditing(true);
|
||||||
|
setLocal(updatedLocal);
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||||
payload: {
|
payload: {
|
||||||
sectionId,
|
sectionId,
|
||||||
module: module,
|
module,
|
||||||
field: "generating",
|
field: "generating",
|
||||||
value: undefined
|
value: undefined
|
||||||
}
|
}
|
||||||
@@ -101,6 +147,96 @@ const Speaking1: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [genResult, generating]);
|
}, [genResult, generating]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (genResult && generating === "video") {
|
||||||
|
const updatedLocal = { ...local, prompts: genResult.result[0].prompts };
|
||||||
|
setLocal(updatedLocal);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_STATE",
|
||||||
|
payload: { sectionId, update: updatedLocal, module }
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||||
|
payload: {
|
||||||
|
sectionId,
|
||||||
|
module,
|
||||||
|
field: "generating",
|
||||||
|
value: undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [genResult, generating]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const speakingScript = levelGenResults?.find((res) => res.generating === `${local.id}-speakingScript`);
|
||||||
|
const isGenerating = levelGenerating?.includes(`${local.id}-speakingScript`);
|
||||||
|
|
||||||
|
if (speakingScript && isGenerating) {
|
||||||
|
const updatedLocal = {
|
||||||
|
...local,
|
||||||
|
first_title: speakingScript.result[0].first_topic,
|
||||||
|
second_title: speakingScript.result[0].second_topic,
|
||||||
|
prompts: [
|
||||||
|
local.prompts[0],
|
||||||
|
local.prompts[1],
|
||||||
|
...speakingScript.result[0].prompts.map((item: any) => ({
|
||||||
|
text: item,
|
||||||
|
video_url: ""
|
||||||
|
}))
|
||||||
|
]
|
||||||
|
};
|
||||||
|
setEditing(true);
|
||||||
|
setLocal(updatedLocal);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
||||||
|
sectionId,
|
||||||
|
field: "levelGenerating",
|
||||||
|
value: levelGenerating.filter((g) => g !== `${local.id}-speakingScript`),
|
||||||
|
module
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [levelGenResults, levelGenerating]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const speakingVideo = levelGenResults?.find((res) => res.generating === `${local.id}-video`);
|
||||||
|
const isGenerating = levelGenerating?.includes(`${local.id}-video`);
|
||||||
|
|
||||||
|
if (speakingVideo && isGenerating) {
|
||||||
|
const updatedLocal = { ...local, video_url: speakingVideo.result[0].video_url };
|
||||||
|
setLocal(updatedLocal);
|
||||||
|
|
||||||
|
const updatedState = {
|
||||||
|
...state,
|
||||||
|
exercises: (state as LevelPart).exercises.map((ex) =>
|
||||||
|
ex.id === local.id ? updatedLocal : ex
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_STATE",
|
||||||
|
payload: { sectionId, update: updatedState, module }
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
||||||
|
sectionId,
|
||||||
|
field: "levelGenerating",
|
||||||
|
value: levelGenerating.filter((g) => g !== `${local.id}-video`),
|
||||||
|
module
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [levelGenResults, levelGenerating]);
|
||||||
|
|
||||||
|
|
||||||
const addPrompt = () => {
|
const addPrompt = () => {
|
||||||
setLocal(prev => ({
|
setLocal(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -159,7 +295,7 @@ const Speaking1: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleNextVideo = () => {
|
const handleNextVideo = () => {
|
||||||
setCurrentVideoIndex((prev) =>
|
setCurrentVideoIndex((prev) =>
|
||||||
(prev < local.prompts.length - 1 ? prev + 1 : prev)
|
(prev < local.prompts.length - 1 ? prev + 1 : prev)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -179,7 +315,7 @@ const Speaking1: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
|
|||||||
module="speaking"
|
module="speaking"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{generating && generating === "speakingScript" ? (
|
{(generating && generating === "speakingScript") || (levelGenerating.find((g) => g === `${local.id}-speakingScript`)) ? (
|
||||||
<GenLoader module={module} />
|
<GenLoader module={module} />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -287,8 +423,8 @@ const Speaking1: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
|
|||||||
onClick={handlePrevVideo}
|
onClick={handlePrevVideo}
|
||||||
disabled={currentVideoIndex === 0}
|
disabled={currentVideoIndex === 0}
|
||||||
className={`p-2 rounded-full ${currentVideoIndex === 0
|
className={`p-2 rounded-full ${currentVideoIndex === 0
|
||||||
? 'text-gray-400 cursor-not-allowed'
|
? 'text-gray-400 cursor-not-allowed'
|
||||||
: 'text-gray-600 hover:bg-gray-100'
|
: 'text-gray-600 hover:bg-gray-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaChevronLeft className="w-4 h-4" />
|
<FaChevronLeft className="w-4 h-4" />
|
||||||
@@ -300,8 +436,8 @@ const Speaking1: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
|
|||||||
onClick={handleNextVideo}
|
onClick={handleNextVideo}
|
||||||
disabled={currentVideoIndex === local.prompts.length - 1}
|
disabled={currentVideoIndex === local.prompts.length - 1}
|
||||||
className={`p-2 rounded-full ${currentVideoIndex === local.prompts.length - 1
|
className={`p-2 rounded-full ${currentVideoIndex === local.prompts.length - 1
|
||||||
? 'text-gray-400 cursor-not-allowed'
|
? 'text-gray-400 cursor-not-allowed'
|
||||||
: 'text-gray-600 hover:bg-gray-100'
|
: 'text-gray-600 hover:bg-gray-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaChevronRight className="w-4 h-4" />
|
<FaChevronRight className="w-4 h-4" />
|
||||||
@@ -323,7 +459,7 @@ const Speaking1: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
{generating && generating === "video" &&
|
{(generating && generating === "video") || levelGenerating.find((g) => g === `${local.id}-video`) &&
|
||||||
<GenLoader module={module} custom="Generating the videos ... This may take a while ..." />
|
<GenLoader module={module} custom="Generating the videos ... This may take a while ..." />
|
||||||
}
|
}
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea";
|
import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { AiOutlinePlus, AiOutlineDelete } from 'react-icons/ai';
|
import { AiOutlinePlus, AiOutlineDelete } from 'react-icons/ai';
|
||||||
import { SpeakingExercise } from "@/interfaces/exam";
|
import { LevelPart, SpeakingExercise } from "@/interfaces/exam";
|
||||||
import useExamEditorStore from "@/stores/examEditor";
|
import useExamEditorStore from "@/stores/examEditor";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import useSectionEdit from "../../Hooks/useSectionEdit";
|
import useSectionEdit from "../../Hooks/useSectionEdit";
|
||||||
@@ -20,61 +20,104 @@ interface Props {
|
|||||||
module?: Module;
|
module?: Module;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const Speaking2: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }) => {
|
const Speaking2: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }) => {
|
||||||
const { dispatch } = useExamEditorStore();
|
const { dispatch } = useExamEditorStore();
|
||||||
const [local, setLocal] = useState(exercise);
|
const [local, setLocal] = useState(exercise);
|
||||||
|
|
||||||
const { generating, genResult, state } = useExamEditorStore(
|
const { sections } = useExamEditorStore((store) => store.modules[module]);
|
||||||
(state) => state.modules[module].sections.find((section) => section.sectionId === sectionId)!
|
const section = sections.find((section) => section.sectionId === sectionId)!;
|
||||||
);
|
const { generating, genResult, state, levelGenResults, levelGenerating } = section;
|
||||||
|
|
||||||
const { editing, setEditing, handleSave, handleDiscard, handleEdit, handlePractice } = useSectionEdit({
|
const { editing, setEditing, handleSave, handleDiscard, handleEdit, handlePractice } = useSectionEdit({
|
||||||
sectionId,
|
sectionId,
|
||||||
onSave: () => {
|
onSave: () => {
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId: sectionId, update: local , module} });
|
|
||||||
|
if (module === "level") {
|
||||||
|
const updatedState = {
|
||||||
|
...state,
|
||||||
|
exercises: (state as LevelPart).exercises.map((ex) =>
|
||||||
|
ex.id === local.id ? local : ex
|
||||||
|
)
|
||||||
|
};
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_STATE",
|
||||||
|
payload: { sectionId, update: updatedState, module }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_STATE",
|
||||||
|
payload: { sectionId, update: local, module }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (genResult) {
|
if (genResult) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||||
payload: {
|
payload: {
|
||||||
sectionId,
|
sectionId,
|
||||||
module: module,
|
module,
|
||||||
field: "genResult",
|
field: "genResult",
|
||||||
value: undefined
|
value: undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const speakingScript = levelGenResults.find((res) => res.generating === `${local.id}-speakingScript`)
|
||||||
|
if (module === "level" && speakingScript) {
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
||||||
|
sectionId,
|
||||||
|
field: "levelGenResults",
|
||||||
|
value: section!.levelGenResults.filter((res) => res.generating !== `${local.id ? `${local.id}-` : ''}speakingScript`),
|
||||||
|
module
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onDiscard: () => {
|
onDiscard: () => {
|
||||||
setLocal(exercise);
|
setLocal(exercise);
|
||||||
},
|
},
|
||||||
onPractice: () => {
|
onPractice: () => {
|
||||||
const updatedExercise = {
|
const updatedLocal = { ...local, isPractice: !local.isPractice };
|
||||||
...state,
|
setLocal(updatedLocal);
|
||||||
isPractice: !local.isPractice
|
|
||||||
};
|
if (module === "level") {
|
||||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
const updatedState = {
|
||||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: updatedExercise, module: module } });
|
...state,
|
||||||
|
exercises: (state as LevelPart).exercises.map((ex) =>
|
||||||
|
ex.id === local.id ? updatedLocal : ex
|
||||||
|
)
|
||||||
|
};
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_STATE",
|
||||||
|
payload: { sectionId, update: updatedState, module }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_STATE",
|
||||||
|
payload: { sectionId, update: updatedLocal, module }
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (genResult && generating === "speakingScript") {
|
if (genResult && generating === "speakingScript") {
|
||||||
setEditing(true);
|
const updatedLocal = {
|
||||||
setLocal({
|
|
||||||
...local,
|
...local,
|
||||||
title: genResult.result[0].topic,
|
title: genResult.result[0].topic,
|
||||||
text: genResult.result[0].question,
|
text: genResult.result[0].question,
|
||||||
prompts: genResult.result[0].prompts
|
prompts: genResult.result[0].prompts
|
||||||
});
|
};
|
||||||
|
setEditing(true);
|
||||||
|
setLocal(updatedLocal);
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||||
payload: {
|
payload: {
|
||||||
sectionId,
|
sectionId,
|
||||||
module: module,
|
module,
|
||||||
field: "generating",
|
field: "generating",
|
||||||
value: undefined
|
value: undefined
|
||||||
}
|
}
|
||||||
@@ -85,13 +128,19 @@ const Speaking2: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (genResult && generating === "video") {
|
if (genResult && generating === "video") {
|
||||||
setLocal({...local, video_url: genResult.result[0].video_url});
|
const updatedLocal = { ...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} });
|
setLocal(updatedLocal);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_STATE",
|
||||||
|
payload: { sectionId, update: updatedLocal, module }
|
||||||
|
});
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||||
payload: {
|
payload: {
|
||||||
sectionId,
|
sectionId,
|
||||||
module: module,
|
module,
|
||||||
field: "generating",
|
field: "generating",
|
||||||
value: undefined
|
value: undefined
|
||||||
}
|
}
|
||||||
@@ -100,6 +149,62 @@ const Speaking2: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [genResult, generating]);
|
}, [genResult, generating]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const speakingScript = levelGenResults.find((res) => res.generating === `${local.id}-speakingScript`);
|
||||||
|
const generating = levelGenerating.find((res) => res === `${local.id}-speakingScript`);
|
||||||
|
if (speakingScript && generating) {
|
||||||
|
const updatedLocal = {
|
||||||
|
...local,
|
||||||
|
title: speakingScript.result[0].topic,
|
||||||
|
text: speakingScript.result[0].question,
|
||||||
|
prompts: speakingScript.result[0].prompts
|
||||||
|
};
|
||||||
|
setEditing(true);
|
||||||
|
setLocal(updatedLocal);
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
||||||
|
sectionId,
|
||||||
|
field: "levelGenerating",
|
||||||
|
value: section!.levelGenerating.filter((g) => g !== `${local.id ? `${local.id}-` : ''}speakingScript`),
|
||||||
|
module
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [levelGenResults, levelGenerating]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const speakingVideo = levelGenResults.find((res) => res.generating === `${local.id}-video`);
|
||||||
|
const generating = levelGenerating.find((res) => res === `${local.id}-video`);
|
||||||
|
|
||||||
|
if (speakingVideo && generating) {
|
||||||
|
const updatedLocal = { ...local, video_url: speakingVideo.result[0].video_url };
|
||||||
|
setLocal(updatedLocal);
|
||||||
|
|
||||||
|
const updatedState = {
|
||||||
|
...state,
|
||||||
|
exercises: (state as LevelPart).exercises.map((ex) =>
|
||||||
|
ex.id === local.id ? updatedLocal : ex
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_STATE",
|
||||||
|
payload: { sectionId, update: updatedState, module }
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
||||||
|
sectionId,
|
||||||
|
field: "levelGenerating",
|
||||||
|
value: section!.levelGenerating.filter((g) => g !== `${local.id ? `${local.id}-` : ''}video`),
|
||||||
|
module
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [levelGenResults, levelGenerating]);
|
||||||
|
|
||||||
const addPrompt = () => {
|
const addPrompt = () => {
|
||||||
setLocal(prev => ({
|
setLocal(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -143,7 +248,7 @@ const Speaking2: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
|
|||||||
<>
|
<>
|
||||||
<div className='relative pb-4'>
|
<div className='relative pb-4'>
|
||||||
<Header
|
<Header
|
||||||
title={`Speaking ${sectionId} Script`}
|
title={`Speaking ${module === "level" ? local.sectionId : sectionId} Script`}
|
||||||
description='Generate or write the script for the video.'
|
description='Generate or write the script for the video.'
|
||||||
editing={editing}
|
editing={editing}
|
||||||
handleSave={handleSave}
|
handleSave={handleSave}
|
||||||
@@ -154,7 +259,7 @@ const Speaking2: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
|
|||||||
module="speaking"
|
module="speaking"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{generating && generating === "speakingScript" ? (
|
{((generating && generating === "speakingScript") || (levelGenerating.find((g) => g === `${local.id}-speakingScript`))) ? (
|
||||||
<GenLoader module={module} />
|
<GenLoader module={module} />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -263,14 +368,14 @@ const Speaking2: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 w-full items-center">
|
<div className="flex flex-col gap-4 w-full items-center">
|
||||||
<video controls className="w-full rounded-xl">
|
<video controls className="w-full rounded-xl">
|
||||||
<source src={local.video_url } />
|
<source src={local.video_url} />
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
}
|
}
|
||||||
{generating && generating === "video" &&
|
{((generating === "video") || (levelGenerating.find((g) => g === `${local.id}-video`) !== undefined)) &&
|
||||||
<GenLoader module={module} custom="Generating the video ... This may take a while ..." />
|
<GenLoader module={module} custom="Generating the video ... This may take a while ..." />
|
||||||
}
|
}
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -8,28 +8,20 @@ import { Module } from "@/interfaces";
|
|||||||
interface Props {
|
interface Props {
|
||||||
sectionId: number;
|
sectionId: number;
|
||||||
exercise: SpeakingExercise | InteractiveSpeakingExercise;
|
exercise: SpeakingExercise | InteractiveSpeakingExercise;
|
||||||
qId?: number;
|
|
||||||
module: Module;
|
module: Module;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Speaking: React.FC<Props> = ({ sectionId, exercise, qId, module = "speaking" }) => {
|
const Speaking: React.FC<Props> = ({ sectionId, module = "speaking" }) => {
|
||||||
const { dispatch } = useExamEditorStore();
|
|
||||||
const { state } = useExamEditorStore(
|
const { state } = useExamEditorStore(
|
||||||
(state) => state.modules[module].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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div tabIndex={0} className="mx-auto p-3 space-y-6" onFocus={onFocus}>
|
<div className="mx-auto p-3 space-y-6">
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="flex flex-col space-y-6">
|
<div className="flex flex-col space-y-6">
|
||||||
{sectionId === 1 && <Speaking1 sectionId={sectionId} exercise={state as InteractiveSpeakingExercise} />}
|
{sectionId === 1 && <Speaking1 sectionId={sectionId} exercise={state as InteractiveSpeakingExercise } />}
|
||||||
{sectionId === 2 && <Speaking2 sectionId={sectionId} exercise={state as SpeakingExercise} />}
|
{sectionId === 2 && <Speaking2 sectionId={sectionId} exercise={state as SpeakingExercise} />}
|
||||||
{sectionId === 3 && <InteractiveSpeaking sectionId={sectionId} exercise={state as InteractiveSpeakingExercise} />}
|
{sectionId === 3 && <InteractiveSpeaking sectionId={sectionId} exercise={state as InteractiveSpeakingExercise} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ const Writing: React.FC<Props> = ({ sectionId, exercise, module, index }) => {
|
|||||||
description='Generate or edit the instructions for the task'
|
description='Generate or edit the instructions for the task'
|
||||||
editing={editing}
|
editing={editing}
|
||||||
handleSave={handleSave}
|
handleSave={handleSave}
|
||||||
handleDelete={handleDelete}
|
handleDelete={module == "level" ? handleDelete : undefined}
|
||||||
handleEdit={handleEdit}
|
handleEdit={handleEdit}
|
||||||
handleDiscard={handleDiscard}
|
handleDiscard={handleDiscard}
|
||||||
handlePractice={handlePractice}
|
handlePractice={handlePractice}
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ const ListeningContext: React.FC<Props> = ({ sectionId, module, listeningSection
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(()=> {
|
||||||
|
if (listeningPart.script == undefined) {
|
||||||
|
setScriptLocal(undefined);
|
||||||
|
}
|
||||||
|
}, [listeningPart])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (genResult && generating === "listeningScript") {
|
if (genResult && generating === "listeningScript") {
|
||||||
setEditing(true);
|
setEditing(true);
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ const ReadingContext: React.FC<Props> = ({ sectionId, module, level = false }) =
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(()=> {
|
||||||
|
if (readingPart.text === undefined) {
|
||||||
|
setTitle('');
|
||||||
|
setContent('');
|
||||||
|
}
|
||||||
|
}, [readingPart])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (genResult && genResult.generating === "passage") {
|
if (genResult && genResult.generating === "passage") {
|
||||||
setEditing(true);
|
setEditing(true);
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import TrueFalse from "../../Exercises/TrueFalse";
|
|||||||
import fillBlanks from "./fillBlanks";
|
import fillBlanks from "./fillBlanks";
|
||||||
import MatchSentences from "../../Exercises/MatchSentences";
|
import MatchSentences from "../../Exercises/MatchSentences";
|
||||||
import Writing from "../../Exercises/Writing";
|
import Writing from "../../Exercises/Writing";
|
||||||
import Speaking from "../../Exercises/Speaking";
|
import Speaking2 from "../../Exercises/Speaking/Speaking2";
|
||||||
|
import Speaking1 from "../../Exercises/Speaking/Speaking1";
|
||||||
|
import InteractiveSpeaking from "../../Exercises/Speaking/InteractiveSpeaking";
|
||||||
|
|
||||||
const getExerciseItems = (exercises: Exercise[], sectionId: number): ExerciseItem[] => {
|
const getExerciseItems = (exercises: Exercise[], sectionId: number): ExerciseItem[] => {
|
||||||
const items: ExerciseItem[] = exercises.map((exercise, index) => {
|
const items: ExerciseItem[] = exercises.map((exercise, index) => {
|
||||||
@@ -79,41 +81,42 @@ const getExerciseItems = (exercises: Exercise[], sectionId: number): ExerciseIte
|
|||||||
),
|
),
|
||||||
content: <Writing key={exercise.id} exercise={exercise} sectionId={sectionId} index={index} module="level" />
|
content: <Writing key={exercise.id} exercise={exercise} sectionId={sectionId} index={index} module="level" />
|
||||||
};
|
};
|
||||||
case "speaking":
|
case "speaking":
|
||||||
return {
|
return {
|
||||||
|
exerciseId: exercise.id,
|
||||||
id: index.toString(),
|
id: index.toString(),
|
||||||
sectionId,
|
sectionId,
|
||||||
label: (
|
label: (
|
||||||
<ExerciseLabel
|
<ExerciseLabel
|
||||||
type={`Speaking Section 2`}
|
type={`Speaking Section 2: Question`}
|
||||||
firstId={exercise.sectionId!.toString()}
|
firstId={(index+1).toString()}
|
||||||
lastId={exercise.sectionId!.toString()}
|
lastId={(index+1).toString()}
|
||||||
prompt={exercise.prompts[2]}
|
prompt={exercise.prompts[2]}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
content: <Speaking key={exercise.id} exercise={exercise} sectionId={sectionId} qId={index} module="level" />
|
content: <Speaking2 key={exercise.id} exercise={exercise} sectionId={sectionId} module="level" />
|
||||||
};
|
};
|
||||||
case "interactiveSpeaking":
|
case "interactiveSpeaking":
|
||||||
|
const content = exercise.sectionId === 1 ? <Speaking1 key={exercise.id} exercise={exercise} sectionId={sectionId} module="level" /> :
|
||||||
|
<InteractiveSpeaking key={exercise.id} exercise={exercise} sectionId={sectionId} module="level"/>
|
||||||
return {
|
return {
|
||||||
|
exerciseId: exercise.id,
|
||||||
id: index.toString(),
|
id: index.toString(),
|
||||||
sectionId,
|
sectionId,
|
||||||
label: (
|
label: (
|
||||||
<ExerciseLabel
|
<ExerciseLabel
|
||||||
type={`Speaking Section 2`}
|
type={`${exercise.sectionId === 1 ? 'Speaking Section 1': 'Interactive Speaking'}: Question`}
|
||||||
firstId={exercise.sectionId!.toString()}
|
firstId={(index+1).toString()}
|
||||||
lastId={exercise.sectionId!.toString()}
|
lastId={(index+1).toString()}
|
||||||
prompt={exercise.prompts[2].text}
|
prompt={exercise.prompts[2].text}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
content: <Speaking key={exercise.id} exercise={exercise} sectionId={sectionId} qId={index} module="level" />
|
content: content
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return {} as unknown as ExerciseItem;
|
return {} as unknown as ExerciseItem;
|
||||||
}
|
}
|
||||||
}).filter(isExerciseItem);
|
}).filter(isExerciseItem);
|
||||||
/*return mappedItems.filter((item): item is ExerciseItem =>
|
|
||||||
item !== null && isExerciseItem(item)
|
|
||||||
);*/
|
|
||||||
return items;
|
return items;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||||
import SortableSection from "../../Shared/SortableSection";
|
import SortableSection from "../../Shared/SortableSection";
|
||||||
import { Exercise, LevelPart, ListeningPart, ReadingPart, SpeakingExercise, WritingExercise } from "@/interfaces/exam";
|
import { Exercise, InteractiveSpeakingExercise, LevelPart, ListeningPart, ReadingPart, SpeakingExercise, WritingExercise } from "@/interfaces/exam";
|
||||||
import ExerciseItem from "./types";
|
import ExerciseItem from "./types";
|
||||||
import Dropdown from "@/components/Dropdown";
|
import Dropdown from "@/components/Dropdown";
|
||||||
import useExamEditorStore from "@/stores/examEditor";
|
import useExamEditorStore from "@/stores/examEditor";
|
||||||
@@ -20,6 +20,7 @@ import React from "react";
|
|||||||
import getExerciseItems from "./exercises";
|
import getExerciseItems from "./exercises";
|
||||||
import { Action } from "@/stores/examEditor/reducers";
|
import { Action } from "@/stores/examEditor/reducers";
|
||||||
import { writingTask } from "@/stores/examEditor/sections";
|
import { writingTask } from "@/stores/examEditor/sections";
|
||||||
|
import { createSpeakingExercise } from "./speaking";
|
||||||
|
|
||||||
|
|
||||||
interface QuestionItemsResult {
|
interface QuestionItemsResult {
|
||||||
@@ -119,14 +120,14 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
|
|||||||
sectionId,
|
sectionId,
|
||||||
module: "level",
|
module: "level",
|
||||||
update: {
|
update: {
|
||||||
exercises: [...(sectionState as ExamPart).exercises,
|
exercises: [...(sectionState as ExamPart).exercises,
|
||||||
...results.map((res)=> {
|
...results.map((res) => {
|
||||||
return {
|
return {
|
||||||
...writingTask(res.generating === "writing_letter" ? 1 : 2),
|
...writingTask(res.generating === "writing_letter" ? 1 : 2),
|
||||||
prompt: res.result[0].prompt,
|
prompt: res.result[0].prompt,
|
||||||
variant: res.generating === "writing_letter" ? "letter" : "essay"
|
variant: res.generating === "writing_letter" ? "letter" : "essay"
|
||||||
} as WritingExercise;
|
} as WritingExercise;
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,6 +157,46 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [levelGenResults, sectionState, levelGenerating, sectionId, currentModule]);
|
}, [levelGenResults, sectionState, levelGenerating, sectionId, currentModule]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (levelGenResults && levelGenResults.some(res => res.generating.startsWith("speaking"))) {
|
||||||
|
const results = levelGenResults.filter(res => res.generating.startsWith("speaking"));
|
||||||
|
const updates = [
|
||||||
|
{
|
||||||
|
type: "UPDATE_SECTION_STATE",
|
||||||
|
payload: {
|
||||||
|
sectionId,
|
||||||
|
module: "level",
|
||||||
|
update: {
|
||||||
|
exercises: [...(sectionState as ExamPart).exercises,
|
||||||
|
...results.map(createSpeakingExercise)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 currentSection = sections.find((s) => s.sectionId === sectionId)!;
|
||||||
|
|
||||||
@@ -199,7 +240,13 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
|
|||||||
const filteredItems = (questions.items ?? []).filter(isValidItem);
|
const filteredItems = (questions.items ?? []).filter(isValidItem);
|
||||||
// #############################################################################
|
// #############################################################################
|
||||||
|
|
||||||
console.log(levelGenerating);
|
|
||||||
|
const onFocus = (questionId: string, id: string | undefined) => {
|
||||||
|
if (id) {
|
||||||
|
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { module: currentModule, sectionId, field: "focusedExercise", value: { questionId, id} } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
@@ -223,7 +270,7 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
|
|||||||
customTitle={item.label}
|
customTitle={item.label}
|
||||||
contentWrapperClassName="rounded-xl"
|
contentWrapperClassName="rounded-xl"
|
||||||
>
|
>
|
||||||
<div className="p-4 shadow-inner border border-gray-200 bg-gray-50 rounded-xl">
|
<div tabIndex={4} className="p-4 shadow-inner border border-gray-200 bg-gray-50 rounded-xl" onFocus={() => onFocus(item.id, item.exerciseId)}>
|
||||||
{item.content}
|
{item.content}
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
@@ -237,9 +284,9 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
|
|||||||
{currentModule === "level" && (
|
{currentModule === "level" && (
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
questions.ids?.length === 0 && !levelGenerating?.some((g) => g?.startsWith("exercises") || g?.startsWith("writing")) && generating !== "exercises"
|
questions.ids?.length === 0 && !levelGenerating?.some((g) => g?.startsWith("exercises") || g?.startsWith("writing") || g?.startsWith("speaking")) && generating !== "exercises"
|
||||||
&& background(<span className="flex justify-center">Generated exercises will appear here!</span>)}
|
&& 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" />}
|
{levelGenerating?.some((g) => g?.startsWith("exercises") || g?.startsWith("writing") || g?.startsWith("speaking")) && <GenLoader module={currentModule} className="mt-4" />}
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
</DndContext >
|
</DndContext >
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { InteractiveSpeakingExercise, SpeakingExercise } from "@/interfaces/exam";
|
||||||
|
import { speakingTask } from "@/stores/examEditor/sections";
|
||||||
|
|
||||||
|
export const createSpeakingExercise = (res: any) => {
|
||||||
|
const taskNumber = Number(res.generating.split("_")[1]);
|
||||||
|
const baseExercise = speakingTask(taskNumber);
|
||||||
|
return {
|
||||||
|
...baseExercise,
|
||||||
|
...getSpeakingTaskData(taskNumber, res.result[0])
|
||||||
|
} as SpeakingExercise | InteractiveSpeakingExercise;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSpeakingTaskData = (taskNumber: number, data: any) => {
|
||||||
|
switch (taskNumber) {
|
||||||
|
case 1:
|
||||||
|
return {
|
||||||
|
first_title: data.first_topic,
|
||||||
|
second_title: data.second_topic,
|
||||||
|
prompts: [
|
||||||
|
...data.prompts.map((item: any) => ({
|
||||||
|
text: item,
|
||||||
|
video_url: ""
|
||||||
|
}))
|
||||||
|
],
|
||||||
|
sectionId: 1,
|
||||||
|
};
|
||||||
|
case 2:
|
||||||
|
return {
|
||||||
|
title: data.topic,
|
||||||
|
text: data.question,
|
||||||
|
prompts: data.prompts,
|
||||||
|
sectionId: 2,
|
||||||
|
type: "speaking"
|
||||||
|
};
|
||||||
|
case 3:
|
||||||
|
return {
|
||||||
|
title: data.topic,
|
||||||
|
prompts: data.questions.map((item: any) => ({
|
||||||
|
text: item || "",
|
||||||
|
video_url: ""
|
||||||
|
})),
|
||||||
|
sectionId: 3,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -3,6 +3,7 @@ export default interface ExerciseItem {
|
|||||||
sectionId: number;
|
sectionId: number;
|
||||||
label: React.ReactNode;
|
label: React.ReactNode;
|
||||||
content: React.ReactNode;
|
content: React.ReactNode;
|
||||||
|
exerciseId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isExerciseItem(item: unknown): item is ExerciseItem {
|
export function isExerciseItem(item: unknown): item is ExerciseItem {
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { Action } from "@/stores/examEditor/reducers";
|
||||||
|
import { ExamPart, Generating } from "@/stores/examEditor/types";
|
||||||
|
import { createSpeakingExercise } from "./speaking";
|
||||||
|
import { writingTask } from "@/stores/examEditor/sections";
|
||||||
|
import { WritingExercise } from "@/interfaces/exam";
|
||||||
|
|
||||||
|
const getResults = (results: any[], type: 'writing' | 'speaking') => {
|
||||||
|
return results.map((res) => {
|
||||||
|
if (type === 'writing') {
|
||||||
|
return {
|
||||||
|
...writingTask(res.generating === "writing_letter" ? 1 : 2),
|
||||||
|
prompt: res.result[0].prompt,
|
||||||
|
variant: res.generating === "writing_letter" ? "letter" : "essay"
|
||||||
|
} as WritingExercise;
|
||||||
|
}
|
||||||
|
return createSpeakingExercise(res);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updates = (
|
||||||
|
results: any[],
|
||||||
|
sectionState: ExamPart,
|
||||||
|
sectionId: number,
|
||||||
|
currentModule: string,
|
||||||
|
levelGenerating: any[],
|
||||||
|
levelGenResults: any[],
|
||||||
|
type: 'writing' | 'speaking'
|
||||||
|
): Action[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: "UPDATE_SECTION_STATE",
|
||||||
|
payload: {
|
||||||
|
sectionId,
|
||||||
|
module: "level",
|
||||||
|
update: {
|
||||||
|
exercises: [
|
||||||
|
...(sectionState as ExamPart).exercises,
|
||||||
|
...getResults(results, type)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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[];
|
||||||
|
};
|
||||||
@@ -37,7 +37,7 @@ export function generate(
|
|||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||||
payload: { sectionId : sectionId, module: level ? "level" : module, field: level ? "levelGenerating" : "generating", value: generatingUpdate }
|
payload: { sectionId, module: level ? "level" : module, field: level ? "levelGenerating" : "generating", value: generatingUpdate }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ export function generate(
|
|||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||||
payload: { sectionId, module: level ? "level" : module, field: level ? "levelGenResults" : "genResult", value: genResults }
|
payload: { sectionId: level ? levelSectionId! : sectionId, module: level ? "level" : module, field: level ? "levelGenResults" : "genResult", value: genResults }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,16 +12,17 @@ interface Props {
|
|||||||
genType: Generating;
|
genType: Generating;
|
||||||
generateFnc: (sectionId: number) => void
|
generateFnc: (sectionId: number) => void
|
||||||
className?: string;
|
className?: string;
|
||||||
levelId?: number;
|
|
||||||
level?: boolean;
|
level?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GenerateBtn: React.FC<Props> = ({ module, sectionId, genType, generateFnc, className, level = false, levelId }) => {
|
const GenerateBtn: React.FC<Props> = ({ module, sectionId, genType, generateFnc, className, level = false }) => {
|
||||||
const section = useExamEditorStore((store) => store.modules[level ? "level" : module].sections.find((s) => s.sectionId == levelId ? levelId : sectionId));
|
const section = useExamEditorStore((store) => store.modules[level ? "level" : module].sections.find((s) => s.sectionId == sectionId));
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const generating = section?.generating;
|
const generating = section?.generating;
|
||||||
|
const genResult = section?.genResult;
|
||||||
const levelGenerating = section?.levelGenerating;
|
const levelGenerating = section?.levelGenerating;
|
||||||
|
const levelGenResults = section?.levelGenResults;
|
||||||
|
|
||||||
useEffect(()=> {
|
useEffect(()=> {
|
||||||
const gen = level ? levelGenerating?.find(g => g === genType) !== undefined : (generating !== undefined && generating === genType);
|
const gen = level ? levelGenerating?.find(g => g === genType) !== undefined : (generating !== undefined && generating === genType);
|
||||||
@@ -29,7 +30,7 @@ const GenerateBtn: React.FC<Props> = ({ module, sectionId, genType, generateFnc,
|
|||||||
setLoading(gen);
|
setLoading(gen);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [generating, levelGenerating])
|
}, [generating, levelGenerating, levelGenResults, genResult])
|
||||||
|
|
||||||
if (section === undefined) return <></>;
|
if (section === undefined) return <></>;
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ const GenerateBtn: React.FC<Props> = ({ module, sectionId, genType, generateFnc,
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
onClick={loading ? () => { } : () => generateFnc(levelId ? levelId : sectionId)}
|
onClick={loading ? () => { } : () => generateFnc(sectionId)}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div key={`section-${sectionId}`} className="flex items-center justify-center">
|
<div key={`section-${sectionId}`} className="flex items-center justify-center">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Module } from "@/interfaces";
|
|||||||
import useExamEditorStore from "@/stores/examEditor";
|
import useExamEditorStore from "@/stores/examEditor";
|
||||||
import Dropdown from "./SettingsDropdown";
|
import Dropdown from "./SettingsDropdown";
|
||||||
import { LevelSectionSettings } from "@/stores/examEditor/types";
|
import { LevelSectionSettings } from "@/stores/examEditor/types";
|
||||||
|
import { LevelPart } from '@/interfaces/exam';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
module: Module;
|
module: Module;
|
||||||
@@ -19,13 +20,15 @@ const SectionPicker: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { dispatch } = useExamEditorStore();
|
const { dispatch } = useExamEditorStore();
|
||||||
const [selectedValue, setSelectedValue] = React.useState<number | undefined>(undefined);
|
const [selectedValue, setSelectedValue] = React.useState<number | undefined>(undefined);
|
||||||
|
|
||||||
const sectionState = useExamEditorStore(state =>
|
const sectionState = useExamEditorStore(state =>
|
||||||
state.modules["level"].sections.find((s) => s.sectionId === sectionId)
|
state.modules["level"].sections.find((s) => s.sectionId === sectionId)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const state = sectionState?.state as LevelPart;
|
||||||
|
|
||||||
if (sectionState === undefined) return null;
|
if (sectionState === undefined) return null;
|
||||||
|
|
||||||
const { readingSection, listeningSection } = sectionState;
|
const { readingSection, listeningSection } = sectionState;
|
||||||
const currentValue = selectedValue ?? (module === "reading" ? readingSection : listeningSection);
|
const currentValue = selectedValue ?? (module === "reading" ? readingSection : listeningSection);
|
||||||
const options = module === "reading" ? [1, 2, 3] : [1, 2, 3, 4];
|
const options = module === "reading" ? [1, 2, 3] : [1, 2, 3, 4];
|
||||||
@@ -34,16 +37,44 @@ const SectionPicker: React.FC<Props> = ({
|
|||||||
const handleSectionChange = (value: number) => {
|
const handleSectionChange = (value: number) => {
|
||||||
const newValue = currentValue === value ? undefined : value;
|
const newValue = currentValue === value ? undefined : value;
|
||||||
setSelectedValue(newValue);
|
setSelectedValue(newValue);
|
||||||
|
let update = {};
|
||||||
|
if (module == "reading") {
|
||||||
|
update = {
|
||||||
|
text: undefined
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (state.audio?.source) {
|
||||||
|
URL.revokeObjectURL(state.audio.source)
|
||||||
|
}
|
||||||
|
update = {
|
||||||
|
audio: undefined,
|
||||||
|
script: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
type: "UPDATE_SECTION_STATE",
|
||||||
payload: {
|
payload: {
|
||||||
sectionId,
|
sectionId,
|
||||||
module: "level",
|
module: "level",
|
||||||
field: module === "reading" ? "readingSection" : "listeningSection",
|
update: {
|
||||||
value: newValue
|
...state,
|
||||||
|
...update
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||||
|
payload: {
|
||||||
|
sectionId,
|
||||||
|
module: "level",
|
||||||
|
field: module === "reading" ? "readingSection" : "listeningSection",
|
||||||
|
value: newValue
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTitle = () => {
|
const getTitle = () => {
|
||||||
@@ -69,7 +100,7 @@ const SectionPicker: React.FC<Props> = ({
|
|||||||
className={`
|
className={`
|
||||||
flex items-center space-x-3 font-semibold cursor-pointer p-2 rounded
|
flex items-center space-x-3 font-semibold cursor-pointer p-2 rounded
|
||||||
transition-colors duration-200
|
transition-colors duration-200
|
||||||
${currentValue === num
|
${currentValue === num
|
||||||
? `bg-ielts-${module}/90 text-white`
|
? `bg-ielts-${module}/90 text-white`
|
||||||
: `hover:bg-ielts-${module}/70 text-gray-700`}
|
: `hover:bg-ielts-${module}/70 text-gray-700`}
|
||||||
`}
|
`}
|
||||||
@@ -81,7 +112,7 @@ const SectionPicker: React.FC<Props> = ({
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={currentValue === num}
|
checked={currentValue === num}
|
||||||
onChange={() => {}}
|
onChange={() => { }}
|
||||||
className={`
|
className={`
|
||||||
h-5 w-5 cursor-pointer
|
h-5 w-5 cursor-pointer
|
||||||
accent-ielts-${module}
|
accent-ielts-${module}
|
||||||
|
|||||||
@@ -55,47 +55,180 @@ const LevelSettings: React.FC = () => {
|
|||||||
const readingSection = section.readingSection;
|
const readingSection = section.readingSection;
|
||||||
const listeningSection = section.listeningSection;
|
const listeningSection = section.listeningSection;
|
||||||
|
|
||||||
const canPreview = currentSection.exercises.length > 0;
|
const canPreviewOrSubmit = sections.length > 0 && sections.some(s => {
|
||||||
|
const part = s.state as LevelPart;
|
||||||
|
return part.exercises.length > 0 && part.exercises.every((exercise) => {
|
||||||
|
if (exercise.type === 'speaking') {
|
||||||
|
return exercise.title !== '' &&
|
||||||
|
exercise.text !== '' &&
|
||||||
|
exercise.video_url !== '' &&
|
||||||
|
exercise.prompts.every(prompt => prompt !== '');
|
||||||
|
} else if (exercise.type === 'interactiveSpeaking') {
|
||||||
|
if ('first_title' in exercise && 'second_title' in exercise) {
|
||||||
|
return exercise.first_title !== '' &&
|
||||||
|
exercise.second_title !== '' &&
|
||||||
|
exercise.prompts.every(prompt => prompt.video_url !== '') &&
|
||||||
|
exercise.prompts.length > 2;
|
||||||
|
}
|
||||||
|
return exercise.title !== '' &&
|
||||||
|
exercise.prompts.every(prompt => prompt.video_url !== '');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const submitLevel = () => {
|
const submitLevel = async () => {
|
||||||
if (title === "") {
|
if (title === "") {
|
||||||
toast.error("Enter a title for the exam!");
|
toast.error("Enter a title for the exam!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const exam: LevelExam = {
|
|
||||||
parts: sections.map((s) => {
|
|
||||||
const part = s.state as LevelPart;
|
|
||||||
return {
|
|
||||||
...part,
|
|
||||||
intro: localSettings.currentIntro,
|
|
||||||
category: localSettings.category
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
isDiagnostic: false,
|
|
||||||
minTimer,
|
|
||||||
module: "level",
|
|
||||||
id: title,
|
|
||||||
difficulty,
|
|
||||||
private: isPrivate,
|
|
||||||
};
|
|
||||||
|
|
||||||
axios.post(`/api/exam/level`, exam)
|
const partsWithMissingAudio = sections.some(s => {
|
||||||
.then((result) => {
|
const part = s.state as LevelPart;
|
||||||
playSound("sent");
|
return part.audio && !part.audio.source;
|
||||||
toast.success(`Submitted Exam ID: ${result.data.id}`);
|
});
|
||||||
})
|
|
||||||
.catch((error) => {
|
if (partsWithMissingAudio) {
|
||||||
console.log(error);
|
toast.error("There are parts with missing audio recordings. Either generate them or remove the listening sections.");
|
||||||
toast.error(error.response.data.error || "Something went wrong while submitting, please try again later.");
|
return;
|
||||||
})
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
const audioFormData = new FormData();
|
||||||
|
const videoFormData = new FormData();
|
||||||
|
const audioMap = new Map<number, string>();
|
||||||
|
const videoMap = new Map<string, string>();
|
||||||
|
|
||||||
|
const partsWithAudio = sections.filter(s => (s.state as LevelPart).audio?.source);
|
||||||
|
await Promise.all(
|
||||||
|
partsWithAudio.map(async (section) => {
|
||||||
|
const levelPart = section.state as LevelPart;
|
||||||
|
const blobUrl = levelPart.audio!.source;
|
||||||
|
const response = await fetch(blobUrl);
|
||||||
|
const blob = await response.blob();
|
||||||
|
audioFormData.append('file', blob, 'audio.mp3');
|
||||||
|
audioMap.set(section.sectionId, blobUrl);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
sections.flatMap(async (section) => {
|
||||||
|
const levelPart = section.state as LevelPart;
|
||||||
|
return Promise.all(
|
||||||
|
levelPart.exercises.map(async (exercise, exerciseIndex) => {
|
||||||
|
if (exercise.type === "speaking") {
|
||||||
|
if (exercise.video_url) {
|
||||||
|
const response = await fetch(exercise.video_url);
|
||||||
|
const blob = await response.blob();
|
||||||
|
videoFormData.append('file', blob, 'video.mp4');
|
||||||
|
videoMap.set(`${section.sectionId}-${exerciseIndex}`, exercise.video_url);
|
||||||
|
}
|
||||||
|
} else if (exercise.type === "interactiveSpeaking") {
|
||||||
|
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();
|
||||||
|
videoFormData.append('file', blob, 'video.mp4');
|
||||||
|
videoMap.set(`${section.sectionId}-${exerciseIndex}-${promptIndex}`, prompt.video_url);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const [audioUrls, videoUrls] = await Promise.all([
|
||||||
|
audioMap.size > 0
|
||||||
|
? axios.post('/api/storage', audioFormData, {
|
||||||
|
params: { directory: 'listening_recordings' },
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
}).then(response => response.data.urls)
|
||||||
|
: [],
|
||||||
|
videoMap.size > 0
|
||||||
|
? axios.post('/api/storage', videoFormData, {
|
||||||
|
params: { directory: 'speaking_videos' },
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
}).then(response => response.data.urls)
|
||||||
|
: []
|
||||||
|
]);
|
||||||
|
|
||||||
|
const exam: LevelExam = {
|
||||||
|
parts: sections.map((s) => {
|
||||||
|
const part = s.state as LevelPart;
|
||||||
|
const audioIndex = Array.from(audioMap.entries())
|
||||||
|
.findIndex(([id]) => id === s.sectionId);
|
||||||
|
const updatedExercises = part.exercises.map((exercise, exerciseIndex) => {
|
||||||
|
if (exercise.type === "speaking") {
|
||||||
|
const videoIndex = Array.from(videoMap.entries())
|
||||||
|
.findIndex(([key]) => key === `${s.sectionId}-${exerciseIndex}`);
|
||||||
|
return {
|
||||||
|
...exercise,
|
||||||
|
video_url: videoIndex !== -1 ? videoUrls[videoIndex] : exercise.video_url
|
||||||
|
};
|
||||||
|
} else if (exercise.type === "interactiveSpeaking") {
|
||||||
|
const updatedPrompts = exercise.prompts.map((prompt, promptIndex) => {
|
||||||
|
const videoIndex = Array.from(videoMap.entries())
|
||||||
|
.findIndex(([key]) => key === `${s.sectionId}-${exerciseIndex}-${promptIndex}`);
|
||||||
|
return {
|
||||||
|
...prompt,
|
||||||
|
video_url: videoIndex !== -1 ? videoUrls[videoIndex] : prompt.video_url
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...exercise,
|
||||||
|
prompts: updatedPrompts
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return exercise;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...part,
|
||||||
|
audio: part.audio ? {
|
||||||
|
...part.audio,
|
||||||
|
source: audioIndex !== -1 ? audioUrls[audioIndex] : part.audio.source
|
||||||
|
} : undefined,
|
||||||
|
exercises: updatedExercises,
|
||||||
|
intro: s.settings.currentIntro,
|
||||||
|
category: s.settings.category
|
||||||
|
};
|
||||||
|
}).filter(part => part.exercises.length > 0),
|
||||||
|
isDiagnostic: false,
|
||||||
|
minTimer,
|
||||||
|
module: "level",
|
||||||
|
id: title,
|
||||||
|
difficulty,
|
||||||
|
private: isPrivate,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await axios.post('/api/exam/level', exam);
|
||||||
|
playSound("sent");
|
||||||
|
toast.success(`Submitted Exam ID: ${result.data.id}`);
|
||||||
|
|
||||||
|
Array.from(audioMap.values()).forEach(url => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
Array.from(videoMap.values()).forEach(url => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error submitting exam:', error);
|
||||||
|
toast.error(
|
||||||
|
"Something went wrong while submitting, please try again later."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const preview = () => {
|
const preview = () => {
|
||||||
setExam({
|
setExam({
|
||||||
parts: sections.map((s) => {
|
parts: sections.map((s) => {
|
||||||
const exercise = s.state as LevelPart;
|
const part = s.state as LevelPart;
|
||||||
return {
|
return {
|
||||||
...exercise,
|
...part,
|
||||||
intro: s.settings.currentIntro,
|
intro: s.settings.currentIntro,
|
||||||
category: s.settings.category
|
category: s.settings.category
|
||||||
};
|
};
|
||||||
@@ -115,7 +248,6 @@ const LevelSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const speakingExercise = focusedExercise === undefined ? undefined : currentSection.exercises.find((ex) => ex.id === focusedExercise.id) as SpeakingExercise | InteractiveSpeakingExercise;
|
const speakingExercise = focusedExercise === undefined ? undefined : currentSection.exercises.find((ex) => ex.id === focusedExercise.id) as SpeakingExercise | InteractiveSpeakingExercise;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsEditor
|
<SettingsEditor
|
||||||
sectionLabel={`Part ${focusedSection}`}
|
sectionLabel={`Part ${focusedSection}`}
|
||||||
@@ -123,8 +255,8 @@ const LevelSettings: React.FC = () => {
|
|||||||
module="level"
|
module="level"
|
||||||
introPresets={[]}
|
introPresets={[]}
|
||||||
preview={preview}
|
preview={preview}
|
||||||
canPreview={canPreview}
|
canPreview={canPreviewOrSubmit}
|
||||||
canSubmit={canPreview}
|
canSubmit={canPreviewOrSubmit}
|
||||||
submitModule={submitLevel}
|
submitModule={submitLevel}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@@ -211,7 +343,6 @@ const LevelSettings: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div >
|
</div >
|
||||||
{/*
|
|
||||||
<div>
|
<div>
|
||||||
<Dropdown title="Add Speaking Exercises" className={
|
<Dropdown title="Add Speaking Exercises" className={
|
||||||
clsx(
|
clsx(
|
||||||
@@ -225,19 +356,19 @@ const LevelSettings: React.FC = () => {
|
|||||||
open={localSettings.isSpeakingDropdownOpen}
|
open={localSettings.isSpeakingDropdownOpen}
|
||||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isSpeakingDropdownOpen: isOpen }, false)}
|
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isSpeakingDropdownOpen: isOpen }, false)}
|
||||||
>
|
>
|
||||||
<Dropdown title="Exercises" className={
|
<div className="space-y-2 px-2 pb-2">
|
||||||
clsx(
|
<Dropdown title="Exercises" className={
|
||||||
"w-full font-semibold flex justify-between items-center p-4 bg-gradient-to-r border",
|
clsx(
|
||||||
"bg-ielts-speaking/70 border-ielts-speaking hover:bg-ielts-speaking",
|
"w-full font-semibold flex justify-between items-center p-4 bg-gradient-to-r border",
|
||||||
"text-white shadow-md transition-all duration-300",
|
"bg-ielts-speaking/70 border-ielts-speaking hover:bg-ielts-speaking",
|
||||||
localSettings.isSpeakingDropdownOpen ? "rounded-t-lg" : "rounded-lg"
|
"text-white shadow-md transition-all duration-300",
|
||||||
)
|
localSettings.isSpeakingExercisesOpen ? "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}
|
contentWrapperClassName={"pt-4 px-2 bg-white rounded-b-lg shadow-md transition-all duration-300 ease-in-out border border-ielts-speaking"}
|
||||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isSpeakingDropdownOpen: isOpen }, false)}
|
open={localSettings.isSpeakingExercisesOpen}
|
||||||
>
|
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isSpeakingExercisesOpen: isOpen }, false)}
|
||||||
<div className="space-y-2 px-2 pb-2">
|
>
|
||||||
<ExercisePicker
|
<ExercisePicker
|
||||||
module="speaking"
|
module="speaking"
|
||||||
sectionId={focusedSection}
|
sectionId={focusedSection}
|
||||||
@@ -245,31 +376,33 @@ const LevelSettings: React.FC = () => {
|
|||||||
levelSectionId={focusedSection}
|
levelSectionId={focusedSection}
|
||||||
level
|
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>
|
||||||
}
|
|
||||||
|
{speakingExercise !== undefined &&
|
||||||
|
<Dropdown title={`Configure Speaking Exercise #${Number(focusedExercise!.questionId) + 1}`} 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.isConfigureExercisesOpen ? "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.isConfigureExercisesOpen}
|
||||||
|
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isConfigureExercisesOpen: isOpen }, false)}
|
||||||
|
>
|
||||||
|
<div className="space-y-2 px-2 pb-2">
|
||||||
|
<SpeakingComponents
|
||||||
|
{...{ localSettings, updateLocalAndScheduleGlobal, section: speakingExercise, id: speakingExercise.id, sectionId: focusedSection }}
|
||||||
|
level
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
*/}
|
|
||||||
</SettingsEditor >
|
</SettingsEditor >
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -184,19 +184,16 @@ const ListeningComponents: React.FC<Props> = ({ currentSection, localSettings, u
|
|||||||
disabled={currentSection === undefined || currentSection.script === undefined && currentSection.audio === undefined || currentSection.exercises.length === 0}
|
disabled={currentSection === undefined || currentSection.script === undefined && currentSection.audio === undefined || currentSection.exercises.length === 0}
|
||||||
contentWrapperClassName={level ? `border border-ielts-listening` : ''}
|
contentWrapperClassName={level ? `border border-ielts-listening` : ''}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center text-mti-gray-dim justify-center mb-4">
|
<div className="flex flex-row items-center text-mti-gray-dim justify-center mb-4 gap-2 p-2">
|
||||||
<span className="bg-gray-100 px-3.5 py-2.5 rounded-l-lg border border-r-0 border-gray-300">
|
<span className="bg-gray-100 px-3.5 py-2.5 rounded-lg border border-gray-300">
|
||||||
Generate audio recording for this section
|
Generate audio recording for this section
|
||||||
</span>
|
</span>
|
||||||
<div className="-ml-2.5">
|
<GenerateBtn
|
||||||
<GenerateBtn
|
module="listening"
|
||||||
module="listening"
|
genType="audio"
|
||||||
genType="audio"
|
sectionId={levelId ? levelId : focusedSection}
|
||||||
sectionId={levelId ? levelId : focusedSection}
|
generateFnc={generateAudio}
|
||||||
generateFnc={generateAudio}
|
/>
|
||||||
levelId={focusedSection}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import useExamEditorStore from "@/stores/examEditor";
|
import useExamEditorStore from "@/stores/examEditor";
|
||||||
import { LevelSectionSettings, SpeakingSectionSettings } from "@/stores/examEditor/types";
|
import { LevelSectionSettings, SpeakingSectionSettings } from "@/stores/examEditor/types";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { generate } from "../Shared/Generate";
|
import { generate } from "../Shared/Generate";
|
||||||
import Dropdown from "../Shared/SettingsDropdown";
|
import Dropdown from "../Shared/SettingsDropdown";
|
||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
@@ -11,6 +11,7 @@ import { InteractiveSpeakingExercise, LevelPart, SpeakingExercise } from "@/inte
|
|||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { generateVideos } from "../Shared/generateVideos";
|
import { generateVideos } from "../Shared/generateVideos";
|
||||||
import { Module } from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
|
import useCanGenerate from "./useCanGenerate";
|
||||||
|
|
||||||
export interface Avatar {
|
export interface Avatar {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -23,16 +24,19 @@ interface Props {
|
|||||||
section: SpeakingExercise | InteractiveSpeakingExercise | LevelPart;
|
section: SpeakingExercise | InteractiveSpeakingExercise | LevelPart;
|
||||||
level?: boolean;
|
level?: boolean;
|
||||||
module?: Module;
|
module?: Module;
|
||||||
|
id?: string;
|
||||||
|
sectionId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndScheduleGlobal, section, level, module = "speaking" }) => {
|
const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndScheduleGlobal, section, level = false, module = "speaking", id, sectionId }) => {
|
||||||
|
|
||||||
const { currentModule, speakingAvatars, dispatch } = useExamEditorStore();
|
const { currentModule, speakingAvatars, dispatch, modules } = useExamEditorStore();
|
||||||
const { focusedSection, difficulty } = useExamEditorStore((store) => store.modules[currentModule])
|
const { focusedSection, difficulty, sections } = useExamEditorStore((store) => store.modules[level ? "level" : currentModule])
|
||||||
|
const state = sections.find((s) => s.sectionId === sectionId);
|
||||||
|
|
||||||
const [selectedAvatar, setSelectedAvatar] = useState<Avatar | null>(null);
|
const [selectedAvatar, setSelectedAvatar] = useState<Avatar | null>(null);
|
||||||
|
|
||||||
const generateScript = useCallback((sectionId: number) => {
|
const generateScript = useCallback((scriptSectionId: number) => {
|
||||||
const queryParams: {
|
const queryParams: {
|
||||||
difficulty: string;
|
difficulty: string;
|
||||||
first_topic?: string;
|
first_topic?: string;
|
||||||
@@ -40,7 +44,7 @@ const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSche
|
|||||||
topic?: string;
|
topic?: string;
|
||||||
} = { difficulty };
|
} = { difficulty };
|
||||||
|
|
||||||
if (sectionId === 1) {
|
if (scriptSectionId === 1) {
|
||||||
if (localSettings.speakingTopic) {
|
if (localSettings.speakingTopic) {
|
||||||
queryParams['first_topic'] = localSettings.speakingTopic;
|
queryParams['first_topic'] = localSettings.speakingTopic;
|
||||||
}
|
}
|
||||||
@@ -52,17 +56,16 @@ const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSche
|
|||||||
queryParams['topic'] = localSettings.speakingTopic;
|
queryParams['topic'] = localSettings.speakingTopic;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
generate(
|
generate(
|
||||||
sectionId,
|
level ? section.sectionId! : focusedSection,
|
||||||
currentModule,
|
"speaking",
|
||||||
"speakingScript",
|
`${id ? `${id}-` : ''}speakingScript`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
queryParams
|
queryParams
|
||||||
},
|
},
|
||||||
(data: any) => {
|
(data: any) => {
|
||||||
switch (sectionId) {
|
switch (level ? section.sectionId! : focusedSection) {
|
||||||
case 1:
|
case 1:
|
||||||
return [{
|
return [{
|
||||||
prompts: data.questions,
|
prompts: data.questions,
|
||||||
@@ -84,10 +87,12 @@ const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSche
|
|||||||
default:
|
default:
|
||||||
return [data];
|
return [data];
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
sectionId,
|
||||||
|
level
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [localSettings, difficulty]);
|
}, [difficulty, level, section.sectionId, focusedSection, id, sectionId, localSettings.speakingTopic, localSettings.speakingSecondTopic]);
|
||||||
|
|
||||||
const onTopicChange = useCallback((speakingTopic: string) => {
|
const onTopicChange = useCallback((speakingTopic: string) => {
|
||||||
updateLocalAndScheduleGlobal({ speakingTopic });
|
updateLocalAndScheduleGlobal({ speakingTopic });
|
||||||
@@ -97,39 +102,33 @@ const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSche
|
|||||||
updateLocalAndScheduleGlobal({ speakingSecondTopic });
|
updateLocalAndScheduleGlobal({ speakingSecondTopic });
|
||||||
}, [updateLocalAndScheduleGlobal]);
|
}, [updateLocalAndScheduleGlobal]);
|
||||||
|
|
||||||
const canGenerate = section && (() => {
|
const canGenerate = useCanGenerate({
|
||||||
switch (focusedSection) {
|
section,
|
||||||
case 1: {
|
sections,
|
||||||
const currentSection = section as InteractiveSpeakingExercise;
|
id,
|
||||||
return currentSection.first_title !== "" &&
|
focusedSection
|
||||||
currentSection.second_title !== "" &&
|
});
|
||||||
currentSection.prompts.every(prompt => prompt.text !== "") && currentSection.prompts.length > 2;
|
|
||||||
}
|
useEffect(() => {
|
||||||
case 2: {
|
if (!canGenerate) {
|
||||||
const currentSection = section as SpeakingExercise;
|
updateLocalAndScheduleGlobal({ isGenerateVideoOpen: false }, false);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
})();
|
}, [canGenerate, updateLocalAndScheduleGlobal]);
|
||||||
|
|
||||||
const generateVideoCallback = useCallback((sectionId: number) => {
|
const generateVideoCallback = useCallback((sectionId: number) => {
|
||||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "generating", value: "video" } })
|
if (level) {
|
||||||
|
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: "level", field: "levelGenerating", value: [...state!.levelGenerating, `${id ? `${id}-` : ''}video`] } })
|
||||||
|
} else {
|
||||||
|
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId: focusedSection, module: "speaking", field: "generating", value: "video" } })
|
||||||
|
}
|
||||||
|
|
||||||
generateVideos(
|
generateVideos(
|
||||||
section as InteractiveSpeakingExercise | SpeakingExercise,
|
section as InteractiveSpeakingExercise | SpeakingExercise,
|
||||||
sectionId,
|
level ? section.sectionId! : focusedSection,
|
||||||
selectedAvatar,
|
selectedAvatar,
|
||||||
speakingAvatars
|
speakingAvatars
|
||||||
).then((results) => {
|
).then((results) => {
|
||||||
switch (sectionId) {
|
switch (level ? section.sectionId! : focusedSection) {
|
||||||
case 1:
|
case 1:
|
||||||
case 3: {
|
case 3: {
|
||||||
const interactiveSection = section as InteractiveSpeakingExercise;
|
const interactiveSection = section as InteractiveSpeakingExercise;
|
||||||
@@ -137,22 +136,40 @@ const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSche
|
|||||||
...prompt,
|
...prompt,
|
||||||
video_url: results[index].url || ''
|
video_url: results[index].url || ''
|
||||||
}));
|
}));
|
||||||
dispatch({
|
if (level) {
|
||||||
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
dispatch({
|
||||||
sectionId, module: currentModule, field: "genResult", value:
|
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
||||||
{ generating: "video", result: [{ prompts: updatedPrompts }], module: module }
|
sectionId, field: "levelGenResults", value: [...state!.levelGenResults,
|
||||||
}
|
{ generating: `${id ? `${id}-` : ''}video`, result: [{ prompts: updatedPrompts }] }], module: "level"
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
||||||
|
sectionId: focusedSection, module: "speaking", field: "genResult", value:
|
||||||
|
{ generating: "video", result: [{ prompts: updatedPrompts }], module: module }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 2: {
|
case 2: {
|
||||||
if (results[0]?.url) {
|
if (results[0]?.url) {
|
||||||
dispatch({
|
if (level) {
|
||||||
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
dispatch({
|
||||||
sectionId, module: currentModule, field: "genResult", value:
|
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
||||||
{ generating: "video", result: [{ video_url: results[0].url }], module: module }
|
sectionId, field: "levelGenResults", value: [...state!.levelGenResults,
|
||||||
}
|
{ generating: `${id ? `${id}-` : ''}video`, result: [{ video_url: results[0].url }] }], module: "level"
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_SECTION_SINGLE_FIELD", payload: {
|
||||||
|
sectionId: focusedSection, module, field: "genResult", value:
|
||||||
|
{ generating: 'video', result: [{ video_url: results[0].url }], module: "speaking" }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -160,8 +177,10 @@ const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSche
|
|||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
toast.error("Failed to generate the video, try again later!")
|
toast.error("Failed to generate the video, try again later!")
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [selectedAvatar, section]);
|
}, [level, section, focusedSection, selectedAvatar, speakingAvatars, dispatch, module, state, id]);
|
||||||
|
|
||||||
|
const secId = level ? section.sectionId! : focusedSection;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -173,11 +192,11 @@ const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSche
|
|||||||
contentWrapperClassName={level ? `border border-ielts-speaking` : ''}
|
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={clsx("gap-2 px-2 pb-4", secId === 1 ? "flex flex-col w-full" : "flex flex-row items-center")}>
|
||||||
<div className="flex flex-col flex-grow gap-4 px-2">
|
<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>
|
<label className="font-normal text-base text-mti-gray-dim">{`${secId === 1 ? "First Topic" : "Topic"}`} (Optional)</label>
|
||||||
<Input
|
<Input
|
||||||
key={`section-${focusedSection}`}
|
key={`section-${secId}`}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Topic"
|
placeholder="Topic"
|
||||||
name="category"
|
name="category"
|
||||||
@@ -186,11 +205,11 @@ const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSche
|
|||||||
value={localSettings.speakingTopic}
|
value={localSettings.speakingTopic}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{focusedSection === 1 &&
|
{secId === 1 &&
|
||||||
<div className="flex flex-col flex-grow gap-4 px-2">
|
<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>
|
<label className="font-normal text-base text-mti-gray-dim">Second Topic (Optional)</label>
|
||||||
<Input
|
<Input
|
||||||
key={`section-${focusedSection}`}
|
key={`section-${secId}`}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Topic"
|
placeholder="Topic"
|
||||||
name="category"
|
name="category"
|
||||||
@@ -200,12 +219,13 @@ const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSche
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div className={clsx("flex h-16 mb-1", focusedSection === 1 ? "justify-center mt-4" : "self-end")}>
|
<div className={clsx("flex h-16 mb-1", secId === 1 ? "justify-center mt-4" : "self-end")}>
|
||||||
<GenerateBtn
|
<GenerateBtn
|
||||||
module="speaking"
|
module="speaking"
|
||||||
genType="speakingScript"
|
genType={`${id ? `${id}-` : ''}speakingScript`}
|
||||||
sectionId={focusedSection}
|
sectionId={focusedSection}
|
||||||
generateFnc={generateScript}
|
generateFnc={generateScript}
|
||||||
|
level={level}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -216,6 +236,7 @@ const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSche
|
|||||||
open={localSettings.isGenerateVideoOpen}
|
open={localSettings.isGenerateVideoOpen}
|
||||||
disabled={!canGenerate}
|
disabled={!canGenerate}
|
||||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isGenerateVideoOpen: isOpen }, false)}
|
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isGenerateVideoOpen: isOpen }, false)}
|
||||||
|
contentWrapperClassName={level ? `border border-ielts-speaking` : ''}
|
||||||
>
|
>
|
||||||
<div className={clsx("flex items-center justify-between gap-4 px-2 pb-4")}>
|
<div className={clsx("flex items-center justify-between gap-4 px-2 pb-4")}>
|
||||||
<div className="relative flex-1 max-w-xs">
|
<div className="relative flex-1 max-w-xs">
|
||||||
@@ -255,9 +276,10 @@ const SpeakingComponents: React.FC<Props> = ({ localSettings, updateLocalAndSche
|
|||||||
|
|
||||||
<GenerateBtn
|
<GenerateBtn
|
||||||
module="speaking"
|
module="speaking"
|
||||||
genType="video"
|
genType={`${id ? `${id}-` : ''}video`}
|
||||||
sectionId={focusedSection}
|
sectionId={focusedSection}
|
||||||
generateFnc={generateVideoCallback}
|
generateFnc={generateVideoCallback}
|
||||||
|
level={level}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { InteractiveSpeakingExercise, LevelPart, SpeakingExercise } from "@/interfaces/exam";
|
||||||
|
import { Section } from '@/stores/examEditor/types';
|
||||||
|
|
||||||
|
interface CheckGenerateProps {
|
||||||
|
section: Section | null;
|
||||||
|
sections: Array<{ sectionId: number; state: Section }>;
|
||||||
|
id?: string;
|
||||||
|
focusedSection: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useCanGenerate = ({ section, sections, id, focusedSection }: CheckGenerateProps) => {
|
||||||
|
const checkCanGenerate = useCallback(() => {
|
||||||
|
if (!section) return false;
|
||||||
|
|
||||||
|
|
||||||
|
const exercise = id
|
||||||
|
? (sections.find(s => s.sectionId === 1)?.state as LevelPart)
|
||||||
|
?.exercises?.find(ex => ex.id === id) ?? section
|
||||||
|
: section;
|
||||||
|
|
||||||
|
const sectionId = id ? (exercise as SpeakingExercise | InteractiveSpeakingExercise).sectionId : focusedSection;
|
||||||
|
|
||||||
|
switch (sectionId) {
|
||||||
|
case 1: {
|
||||||
|
const currentSection = exercise as InteractiveSpeakingExercise;
|
||||||
|
return currentSection.first_title &&
|
||||||
|
currentSection.second_title &&
|
||||||
|
currentSection.prompts?.length > 2 &&
|
||||||
|
currentSection.prompts.every(prompt => prompt.text)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
const currentSection = exercise as SpeakingExercise;
|
||||||
|
return currentSection.title &&
|
||||||
|
currentSection.text &&
|
||||||
|
currentSection.prompts?.length > 0 &&
|
||||||
|
currentSection.prompts.every(prompt => prompt)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
const currentSection = exercise as InteractiveSpeakingExercise;
|
||||||
|
return currentSection.title &&
|
||||||
|
currentSection.prompts?.length > 0 &&
|
||||||
|
currentSection.prompts.every(prompt => prompt.text)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [section, sections, id, focusedSection]);
|
||||||
|
|
||||||
|
const [canGenerate, setCanGenerate] = useState(checkCanGenerate());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCanGenerate(checkCanGenerate());
|
||||||
|
}, [checkCanGenerate, section]);
|
||||||
|
|
||||||
|
return canGenerate;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCanGenerate;
|
||||||
@@ -35,7 +35,7 @@ export const listeningSection = (task: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const speakingTask = (task: number) => {
|
export const speakingTask = (task: number) => {
|
||||||
if (task === 3) {
|
if ([1,3].includes(task)) {
|
||||||
return {
|
return {
|
||||||
id: v4(),
|
id: v4(),
|
||||||
type: "interactiveSpeaking",
|
type: "interactiveSpeaking",
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ export interface LevelSectionSettings extends SectionSettings {
|
|||||||
speakingSecondTopic?: string;
|
speakingSecondTopic?: string;
|
||||||
isSpeakingTopicOpen: boolean;
|
isSpeakingTopicOpen: boolean;
|
||||||
isGenerateVideoOpen: boolean;
|
isGenerateVideoOpen: boolean;
|
||||||
|
isSpeakingExercisesOpen: boolean;
|
||||||
|
isConfigureExercisesOpen: boolean;
|
||||||
|
|
||||||
// section picker
|
// section picker
|
||||||
isReadingPickerOpen: boolean;
|
isReadingPickerOpen: boolean;
|
||||||
@@ -80,6 +82,7 @@ export interface LevelSectionSettings extends SectionSettings {
|
|||||||
|
|
||||||
export type Context = "passage" | "video" | "audio" | "listeningScript" | "speakingScript" | "writing";
|
export type Context = "passage" | "video" | "audio" | "listeningScript" | "speakingScript" | "writing";
|
||||||
export type Generating = Context | "exercises" | string | undefined;
|
export type Generating = Context | "exercises" | string | undefined;
|
||||||
|
export type LevelGenResults = {generating: string, result: Record<string, any>[], module: Module};
|
||||||
export type Section = LevelPart | ReadingPart | ListeningPart | WritingExercise | SpeakingExercise | InteractiveSpeakingExercise;
|
export type Section = LevelPart | ReadingPart | ListeningPart | WritingExercise | SpeakingExercise | InteractiveSpeakingExercise;
|
||||||
export type ExamPart = ListeningPart | ReadingPart | LevelPart;
|
export type ExamPart = ListeningPart | ReadingPart | LevelPart;
|
||||||
|
|
||||||
@@ -91,7 +94,7 @@ export interface SectionState {
|
|||||||
generating: Generating;
|
generating: Generating;
|
||||||
genResult: {generating: string, result: Record<string, any>[], module: Module} | undefined;
|
genResult: {generating: string, result: Record<string, any>[], module: Module} | undefined;
|
||||||
levelGenerating: Generating[];
|
levelGenerating: Generating[];
|
||||||
levelGenResults: {generating: string, result: Record<string, any>[], module: Module}[];
|
levelGenResults: LevelGenResults[];
|
||||||
focusedExercise?: {questionId: number; id: string} | undefined;
|
focusedExercise?: {questionId: number; id: string} | undefined;
|
||||||
writingSection?: number;
|
writingSection?: number;
|
||||||
speakingSection?: number;
|
speakingSection?: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user