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
This commit is contained in:
205
src/pages/admin/AiIeltsValidation.tsx
Normal file
205
src/pages/admin/AiIeltsValidation.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user