diff --git a/src/components/ExamEditor/Exercises/Script/Message.tsx b/src/components/ExamEditor/Exercises/Script/Message.tsx new file mode 100644 index 00000000..b2e3fd4c --- /dev/null +++ b/src/components/ExamEditor/Exercises/Script/Message.tsx @@ -0,0 +1,86 @@ +import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea"; +import { useState } from "react"; +import { FaEdit, FaFemale, FaMale } from "react-icons/fa"; +import { FaTrash } from "react-icons/fa6"; +import { ScriptLine } from "."; + +interface MessageProps { + message: ScriptLine & { position: 'left' | 'right' }; + color: string; + editing: boolean; + onEdit?: (text: string) => void; + onDelete?: () => void; +} + +const Message: React.FC = ({ message, color, editing, onEdit, onDelete }) => { + const [isEditing, setIsEditing] = useState(false); + const [editText, setEditText] = useState(message.text); + + return ( +
+
+
+ {message.gender === 'male' ? ( + + ) : ( + + )} + {message.name} +
+
+ {isEditing ? ( +
+ +
+ + +
+
+ ) : ( +
+

{message.text}

+ {editing && ( +
+
+ + +
+
+ )} +
+ )} +
+
+
+ ); +}; + +export default Message; diff --git a/src/components/ExamEditor/Exercises/Script/index.tsx b/src/components/ExamEditor/Exercises/Script/index.tsx new file mode 100644 index 00000000..3fc586a1 --- /dev/null +++ b/src/components/ExamEditor/Exercises/Script/index.tsx @@ -0,0 +1,330 @@ +import React, { useState, useMemo } from 'react'; +import { Script } from "@/interfaces/exam"; +import Message from './Message'; +import AutoExpandingTextArea from '@/components/Low/AutoExpandingTextarea'; +import { Card, CardContent } from '@/components/ui/card'; +import Input from '@/components/Low/Input'; +import { FaFemale, FaMale, FaPlus } from 'react-icons/fa'; +import clsx from 'clsx'; + +export interface Speaker { + id: number; + name: string; + gender: 'male' | 'female'; + color: string; + position: 'left' | 'right'; +} + +type Gender = 'male' | 'female'; + +export interface ScriptLine { + name: string; + gender: Gender; + text: string; + voice?: string; +} + +interface MessageWithPosition extends ScriptLine { + position: 'left' | 'right'; +} + +interface Props { + section: number; + editing?: boolean; + local: Script; + setLocal: (script: Script) => void; +} + +const colorOptions = [ + 'red', 'blue', 'green', 'purple', 'pink', 'indigo', 'teal', 'orange', + 'cyan', 'emerald', 'sky', 'violet', 'fuchsia', 'rose', 'lime', 'slate' +]; + +const ScriptEditor: React.FC = ({ section, editing = false, local, setLocal }) => { + const isConversation = [1, 3].includes(section); + + const [selectedSpeaker, setSelectedSpeaker] = useState(''); + const [newMessage, setNewMessage] = useState(''); + + const speakerCount = section === 1 ? 2 : 4; + + const [speakers, setSpeakers] = useState(() => { + const existingScript = local as ScriptLine[]; + const existingSpeakers = new Set(); + const speakerGenders = new Map(); + + if (Array.isArray(existingScript)) { + existingScript.forEach(line => { + existingSpeakers.add(line.name); + speakerGenders.set(line.name, line.gender.toLowerCase() === 'female' ? 'female' : 'male' as 'male' | 'female'); + }); + } + + const speakerArray = Array.from(existingSpeakers); + const totalNeeded = Math.max(speakerCount, speakerArray.length); + + return Array.from({ length: totalNeeded }, (_, index) => { + if (index < speakerArray.length) { + return { + id: index, + name: speakerArray[index], + gender: speakerGenders.get(speakerArray[index]) || 'male', + color: colorOptions[index], + position: index % 2 === 0 ? 'left' : 'right' + }; + } + return { + id: index, + name: '', + gender: 'male', + color: colorOptions[index], + position: index % 2 === 0 ? 'left' : 'right' + }; + }); + }); + + const speakerProperties = useMemo(() => { + return speakers.reduce((acc, speaker) => { + if (speaker.name) { + acc[speaker.name] = { + color: speaker.color, + gender: speaker.gender + }; + } + return acc; + }, {} as Record); + }, [speakers]); + + const allSpeakersConfigured = useMemo(() => { + return speakers.every(speaker => speaker.name.trim() !== ''); + }, [speakers]); + + const updateSpeaker = (index: number, updates: Partial) => { + const updatedSpeakers = speakers.map((speaker, i) => { + if (i === index) { + return { ...speaker, ...updates }; + } + return speaker; + }); + setSpeakers(updatedSpeakers); + + if (Array.isArray(local)) { + if ('name' in updates && speakers[index].name) { + const oldName = speakers[index].name; + const newName = updates.name || ''; + const updatedScript = local.map(line => { + if (line.name === oldName) { + return { ...line, name: newName }; + } + return line; + }); + setLocal(updatedScript); + } + + if ('gender' in updates && speakers[index].name && updates.gender) { + const name = speakers[index].name; + const newGender = updates.gender; + const updatedScript = local.map(line => { + if (line.name === name) { + return { ...line, gender: newGender }; + } + return line; + }); + setLocal(updatedScript); + } + } + + if ('name' in updates && speakers[index].name === selectedSpeaker) { + setSelectedSpeaker(updates.name || ''); + } + }; + + + const addMessage = () => { + if (!isConversation || !selectedSpeaker || !newMessage.trim()) return; + if (!Array.isArray(local)) return; + + const speaker = speakers.find(s => s.name === selectedSpeaker); + if (!speaker) return; + + const newLine: ScriptLine = { + name: selectedSpeaker, + gender: speaker.gender, + text: newMessage.trim() + }; + + const updatedScript = [...local, newLine]; + setLocal(updatedScript); + setNewMessage(''); + }; + + const updateMessage = (index: number, newText: string) => { + if (!Array.isArray(local)) return; + + const updatedScript = [...local]; + updatedScript[index] = { ...updatedScript[index], text: newText }; + setLocal(updatedScript); + }; + + const deleteMessage = (index: number) => { + if (!Array.isArray(local)) return; + + const updatedScript = local.filter((_, i) => i !== index); + setLocal(updatedScript); + }; + + const updateMonologue = (text: string) => { + setLocal(text); + }; + + const messages = useMemo(() => { + if (typeof local === 'string' || !Array.isArray(local)) return []; + + return local.reduce((acc, line, index) => { + const normalizedLine = { + ...line, + gender: line.gender.toLowerCase() === 'female' ? 'female' : 'male' + } as ScriptLine; + + if (index === 0) { + acc.push({ ...normalizedLine, position: 'left' }); + } else { + const prevMsg = acc[index - 1]; + const position = line.name === prevMsg.name + ? prevMsg.position + : (prevMsg.position === 'left' ? 'right' : 'left'); + acc.push({ ...normalizedLine, position }); + } + return acc; + }, []); + }, [local]); + + if (!isConversation) { + return ( + + +
+ {editing ? ( + + ) : ( +
+ {(local as string) || "Edit, generate or import your own audio."} +
+ )} +
+
+
+ ); + } + + return ( + + +
+ {editing && ( +
+

Edit Conversation

+
+ {speakers.map((speaker, index) => ( +
+
+ updateSpeaker(index, { name: text })} + placeholder="Speaker name" + /> +
+ +
+ +
+ {speaker.gender === 'male' ? ( + + ) : ( + + )} +
+
+
+ ))} +
+
+
+ + +
+ +
+ +
+
+
+ )} + +
+ {messages.map((message, index) => { + const properties = speakerProperties[message.name]; + if (!properties) return null; + + return ( + updateMessage(index, text)} + onDelete={() => deleteMessage(index)} + /> + ); + })} +
+
+
+
+ ); +}; + +export default ScriptEditor; diff --git a/src/components/ExamEditor/Exercises/Shared/Script.tsx b/src/components/ExamEditor/Exercises/Shared/Script.tsx deleted file mode 100644 index 759150c4..00000000 --- a/src/components/ExamEditor/Exercises/Shared/Script.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { Script } from '@/interfaces/exam'; -import { FaFemale, FaMale } from "react-icons/fa"; -import AutoExpandingTextArea from '@/components/Low/AutoExpandingTextarea'; -import clsx from 'clsx'; - -const colorOptions = [ - 'red', 'blue', 'green', 'purple', 'pink', 'indigo', 'teal', 'orange', - 'cyan', 'emerald', 'sky', 'violet', 'fuchsia', 'rose', 'lime', 'slate' -]; - -interface Speaker { - id: number; - name: string; - gender: 'male' | 'female'; - color: string; - position: 'left' | 'right'; -} - -interface Props { - script?: Script; - setScript: React.Dispatch>; - editing?: boolean; -} - -const ScriptRender: React.FC = ({ script, setScript, editing = false }) => { - const [speakers, setSpeakers] = useState(() => { - if (!script || typeof script === 'string') return []; - - const uniqueSpeakers = new Map(); - const usedColors = new Set(); - let isLeft = true; - - script.forEach((line, index) => { - if (!uniqueSpeakers.has(line.name)) { - const availableColors = colorOptions.filter(color => !usedColors.has(color)); - if (availableColors.length === 0) { - usedColors.clear(); - } - const randomColor = availableColors[Math.floor(Math.random() * availableColors.length)]; - usedColors.add(randomColor); - - uniqueSpeakers.set(line.name, { - id: index, - name: line.name, - gender: line.gender, - color: randomColor, - position: isLeft ? 'left' : 'right' - }); - isLeft = !isLeft; - } - }); - return Array.from(uniqueSpeakers.values()); - }); - - const speakerProperties = useMemo(() => { - return speakers.reduce((acc, speaker) => { - acc[speaker.name] = { - color: speaker.color, - position: speaker.position - }; - return acc; - }, {} as Record); - }, [speakers]); - - if (script === undefined) return null; - - if (typeof script === 'string') { - return ( -
- {editing ? ( - setScript(text)} - /> - ) : ( -

") }} /> - )} -

- ); - } - - const updateMessage = (index: number, newText: string) => { - setScript([ - ...script.slice(0, index), - { ...script[index], text: newText }, - ...script.slice(index + 1) - ]); - }; - - return ( -
-
- {script.map((line, index) => { - const { color, position } = speakerProperties[line.name]; - - return ( -
-
-
- {line.gender === 'male' ? ( - - ) : ( - - )} - - {line.name} - -
-
- {editing ? ( - updateMessage(index, text)} - /> - ) : ( -

{line.text}

- )} -
-
-
- ); - })} -
-
- ); -}; - -export default ScriptRender; \ No newline at end of file diff --git a/src/components/ExamEditor/SectionRenderer/SectionContext/listening.tsx b/src/components/ExamEditor/SectionRenderer/SectionContext/listening.tsx index 88bcd95d..032c89a4 100644 --- a/src/components/ExamEditor/SectionRenderer/SectionContext/listening.tsx +++ b/src/components/ExamEditor/SectionRenderer/SectionContext/listening.tsx @@ -2,10 +2,10 @@ import { useEffect, useState } from "react"; import { ListeningPart } from "@/interfaces/exam"; import SectionContext from "."; import useExamEditorStore from "@/stores/examEditor"; -import { FaFemale, FaMale } from "react-icons/fa"; import useSectionEdit from "../../Hooks/useSectionEdit"; -import ScriptRender from "../../Exercises/Shared/Script"; +import ScriptRender from "../../Exercises/Script"; import { Card, CardContent } from "@/components/ui/card"; +import Dropdown from "@/components/Dropdown"; const ListeningContext: React.FC<{ sectionId: number; }> = ({ sectionId }) => { @@ -15,50 +15,55 @@ const ListeningContext: React.FC<{ sectionId: number; }> = ({ sectionId }) => { ); const listeningPart = state as ListeningPart; - const [script, setScript] = useState(listeningPart.script); + const [scriptLocal, setScriptLocal] = useState(listeningPart.script); const { editing, handleSave, handleDiscard, modeHandle, setEditing } = useSectionEdit({ sectionId, mode: "edit", onSave: () => { const newState = { ...listeningPart }; - newState.script = script; + newState.script = scriptLocal; dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState } }) setEditing(false); }, onDiscard: () => { - setScript(listeningPart.script); + setScriptLocal(listeningPart.script); }, }); useEffect(() => { if (genResult !== undefined && generating === "context") { setEditing(true); - setScript(genResult[0].script) + setScriptLocal(genResult[0].script); dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "genResult", value: undefined } }) } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [genResult, dispatch, sectionId, setEditing, currentModule]); const renderContent = (editing: boolean) => { - - if (script === undefined && !editing) { - return (

- Generate or import audio to add exercises! -

- ) + if (scriptLocal === undefined) { + return ( + + + Edit, generate or import your own audio. + + + ); } - return ( - - - - - + + + + ); }; diff --git a/src/components/ExamEditor/SectionRenderer/SectionExercises/index.tsx b/src/components/ExamEditor/SectionRenderer/SectionExercises/index.tsx index d635b04d..7f40e22e 100644 --- a/src/components/ExamEditor/SectionRenderer/SectionExercises/index.tsx +++ b/src/components/ExamEditor/SectionRenderer/SectionExercises/index.tsx @@ -1,13 +1,13 @@ import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; import SortableSection from "../../Shared/SortableSection"; import getReadingQuestions from '../SectionExercises/reading'; -import { Exercise, LevelPart, ReadingPart, SpeakingExercise, WritingExercise } from "@/interfaces/exam"; -import { ReadingExercise } from "./types"; +import { Exercise, LevelPart, ListeningPart, ReadingPart, SpeakingExercise, WritingExercise } from "@/interfaces/exam"; +import ExerciseItem, { ReadingExercise } from "./types"; import Dropdown from "@/components/Dropdown"; import useExamEditorStore from "@/stores/examEditor"; import Writing from "../../Exercises/Writing"; import Speaking from "../../Exercises/Speaking"; -import { ReactNode, useEffect, useState } from "react"; +import { ReactElement, ReactNode, useEffect, useState } from "react"; import { DndContext, PointerSensor, @@ -15,15 +15,18 @@ import { useSensors, DragEndEvent, closestCenter, + UniqueIdentifier, } from '@dnd-kit/core'; import GenLoader from "../../Exercises/Shared/GenLoader"; import { ExamPart } from "@/stores/examEditor/types"; import getListeningItems from "./listening"; import getLevelQuestionItems from "./level"; +import React from "react"; -export interface Props { - sectionId: number; +interface QuestionItemsResult { + ids: string[]; + items: ExerciseItem[]; } const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => { @@ -49,7 +52,7 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => { }) dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "genResult", value: undefined } }) } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [genResult, dispatch, sectionId, currentModule]); const currentSection = sections.find((s) => s.sectionId === sectionId)!; @@ -58,27 +61,44 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => { useSensor(PointerSensor), ); - const questionItems = () => { - let ids, items; + const questionItems = (): QuestionItemsResult => { + let result: QuestionItemsResult = { + ids: [], + items: [] + }; + switch (currentModule) { - case "reading": - items = getReadingQuestions((currentSection.state as ReadingPart).exercises as ReadingExercise[], sectionId); - ids = items.map(q => q.id.toString()); + case "reading": { + const items = getReadingQuestions( + (currentSection.state as ReadingPart).exercises as ReadingExercise[], + sectionId + ); + result.items = items.filter((item): item is ExerciseItem => item !== undefined); + result.ids = result.items.map(item => item.id); break; - case "listening": - items = getListeningItems((currentSection.state as ReadingPart).exercises as ReadingExercise[], sectionId); - ids = items.map(q => q?.id.toString()); + } + case "listening": { + const items = getListeningItems( + (currentSection.state as ListeningPart).exercises as Exercise[], + sectionId + ); + result.items = items.filter((item): item is ExerciseItem => item !== undefined); + result.ids = result.items.map(item => item.id); break; - case "level": - items = getLevelQuestionItems((currentSection.state as LevelPart).exercises as Exercise[], sectionId); - ids = items.map(q => q.id.toString()); + } + case "level": { + const items = getLevelQuestionItems( + (currentSection.state as LevelPart).exercises as Exercise[], + sectionId + ); + result.items = items.filter((item): item is ExerciseItem => item !== undefined); + result.ids = result.items.map(item => item.id); break; + } } - return { ids, items } - } - - const questions = questionItems(); + return result; + }; const background = (component: ReactNode) => { return ( @@ -91,6 +111,20 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => { if (currentModule == "writing") return background(); if (currentModule == "speaking") return background(); + const questions = questionItems(); + + const filteredIds = (questions.ids ?? []).filter(Boolean); + + function isValidItem(item: ExerciseItem | undefined): item is ExerciseItem { + return item !== undefined && + typeof item.id === 'string' && + typeof item.sectionId === 'number' && + React.isValidElement(item.label) && + React.isValidElement(item.content); + } + + const filteredItems = (questions.items ?? []).filter(isValidItem); + return ( = ({ sectionId }) => { questions.ids.length > 0 && (
- {questions.items.map(item => ( - + {filteredItems.map(item => ( + item !== undefined); + }).filter(isExerciseItem); - return items || []; + return items; }; diff --git a/src/components/ExamEditor/SectionRenderer/SectionExercises/listening.tsx b/src/components/ExamEditor/SectionRenderer/SectionExercises/listening.tsx index 9420ee73..9b520eca 100644 --- a/src/components/ExamEditor/SectionRenderer/SectionExercises/listening.tsx +++ b/src/components/ExamEditor/SectionRenderer/SectionExercises/listening.tsx @@ -1,4 +1,4 @@ -import ExerciseItem from './types'; +import ExerciseItem, { isExerciseItem } from './types'; import ExerciseLabel from '../../Shared/ExerciseLabel'; import FillBlanksLetters from '../../Exercises/Blanks/Letters'; import { Exercise, WriteBlanksExercise } from '@/interfaces/exam'; @@ -7,15 +7,14 @@ import WriteBlanksForm from '../../Exercises/WriteBlanksForm'; import WriteBlanksFill from '../../Exercises/Blanks/WriteBlankFill'; import WriteBlanks from '../../Exercises/WriteBlanks'; - -const writeBlanks = (exercise: WriteBlanksExercise, index: number, sectionId: number, previewLabel: (text: string) => string) => { +const writeBlanks = (exercise: WriteBlanksExercise, index: number, sectionId: number, previewLabel: (text: string) => string): ExerciseItem => { const firstWordId = exercise.solutions[0].id; const lastWordId = exercise.solutions[exercise.solutions.length - 1].id; switch (exercise.variant) { case 'form': return { - id: index, + id: index.toString(), sectionId, label: ( }; } -} - - -const getListeningItems = (exercises: Exercise[], sectionId: number) => { + throw new Error(`Just so that typescript doesnt complain`); +}; +const getListeningItems = (exercises: Exercise[], sectionId: number): ExerciseItem[] => { const previewLabel = (text: string) => { return text !== undefined ? text.replaceAll('\\n', ' ').split(' ').slice(0, 15).join(' ') : ""; - } + }; - const items = exercises.map((exercise, index) => { + const mappedItems = exercises.map((exercise, index): ExerciseItem | null => { let firstWordId, lastWordId; + switch (exercise.type) { case "fillBlanks": firstWordId = exercise.solutions[0].id; lastWordId = exercise.solutions[exercise.solutions.length - 1].id; return { - id: index, + id: index.toString(), sectionId, label: ( { /> ), content: - }; + case "writeBlanks": return writeBlanks(exercise, index, sectionId, previewLabel); + case "multipleChoice": firstWordId = exercise.questions[0].id; lastWordId = exercise.questions[exercise.questions.length - 1].id; return { - id: index, + id: index.toString(), sectionId, label: ( { ), content: }; - } - }).filter((item) => item !== undefined); - return items || []; + default: + return null; + } + }); + + return mappedItems.filter((item): item is ExerciseItem => + item !== null && isExerciseItem(item) + ); }; - -export default getListeningItems; +export default getListeningItems; \ No newline at end of file diff --git a/src/components/ExamEditor/SectionRenderer/SectionExercises/reading.tsx b/src/components/ExamEditor/SectionRenderer/SectionExercises/reading.tsx index 3264fb85..7b31d770 100644 --- a/src/components/ExamEditor/SectionRenderer/SectionExercises/reading.tsx +++ b/src/components/ExamEditor/SectionRenderer/SectionExercises/reading.tsx @@ -1,4 +1,4 @@ -import ExerciseItem, { ReadingExercise } from './types'; +import ExerciseItem, { isExerciseItem, ReadingExercise } from './types'; import WriteBlanks from "@/editor/Exercises/WriteBlanks"; import ExerciseLabel from '../../Shared/ExerciseLabel'; import MatchSentences from '../../Exercises/MatchSentences'; @@ -19,7 +19,7 @@ const getExerciseItems = (exercises: ReadingExercise[], sectionId: number): Exer firstWordId = exercise.solutions[0].id; lastWordId = exercise.solutions[exercise.solutions.length - 1].id; return { - id: index, + id: index.toString(), sectionId, label: ( item !== undefined); + }).filter(isExerciseItem); - return items || []; + return items; }; diff --git a/src/components/ExamEditor/SectionRenderer/SectionExercises/types.ts b/src/components/ExamEditor/SectionRenderer/SectionExercises/types.ts index bea1509f..0a06164b 100644 --- a/src/components/ExamEditor/SectionRenderer/SectionExercises/types.ts +++ b/src/components/ExamEditor/SectionRenderer/SectionExercises/types.ts @@ -1,10 +1,19 @@ import { FillBlanksExercise, MatchSentencesExercise, TrueFalseExercise, WriteBlanksExercise } from "@/interfaces/exam"; export default interface ExerciseItem { - id: number; + id: string; sectionId: number; label: React.ReactNode; content: React.ReactNode; } export type ReadingExercise = FillBlanksExercise | TrueFalseExercise | MatchSentencesExercise | WriteBlanksExercise; + +export function isExerciseItem(item: unknown): item is ExerciseItem { + return item !== undefined && + item !== null && + typeof (item as ExerciseItem).id === 'string' && + typeof (item as ExerciseItem).sectionId === 'number' && + (item as ExerciseItem).label !== undefined && + (item as ExerciseItem).content !== undefined; +} diff --git a/src/components/ExamEditor/Shared/ExercisePicker/index.tsx b/src/components/ExamEditor/Shared/ExercisePicker/index.tsx index 46b11cdf..33e3cb8e 100644 --- a/src/components/ExamEditor/Shared/ExercisePicker/index.tsx +++ b/src/components/ExamEditor/Shared/ExercisePicker/index.tsx @@ -7,7 +7,7 @@ import ExerciseWizard, { ExerciseConfig } from "./ExerciseWizard"; import { generate } from "../../SettingsEditor/Shared/Generate"; import { Module } from "@/interfaces"; import useExamEditorStore from "@/stores/examEditor"; -import { Dialog, ListeningPart, ReadingPart } from "@/interfaces/exam"; +import { ListeningPart, Message, ReadingPart } from "@/interfaces/exam"; interface ExercisePickerProps { module: string; @@ -81,7 +81,7 @@ const ExercisePicker: React.FC = ({ switch (sectionId) { case 1: case 3: - dialog = script as Dialog[]; + dialog = script as Message[]; text = dialog.map((d) => `${d.name}: ${d.text}`).join("\n"); context = { text: text } break; diff --git a/src/components/ExamEditor/Shared/ExercisePicker/templates.ts b/src/components/ExamEditor/Shared/ExercisePicker/templates.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/components/Low/AutoExpandingTextarea.tsx b/src/components/Low/AutoExpandingTextarea.tsx index 5614049e..39877246 100755 --- a/src/components/Low/AutoExpandingTextarea.tsx +++ b/src/components/Low/AutoExpandingTextarea.tsx @@ -5,6 +5,7 @@ interface Props { onChange: (value: string) => void; className?: string; placeholder?: string; + disabled?: boolean; onBlur?: () => void; } @@ -12,6 +13,7 @@ const AutoExpandingTextArea: React.FC = ({ value, className = 'w-full cursor-text px-7 py-8 input border-2 border-mti-gray-platinum bg-white rounded-3xl', placeholder = "Enter text here...", + disabled = false, onChange, onBlur, }) => { @@ -45,6 +47,7 @@ const AutoExpandingTextArea: React.FC = ({ placeholder={placeholder} style={{ overflow: 'hidden', resize: 'none' }} onBlur={onBlur} + disabled={disabled} autoFocus /> ); diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts index 77846f21..0a5e3df9 100644 --- a/src/interfaces/exam.ts +++ b/src/interfaces/exam.ts @@ -62,8 +62,8 @@ export interface ListeningExam extends ExamBase { module: "listening"; } -export type Dialog = { "name": string; "gender": string; "text": string; }; -export type Script = Dialog[] | string; +export type Message = { name: string; gender: string; text: string; voice?: string; }; +export type Script = Message[] | string; export interface ListeningPart extends Section { audio?: {