From 1086e789360f8d6c5278a73738ae1fc491dffdf1 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Tue, 26 Mar 2024 00:42:39 +0000 Subject: [PATCH] Updated the MatchSentences exercise to work better now --- package.json | 1 + src/components/Exercises/FillBlanks.tsx | 2 +- src/components/Exercises/MatchSentences.tsx | 133 ++++++++++++------ src/components/Exercises/MultipleChoice.tsx | 2 +- src/components/Exercises/TrueFalse.tsx | 2 +- src/components/Exercises/WriteBlanks.tsx | 2 +- src/components/Solutions/FillBlanks.tsx | 2 +- src/components/Solutions/MatchSentences.tsx | 101 ++++++------- src/components/Solutions/TrueFalse.tsx | 2 +- src/components/Solutions/WriteBlanks.tsx | 2 +- src/exams/Reading.tsx | 79 ++++++++--- src/interfaces/exam.ts | 24 ++-- src/pages/(generation)/ReadingGeneration.tsx | 1 - .../exam/[module]/generate/[...endpoint].ts | 4 +- src/pages/record.tsx | 6 +- yarn.lock | 23 +++ 16 files changed, 251 insertions(+), 135 deletions(-) diff --git a/package.json b/package.json index 61673f4e..6a4285a8 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@beam-australia/react-env": "^3.1.1", + "@dnd-kit/core": "^6.1.0", "@headlessui/react": "^1.7.13", "@mdi/js": "^7.1.96", "@mdi/react": "^1.6.1", diff --git a/src/components/Exercises/FillBlanks.tsx b/src/components/Exercises/FillBlanks.tsx index b0433ac6..92487c0a 100644 --- a/src/components/Exercises/FillBlanks.tsx +++ b/src/components/Exercises/FillBlanks.tsx @@ -128,7 +128,7 @@ export default function FillBlanks({ return ( <> -
+
{(!!currentBlankId || isDrawerShowing) && ( +
+ + {question.sentence} +
+
+ {answer && `Paragraph ${answer}`} +
+
+ ); +} + +function DraggableOptionArea({option}: {option: MatchSentenceExerciseOption}) { + const {attributes, listeners, setNodeRef, transform} = useDraggable({ + id: `draggable_option_${option.id}`, + }); + + const style = transform + ? { + transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`, + zIndex: 99, + } + : undefined; + + return ( +
+ +
+ ); +} export default function MatchSentences({id, options, type, prompt, sentences, userSolutions, onNext, onBack}: MatchSentencesExercise & CommonProps) { - const [selectedQuestion, setSelectedQuestion] = useState(); const [answers, setAnswers] = useState<{question: string; option: string}[]>(userSolutions); const hasExamEnded = useExamStore((state) => state.hasExamEnded); + const handleDragEnd = (event: DragEndEvent) => { + if (event.over && event.over.id.toString().startsWith("droppable")) { + const optionID = event.active.id.toString().replace("draggable_option_", ""); + const sentenceID = event.over.id.toString().replace("droppable_sentence_", ""); + + setAnswers((prev) => [...prev.filter((x) => x.question.toString() !== sentenceID), {question: sentenceID, option: optionID}]); + } + }; + const calculateScore = () => { const total = sentences.length; const correct = answers.filter( @@ -26,11 +87,9 @@ export default function MatchSentences({id, options, type, prompt, sentences, us return {total, correct, missing}; }; - const selectOption = (option: string) => { - if (!selectedQuestion) return; - setAnswers((prev) => [...prev.filter((x) => x.question !== selectedQuestion), {question: selectedQuestion, option}]); - setSelectedQuestion(undefined); - }; + useEffect(() => { + console.log(answers); + }, [answers]); useEffect(() => { if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type}); @@ -39,7 +98,7 @@ export default function MatchSentences({id, options, type, prompt, sentences, us return ( <> -
+
{prompt.split("\\n").map((line, index) => ( @@ -48,46 +107,28 @@ export default function MatchSentences({id, options, type, prompt, sentences, us ))} -
-
- {sentences.map(({sentence, id}) => ( -
- {sentence} - + + +
+
+ {sentences.map((question) => ( + x.question.toString() === question.id.toString())?.option} + /> + ))} +
+
+ Drag one of these paragraphs into the slots above: +
+ {options.map((option) => ( + + ))}
- ))} +
-
- {options.map(({sentence, id}) => ( -
- - {sentence} -
- ))} -
- {answers.map((solution, index) => ( - - ))} -
+
diff --git a/src/components/Exercises/MultipleChoice.tsx b/src/components/Exercises/MultipleChoice.tsx index 6bf1a25e..51b58beb 100644 --- a/src/components/Exercises/MultipleChoice.tsx +++ b/src/components/Exercises/MultipleChoice.tsx @@ -117,7 +117,7 @@ export default function MultipleChoice({ return ( <> -
+
{prompt} {questionIndex < questions.length && ( -
+
{prompt.split("\\n").map((line, index) => ( diff --git a/src/components/Exercises/WriteBlanks.tsx b/src/components/Exercises/WriteBlanks.tsx index e89d57a4..a3edfd85 100644 --- a/src/components/Exercises/WriteBlanks.tsx +++ b/src/components/Exercises/WriteBlanks.tsx @@ -88,7 +88,7 @@ export default function WriteBlanks({id, prompt, type, maxWords, solutions, user return ( <> -
+
{prompt.split("\\n").map((line, index) => ( diff --git a/src/components/Solutions/FillBlanks.tsx b/src/components/Solutions/FillBlanks.tsx index 413bfc9d..dde2820b 100644 --- a/src/components/Solutions/FillBlanks.tsx +++ b/src/components/Solutions/FillBlanks.tsx @@ -75,7 +75,7 @@ export default function FillBlanksSolutions({id, type, prompt, solutions, text, return ( <> -
+
{prompt.split("\\n").map((line, index) => ( diff --git a/src/components/Solutions/MatchSentences.tsx b/src/components/Solutions/MatchSentences.tsx index 27b7dfcc..c8f3b186 100644 --- a/src/components/Solutions/MatchSentences.tsx +++ b/src/components/Solutions/MatchSentences.tsx @@ -1,4 +1,4 @@ -import {MatchSentencesExercise} from "@/interfaces/exam"; +import {MatchSentenceExerciseSentence, MatchSentencesExercise} from "@/interfaces/exam"; import clsx from "clsx"; import LineTo from "react-lineto"; import {CommonProps} from "."; @@ -9,6 +9,48 @@ import {Fragment} from "react"; import Button from "../Low/Button"; import Xarrow from "react-xarrows"; +function QuestionSolutionArea({ + question, + userSolution, +}: { + question: MatchSentenceExerciseSentence; + userSolution?: {question: string; option: string}; +}) { + return ( +
+
+ + {question.sentence} +
+
+ + {userSolution && userSolution?.option.toString() !== question.solution.toString() && `Paragraph ${userSolution.option}`} + + Paragraph {question.solution} +
+
+ ); +} + export default function MatchSentencesSolutions({ id, type, @@ -31,7 +73,7 @@ export default function MatchSentencesSolutions({ return ( <> -
+
{prompt.split("\\n").map((line, index) => ( @@ -40,57 +82,18 @@ export default function MatchSentencesSolutions({ ))} -
+
- {sentences.map(({sentence, id, solution}) => ( -
- {sentence} - -
- ))} -
-
- {options.map(({sentence, id}) => ( -
- - {sentence} -
- ))} -
- {userSolutions && - sentences.map((sentence, index) => ( - x.question === sentence.id) - ? "#CC5454" - : userSolutions.find((x) => x.question === sentence.id)?.option === sentence.solution - ? "#7872BF" - : "#CC5454" - } - showHead={false} + {sentences.map((question) => ( + x.question.toString() === question.id.toString())} + key={`question_${question.id}`} /> ))} +
+
Correct diff --git a/src/components/Solutions/TrueFalse.tsx b/src/components/Solutions/TrueFalse.tsx index 44699bfb..a4ffc015 100644 --- a/src/components/Solutions/TrueFalse.tsx +++ b/src/components/Solutions/TrueFalse.tsx @@ -38,7 +38,7 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu return ( <> -
+
{prompt.split("\\n").map((line, index) => ( diff --git a/src/components/Solutions/WriteBlanks.tsx b/src/components/Solutions/WriteBlanks.tsx index 617fe4b9..2e37c440 100644 --- a/src/components/Solutions/WriteBlanks.tsx +++ b/src/components/Solutions/WriteBlanks.tsx @@ -107,7 +107,7 @@ export default function WriteBlanksSolutions({ return ( <> -
+
{prompt.split("\\n").map((line, index) => ( diff --git a/src/exams/Reading.tsx b/src/exams/Reading.tsx index 2497e892..64ee49fd 100644 --- a/src/exams/Reading.tsx +++ b/src/exams/Reading.tsx @@ -1,4 +1,4 @@ -import {MultipleChoiceExercise, ReadingExam, UserSolution} from "@/interfaces/exam"; +import {MultipleChoiceExercise, ReadingExam, ReadingPart, UserSolution} from "@/interfaces/exam"; import {Fragment, useEffect, useState} from "react"; import Icon from "@mdi/react"; import {mdiArrowRight, mdiNotebook} from "@mdi/js"; @@ -10,7 +10,7 @@ import {renderExercise} from "@/components/Exercises"; import {renderSolution} from "@/components/Solutions"; import {Panel} from "primereact/panel"; import {Steps} from "primereact/steps"; -import {BsAlarm, BsBook, BsClock, BsStopwatch} from "react-icons/bs"; +import {BsAlarm, BsBook, BsChevronDown, BsChevronUp, BsClock, BsStopwatch} from "react-icons/bs"; import ProgressBar from "@/components/Low/ProgressBar"; import ModuleTitle from "@/components/Medium/ModuleTitle"; import {Divider} from "primereact/divider"; @@ -26,6 +26,8 @@ interface Props { onFinish: (userSolutions: UserSolution[]) => void; } +const numberToLetter = (number: number) => (number + 9).toString(36).toUpperCase(); + function TextModal({isOpen, title, content, onClose}: {isOpen: boolean; title: string; content: string; onClose: () => void}) { return ( @@ -80,12 +82,37 @@ function TextModal({isOpen, title, content, onClose}: {isOpen: boolean; title: s ); } +function TextComponent({part, exerciseType}: {part: ReadingPart; exerciseType: string}) { + return ( +
+

{part.text.title}

+
+ {part.text.content + .split(/\n|(\\n)/g) + .filter((x) => x && x.length > 0) + .map((line, index) => ( + + {exerciseType === "matchSentences" && ( +
+ {numberToLetter(index + 1)} +

{line}

+
+ )} + {exerciseType !== "matchSentences" &&

{line}

} +
+ ))} +
+ ); +} + export default function Reading({exam, showSolutions = false, onFinish}: Props) { const [questionIndex, setQuestionIndex] = useState(0); const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); const [showTextModal, setShowTextModal] = useState(false); const [showBlankModal, setShowBlankModal] = useState(false); const [multipleChoicesDone, setMultipleChoicesDone] = useState<{id: string; amount: number}[]>([]); + const [isTextMinimized, setIsTextMinimzed] = useState(false); + const [exerciseType, setExerciseType] = useState(""); const {userSolutions, setUserSolutions} = useExamStore((state) => state); const {hasExamEnded, setHasExamEnded} = useExamStore((state) => state); @@ -154,6 +181,7 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props) } if (storeQuestionIndex > 0) { const exercise = getExercise(); + setExerciseType(exercise.type); setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise.id), {id: exercise.id, amount: storeQuestionIndex}]); } setStoreQuestionIndex(0); @@ -211,6 +239,7 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props) useEffect(() => { if (partIndex > -1 && exerciseIndex > -1) { const exercise = getExercise(); + setExerciseType(exercise.type); setMultipleChoicesDone((prev) => prev.filter((x) => x.id !== exercise.id)); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -234,22 +263,29 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props) }; const renderText = () => ( -
-
-

- Please read the following excerpt attentively, you will then be asked questions about the text you've read. -

- You will be allowed to read the text while doing the exercises -
-
-

{exam.parts[partIndex].text.title}

-
- - {exam.parts[partIndex].text.content.split("\\n").map((line, index) => ( -

{line}

- ))} -
-
+
+ + {!isTextMinimized && ( + <> +
+

+ Please read the following excerpt attentively, you will then be asked questions about the text you've read. +

+ You will be allowed to read the text while doing the exercises +
+ + + )} + {isTextMinimized && Reading Passage}
); @@ -266,7 +302,12 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props) disableTimer={showSolutions} label={exerciseIndex === -1 ? undefined : convertCamelCaseToReadable(exam.parts[partIndex].exercises[exerciseIndex].type)} /> -
-1 && "grid grid-cols-2 gap-4")}> +
-1 && !isTextMinimized && "grid grid-cols-2 gap-4", + exerciseIndex > -1 && isTextMinimized && "flex flex-col gap-2", + )}> {partIndex > -1 && renderText()} {exerciseIndex > -1 && diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts index b42675ca..c219ef19 100644 --- a/src/interfaces/exam.ts +++ b/src/interfaces/exam.ts @@ -233,17 +233,21 @@ export interface MatchSentencesExercise { id: string; prompt: string; userSolutions: {question: string; option: string}[]; - sentences: { - id: string; - sentence: string; - solution: string; - color: string; - }[]; + sentences: MatchSentenceExerciseSentence[]; allowRepetition: boolean; - options: { - id: string; - sentence: string; - }[]; + options: MatchSentenceExerciseOption[]; +} + +export interface MatchSentenceExerciseSentence { + id: string; + sentence: string; + solution: string; + color: string; +} + +export interface MatchSentenceExerciseOption { + id: string; + sentence: string; } export interface MultipleChoiceExercise { diff --git a/src/pages/(generation)/ReadingGeneration.tsx b/src/pages/(generation)/ReadingGeneration.tsx index 84196f6f..a49d9676 100644 --- a/src/pages/(generation)/ReadingGeneration.tsx +++ b/src/pages/(generation)/ReadingGeneration.tsx @@ -124,7 +124,6 @@ const ReadingGeneration = () => { const availableTypes = [ {type: "fillBlanks", label: "Fill the Blanks"}, - {type: "multipleChoice", label: "Multiple Choice"}, {type: "trueFalse", label: "True or False"}, {type: "writeBlanks", label: "Write the Blanks"}, {type: "matchSentences", label: "Match Sentences"}, diff --git a/src/pages/api/exam/[module]/generate/[...endpoint].ts b/src/pages/api/exam/[module]/generate/[...endpoint].ts index 3cb7c052..f1fa416f 100644 --- a/src/pages/api/exam/[module]/generate/[...endpoint].ts +++ b/src/pages/api/exam/[module]/generate/[...endpoint].ts @@ -29,14 +29,14 @@ async function get(req: NextApiRequest, res: NextApiResponse) { module: Module; endpoint: string; topic?: string; - exercises?: string[]; + exercises?: string[] | string; difficulty?: Difficulty; }; const url = `${process.env.BACKEND_URL}/${endpoint}`; const params = new URLSearchParams(); if (topic) params.append("topic", topic); - if (exercises) exercises.forEach((exercise) => params.append("exercises", exercise)); + if (exercises) (typeof exercises === "string" ? [exercises] : exercises).forEach((exercise) => params.append("exercises", exercise)); if (difficulty) params.append("difficulty", difficulty); const result = await axios.get(`${url}${params.toString().length > 0 ? `?${params.toString()}` : ""}`, { diff --git a/src/pages/record.tsx b/src/pages/record.tsx index d56cc63b..558da5a8 100644 --- a/src/pages/record.tsx +++ b/src/pages/record.tsx @@ -74,7 +74,11 @@ export default function History({user}: {user: User}) { setGroupedStats( groupByDate( stats.filter((x) => { - if ((x.module === "writing" || x.module === "speaking") && !x.isDisabled && !x.solutions.every((y) => "evaluation" in y)) + if ( + (x.module === "writing" || x.module === "speaking") && + !x.isDisabled && + !x.solutions.every((y) => Object.keys(y).includes("evaluation")) + ) return false; return true; }), diff --git a/yarn.lock b/yarn.lock index f262d9df..cfab02ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -88,6 +88,29 @@ dotenv-expand "^5.1.0" minimist "^1.2.0" +"@dnd-kit/accessibility@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz#1054e19be276b5f1154ced7947fc0cb5d99192e0" + integrity sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ== + dependencies: + tslib "^2.0.0" + +"@dnd-kit/core@^6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.1.0.tgz#e81a3d10d9eca5d3b01cbf054171273a3fe01def" + integrity sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg== + dependencies: + "@dnd-kit/accessibility" "^3.1.0" + "@dnd-kit/utilities" "^3.2.2" + tslib "^2.0.0" + +"@dnd-kit/utilities@^3.2.2": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz#5a32b6af356dc5f74d61b37d6f7129a4040ced7b" + integrity sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg== + dependencies: + tslib "^2.0.0" + "@emotion/babel-plugin@^11.11.0": version "11.11.0" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c"