Exam generation rework, batch user tables, fastapi endpoint switch
This commit is contained in:
61
src/components/ExamEditor/SettingsEditor/Shared/Generate.ts
Normal file
61
src/components/ExamEditor/SettingsEditor/Shared/Generate.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import axios from "axios";
|
||||
import { playSound } from "@/utils/sound";
|
||||
import { toast } from "react-toastify";
|
||||
import { Generating } from "@/stores/examEditor/types";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import { Module } from "@/interfaces";
|
||||
|
||||
interface GeneratorConfig {
|
||||
method: 'GET' | 'POST';
|
||||
queryParams?: Record<string, string>;
|
||||
body?: Record<string, any>;
|
||||
}
|
||||
|
||||
export function generate(
|
||||
sectionId: number,
|
||||
module: Module,
|
||||
type: "context" | "exercises",
|
||||
config: GeneratorConfig,
|
||||
mapData: (data: any) => Record<string, any>[]
|
||||
) {
|
||||
const dispatch = useExamEditorStore.getState().dispatch;
|
||||
|
||||
const setGenerating = (sectionId: number, generating: Generating) => {
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||
payload: { sectionId, module, field: "generating", value: generating }
|
||||
});
|
||||
};
|
||||
|
||||
const setGeneratedExercises = (sectionId: number, exercises: Record<string, any>[] | undefined) => {
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_SINGLE_FIELD",
|
||||
payload: { sectionId, module, field: "genResult", value: exercises }
|
||||
});
|
||||
};
|
||||
|
||||
setGenerating(sectionId, type);
|
||||
|
||||
const queryString = config.queryParams
|
||||
? new URLSearchParams(config.queryParams).toString()
|
||||
: '';
|
||||
|
||||
const url = `/api/exam/generate/${module}/${sectionId}${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const request = config.method === 'POST'
|
||||
? axios.post(url, config.body)
|
||||
: axios.get(url);
|
||||
|
||||
request
|
||||
.then((result) => {
|
||||
playSound("check");
|
||||
setGeneratedExercises(sectionId, mapData(result.data));
|
||||
})
|
||||
.catch((error) => {
|
||||
playSound("error");
|
||||
toast.error("Something went wrong! Try to generate again.");
|
||||
})
|
||||
.finally(() => {
|
||||
setGenerating(sectionId, undefined);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Module } from "@/interfaces";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import { Generating } from "@/stores/examEditor/types";
|
||||
import clsx from "clsx";
|
||||
import { BsArrowRepeat } from "react-icons/bs";
|
||||
import { GiBrain } from "react-icons/gi";
|
||||
|
||||
interface Props {
|
||||
module: Module;
|
||||
sectionId: number;
|
||||
genType: Generating;
|
||||
generateFnc: (sectionId: number) => void
|
||||
}
|
||||
|
||||
const GenerateBtn: React.FC<Props> = ({module, sectionId, genType, generateFnc}) => {
|
||||
const {generating} = useExamEditorStore((store) => store.modules[module].sections.find((s)=> s.sectionId == sectionId))!;
|
||||
const loading = generating && generating === genType;
|
||||
return (
|
||||
<button
|
||||
key={`section-${sectionId}`}
|
||||
className={clsx(
|
||||
"flex items-center w-[140px] justify-center px-4 py-2 text-white rounded-xl transition-colors duration-300 text-lg",
|
||||
`bg-ielts-${module}/70 border border-ielts-${module} hover:bg-ielts-${module}`
|
||||
)}
|
||||
onClick={loading ? () => { } : () => generateFnc(sectionId)}
|
||||
>
|
||||
{loading ? (
|
||||
<div key={`section-${sectionId}`} className="flex items-center justify-center">
|
||||
<BsArrowRepeat className="text-white animate-spin" size={25} />
|
||||
</div>
|
||||
) : (
|
||||
<div key={`section-${sectionId}`} className="flex flex-row">
|
||||
<GiBrain className="mr-2" size={24} />
|
||||
<span>Generate</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default GenerateBtn;
|
||||
@@ -0,0 +1,33 @@
|
||||
import Dropdown from "@/components/Dropdown";
|
||||
import clsx from "clsx";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
module: string;
|
||||
title: string;
|
||||
open: boolean;
|
||||
disabled?: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const SettingsDropdown: React.FC<Props> = ({ module, title, open, setIsOpen, children, disabled = false }) => {
|
||||
return (
|
||||
<Dropdown
|
||||
title={title}
|
||||
className={clsx(
|
||||
"w-full font-semibold flex justify-between items-center p-4 bg-gradient-to-r border text-white shadow-md transition-all duration-300 disabled:cursor-not-allowed",
|
||||
`bg-ielts-${module}/70 border border-ielts-${module} hover:bg-ielts-${module} disabled:bg-ielts-${module}/30`,
|
||||
open ? "rounded-t-lg" : "rounded-lg"
|
||||
)}
|
||||
contentWrapperClassName="pt-6 px-2 bg-white rounded-b-lg shadow-md transition-all duration-300 ease-in-out"
|
||||
open={open}
|
||||
setIsOpen={setIsOpen}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsDropdown;
|
||||
140
src/components/ExamEditor/SettingsEditor/index.tsx
Normal file
140
src/components/ExamEditor/SettingsEditor/index.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React, { ReactNode, useCallback, useEffect, useMemo, useState, useRef } from "react";
|
||||
import { FaEye } from "react-icons/fa";
|
||||
import clsx from "clsx";
|
||||
import Select from "@/components/Low/Select";
|
||||
import Input from "@/components/Low/Input";
|
||||
import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea";
|
||||
import Option from '@/interfaces/option'
|
||||
import Dropdown from "./Shared/SettingsDropdown";
|
||||
import useSettingsState from "../Hooks/useSettingsState";
|
||||
import { Module } from "@/interfaces";
|
||||
import { SectionSettings } from "@/stores/examEditor/types";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
|
||||
interface SettingsEditorProps {
|
||||
sectionId: number,
|
||||
sectionLabel: string;
|
||||
module: Module,
|
||||
introPresets: Option[];
|
||||
children?: ReactNode;
|
||||
canPreview: boolean;
|
||||
preview: () => void;
|
||||
}
|
||||
|
||||
const SettingsEditor: React.FC<SettingsEditorProps> = ({
|
||||
sectionId,
|
||||
sectionLabel,
|
||||
module,
|
||||
introPresets,
|
||||
children,
|
||||
preview,
|
||||
canPreview,
|
||||
}) => {
|
||||
const examLabel = useExamEditorStore((state) => state.modules[module].examLabel) || '';
|
||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<SectionSettings>(
|
||||
module,
|
||||
sectionId
|
||||
);
|
||||
|
||||
const options = useMemo(() => [
|
||||
{ value: 'None', label: 'None' },
|
||||
...introPresets,
|
||||
{ value: 'Custom', label: 'Custom' }
|
||||
], [introPresets]);
|
||||
|
||||
const onCategoryChange = useCallback((text: string) => {
|
||||
updateLocalAndScheduleGlobal({ category: text });
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
const onIntroOptionChange = useCallback((option: { value: string | null, label: string }) => {
|
||||
let updates: Partial<SectionSettings> = { introOption: option };
|
||||
|
||||
switch (option.label) {
|
||||
case 'None':
|
||||
updates.currentIntro = undefined;
|
||||
break;
|
||||
case 'Custom':
|
||||
updates.currentIntro = localSettings.customIntro;
|
||||
break;
|
||||
default:
|
||||
const selectedPreset = introPresets.find(preset => preset.label === option.label);
|
||||
if (selectedPreset) {
|
||||
updates.currentIntro = selectedPreset.value!
|
||||
.replace('{part}', sectionLabel)
|
||||
.replace('{label}', examLabel);
|
||||
}
|
||||
}
|
||||
|
||||
updateLocalAndScheduleGlobal(updates);
|
||||
}, [updateLocalAndScheduleGlobal, localSettings.customIntro, introPresets, sectionLabel, examLabel]);
|
||||
|
||||
const onCustomIntroChange = useCallback((text: string) => {
|
||||
updateLocalAndScheduleGlobal({
|
||||
introOption: { value: 'Custom', label: 'Custom' },
|
||||
customIntro: text,
|
||||
currentIntro: text
|
||||
});
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-8 border bg-ielts-${module}/20 rounded-3xl p-8 w-1/3 h-fit`}>
|
||||
<div className={`w-full flex justify-center text-ielts-${module} font-bold text-xl`}>{sectionLabel} Settings</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Dropdown
|
||||
title="Category"
|
||||
module={module}
|
||||
open={localSettings.isCategoryDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isCategoryDropdownOpen: isOpen })}
|
||||
>
|
||||
<Input
|
||||
key={`section-${sectionId}`}
|
||||
type="text"
|
||||
placeholder="Category"
|
||||
name="category"
|
||||
onChange={onCategoryChange}
|
||||
roundness="full"
|
||||
value={localSettings.category || ''}
|
||||
/>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
title="Divider"
|
||||
module={module}
|
||||
open={localSettings.isIntroDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isIntroDropdownOpen: isOpen })}
|
||||
>
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
<Select
|
||||
options={options}
|
||||
onChange={(o) => onIntroOptionChange({ value: o!.value, label: o!.label })}
|
||||
value={localSettings.introOption}
|
||||
/>
|
||||
{localSettings.introOption.value !== "None" && (
|
||||
<AutoExpandingTextArea
|
||||
key={`section-${sectionId}`}
|
||||
value={localSettings.currentIntro || ''}
|
||||
onChange={onCustomIntroChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Dropdown>
|
||||
{children}
|
||||
<div className="flex flex-row justify-center mt-4">
|
||||
<button
|
||||
className={clsx(
|
||||
"flex items-center justify-center px-4 py-2 text-white rounded-xl transition-colors duration-300",
|
||||
`bg-ielts-${module}/70 border border-ielts-${module} hover:bg-ielts-${module} disabled:bg-ielts-${module}/30`,
|
||||
"disabled:cursor-not-allowed disabled:text-gray-200"
|
||||
)}
|
||||
onClick={preview}
|
||||
disabled={!canPreview}
|
||||
>
|
||||
<FaEye className="mr-2" size={18} />
|
||||
Preview Module
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsEditor;
|
||||
88
src/components/ExamEditor/SettingsEditor/level.tsx
Normal file
88
src/components/ExamEditor/SettingsEditor/level.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Exercise, LevelExam, LevelPart } from "@/interfaces/exam";
|
||||
import SettingsEditor from ".";
|
||||
import Option from "@/interfaces/option";
|
||||
import Dropdown from "@/components/Dropdown";
|
||||
import clsx from "clsx";
|
||||
import ExercisePicker from "../Shared/ExercisePicker";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import useSettingsState from "../Hooks/useSettingsState";
|
||||
import { SectionSettings } from "@/stores/examEditor/types";
|
||||
|
||||
|
||||
const LevelSettings: React.FC = () => {
|
||||
|
||||
const {currentModule } = useExamEditorStore();
|
||||
const {
|
||||
focusedSection,
|
||||
difficulty,
|
||||
sections,
|
||||
} = useExamEditorStore(state => state.modules[currentModule]);
|
||||
|
||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<SectionSettings>(
|
||||
currentModule,
|
||||
focusedSection
|
||||
);
|
||||
|
||||
const currentSection = sections.find((section) => section.sectionId == focusedSection)!.state as LevelPart;
|
||||
|
||||
const defaultPresets: Option[] = [
|
||||
{
|
||||
label: "Preset: Multiple Choice",
|
||||
value: "Not available."
|
||||
},
|
||||
{
|
||||
label: "Preset: Multiple Choice - Blank Space",
|
||||
value: "Welcome to {part} of the {label}. In this section, you'll be asked to select the correct word or group of words that best completes each sentence.\n\nFor each question, carefully read the sentence and click on the option (A, B, C, or D) that you believe is correct. After making your selection, you can proceed to the next question by clicking \"Next\". If you need to review or change your previous answers, you can go back at any time by clicking \"Back\".",
|
||||
},
|
||||
{
|
||||
label: "Preset: Multiple Choice - Underlined",
|
||||
value: "Welcome to {part} of the {label}. In this section, you'll be asked to identify the underlined word or group of words that is not correct in each sentence.\n\nFor each question, carefully review the sentence and click on the option (A, B, C, or D) that you believe contains the incorrect word or group of words. After making your selection, you can proceed to the next question by clicking \"Next\". If needed, you can go back to previous questions by clicking \"Back\"."
|
||||
},
|
||||
{
|
||||
label: "Preset: Blank Space",
|
||||
value: "Not available."
|
||||
},
|
||||
{
|
||||
label: "Preset: Reading Passage",
|
||||
value: "Welcome to {part} of the {label}. In this section, you will read a text and answer the questions that follow.\n\nCarefully read the provided text, then select the correct answer (A, B, C, or D) for each question. After making your selection, you can proceed to the next question by clicking \"Next\". If you need to review or change your answers, you can go back at any time by clicking \"Back\"."
|
||||
},
|
||||
{
|
||||
label: "Preset: Multiple Choice - Fill Blanks",
|
||||
value: "Welcome to {part} of the {label}. In this section, you will read a text and choose the correct word to fill in each blank space.\n\nFor each question, carefully read the text and click on the option that you believe best fits the context."
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsEditor
|
||||
sectionLabel={`Part ${focusedSection}`}
|
||||
sectionId={focusedSection}
|
||||
module="level"
|
||||
introPresets={defaultPresets}
|
||||
preview={()=>{}}
|
||||
canPreview={false}
|
||||
>
|
||||
<div>
|
||||
<Dropdown title="Add Exercises" className={
|
||||
clsx(
|
||||
"w-full font-semibold flex justify-between items-center p-4 bg-gradient-to-r border",
|
||||
"bg-ielts-level/70 border-ielts-level hover:bg-ielts-level",
|
||||
"text-white shadow-md transition-all duration-300",
|
||||
localSettings.isExerciseDropdownOpen ? "rounded-t-lg" : "rounded-lg"
|
||||
)
|
||||
}
|
||||
contentWrapperClassName={"pt-4 px-2 bg-white rounded-b-lg shadow-md transition-all duration-300 ease-in-out"}
|
||||
open={localSettings.isExerciseDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen })}
|
||||
>
|
||||
<ExercisePicker
|
||||
module="level"
|
||||
sectionId={focusedSection}
|
||||
difficulty={difficulty}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</SettingsEditor>
|
||||
);
|
||||
};
|
||||
|
||||
export default LevelSettings;
|
||||
135
src/components/ExamEditor/SettingsEditor/listening.tsx
Normal file
135
src/components/ExamEditor/SettingsEditor/listening.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import Dropdown from "./Shared/SettingsDropdown";
|
||||
import ExercisePicker from "../Shared/ExercisePicker";
|
||||
import SettingsEditor from ".";
|
||||
import GenerateBtn from "./Shared/GenerateBtn";
|
||||
import { useCallback, useState } from "react";
|
||||
import { generate } from "./Shared/Generate";
|
||||
import { Generating, ListeningSectionSettings } from "@/stores/examEditor/types";
|
||||
import Option from "@/interfaces/option";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import useSettingsState from "../Hooks/useSettingsState";
|
||||
import { ListeningPart } from "@/interfaces/exam";
|
||||
import Input from "@/components/Low/Input";
|
||||
|
||||
const ListeningSettings: React.FC = () => {
|
||||
const {currentModule } = useExamEditorStore();
|
||||
const {
|
||||
focusedSection,
|
||||
difficulty,
|
||||
sections,
|
||||
} = useExamEditorStore(state => state.modules[currentModule]);
|
||||
|
||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<ListeningSectionSettings>(
|
||||
currentModule,
|
||||
focusedSection
|
||||
);
|
||||
|
||||
const currentSection = sections.find((section) => section.sectionId == focusedSection)!.state as ListeningPart;
|
||||
|
||||
const defaultPresets: Option[] = [
|
||||
{
|
||||
label: "Preset: Writing Task 1",
|
||||
value: "Welcome to {part} of the {label}. You will write a letter of at least 150 words in response to a given situation. Your letter may be personal, semi-formal, or formal. You have 20 minutes for this task."
|
||||
},
|
||||
{
|
||||
label: "Preset: Writing Task 2",
|
||||
value: "Welcome to {part} of the {label}. You will write a semi-formal/neutral essay of at least 250 words on a topic of general interest. Discuss the given opinion, argument, or problem. Organize your ideas clearly and support them with relevant examples. You have 40 minutes for this task."
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
const generateScript = useCallback(() => {
|
||||
generate(
|
||||
focusedSection,
|
||||
currentModule,
|
||||
"context",
|
||||
{
|
||||
method: 'GET',
|
||||
queryParams: {
|
||||
difficulty,
|
||||
...(localSettings.topic && { topic: localSettings.topic })
|
||||
}
|
||||
},
|
||||
(data: any) => [{
|
||||
script: data.dialog
|
||||
}]
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localSettings.topic, difficulty, focusedSection]);
|
||||
|
||||
const onTopicChange = useCallback((topic: string) => {
|
||||
updateLocalAndScheduleGlobal({ topic });
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
return (
|
||||
<SettingsEditor
|
||||
sectionLabel={`Section ${focusedSection}`}
|
||||
sectionId={focusedSection}
|
||||
module="listening"
|
||||
introPresets={[defaultPresets[focusedSection - 1]]}
|
||||
preview={()=> {}}
|
||||
canPreview={false}
|
||||
>
|
||||
<Dropdown
|
||||
title="Audio Context"
|
||||
module={currentModule}
|
||||
open={localSettings.isAudioContextOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isAudioContextOpen: isOpen })}
|
||||
>
|
||||
<div className="flex flex-row gap-2 items-center px-2 pb-4">
|
||||
<div className="flex flex-col flex-grow gap-4 px-2">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Topic (Optional)</label>
|
||||
<Input
|
||||
key={`section-${focusedSection}`}
|
||||
type="text"
|
||||
placeholder="Topic"
|
||||
name="category"
|
||||
onChange={onTopicChange}
|
||||
roundness="full"
|
||||
value={localSettings.topic}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex self-end h-16 mb-1">
|
||||
<GenerateBtn
|
||||
module={currentModule}
|
||||
genType="context"
|
||||
sectionId={focusedSection}
|
||||
generateFnc={generateScript}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
title="Add Exercises"
|
||||
module={currentModule}
|
||||
open={localSettings.isExerciseDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen })}
|
||||
disabled={currentSection.script === undefined && currentSection.audio === undefined}
|
||||
>
|
||||
<ExercisePicker
|
||||
module="listening"
|
||||
sectionId={focusedSection}
|
||||
difficulty={difficulty}
|
||||
/>
|
||||
</Dropdown>
|
||||
{/*
|
||||
<Dropdown
|
||||
title="Generate Audio"
|
||||
module={currentModule}
|
||||
open={localSettings.isExerciseDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen })}
|
||||
disabled={currentSection.script === undefined && currentSection.audio === undefined}
|
||||
>
|
||||
<ExercisePicker
|
||||
module="listening"
|
||||
sectionId={focusedSection}
|
||||
selectedExercises={selectedExercises}
|
||||
setSelectedExercises={setSelectedExercises}
|
||||
difficulty={difficulty}
|
||||
/>
|
||||
</Dropdown>*/}
|
||||
</SettingsEditor>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListeningSettings;
|
||||
131
src/components/ExamEditor/SettingsEditor/reading.tsx
Normal file
131
src/components/ExamEditor/SettingsEditor/reading.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import SettingsEditor from ".";
|
||||
import Option from "@/interfaces/option";
|
||||
import Dropdown from "./Shared/SettingsDropdown";
|
||||
import Input from "@/components/Low/Input";
|
||||
import ExercisePicker from "../Shared/ExercisePicker";
|
||||
import { generate } from "./Shared/Generate";
|
||||
import GenerateBtn from "./Shared/GenerateBtn";
|
||||
import useSettingsState from "../Hooks/useSettingsState";
|
||||
import { ReadingPart } from "@/interfaces/exam";
|
||||
import { ReadingSectionSettings } from "@/stores/examEditor/types";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import openDetachedTab from "@/utils/popout";
|
||||
import { useRouter } from "next/router";
|
||||
import { usePersistentExamStore } from "@/stores/examStore";
|
||||
|
||||
const ReadingSettings: React.FC = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const { currentModule } = useExamEditorStore();
|
||||
const {
|
||||
focusedSection,
|
||||
difficulty,
|
||||
sections,
|
||||
} = useExamEditorStore(state => state.modules[currentModule]);
|
||||
|
||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<ReadingSectionSettings>(
|
||||
currentModule,
|
||||
focusedSection
|
||||
);
|
||||
|
||||
const currentSection = sections.find((section) => section.sectionId == focusedSection)!.state as ReadingPart;
|
||||
|
||||
const defaultPresets: Option[] = [
|
||||
{
|
||||
label: "Preset: Writing Task 1",
|
||||
value: "Welcome to {part} of the {label}. You will write a letter of at least 150 words in response to a given situation. Your letter may be personal, semi-formal, or formal. You have 20 minutes for this task."
|
||||
},
|
||||
{
|
||||
label: "Preset: Writing Task 2",
|
||||
value: "Welcome to {part} of the {label}. You will write a semi-formal/neutral essay of at least 250 words on a topic of general interest. Discuss the given opinion, argument, or problem. Organize your ideas clearly and support them with relevant examples. You have 40 minutes for this task."
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
const generatePassage = useCallback(() => {
|
||||
generate(
|
||||
focusedSection,
|
||||
currentModule,
|
||||
"context",
|
||||
{
|
||||
method: 'GET',
|
||||
queryParams: {
|
||||
difficulty,
|
||||
...(localSettings.topic && { topic: localSettings.topic })
|
||||
}
|
||||
},
|
||||
(data: any) => [{
|
||||
title: data.title,
|
||||
text: data.text
|
||||
}]
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localSettings.topic, difficulty, focusedSection]);
|
||||
|
||||
const onTopicChange = useCallback((topic: string) => {
|
||||
updateLocalAndScheduleGlobal({ topic });
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
|
||||
const canPreview = sections.some(
|
||||
(s) => (s.state as ReadingPart).exercises && (s.state as ReadingPart).exercises.length > 0
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingsEditor
|
||||
sectionLabel={`Passage ${focusedSection}`}
|
||||
sectionId={focusedSection}
|
||||
module="reading"
|
||||
introPresets={[defaultPresets[focusedSection - 1]]}
|
||||
preview={() => { }}
|
||||
canPreview={canPreview}
|
||||
>
|
||||
<Dropdown
|
||||
title="Generate Passage"
|
||||
module={currentModule}
|
||||
open={localSettings.isPassageOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isPassageOpen: isOpen })}
|
||||
>
|
||||
<div className="flex flex-row gap-2 items-center px-2 pb-4">
|
||||
<div className="flex flex-col flex-grow gap-4 px-2">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Topic (Optional)</label>
|
||||
<Input
|
||||
key={`section-${focusedSection}`}
|
||||
type="text"
|
||||
placeholder="Topic"
|
||||
name="category"
|
||||
onChange={onTopicChange}
|
||||
roundness="full"
|
||||
value={localSettings.topic}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex self-end h-16 mb-1">
|
||||
<GenerateBtn
|
||||
module={currentModule}
|
||||
genType="context"
|
||||
sectionId={focusedSection}
|
||||
generateFnc={generatePassage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
title="Add Exercises"
|
||||
module={currentModule}
|
||||
open={localSettings.isExerciseDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen })}
|
||||
disabled={currentSection.text.content === "" || currentSection.text.title === ""}
|
||||
>
|
||||
<ExercisePicker
|
||||
module="reading"
|
||||
sectionId={focusedSection}
|
||||
difficulty={difficulty}
|
||||
extraArgs={{ text: currentSection.text.content }}
|
||||
/>
|
||||
</Dropdown>
|
||||
</SettingsEditor>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReadingSettings;
|
||||
157
src/components/ExamEditor/SettingsEditor/speaking.tsx
Normal file
157
src/components/ExamEditor/SettingsEditor/speaking.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import useSettingsState from "../Hooks/useSettingsState";
|
||||
import { SpeakingSectionSettings } from "@/stores/examEditor/types";
|
||||
import Option from "@/interfaces/option";
|
||||
import { useCallback } from "react";
|
||||
import { generate } from "./Shared/Generate";
|
||||
import SettingsEditor from ".";
|
||||
import Dropdown from "./Shared/SettingsDropdown";
|
||||
import Input from "@/components/Low/Input";
|
||||
import GenerateBtn from "./Shared/GenerateBtn";
|
||||
import clsx from "clsx";
|
||||
|
||||
|
||||
const SpeakingSettings: React.FC = () => {
|
||||
|
||||
const { currentModule, dispatch } = useExamEditorStore();
|
||||
const { focusedSection, difficulty } = useExamEditorStore((store) => store.modules[currentModule])
|
||||
|
||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<SpeakingSectionSettings>(
|
||||
currentModule,
|
||||
focusedSection,
|
||||
);
|
||||
|
||||
const defaultPresets: Option[] = [
|
||||
{
|
||||
label: "Preset: Writing Task 1",
|
||||
value: "Welcome to {part} of the {label}. You will write a letter of at least 150 words in response to a given situation. Your letter may be personal, semi-formal, or formal. You have 20 minutes for this task."
|
||||
},
|
||||
{
|
||||
label: "Preset: Writing Task 2",
|
||||
value: "Welcome to {part} of the {label}. You will write a semi-formal/neutral essay of at least 250 words on a topic of general interest. Discuss the given opinion, argument, or problem. Organize your ideas clearly and support them with relevant examples. You have 40 minutes for this task."
|
||||
}
|
||||
];
|
||||
|
||||
const generateScript = useCallback((sectionId: number) => {
|
||||
|
||||
const queryParams: {
|
||||
difficulty: string;
|
||||
first_topic?: string;
|
||||
second_topic?: string;
|
||||
topic?: string;
|
||||
} = { difficulty };
|
||||
|
||||
if (sectionId === 1) {
|
||||
if (localSettings.topic) {
|
||||
queryParams['first_topic'] = localSettings.topic;
|
||||
}
|
||||
if (localSettings.secondTopic) {
|
||||
queryParams['second_topic'] = localSettings.secondTopic;
|
||||
}
|
||||
} else {
|
||||
if (localSettings.topic) {
|
||||
queryParams['topic'] = localSettings.topic;
|
||||
}
|
||||
}
|
||||
|
||||
generate(
|
||||
sectionId,
|
||||
currentModule,
|
||||
"context",
|
||||
{
|
||||
method: 'GET',
|
||||
queryParams
|
||||
},
|
||||
(data: any) => {
|
||||
switch (sectionId) {
|
||||
case 1:
|
||||
return [{
|
||||
questions: data.questions,
|
||||
firstTopic: data.first_topic,
|
||||
secondTopic: data.second_topic
|
||||
}];
|
||||
case 2:
|
||||
return [{
|
||||
topic: data.topic,
|
||||
question: data.question,
|
||||
prompts: data.prompts,
|
||||
suffix: data.suffix
|
||||
}];
|
||||
case 3:
|
||||
return [{
|
||||
topic: data.topic,
|
||||
questions: data.questions
|
||||
}];
|
||||
default:
|
||||
return [data];
|
||||
}
|
||||
}
|
||||
);
|
||||
}, [localSettings, difficulty]);
|
||||
|
||||
const onTopicChange = useCallback((topic: string) => {
|
||||
updateLocalAndScheduleGlobal({ topic });
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
const onSecondTopicChange = useCallback((topic: string) => {
|
||||
updateLocalAndScheduleGlobal({ });
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
return (
|
||||
<SettingsEditor
|
||||
sectionLabel={`Speaking ${focusedSection}`}
|
||||
sectionId={focusedSection}
|
||||
module="speaking"
|
||||
introPresets={[defaultPresets[focusedSection - 1]]}
|
||||
preview={() => { }}
|
||||
canPreview={false}
|
||||
>
|
||||
<Dropdown
|
||||
title="Generate Script"
|
||||
module={currentModule}
|
||||
open={localSettings.isExerciseDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen })}
|
||||
>
|
||||
|
||||
<div className={clsx("gap-2 px-2 pb-4", focusedSection === 1 ? "flex flex-col w-full" : "flex flex-row items-center" )}>
|
||||
<div className="flex flex-col flex-grow gap-4 px-2">
|
||||
<label className="font-normal text-base text-mti-gray-dim">{`${focusedSection === 1 ? "First Topic" : "Topic"}`} (Optional)</label>
|
||||
<Input
|
||||
key={`section-${focusedSection}`}
|
||||
type="text"
|
||||
placeholder="Topic"
|
||||
name="category"
|
||||
onChange={onTopicChange}
|
||||
roundness="full"
|
||||
value={localSettings.topic}
|
||||
/>
|
||||
</div>
|
||||
{focusedSection === 1 &&
|
||||
<div className="flex flex-col flex-grow gap-4 px-2">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Second Topic (Optional)</label>
|
||||
<Input
|
||||
key={`section-${focusedSection}`}
|
||||
type="text"
|
||||
placeholder="Topic"
|
||||
name="category"
|
||||
onChange={onSecondTopicChange}
|
||||
roundness="full"
|
||||
value={localSettings.secondTopic}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<div className={clsx("flex h-16 mb-1", focusedSection === 1 ? "justify-center mt-4" : "self-end")}>
|
||||
<GenerateBtn
|
||||
module={currentModule}
|
||||
genType="context"
|
||||
sectionId={focusedSection}
|
||||
generateFnc={generateScript}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</SettingsEditor>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpeakingSettings;
|
||||
139
src/components/ExamEditor/SettingsEditor/writing.tsx
Normal file
139
src/components/ExamEditor/SettingsEditor/writing.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import SettingsEditor from ".";
|
||||
import Option from "@/interfaces/option";
|
||||
import Dropdown from "./Shared/SettingsDropdown";
|
||||
import Input from "@/components/Low/Input";
|
||||
import { generate } from "./Shared/Generate";
|
||||
import useSettingsState from "../Hooks/useSettingsState";
|
||||
import GenerateBtn from "./Shared/GenerateBtn";
|
||||
import { SectionSettings } from "@/stores/examEditor/types";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import { useRouter } from "next/router";
|
||||
import { usePersistentExamStore } from "@/stores/examStore";
|
||||
import openDetachedTab from "@/utils/popout";
|
||||
import { WritingExercise } from "@/interfaces/exam";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
const WritingSettings: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const [preview, setPreview] = useState({canPreview: false, openTab: () => {}});
|
||||
const { currentModule } = useExamEditorStore();
|
||||
const {
|
||||
minTimer,
|
||||
difficulty,
|
||||
isPrivate,
|
||||
sections,
|
||||
focusedSection,
|
||||
} = useExamEditorStore((store) => store.modules["writing"]);
|
||||
|
||||
const states = sections.flatMap((s)=> s.state) as WritingExercise[];
|
||||
|
||||
const {
|
||||
setExam,
|
||||
setExerciseIndex,
|
||||
} = usePersistentExamStore();
|
||||
|
||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<SectionSettings>(
|
||||
currentModule,
|
||||
focusedSection,
|
||||
);
|
||||
|
||||
const defaultPresets: Option[] = [
|
||||
{
|
||||
label: "Preset: Writing Task 1",
|
||||
value: "Welcome to {part} of the {label}. You will write a letter of at least 150 words in response to a given situation. Your letter may be personal, semi-formal, or formal. You have 20 minutes for this task."
|
||||
},
|
||||
{
|
||||
label: "Preset: Writing Task 2",
|
||||
value: "Welcome to {part} of the {label}. You will write a semi-formal/neutral essay of at least 250 words on a topic of general interest. Discuss the given opinion, argument, or problem. Organize your ideas clearly and support them with relevant examples. You have 40 minutes for this task."
|
||||
}
|
||||
];
|
||||
|
||||
const generatePassage = useCallback((sectionId: number) => {
|
||||
generate(
|
||||
sectionId,
|
||||
currentModule,
|
||||
"context",
|
||||
{
|
||||
method: 'GET',
|
||||
queryParams: {
|
||||
difficulty,
|
||||
...(localSettings.topic && { topic: localSettings.topic })
|
||||
}
|
||||
},
|
||||
(data: any) => [{
|
||||
prompt: data.question
|
||||
}]
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localSettings.topic, difficulty]);
|
||||
|
||||
const onTopicChange = useCallback((topic: string) => {
|
||||
updateLocalAndScheduleGlobal({ topic });
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
useEffect(() => {
|
||||
const openTab = () => {
|
||||
setExam({
|
||||
isDiagnostic: false,
|
||||
minTimer,
|
||||
module: "writing",
|
||||
exercises: states.filter((s) => s.prompt && s.prompt !== ""),
|
||||
id: v4(),
|
||||
variant: undefined,
|
||||
difficulty,
|
||||
private: isPrivate,
|
||||
});
|
||||
setExerciseIndex(0);
|
||||
openDetachedTab("popout?type=Exam&module=writing", router)
|
||||
}
|
||||
setPreview({
|
||||
canPreview: states.some((s) => s.prompt && s.prompt !== ""),
|
||||
openTab
|
||||
})
|
||||
}, [states.some((s) => s.prompt !== "")])
|
||||
|
||||
return (
|
||||
<SettingsEditor
|
||||
sectionLabel={`Task ${focusedSection}`}
|
||||
sectionId={focusedSection}
|
||||
module="writing"
|
||||
introPresets={[defaultPresets[focusedSection - 1]]}
|
||||
preview={preview.openTab}
|
||||
canPreview={preview.canPreview}
|
||||
>
|
||||
<Dropdown
|
||||
title="Generate Instructions"
|
||||
module={currentModule}
|
||||
open={localSettings.isExerciseDropdownOpen}
|
||||
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen })}
|
||||
>
|
||||
|
||||
<div className="flex flex-row gap-2 items-center px-2 pb-4">
|
||||
<div className="flex flex-col flex-grow gap-4 px-2">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Topic (Optional)</label>
|
||||
<Input
|
||||
key={`section-${focusedSection}`}
|
||||
type="text"
|
||||
placeholder="Topic"
|
||||
name="category"
|
||||
onChange={onTopicChange}
|
||||
roundness="full"
|
||||
value={localSettings.topic}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex self-end h-16 mb-1">
|
||||
<GenerateBtn
|
||||
genType="context"
|
||||
module={currentModule}
|
||||
sectionId={focusedSection}
|
||||
generateFnc={generatePassage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</SettingsEditor>
|
||||
);
|
||||
};
|
||||
|
||||
export default WritingSettings;
|
||||
Reference in New Issue
Block a user