Listening preview and some more patches
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AlertItem } from "../../Shared/Alert";
|
||||
import { BlankState } from "../BlanksReducer";
|
||||
|
||||
|
||||
export const validateWriteBlanks = (
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
49
src/components/ExamEditor/Exercises/Shared/PromptEdit.tsx
Normal file
49
src/components/ExamEditor/Exercises/Shared/PromptEdit.tsx
Normal 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;
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user