Finallyyyyyy finished the whole Speaking flow along with the solution page
This commit is contained in:
@@ -4,7 +4,6 @@ import {Fragment, useEffect, useState} from "react";
|
|||||||
import {BsCheckCircleFill, BsMicFill, BsPauseCircle, BsPlayCircle, BsTrashFill} from "react-icons/bs";
|
import {BsCheckCircleFill, BsMicFill, BsPauseCircle, BsPlayCircle, BsTrashFill} from "react-icons/bs";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
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), {
|
const ReactMediaRecorder = dynamic(() => import("react-media-recorder").then((mod) => mod.ReactMediaRecorder), {
|
||||||
@@ -29,31 +28,6 @@ export default function Speaking({id, title, text, type, prompts, onNext, onBack
|
|||||||
};
|
};
|
||||||
}, [isRecording]);
|
}, [isRecording]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const uploadFile = () => {
|
|
||||||
if (mediaBlob) {
|
|
||||||
axios.get(mediaBlob, {responseType: "arraybuffer"}).then((response) => {
|
|
||||||
const audioBlob = Buffer.from(response.data, "binary");
|
|
||||||
const audioFile = new File([audioBlob], "audio.wav", {type: "audio/wav"});
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("audio", audioFile, "audio.wav");
|
|
||||||
formData.append("question", `${text.replaceAll("\n", "")} You should talk about: ${prompts.join(", ")}`);
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "audio/mp3",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
axios.post("/api/evaluate/speaking", formData, config);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (mediaBlob) uploadFile();
|
|
||||||
}, [mediaBlob, text, prompts]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full w-full gap-9">
|
<div className="flex flex-col h-full w-full gap-9">
|
||||||
<div className="flex flex-col w-full gap-14 bg-mti-gray-smoke rounded-xl py-8 pb-12 px-16">
|
<div className="flex flex-col w-full gap-14 bg-mti-gray-smoke rounded-xl py-8 pb-12 px-16">
|
||||||
|
|||||||
84
src/components/Solutions/Speaking.tsx
Normal file
84
src/components/Solutions/Speaking.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import {SpeakingExercise} from "@/interfaces/exam";
|
||||||
|
import {CommonProps} from ".";
|
||||||
|
import {Fragment, useEffect, useState} from "react";
|
||||||
|
import Button from "../Low/Button";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const Waveform = dynamic(() => import("../Waveform"), {ssr: false});
|
||||||
|
|
||||||
|
export default function Speaking({title, text, prompts, userSolutions, onNext, onBack}: SpeakingExercise & CommonProps) {
|
||||||
|
const [solutionURL, setSolutionURL] = useState<string>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
axios.post(`/api/speaking`, {path: userSolutions[0].solution}, {responseType: "arraybuffer"}).then(({data}) => {
|
||||||
|
const blob = new Blob([data], {type: "audio/wav"});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
setSolutionURL(url);
|
||||||
|
});
|
||||||
|
}, [userSolutions]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col h-full w-full gap-8 mb-20">
|
||||||
|
<div className="flex flex-col w-full gap-14 bg-mti-gray-smoke rounded-xl py-8 pb-12 px-16">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<span className="font-semibold">{title}</span>
|
||||||
|
<span className="font-regular">
|
||||||
|
{text.split("\\n").map((line, index) => (
|
||||||
|
<Fragment key={index}>
|
||||||
|
<span>{line}</span>
|
||||||
|
<br />
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<span className="font-bold">You should talk about the following things:</span>
|
||||||
|
<div className="flex flex-col gap-1 ml-4">
|
||||||
|
{prompts.map((x, index) => (
|
||||||
|
<li className="italic" key={index}>
|
||||||
|
{x}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full h-full flex flex-col gap-8">
|
||||||
|
<div className="w-full p-4 px-8 bg-transparent border-2 border-mti-gray-platinum rounded-2xl flex-col gap-8 items-center">
|
||||||
|
<div className="flex gap-8 items-center justify-center py-8">
|
||||||
|
{solutionURL && <Waveform audio={solutionURL} waveColor="#FCDDEC" progressColor="#EF5DA8" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{userSolutions && userSolutions.length > 0 && (
|
||||||
|
<div className="flex flex-col gap-4 w-full">
|
||||||
|
<div className="flex gap-4 px-1">
|
||||||
|
{Object.keys(userSolutions[0].evaluation!.task_response).map((key) => (
|
||||||
|
<div className="bg-ielts-speaking text-ielts-speaking-light rounded-xl px-4 py-2" key={key}>
|
||||||
|
{key}: Level {userSolutions[0].evaluation!.task_response[key]}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-full min-h-fit cursor-text px-7 py-8 bg-mti-gray-smoke rounded-3xl">
|
||||||
|
{userSolutions[0].evaluation!.comment}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
||||||
|
<Button color="green" variant="outline" onClick={onBack} className="max-w-[200px] w-full">
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button color="green" onClick={() => onNext()} className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,16 @@
|
|||||||
import {Exercise, FillBlanksExercise, MatchSentencesExercise, MultipleChoiceExercise, WriteBlanksExercise, WritingExercise} from "@/interfaces/exam";
|
import {
|
||||||
|
Exercise,
|
||||||
|
FillBlanksExercise,
|
||||||
|
MatchSentencesExercise,
|
||||||
|
MultipleChoiceExercise,
|
||||||
|
SpeakingExercise,
|
||||||
|
WriteBlanksExercise,
|
||||||
|
WritingExercise,
|
||||||
|
} from "@/interfaces/exam";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import FillBlanks from "./FillBlanks";
|
import FillBlanks from "./FillBlanks";
|
||||||
import MultipleChoice from "./MultipleChoice";
|
import MultipleChoice from "./MultipleChoice";
|
||||||
|
import Speaking from "./Speaking";
|
||||||
import WriteBlanks from "./WriteBlanks";
|
import WriteBlanks from "./WriteBlanks";
|
||||||
import Writing from "./Writing";
|
import Writing from "./Writing";
|
||||||
|
|
||||||
@@ -24,5 +33,7 @@ export const renderSolution = (exercise: Exercise, onNext: () => void, onBack: (
|
|||||||
return <WriteBlanks {...(exercise as WriteBlanksExercise)} onNext={onNext} onBack={onBack} />;
|
return <WriteBlanks {...(exercise as WriteBlanksExercise)} onNext={onNext} onBack={onBack} />;
|
||||||
case "writing":
|
case "writing":
|
||||||
return <Writing {...(exercise as WritingExercise)} onNext={onNext} onBack={onBack} />;
|
return <Writing {...(exercise as WritingExercise)} onNext={onNext} onBack={onBack} />;
|
||||||
|
case "speaking":
|
||||||
|
return <Speaking {...(exercise as SpeakingExercise)} onNext={onNext} onBack={onBack} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default function App({Component, pageProps}: AppProps) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset();
|
if (router.pathname !== "/exercises") reset();
|
||||||
}, [router.pathname, reset]);
|
}, [router.pathname, reset]);
|
||||||
|
|
||||||
return <Component {...pageProps} />;
|
return <Component {...pageProps} />;
|
||||||
|
|||||||
@@ -20,26 +20,25 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const storage = getStorage(app);
|
const storage = getStorage(app);
|
||||||
|
|
||||||
const form = formidable({keepExtensions: true, uploadDir: "./"});
|
const form = formidable({keepExtensions: true, uploadDir: "./"});
|
||||||
form.parse(req, (err, fields, files) => {
|
const [fields, files] = await form.parse(req);
|
||||||
const audioFile = (files.audio as unknown as PersistentFile[])[0];
|
const audioFile = (files.audio as unknown as PersistentFile[])[0];
|
||||||
const audioFileRef = ref(storage, `speaking_recordings/${(audioFile as any).newFilename}`);
|
const audioFileRef = ref(storage, `speaking_recordings/${(audioFile as any).newFilename}`);
|
||||||
|
|
||||||
const binary = fs.readFileSync((audioFile as any).filepath).buffer;
|
const binary = fs.readFileSync((audioFile as any).filepath).buffer;
|
||||||
uploadBytes(audioFileRef, binary).then(async (snapshot) => {
|
const snapshot = await uploadBytes(audioFileRef, binary);
|
||||||
const backendRequest = await axios.post(
|
|
||||||
`${process.env.BACKEND_URL}/speaking_task_1`,
|
|
||||||
{question: (fields.question as string[]).join(""), answer: snapshot.metadata.fullPath},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
fs.rmSync((audioFile as any).filepath);
|
const backendRequest = await axios.post(
|
||||||
res.status(200).json({...backendRequest.data, fullPath: snapshot.metadata.fullPath});
|
`${process.env.BACKEND_URL}/speaking_task_1`,
|
||||||
});
|
{question: (fields.question as string[]).join(""), answer: snapshot.metadata.fullPath},
|
||||||
});
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.rmSync((audioFile as any).filepath);
|
||||||
|
res.status(200).json({...backendRequest.data, fullPath: snapshot.metadata.fullPath});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
|||||||
@@ -4,19 +4,23 @@ import {withIronSessionApiRoute} from "iron-session/next";
|
|||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import {getDownloadURL, getStorage, ref} from "firebase/storage";
|
import {getDownloadURL, getStorage, ref} from "firebase/storage";
|
||||||
import {app} from "@/firebase";
|
import {app} from "@/firebase";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
// export default withIronSessionApiRoute(handler, sessionOptions);
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
export default handler;
|
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
// if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
// res.status(401).json({ok: false});
|
res.status(401).json({ok: false});
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
|
|
||||||
const storage = getStorage(app);
|
const storage = getStorage(app);
|
||||||
const {path} = req.body as {path: string};
|
const {path} = req.body as {path: string};
|
||||||
|
|
||||||
const pathReference = ref(storage, path);
|
const pathReference = ref(storage, path);
|
||||||
getDownloadURL(pathReference).then((url) => res.status(200).json({url}));
|
const url = await getDownloadURL(pathReference);
|
||||||
|
|
||||||
|
const response = await axios.get(url, {responseType: "arraybuffer"});
|
||||||
|
|
||||||
|
res.status(200).send(response.data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export default function Page() {
|
|||||||
const speakingExam = exams.find((x) => x.id === examId)!;
|
const speakingExam = exams.find((x) => x.id === examId)!;
|
||||||
const exercise = speakingExam.exercises.find((x) => x.id === exerciseId)! as SpeakingExercise;
|
const exercise = speakingExam.exercises.find((x) => x.id === exerciseId)! as SpeakingExercise;
|
||||||
|
|
||||||
const blobResponse = await axios.get(exercise.userSolutions[0].solution.trim());
|
const blobResponse = await axios.get(solution.solutions[0].solution.trim(), {responseType: "arraybuffer"});
|
||||||
const audioBlob = Buffer.from(blobResponse.data, "binary");
|
const audioBlob = Buffer.from(blobResponse.data, "binary");
|
||||||
const audioFile = new File([audioBlob], "audio.wav", {type: "audio/wav"});
|
const audioFile = new File([audioBlob], "audio.wav", {type: "audio/wav"});
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ export default function Page() {
|
|||||||
{
|
{
|
||||||
...solution,
|
...solution,
|
||||||
score: {
|
score: {
|
||||||
correct: speakingReverseMarking[response.data.overall],
|
correct: speakingReverseMarking[response.data.overall] || 0,
|
||||||
missing: 0,
|
missing: 0,
|
||||||
total: 100,
|
total: 100,
|
||||||
},
|
},
|
||||||
@@ -180,7 +180,7 @@ export default function Page() {
|
|||||||
{
|
{
|
||||||
...solution,
|
...solution,
|
||||||
score: {
|
score: {
|
||||||
correct: writingReverseMarking[response.data.overall],
|
correct: writingReverseMarking[response.data.overall] || 0,
|
||||||
missing: 0,
|
missing: 0,
|
||||||
total: 100,
|
total: 100,
|
||||||
},
|
},
|
||||||
@@ -294,11 +294,6 @@ export default function Page() {
|
|||||||
return <Writing exam={exam} onFinish={onFinish} showSolutions={showSolutions} />;
|
return <Writing exam={exam} onFinish={onFinish} showSolutions={showSolutions} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exam && exam.module === "speaking" && showSolutions) {
|
|
||||||
setModuleIndex((prev) => prev + 1);
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exam && exam.module === "speaking") {
|
if (exam && exam.module === "speaking") {
|
||||||
return <Speaking exam={exam} onFinish={onFinish} showSolutions={showSolutions} />;
|
return <Speaking exam={exam} onFinish={onFinish} showSolutions={showSolutions} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,17 @@ import {Module} from "@/interfaces";
|
|||||||
|
|
||||||
import Selection from "@/exams/Selection";
|
import Selection from "@/exams/Selection";
|
||||||
import Reading from "@/exams/Reading";
|
import Reading from "@/exams/Reading";
|
||||||
import {Exam, ListeningExam, ReadingExam, SpeakingExam, UserSolution, Evaluation, WritingExam, WritingExercise} from "@/interfaces/exam";
|
import {
|
||||||
|
Exam,
|
||||||
|
ListeningExam,
|
||||||
|
ReadingExam,
|
||||||
|
SpeakingExam,
|
||||||
|
UserSolution,
|
||||||
|
Evaluation,
|
||||||
|
WritingExam,
|
||||||
|
WritingExercise,
|
||||||
|
SpeakingExercise,
|
||||||
|
} from "@/interfaces/exam";
|
||||||
import Listening from "@/exams/Listening";
|
import Listening from "@/exams/Listening";
|
||||||
import Writing from "@/exams/Writing";
|
import Writing from "@/exams/Writing";
|
||||||
import {ToastContainer, toast} from "react-toastify";
|
import {ToastContainer, toast} from "react-toastify";
|
||||||
@@ -22,7 +32,7 @@ import useExamStore from "@/stores/examStore";
|
|||||||
import Sidebar from "@/components/Sidebar";
|
import Sidebar from "@/components/Sidebar";
|
||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import {sortByModule} from "@/utils/moduleUtils";
|
import {sortByModule} from "@/utils/moduleUtils";
|
||||||
import {writingReverseMarking} from "@/utils/score";
|
import {speakingReverseMarking, writingReverseMarking} from "@/utils/score";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
@@ -122,6 +132,42 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const evaluateSpeakingAnswer = async (examId: string, exerciseId: string, solution: UserSolution) => {
|
||||||
|
const speakingExam = exams.find((x) => x.id === examId)!;
|
||||||
|
const exercise = speakingExam.exercises.find((x) => x.id === exerciseId)! as SpeakingExercise;
|
||||||
|
|
||||||
|
const blobResponse = await axios.get(solution.solutions[0].solution.trim(), {responseType: "arraybuffer"});
|
||||||
|
const audioBlob = Buffer.from(blobResponse.data, "binary");
|
||||||
|
const audioFile = new File([audioBlob], "audio.wav", {type: "audio/wav"});
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("audio", audioFile, "audio.wav");
|
||||||
|
formData.append("question", `${exercise.text.replaceAll("\n", "")} You should talk about: ${exercise.prompts.join(", ")}`);
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "audio/mp3",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios.post("/api/evaluate/speaking", formData, config);
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
setUserSolutions([
|
||||||
|
...userSolutions.filter((x) => x.exercise !== exerciseId),
|
||||||
|
{
|
||||||
|
...solution,
|
||||||
|
score: {
|
||||||
|
correct: speakingReverseMarking[response.data.overall] || 0,
|
||||||
|
missing: 0,
|
||||||
|
total: 100,
|
||||||
|
},
|
||||||
|
solutions: [{id: exerciseId, solution: response.data.fullPath, evaluation: response.data}],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const evaluateWritingAnswer = async (examId: string, exerciseId: string, solution: UserSolution) => {
|
const evaluateWritingAnswer = async (examId: string, exerciseId: string, solution: UserSolution) => {
|
||||||
const writingExam = exams.find((x) => x.id === examId)!;
|
const writingExam = exams.find((x) => x.id === examId)!;
|
||||||
const exercise = writingExam.exercises.find((x) => x.id === exerciseId)! as WritingExercise;
|
const exercise = writingExam.exercises.find((x) => x.id === exerciseId)! as WritingExercise;
|
||||||
@@ -137,7 +183,7 @@ export default function Page() {
|
|||||||
{
|
{
|
||||||
...solution,
|
...solution,
|
||||||
score: {
|
score: {
|
||||||
correct: writingReverseMarking[response.data.overall],
|
correct: writingReverseMarking[response.data.overall] || 0,
|
||||||
missing: 0,
|
missing: 0,
|
||||||
total: 100,
|
total: 100,
|
||||||
},
|
},
|
||||||
@@ -148,9 +194,7 @@ export default function Page() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateExamWithUserSolutions = (exam: Exam): Exam => {
|
const updateExamWithUserSolutions = (exam: Exam): Exam => {
|
||||||
const exercises = exam.exercises.map((x) =>
|
const exercises = exam.exercises.map((x) => Object.assign(x, {userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions}));
|
||||||
Object.assign(x, !x.userSolutions ? {userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions} : x.userSolutions),
|
|
||||||
);
|
|
||||||
|
|
||||||
return Object.assign(exam, exercises);
|
return Object.assign(exam, exercises);
|
||||||
};
|
};
|
||||||
@@ -158,11 +202,17 @@ export default function Page() {
|
|||||||
const onFinish = (solutions: UserSolution[]) => {
|
const onFinish = (solutions: UserSolution[]) => {
|
||||||
const solutionIds = solutions.map((x) => x.exercise);
|
const solutionIds = solutions.map((x) => x.exercise);
|
||||||
|
|
||||||
if (exam && exam.module === "writing" && solutions.length > 0 && !showSolutions) {
|
if (exam && (exam.module === "writing" || exam.module === "speaking") && solutions.length > 0 && !showSolutions) {
|
||||||
setHasBeenUploaded(true);
|
setHasBeenUploaded(true);
|
||||||
setIsEvaluationLoading(true);
|
setIsEvaluationLoading(true);
|
||||||
Promise.all(
|
Promise.all(
|
||||||
exam.exercises.map((exercise) => evaluateWritingAnswer(exam.id, exercise.id, solutions.find((x) => x.exercise === exercise.id)!)),
|
exam.exercises.map((exercise) =>
|
||||||
|
(exam.module === "writing" ? evaluateWritingAnswer : evaluateSpeakingAnswer)(
|
||||||
|
exam.id,
|
||||||
|
exercise.id,
|
||||||
|
solutions.find((x) => x.exercise === exercise.id)!,
|
||||||
|
),
|
||||||
|
),
|
||||||
).finally(() => {
|
).finally(() => {
|
||||||
setIsEvaluationLoading(false);
|
setIsEvaluationLoading(false);
|
||||||
setHasBeenUploaded(false);
|
setHasBeenUploaded(false);
|
||||||
@@ -245,11 +295,6 @@ export default function Page() {
|
|||||||
return <Writing exam={exam} onFinish={onFinish} showSolutions={showSolutions} />;
|
return <Writing exam={exam} onFinish={onFinish} showSolutions={showSolutions} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exam && exam.module === "speaking" && showSolutions) {
|
|
||||||
setModuleIndex((prev) => prev + 1);
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exam && exam.module === "speaking") {
|
if (exam && exam.module === "speaking") {
|
||||||
return <Speaking exam={exam} onFinish={onFinish} showSolutions={showSolutions} />;
|
return <Speaking exam={exam} onFinish={onFinish} showSolutions={showSolutions} />;
|
||||||
}
|
}
|
||||||
@@ -260,7 +305,7 @@ export default function Page() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Exam | IELTS GPT</title>
|
<title>Exercises | IELTS GPT</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ export default function History({user}: {user: User}) {
|
|||||||
.sort(sortByModule)
|
.sort(sortByModule)
|
||||||
.map((x) => x!.module),
|
.map((x) => x!.module),
|
||||||
);
|
);
|
||||||
router.push("/exam");
|
router.push("/exercises");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,27 +4,45 @@ type Type = "academic" | "general";
|
|||||||
|
|
||||||
export const writingReverseMarking: {[key: number]: number} = {
|
export const writingReverseMarking: {[key: number]: number} = {
|
||||||
9: 90,
|
9: 90,
|
||||||
|
8.5: 85,
|
||||||
8: 80,
|
8: 80,
|
||||||
|
7.5: 75,
|
||||||
7: 70,
|
7: 70,
|
||||||
|
6.5: 65,
|
||||||
6: 60,
|
6: 60,
|
||||||
|
5.5: 55,
|
||||||
5: 50,
|
5: 50,
|
||||||
|
4.5: 45,
|
||||||
4: 40,
|
4: 40,
|
||||||
|
3.5: 35,
|
||||||
3: 30,
|
3: 30,
|
||||||
|
2.5: 25,
|
||||||
2: 20,
|
2: 20,
|
||||||
|
1.5: 15,
|
||||||
1: 10,
|
1: 10,
|
||||||
|
0.5: 5,
|
||||||
0: 0,
|
0: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const speakingReverseMarking: {[key: number]: number} = {
|
export const speakingReverseMarking: {[key: number]: number} = {
|
||||||
9: 90,
|
9: 90,
|
||||||
|
8.5: 85,
|
||||||
8: 80,
|
8: 80,
|
||||||
|
7.5: 75,
|
||||||
7: 70,
|
7: 70,
|
||||||
|
6.5: 65,
|
||||||
6: 60,
|
6: 60,
|
||||||
|
5.5: 55,
|
||||||
5: 50,
|
5: 50,
|
||||||
|
4.5: 45,
|
||||||
4: 40,
|
4: 40,
|
||||||
|
3.5: 35,
|
||||||
3: 30,
|
3: 30,
|
||||||
|
2.5: 25,
|
||||||
2: 20,
|
2: 20,
|
||||||
|
1.5: 15,
|
||||||
1: 10,
|
1: 10,
|
||||||
|
0.5: 5,
|
||||||
0: 0,
|
0: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user