Updated part of the speaking accordingly

This commit is contained in:
Tiago Ribeiro
2024-06-13 18:17:07 +01:00
parent 11c6f70576
commit 0eddded560
6 changed files with 82 additions and 30 deletions

View File

@@ -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">
<div className="flex flex-col gap-0">
<span className="font-semibold">{title}</span> <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>

View File

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

View File

@@ -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;
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> </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 ? (

View File

@@ -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;
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> </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) ? (

View File

@@ -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;
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> </div>
{userSolutions[0].evaluation && userSolutions[0].evaluation.perfect_answer ? ( {userSolutions[0].evaluation && userSolutions[0].evaluation.perfect_answer ? (
<Tab.Group> <Tab.Group>

View File

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