Updated part of the speaking accordingly
This commit is contained in:
@@ -7,18 +7,20 @@ import Button from "../Low/Button";
|
|||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import {downloadBlob} from "@/utils/evaluation";
|
import {downloadBlob} from "@/utils/evaluation";
|
||||||
import axios from "axios";
|
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), {
|
const ReactMediaRecorder = dynamic(() => import("react-media-recorder").then((mod) => mod.ReactMediaRecorder), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function Speaking({id, title, text, video_url, type, prompts, 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 [recordingDuration, setRecordingDuration] = useState(0);
|
||||||
const [isRecording, setIsRecording] = useState(false);
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
const [mediaBlob, setMediaBlob] = useState<string>();
|
const [mediaBlob, setMediaBlob] = useState<string>();
|
||||||
const [audioURL, setAudioURL] = useState<string>();
|
const [audioURL, setAudioURL] = useState<string>();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isPromptsModalOpen, setIsPromptsModalOpen] = useState(false);
|
||||||
|
|
||||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||||
|
|
||||||
@@ -93,9 +95,26 @@ export default function Speaking({id, title, text, video_url, type, prompts, use
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full w-full gap-9">
|
<div className="flex flex-col h-full w-full gap-9">
|
||||||
|
<Modal className="!w-96 aspect-square" isOpen={isPromptsModalOpen} onClose={() => setIsPromptsModalOpen(false)}>
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 w-full h-full">
|
||||||
|
<div className="flex flex-col gap-1 ml-4">
|
||||||
|
{prompts.map((x, index) => (
|
||||||
|
<li className="italic" key={index}>
|
||||||
|
{x}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{!!suffix && <span className="font-bold">{suffix}</span>}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
<div className="flex flex-col w-full gap-2 bg-mti-gray-smoke rounded-xl py-8 px-16">
|
<div className="flex flex-col w-full gap-2 bg-mti-gray-smoke rounded-xl py-8 px-16">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<span className="font-semibold">{title}</span>
|
<div className="flex flex-col gap-0">
|
||||||
|
<span className="font-semibold">{title}</span>
|
||||||
|
{prompts.length > 0 && (
|
||||||
|
<span className="font-semibold">You should talk for at least 30 seconds for your answer to be valid.</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{!video_url && (
|
{!video_url && (
|
||||||
<span className="font-regular">
|
<span className="font-regular">
|
||||||
{text.split("\\n").map((line, index) => (
|
{text.split("\\n").map((line, index) => (
|
||||||
@@ -107,7 +126,7 @@ export default function Speaking({id, title, text, video_url, type, prompts, use
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-6">
|
<div className="flex gap-6 items-center">
|
||||||
{video_url && (
|
{video_url && (
|
||||||
<div className="flex flex-col gap-4 w-full items-center">
|
<div className="flex flex-col gap-4 w-full items-center">
|
||||||
<video key={id} autoPlay controls className="max-w-3xl rounded-xl">
|
<video key={id} autoPlay controls className="max-w-3xl rounded-xl">
|
||||||
@@ -116,15 +135,8 @@ export default function Speaking({id, title, text, video_url, type, prompts, use
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{prompts && prompts.length > 0 && (
|
{prompts && prompts.length > 0 && (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="w-full aspect-square bg-white shadow rounded-xl flex items-center justify-center">
|
||||||
<span className="font-bold">You should talk about the following things:</span>
|
<Button onClick={() => setIsPromptsModalOpen(true)}>View Prompts</Button>
|
||||||
<div className="flex flex-col gap-1 ml-4">
|
|
||||||
{prompts.map((x, index) => (
|
|
||||||
<li className="italic" key={index}>
|
|
||||||
{x}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import {Dialog, Transition} from "@headlessui/react";
|
import {Dialog, Transition} from "@headlessui/react";
|
||||||
|
import clsx from "clsx";
|
||||||
import {Fragment, ReactElement} from "react";
|
import {Fragment, ReactElement} from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
className?: string;
|
||||||
children?: ReactElement;
|
children?: ReactElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Modal({isOpen, title, onClose, children}: Props) {
|
export default function Modal({isOpen, title, className, onClose, children}: Props) {
|
||||||
return (
|
return (
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
<Dialog as="div" className="relative z-[200]" onClose={onClose}>
|
<Dialog as="div" className="relative z-[200]" onClose={onClose}>
|
||||||
@@ -33,7 +35,11 @@ export default function Modal({isOpen, title, onClose, children}: Props) {
|
|||||||
leave="ease-in duration-200"
|
leave="ease-in duration-200"
|
||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95">
|
leaveTo="opacity-0 scale-95">
|
||||||
<Dialog.Panel className="w-full max-w-6xl transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
<Dialog.Panel
|
||||||
|
className={clsx(
|
||||||
|
"w-full max-w-6xl transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all",
|
||||||
|
className,
|
||||||
|
)}>
|
||||||
{title && (
|
{title && (
|
||||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
||||||
{title}
|
{title}
|
||||||
|
|||||||
@@ -132,11 +132,22 @@ export default function InteractiveSpeaking({
|
|||||||
{userSolutions && userSolutions.length > 0 && userSolutions[0].evaluation && typeof userSolutions[0].evaluation !== "string" && (
|
{userSolutions && userSolutions.length > 0 && userSolutions[0].evaluation && typeof userSolutions[0].evaluation !== "string" && (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<div className="flex gap-4 px-1">
|
<div className="flex gap-4 px-1">
|
||||||
{Object.keys(userSolutions[0].evaluation!.task_response).map((key) => (
|
{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}>
|
const taskResponse = userSolutions[0].evaluation!.task_response[key];
|
||||||
{key}: Level {userSolutions[0].evaluation!.task_response[key]}
|
const grade: number = typeof taskResponse === "number" ? taskResponse : taskResponse.grade;
|
||||||
</div>
|
|
||||||
))}
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"bg-ielts-speaking text-ielts-speaking-light rounded-xl px-4 py-2",
|
||||||
|
typeof taskResponse !== "number" && "tooltip",
|
||||||
|
)}
|
||||||
|
data-tip={typeof taskResponse !== "number" ? taskResponse.comment : ""}
|
||||||
|
key={key}>
|
||||||
|
{key}: Level {grade}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
{userSolutions[0].evaluation &&
|
{userSolutions[0].evaluation &&
|
||||||
Object.keys(userSolutions[0].evaluation).filter((x) => x.startsWith("perfect_answer")).length === 3 ? (
|
Object.keys(userSolutions[0].evaluation).filter((x) => x.startsWith("perfect_answer")).length === 3 ? (
|
||||||
|
|||||||
@@ -126,11 +126,22 @@ export default function Speaking({id, type, title, video_url, text, prompts, use
|
|||||||
{userSolutions && userSolutions.length > 0 && userSolutions[0].evaluation && typeof userSolutions[0].evaluation !== "string" && (
|
{userSolutions && userSolutions.length > 0 && userSolutions[0].evaluation && typeof userSolutions[0].evaluation !== "string" && (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<div className="flex gap-4 px-1">
|
<div className="flex gap-4 px-1">
|
||||||
{Object.keys(userSolutions[0].evaluation!.task_response).map((key) => (
|
{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}>
|
const taskResponse = userSolutions[0].evaluation!.task_response[key];
|
||||||
{key}: Level {userSolutions[0].evaluation!.task_response[key]}
|
const grade: number = typeof taskResponse === "number" ? taskResponse : taskResponse.grade;
|
||||||
</div>
|
|
||||||
))}
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"bg-ielts-speaking text-ielts-speaking-light rounded-xl px-4 py-2",
|
||||||
|
typeof taskResponse !== "number" && "tooltip",
|
||||||
|
)}
|
||||||
|
data-tip={typeof taskResponse !== "number" ? taskResponse.comment : ""}
|
||||||
|
key={key}>
|
||||||
|
{key}: Level {grade}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
{userSolutions[0].evaluation &&
|
{userSolutions[0].evaluation &&
|
||||||
(userSolutions[0].evaluation.perfect_answer || userSolutions[0].evaluation.perfect_answer_1) ? (
|
(userSolutions[0].evaluation.perfect_answer || userSolutions[0].evaluation.perfect_answer_1) ? (
|
||||||
|
|||||||
@@ -117,11 +117,22 @@ export default function Writing({id, type, prompt, attachment, userSolutions, on
|
|||||||
{userSolutions && userSolutions.length > 0 && userSolutions[0].evaluation && typeof userSolutions[0].evaluation !== "string" && (
|
{userSolutions && userSolutions.length > 0 && userSolutions[0].evaluation && typeof userSolutions[0].evaluation !== "string" && (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<div className="flex gap-4 px-1">
|
<div className="flex gap-4 px-1">
|
||||||
{Object.keys(userSolutions[0].evaluation!.task_response).map((key) => (
|
{Object.keys(userSolutions[0].evaluation!.task_response).map((key) => {
|
||||||
<div className="bg-ielts-writing text-ielts-writing-light rounded-xl px-4 py-2" key={key}>
|
const taskResponse = userSolutions[0].evaluation!.task_response[key];
|
||||||
{key}: Level {userSolutions[0].evaluation!.task_response[key]}
|
const grade: number = typeof taskResponse === "number" ? taskResponse : taskResponse.grade;
|
||||||
</div>
|
|
||||||
))}
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"bg-ielts-speaking text-ielts-speaking-light rounded-xl px-4 py-2",
|
||||||
|
typeof taskResponse !== "number" && "tooltip",
|
||||||
|
)}
|
||||||
|
data-tip={typeof taskResponse !== "number" ? taskResponse.comment : ""}
|
||||||
|
key={key}>
|
||||||
|
{key}: Level {grade}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
{userSolutions[0].evaluation && userSolutions[0].evaluation.perfect_answer ? (
|
{userSolutions[0].evaluation && userSolutions[0].evaluation.perfect_answer ? (
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export type Exercise =
|
|||||||
export interface Evaluation {
|
export interface Evaluation {
|
||||||
comment: string;
|
comment: string;
|
||||||
overall: number;
|
overall: number;
|
||||||
task_response: {[key: string]: number};
|
task_response: {[key: string]: number | {grade: number; comment: string}};
|
||||||
misspelled_pairs?: {correction: string | null; misspelled: string}[];
|
misspelled_pairs?: {correction: string | null; misspelled: string}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,6 +163,7 @@ export interface SpeakingExercise {
|
|||||||
title: string;
|
title: string;
|
||||||
text: string;
|
text: string;
|
||||||
prompts: string[];
|
prompts: string[];
|
||||||
|
suffix?: string;
|
||||||
video_url: string;
|
video_url: string;
|
||||||
userSolutions: {
|
userSolutions: {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user