241 lines
8.3 KiB
TypeScript
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;
|