Merged in feature/ExamGenRework (pull request #110)
isPractice hotfix Approved-by: Tiago Ribeiro
This commit is contained in:
@@ -351,7 +351,19 @@ const EXERCISES: ExerciseGen[] = [
|
||||
type: "speaking_1",
|
||||
icon: FaComments,
|
||||
extra: [
|
||||
generate()
|
||||
generate(),
|
||||
{
|
||||
label: "First Topic",
|
||||
param: "first_topic",
|
||||
value: "",
|
||||
type: "text"
|
||||
},
|
||||
{
|
||||
label: "Second Topic",
|
||||
param: "second_topic",
|
||||
value: "",
|
||||
type: "text"
|
||||
},
|
||||
],
|
||||
module: "speaking"
|
||||
},
|
||||
@@ -360,7 +372,13 @@ const EXERCISES: ExerciseGen[] = [
|
||||
type: "speaking_2",
|
||||
icon: FaUserFriends,
|
||||
extra: [
|
||||
generate()
|
||||
generate(),
|
||||
{
|
||||
label: "Topic",
|
||||
param: "topic",
|
||||
value: "",
|
||||
type: "text"
|
||||
},
|
||||
],
|
||||
module: "speaking"
|
||||
},
|
||||
@@ -369,7 +387,13 @@ const EXERCISES: ExerciseGen[] = [
|
||||
type: "speaking_3",
|
||||
icon: FaHandshake,
|
||||
extra: [
|
||||
generate()
|
||||
generate(),
|
||||
{
|
||||
label: "Topic",
|
||||
param: "topic",
|
||||
value: "",
|
||||
type: "text"
|
||||
},
|
||||
],
|
||||
module: "speaking"
|
||||
},
|
||||
|
||||
@@ -112,6 +112,7 @@ const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: num
|
||||
newState.exercises = newState.exercises.map((ex) =>
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -108,6 +108,7 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }
|
||||
newState.exercises = newState.exercises.map((ex) =>
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
}
|
||||
});
|
||||
@@ -200,7 +201,7 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }
|
||||
|
||||
useEffect(() => {
|
||||
validateBlanks(blanksState.blanks, answers, alerts, setAlerts);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [answers, blanksState.blanks, blanksState.textMode]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -226,16 +227,16 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }
|
||||
solution
|
||||
])
|
||||
));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleBlankRemove = (blankId: number) => {
|
||||
if (!editing) setEditing(true);
|
||||
|
||||
|
||||
const newAnswers = new Map(answers);
|
||||
newAnswers.delete(blankId.toString());
|
||||
setAnswers(newAnswers);
|
||||
|
||||
|
||||
setLocal(prev => ({
|
||||
...prev,
|
||||
words: (prev.words as FillBlanksMCOption[]).filter(w => w.id !== blankId.toString()),
|
||||
@@ -244,7 +245,7 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }
|
||||
solution
|
||||
}))
|
||||
}));
|
||||
|
||||
|
||||
blanksDispatcher({ type: "REMOVE_BLANK", payload: blankId });
|
||||
};
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ const WriteBlanksFill: React.FC<{ exercise: WriteBlanksExercise; sectionId: numb
|
||||
newState.exercises = newState.exercises.map((ex) =>
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -76,6 +76,7 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu
|
||||
newState.exercises = newState.exercises.map((ex) =>
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -107,6 +107,7 @@ const UnderlineMultipleChoice: React.FC<{exercise: MultipleChoiceExercise, secti
|
||||
newState.exercises = newState.exercises.map((ex) =>
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -186,6 +186,7 @@ const MultipleChoice: React.FC<MultipleChoiceProps> = ({ exercise, sectionId, op
|
||||
newState.exercises = newState.exercises.map((ex) =>
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -55,6 +55,7 @@ const InteractiveSpeaking: React.FC<Props> = ({ sectionId, exercise, module = "s
|
||||
...state,
|
||||
isPractice: !local.isPractice
|
||||
};
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: updatedExercise, module: module } });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -67,6 +67,7 @@ const Speaking1: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
|
||||
...state,
|
||||
isPractice: !local.isPractice
|
||||
};
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: updatedExercise, module: module } });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -54,6 +54,7 @@ const Speaking2: React.FC<Props> = ({ sectionId, exercise, module = "speaking" }
|
||||
...state,
|
||||
isPractice: !local.isPractice
|
||||
};
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: updatedExercise, module: module } });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,21 +3,30 @@ import { SpeakingExercise, InteractiveSpeakingExercise } from "@/interfaces/exam
|
||||
import Speaking2 from "./Speaking2";
|
||||
import InteractiveSpeaking from "./InteractiveSpeaking";
|
||||
import Speaking1 from "./Speaking1";
|
||||
import { Module } from "@/interfaces";
|
||||
|
||||
interface Props {
|
||||
sectionId: number;
|
||||
exercise: SpeakingExercise | InteractiveSpeakingExercise;
|
||||
qId?: number;
|
||||
module: Module;
|
||||
}
|
||||
|
||||
const Speaking: React.FC<Props> = ({ sectionId, exercise }) => {
|
||||
const { currentModule } = useExamEditorStore();
|
||||
const Speaking: React.FC<Props> = ({ sectionId, exercise, qId, module = "speaking" }) => {
|
||||
const { dispatch } = useExamEditorStore();
|
||||
const { state } = useExamEditorStore(
|
||||
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
|
||||
(state) => state.modules[module].sections.find((section) => section.sectionId === sectionId)!
|
||||
);
|
||||
|
||||
const onFocus = () => {
|
||||
if (qId) {
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { module, sectionId, field: "focusedExercise", value: { questionId: qId, id: exercise.id } } })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto p-3 space-y-6">
|
||||
<div tabIndex={0} className="mx-auto p-3 space-y-6" onFocus={onFocus}>
|
||||
<div className="p-4">
|
||||
<div className="flex flex-col space-y-6">
|
||||
{sectionId === 1 && <Speaking1 sectionId={sectionId} exercise={state as InteractiveSpeakingExercise} />}
|
||||
|
||||
@@ -107,12 +107,13 @@ const TrueFalse: React.FC<{ exercise: TrueFalseExercise, sectionId: number }> =
|
||||
onPractice: () => {
|
||||
const updatedExercise = {
|
||||
...local,
|
||||
isPractice: !local.isPractice
|
||||
isPractice: false
|
||||
};
|
||||
const newState = { ...section };
|
||||
newState.exercises = newState.exercises.map((ex) =>
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
updateLocal({...local, isPractice: !local.isPractice})
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
}
|
||||
});
|
||||
@@ -140,6 +141,7 @@ const TrueFalse: React.FC<{ exercise: TrueFalseExercise, sectionId: number }> =
|
||||
handleDelete={handleDelete}
|
||||
handleDiscard={handleDiscard}
|
||||
handlePractice={handlePractice}
|
||||
isEvaluationEnabled={!local.isPractice}
|
||||
/>
|
||||
{alerts.length > 0 && <Alert className="mb-6" alerts={alerts} />}
|
||||
<PromptEdit
|
||||
|
||||
@@ -85,6 +85,7 @@ const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise;
|
||||
newState.exercises = newState.exercises.map((ex) =>
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -71,6 +71,7 @@ const WriteBlanksForm: React.FC<{ sectionId: number; exercise: WriteBlanksExerci
|
||||
newState.exercises = newState.exercises.map((ex) =>
|
||||
ex.id === exercise.id ? updatedExercise : ex
|
||||
);
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -68,6 +68,7 @@ const Writing: React.FC<Props> = ({ sectionId, exercise, module, index }) => {
|
||||
...state,
|
||||
isPractice: !local.isPractice
|
||||
};
|
||||
setLocal((prev) => ({...prev, isPractice: !local.isPractice}))
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import TrueFalse from "../../Exercises/TrueFalse";
|
||||
import fillBlanks from "./fillBlanks";
|
||||
import MatchSentences from "../../Exercises/MatchSentences";
|
||||
import Writing from "../../Exercises/Writing";
|
||||
import Speaking from "../../Exercises/Speaking";
|
||||
|
||||
const getExerciseItems = (exercises: Exercise[], sectionId: number): ExerciseItem[] => {
|
||||
const items: ExerciseItem[] = exercises.map((exercise, index) => {
|
||||
@@ -78,6 +79,34 @@ const getExerciseItems = (exercises: Exercise[], sectionId: number): ExerciseIte
|
||||
),
|
||||
content: <Writing key={exercise.id} exercise={exercise} sectionId={sectionId} index={index} module="level" />
|
||||
};
|
||||
case "speaking":
|
||||
return {
|
||||
id: index.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
type={`Speaking Section 2`}
|
||||
firstId={exercise.sectionId!.toString()}
|
||||
lastId={exercise.sectionId!.toString()}
|
||||
prompt={exercise.prompts[2]}
|
||||
/>
|
||||
),
|
||||
content: <Speaking key={exercise.id} exercise={exercise} sectionId={sectionId} qId={index} module="level" />
|
||||
};
|
||||
case "interactiveSpeaking":
|
||||
return {
|
||||
id: index.toString(),
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
type={`Speaking Section 2`}
|
||||
firstId={exercise.sectionId!.toString()}
|
||||
lastId={exercise.sectionId!.toString()}
|
||||
prompt={exercise.prompts[2].text}
|
||||
/>
|
||||
),
|
||||
content: <Speaking key={exercise.id} exercise={exercise} sectionId={sectionId} qId={index} module="level" />
|
||||
};
|
||||
default:
|
||||
return {} as unknown as ExerciseItem;
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
|
||||
}
|
||||
|
||||
if (currentModule == "writing") return background(<Writing sectionId={sectionId} exercise={currentSection.state as WritingExercise} module="writing" />);
|
||||
if (currentModule == "speaking") return background(<Speaking sectionId={sectionId} exercise={currentSection.state as SpeakingExercise} />);
|
||||
if (currentModule == "speaking") return background(<Speaking sectionId={sectionId} exercise={currentSection.state as SpeakingExercise} module="speaking" />);
|
||||
|
||||
const questions = questionItems();
|
||||
|
||||
|
||||
@@ -18,12 +18,12 @@ const SectionPicker: React.FC<Props> = ({
|
||||
updateLocalAndScheduleGlobal
|
||||
}) => {
|
||||
const { dispatch } = useExamEditorStore();
|
||||
const [selectedValue, setSelectedValue] = React.useState<number | null>(null);
|
||||
const [selectedValue, setSelectedValue] = React.useState<number | undefined>(undefined);
|
||||
|
||||
const sectionState = useExamEditorStore(state =>
|
||||
state.modules["level"].sections.find((s) => s.sectionId === sectionId)
|
||||
);
|
||||
|
||||
|
||||
if (sectionState === undefined) return null;
|
||||
|
||||
const { readingSection, listeningSection } = sectionState;
|
||||
@@ -32,14 +32,16 @@ const SectionPicker: React.FC<Props> = ({
|
||||
const openPicker = module === "reading" ? "isReadingPickerOpen" : "isListeningPickerOpen";
|
||||
|
||||
const handleSectionChange = (value: number) => {
|
||||
setSelectedValue(value);
|
||||
const newValue = currentValue === value ? undefined : value;
|
||||
setSelectedValue(newValue);
|
||||
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||
payload: {
|
||||
sectionId,
|
||||
module: "level",
|
||||
field: module === "reading" ? "readingSection" : "listeningSection",
|
||||
value: value
|
||||
value: newValue
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -67,17 +69,19 @@ const SectionPicker: React.FC<Props> = ({
|
||||
className={`
|
||||
flex items-center space-x-3 font-semibold cursor-pointer p-2 rounded
|
||||
transition-colors duration-200
|
||||
${currentValue === num
|
||||
${currentValue === num
|
||||
? `bg-ielts-${module}/90 text-white`
|
||||
: `hover:bg-ielts-${module}/70 text-gray-700`}
|
||||
`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleSectionChange(num);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={`${module === "reading" ? 'passage' : 'section'}-${sectionId}`}
|
||||
value={num}
|
||||
type="checkbox"
|
||||
checked={currentValue === num}
|
||||
onChange={() => handleSectionChange(num)}
|
||||
onChange={() => {}}
|
||||
className={`
|
||||
h-5 w-5 cursor-pointer
|
||||
accent-ielts-${module}
|
||||
@@ -95,4 +99,4 @@ const SectionPicker: React.FC<Props> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionPicker;
|
||||
export default SectionPicker;
|
||||
|
||||
@@ -114,7 +114,7 @@ const LevelSettings: React.FC = () => {
|
||||
openDetachedTab("popout?type=Exam&module=level", router)
|
||||
}
|
||||
|
||||
const speakingExercise = focusedExercise === undefined ? undefined : currentSection.exercises[focusedExercise] as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
const speakingExercise = focusedExercise === undefined ? undefined : currentSection.exercises.find((ex) => ex.id === focusedExercise.id) as SpeakingExercise | InteractiveSpeakingExercise;
|
||||
|
||||
return (
|
||||
<SettingsEditor
|
||||
@@ -249,7 +249,7 @@ const LevelSettings: React.FC = () => {
|
||||
</Dropdown>
|
||||
|
||||
{speakingExercise !== undefined &&
|
||||
<Dropdown title="Configure Speaking Exercise" className={
|
||||
<Dropdown title={`Configure Speaking Exercise ${focusedExercise?.questionId}`} className={
|
||||
clsx(
|
||||
"w-full font-semibold flex justify-between items-center p-4 bg-gradient-to-r border",
|
||||
"bg-ielts-speaking/70 border-ielts-speaking hover:bg-ielts-speaking",
|
||||
|
||||
@@ -136,6 +136,7 @@ export const defaultSectionSettings = (module: Module, sectionId: number, part?:
|
||||
state: part !== undefined ? part : defaultSection(module, sectionId),
|
||||
generating: undefined,
|
||||
genResult: undefined,
|
||||
focusedExercise: undefined,
|
||||
expandedSubSections: [],
|
||||
levelGenerating: [],
|
||||
levelGenResults: []
|
||||
@@ -143,7 +144,7 @@ export const defaultSectionSettings = (module: Module, sectionId: number, part?:
|
||||
}
|
||||
|
||||
|
||||
const defaultModuleSettings = (module: Module, minTimer: number, states?: SectionState[]): ModuleState => {
|
||||
const defaultModuleSettings = (module: Module, minTimer: number): ModuleState => {
|
||||
const state: ModuleState = {
|
||||
examLabel: defaultExamLabel(module),
|
||||
minTimer,
|
||||
|
||||
@@ -92,7 +92,7 @@ export interface SectionState {
|
||||
genResult: {generating: string, result: Record<string, any>[], module: Module} | undefined;
|
||||
levelGenerating: Generating[];
|
||||
levelGenResults: {generating: string, result: Record<string, any>[], module: Module}[];
|
||||
focusedExercise?: number;
|
||||
focusedExercise?: {questionId: number; id: string} | undefined;
|
||||
writingSection?: number;
|
||||
speakingSection?: number;
|
||||
readingSection?: number;
|
||||
|
||||
Reference in New Issue
Block a user