feat(v3): restructure project + add complete frontend
- 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
This commit is contained in:
158
src/pages/admin/AdaptiveDashboard.tsx
Normal file
158
src/pages/admin/AdaptiveDashboard.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user