ENCOA-137: Top right side Next Button on the exams
This commit is contained in:
@@ -1,12 +1,11 @@
|
|||||||
import { FillBlanksExercise, FillBlanksMCOption } from "@/interfaces/exam";
|
import {FillBlanksExercise, FillBlanksMCOption} from "@/interfaces/exam";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
|
import {Fragment, useCallback, useEffect, useMemo, useState} from "react";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import { CommonProps } from "..";
|
import {CommonProps} from "..";
|
||||||
import Button from "../../Low/Button";
|
import Button from "../../Low/Button";
|
||||||
import { v4 } from "uuid";
|
import {v4} from "uuid";
|
||||||
|
|
||||||
|
|
||||||
const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||||
id,
|
id,
|
||||||
@@ -20,30 +19,27 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
}) => {
|
}) => {
|
||||||
const { shuffles, exam, partIndex, questionIndex, exerciseIndex } = useExamStore((state) => state);
|
const {shuffles, exam, partIndex, questionIndex, exerciseIndex} = useExamStore((state) => state);
|
||||||
const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions);
|
const [answers, setAnswers] = useState<{id: string; solution: string}[]>(userSolutions);
|
||||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||||
const setCurrentSolution = useExamStore((state) => state.setCurrentSolution);
|
const setCurrentSolution = useExamStore((state) => state.setCurrentSolution);
|
||||||
const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles;
|
const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles;
|
||||||
|
|
||||||
const [currentMCSelection, setCurrentMCSelection] = useState<{ id: string, selection: FillBlanksMCOption }>();
|
const [currentMCSelection, setCurrentMCSelection] = useState<{id: string; selection: FillBlanksMCOption}>();
|
||||||
|
|
||||||
const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
|
const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
|
||||||
return Array.isArray(words) && words.every(
|
return Array.isArray(words) && words.every((word) => word && typeof word === "object" && "id" in word && "options" in word);
|
||||||
word => word && typeof word === 'object' && 'id' in word && 'options' in word
|
};
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const excludeWordMCType = (x: any) => {
|
const excludeWordMCType = (x: any) => {
|
||||||
return typeof x === "string" ? x : x as { letter: string; word: string };
|
return typeof x === "string" ? x : (x as {letter: string; word: string});
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type });
|
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [hasExamEnded]);
|
}, [hasExamEnded]);
|
||||||
|
|
||||||
|
|
||||||
let correctWords: any;
|
let correctWords: any;
|
||||||
if (exam && (exam.module === "level" || exam.module === "reading") && exam.parts[partIndex].exercises[exerciseIndex].type === "fillBlanks") {
|
if (exam && (exam.module === "level" || exam.module === "reading") && exam.parts[partIndex].exercises[exerciseIndex].type === "fillBlanks") {
|
||||||
correctWords = (exam.parts[partIndex].exercises[exerciseIndex] as FillBlanksExercise).words;
|
correctWords = (exam.parts[partIndex].exercises[exerciseIndex] as FillBlanksExercise).words;
|
||||||
@@ -57,7 +53,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
const option = correctWords!.find((w: any) => {
|
const option = correctWords!.find((w: any) => {
|
||||||
if (typeof w === "string") {
|
if (typeof w === "string") {
|
||||||
return w.toLowerCase() === x.solution.toLowerCase();
|
return w.toLowerCase() === x.solution.toLowerCase();
|
||||||
} else if ('letter' in w) {
|
} else if ("letter" in w) {
|
||||||
return w.letter.toLowerCase() === x.solution.toLowerCase();
|
return w.letter.toLowerCase() === x.solution.toLowerCase();
|
||||||
} else {
|
} else {
|
||||||
return w.id.toString() === x.id.toString();
|
return w.id.toString() === x.id.toString();
|
||||||
@@ -67,17 +63,18 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
|
|
||||||
if (typeof option === "string") {
|
if (typeof option === "string") {
|
||||||
return solution.toLowerCase() === option.toLowerCase();
|
return solution.toLowerCase() === option.toLowerCase();
|
||||||
} else if ('letter' in option) {
|
} else if ("letter" in option) {
|
||||||
return solution.toLowerCase() === option.word.toLowerCase();
|
return solution.toLowerCase() === option.word.toLowerCase();
|
||||||
} else if ('options' in option) {
|
} else if ("options" in option) {
|
||||||
return option.options[solution as keyof typeof option.options] == x.solution;
|
return option.options[solution as keyof typeof option.options] == x.solution;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}).length;
|
}).length;
|
||||||
const missing = total - answers!.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
const missing = total - answers!.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
||||||
return { total, correct, missing };
|
return {total, correct, missing};
|
||||||
};
|
};
|
||||||
const renderLines = useCallback((line: string) => {
|
const renderLines = useCallback(
|
||||||
|
(line: string) => {
|
||||||
return (
|
return (
|
||||||
<div className="text-base leading-5" key={v4()}>
|
<div className="text-base leading-5" key={v4()}>
|
||||||
{reactStringReplace(line, /({{\d+}})/g, (match) => {
|
{reactStringReplace(line, /({{\d+}})/g, (match) => {
|
||||||
@@ -88,41 +85,43 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
currentMCSelection?.id == id && "!bg-mti-purple !text-white !outline-none !ring-0",
|
currentMCSelection?.id == id && "!bg-mti-purple !text-white !outline-none !ring-0",
|
||||||
!userSolution && "text-center text-mti-purple-light bg-mti-purple-ultralight",
|
!userSolution && "text-center text-mti-purple-light bg-mti-purple-ultralight",
|
||||||
userSolution && "text-center text-mti-purple-dark bg-mti-purple-ultralight",
|
userSolution && "text-center text-mti-purple-dark bg-mti-purple-ultralight",
|
||||||
)
|
);
|
||||||
return (
|
return variant === "mc" ? (
|
||||||
variant === "mc" ? (
|
|
||||||
<>
|
<>
|
||||||
{/*<span className="mr-2">{`(${id})`}</span>*/}
|
{/*<span className="mr-2">{`(${id})`}</span>*/}
|
||||||
<button
|
<button
|
||||||
className={styles}
|
className={styles}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCurrentMCSelection(
|
setCurrentMCSelection({
|
||||||
{
|
|
||||||
id: id,
|
id: id,
|
||||||
selection: words.find((x) => {
|
selection: words.find((x) => {
|
||||||
if (typeof x !== "string" && 'id' in x) {
|
if (typeof x !== "string" && "id" in x) {
|
||||||
return (x as FillBlanksMCOption).id.toString() == id.toString();
|
return (x as FillBlanksMCOption).id.toString() == id.toString();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}) as FillBlanksMCOption
|
}) as FillBlanksMCOption,
|
||||||
}
|
});
|
||||||
);
|
}}>
|
||||||
}}
|
{userSolution?.solution === undefined ? (
|
||||||
>
|
<span className="text-transparent select-none">placeholder</span>
|
||||||
{userSolution?.solution === undefined ? <span className="text-transparent select-none">placeholder</span> : <span> {userSolution.solution} </span>}
|
) : (
|
||||||
|
<span> {userSolution.solution} </span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<input
|
<input
|
||||||
className={styles}
|
className={styles}
|
||||||
onChange={(e) => setAnswers((prev) => [...prev.filter((x) => x.id !== id), { id, solution: e.target.value }])}
|
onChange={(e) => setAnswers((prev) => [...prev.filter((x) => x.id !== id), {id, solution: e.target.value}])}
|
||||||
value={userSolution?.solution} />
|
value={userSolution?.solution}
|
||||||
)
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, [variant, words, setCurrentMCSelection, answers, currentMCSelection]);
|
},
|
||||||
|
[variant, words, setCurrentMCSelection, answers, currentMCSelection],
|
||||||
|
);
|
||||||
|
|
||||||
const memoizedLines = useMemo(() => {
|
const memoizedLines = useMemo(() => {
|
||||||
return text.split("\\n").map((line, index) => (
|
return text.split("\\n").map((line, index) => (
|
||||||
@@ -134,50 +133,77 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [text, variant, renderLines, currentMCSelection]);
|
}, [text, variant, renderLines, currentMCSelection]);
|
||||||
|
|
||||||
|
|
||||||
const onSelection = (questionID: string, value: string) => {
|
const onSelection = (questionID: string, value: string) => {
|
||||||
setAnswers((prev) => [...prev.filter((x) => x.id !== questionID), { id: questionID, solution: value }]);
|
setAnswers((prev) => [...prev.filter((x) => x.id !== questionID), {id: questionID, solution: value}]);
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (variant === "mc") {
|
if (variant === "mc") {
|
||||||
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
setCurrentSolution({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps});
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers])
|
}, [answers]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps})}
|
||||||
|
className="max-w-[200px] w-full"
|
||||||
|
disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={() => {
|
||||||
|
onNext({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps});
|
||||||
|
}}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
||||||
{variant !== "mc" && <span className="text-sm w-full leading-6">
|
{variant !== "mc" && (
|
||||||
|
<span className="text-sm w-full leading-6">
|
||||||
{prompt.split("\\n").map((line, index) => (
|
{prompt.split("\\n").map((line, index) => (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
{line}
|
{line}
|
||||||
<br />
|
<br />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</span>}
|
|
||||||
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">
|
|
||||||
{memoizedLines}
|
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">{memoizedLines}</span>
|
||||||
{variant === "mc" && typeCheckWordsMC(words) ? (
|
{variant === "mc" && typeCheckWordsMC(words) ? (
|
||||||
<>
|
<>
|
||||||
{currentMCSelection && (
|
{currentMCSelection && (
|
||||||
<div className="bg-mti-gray-smoke rounded-xl flex flex-col gap-4 px-16 py-8">
|
<div className="bg-mti-gray-smoke rounded-xl flex flex-col gap-4 px-16 py-8">
|
||||||
<span className="font-medium text-lg text-mti-purple-dark mb-4 px-2">{`${currentMCSelection.id} - Select the appropriate word.`}</span>
|
<span className="font-medium text-lg text-mti-purple-dark mb-4 px-2">{`${currentMCSelection.id} - Select the appropriate word.`}</span>
|
||||||
<div className="flex gap-4 flex-wrap justify-between">
|
<div className="flex gap-4 flex-wrap justify-between">
|
||||||
{currentMCSelection.selection?.options && Object.entries(currentMCSelection.selection.options).sort((a, b) => a[0].localeCompare(b[0])).map(([key, value]) => {
|
{currentMCSelection.selection?.options &&
|
||||||
return <div
|
Object.entries(currentMCSelection.selection.options)
|
||||||
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
|
.map(([key, value]) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
key={v4()}
|
key={v4()}
|
||||||
onClick={() => onSelection(currentMCSelection.id, value)}
|
onClick={() => onSelection(currentMCSelection.id, value)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-base",
|
"flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-base",
|
||||||
!!answers.find((x) => x.solution.toLocaleLowerCase() === value.toLocaleLowerCase() && x.id === currentMCSelection.id) &&
|
!!answers.find(
|
||||||
"!bg-mti-purple-light !text-white",
|
(x) =>
|
||||||
|
x.solution.toLocaleLowerCase() === value.toLocaleLowerCase() &&
|
||||||
|
x.id === currentMCSelection.id,
|
||||||
|
) && "!bg-mti-purple-light !text-white",
|
||||||
)}>
|
)}>
|
||||||
<span className="font-semibold">{key}.</span>
|
<span className="font-semibold">{key}.</span>
|
||||||
<span>{value}</span>
|
<span>{value}</span>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -195,43 +221,42 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
<span
|
<span
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"border border-mti-purple-light rounded-full px-3 py-0.5 transition ease-in-out duration-300",
|
"border border-mti-purple-light rounded-full px-3 py-0.5 transition ease-in-out duration-300",
|
||||||
!!answers.find((x) => x.solution.toLowerCase() === (typeof v === "string" ? v : ("letter" in v ? v.letter : "")).toLowerCase()) &&
|
!!answers.find(
|
||||||
"bg-mti-purple-dark text-white",
|
(x) =>
|
||||||
|
x.solution.toLowerCase() ===
|
||||||
|
(typeof v === "string" ? v : "letter" in v ? v.letter : "").toLowerCase(),
|
||||||
|
) && "bg-mti-purple-dark text-white",
|
||||||
)}
|
)}
|
||||||
key={v4()}
|
key={v4()}>
|
||||||
>
|
|
||||||
{text}
|
{text}
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div >
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps })}
|
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps})}
|
||||||
className="max-w-[200px] w-full"
|
className="max-w-[200px] w-full"
|
||||||
disabled={
|
disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
|
||||||
exam && exam.module === "level" &&
|
|
||||||
partIndex === 0 &&
|
|
||||||
questionIndex === 0
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
onClick={() => { onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps }) }}
|
onClick={() => {
|
||||||
|
onNext({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps});
|
||||||
|
}}
|
||||||
className="max-w-[200px] self-end w-full">
|
className="max-w-[200px] self-end w-full">
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default FillBlanks;
|
export default FillBlanks;
|
||||||
|
|||||||
@@ -152,6 +152,16 @@ export default function InteractiveSpeaking({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="flex flex-col gap-4 mt-4 w-full">
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button color="purple" variant="outline" isLoading={isLoading} onClick={back} className="max-w-[200px] self-end w-full">
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button color="purple" disabled={!mediaBlob} isLoading={isLoading} onClick={next} className="max-w-[200px] self-end w-full">
|
||||||
|
{questionIndex + 1 < prompts.length ? "Next Prompt" : "Submit"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<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-8 bg-mti-gray-smoke rounded-xl py-8 pb-12 px-16">
|
<div className="flex flex-col w-full gap-8 bg-mti-gray-smoke rounded-xl py-8 pb-12 px-16">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
@@ -293,5 +303,6 @@ export default function InteractiveSpeaking({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,9 +71,9 @@ export default function MatchSentences({id, options, type, prompt, sentences, us
|
|||||||
const setCurrentSolution = useExamStore((state) => state.setCurrentSolution);
|
const setCurrentSolution = useExamStore((state) => state.setCurrentSolution);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type});
|
setCurrentSolution({exercise: id, solutions: answers, score: calculateScore(), type});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers, setAnswers])
|
}, [answers, setAnswers]);
|
||||||
|
|
||||||
const handleDragEnd = (event: DragEndEvent) => {
|
const handleDragEnd = (event: DragEndEvent) => {
|
||||||
if (event.over && event.over.id.toString().startsWith("droppable")) {
|
if (event.over && event.over.id.toString().startsWith("droppable")) {
|
||||||
@@ -100,7 +100,24 @@ export default function MatchSentences({id, options, type, prompt, sentences, us
|
|||||||
}, [hasExamEnded]);
|
}, [hasExamEnded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col gap-4 mt-4">
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type})}
|
||||||
|
className="max-w-[200px] w-full">
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={() => onNext({exercise: id, solutions: answers, score: calculateScore(), type})}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
||||||
<span className="text-sm w-full leading-6">
|
<span className="text-sm w-full leading-6">
|
||||||
{prompt.split("\\n").map((line, index) => (
|
{prompt.split("\\n").map((line, index) => (
|
||||||
@@ -150,6 +167,6 @@ export default function MatchSentences({id, options, type, prompt, sentences, us
|
|||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,8 +151,29 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-4 mt-2 mb-20">
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={back}
|
||||||
|
className="max-w-[200px] w-full"
|
||||||
|
disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button color="purple" onClick={next} className="max-w-[200px] self-end w-full">
|
||||||
|
{exam &&
|
||||||
|
exam.module === "level" &&
|
||||||
|
partIndex === exam.parts.length - 1 &&
|
||||||
|
exerciseIndex === exam.parts[partIndex].exercises.length - 1 &&
|
||||||
|
questionIndex + 1 >= questions.length - 1
|
||||||
|
? "Submit"
|
||||||
|
: "Next"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 mt-4 mb-20">
|
||||||
<div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
|
<div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
|
||||||
{/*<span className="text-xl font-semibold mb-2">{"Select the appropriate option."}</span>*/}
|
{/*<span className="text-xl font-semibold mb-2">{"Select the appropriate option."}</span>*/}
|
||||||
{questionIndex < questions.length && (
|
{questionIndex < questions.length && (
|
||||||
@@ -195,6 +216,6 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio
|
|||||||
: "Next"}
|
: "Next"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { SpeakingExercise } from "@/interfaces/exam";
|
import {SpeakingExercise} from "@/interfaces/exam";
|
||||||
import { CommonProps } from ".";
|
import {CommonProps} from ".";
|
||||||
import { Fragment, useEffect, useState } from "react";
|
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 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";
|
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, 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 [recordingDuration, setRecordingDuration] = useState(0);
|
||||||
const [isRecording, setIsRecording] = useState(false);
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
const [mediaBlob, setMediaBlob] = useState<string>();
|
const [mediaBlob, setMediaBlob] = useState<string>();
|
||||||
@@ -28,7 +28,7 @@ export default function Speaking({ id, title, text, video_url, type, prompts, su
|
|||||||
const saveToStorage = async () => {
|
const saveToStorage = async () => {
|
||||||
if (mediaBlob && mediaBlob.startsWith("blob")) {
|
if (mediaBlob && mediaBlob.startsWith("blob")) {
|
||||||
const blobBuffer = await downloadBlob(mediaBlob);
|
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.", "");
|
const seed = Math.random().toString().replace("0.", "");
|
||||||
|
|
||||||
@@ -42,8 +42,8 @@ export default function Speaking({ id, title, text, video_url, type, prompts, su
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await axios.post<{ path: string }>("/api/storage/insert", formData, config);
|
const response = await axios.post<{path: string}>("/api/storage/insert", formData, config);
|
||||||
if (audioURL) await axios.post("/api/storage/delete", { path: audioURL });
|
if (audioURL) await axios.post("/api/storage/delete", {path: audioURL});
|
||||||
return response.data.path;
|
return response.data.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ export default function Speaking({ id, title, text, video_url, type, prompts, su
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userSolutions.length > 0) {
|
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 && !mediaBlob) setMediaBlob(solution);
|
||||||
if (solution && !solution.startsWith("blob")) setAudioURL(solution);
|
if (solution && !solution.startsWith("blob")) setAudioURL(solution);
|
||||||
}
|
}
|
||||||
@@ -79,8 +79,8 @@ export default function Speaking({ id, title, text, video_url, type, prompts, su
|
|||||||
const next = async () => {
|
const next = async () => {
|
||||||
onNext({
|
onNext({
|
||||||
exercise: id,
|
exercise: id,
|
||||||
solutions: mediaBlob ? [{ id, solution: mediaBlob }] : [],
|
solutions: mediaBlob ? [{id, solution: mediaBlob}] : [],
|
||||||
score: { correct: 0, total: 100, missing: 0 },
|
score: {correct: 0, total: 100, missing: 0},
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -88,8 +88,8 @@ export default function Speaking({ id, title, text, video_url, type, prompts, su
|
|||||||
const back = async () => {
|
const back = async () => {
|
||||||
onBack({
|
onBack({
|
||||||
exercise: id,
|
exercise: id,
|
||||||
solutions: mediaBlob ? [{ id, solution: mediaBlob }] : [],
|
solutions: mediaBlob ? [{id, solution: mediaBlob}] : [],
|
||||||
score: { correct: 0, total: 100, missing: 0 },
|
score: {correct: 0, total: 100, missing: 0},
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -116,6 +116,16 @@ export default function Speaking({ id, title, text, video_url, type, prompts, su
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="flex flex-col gap-4 mt-4 w-full">
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button color="purple" variant="outline" isLoading={isLoading} onClick={back} className="max-w-[200px] self-end w-full">
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button color="purple" isLoading={isLoading} disabled={!mediaBlob} onClick={next} className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col h-full w-full gap-9">
|
<div className="flex flex-col h-full w-full gap-9">
|
||||||
<Modal title="Prompts" className="!w-96 aspect-square" isOpen={isPromptsModalOpen} onClose={() => setIsPromptsModalOpen(false)}>
|
<Modal title="Prompts" 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 items-center justify-center gap-4 w-full h-full">
|
||||||
@@ -134,7 +144,9 @@ export default function Speaking({ id, title, text, video_url, type, prompts, su
|
|||||||
<div className="flex flex-col gap-0">
|
<div className="flex flex-col gap-0">
|
||||||
<span className="font-semibold">{title}</span>
|
<span className="font-semibold">{title}</span>
|
||||||
{prompts.length > 0 && (
|
{prompts.length > 0 && (
|
||||||
<span className="font-semibold">You should talk for at least 1 minute and 30 seconds for your answer to be valid.</span>
|
<span className="font-semibold">
|
||||||
|
You should talk for at least 1 minute and 30 seconds for your answer to be valid.
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!video_url && (
|
{!video_url && (
|
||||||
@@ -177,7 +189,7 @@ export default function Speaking({ id, title, text, video_url, type, prompts, su
|
|||||||
<ReactMediaRecorder
|
<ReactMediaRecorder
|
||||||
audio
|
audio
|
||||||
onStop={(blob) => setMediaBlob(blob)}
|
onStop={(blob) => setMediaBlob(blob)}
|
||||||
render={({ status, startRecording, stopRecording, pauseRecording, resumeRecording, clearBlobUrl, mediaBlobUrl }) => (
|
render={({status, startRecording, stopRecording, pauseRecording, resumeRecording, clearBlobUrl, mediaBlobUrl}) => (
|
||||||
<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="w-full p-4 px-8 bg-transparent border-2 border-mti-gray-platinum rounded-2xl flex-col gap-8 items-center">
|
||||||
<p className="text-base font-normal">Record your answer:</p>
|
<p className="text-base font-normal">Record your answer:</p>
|
||||||
<div className="flex gap-8 items-center justify-center py-8">
|
<div className="flex gap-8 items-center justify-center py-8">
|
||||||
@@ -300,5 +312,6 @@ export default function Speaking({ id, title, text, video_url, type, prompts, su
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,10 +30,9 @@ export default function TrueFalse({id, type, prompt, questions, userSolutions, o
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type});
|
setCurrentSolution({exercise: id, solutions: answers, score: calculateScore(), type});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers, setAnswers])
|
}, [answers, setAnswers]);
|
||||||
|
|
||||||
|
|
||||||
const toggleAnswer = (solution: "true" | "false" | "not_given", questionId: string) => {
|
const toggleAnswer = (solution: "true" | "false" | "not_given", questionId: string) => {
|
||||||
const answer = answers.find((x) => x.id === questionId);
|
const answer = answers.find((x) => x.id === questionId);
|
||||||
@@ -46,7 +45,24 @@ export default function TrueFalse({id, type, prompt, questions, userSolutions, o
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col gap-4 mt-4">
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type})}
|
||||||
|
className="max-w-[200px] w-full">
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={() => onNext({exercise: id, solutions: answers, score: calculateScore(), type})}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
||||||
<span className="text-sm w-full leading-6">
|
<span className="text-sm w-full leading-6">
|
||||||
{prompt.split("\\n").map((line, index) => (
|
{prompt.split("\\n").map((line, index) => (
|
||||||
@@ -123,6 +139,6 @@ export default function TrueFalse({id, type, prompt, questions, userSolutions, o
|
|||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,9 +71,9 @@ export default function WriteBlanks({id, prompt, type, maxWords, solutions, user
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type });
|
setCurrentSolution({exercise: id, solutions: answers, score: calculateScore(), type});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers, setAnswers])
|
}, [answers, setAnswers]);
|
||||||
|
|
||||||
const renderLines = (line: string) => {
|
const renderLines = (line: string) => {
|
||||||
return (
|
return (
|
||||||
@@ -92,7 +92,24 @@ export default function WriteBlanks({id, prompt, type, maxWords, solutions, user
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type})}
|
||||||
|
className="max-w-[200px] w-full">
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={() => onNext({exercise: id, solutions: answers, score: calculateScore(), type})}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
||||||
<span className="text-sm w-full leading-6">
|
<span className="text-sm w-full leading-6">
|
||||||
{prompt.split("\\n").map((line, index) => (
|
{prompt.split("\\n").map((line, index) => (
|
||||||
@@ -128,6 +145,6 @@ export default function WriteBlanks({id, prompt, type, maxWords, solutions, user
|
|||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,34 @@ export default function Writing({
|
|||||||
}, [inputText, wordCounter]);
|
}, [inputText, wordCounter]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col gap-4 mt-4">
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
onBack({exercise: id, solutions: [{id, solution: inputText}], score: {correct: 100, total: 100, missing: 0}, type})
|
||||||
|
}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
disabled={!isSubmitEnabled}
|
||||||
|
onClick={() =>
|
||||||
|
onNext({
|
||||||
|
exercise: id,
|
||||||
|
solutions: [{id, solution: inputText.replaceAll(/\s{2,}/g, " ")}],
|
||||||
|
score: {correct: 100, total: 100, missing: 0},
|
||||||
|
type,
|
||||||
|
module: "writing",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{attachment && (
|
{attachment && (
|
||||||
<Transition show={isModalOpen} as={Fragment}>
|
<Transition show={isModalOpen} as={Fragment}>
|
||||||
<Dialog onClose={() => setIsModalOpen(false)} className="relative z-50">
|
<Dialog onClose={() => setIsModalOpen(false)} className="relative z-50">
|
||||||
@@ -170,6 +197,6 @@ export default function Writing({
|
|||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,16 @@
|
|||||||
import { FillBlanksExercise, FillBlanksMCOption, ShuffleMap } from "@/interfaces/exam";
|
import {FillBlanksExercise, FillBlanksMCOption, ShuffleMap} from "@/interfaces/exam";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import { CommonProps } from ".";
|
import {CommonProps} from ".";
|
||||||
import { Fragment } from "react";
|
import {Fragment} from "react";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
|
|
||||||
export default function FillBlanksSolutions({
|
export default function FillBlanksSolutions({id, type, prompt, solutions, words, text, onNext, onBack}: FillBlanksExercise & CommonProps) {
|
||||||
id,
|
|
||||||
type,
|
|
||||||
prompt,
|
|
||||||
solutions,
|
|
||||||
words,
|
|
||||||
text,
|
|
||||||
onNext,
|
|
||||||
onBack,
|
|
||||||
}: FillBlanksExercise & CommonProps) {
|
|
||||||
const storeUserSolutions = useExamStore((state) => state.userSolutions);
|
const storeUserSolutions = useExamStore((state) => state.userSolutions);
|
||||||
const {questionIndex, setQuestionIndex, partIndex, exam} = useExamStore((state) => state);
|
const {questionIndex, setQuestionIndex, partIndex, exam} = useExamStore((state) => state);
|
||||||
|
|
||||||
const correctUserSolutions = storeUserSolutions.find(
|
const correctUserSolutions = storeUserSolutions.find((solution) => solution.exercise === id)?.solutions;
|
||||||
(solution) => solution.exercise === id
|
|
||||||
)?.solutions;
|
|
||||||
|
|
||||||
const shuffles = useExamStore((state) => state.shuffles);
|
const shuffles = useExamStore((state) => state.shuffles);
|
||||||
|
|
||||||
@@ -34,7 +23,7 @@ export default function FillBlanksSolutions({
|
|||||||
const option = words.find((w) => {
|
const option = words.find((w) => {
|
||||||
if (typeof w === "string") {
|
if (typeof w === "string") {
|
||||||
return w.toLowerCase() === x.solution.toLowerCase();
|
return w.toLowerCase() === x.solution.toLowerCase();
|
||||||
} else if ('letter' in w) {
|
} else if ("letter" in w) {
|
||||||
return w.letter.toLowerCase() === x.solution.toLowerCase();
|
return w.letter.toLowerCase() === x.solution.toLowerCase();
|
||||||
} else {
|
} else {
|
||||||
return w.id.toString() === x.id.toString();
|
return w.id.toString() === x.id.toString();
|
||||||
@@ -44,23 +33,20 @@ export default function FillBlanksSolutions({
|
|||||||
|
|
||||||
if (typeof option === "string") {
|
if (typeof option === "string") {
|
||||||
return solution.toLowerCase() === option.toLowerCase();
|
return solution.toLowerCase() === option.toLowerCase();
|
||||||
} else if ('letter' in option) {
|
} else if ("letter" in option) {
|
||||||
return solution.toLowerCase() === option.word.toLowerCase();
|
return solution.toLowerCase() === option.word.toLowerCase();
|
||||||
} else if ('options' in option) {
|
} else if ("options" in option) {
|
||||||
return option.options[solution as keyof typeof option.options] == x.solution;
|
return option.options[solution as keyof typeof option.options] == x.solution;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}).length;
|
}).length;
|
||||||
const missing = total - correctUserSolutions!.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
const missing = total - correctUserSolutions!.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
||||||
return { total, correct, missing };
|
return {total, correct, missing};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
|
const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
|
||||||
return Array.isArray(words) && words.every(
|
return Array.isArray(words) && words.every((word) => word && typeof word === "object" && "id" in word && "options" in word);
|
||||||
word => word && typeof word === 'object' && 'id' in word && 'options' in word
|
};
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderLines = (line: string) => {
|
const renderLines = (line: string) => {
|
||||||
return (
|
return (
|
||||||
@@ -68,17 +54,17 @@ export default function FillBlanksSolutions({
|
|||||||
{reactStringReplace(line, /({{\d+}})/g, (match) => {
|
{reactStringReplace(line, /({{\d+}})/g, (match) => {
|
||||||
const questionId = match.replaceAll(/[\{\}]/g, "");
|
const questionId = match.replaceAll(/[\{\}]/g, "");
|
||||||
const userSolution = correctUserSolutions!.find((x) => x.id.toString() === questionId.toString());
|
const userSolution = correctUserSolutions!.find((x) => x.id.toString() === questionId.toString());
|
||||||
const answerSolution = solutions.find(sol => sol.id.toString() === questionId.toString())!.solution;
|
const answerSolution = solutions.find((sol) => sol.id.toString() === questionId.toString())!.solution;
|
||||||
const questionShuffleMap = shuffles.find((x) => x.exerciseID == id)?.shuffles.find((y) => y.questionID == questionId);
|
const questionShuffleMap = shuffles.find((x) => x.exerciseID == id)?.shuffles.find((y) => y.questionID == questionId);
|
||||||
const newAnswerSolution = questionShuffleMap ? questionShuffleMap.map[answerSolution].toLowerCase() : answerSolution.toLowerCase();
|
const newAnswerSolution = questionShuffleMap
|
||||||
|
? questionShuffleMap.map[answerSolution].toLowerCase()
|
||||||
|
: answerSolution.toLowerCase();
|
||||||
|
|
||||||
if (!userSolution) {
|
if (!userSolution) {
|
||||||
let answerText;
|
let answerText;
|
||||||
if (typeCheckWordsMC(words)) {
|
if (typeCheckWordsMC(words)) {
|
||||||
const options = words.find((x) => x.id.toString() === questionId.toString());
|
const options = words.find((x) => x.id.toString() === questionId.toString());
|
||||||
const correctKey = Object.keys(options!.options).find(key =>
|
const correctKey = Object.keys(options!.options).find((key) => key.toLowerCase() === newAnswerSolution);
|
||||||
key.toLowerCase() === newAnswerSolution
|
|
||||||
);
|
|
||||||
answerText = options!.options[correctKey as keyof typeof options];
|
answerText = options!.options[correctKey as keyof typeof options];
|
||||||
} else {
|
} else {
|
||||||
answerText = answerSolution;
|
answerText = answerSolution;
|
||||||
@@ -97,19 +83,19 @@ export default function FillBlanksSolutions({
|
|||||||
const userSolutionWord = words.find((w) =>
|
const userSolutionWord = words.find((w) =>
|
||||||
typeof w === "string"
|
typeof w === "string"
|
||||||
? w.toLowerCase() === userSolution.solution.toLowerCase()
|
? w.toLowerCase() === userSolution.solution.toLowerCase()
|
||||||
: 'letter' in w
|
: "letter" in w
|
||||||
? w.letter.toLowerCase() === userSolution.solution.toLowerCase()
|
? w.letter.toLowerCase() === userSolution.solution.toLowerCase()
|
||||||
: 'options' in w
|
: "options" in w
|
||||||
? w.id === userSolution.questionId
|
? w.id === userSolution.questionId
|
||||||
: false
|
: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
const userSolutionText =
|
const userSolutionText =
|
||||||
typeof userSolutionWord === "string"
|
typeof userSolutionWord === "string"
|
||||||
? userSolutionWord
|
? userSolutionWord
|
||||||
: userSolutionWord && 'letter' in userSolutionWord
|
: userSolutionWord && "letter" in userSolutionWord
|
||||||
? userSolutionWord.word
|
? userSolutionWord.word
|
||||||
: userSolutionWord && 'options' in userSolutionWord
|
: userSolutionWord && "options" in userSolutionWord
|
||||||
? userSolution.solution
|
? userSolution.solution
|
||||||
: userSolution.solution;
|
: userSolution.solution;
|
||||||
|
|
||||||
@@ -118,16 +104,13 @@ export default function FillBlanksSolutions({
|
|||||||
if (typeCheckWordsMC(words)) {
|
if (typeCheckWordsMC(words)) {
|
||||||
const options = words.find((x) => x.id.toString() === questionId.toString());
|
const options = words.find((x) => x.id.toString() === questionId.toString());
|
||||||
if (options) {
|
if (options) {
|
||||||
const correctKey = Object.keys(options.options).find(key =>
|
const correctKey = Object.keys(options.options).find((key) => key.toLowerCase() === newAnswerSolution);
|
||||||
key.toLowerCase() === newAnswerSolution
|
|
||||||
);
|
|
||||||
correct = userSolution.solution == options.options[correctKey as keyof typeof options.options];
|
correct = userSolution.solution == options.options[correctKey as keyof typeof options.options];
|
||||||
solutionText = options.options[correctKey as keyof typeof options.options] || answerSolution;
|
solutionText = options.options[correctKey as keyof typeof options.options] || answerSolution;
|
||||||
} else {
|
} else {
|
||||||
correct = false;
|
correct = false;
|
||||||
solutionText = answerSolution;
|
solutionText = answerSolution;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
correct = userSolutionText === answerSolution;
|
correct = userSolutionText === answerSolution;
|
||||||
solutionText = answerSolution;
|
solutionText = answerSolution;
|
||||||
@@ -170,7 +153,25 @@ export default function FillBlanksSolutions({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onBack({exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type})}
|
||||||
|
className="max-w-[200px] w-full"
|
||||||
|
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={() => onNext({exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type})}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
||||||
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">
|
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">
|
||||||
{correctUserSolutions &&
|
{correctUserSolutions &&
|
||||||
@@ -201,25 +202,19 @@ export default function FillBlanksSolutions({
|
|||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onBack({ exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type })}
|
onClick={() => onBack({exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type})}
|
||||||
className="max-w-[200px] w-full"
|
className="max-w-[200px] w-full"
|
||||||
disabled={
|
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
||||||
exam &&
|
|
||||||
typeof partIndex !== "undefined" &&
|
|
||||||
exam.module === "level" &&
|
|
||||||
questionIndex === 0 &&
|
|
||||||
partIndex === 0
|
|
||||||
}>
|
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
onClick={() => onNext({ exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type })}
|
onClick={() => onNext({exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type})}
|
||||||
className="max-w-[200px] self-end w-full">
|
className="max-w-[200px] self-end w-full">
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import { InteractiveSpeakingExercise } from "@/interfaces/exam";
|
import {InteractiveSpeakingExercise} from "@/interfaces/exam";
|
||||||
import { CommonProps } from ".";
|
import {CommonProps} from ".";
|
||||||
import { useEffect, useState } from "react";
|
import {useEffect, useState} from "react";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { speakingReverseMarking } from "@/utils/score";
|
import {speakingReverseMarking} from "@/utils/score";
|
||||||
import { Tab } from "@headlessui/react";
|
import {Tab} from "@headlessui/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import Modal from "../Modal";
|
import Modal from "../Modal";
|
||||||
import ReactDiffViewer, { DiffMethod } from "react-diff-viewer";
|
import ReactDiffViewer, {DiffMethod} from "react-diff-viewer";
|
||||||
|
|
||||||
const Waveform = dynamic(() => import("../Waveform"), { ssr: false });
|
const Waveform = dynamic(() => import("../Waveform"), {ssr: false});
|
||||||
|
|
||||||
export default function InteractiveSpeaking({
|
export default function InteractiveSpeaking({
|
||||||
id,
|
id,
|
||||||
@@ -26,20 +26,24 @@ export default function InteractiveSpeaking({
|
|||||||
const [solutionsURL, setSolutionsURL] = useState<string[]>([]);
|
const [solutionsURL, setSolutionsURL] = useState<string[]>([]);
|
||||||
const [diffNumber, setDiffNumber] = useState(0);
|
const [diffNumber, setDiffNumber] = useState(0);
|
||||||
|
|
||||||
const tooltips: { [key: string]: string } = {
|
const tooltips: {[key: string]: string} = {
|
||||||
"Grammatical Range and Accuracy": "Assesses the variety and correctness of grammatical structures used. A higher score indicates a wide range of complex and accurate grammar; a lower score suggests the need for more basic grammar practice.",
|
"Grammatical Range and Accuracy":
|
||||||
"Fluency and Coherence": "Evaluates smoothness and logical flow of speech. A higher score means natural, effortless speech and clear idea progression; a lower score indicates frequent pauses and difficulty in maintaining coherence.",
|
"Assesses the variety and correctness of grammatical structures used. A higher score indicates a wide range of complex and accurate grammar; a lower score suggests the need for more basic grammar practice.",
|
||||||
"Pronunciation": "Measures clarity and accuracy of spoken words. A higher score reflects clear, well-articulated speech with correct intonation; a lower score shows challenges in being understood.",
|
"Fluency and Coherence":
|
||||||
"Lexical Resource": "Looks at the range and appropriateness of vocabulary. A higher score demonstrates a rich and precise vocabulary; a lower score suggests limited vocabulary usage and appropriateness.",
|
"Evaluates smoothness and logical flow of speech. A higher score means natural, effortless speech and clear idea progression; a lower score indicates frequent pauses and difficulty in maintaining coherence.",
|
||||||
|
Pronunciation:
|
||||||
|
"Measures clarity and accuracy of spoken words. A higher score reflects clear, well-articulated speech with correct intonation; a lower score shows challenges in being understood.",
|
||||||
|
"Lexical Resource":
|
||||||
|
"Looks at the range and appropriateness of vocabulary. A higher score demonstrates a rich and precise vocabulary; a lower score suggests limited vocabulary usage and appropriateness.",
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userSolutions && userSolutions.length > 0 && userSolutions[0].solution) {
|
if (userSolutions && userSolutions.length > 0 && userSolutions[0].solution) {
|
||||||
Promise.all(userSolutions[0].solution.map((x) => axios.post(`/api/speaking`, { path: x.answer }, { responseType: "arraybuffer" }))).then(
|
Promise.all(userSolutions[0].solution.map((x) => axios.post(`/api/speaking`, {path: x.answer}, {responseType: "arraybuffer"}))).then(
|
||||||
(values) => {
|
(values) => {
|
||||||
setSolutionsURL(
|
setSolutionsURL(
|
||||||
values.map(({ data }) => {
|
values.map(({data}) => {
|
||||||
const blob = new Blob([data], { type: "audio/wav" });
|
const blob = new Blob([data], {type: "audio/wav"});
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
@@ -51,7 +55,41 @@ export default function InteractiveSpeaking({
|
|||||||
}, [userSolutions]);
|
}, [userSolutions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col gap-4 mt-4 w-full">
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
onBack({
|
||||||
|
exercise: id,
|
||||||
|
solutions: userSolutions,
|
||||||
|
score: {total: 100, missing: 0, correct: speakingReverseMarking[userSolutions[0]!.evaluation!.overall] || 0},
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={() =>
|
||||||
|
onNext({
|
||||||
|
exercise: id,
|
||||||
|
solutions: userSolutions,
|
||||||
|
score: {
|
||||||
|
total: 100,
|
||||||
|
missing: 0,
|
||||||
|
correct: userSolutions[0]?.evaluation ? speakingReverseMarking[userSolutions[0]!.evaluation!.overall] || 0 : 0,
|
||||||
|
},
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Modal title={`Correction (Prompt ${diffNumber})`} isOpen={diffNumber !== 0} onClose={() => setDiffNumber(0)}>
|
<Modal title={`Correction (Prompt ${diffNumber})`} isOpen={diffNumber !== 0} onClose={() => setDiffNumber(0)}>
|
||||||
<>
|
<>
|
||||||
{userSolutions &&
|
{userSolutions &&
|
||||||
@@ -71,13 +109,13 @@ export default function InteractiveSpeaking({
|
|||||||
fontFamily: '"Open Sans", system-ui, -apple-system, "Helvetica Neue", sans-serif',
|
fontFamily: '"Open Sans", system-ui, -apple-system, "Helvetica Neue", sans-serif',
|
||||||
padding: "32px 28px",
|
padding: "32px 28px",
|
||||||
},
|
},
|
||||||
marker: { display: "none" },
|
marker: {display: "none"},
|
||||||
diffRemoved: { padding: "32px 28px" },
|
diffRemoved: {padding: "32px 28px"},
|
||||||
diffAdded: { padding: "32px 28px" },
|
diffAdded: {padding: "32px 28px"},
|
||||||
|
|
||||||
wordRemoved: { padding: "0px", display: "initial" },
|
wordRemoved: {padding: "0px", display: "initial"},
|
||||||
wordAdded: { padding: "0px", display: "initial" },
|
wordAdded: {padding: "0px", display: "initial"},
|
||||||
wordDiff: { padding: "0px", display: "initial" },
|
wordDiff: {padding: "0px", display: "initial"},
|
||||||
}}
|
}}
|
||||||
oldValue={userSolutions[0].evaluation[`transcript_${diffNumber}`]?.replaceAll("\\n", "\n")}
|
oldValue={userSolutions[0].evaluation[`transcript_${diffNumber}`]?.replaceAll("\\n", "\n")}
|
||||||
newValue={userSolutions[0].evaluation[`fixed_text_${diffNumber}`]?.replaceAll("\\n", "\n")}
|
newValue={userSolutions[0].evaluation[`fixed_text_${diffNumber}`]?.replaceAll("\\n", "\n")}
|
||||||
@@ -122,13 +160,13 @@ export default function InteractiveSpeaking({
|
|||||||
{userSolutions &&
|
{userSolutions &&
|
||||||
userSolutions.length > 0 &&
|
userSolutions.length > 0 &&
|
||||||
userSolutions[0].evaluation &&
|
userSolutions[0].evaluation &&
|
||||||
userSolutions[0].evaluation[`transcript_${(index + 1)}`] &&
|
userSolutions[0].evaluation[`transcript_${index + 1}`] &&
|
||||||
userSolutions[0].evaluation[`fixed_text_${(index + 1)}`] && (
|
userSolutions[0].evaluation[`fixed_text_${index + 1}`] && (
|
||||||
<Button
|
<Button
|
||||||
className="w-full max-w-[180px] !py-2 self-center"
|
className="w-full max-w-[180px] !py-2 self-center"
|
||||||
color="pink"
|
color="pink"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setDiffNumber((index + 1))}>
|
onClick={() => setDiffNumber(index + 1)}>
|
||||||
View Correction
|
View Correction
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -144,9 +182,13 @@ export default function InteractiveSpeaking({
|
|||||||
const grade: number = typeof taskResponse === "number" ? taskResponse : taskResponse.grade;
|
const grade: number = typeof taskResponse === "number" ? taskResponse : taskResponse.grade;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx("bg-ielts-speaking text-ielts-speaking-light rounded-xl px-4 py-2 tooltip tooltip-bottom",
|
<div
|
||||||
index === 0 && "tooltip-right"
|
className={clsx(
|
||||||
)} key={key} data-tip={tooltips[key] || "No additional information available"}>
|
"bg-ielts-speaking text-ielts-speaking-light rounded-xl px-4 py-2 tooltip tooltip-bottom",
|
||||||
|
index === 0 && "tooltip-right",
|
||||||
|
)}
|
||||||
|
key={key}
|
||||||
|
data-tip={tooltips[key] || "No additional information available"}>
|
||||||
{key}: Level {grade}
|
{key}: Level {grade}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -157,7 +199,7 @@ export default function InteractiveSpeaking({
|
|||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-speaking/20 p-1">
|
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-speaking/20 p-1">
|
||||||
<Tab
|
<Tab
|
||||||
className={({ selected }) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
||||||
@@ -168,7 +210,7 @@ export default function InteractiveSpeaking({
|
|||||||
General Feedback
|
General Feedback
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
className={({ selected }) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
||||||
@@ -178,18 +220,24 @@ export default function InteractiveSpeaking({
|
|||||||
}>
|
}>
|
||||||
Evaluation
|
Evaluation
|
||||||
</Tab>
|
</Tab>
|
||||||
{Object.keys(userSolutions[0].evaluation).filter((x) => x.startsWith("perfect_answer")).map((key, index) => (
|
{Object.keys(userSolutions[0].evaluation)
|
||||||
|
.filter((x) => x.startsWith("perfect_answer"))
|
||||||
|
.map((key, index) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={key}
|
key={key}
|
||||||
className={({ selected }) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
selected
|
||||||
|
? "bg-white shadow"
|
||||||
|
: "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
Recommended Answer<br />(Prompt {index + 1})
|
Recommended Answer
|
||||||
|
<br />
|
||||||
|
(Prompt {index + 1})
|
||||||
</Tab>
|
</Tab>
|
||||||
))}
|
))}
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
@@ -202,10 +250,16 @@ export default function InteractiveSpeaking({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key} className="flex flex-col gap-2">
|
<div key={key} className="flex flex-col gap-2">
|
||||||
<div className={clsx("bg-ielts-speaking text-ielts-speaking-light rounded-xl px-4 py-2 w-fit")} key={key}>
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"bg-ielts-speaking text-ielts-speaking-light rounded-xl px-4 py-2 w-fit",
|
||||||
|
)}
|
||||||
|
key={key}>
|
||||||
{key}: Level {grade}
|
{key}: Level {grade}
|
||||||
</div>
|
</div>
|
||||||
{typeof taskResponse !== "number" && <span className="px-2 py-2">{taskResponse.comment}</span>}
|
{typeof taskResponse !== "number" && (
|
||||||
|
<span className="px-2 py-2">{taskResponse.comment}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -214,10 +268,15 @@ export default function InteractiveSpeaking({
|
|||||||
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
<span className="w-full h-full min-h-fit cursor-text">{userSolutions[0].evaluation!.comment}</span>
|
<span className="w-full h-full min-h-fit cursor-text">{userSolutions[0].evaluation!.comment}</span>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
{Object.keys(userSolutions[0].evaluation).filter((x) => x.startsWith("perfect_answer")).map((key, index) => (
|
{Object.keys(userSolutions[0].evaluation)
|
||||||
|
.filter((x) => x.startsWith("perfect_answer"))
|
||||||
|
.map((key, index) => (
|
||||||
<Tab.Panel key={key} className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
<Tab.Panel key={key} className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
<span className="w-full h-full min-h-fit cursor-text whitespace-pre-wrap">
|
<span className="w-full h-full min-h-fit cursor-text whitespace-pre-wrap">
|
||||||
{userSolutions[0].evaluation![`perfect_answer_${(index + 1)}`].answer.replaceAll(/\s{2,}/g, "\n\n")}
|
{userSolutions[0].evaluation![`perfect_answer_${index + 1}`].answer.replaceAll(
|
||||||
|
/\s{2,}/g,
|
||||||
|
"\n\n",
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
))}
|
))}
|
||||||
@@ -241,7 +300,7 @@ export default function InteractiveSpeaking({
|
|||||||
onBack({
|
onBack({
|
||||||
exercise: id,
|
exercise: id,
|
||||||
solutions: userSolutions,
|
solutions: userSolutions,
|
||||||
score: { total: 100, missing: 0, correct: speakingReverseMarking[userSolutions[0]!.evaluation!.overall] || 0 },
|
score: {total: 100, missing: 0, correct: speakingReverseMarking[userSolutions[0]!.evaluation!.overall] || 0},
|
||||||
type,
|
type,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -266,6 +325,6 @@ export default function InteractiveSpeaking({
|
|||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,25 @@ export default function MatchSentencesSolutions({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col gap-4 mt-4">
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||||
|
className="max-w-[200px] w-full"
|
||||||
|
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={() => onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
||||||
<span className="text-sm w-full leading-6">
|
<span className="text-sm w-full leading-6">
|
||||||
{prompt.split("\\n").map((line, index) => (
|
{prompt.split("\\n").map((line, index) => (
|
||||||
@@ -116,13 +134,7 @@ export default function MatchSentencesSolutions({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||||
className="max-w-[200px] w-full"
|
className="max-w-[200px] w-full"
|
||||||
disabled={
|
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
||||||
exam &&
|
|
||||||
typeof partIndex !== "undefined" &&
|
|
||||||
exam.module === "level" &&
|
|
||||||
questionIndex === 0 &&
|
|
||||||
partIndex === 0
|
|
||||||
}>
|
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -133,6 +145,6 @@ export default function MatchSentencesSolutions({
|
|||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,8 +127,23 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-4 w-full h-full mb-20">
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={back}
|
||||||
|
className="max-w-[200px] w-full"
|
||||||
|
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button color="purple" onClick={next} className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 w-full h-full mb-20 mt-4">
|
||||||
<div className="flex flex-col gap-4 mt-2">
|
<div className="flex flex-col gap-4 mt-2">
|
||||||
<div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
|
<div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
|
||||||
{/*<span className="text-xl font-semibold">{prompt}</span>*/}
|
{/*<span className="text-xl font-semibold">{prompt}</span>*/}
|
||||||
@@ -180,6 +195,6 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
|
|||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import { SpeakingExercise } from "@/interfaces/exam";
|
import {SpeakingExercise} from "@/interfaces/exam";
|
||||||
import { CommonProps } from ".";
|
import {CommonProps} from ".";
|
||||||
import { Fragment, useEffect, useState } from "react";
|
import {Fragment, useEffect, useState} from "react";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { speakingReverseMarking } from "@/utils/score";
|
import {speakingReverseMarking} from "@/utils/score";
|
||||||
import { Tab } from "@headlessui/react";
|
import {Tab} from "@headlessui/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import Modal from "../Modal";
|
import Modal from "../Modal";
|
||||||
import { BsQuestionCircleFill } from "react-icons/bs";
|
import {BsQuestionCircleFill} from "react-icons/bs";
|
||||||
import ReactDiffViewer, { DiffMethod } from "react-diff-viewer";
|
import ReactDiffViewer, {DiffMethod} from "react-diff-viewer";
|
||||||
|
|
||||||
const Waveform = dynamic(() => import("../Waveform"), { ssr: false });
|
const Waveform = dynamic(() => import("../Waveform"), {ssr: false});
|
||||||
|
|
||||||
export default function Speaking({ id, type, title, video_url, text, prompts, userSolutions, onNext, onBack }: SpeakingExercise & CommonProps) {
|
export default function Speaking({id, type, title, video_url, text, prompts, userSolutions, onNext, onBack}: SpeakingExercise & CommonProps) {
|
||||||
const [solutionURL, setSolutionURL] = useState<string>();
|
const [solutionURL, setSolutionURL] = useState<string>();
|
||||||
const [showDiff, setShowDiff] = useState(false);
|
const [showDiff, setShowDiff] = useState(false);
|
||||||
|
|
||||||
@@ -23,8 +23,8 @@ export default function Speaking({ id, type, title, video_url, text, prompts, us
|
|||||||
const solution = userSolutions[0].solution;
|
const solution = userSolutions[0].solution;
|
||||||
|
|
||||||
if (solution.startsWith("https://")) return setSolutionURL(solution);
|
if (solution.startsWith("https://")) return setSolutionURL(solution);
|
||||||
axios.post(`/api/speaking`, { path: userSolutions[0].solution }, { responseType: "arraybuffer" }).then(({ data }) => {
|
axios.post(`/api/speaking`, {path: userSolutions[0].solution}, {responseType: "arraybuffer"}).then(({data}) => {
|
||||||
const blob = new Blob([data], { type: "audio/wav" });
|
const blob = new Blob([data], {type: "audio/wav"});
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
setSolutionURL(url);
|
setSolutionURL(url);
|
||||||
@@ -32,15 +32,53 @@ export default function Speaking({ id, type, title, video_url, text, prompts, us
|
|||||||
}
|
}
|
||||||
}, [userSolutions]);
|
}, [userSolutions]);
|
||||||
|
|
||||||
const tooltips: { [key: string]: string } = {
|
const tooltips: {[key: string]: string} = {
|
||||||
"Grammatical Range and Accuracy": "Assesses the variety and correctness of grammatical structures used. A higher score indicates a wide range of complex and accurate grammar; a lower score suggests the need for more basic grammar practice.",
|
"Grammatical Range and Accuracy":
|
||||||
"Fluency and Coherence": "Evaluates smoothness and logical flow of speech. A higher score means natural, effortless speech and clear idea progression; a lower score indicates frequent pauses and difficulty in maintaining coherence.",
|
"Assesses the variety and correctness of grammatical structures used. A higher score indicates a wide range of complex and accurate grammar; a lower score suggests the need for more basic grammar practice.",
|
||||||
"Pronunciation": "Measures clarity and accuracy of spoken words. A higher score reflects clear, well-articulated speech with correct intonation; a lower score shows challenges in being understood.",
|
"Fluency and Coherence":
|
||||||
"Lexical Resource": "Looks at the range and appropriateness of vocabulary. A higher score demonstrates a rich and precise vocabulary; a lower score suggests limited vocabulary usage and appropriateness.",
|
"Evaluates smoothness and logical flow of speech. A higher score means natural, effortless speech and clear idea progression; a lower score indicates frequent pauses and difficulty in maintaining coherence.",
|
||||||
|
Pronunciation:
|
||||||
|
"Measures clarity and accuracy of spoken words. A higher score reflects clear, well-articulated speech with correct intonation; a lower score shows challenges in being understood.",
|
||||||
|
"Lexical Resource":
|
||||||
|
"Looks at the range and appropriateness of vocabulary. A higher score demonstrates a rich and precise vocabulary; a lower score suggests limited vocabulary usage and appropriateness.",
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col gap-4 mt-4 w-full">
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
onBack({
|
||||||
|
exercise: id,
|
||||||
|
solutions: userSolutions,
|
||||||
|
score: {total: 100, missing: 0, correct: speakingReverseMarking[userSolutions[0]!.evaluation!.overall] || 0},
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={() =>
|
||||||
|
onNext({
|
||||||
|
exercise: id,
|
||||||
|
solutions: userSolutions,
|
||||||
|
score: {
|
||||||
|
total: 100,
|
||||||
|
missing: 0,
|
||||||
|
correct: userSolutions[0]?.evaluation ? speakingReverseMarking[userSolutions[0]!.evaluation!.overall] || 0 : 0,
|
||||||
|
},
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Modal title="Correction" isOpen={showDiff} onClose={() => setShowDiff(false)}>
|
<Modal title="Correction" isOpen={showDiff} onClose={() => setShowDiff(false)}>
|
||||||
<>
|
<>
|
||||||
{userSolutions &&
|
{userSolutions &&
|
||||||
@@ -58,13 +96,13 @@ export default function Speaking({ id, type, title, video_url, text, prompts, us
|
|||||||
fontFamily: '"Open Sans", system-ui, -apple-system, "Helvetica Neue", sans-serif',
|
fontFamily: '"Open Sans", system-ui, -apple-system, "Helvetica Neue", sans-serif',
|
||||||
padding: "32px 28px",
|
padding: "32px 28px",
|
||||||
},
|
},
|
||||||
marker: { display: "none" },
|
marker: {display: "none"},
|
||||||
diffRemoved: { padding: "32px 28px" },
|
diffRemoved: {padding: "32px 28px"},
|
||||||
diffAdded: { padding: "32px 28px" },
|
diffAdded: {padding: "32px 28px"},
|
||||||
|
|
||||||
wordRemoved: { padding: "0px", display: "initial" },
|
wordRemoved: {padding: "0px", display: "initial"},
|
||||||
wordAdded: { padding: "0px", display: "initial" },
|
wordAdded: {padding: "0px", display: "initial"},
|
||||||
wordDiff: { padding: "0px", display: "initial" },
|
wordDiff: {padding: "0px", display: "initial"},
|
||||||
}}
|
}}
|
||||||
oldValue={userSolutions[0].evaluation.transcript_1.replaceAll("\\n", "\n")}
|
oldValue={userSolutions[0].evaluation.transcript_1.replaceAll("\\n", "\n")}
|
||||||
newValue={userSolutions[0].evaluation.fixed_text_1.replaceAll("\\n", "\n")}
|
newValue={userSolutions[0].evaluation.fixed_text_1.replaceAll("\\n", "\n")}
|
||||||
@@ -138,9 +176,13 @@ export default function Speaking({ id, type, title, video_url, text, prompts, us
|
|||||||
const grade: number = typeof taskResponse === "number" ? taskResponse : taskResponse.grade;
|
const grade: number = typeof taskResponse === "number" ? taskResponse : taskResponse.grade;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx("bg-ielts-speaking text-ielts-speaking-light rounded-xl px-4 py-2 tooltip tooltip-bottom",
|
<div
|
||||||
index === 0 && "tooltip-right"
|
className={clsx(
|
||||||
)} key={key} data-tip={tooltips[key] || "No additional information available"}>
|
"bg-ielts-speaking text-ielts-speaking-light rounded-xl px-4 py-2 tooltip tooltip-bottom",
|
||||||
|
index === 0 && "tooltip-right",
|
||||||
|
)}
|
||||||
|
key={key}
|
||||||
|
data-tip={tooltips[key] || "No additional information available"}>
|
||||||
{key}: Level {grade}
|
{key}: Level {grade}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -151,7 +193,7 @@ export default function Speaking({ id, type, title, video_url, text, prompts, us
|
|||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-speaking/20 p-1">
|
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-speaking/20 p-1">
|
||||||
<Tab
|
<Tab
|
||||||
className={({ selected }) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
||||||
@@ -162,7 +204,7 @@ export default function Speaking({ id, type, title, video_url, text, prompts, us
|
|||||||
General Feedback
|
General Feedback
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
className={({ selected }) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
||||||
@@ -173,7 +215,7 @@ export default function Speaking({ id, type, title, video_url, text, prompts, us
|
|||||||
Evaluation
|
Evaluation
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
className={({ selected }) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
||||||
@@ -194,10 +236,16 @@ export default function Speaking({ id, type, title, video_url, text, prompts, us
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key} className="flex flex-col gap-2">
|
<div key={key} className="flex flex-col gap-2">
|
||||||
<div className={clsx("bg-ielts-speaking text-ielts-speaking-light rounded-xl px-4 py-2 w-fit")} key={key}>
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"bg-ielts-speaking text-ielts-speaking-light rounded-xl px-4 py-2 w-fit",
|
||||||
|
)}
|
||||||
|
key={key}>
|
||||||
{key}: Level {grade}
|
{key}: Level {grade}
|
||||||
</div>
|
</div>
|
||||||
{typeof taskResponse !== "number" && <span className="px-2 py-2">{taskResponse.comment}</span>}
|
{typeof taskResponse !== "number" && (
|
||||||
|
<span className="px-2 py-2">{taskResponse.comment}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -236,7 +284,7 @@ export default function Speaking({ id, type, title, video_url, text, prompts, us
|
|||||||
onBack({
|
onBack({
|
||||||
exercise: id,
|
exercise: id,
|
||||||
solutions: userSolutions,
|
solutions: userSolutions,
|
||||||
score: { total: 100, missing: 0, correct: speakingReverseMarking[userSolutions[0]!.evaluation!.overall] || 0 },
|
score: {total: 100, missing: 0, correct: speakingReverseMarking[userSolutions[0]!.evaluation!.overall] || 0},
|
||||||
type,
|
type,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -261,6 +309,6 @@ export default function Speaking({ id, type, title, video_url, text, prompts, us
|
|||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,25 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col gap-4 mt-4">
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||||
|
className="max-w-[200px] w-full"
|
||||||
|
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={() => onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
||||||
<span className="text-sm w-full leading-6">
|
<span className="text-sm w-full leading-6">
|
||||||
{prompt.split("\\n").map((line, index) => (
|
{prompt.split("\\n").map((line, index) => (
|
||||||
@@ -125,13 +143,7 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||||
className="max-w-[200px] w-full"
|
className="max-w-[200px] w-full"
|
||||||
disabled={
|
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
||||||
exam &&
|
|
||||||
typeof partIndex !== "undefined" &&
|
|
||||||
exam.module === "level" &&
|
|
||||||
questionIndex === 0 &&
|
|
||||||
partIndex === 0
|
|
||||||
}>
|
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -142,6 +154,6 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu
|
|||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,25 @@ export default function WriteBlanksSolutions({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||||
|
className="max-w-[200px] w-full"
|
||||||
|
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={() => onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
||||||
<span className="text-sm w-full leading-6">
|
<span className="text-sm w-full leading-6">
|
||||||
{prompt.split("\\n").map((line, index) => (
|
{prompt.split("\\n").map((line, index) => (
|
||||||
@@ -146,13 +164,7 @@ export default function WriteBlanksSolutions({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||||
className="max-w-[200px] w-full"
|
className="max-w-[200px] w-full"
|
||||||
disabled={
|
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
||||||
exam &&
|
|
||||||
typeof partIndex !== "undefined" &&
|
|
||||||
exam.module === "level" &&
|
|
||||||
questionIndex === 0 &&
|
|
||||||
partIndex === 0
|
|
||||||
}>
|
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -163,6 +175,6 @@ export default function WriteBlanksSolutions({
|
|||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,70 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import { WritingExercise } from "@/interfaces/exam";
|
import {WritingExercise} from "@/interfaces/exam";
|
||||||
import { CommonProps } from ".";
|
import {CommonProps} from ".";
|
||||||
import { Fragment, useEffect, useState } from "react";
|
import {Fragment, useEffect, useState} from "react";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import { Dialog, Tab, Transition } from "@headlessui/react";
|
import {Dialog, Tab, Transition} from "@headlessui/react";
|
||||||
import { writingReverseMarking } from "@/utils/score";
|
import {writingReverseMarking} from "@/utils/score";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import ReactDiffViewer, { DiffMethod } from "react-diff-viewer";
|
import ReactDiffViewer, {DiffMethod} from "react-diff-viewer";
|
||||||
import useUser from "@/hooks/useUser";
|
import useUser from "@/hooks/useUser";
|
||||||
import AIDetection from "../AIDetection";
|
import AIDetection from "../AIDetection";
|
||||||
|
|
||||||
export default function Writing({ id, type, prompt, attachment, userSolutions, onNext, onBack }: WritingExercise & CommonProps) {
|
export default function Writing({id, type, prompt, attachment, userSolutions, onNext, onBack}: WritingExercise & CommonProps) {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [showDiff, setShowDiff] = useState(false);
|
const [showDiff, setShowDiff] = useState(false);
|
||||||
|
|
||||||
const { user } = useUser();
|
const {user} = useUser();
|
||||||
|
|
||||||
const aiEval = userSolutions && userSolutions.length > 0 ? userSolutions[0].evaluation?.ai_detection : undefined;
|
const aiEval = userSolutions && userSolutions.length > 0 ? userSolutions[0].evaluation?.ai_detection : undefined;
|
||||||
|
|
||||||
const tooltips: { [key: string]: string } = {
|
const tooltips: {[key: string]: string} = {
|
||||||
"Lexical Resource": "Assesses the diversity and accuracy of vocabulary used. A higher score indicates varied and precise word choice; a lower score points to limited vocabulary and inaccuracies.",
|
"Lexical Resource":
|
||||||
"Task Achievement": "Evaluates how well the task requirements are fulfilled. A higher score means all parts of the task are addressed thoroughly; a lower score shows incomplete or inadequate task response.",
|
"Assesses the diversity and accuracy of vocabulary used. A higher score indicates varied and precise word choice; a lower score points to limited vocabulary and inaccuracies.",
|
||||||
"Coherence and Cohesion": "Measures logical organization and flow of writing. A higher score reflects well-structured and connected ideas; a lower score indicates disorganized writing and poor linkage between ideas.",
|
"Task Achievement":
|
||||||
"Grammatical Range and Accuracy": "Looks at the range and precision of grammatical structures. A higher score shows varied and accurate grammar use; a lower score suggests frequent errors and limited range.",
|
"Evaluates how well the task requirements are fulfilled. A higher score means all parts of the task are addressed thoroughly; a lower score shows incomplete or inadequate task response.",
|
||||||
|
"Coherence and Cohesion":
|
||||||
|
"Measures logical organization and flow of writing. A higher score reflects well-structured and connected ideas; a lower score indicates disorganized writing and poor linkage between ideas.",
|
||||||
|
"Grammatical Range and Accuracy":
|
||||||
|
"Looks at the range and precision of grammatical structures. A higher score shows varied and accurate grammar use; a lower score suggests frequent errors and limited range.",
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col gap-4 mt-4">
|
||||||
|
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
onBack({
|
||||||
|
exercise: id,
|
||||||
|
solutions: userSolutions,
|
||||||
|
score: {total: 100, missing: 0, correct: writingReverseMarking[userSolutions[0]!.evaluation!.overall] || 0},
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={() =>
|
||||||
|
onNext({
|
||||||
|
exercise: id,
|
||||||
|
solutions: userSolutions,
|
||||||
|
score: {
|
||||||
|
total: 100,
|
||||||
|
missing: 0,
|
||||||
|
correct: userSolutions[0]?.evaluation ? writingReverseMarking[userSolutions[0]!.evaluation!.overall] || 0 : 0,
|
||||||
|
},
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{attachment && (
|
{attachment && (
|
||||||
<Transition show={isModalOpen} as={Fragment}>
|
<Transition show={isModalOpen} as={Fragment}>
|
||||||
<Dialog onClose={() => setIsModalOpen(false)} className="relative z-50">
|
<Dialog onClose={() => setIsModalOpen(false)} className="relative z-50">
|
||||||
@@ -99,13 +137,13 @@ export default function Writing({ id, type, prompt, attachment, userSolutions, o
|
|||||||
fontFamily: '"Open Sans", system-ui, -apple-system, "Helvetica Neue", sans-serif',
|
fontFamily: '"Open Sans", system-ui, -apple-system, "Helvetica Neue", sans-serif',
|
||||||
padding: "32px 28px",
|
padding: "32px 28px",
|
||||||
},
|
},
|
||||||
marker: { display: "none" },
|
marker: {display: "none"},
|
||||||
diffRemoved: { padding: "32px 28px" },
|
diffRemoved: {padding: "32px 28px"},
|
||||||
diffAdded: { padding: "32px 28px" },
|
diffAdded: {padding: "32px 28px"},
|
||||||
|
|
||||||
wordRemoved: { padding: "0px", display: "initial" },
|
wordRemoved: {padding: "0px", display: "initial"},
|
||||||
wordAdded: { padding: "0px", display: "initial" },
|
wordAdded: {padding: "0px", display: "initial"},
|
||||||
wordDiff: { padding: "0px", display: "initial" },
|
wordDiff: {padding: "0px", display: "initial"},
|
||||||
}}
|
}}
|
||||||
oldValue={userSolutions[0].solution.replaceAll("\\n", "\n")}
|
oldValue={userSolutions[0].solution.replaceAll("\\n", "\n")}
|
||||||
newValue={userSolutions[0].evaluation!.fixed_text!.replaceAll("\\n", "\n")}
|
newValue={userSolutions[0].evaluation!.fixed_text!.replaceAll("\\n", "\n")}
|
||||||
@@ -135,10 +173,13 @@ export default function Writing({ id, type, prompt, attachment, userSolutions, o
|
|||||||
const grade: number = typeof taskResponse === "number" ? taskResponse : taskResponse.grade;
|
const grade: number = typeof taskResponse === "number" ? taskResponse : taskResponse.grade;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(
|
<div
|
||||||
|
className={clsx(
|
||||||
"bg-ielts-writing text-ielts-writing-light rounded-xl px-4 py-2 tooltip tooltip-bottom",
|
"bg-ielts-writing text-ielts-writing-light rounded-xl px-4 py-2 tooltip tooltip-bottom",
|
||||||
index === 0 && "tooltip-right"
|
index === 0 && "tooltip-right",
|
||||||
)} key={key} data-tip={tooltips[key] || "No additional information available"}>
|
)}
|
||||||
|
key={key}
|
||||||
|
data-tip={tooltips[key] || "No additional information available"}>
|
||||||
{key}: Level {grade}
|
{key}: Level {grade}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -148,7 +189,7 @@ export default function Writing({ id, type, prompt, attachment, userSolutions, o
|
|||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-writing/20 p-1">
|
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-writing/20 p-1">
|
||||||
<Tab
|
<Tab
|
||||||
className={({ selected }) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-writing/80",
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-writing/80",
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-writing focus:outline-none focus:ring-2",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-writing focus:outline-none focus:ring-2",
|
||||||
@@ -159,7 +200,7 @@ export default function Writing({ id, type, prompt, attachment, userSolutions, o
|
|||||||
General Feedback
|
General Feedback
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
className={({ selected }) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-writing/80",
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-writing/80",
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-writing focus:outline-none focus:ring-2",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-writing focus:outline-none focus:ring-2",
|
||||||
@@ -170,7 +211,7 @@ export default function Writing({ id, type, prompt, attachment, userSolutions, o
|
|||||||
Evaluation
|
Evaluation
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
className={({ selected }) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-writing/80",
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-writing/80",
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-writing focus:outline-none focus:ring-2",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-writing focus:outline-none focus:ring-2",
|
||||||
@@ -182,7 +223,7 @@ export default function Writing({ id, type, prompt, attachment, userSolutions, o
|
|||||||
</Tab>
|
</Tab>
|
||||||
{aiEval && user?.type !== "student" && (
|
{aiEval && user?.type !== "student" && (
|
||||||
<Tab
|
<Tab
|
||||||
className={({ selected }) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-writing/80",
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-writing/80",
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-writing focus:outline-none focus:ring-2",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-writing focus:outline-none focus:ring-2",
|
||||||
@@ -204,10 +245,16 @@ export default function Writing({ id, type, prompt, attachment, userSolutions, o
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key} className="flex flex-col gap-2">
|
<div key={key} className="flex flex-col gap-2">
|
||||||
<div className={clsx("bg-ielts-writing text-ielts-writing-light rounded-xl px-4 py-2 w-fit")} key={key}>
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"bg-ielts-writing text-ielts-writing-light rounded-xl px-4 py-2 w-fit",
|
||||||
|
)}
|
||||||
|
key={key}>
|
||||||
{key}: Level {grade}
|
{key}: Level {grade}
|
||||||
</div>
|
</div>
|
||||||
{typeof taskResponse !== "number" && <span className="px-2 py-2">{taskResponse.comment}</span>}
|
{typeof taskResponse !== "number" && (
|
||||||
|
<span className="px-2 py-2">{taskResponse.comment}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -248,7 +295,7 @@ export default function Writing({ id, type, prompt, attachment, userSolutions, o
|
|||||||
onBack({
|
onBack({
|
||||||
exercise: id,
|
exercise: id,
|
||||||
solutions: userSolutions,
|
solutions: userSolutions,
|
||||||
score: { total: 100, missing: 0, correct: writingReverseMarking[userSolutions[0]!.evaluation!.overall] || 0 },
|
score: {total: 100, missing: 0, correct: writingReverseMarking[userSolutions[0]!.evaluation!.overall] || 0},
|
||||||
type,
|
type,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -273,6 +320,6 @@ export default function Writing({ id, type, prompt, attachment, userSolutions, o
|
|||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user