Full React 18 + TypeScript + Vite frontend with: - 90+ pages (admin, student, teacher, public) - shadcn/ui component library with 50+ components - JWT authentication with role-based access control - TanStack React Query for server state management - 30+ API service modules - AI-powered features (coaching, grading, generation) - Adaptive learning UI (diagnostics, proficiency, plans) - Institutional LMS management (courses, batches, timetable) - Communication suite (discussions, announcements, DMs) - Full CRUD with validation and confirmation dialogs Made-with: Cursor
216 lines
8.2 KiB
TypeScript
216 lines
8.2 KiB
TypeScript
import { useState } from "react";
|
|
import { useMutation } from "@tanstack/react-query";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Sparkles, Loader2 } from "lucide-react";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { lmsService } from "@/services/lms.service";
|
|
|
|
interface Props {
|
|
type: "course" | "assignment" | "batch" | "rubric" | "exam" | "student" | "teacher";
|
|
trigger?: React.ReactNode;
|
|
onGenerated?: (data: any) => void;
|
|
}
|
|
|
|
const typeLabels: Record<string, string> = {
|
|
course: "Course", assignment: "Assignment", batch: "Batch", rubric: "Rubric",
|
|
exam: "Exam", student: "Student Plan", teacher: "Teacher Assignment",
|
|
};
|
|
|
|
export default function AiCreationAssistant({ type, trigger, onGenerated }: Props) {
|
|
const [open, setOpen] = useState(false);
|
|
const [prompt, setPrompt] = useState("");
|
|
const [level, setLevel] = useState("b2");
|
|
const [subjectId, setSubjectId] = useState("");
|
|
const { toast } = useToast();
|
|
|
|
const generateCourseMutation = useMutation({
|
|
mutationFn: (payload: { title: string; subject_id?: number; level?: string }) =>
|
|
lmsService.aiGenerateCourse(payload),
|
|
onError: (err: Error) => {
|
|
toast({
|
|
variant: "destructive",
|
|
title: "Generation failed",
|
|
description: err.message || "Could not generate course outline.",
|
|
});
|
|
},
|
|
});
|
|
|
|
const handleGenerate = () => {
|
|
if (type !== "course") {
|
|
toast({
|
|
variant: "destructive",
|
|
title: "Not available",
|
|
description: "Live AI generation is wired for courses only. Use the course type to generate an outline.",
|
|
});
|
|
return;
|
|
}
|
|
const title = prompt.trim() || "Untitled course";
|
|
const sid = subjectId.trim() ? Number(subjectId) : undefined;
|
|
generateCourseMutation.mutate({
|
|
title,
|
|
...(sid !== undefined && !Number.isNaN(sid) ? { subject_id: sid } : {}),
|
|
level,
|
|
});
|
|
};
|
|
|
|
const handleApply = () => {
|
|
if (!generateCourseMutation.data) return;
|
|
onGenerated?.(generateCourseMutation.data);
|
|
toast({
|
|
title: `AI ${typeLabels[type]} Applied`,
|
|
description: `The AI-generated ${type} has been applied to your form.`,
|
|
});
|
|
setOpen(false);
|
|
generateCourseMutation.reset();
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{trigger ? (
|
|
<div onClick={() => setOpen(true)}>{trigger}</div>
|
|
) : (
|
|
<Button variant="outline" size="sm" onClick={() => setOpen(true)}>
|
|
<Sparkles className="h-3.5 w-3.5 mr-1 text-primary" /> AI Generate {typeLabels[type]}
|
|
</Button>
|
|
)}
|
|
|
|
<Dialog
|
|
open={open}
|
|
onOpenChange={(v) => {
|
|
setOpen(v);
|
|
if (!v) generateCourseMutation.reset();
|
|
}}
|
|
>
|
|
<DialogContent className="max-w-lg max-h-[80vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<Sparkles className="h-4 w-4 text-primary" /> AI {typeLabels[type]} Generator
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
{type === "course" && generateCourseMutation.isSuccess ? (
|
|
<div className="space-y-4">
|
|
<div className="rounded-lg bg-muted/30 p-4 space-y-3">
|
|
<p className="text-xs font-semibold text-primary flex items-center gap-1">
|
|
<Sparkles className="h-3 w-3" /> AI Generated {typeLabels[type]}
|
|
</p>
|
|
<div>
|
|
<span className="text-xs text-muted-foreground">Outline</span>
|
|
<pre className="text-sm mt-1 whitespace-pre-wrap break-words rounded-md border bg-background p-3 max-h-[40vh] overflow-y-auto">
|
|
{typeof generateCourseMutation.data?.outline === "string"
|
|
? generateCourseMutation.data.outline
|
|
: JSON.stringify(generateCourseMutation.data?.outline, null, 2)}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<Button className="flex-1" onClick={handleApply}>
|
|
Apply to Form
|
|
</Button>
|
|
<Button variant="outline" onClick={() => generateCourseMutation.reset()}>
|
|
Regenerate
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label>Describe what you need</Label>
|
|
<Textarea
|
|
placeholder={`Describe the ${type} you want to create... (e.g. "An intermediate IELTS writing course focusing on Task 2 essays")`}
|
|
rows={3}
|
|
value={prompt}
|
|
onChange={(e) => setPrompt(e.target.value)}
|
|
/>
|
|
</div>
|
|
{type === "course" && (
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="space-y-2">
|
|
<Label>Level</Label>
|
|
<Select value={level} onValueChange={setLevel}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{["A1", "A2", "B1", "B2", "C1", "C2"].map((l) => (
|
|
<SelectItem key={l} value={l.toLowerCase()}>
|
|
{l}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Subject ID (optional)</Label>
|
|
<Input
|
|
inputMode="numeric"
|
|
placeholder="e.g. 12"
|
|
value={subjectId}
|
|
onChange={(e) => setSubjectId(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{(type === "assignment" || type === "exam") && (
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="space-y-2">
|
|
<Label>Level</Label>
|
|
<Select defaultValue="b2">
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{["A1", "A2", "B1", "B2", "C1", "C2"].map((l) => (
|
|
<SelectItem key={l} value={l.toLowerCase()}>
|
|
{l}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Focus Area</Label>
|
|
<Select defaultValue="general">
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="general">General</SelectItem>
|
|
<SelectItem value="reading">Reading</SelectItem>
|
|
<SelectItem value="writing">Writing</SelectItem>
|
|
<SelectItem value="speaking">Speaking</SelectItem>
|
|
<SelectItem value="listening">Listening</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<Button
|
|
className="w-full"
|
|
onClick={handleGenerate}
|
|
disabled={generateCourseMutation.isPending}
|
|
>
|
|
{generateCourseMutation.isPending ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> Generating...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Sparkles className="h-4 w-4 mr-2" /> Generate with AI
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
}
|