Listening preview and some more patches

This commit is contained in:
Carlos-Mesquita
2024-11-06 09:23:34 +00:00
parent ffa2045a2d
commit b50913eda8
24 changed files with 467 additions and 253 deletions

View File

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

View File

@@ -1,5 +1,4 @@
import { AlertItem } from "../../Shared/Alert";
import { BlankState } from "../BlanksReducer";
export const validateWriteBlanks = (

View File

@@ -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<Props> = ({
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));

View File

@@ -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<AlertItem[]>([]);
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
<div className="space-y-4">
{alerts.length > 0 && <Alert alerts={alerts} />}
<PromptEdit
value={local.prompt}
onChange={(text) => updateLocal({ ...local, prompt: text })}
/>
<QuestionsList
ids={local.sentences.map(s => s.id)}
handleDragEnd={handleDragEnd}

View File

@@ -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<Props> = ({ value, onChange }) => {
const [editing, setEditing] = useState(false);
return (
<>
<Card className="mb-6">
<CardContent className="p-4">
<div className="flex justify-between items-start gap-4">
{editing ? (
<AutoExpandingTextArea
className="flex-1 p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none min-h-[100px]"
value={value}
onChange={(text) => onChange(text)}
onBlur={()=> setEditing(false)}
/>
) : (
<div className="flex-1">
<h3 className="font-medium text-gray-800 mb-2">Question/Instructions displayed to the student:</h3>
<p className="text-gray-600">{value}</p>
</div>
)}
<button
onClick={() => setEditing(!editing)}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
{editing ?
<MdEditOff size={20} className="text-gray-500" /> :
<MdEdit size={20} className="text-gray-500" />
}
</button>
</div>
</CardContent>
</Card>
</>
);
}
export default PromptEdit;

View File

@@ -24,11 +24,20 @@ const Speaking: React.FC<Props> = ({ 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<Props> = ({ sectionId, exercise }) => {
onSave: () => {
let newExercise;
if (sectionId === 1) {
newExercise = {
newExercise = {
...local,
prompts: questions
} as SpeakingExercise;
@@ -53,7 +62,6 @@ const Speaking: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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
}
});
}

View File

@@ -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<AlertItem[]>([]);
@@ -134,36 +131,10 @@ const TrueFalse: React.FC<{ exercise: TrueFalseExercise, sectionId: number }> =
handleDiscard={handleDiscard}
/>
{alerts.length > 0 && <Alert className="mb-6" alerts={alerts} />}
<Card className="mb-6">
<CardContent className="p-4">
<div className="flex justify-between items-start gap-4">
{editingPrompt ? (
<textarea
className="flex-1 p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none min-h-[100px]"
value={local.prompt}
onChange={(e) => updateLocal({ ...local, prompt: e.target.value })}
onBlur={() => setEditingPrompt(false)}
autoFocus
/>
) : (
<div className="flex-1">
<h3 className="font-medium text-gray-800 mb-2">Question/Instructions displayed to the student:</h3>
<p className="text-gray-600">{local.prompt}</p>
</div>
)}
<button
onClick={() => setEditingPrompt(!editingPrompt)}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
{editingPrompt ?
<MdEditOff size={20} className="text-gray-500" /> :
<MdEdit size={20} className="text-gray-500" />
}
</button>
</div>
</CardContent>
</Card>
<PromptEdit
value={local.prompt}
onChange={(text) => updateLocal({ ...local, prompt: text })}
/>
<div className="space-y-4">
<QuestionsList
ids={local.questions.map(q => q.id)}

View File

@@ -23,7 +23,7 @@ import { handleWriteBlanksReorder } from '@/stores/examEditor/reorder/local';
import { ParsedQuestion, parseText, reconstructText } from './parsing';
const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise }> = ({ sectionId, exercise }) => {
const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise; title?: string; }> = ({ sectionId, exercise, title = "Write Blanks Exercise" }) => {
const { currentModule, dispatch } = useExamEditorStore();
const { state } = useExamEditorStore(
@@ -198,7 +198,6 @@ const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise }
const handleDragEnd = (event: DragEndEvent) => {
setEditing(true);
console.log("ASOJNFOAI+SHJOIPFAS");
setLocal(handleWriteBlanksReorder(event, local));
}
@@ -223,7 +222,7 @@ const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise }
return (
<div className="p-4">
<Header
title="Write Blanks Exercise"
title={title}
description="Edit questions and their solutions"
editing={editing}
handleSave={handleSave}

View File

@@ -15,15 +15,13 @@ interface Props {
exercise: WritingExercise;
}
const Writing: React.FC<Props> = ({ sectionId }) => {
const Writing: React.FC<Props> = ({ sectionId, exercise }) => {
const { currentModule, dispatch } = useExamEditorStore();
const { edit } = useExamEditorStore((store) => store.modules[currentModule]);
const { generating, genResult, state } = useExamEditorStore(
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
);
const exercise = state as WritingExercise;
const [local, setLocal] = useState(exercise);
const [prompt, setPrompt] = useState(exercise.prompt);
const [loading, setLoading] = useState(generating && generating == "exercises");
@@ -43,7 +41,9 @@ const Writing: React.FC<Props> = ({ sectionId }) => {
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId: sectionId, update: newExercise } });
},
onDiscard: () => {
setEditing(false);
setLocal(exercise);
setPrompt(exercise.prompt);
},
});
@@ -67,10 +67,6 @@ const Writing: React.FC<Props> = ({ sectionId }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [genResult, dispatch, sectionId, setEditing, currentModule]);
useEffect(() => {
setLocal(state as WritingExercise);
}, [state]);
useEffect(() => {
setEditingAlert(prompt !== local.prompt, setAlerts);
}, [prompt, local.prompt]);