Files
BoostAI/Frontend/src/components/dashboard/teacher/dashboard-teacher-assignment-create.tsx
2026-05-25 17:05:06 +01:00

241 lines
8.3 KiB
TypeScript

import type { Component } from "solid-js";
import { createMemo, createResource, createSignal, onMount } from "solid-js";
import { createStore } from "solid-js/store";
import { useAuth } from "~/context/auth/context";
import styles from "./dashboard-teacher-assignments.module.scss";
import { createTeacherAssignment, generateTeacherQuestions, getTeacherAssignmentSetupData } from "./dashboard-teacher-assignments.data";
import { defaultGenerationForm, emptySetupForm } from "./dashboard-teacher-assignment-create.constants";
import { buildGenerationSuccessMessage, buildMixedGenerationInput, mergeSelectedQuestionIds, parseOptionalInteger } from "./dashboard-teacher-assignment-create.helpers";
import {
AssignmentCreateHero,
AssignmentDetailsSection,
PersonalizedGenerationSection,
QuestionBankSection,
SetupSummarySection,
SharedGenerationSection,
SubmissionSection,
} from "./dashboard-teacher-assignment-create.sections";
import type { AssignmentSetupForm, QuestionGenerationForm } from "./dashboard-teacher-assignment-create.types";
const DashboardTeacherAssignmentCreate: Component = () => {
const auth = useAuth();
const [teacherId, setTeacherId] = createSignal<number | null>(null);
const [setupData, { mutate: mutateSetupData }] = createResource(teacherId, getTeacherAssignmentSetupData);
const [form, setForm] = createStore<AssignmentSetupForm>(emptySetupForm);
const [generationForm, setGenerationForm] = createStore<QuestionGenerationForm>(defaultGenerationForm);
const [isSubmitting, setIsSubmitting] = createSignal(false);
const [isGenerating, setIsGenerating] = createSignal(false);
const [errorMessage, setErrorMessage] = createSignal<string | null>(null);
const [successMessage, setSuccessMessage] = createSignal<string | null>(null);
onMount(() => {
if (auth.user()?.role === "teacher") {
setTeacherId(auth.user()!.id);
}
});
const selectedClassroom = createMemo(() => setupData()?.classrooms.find((classroom) => `${classroom.id}` === form.classroomId));
const selectedQuestionCount = createMemo(() => form.selectedQuestionIds.length);
const mixedGenerationEnabled = createMemo(() => form.useMixedGeneration);
const mixedGenerationQuestionCount = createMemo(() => Number(form.totalQuestions) || 0);
const mixedGenerationRatioLabel = createMemo(() => {
const parsed = Number(form.personalizedRatio);
if (!Number.isFinite(parsed) || parsed <= 0) return "30% personalized";
return `${parsed}% personalized`;
});
const handleTextInput =
(
field:
| "classroomId"
| "title"
| "instructions"
| "dueAt"
| "primaryTopic"
| "primaryDifficulty"
| "totalQuestions"
| "personalizedRatio"
| "seed"
| "personalizedDifficulty"
| "subject",
) =>
(event: Event) => {
const target = event.currentTarget as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
setForm(field, target.value);
setSuccessMessage(null);
};
const handleMixedGenerationToggle = (event: Event) => {
const target = event.currentTarget as HTMLInputElement;
setForm("useMixedGeneration", target.checked);
setSuccessMessage(null);
};
const handleGenerationInput = (field: keyof QuestionGenerationForm) => (event: Event) => {
const target = event.currentTarget as HTMLInputElement | HTMLSelectElement;
setGenerationForm(field, target.value as QuestionGenerationForm[keyof QuestionGenerationForm]);
setSuccessMessage(null);
};
const handleQuestionToggle = (questionId: number) => (event: Event) => {
const target = event.currentTarget as HTMLInputElement;
setForm("selectedQuestionIds", target.checked ? [...form.selectedQuestionIds, questionId] : form.selectedQuestionIds.filter((id) => id !== questionId));
setSuccessMessage(null);
};
const resetForm = () => {
setForm({ ...emptySetupForm, classroomId: form.classroomId });
};
const mergeGeneratedQuestions = (generatedQuestionIds: number[], questions: Awaited<ReturnType<typeof generateTeacherQuestions>>["questions"]) => {
mutateSetupData((current) => {
if (!current) return current;
const existingQuestions = current.questions.filter((question) => !generatedQuestionIds.includes(question.id));
return {
...current,
questions: [...questions, ...existingQuestions],
};
});
setForm("selectedQuestionIds", mergeSelectedQuestionIds(form.selectedQuestionIds, generatedQuestionIds));
};
const handleGenerateQuestions = async (event: Event) => {
event.preventDefault();
const currentTeacherId = teacherId();
if (!currentTeacherId) {
setErrorMessage("Your teacher session is still loading.");
return;
}
const parsedCount = Number(generationForm.count);
const parsedSeed = parseOptionalInteger(generationForm.seed);
if (!Number.isInteger(parsedCount) || parsedCount < 1 || parsedCount > 25) {
setErrorMessage("Choose a question count between 1 and 25.");
return;
}
if (generationForm.seed.trim() && (!Number.isInteger(parsedSeed) || Number.isNaN(parsedSeed))) {
setErrorMessage("Seed must be a whole number when provided.");
return;
}
setIsGenerating(true);
setErrorMessage(null);
setSuccessMessage(null);
try {
const result = await generateTeacherQuestions({
topic: generationForm.topic,
difficulty: generationForm.difficulty,
count: parsedCount,
seed: parsedSeed,
});
mergeGeneratedQuestions(result.generatedQuestionIds, result.questions);
setSuccessMessage(buildGenerationSuccessMessage(generationForm, result.count, result.seed));
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : "Unable to generate questions right now.");
} finally {
setIsGenerating(false);
}
};
const handleSubmit = async (event: Event) => {
event.preventDefault();
const currentTeacherId = teacherId();
if (!currentTeacherId) {
setErrorMessage("Your teacher session is still loading.");
return;
}
setIsSubmitting(true);
setErrorMessage(null);
setSuccessMessage(null);
try {
const mixedGeneration = buildMixedGenerationInput(form);
await createTeacherAssignment({
teacherId: currentTeacherId,
classroomId: Number(form.classroomId),
title: form.title,
instructions: form.instructions,
dueAt: form.dueAt,
selectedQuestionIds: form.selectedQuestionIds,
mixedGeneration,
});
setSuccessMessage(
mixedGenerationEnabled()
? "Personalized homework created and assigned. Each student now has a mixed generated question set."
: "Homework created and assigned to the selected class.",
);
resetForm();
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : "Unable to create homework right now.");
} finally {
setIsSubmitting(false);
}
};
return (
<section class={styles.section}>
<AssignmentCreateHero />
<form class={styles.setupGrid} onSubmit={(event) => void handleSubmit(event)}>
<article class={styles.setupCard}>
<AssignmentDetailsSection
form={form}
classrooms={setupData()?.classrooms ?? []}
selectedQuestionCount={selectedQuestionCount()}
onInput={handleTextInput}
/>
<PersonalizedGenerationSection
form={form}
mixedGenerationEnabled={mixedGenerationEnabled()}
mixedGenerationQuestionCount={mixedGenerationQuestionCount()}
mixedGenerationRatioLabel={mixedGenerationRatioLabel()}
onInput={handleTextInput}
onToggle={handleMixedGenerationToggle}
/>
<SharedGenerationSection
form={generationForm}
isGenerating={isGenerating()}
isSubmitting={isSubmitting()}
onInput={handleGenerationInput}
onGenerate={(event) => void handleGenerateQuestions(event)}
/>
<QuestionBankSection
questions={setupData()?.questions ?? []}
loading={setupData.loading}
mixedGenerationEnabled={mixedGenerationEnabled()}
selectedQuestionIds={form.selectedQuestionIds}
onToggle={handleQuestionToggle}
/>
<SubmissionSection
isSubmitting={isSubmitting()}
errorMessage={errorMessage()}
successMessage={successMessage()}
onReset={resetForm}
/>
</article>
<SetupSummarySection
classroomName={selectedClassroom()?.name ?? ""}
rosterLabel={selectedClassroom()?.studentCountLabel ?? ""}
mixedGenerationEnabled={mixedGenerationEnabled()}
mixedGenerationQuestionCount={mixedGenerationQuestionCount()}
selectedQuestionCount={selectedQuestionCount()}
/>
</form>
</section>
);
};
export default DashboardTeacherAssignmentCreate;