- 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
128 lines
5.4 KiB
TypeScript
128 lines
5.4 KiB
TypeScript
import { useState } from "react";
|
|
import { useParams, useNavigate } from "react-router-dom";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Progress } from "@/components/ui/progress";
|
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Loader2, CheckCircle2, ArrowRight, Clock } from "lucide-react";
|
|
import { useStartDiagnostic, useAnswerDiagnostic } from "@/hooks/queries";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import type { DiagnosticQuestion } from "@/types";
|
|
|
|
export default function DiagnosticTest() {
|
|
const { subjectId } = useParams<{ subjectId: string }>();
|
|
const navigate = useNavigate();
|
|
const { toast } = useToast();
|
|
const startMutation = useStartDiagnostic();
|
|
const answerMutation = useAnswerDiagnostic();
|
|
|
|
const [sessionId, setSessionId] = useState<number | null>(null);
|
|
const [currentQuestion, setCurrentQuestion] = useState<DiagnosticQuestion | null>(null);
|
|
const [selectedAnswer, setSelectedAnswer] = useState("");
|
|
const [questionIndex, setQuestionIndex] = useState(0);
|
|
const [totalQuestions, setTotalQuestions] = useState(0);
|
|
const [completed, setCompleted] = useState(false);
|
|
|
|
const handleStart = () => {
|
|
startMutation.mutate(Number(subjectId), {
|
|
onSuccess: (data) => {
|
|
setSessionId(data.session.id);
|
|
setCurrentQuestion(data.first_question);
|
|
setTotalQuestions(data.session.total_questions);
|
|
setQuestionIndex(1);
|
|
},
|
|
onError: () => toast({ title: "Error", description: "Failed to start diagnostic", variant: "destructive" }),
|
|
});
|
|
};
|
|
|
|
const handleAnswer = () => {
|
|
if (!sessionId || !currentQuestion || !selectedAnswer) return;
|
|
answerMutation.mutate(
|
|
{ session_id: sessionId, question_id: currentQuestion.id, answer: selectedAnswer },
|
|
{
|
|
onSuccess: (data) => {
|
|
setSelectedAnswer("");
|
|
if (data.completed) {
|
|
setCompleted(true);
|
|
} else if (data.next_question) {
|
|
setCurrentQuestion(data.next_question);
|
|
setQuestionIndex((i) => i + 1);
|
|
}
|
|
},
|
|
onError: () => toast({ title: "Error", description: "Failed to submit answer", variant: "destructive" }),
|
|
}
|
|
);
|
|
};
|
|
|
|
if (completed) {
|
|
return (
|
|
<div className="max-w-lg mx-auto text-center py-16 space-y-6">
|
|
<CheckCircle2 className="h-16 w-16 text-green-500 mx-auto" />
|
|
<h1 className="text-2xl font-bold">Diagnostic Complete!</h1>
|
|
<p className="text-muted-foreground">Your proficiency profile has been generated.</p>
|
|
<div className="flex gap-3 justify-center">
|
|
<Button onClick={() => navigate(`/student/proficiency/${subjectId}`)}>View Proficiency</Button>
|
|
<Button variant="outline" onClick={() => navigate(`/student/plan/${subjectId}`)}>See Learning Plan</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!sessionId) {
|
|
return (
|
|
<div className="max-w-lg mx-auto text-center py-16 space-y-6">
|
|
<Clock className="h-16 w-16 text-primary mx-auto opacity-60" />
|
|
<h1 className="text-2xl font-bold">Diagnostic Assessment</h1>
|
|
<p className="text-muted-foreground">This adaptive test will assess your current knowledge across all topics. Questions adapt to your performance in real time.</p>
|
|
<Button size="lg" onClick={handleStart} disabled={startMutation.isPending}>
|
|
{startMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
Begin Assessment
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-2xl mx-auto space-y-6">
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between text-sm text-muted-foreground">
|
|
<span>Question {questionIndex} of ~{totalQuestions}</span>
|
|
<Badge variant="outline">{currentQuestion?.difficulty}</Badge>
|
|
</div>
|
|
<Progress value={(questionIndex / totalQuestions) * 100} className="h-2" />
|
|
</div>
|
|
|
|
{currentQuestion && (
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-2">
|
|
<span>{currentQuestion.domain_name}</span>
|
|
<span>·</span>
|
|
<span>{currentQuestion.topic_name}</span>
|
|
</div>
|
|
<CardTitle className="text-lg">{currentQuestion.question_text}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{currentQuestion.options && (
|
|
<RadioGroup value={selectedAnswer} onValueChange={setSelectedAnswer}>
|
|
{currentQuestion.options.map((opt, i) => (
|
|
<div key={i} className="flex items-center space-x-3 rounded-lg border p-3 hover:bg-accent/50 transition-colors">
|
|
<RadioGroupItem value={opt} id={`opt-${i}`} />
|
|
<Label htmlFor={`opt-${i}`} className="flex-1 cursor-pointer">{opt}</Label>
|
|
</div>
|
|
))}
|
|
</RadioGroup>
|
|
)}
|
|
<Button className="w-full" onClick={handleAnswer} disabled={!selectedAnswer || answerMutation.isPending}>
|
|
{answerMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
Submit Answer <ArrowRight className="ml-1 h-4 w-4" />
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|