- Restructure: move backend from new_project/ to backend/ - Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks) - Add docs/ with SRS specs, user stories, and workflow documentation - Update .gitignore for new directory layout Workflows implemented: WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration, WF4 General English Exam, WF5 Course Generation, WF6 Entity Student Onboarding, AI Course Generation, Adaptive Learning Engine UI, White-Label Branding, Score Release Made-with: Cursor
159 lines
6.2 KiB
TypeScript
159 lines
6.2 KiB
TypeScript
import { useState, type ComponentType } from "react";
|
|
import { Link } from "react-router-dom";
|
|
import { Activity, AlertTriangle, Brain, Gauge, Users } from "lucide-react";
|
|
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 { Badge } from "@/components/ui/badge";
|
|
import { useAdaptiveDashboard, useAdaptiveStudents } from "@/hooks/queries/useAdaptiveEngine";
|
|
import type { AdaptiveDashboardMetrics, AdaptiveEngineStudentRow } from "@/types";
|
|
import { LineChart, Line, ResponsiveContainer } from "recharts";
|
|
|
|
export default function AdaptiveDashboard() {
|
|
const [page, setPage] = useState(1);
|
|
const limit = 10;
|
|
|
|
const { data: metrics } = useAdaptiveDashboard();
|
|
const { data: studentsPage, isLoading } = useAdaptiveStudents({ page, limit });
|
|
|
|
const m: AdaptiveDashboardMetrics = metrics ?? {
|
|
active_students: 0,
|
|
engine_phase: "—",
|
|
decisions_today: 0,
|
|
average_improvement: 0,
|
|
alerts_count: 0,
|
|
};
|
|
|
|
const rows: AdaptiveEngineStudentRow[] = studentsPage?.data ?? [];
|
|
const total = studentsPage?.pagination?.total ?? rows.length;
|
|
const pages = Math.max(1, Math.ceil(total / limit));
|
|
|
|
return (
|
|
<div className="space-y-8 p-6">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold tracking-tight">Adaptive engine</h1>
|
|
<p className="text-muted-foreground text-sm mt-1">Live decisions and learner risk</p>
|
|
</div>
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-5">
|
|
<MetricCard icon={Users} label="Active students" value={String(m.active_students)} />
|
|
<MetricCard icon={Brain} label="Engine phase" value={m.engine_phase} />
|
|
<MetricCard icon={Activity} label="Decisions today" value={String(m.decisions_today)} />
|
|
<MetricCard icon={Gauge} label="Avg improvement" value={`${m.average_improvement}%`} />
|
|
<MetricCard icon={AlertTriangle} label="Alerts" value={String(m.alerts_count)} />
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Students</CardTitle>
|
|
<CardDescription>Signals, pacing, and trajectory</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{isLoading ? (
|
|
<p className="text-muted-foreground py-8 text-center">Loading…</p>
|
|
) : (
|
|
<>
|
|
<div className="rounded-md border overflow-x-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Name</TableHead>
|
|
<TableHead>Subject</TableHead>
|
|
<TableHead>Current level</TableHead>
|
|
<TableHead>Target</TableHead>
|
|
<TableHead>Days active</TableHead>
|
|
<TableHead>Last decision</TableHead>
|
|
<TableHead>Progress</TableHead>
|
|
<TableHead>Alert</TableHead>
|
|
<TableHead />
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{rows.map((r) => (
|
|
<TableRow key={r.student_id}>
|
|
<TableCell className="font-medium">{r.name}</TableCell>
|
|
<TableCell>{r.subject}</TableCell>
|
|
<TableCell>{r.current_level}</TableCell>
|
|
<TableCell>{r.target}</TableCell>
|
|
<TableCell>{r.days_active}</TableCell>
|
|
<TableCell className="whitespace-nowrap text-sm text-muted-foreground">
|
|
{new Date(r.last_decision_at).toLocaleString()}
|
|
</TableCell>
|
|
<TableCell className="w-32">
|
|
<Spark vals={r.progress_sparkline} />
|
|
</TableCell>
|
|
<TableCell>
|
|
{r.alert ? (
|
|
<Badge variant="destructive" className="text-xs">
|
|
{r.alert}
|
|
</Badge>
|
|
) : (
|
|
"—"
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Button variant="link" className="px-0" asChild>
|
|
<Link to={`/admin/adaptive/student/${r.student_id}`}>Open</Link>
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
<div className="flex items-center justify-between mt-4 text-sm">
|
|
<span className="text-muted-foreground">
|
|
Page {page} of {pages}
|
|
</span>
|
|
<div className="flex gap-2">
|
|
<Button size="sm" variant="outline" disabled={page <= 1} onClick={() => setPage((p) => p - 1)}>
|
|
Previous
|
|
</Button>
|
|
<Button size="sm" variant="outline" disabled={page >= pages} onClick={() => setPage((p) => p + 1)}>
|
|
Next
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function MetricCard({
|
|
icon: Icon,
|
|
label,
|
|
value,
|
|
}: {
|
|
icon: ComponentType<{ className?: string }>;
|
|
label: string;
|
|
value: string;
|
|
}) {
|
|
return (
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">{label}</CardTitle>
|
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{value}</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function Spark({ vals }: { vals: number[] }) {
|
|
const data = (vals?.length ? vals : [0, 0, 0, 0, 0]).map((v, i) => ({ i, v }));
|
|
return (
|
|
<div className="h-10 w-28">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<LineChart data={data}>
|
|
<Line type="monotone" dataKey="v" stroke="hsl(var(--primary))" strokeWidth={2} dot={false} />
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
);
|
|
}
|