From a4a40b91456c00cfd7dbc44a3dca6081a3778d86 Mon Sep 17 00:00:00 2001 From: Carlos Mesquita Date: Mon, 5 Aug 2024 21:44:14 +0100 Subject: [PATCH] Bug fixes to training, added a spinner to record while it loads, made changes to speaking as requested --- src/components/Exercises/Speaking.tsx | 70 ++++++++++++++----- src/components/StatGridItem.tsx | 30 +++++--- .../TrainingContent/TrainingScore.tsx | 3 +- src/pages/record.tsx | 20 ++++-- src/pages/training/[id]/index.tsx | 4 +- 5 files changed, 92 insertions(+), 35 deletions(-) diff --git a/src/components/Exercises/Speaking.tsx b/src/components/Exercises/Speaking.tsx index 4a35458a..106b76d4 100644 --- a/src/components/Exercises/Speaking.tsx +++ b/src/components/Exercises/Speaking.tsx @@ -1,33 +1,34 @@ -import {SpeakingExercise} from "@/interfaces/exam"; -import {CommonProps} from "."; -import {Fragment, useEffect, useState} from "react"; -import {BsCheckCircleFill, BsMicFill, BsPauseCircle, BsPlayCircle, BsTrashFill} from "react-icons/bs"; +import { SpeakingExercise } from "@/interfaces/exam"; +import { CommonProps } from "."; +import { Fragment, useEffect, useState } from "react"; +import { BsCheckCircleFill, BsMicFill, BsPauseCircle, BsPlayCircle, BsTrashFill } from "react-icons/bs"; import dynamic from "next/dynamic"; import Button from "../Low/Button"; import useExamStore from "@/stores/examStore"; -import {downloadBlob} from "@/utils/evaluation"; +import { downloadBlob } from "@/utils/evaluation"; import axios from "axios"; import Modal from "../Modal"; -const Waveform = dynamic(() => import("../Waveform"), {ssr: false}); +const Waveform = dynamic(() => import("../Waveform"), { ssr: false }); const ReactMediaRecorder = dynamic(() => import("react-media-recorder").then((mod) => mod.ReactMediaRecorder), { ssr: false, }); -export default function Speaking({id, title, text, video_url, type, prompts, suffix, userSolutions, onNext, onBack}: SpeakingExercise & CommonProps) { +export default function Speaking({ id, title, text, video_url, type, prompts, suffix, userSolutions, onNext, onBack }: SpeakingExercise & CommonProps) { const [recordingDuration, setRecordingDuration] = useState(0); const [isRecording, setIsRecording] = useState(false); const [mediaBlob, setMediaBlob] = useState(); const [audioURL, setAudioURL] = useState(); const [isLoading, setIsLoading] = useState(false); const [isPromptsModalOpen, setIsPromptsModalOpen] = useState(false); + const [inputText, setInputText] = useState(""); const hasExamEnded = useExamStore((state) => state.hasExamEnded); const saveToStorage = async () => { if (mediaBlob && mediaBlob.startsWith("blob")) { const blobBuffer = await downloadBlob(mediaBlob); - const audioFile = new File([blobBuffer], "audio.wav", {type: "audio/wav"}); + const audioFile = new File([blobBuffer], "audio.wav", { type: "audio/wav" }); const seed = Math.random().toString().replace("0.", ""); @@ -41,8 +42,8 @@ export default function Speaking({id, title, text, video_url, type, prompts, suf }, }; - const response = await axios.post<{path: string}>("/api/storage/insert", formData, config); - if (audioURL) await axios.post("/api/storage/delete", {path: audioURL}); + const response = await axios.post<{ path: string }>("/api/storage/insert", formData, config); + if (audioURL) await axios.post("/api/storage/delete", { path: audioURL }); return response.data.path; } @@ -51,7 +52,7 @@ export default function Speaking({id, title, text, video_url, type, prompts, suf useEffect(() => { if (userSolutions.length > 0) { - const {solution} = userSolutions[0] as {solution?: string}; + const { solution } = userSolutions[0] as { solution?: string }; if (solution && !mediaBlob) setMediaBlob(solution); if (solution && !solution.startsWith("blob")) setAudioURL(solution); } @@ -78,8 +79,8 @@ export default function Speaking({id, title, text, video_url, type, prompts, suf const next = async () => { onNext({ exercise: id, - solutions: mediaBlob ? [{id, solution: mediaBlob}] : [], - score: {correct: 0, total: 100, missing: 0}, + solutions: mediaBlob ? [{ id, solution: mediaBlob }] : [], + score: { correct: 0, total: 100, missing: 0 }, type, }); }; @@ -87,12 +88,33 @@ export default function Speaking({id, title, text, video_url, type, prompts, suf const back = async () => { onBack({ exercise: id, - solutions: mediaBlob ? [{id, solution: mediaBlob}] : [], - score: {correct: 0, total: 100, missing: 0}, + solutions: mediaBlob ? [{ id, solution: mediaBlob }] : [], + score: { correct: 0, total: 100, missing: 0 }, type, }); }; + const handleNoteWriting = (e: React.ChangeEvent) => { + const newText = e.target.value; + const words = newText.match(/\S+/g); + const wordCount = words ? words.length : 0; + + if (wordCount <= 100) { + setInputText(newText); + } else { + let count = 0; + let lastIndex = 0; + const matches = newText.matchAll(/\S+/g); + for (const match of matches) { + count++; + if (count > 100) break; + lastIndex = match.index! + match[0].length; + } + + setInputText(newText.slice(0, lastIndex)); + } + }; + return (
setIsPromptsModalOpen(false)}> @@ -112,7 +134,7 @@ export default function Speaking({id, title, text, video_url, type, prompts, suf
{title} {prompts.length > 0 && ( - You should talk for at least 30 seconds for your answer to be valid. + You should talk for at least 1 minute and 30 seconds for your answer to be valid. )}
{!video_url && ( @@ -138,10 +160,24 @@ export default function Speaking({id, title, text, video_url, type, prompts, suf
+ {prompts && prompts.length > 0 && ( +
+