import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; import SortableSection from "../../Shared/SortableSection"; import { Difficulty, Exercise, InteractiveSpeakingExercise, LevelPart, ListeningPart, ReadingPart, SpeakingExercise, WritingExercise } from "@/interfaces/exam"; import ExerciseItem 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 } from "react"; import { DndContext, PointerSensor, useSensor, useSensors, closestCenter, } from '@dnd-kit/core'; import GenLoader from "../../Exercises/Shared/GenLoader"; import { ExamPart, Generating } from "@/stores/examEditor/types"; import React from "react"; import getExerciseItems from "./exercises"; import { Action } from "@/stores/examEditor/reducers"; import { writingTask } from "@/stores/examEditor/sections"; import { createSpeakingExercise } from "./speaking"; interface QuestionItemsResult { ids: string[]; items: ExerciseItem[]; } const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => { const dispatch = useExamEditorStore(state => state.dispatch); const currentModule = useExamEditorStore(state => state.currentModule); const {sections, expandedSections, difficulty} = useExamEditorStore(state => state.modules[currentModule]); const section = useExamEditorStore( state => state.modules[currentModule].sections.find( section => section.sectionId === sectionId ) ); const genResult = section?.genResult; const generating = section?.generating; const levelGenResults = section?.levelGenResults; const levelGenerating = section?.levelGenerating; const sectionState = section?.state; useEffect(() => { if (genResult && genResult.generating === "exercises" && genResult.module === currentModule) { const newExercises = genResult.result[0].exercises; const newDifficulties = newExercises .map((ex: Exercise) => ex.difficulty) .filter((diff: Difficulty) => !difficulty.includes(diff)); if (newDifficulties.length > 0) { dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, ...newDifficulties]} } }); } dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, module: genResult.module, update: { exercises: [...(sectionState as ExamPart).exercises, ...newExercises] } } }) dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "generating", value: undefined } }) dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "genResult", value: undefined } }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [genResult, dispatch, sectionId, currentModule]); const handleExerciseGen = ( results: any[], assignExercisesFn: (results: any[]) => any[], { sectionId, currentModule, sectionState, levelGenerating, levelGenResults }: { sectionId: number; currentModule: string; sectionState: ExamPart; levelGenerating?: Generating[]; levelGenResults: any[]; } ) => { const nonWritingOrSpeaking = results[0]?.generating.startsWith("exercises"); const newExercises = assignExercisesFn(results); const newDifficulties = newExercises .map((ex: Exercise) => ex.difficulty) .filter((diff: Difficulty | undefined): diff is Difficulty => diff !== undefined && !difficulty.includes(diff) ); if (newDifficulties.length > 0) { dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, ...newDifficulties]} } }); } const updates = [ { type: "UPDATE_SECTION_STATE", payload: { sectionId, module: "level", update: { exercises: [ ...sectionState.exercises, ...newExercises ] } } }, { type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "levelGenerating", value: levelGenerating?.filter(g => nonWritingOrSpeaking ? !g?.startsWith("exercises") : !results.flatMap(res => res.generating as Generating).includes(g) ) } }, { type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "levelGenResults", value: levelGenResults.filter(res => nonWritingOrSpeaking ? !res.generating.startsWith("exercises") : !results.flatMap(res => res.generating as Generating).includes(res.generating) ) } } ] as Action[]; updates.forEach(update => dispatch(update)); }; useEffect(() => { if (levelGenResults && levelGenResults?.some(res => res.generating.startsWith("exercises"))) { const results = levelGenResults.filter(res => res.generating.startsWith("exercises") ); const assignExercises = (results: any[]) => results .map(res => res.result[0].exercises) .flat(); handleExerciseGen( results, assignExercises, { sectionId, currentModule, sectionState: sectionState as ExamPart, levelGenerating, levelGenResults } ); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [levelGenResults, sectionState, levelGenerating, sectionId, currentModule]); useEffect(() => { if (levelGenResults && levelGenResults?.some(res => res.generating === "writing_letter" || res.generating === "writing_2" )) { const results = levelGenResults.filter(res => res.generating === "writing_letter" || res.generating === "writing_2" ); const assignExercises = (results: any[]) => results.map(res => ({ ...writingTask(res.generating === "writing_letter" ? 1 : 2), prompt: res.result[0].prompt, difficulty: res.result[0].difficulty, variant: res.generating === "writing_letter" ? "letter" : "essay" }) as WritingExercise); handleExerciseGen( results, assignExercises, { sectionId, currentModule, sectionState: sectionState as ExamPart, levelGenerating, levelGenResults } ); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [levelGenResults, sectionState, levelGenerating, sectionId, currentModule]); useEffect(() => { if (levelGenResults && levelGenResults?.some(res => res.generating.startsWith("speaking"))) { const results = levelGenResults.filter(res => res.generating.startsWith("speaking") ); const assignExercises = (results: any[]) => results.map(createSpeakingExercise); handleExerciseGen( results, assignExercises, { sectionId, currentModule, sectionState: sectionState as ExamPart, levelGenerating, levelGenResults } ); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [levelGenResults, sectionState, levelGenerating, sectionId, currentModule]); const currentSection = sections.find((s) => s.sectionId === sectionId)!; const sensors = useSensors( useSensor(PointerSensor), ); const questionItems = (): QuestionItemsResult => { const part = currentSection.state as ReadingPart | ListeningPart | LevelPart; const items = getExerciseItems(part.exercises, sectionId); return { items, ids: items.map(item => item.id) } }; const background = (component: ReactNode) => { return (
{component}
); } if (currentModule == "writing") return background(); if (currentModule == "speaking") return background(); const questions = questionItems(); // ############################################################################# // Typescript checks so that the compiler and builder don't freak out 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); // ############################################################################# const onFocus = (questionId: string, id: string | undefined) => { if (id) { dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { module: currentModule, sectionId, field: "focusedExercise", value: { questionId, id } } }) } } return ( dispatch({ type: "REORDER_EXERCISES", payload: { event: e, sectionId, module: currentModule } })} > {expandedSections.includes(sectionId) && questions.items && questions.items.length > 0 && questions.ids && questions.ids.length > 0 && (
{filteredItems.map(item => (
onFocus(item.id, item.exerciseId)}> {item.content}
))}
) } {generating === "exercises" && } {currentModule === "level" && ( <> { questions.ids?.length === 0 && !levelGenerating?.some((g) => g?.startsWith("exercises") || g?.startsWith("writing") || g?.startsWith("speaking")) && generating !== "exercises" && background(Generated exercises will appear here!)} {levelGenerating?.some((g) => g?.startsWith("exercises") || g?.startsWith("writing") || g?.startsWith("speaking")) && } ) }
); } export default SectionExercises;