- Restructure: move backend from new_project/ to backend/ - Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks) - Add docs/ with SRS specs, user stories, and workflow documentation - Update .gitignore for new directory layout Workflows implemented: WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration, WF4 General English Exam, WF5 Course Generation, WF6 Entity Student Onboarding, AI Course Generation, Adaptive Learning Engine UI, White-Label Branding, Score Release Made-with: Cursor
137 lines
4.9 KiB
TypeScript
137 lines
4.9 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 { Textarea } from "@/components/ui/textarea";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { useApproveQuality, useQualityGate, useRejectQuality } from "@/hooks/queries/useAiCourse";
|
|
import { CheckCircle2, XCircle } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
|
|
export default function AiEnglishQuality() {
|
|
const { courseId: cid } = useParams<{ courseId: string }>();
|
|
const courseId = Number(cid);
|
|
const { data, isLoading, refetch } = useQualityGate(Number.isFinite(courseId) ? courseId : undefined);
|
|
const approve = useApproveQuality();
|
|
const reject = useRejectQuality();
|
|
const [rejectOpen, setRejectOpen] = useState(false);
|
|
const [notes, setNotes] = useState("");
|
|
|
|
const checks = data?.checks ?? [];
|
|
|
|
if (isLoading) {
|
|
return <div className="p-6 text-muted-foreground">Loading quality gate…</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6 p-6 max-w-5xl mx-auto">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold tracking-tight">English course quality</h1>
|
|
<p className="text-muted-foreground text-sm mt-1">
|
|
Attempts {data?.attempts ?? 0} / {data?.max_attempts ?? 0}
|
|
</p>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Quality gate checks</CardTitle>
|
|
<CardDescription>Readability, calibration, grammar, and length</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="rounded-md border overflow-x-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Check</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead>Detail</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{checks.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 max-w-md">{c.detail}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-3">
|
|
<Button
|
|
onClick={() =>
|
|
approve.mutate(courseId, {
|
|
onSuccess: () => {
|
|
toast.success("Approved.");
|
|
refetch();
|
|
},
|
|
onError: (e) => toast.error(e instanceof Error ? e.message : "Failed"),
|
|
})
|
|
}
|
|
disabled={!Number.isFinite(courseId) || approve.isPending}
|
|
>
|
|
Approve
|
|
</Button>
|
|
<Button variant="destructive" onClick={() => setRejectOpen(true)}>
|
|
Reject & Regenerate
|
|
</Button>
|
|
<Button variant="outline" onClick={() => refetch()}>
|
|
Edit
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Dialog open={rejectOpen} onOpenChange={setRejectOpen}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Regeneration notes</DialogTitle>
|
|
</DialogHeader>
|
|
<Textarea value={notes} onChange={(e) => setNotes(e.target.value)} placeholder="What should change?" rows={4} />
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setRejectOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={() =>
|
|
reject.mutate(
|
|
{ courseId, notes },
|
|
{
|
|
onSuccess: () => {
|
|
toast.success("Rejected; regeneration requested.");
|
|
setRejectOpen(false);
|
|
refetch();
|
|
},
|
|
onError: (e) => toast.error(e instanceof Error ? e.message : "Failed"),
|
|
},
|
|
)
|
|
}
|
|
disabled={reject.isPending}
|
|
>
|
|
Submit
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|