feat: add complete EnCoach frontend application
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
This commit is contained in:
215
src/components/ai/AiCreationAssistant.tsx
Normal file
215
src/components/ai/AiCreationAssistant.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user