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
79 lines
2.5 KiB
TypeScript
79 lines
2.5 KiB
TypeScript
import { useState } from "react";
|
|
import { useMutation } from "@tanstack/react-query";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
import { Sparkles, Loader2 } from "lucide-react";
|
|
import { coachingService } from "@/services/coaching.service";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
|
|
export default function AiGradeExplainer({
|
|
studentName,
|
|
scores,
|
|
}: {
|
|
studentName: string;
|
|
scores?: Record<string, number>;
|
|
}) {
|
|
const [open, setOpen] = useState(false);
|
|
const { toast } = useToast();
|
|
|
|
const explainMutation = useMutation({
|
|
mutationFn: () =>
|
|
coachingService.explain({
|
|
score_data: scores ?? {},
|
|
student_context: `IELTS / course grades for student: ${studentName}. Summarize what the scores mean and what to focus on next.`,
|
|
}),
|
|
onError: (err: Error) => {
|
|
toast({
|
|
variant: "destructive",
|
|
title: "Could not explain grades",
|
|
description: err.message || "Try again in a moment.",
|
|
});
|
|
},
|
|
});
|
|
|
|
const handleOpen = () => {
|
|
setOpen(true);
|
|
explainMutation.reset();
|
|
explainMutation.mutate();
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={handleOpen} title="AI Explain Grade">
|
|
<Sparkles className="h-3.5 w-3.5 text-primary" />
|
|
</Button>
|
|
<Dialog
|
|
open={open}
|
|
onOpenChange={(v) => {
|
|
setOpen(v);
|
|
if (!v) explainMutation.reset();
|
|
}}
|
|
>
|
|
<DialogContent className="max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<Sparkles className="h-4 w-4 text-primary" />
|
|
AI Grade Explanation — {studentName}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
{explainMutation.isPending ? (
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground py-6 justify-center">
|
|
<Loader2 className="h-5 w-5 animate-spin text-primary" /> Analyzing grades...
|
|
</div>
|
|
) : explainMutation.isError ? (
|
|
<p className="text-sm text-destructive text-center py-4">
|
|
Something went wrong. Close and try again.
|
|
</p>
|
|
) : (
|
|
<div className="rounded-lg bg-muted/30 p-4">
|
|
<p className="text-sm leading-relaxed">
|
|
{explainMutation.data?.explanation}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
}
|