Files
encoach_backend_new_v2/frontend/src/pages/student/StudentGrades.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

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>
);
}