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 AnswerEntry { question_id: number; answer: string; score: number; is_correct: boolean; feedback: string; } interface ExamResultsData { attempt_id: number; exam_id: number | null; exam_title?: string; status: string; completed_at?: string; released_at?: string; started_at?: string | null; finished_at?: string | null; total_score?: number; max_score?: number; percentage?: number; listening_band?: number; reading_band?: number; writing_band?: number; speaking_band?: number; overall_band?: number; cefr_level?: string; scores?: ScoreEntry[]; feedback?: FeedbackEntry[]; answers?: AnswerEntry[]; } 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 hasDetailedScores = Array.isArray(results.scores) && results.scores.length > 0; const overall = results.overall_band ?? 0; const pct = results.percentage ?? (results.max_score ? Math.round((results.total_score ?? 0) / results.max_score * 100) : 0); const cefr = results.cefr_level?.toUpperCase() || "N/A"; const passed = hasDetailedScores ? overall >= 7 : pct >= 70; const skillScores = hasDetailedScores ? 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 feedbackList = results.feedback ?? []; const feedbackBySkill: Record = {}; for (const fb of feedbackList) { const key = "General"; if (!feedbackBySkill[key]) feedbackBySkill[key] = []; feedbackBySkill[key].push(fb); } const answerEntries = results.answers ?? []; return (
{results.exam_title && (

{results.exam_title}

)}

Your overall result

{hasDetailedScores ? (
{overall} Band
) : (
{pct}%
)}

{results.total_score ?? 0} / {results.max_score ?? 0} marks

{hasDetailedScores &&

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}` : "—"} ))}
{answerEntries.length > 0 && ( Your Answers {answerEntries.filter((a) => a.is_correct).length} correct out of {answerEntries.length} # Your Answer Score Result {answerEntries.map((a, i) => ( {i + 1} {a.answer || "—"} {a.score} {a.is_correct ? "Correct" : "Incorrect"} ))}
)} {feedbackList.length > 0 && (

Feedback

{feedbackList.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.

); }