Now only when the user submits the listening exam are the mp3 are uploaded onto firebase bucket
This commit is contained in:
@@ -10,9 +10,10 @@ interface Props {
|
||||
sectionId: number;
|
||||
genType: Generating;
|
||||
generateFnc: (sectionId: number) => void
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const GenerateBtn: React.FC<Props> = ({module, sectionId, genType, generateFnc}) => {
|
||||
const GenerateBtn: React.FC<Props> = ({module, sectionId, genType, generateFnc, className}) => {
|
||||
const {generating} = useExamEditorStore((store) => store.modules[module].sections.find((s)=> s.sectionId == sectionId))!;
|
||||
const loading = generating && generating === genType;
|
||||
return (
|
||||
@@ -20,7 +21,8 @@ const GenerateBtn: React.FC<Props> = ({module, sectionId, genType, generateFnc})
|
||||
key={`section-${sectionId}`}
|
||||
className={clsx(
|
||||
"flex items-center w-[140px] justify-center px-4 py-2 text-white rounded-xl transition-colors duration-300 text-lg",
|
||||
`bg-ielts-${module}/70 border border-ielts-${module} hover:bg-ielts-${module}`
|
||||
`bg-ielts-${module}/70 border border-ielts-${module} hover:bg-ielts-${module}`,
|
||||
className
|
||||
)}
|
||||
onClick={loading ? () => { } : () => generateFnc(sectionId)}
|
||||
>
|
||||
|
||||
@@ -9,9 +9,10 @@ interface Props {
|
||||
disabled?: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
children: ReactNode;
|
||||
center?: boolean;
|
||||
}
|
||||
|
||||
const SettingsDropdown: React.FC<Props> = ({ module, title, open, setIsOpen, children, disabled = false }) => {
|
||||
const SettingsDropdown: React.FC<Props> = ({ module, title, open, setIsOpen, children, disabled = false, center = false}) => {
|
||||
return (
|
||||
<Dropdown
|
||||
title={title}
|
||||
@@ -20,7 +21,7 @@ const SettingsDropdown: React.FC<Props> = ({ module, title, open, setIsOpen, chi
|
||||
`bg-ielts-${module}/70 border border-ielts-${module} hover:bg-ielts-${module} disabled:bg-ielts-${module}/30`,
|
||||
open ? "rounded-t-lg" : "rounded-lg"
|
||||
)}
|
||||
contentWrapperClassName="pt-6 px-2 bg-white rounded-b-lg shadow-md transition-all duration-300 ease-in-out"
|
||||
contentWrapperClassName={`pt-6 px-2 bg-white rounded-b-lg shadow-md transition-all duration-300 ease-in-out ${center ? "flex justify-center" : ""}`}
|
||||
open={open}
|
||||
setIsOpen={setIsOpen}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -19,13 +19,14 @@ import { toast } from "react-toastify";
|
||||
|
||||
const ListeningSettings: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const {currentModule, title } = useExamEditorStore();
|
||||
const [audioLoading, setAudioLoading] = useState(false);
|
||||
const { currentModule, title, dispatch } = useExamEditorStore();
|
||||
const {
|
||||
focusedSection,
|
||||
difficulty,
|
||||
sections,
|
||||
minTimer,
|
||||
isPrivate
|
||||
isPrivate,
|
||||
} = useExamEditorStore(state => state.modules[currentModule]);
|
||||
|
||||
const {
|
||||
@@ -77,39 +78,81 @@ const ListeningSettings: React.FC = () => {
|
||||
updateLocalAndScheduleGlobal({ topic });
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
const submitListening = () => {
|
||||
const submitListening = async () => {
|
||||
if (title === "") {
|
||||
toast.error("Enter a title for the exam!");
|
||||
return;
|
||||
}
|
||||
const exam: ListeningExam = {
|
||||
parts: sections.map((s) => {
|
||||
const exercise = s.state as ListeningPart;
|
||||
return {
|
||||
...exercise,
|
||||
intro: localSettings.currentIntro,
|
||||
category: localSettings.category
|
||||
};
|
||||
}),
|
||||
isDiagnostic: false,
|
||||
minTimer,
|
||||
module: "listening",
|
||||
id: title,
|
||||
variant: sections.length === 4 ? "full" : "partial",
|
||||
difficulty,
|
||||
private: isPrivate,
|
||||
};
|
||||
try {
|
||||
const sectionsWithAudio = sections.filter(s => (s.state as ListeningPart).audio?.source);
|
||||
|
||||
axios.post(`/api/exam/listening`, exam)
|
||||
.then((result) => {
|
||||
if (sectionsWithAudio.length > 0) {
|
||||
const formData = new FormData();
|
||||
const sectionMap = new Map<number, string>();
|
||||
|
||||
await Promise.all(
|
||||
sectionsWithAudio.map(async (section) => {
|
||||
const listeningPart = section.state as ListeningPart;
|
||||
const blobUrl = listeningPart.audio!.source;
|
||||
const response = await fetch(blobUrl);
|
||||
const blob = await response.blob();
|
||||
formData.append('file', blob, 'audio.mp3');
|
||||
sectionMap.set(section.sectionId, blobUrl);
|
||||
})
|
||||
);
|
||||
|
||||
const response = await axios.post('/api/storage', formData, {
|
||||
params: {
|
||||
directory: 'listening_recordings'
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
|
||||
const { urls } = response.data;
|
||||
|
||||
const exam: ListeningExam = {
|
||||
parts: sections.map((s) => {
|
||||
const exercise = s.state as ListeningPart;
|
||||
const index = Array.from(sectionMap.entries())
|
||||
.findIndex(([id]) => id === s.sectionId);
|
||||
|
||||
return {
|
||||
...exercise,
|
||||
audio: exercise.audio ? {
|
||||
...exercise.audio,
|
||||
source: index !== -1 ? urls[index] : exercise.audio.source
|
||||
} : undefined,
|
||||
intro: localSettings.currentIntro,
|
||||
category: localSettings.category
|
||||
};
|
||||
}),
|
||||
isDiagnostic: false,
|
||||
minTimer,
|
||||
module: "listening",
|
||||
id: title,
|
||||
variant: sections.length === 4 ? "full" : "partial",
|
||||
difficulty,
|
||||
private: isPrivate,
|
||||
};
|
||||
|
||||
const result = await axios.post('/api/exam/listening', exam);
|
||||
playSound("sent");
|
||||
toast.success(`Submitted Exam ID: ${result.data.id}`);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
toast.error(error.response.data.error || "Something went wrong while submitting, please try again later.");
|
||||
})
|
||||
}
|
||||
|
||||
} else {
|
||||
toast.error('No audio sections found in the exam! Please either import them or generate them.');
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error submitting exam:', error);
|
||||
toast.error(
|
||||
"Something went wrong while submitting, please try again later."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const preview = () => {
|
||||
setExam({
|
||||
@@ -125,7 +168,7 @@ const ListeningSettings: React.FC = () => {
|
||||
module: "listening",
|
||||
id: title,
|
||||
isDiagnostic: false,
|
||||
variant: undefined,
|
||||
variant: sections.length === 4 ? "full" : "partial",
|
||||
difficulty,
|
||||
private: isPrivate,
|
||||
} as ListeningExam);
|
||||
@@ -135,14 +178,65 @@ const ListeningSettings: React.FC = () => {
|
||||
openDetachedTab("popout?type=Exam&module=listening", router)
|
||||
}
|
||||
|
||||
const generateAudio = useCallback(async (sectionId: number) => {
|
||||
let body: any;
|
||||
if ([1, 3].includes(sectionId)) {
|
||||
body = { conversation: currentSection.script }
|
||||
} else {
|
||||
body = { monologue: currentSection.script }
|
||||
}
|
||||
|
||||
try {
|
||||
setAudioLoading(true);
|
||||
const response = await axios.post(
|
||||
'/api/exam/media/listening',
|
||||
body,
|
||||
{
|
||||
responseType: 'arraybuffer',
|
||||
headers: {
|
||||
'Accept': 'audio/mpeg'
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const blob = new Blob([response.data], { type: 'audio/mpeg' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
if (currentSection.audio?.source) {
|
||||
URL.revokeObjectURL(currentSection.audio?.source)
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_STATE",
|
||||
payload: {
|
||||
sectionId,
|
||||
update: {
|
||||
audio: {
|
||||
source: url,
|
||||
repeatableTimes: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
toast.success('Audio generated successfully!');
|
||||
} catch (error: any) {
|
||||
toast.error('Failed to generate audio');
|
||||
} finally {
|
||||
setAudioLoading(false);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentSection.script, dispatch]);
|
||||
|
||||
const canPreview = sections.some(
|
||||
(s) => (s.state as ListeningPart).exercises && (s.state as ListeningPart).exercises.length > 0
|
||||
);
|
||||
|
||||
const canSubmit = sections.every(
|
||||
(s) => (s.state as ListeningPart).exercises &&
|
||||
(s.state as ListeningPart).exercises.length > 0 &&
|
||||
(s.state as ListeningPart).audio !== undefined
|
||||
(s) => (s.state as ListeningPart).exercises &&
|
||||
(s.state as ListeningPart).exercises.length > 0 &&
|
||||
(s.state as ListeningPart).audio !== undefined
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -198,22 +292,23 @@ const ListeningSettings: React.FC = () => {
|
||||
difficulty={difficulty}
|
||||
/>
|
||||
</Dropdown>
|
||||
{/*
|
||||
|
||||
<Dropdown
|
||||
title="Generate Audio"
|
||||
module={currentModule}
|
||||
open={localSettings.isExerciseDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen })}
|
||||
disabled={currentSection.script === undefined && currentSection.audio === undefined}
|
||||
open={localSettings.isAudioGenerationOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isAudioGenerationOpen: isOpen }, false)}
|
||||
disabled={currentSection.script === undefined && currentSection.audio === undefined || currentSection.exercises.length === 0}
|
||||
center
|
||||
>
|
||||
<ExercisePicker
|
||||
module="listening"
|
||||
<GenerateBtn
|
||||
module={currentModule}
|
||||
genType="context"
|
||||
sectionId={focusedSection}
|
||||
selectedExercises={selectedExercises}
|
||||
setSelectedExercises={setSelectedExercises}
|
||||
difficulty={difficulty}
|
||||
generateFnc={generateAudio}
|
||||
className="mb-4"
|
||||
/>
|
||||
</Dropdown>*/}
|
||||
</Dropdown>
|
||||
</SettingsEditor>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user