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:
Yamen Ahmad
2026-04-10 17:26:42 +04:00
commit 11a7265460
392 changed files with 62287 additions and 0 deletions

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