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
87 lines
4.2 KiB
TypeScript
87 lines
4.2 KiB
TypeScript
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { useGrades } from "@/hooks/queries";
|
|
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
|
|
import AiGradeExplainer from "@/components/ai/AiGradeExplainer";
|
|
import AiReportNarrative from "@/components/ai/AiReportNarrative";
|
|
|
|
export default function StudentGrades() {
|
|
const { data: gradeRecords = [], isLoading } = useGrades();
|
|
if (isLoading) return <div className="flex items-center justify-center min-h-[400px]"><div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" /></div>;
|
|
|
|
const avgGrade = gradeRecords.length
|
|
? (gradeRecords.reduce((sum, g) => sum + (g.grade / g.maxGrade) * 100, 0) / gradeRecords.length).toFixed(1)
|
|
: "0";
|
|
const highest = gradeRecords.length
|
|
? Math.max(...gradeRecords.map(g => (g.grade / g.maxGrade) * 100)).toFixed(1)
|
|
: "0";
|
|
|
|
const chartData = gradeRecords.map(g => ({
|
|
name: g.assignmentTitle.length > 15 ? g.assignmentTitle.slice(0, 15) + "…" : g.assignmentTitle,
|
|
grade: Math.round((g.grade / g.maxGrade) * 100),
|
|
}));
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold">Grades</h1>
|
|
<p className="text-muted-foreground">Track your academic performance.</p>
|
|
</div>
|
|
|
|
<AiReportNarrative report_type="grades" data={{ avgGrade, highest, count: gradeRecords.length }} />
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
<Card><CardContent className="pt-6 text-center"><p className="text-sm text-muted-foreground">Average</p><p className="text-3xl font-bold text-primary">{avgGrade}%</p></CardContent></Card>
|
|
<Card><CardContent className="pt-6 text-center"><p className="text-sm text-muted-foreground">Highest</p><p className="text-3xl font-bold text-success">{highest}%</p></CardContent></Card>
|
|
<Card><CardContent className="pt-6 text-center"><p className="text-sm text-muted-foreground">Graded Items</p><p className="text-3xl font-bold">{gradeRecords.length}</p></CardContent></Card>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader><CardTitle className="text-lg">Grade Distribution</CardTitle></CardHeader>
|
|
<CardContent>
|
|
<ResponsiveContainer width="100%" height={250}>
|
|
<BarChart data={chartData}>
|
|
<CartesianGrid strokeDasharray="3 3" className="stroke-border" />
|
|
<XAxis dataKey="name" tick={{ fontSize: 11 }} className="fill-muted-foreground" />
|
|
<YAxis domain={[0, 100]} tick={{ fontSize: 11 }} className="fill-muted-foreground" />
|
|
<Tooltip />
|
|
<Bar dataKey="grade" fill="hsl(192, 82%, 32%)" radius={[4, 4, 0, 0]} />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader><CardTitle className="text-lg">All Grades</CardTitle></CardHeader>
|
|
<CardContent>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Assignment</TableHead>
|
|
<TableHead>Course</TableHead>
|
|
<TableHead>Type</TableHead>
|
|
<TableHead>Date</TableHead>
|
|
<TableHead className="text-right">Grade</TableHead>
|
|
<TableHead className="w-10"></TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{gradeRecords.map(g => (
|
|
<TableRow key={g.id}>
|
|
<TableCell className="font-medium">{g.assignmentTitle}</TableCell>
|
|
<TableCell>{g.courseName}</TableCell>
|
|
<TableCell><Badge variant="outline" className="text-xs capitalize">{g.type}</Badge></TableCell>
|
|
<TableCell>{g.date}</TableCell>
|
|
<TableCell className="text-right font-bold">{g.grade}/{g.maxGrade}</TableCell>
|
|
<TableCell><AiGradeExplainer studentName="Sarah Johnson" /></TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|