Files
encoach_frontend_new_v2/src/components/ai/AiCreationAssistant.tsx
Yamen Ahmad 110a0b7105 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
2026-04-01 16:59:11 +04:00

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>
</>
);
}