From b50913eda8195322550a0607cb6facef16c8574f Mon Sep 17 00:00:00 2001 From: Carlos-Mesquita Date: Wed, 6 Nov 2024 09:23:34 +0000 Subject: [PATCH] Listening preview and some more patches --- .../Exercises/Blanks/BlanksReducer.tsx | 4 +- .../Blanks/WriteBlankFill/validation.ts | 1 - .../ExamEditor/Exercises/Blanks/index.tsx | 6 +- .../Exercises/MatchSentences/index.tsx | 22 +- .../Exercises/Shared/PromptEdit.tsx | 49 ++++ .../ExamEditor/Exercises/Speaking/index.tsx | 56 +++-- .../ExamEditor/Exercises/TrueFalse/index.tsx | 39 +-- .../Exercises/WriteBlanks/index.tsx | 5 +- .../ExamEditor/Exercises/Writing/index.tsx | 12 +- .../ExamEditor/Hooks/useSettingsState.tsx | 6 +- .../SectionContext/listening.tsx | 1 + .../SectionExercises/listening.tsx | 17 ++ .../ExamEditor/SettingsEditor/index.tsx | 8 +- .../ExamEditor/SettingsEditor/level.tsx | 2 +- .../ExamEditor/SettingsEditor/listening.tsx | 100 +++++++- .../ExamEditor/SettingsEditor/reading.tsx | 8 +- .../ExamEditor/SettingsEditor/speaking.tsx | 2 +- .../ExamEditor/SettingsEditor/writing.tsx | 2 +- src/components/ExamEditor/Shared/Header.tsx | 24 +- src/components/Popouts/Exam.tsx | 10 +- src/exams/Listening.tsx | 224 +++++++++++------- src/exams/Reading.tsx | 22 +- src/interfaces/exam.ts | 8 + src/stores/examEditor/reorder/global.ts | 92 ++++--- 24 files changed, 467 insertions(+), 253 deletions(-) create mode 100644 src/components/ExamEditor/Exercises/Shared/PromptEdit.tsx diff --git a/src/components/ExamEditor/Exercises/Blanks/BlanksReducer.tsx b/src/components/ExamEditor/Exercises/Blanks/BlanksReducer.tsx index 5f3862a8..f7b67cc4 100644 --- a/src/components/ExamEditor/Exercises/Blanks/BlanksReducer.tsx +++ b/src/components/ExamEditor/Exercises/Blanks/BlanksReducer.tsx @@ -1,13 +1,13 @@ import { toast } from "react-toastify"; -type TextToken = { +export type TextToken = { type: 'text'; content: string; isWhitespace: boolean; isLineBreak?: boolean; }; -type BlankToken = { +export type BlankToken = { type: 'blank'; id: number; }; diff --git a/src/components/ExamEditor/Exercises/Blanks/WriteBlankFill/validation.ts b/src/components/ExamEditor/Exercises/Blanks/WriteBlankFill/validation.ts index 538581c5..eb1df2b9 100644 --- a/src/components/ExamEditor/Exercises/Blanks/WriteBlankFill/validation.ts +++ b/src/components/ExamEditor/Exercises/Blanks/WriteBlankFill/validation.ts @@ -1,5 +1,4 @@ import { AlertItem } from "../../Shared/Alert"; -import { BlankState } from "../BlanksReducer"; export const validateWriteBlanks = ( diff --git a/src/components/ExamEditor/Exercises/Blanks/index.tsx b/src/components/ExamEditor/Exercises/Blanks/index.tsx index a93a3b9e..855a4c7d 100644 --- a/src/components/ExamEditor/Exercises/Blanks/index.tsx +++ b/src/components/ExamEditor/Exercises/Blanks/index.tsx @@ -18,7 +18,7 @@ import Alert, { AlertItem } from "../Shared/Alert"; import clsx from "clsx"; import { Card, CardContent } from "@/components/ui/card"; import { Blank, DropZone } from "./DragNDrop"; -import { getTextSegments, BlankState, BlanksState, BlanksAction } from "./BlanksReducer"; +import { getTextSegments, BlankState, BlanksState, BlanksAction, BlankToken } from "./BlanksReducer"; interface Props { @@ -104,11 +104,11 @@ const BlanksEditor: React.FC = ({ const existingBlankIds = getTextSegments(state.text) .filter(token => token.type === 'blank') - .map(token => token.id); + .map(token => (token as BlankToken).id); const newBlankIds = getTextSegments(processedText) .filter(token => token.type === 'blank') - .map(token => token.id); + .map(token => (token as BlankToken).id); const removedBlankIds = existingBlankIds.filter(id => !newBlankIds.includes(id)); diff --git a/src/components/ExamEditor/Exercises/MatchSentences/index.tsx b/src/components/ExamEditor/Exercises/MatchSentences/index.tsx index eb724fe0..18dbf2e3 100644 --- a/src/components/ExamEditor/Exercises/MatchSentences/index.tsx +++ b/src/components/ExamEditor/Exercises/MatchSentences/index.tsx @@ -17,6 +17,7 @@ import setEditingAlert from '../Shared/setEditingAlert'; import { toast } from 'react-toastify'; import { DragEndEvent } from '@dnd-kit/core'; import { handleMatchSentencesReorder } from '@/stores/examEditor/reorder/local'; +import PromptEdit from '../Shared/PromptEdit'; const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: number }> = ({ exercise, sectionId }) => { const { currentModule, dispatch } = useExamEditorStore(); @@ -31,6 +32,11 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu const [showReference, setShowReference] = useState(false); const [alerts, setAlerts] = useState([]); + const updateLocal = (exercise: MatchSentencesExercise) => { + setLocal(exercise); + setEditing(true); + }; + const { editing, setEditing, handleSave, handleDiscard, modeHandle } = useSectionEdit({ sectionId, onSave: () => { @@ -73,9 +79,8 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu }, [local.sentences]); const addHeading = () => { - setEditing(true); const newId = (parseInt(local.sentences[local.sentences.length - 1].id) + 1).toString(); - setLocal({ + updateLocal({ ...local, sentences: [ ...local.sentences, @@ -89,7 +94,6 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu }; const updateHeading = (index: number, field: string, value: string) => { - setEditing(true); const newSentences = [...local.sentences]; if (field === 'solution') { @@ -100,11 +104,10 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu } newSentences[index] = { ...newSentences[index], [field]: value }; - setLocal({ ...local, sentences: newSentences }); + updateLocal({ ...local, sentences: newSentences }); }; const deleteHeading = (index: number) => { - setEditing(true); if (local.sentences.length <= 1) { toast.error(`There needs to be at least one ${exercise.variant && exercise.variant == "ideaMatch" ? "idea/opinion" : "heading"}!`); return; @@ -116,7 +119,7 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu } const newSentences = local.sentences.filter((_, i) => i !== index); - setLocal({ ...local, sentences: newSentences }); + updateLocal({ ...local, sentences: newSentences }); }; useEffect(() => { @@ -129,8 +132,7 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu const handleDragEnd = (event: DragEndEvent) => { - setEditing(true); - setLocal(handleMatchSentencesReorder(event, local)); + updateLocal(handleMatchSentencesReorder(event, local)); } return ( @@ -154,6 +156,10 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu
{alerts.length > 0 && } + updateLocal({ ...local, prompt: text })} + /> s.id)} handleDragEnd={handleDragEnd} diff --git a/src/components/ExamEditor/Exercises/Shared/PromptEdit.tsx b/src/components/ExamEditor/Exercises/Shared/PromptEdit.tsx new file mode 100644 index 00000000..3fa21c3c --- /dev/null +++ b/src/components/ExamEditor/Exercises/Shared/PromptEdit.tsx @@ -0,0 +1,49 @@ +import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea"; +import { Card, CardContent } from "@/components/ui/card"; +import { useState } from "react"; +import { MdEdit, MdEditOff } from "react-icons/md"; + +interface Props { + value: string; + onChange: (text: string) => void; +} + + +const PromptEdit: React.FC = ({ value, onChange }) => { + const [editing, setEditing] = useState(false); + + return ( + <> + + +
+ {editing ? ( + onChange(text)} + onBlur={()=> setEditing(false)} + /> + ) : ( +
+

Question/Instructions displayed to the student:

+

{value}

+
+ )} + +
+
+
+ + ); +} + +export default PromptEdit; \ No newline at end of file diff --git a/src/components/ExamEditor/Exercises/Speaking/index.tsx b/src/components/ExamEditor/Exercises/Speaking/index.tsx index 9033b856..0dea9f87 100644 --- a/src/components/ExamEditor/Exercises/Speaking/index.tsx +++ b/src/components/ExamEditor/Exercises/Speaking/index.tsx @@ -24,11 +24,20 @@ const Speaking: React.FC = ({ sectionId, exercise }) => { const [loading, setLoading] = useState(generating === "context"); const [questions, setQuestions] = useState(() => { if (sectionId === 1) { - return (exercise as SpeakingExercise).prompts || Array(5).fill(""); + if ((exercise as SpeakingExercise).prompts.length > 0) { + return (exercise as SpeakingExercise).prompts; + } + return Array(5).fill(""); } else if (sectionId === 2) { - return [(exercise as SpeakingExercise).text || "", ...(exercise as SpeakingExercise).prompts || Array(3).fill("")]; + if ((exercise as SpeakingExercise).text && (exercise as SpeakingExercise).prompts.length > 0) { + return (exercise as SpeakingExercise).prompts; + } + return Array(3).fill(""); } else { - return (exercise as InteractiveSpeakingExercise).prompts?.map(p => p.text) || Array(5).fill(""); + if ((exercise as InteractiveSpeakingExercise).prompts.length > 0) { + return (exercise as InteractiveSpeakingExercise).prompts?.map(p => p.text); + } + return Array(5).fill(""); } }); @@ -42,7 +51,7 @@ const Speaking: React.FC = ({ sectionId, exercise }) => { onSave: () => { let newExercise; if (sectionId === 1) { - newExercise = { + newExercise = { ...local, prompts: questions } as SpeakingExercise; @@ -53,7 +62,6 @@ const Speaking: React.FC = ({ sectionId, exercise }) => { prompts: questions.slice(1), } as SpeakingExercise; } else { - // Section 3 newExercise = { ...local, prompts: questions.map(text => ({ @@ -63,22 +71,25 @@ const Speaking: React.FC = ({ sectionId, exercise }) => { } as InteractiveSpeakingExercise; } setEditing(false); - dispatch({ - type: "UPDATE_SECTION_STATE", - payload: { - sectionId: sectionId, - update: newExercise - } + dispatch({ + type: "UPDATE_SECTION_STATE", + payload: { + sectionId: sectionId, + update: newExercise + } }); }, onDiscard: () => { setLocal(exercise); + }, + onMode: () => { + setLocal(exercise); if (sectionId === 1) { - setQuestions((exercise as SpeakingExercise).prompts || Array(5).fill("")); + setQuestions(Array(5).fill("")); } else if (sectionId === 2) { - setQuestions([(exercise as SpeakingExercise).text || "", ...(exercise as SpeakingExercise).prompts || Array(3).fill("")]); + setQuestions(Array(3).fill("")); } else { - setQuestions((exercise as InteractiveSpeakingExercise).prompts?.map(p => p.text) || Array(5).fill("")); + setQuestions(Array(5).fill("")); } }, }); @@ -90,6 +101,7 @@ const Speaking: React.FC = ({ sectionId, exercise }) => { if (isLoading) { updateModule({ edit: Array.from(new Set([...edit, sectionId])) }); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [generating, sectionId]); useEffect(() => { @@ -102,14 +114,14 @@ const Speaking: React.FC = ({ sectionId, exercise }) => { } else { setQuestions(genResult[0].questions); } - - dispatch({ - type: "UPDATE_SECTION_SINGLE_FIELD", - payload: { - sectionId, - module: currentModule, - field: "genResult", - value: undefined + + dispatch({ + type: "UPDATE_SECTION_SINGLE_FIELD", + payload: { + sectionId, + module: currentModule, + field: "genResult", + value: undefined } }); } diff --git a/src/components/ExamEditor/Exercises/TrueFalse/index.tsx b/src/components/ExamEditor/Exercises/TrueFalse/index.tsx index 33cdcffe..f9541ec4 100644 --- a/src/components/ExamEditor/Exercises/TrueFalse/index.tsx +++ b/src/components/ExamEditor/Exercises/TrueFalse/index.tsx @@ -1,9 +1,6 @@ import React, { useEffect, useState } from 'react'; -import { Card, CardContent } from '@/components/ui/card'; import { MdAdd, - MdEdit, - MdEditOff, } from 'react-icons/md'; import Alert, { AlertItem } from '../Shared/Alert'; import { ReadingPart, TrueFalseExercise } from '@/interfaces/exam'; @@ -18,6 +15,7 @@ import validateTrueFalseQuestions from './validation'; import setEditingAlert from '../Shared/setEditingAlert'; import { DragEndEvent } from '@dnd-kit/core'; import { handleTrueFalseReorder } from '@/stores/examEditor/reorder/local'; +import PromptEdit from '../Shared/PromptEdit'; const TrueFalse: React.FC<{ exercise: TrueFalseExercise, sectionId: number }> = ({ exercise, sectionId }) => { const { currentModule, dispatch } = useExamEditorStore(); @@ -28,7 +26,6 @@ const TrueFalse: React.FC<{ exercise: TrueFalseExercise, sectionId: number }> = const section = state as ReadingPart; const [local, setLocal] = useState(exercise); - const [editingPrompt, setEditingPrompt] = useState(false); const [alerts, setAlerts] = useState([]); @@ -134,36 +131,10 @@ const TrueFalse: React.FC<{ exercise: TrueFalseExercise, sectionId: number }> = handleDiscard={handleDiscard} /> {alerts.length > 0 && } - - -
- {editingPrompt ? ( -