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
127 lines
5.7 KiB
TypeScript
127 lines
5.7 KiB
TypeScript
import { useState } from "react";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, PieChart, Pie, Cell } from "recharts";
|
|
import AiReportNarrative from "@/components/ai/AiReportNarrative";
|
|
|
|
const thresholds = ["0%", "50%", "70%", "90%"];
|
|
|
|
const barData = [
|
|
{ module: "Reading", score: 72 },
|
|
{ module: "Listening", score: 68 },
|
|
{ module: "Writing", score: 61 },
|
|
{ module: "Speaking", score: 65 },
|
|
];
|
|
|
|
const trendData = [
|
|
{ month: "Jan", avg: 58 }, { month: "Feb", avg: 62 }, { month: "Mar", avg: 65 },
|
|
{ month: "Apr", avg: 64 }, { month: "May", avg: 69 }, { month: "Jun", avg: 72 },
|
|
];
|
|
|
|
const distData = [
|
|
{ name: "A1", value: 15, color: "hsl(0, 72%, 51%)" },
|
|
{ name: "A2", value: 22, color: "hsl(38, 92%, 50%)" },
|
|
{ name: "B1", value: 30, color: "hsl(199, 89%, 48%)" },
|
|
{ name: "B2", value: 20, color: "hsl(243, 75%, 59%)" },
|
|
{ name: "C1", value: 10, color: "hsl(142, 71%, 45%)" },
|
|
{ name: "C2", value: 3, color: "hsl(280, 65%, 50%)" },
|
|
];
|
|
|
|
export default function StatsCorporatePage() {
|
|
const [threshold, setThreshold] = useState("0%");
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold tracking-tight">Corporate Statistics</h1>
|
|
<p className="text-muted-foreground">Entity-level performance analytics and reports.</p>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-3 items-center">
|
|
<div className="flex gap-1">
|
|
{thresholds.map(t => (
|
|
<Button key={t} variant={threshold === t ? "default" : "outline"} size="sm" onClick={() => setThreshold(t)}>{t}</Button>
|
|
))}
|
|
</div>
|
|
<Select><SelectTrigger className="w-[160px]"><SelectValue placeholder="All Entities" /></SelectTrigger>
|
|
<SelectContent><SelectItem value="all">All</SelectItem><SelectItem value="acme">Acme Corp</SelectItem></SelectContent>
|
|
</Select>
|
|
<Select><SelectTrigger className="w-[180px]"><SelectValue placeholder="All Assignments" /></SelectTrigger>
|
|
<SelectContent><SelectItem value="all">All Assignments</SelectItem></SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<Tabs defaultValue="overview">
|
|
<TabsList>
|
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
<TabsTrigger value="trends">Trends</TabsTrigger>
|
|
<TabsTrigger value="distribution">Distribution</TabsTrigger>
|
|
<TabsTrigger value="comparison">Comparison</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="overview" className="mt-4">
|
|
<AiReportNarrative report_type="corporate-overview" data={{ modules: barData }} />
|
|
<Card className="border-0 shadow-sm">
|
|
<CardHeader><CardTitle className="text-base">Average Score by Module</CardTitle></CardHeader>
|
|
<CardContent>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<BarChart data={barData}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="hsl(220, 16%, 90%)" />
|
|
<XAxis dataKey="module" />
|
|
<YAxis domain={[0, 100]} />
|
|
<Tooltip />
|
|
<Bar dataKey="score" fill="hsl(243, 75%, 59%)" radius={[4, 4, 0, 0]} />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="trends" className="mt-4">
|
|
<AiReportNarrative report_type="corporate-trends" data={{ trends: trendData }} />
|
|
<Card className="border-0 shadow-sm">
|
|
<CardHeader><CardTitle className="text-base">Score Trend Over Time</CardTitle></CardHeader>
|
|
<CardContent>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<LineChart data={trendData}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="hsl(220, 16%, 90%)" />
|
|
<XAxis dataKey="month" />
|
|
<YAxis domain={[0, 100]} />
|
|
<Tooltip />
|
|
<Line type="monotone" dataKey="avg" stroke="hsl(243, 75%, 59%)" strokeWidth={2} dot={{ r: 4 }} />
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="distribution" className="mt-4">
|
|
<AiReportNarrative report_type="corporate-distribution" data={{ distribution: distData }} />
|
|
<Card className="border-0 shadow-sm">
|
|
<CardHeader><CardTitle className="text-base">Level Distribution</CardTitle></CardHeader>
|
|
<CardContent className="flex justify-center">
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<PieChart>
|
|
<Pie data={distData} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={100} label>
|
|
{distData.map((entry, i) => <Cell key={i} fill={entry.color} />)}
|
|
</Pie>
|
|
<Tooltip />
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="comparison" className="mt-4">
|
|
<AiReportNarrative report_type="corporate-comparison" data={{ threshold }} />
|
|
<Card className="border-0 shadow-sm">
|
|
<CardContent className="p-8 text-center text-muted-foreground">Entity comparison charts will appear here based on selected filters.</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
}
|