Files
encoach_frontend_new_v2/frontend/src/pages/admin/AiEnglishQuality.tsx
Yamen Ahmad b02ee8b6b7 feat: Generation Page AI workflows + AI/Vector modules + exam session fixes
Generation Page (complete rebuild):
- Full production-parity exam generation wizard with 4 IELTS modules
- Reading: AI passage gen, 5 exercise types (MCQ, Fill, Write, T/F, Match)
- Listening: 4 section types, AI context gen, TTS audio gen (ElevenLabs)
- Writing: Task 1/2, AI instruction gen, word limits, marks
- Speaking: 3 parts, AI script gen, avatar video gen (7 avatars)
- Per-module config: timer, CEFR difficulty, access, approval, rubrics
- Exam submission workflow (draft/published)

Exam Structures:
- New encoach.exam.structure model + CRUD controller
- ExamStructuresPage wired to real API

AI Module (encoach_ai):
- OpenAI service, ElevenLabs TTS, AWS Polly, ELAI avatars
- AI settings model with Odoo config parameters
- 7 generation endpoints (passage, exercises, instructions, scripts, context)

Vector Module (encoach_vector):
- pgvector integration for RAG-based content search
- Embedding service with sentence-transformers

Exam Session Fixes:
- Fixed ExamSession.tsx field mapping (question_type→type, exam_title→title)
- Fixed submit payload to include attempt_id and answers
- Fixed normalizeType to handle null/undefined

Tested: 12/12 API tests passed, browser-verified with real OpenAI calls
Made-with: Cursor
2026-04-11 14:27:03 +04:00

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, reason: 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>
);
}