ENCOA-228 Now when user navigates between modules the generation items persist. Reading, listening and writing added to level module
This commit is contained in:
261
src/components/ExamEditor/SettingsEditor/speaking/index.tsx
Normal file
261
src/components/ExamEditor/SettingsEditor/speaking/index.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import useSettingsState from "../../Hooks/useSettingsState";
|
||||
import { SpeakingSectionSettings } from "@/stores/examEditor/types";
|
||||
import Option from "@/interfaces/option";
|
||||
import SettingsEditor from "..";
|
||||
import { InteractiveSpeakingExercise, SpeakingExam, SpeakingExercise } from "@/interfaces/exam";
|
||||
import { toast } from "react-toastify";
|
||||
import { usePersistentExamStore } from "@/stores/examStore";
|
||||
import { useRouter } from "next/router";
|
||||
import openDetachedTab from "@/utils/popout";
|
||||
import axios from "axios";
|
||||
import { playSound } from "@/utils/sound";
|
||||
import SpeakingComponents from "./components";
|
||||
|
||||
export interface Avatar {
|
||||
name: string;
|
||||
gender: string;
|
||||
}
|
||||
|
||||
const SpeakingSettings: React.FC = () => {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
setExam,
|
||||
setExerciseIndex,
|
||||
setQuestionIndex,
|
||||
setBgColor,
|
||||
} = usePersistentExamStore();
|
||||
|
||||
const { title, currentModule } = useExamEditorStore();
|
||||
const { focusedSection, difficulty, sections, minTimer, isPrivate } = useExamEditorStore((store) => store.modules[currentModule])
|
||||
|
||||
const section = sections.find((section) => section.sectionId == focusedSection)?.state;
|
||||
|
||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<SpeakingSectionSettings>(
|
||||
currentModule,
|
||||
focusedSection,
|
||||
);
|
||||
|
||||
if (section === undefined) return <></>;
|
||||
|
||||
const currentSection = section as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
|
||||
const defaultPresets: Option[] = [
|
||||
{
|
||||
label: "Preset: Speaking Part 1",
|
||||
value: "Welcome to {part} of the {label}. You will engage in a conversation about yourself and familiar topics such as your home, family, work, studies, and interests. General questions will be asked."
|
||||
},
|
||||
{
|
||||
label: "Preset: Speaking Part 2",
|
||||
value: "Welcome to {part} of the {label}. You will be given a topic card describing a particular person, object, event, or experience."
|
||||
},
|
||||
{
|
||||
label: "Preset: Speaking Part 3",
|
||||
value: "Welcome to {part} of the {label}. You will engage in an in-depth discussion about abstract ideas and issues. The examiner will ask questions that require you to explain, analyze, and speculate about various aspects of the topic."
|
||||
}
|
||||
];
|
||||
|
||||
const canPreviewOrSubmit = (() => {
|
||||
return sections.every((s) => {
|
||||
const section = s.state as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
switch (section.type) {
|
||||
case 'speaking':
|
||||
return section.title !== '' &&
|
||||
section.text !== '' &&
|
||||
section.video_url !== '' &&
|
||||
section.prompts.every(prompt => prompt !== '');
|
||||
|
||||
case 'interactiveSpeaking':
|
||||
if ('first_title' in section && 'second_title' in section) {
|
||||
return section.first_title !== '' &&
|
||||
section.second_title !== '' &&
|
||||
section.prompts.every(prompt => prompt.video_url !== '') &&
|
||||
section.prompts.length > 2;
|
||||
}
|
||||
return section.title !== '' &&
|
||||
section.prompts.every(prompt => prompt.video_url !== '');
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
const submitSpeaking = async () => {
|
||||
if (title === "") {
|
||||
toast.error("Enter a title for the exam!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
const urlMap = new Map<string, string>();
|
||||
|
||||
const sectionsWithVideos = sections.filter(s => {
|
||||
const exercise = s.state as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
if (exercise.type === "speaking") {
|
||||
return exercise.video_url !== "";
|
||||
}
|
||||
if (exercise.type === "interactiveSpeaking") {
|
||||
return exercise.prompts?.some(prompt => prompt.video_url !== "");
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (sectionsWithVideos.length === 0) {
|
||||
toast.error('No video sections found in the exam! Please record or import videos.');
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
sectionsWithVideos.map(async (section) => {
|
||||
const exercise = section.state as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
|
||||
if (exercise.type === "speaking") {
|
||||
const response = await fetch(exercise.video_url);
|
||||
const blob = await response.blob();
|
||||
formData.append('file', blob, 'video.mp4');
|
||||
urlMap.set(`${section.sectionId}`, exercise.video_url);
|
||||
} else {
|
||||
await Promise.all(
|
||||
exercise.prompts.map(async (prompt, promptIndex) => {
|
||||
if (prompt.video_url) {
|
||||
const response = await fetch(prompt.video_url);
|
||||
const blob = await response.blob();
|
||||
formData.append('file', blob, 'video.mp4');
|
||||
urlMap.set(`${section.sectionId}-${promptIndex}`, prompt.video_url);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const response = await axios.post('/api/storage', formData, {
|
||||
params: {
|
||||
directory: 'speaking_videos'
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
|
||||
const { urls } = response.data;
|
||||
|
||||
const exam: SpeakingExam = {
|
||||
exercises: sections.map((s) => {
|
||||
const exercise = s.state as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
|
||||
if (exercise.type === "speaking") {
|
||||
const videoIndex = Array.from(urlMap.entries())
|
||||
.findIndex(([key]) => key === `${s.sectionId}`);
|
||||
|
||||
return {
|
||||
...exercise,
|
||||
video_url: videoIndex !== -1 ? urls[videoIndex] : exercise.video_url,
|
||||
intro: s.settings.currentIntro,
|
||||
category: s.settings.category
|
||||
};
|
||||
} else {
|
||||
const updatedPrompts = exercise.prompts.map((prompt, promptIndex) => {
|
||||
const videoIndex = Array.from(urlMap.entries())
|
||||
.findIndex(([key]) => key === `${s.sectionId}-${promptIndex}`);
|
||||
|
||||
return {
|
||||
...prompt,
|
||||
video_url: videoIndex !== -1 ? urls[videoIndex] : prompt.video_url
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...exercise,
|
||||
prompts: updatedPrompts,
|
||||
intro: s.settings.currentIntro,
|
||||
category: s.settings.category
|
||||
};
|
||||
}
|
||||
}),
|
||||
minTimer,
|
||||
module: "speaking",
|
||||
id: title,
|
||||
isDiagnostic: false,
|
||||
variant: undefined,
|
||||
difficulty,
|
||||
instructorGender: "varied",
|
||||
private: isPrivate,
|
||||
};
|
||||
|
||||
const result = await axios.post('/api/exam/speaking', exam);
|
||||
playSound("sent");
|
||||
toast.success(`Submitted Exam ID: ${result.data.id}`);
|
||||
|
||||
Array.from(urlMap.values()).forEach(url => {
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
toast.error(
|
||||
"Something went wrong while submitting, please try again later."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const preview = () => {
|
||||
setExam({
|
||||
exercises: sections
|
||||
.filter((s) => {
|
||||
const exercise = s.state as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
|
||||
if (exercise.type === "speaking") {
|
||||
return exercise.video_url !== "";
|
||||
}
|
||||
|
||||
if (exercise.type === "interactiveSpeaking") {
|
||||
return exercise.prompts?.every(prompt => prompt.video_url !== "");
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.map((s) => {
|
||||
const exercise = s.state as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
return {
|
||||
...exercise,
|
||||
intro: s.settings.currentIntro,
|
||||
category: s.settings.category
|
||||
};
|
||||
}),
|
||||
minTimer,
|
||||
module: "speaking",
|
||||
id: title,
|
||||
isDiagnostic: false,
|
||||
variant: undefined,
|
||||
difficulty,
|
||||
private: isPrivate,
|
||||
} as SpeakingExam);
|
||||
setExerciseIndex(0);
|
||||
setQuestionIndex(0);
|
||||
setBgColor("bg-white");
|
||||
openDetachedTab("popout?type=Exam&module=speaking", router)
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsEditor
|
||||
sectionLabel={`Speaking ${focusedSection}`}
|
||||
sectionId={focusedSection}
|
||||
module="speaking"
|
||||
introPresets={[defaultPresets[focusedSection - 1]]}
|
||||
preview={preview}
|
||||
canPreview={canPreviewOrSubmit}
|
||||
canSubmit={canPreviewOrSubmit}
|
||||
submitModule={submitSpeaking}
|
||||
>
|
||||
<SpeakingComponents
|
||||
{...{ localSettings, updateLocalAndScheduleGlobal, section: currentSection }}
|
||||
/>
|
||||
</SettingsEditor>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpeakingSettings;
|
||||
Reference in New Issue
Block a user