import { Link, useParams, useSearchParams } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, ResponsiveContainer, } from "recharts"; import { Award, BookOpen, Download, RefreshCw, Loader2 } from "lucide-react"; import { examSessionService } from "@/services/exam-session.service"; import { reportService } from "@/services/report.service"; import { useState } from "react"; interface ScoreEntry { skill: string; band_score: number; raw_score: number; max_score: number; cefr_level: string; } interface FeedbackEntry { question_id: number | null; feedback_text: string; source: string; } interface ExamResultsData { attempt_id: number; exam_id: number | null; status: string; completed_at: string; released_at: string; listening_band: number; reading_band: number; writing_band: number; speaking_band: number; overall_band: number; cefr_level: string; scores: ScoreEntry[]; feedback: FeedbackEntry[]; } export default function ExamResults() { const { examId } = useParams(); const [searchParams] = useSearchParams(); const practice = searchParams.get("mode") === "practice"; const [downloading, setDownloading] = useState(false); const { data: results, isLoading, isError } = useQuery({ queryKey: ["exam-results", examId], queryFn: () => examSessionService.getResults(Number(examId)), enabled: !!examId, }); const handleDownloadPdf = async () => { if (!results?.attempt_id) return; setDownloading(true); try { await reportService.downloadPdf(results.attempt_id); } catch { // error handled silently } finally { setDownloading(false); } }; if (isLoading) { return (
); } if (isError || !results) { return (

Results not yet available

Your results may still be pending approval or grading.

); } const overall = results.overall_band; const cefr = results.cefr_level?.toUpperCase() || "N/A"; const passed = overall >= 7; const skillScores = results.scores.filter((s) => s.skill !== "overall"); const SKILLS = skillScores.map((s) => ({ skill: s.skill.charAt(0).toUpperCase() + s.skill.slice(1), band: s.band_score, cefr: s.cefr_level?.toUpperCase() || "N/A", gap: Math.max(0, 8 - s.band_score), target: 8, })); const RADAR_DATA = SKILLS.map((s) => ({ skill: s.skill, band: s.band })); const feedbackBySkill: Record = {}; for (const fb of results.feedback) { const key = "General"; if (!feedbackBySkill[key]) feedbackBySkill[key] = []; feedbackBySkill[key].push(fb); } return (

Your overall result

{overall} Band

CEFR equivalent: {cefr}

{practice ? Practice mode : null}
{RADAR_DATA.length > 0 && ( Skill profile Per-skill performance vs maximum scale
)} Skills breakdown Skill Band CEFR Gap to Target {SKILLS.map((s) => ( {s.skill} {s.band} {s.cefr} {s.gap > 0 ? `−${s.gap}` : "—"} ))}
{results.feedback.length > 0 && (

Feedback

{results.feedback.map((fb, i) => ( {fb.source === "ai" ? "AI Feedback" : fb.source === "teacher" ? "Teacher Feedback" : "Feedback"}{" "} {fb.question_id ? `(Q${fb.question_id})` : ""} {fb.feedback_text || "No detailed feedback available."} ))}
)} Areas to improve Focus recommendations based on this attempt
    {SKILLS.filter((s) => s.gap > 0) .sort((a, b) => b.gap - a.gap) .map((s) => (
  • Focus on {s.skill} — current band {s.band}, target {s.target} (gap: {s.gap}).
  • ))} {SKILLS.every((s) => s.gap === 0) &&
  • Excellent performance across all skills!
  • }
Pass path Congratulations — you are on track for your goal.

Consider registering for the official test while skills are strong.

Below threshold Close the gap with targeted training.

Start adaptive training tailored to your weakest skills.

); }