feat(v3): restructure project + add complete frontend
- 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
This commit is contained in:
136
src/pages/admin/AiEnglishQuality.tsx
Normal file
136
src/pages/admin/AiEnglishQuality.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user