Now only when the user submits the listening exam are the mp3 are uploaded onto firebase bucket

This commit is contained in:
Carlos-Mesquita
2024-11-07 11:06:33 +00:00
parent 0741c4c647
commit 50481a836e
11 changed files with 373 additions and 76 deletions

View File

@@ -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)}
>

View File

@@ -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}

View File

@@ -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>
);
};