Files
encoach_frontend_new_v3/src/pages/admin/AiIeltsValidation.tsx
Yamen Ahmad 88a385ed07 feat: initial frontend codebase — EnCoach v3
React 18 + TypeScript + Vite 5 + Tailwind CSS + shadcn/ui frontend:
- Multi-role auth: student, teacher, admin, corporate, agent, developer
- Student portal: dashboard, courses, adaptive learning, exams, placement
- Teacher portal: courses, assignments, grading, attendance, AI workbench
- Admin portal: 60+ management pages (LMS, exams, users, entities, AI)
- AI-powered features: study coach, generation page, gap analysis, TTS
- Exam system: session, auto-save, results, grading queue
- Full IELTS workflow: reading/listening/writing/speaking modules

Made-with: Cursor
2026-04-11 15:47:32 +04:00

206 lines
7.8 KiB
TypeScript

import { useState } from "react";
import { useParams } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useIeltsValidation, useSubmitExaminerReview } from "@/hooks/queries/useAiCourse";
import type { ExaminerReviewItem } from "@/types";
import { CheckCircle2, XCircle } from "lucide-react";
import { toast } from "sonner";
export default function AiIeltsValidation() {
const { courseId: cid } = useParams<{ courseId: string }>();
const courseId = Number(cid);
const { data, isLoading, refetch } = useIeltsValidation(Number.isFinite(courseId) ? courseId : undefined);
const submitReview = useSubmitExaminerReview();
const [preview, setPreview] = useState<ExaminerReviewItem | null>(null);
const [notes, setNotes] = useState("");
const [checklist, setChecklist] = useState({
topic_accuracy: true,
difficulty_appropriate: true,
ielts_aligned: true,
culturally_sensitive: true,
});
const layer1 = data?.layer1_checks ?? [];
const layer2 = data?.layer2_items ?? [];
const send = (approved: boolean) => {
if (!preview) return;
submitReview.mutate({
logId: preview.id,
action: approved ? "approve" : "reject",
examiner_notes: approved ? undefined : notes,
}, {
onSuccess: () => {
toast.success(approved ? "Approved." : "Rejected with notes.");
setPreview(null);
setNotes("");
refetch();
},
onError: (e) => toast.error(e instanceof Error ? e.message : "Failed"),
});
};
if (isLoading) {
return <div className="p-6 text-muted-foreground">Loading validation</div>;
}
return (
<div className="space-y-8 p-6 max-w-6xl mx-auto">
<div>
<h1 className="text-2xl font-semibold tracking-tight">IELTS content validation</h1>
<p className="text-muted-foreground text-sm mt-1">Layer 1 automation · Layer 2 examiner queue</p>
</div>
<Card>
<CardHeader>
<CardTitle>Layer 1 Automated checks</CardTitle>
<CardDescription>Format, calibration, answer keys</CardDescription>
</CardHeader>
<CardContent>
<div className="rounded-md border overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Check</TableHead>
<TableHead>Status</TableHead>
<TableHead>Detail</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{layer1.map((c) => (
<TableRow key={c.name}>
<TableCell className="font-medium">{c.name}</TableCell>
<TableCell>
<span className="inline-flex items-center gap-1">
{c.status === "pass" ? (
<CheckCircle2 className="h-4 w-4 text-green-600" />
) : (
<XCircle className="h-4 w-4 text-destructive" />
)}
{c.status}
</span>
</TableCell>
<TableCell className="text-muted-foreground">{c.detail}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Layer 2 Examiner review queue</CardTitle>
<CardDescription>Approve or reject with structured notes</CardDescription>
</CardHeader>
<CardContent>
<div className="rounded-md border overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Skill</TableHead>
<TableHead>Content type</TableHead>
<TableHead>Target band</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{layer2.map((item) => (
<TableRow key={item.id}>
<TableCell>{item.skill}</TableCell>
<TableCell>{item.content_type}</TableCell>
<TableCell>{item.target_band}</TableCell>
<TableCell>{item.status}</TableCell>
<TableCell className="text-right">
<Button size="sm" variant="outline" onClick={() => setPreview(item)}>
Review
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
<Dialog open={!!preview} onOpenChange={(o) => !o && setPreview(null)}>
<DialogContent className="max-w-lg max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Content preview</DialogTitle>
</DialogHeader>
{preview && (
<div className="space-y-4 text-sm">
<p>
<span className="font-medium">Skill:</span> {preview.skill} ·{" "}
<span className="font-medium">Type:</span> {preview.content_type} ·{" "}
<span className="font-medium">Band:</span> {preview.target_band}
</p>
<div className="rounded-md border bg-muted/40 p-4 min-h-[120px] text-muted-foreground">
Full content preview loads here from the authoring pipeline.
</div>
<div className="space-y-3">
<Label className="text-base">Examiner checklist</Label>
<div className="space-y-2">
{(
[
["topic_accuracy", "Accuracy"],
["difficulty_appropriate", "Difficulty"],
["ielts_aligned", "IELTS alignment"],
["culturally_sensitive", "Cultural sensitivity"],
] as const
).map(([key, label]) => (
<div key={key} className="flex items-center gap-2">
<Checkbox
id={key}
checked={checklist[key]}
onCheckedChange={(c) => setChecklist((prev) => ({ ...prev, [key]: !!c }))}
/>
<Label htmlFor={key} className="font-normal cursor-pointer">
{label}
</Label>
</div>
))}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="notes">Rejection notes</Label>
<Textarea
id="notes"
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Required when rejecting"
rows={3}
/>
</div>
</div>
)}
<DialogFooter className="gap-2 sm:gap-0">
<Button variant="outline" onClick={() => send(true)} disabled={submitReview.isPending}>
Approve
</Button>
<Button variant="destructive" onClick={() => send(false)} disabled={submitReview.isPending}>
Reject with Notes
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}