"use client"; import React, { useState, useEffect, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { notFound } from 'next/navigation'; import type { TestSession, UserAnswer, Question, TestResult } from '@/lib/types'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Label } from '@/components/ui/label'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'; import { useToast } from '@/hooks/use-toast'; import { ChevronLeft, ChevronRight, Bookmark, Circle, CheckCircle, XCircle, Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useUser, useFirestore } from '@/firebase'; import { doc, writeBatch } from 'firebase/firestore'; import ReactMarkdown from 'react-markdown'; import remarkMath from 'remark-math'; import rehypeKatex from 'rehype-katex'; const Palette = ({ session, currentQuestionIndex, onQuestionSelect, }: { session: TestSession; currentQuestionIndex: number; onQuestionSelect: (index: number) => void; }) => { return (
{session.questions.map((_, index) => { const status = session.userAnswers[index].status; let statusClass = ''; let isCurrent = index === currentQuestionIndex; if (isCurrent) { statusClass = 'bg-primary text-primary-foreground scale-110'; } else { switch (status) { case 'answered': statusClass = 'bg-green-500/20 border-green-700 text-green-400'; break; case 'not-answered': statusClass = 'bg-red-500/20 border-red-700 text-red-400'; break; case 'marked-for-review': statusClass = 'bg-purple-500/20 border-purple-700 text-purple-400'; break; case 'answered-and-marked-for-review': statusClass = 'bg-yellow-500/20 border-yellow-700 text-yellow-400'; break; default: statusClass = 'bg-muted/40 hover:bg-muted/80 border-white/20'; } } return ( ); })}
Answered
Not Answered
Marked
Answered & Marked
Not Visited
) } export function TestInterface({ testId }: { testId: string }) { const router = useRouter(); const { toast } = useToast(); const [session, setSession] = useState(null); const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); const [timeLeft, setTimeLeft] = useState(null); const [isMounted, setIsMounted] = useState(false); const { user } = useUser(); const firestore = useFirestore(); const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { const sessionData = localStorage.getItem(`test_session_${testId}`); if (sessionData) { const parsedSession: TestSession = JSON.parse(sessionData); setSession(parsedSession); if (parsedSession.config.timeLimit) { const elapsedTime = Math.floor((Date.now() - parsedSession.startTime) / 1000); setTimeLeft(parsedSession.config.timeLimit * 60 - elapsedTime); } } setIsMounted(true); }, [testId]); const updateSessionState = (newSession: TestSession) => { setSession(newSession); localStorage.setItem(`test_session_${testId}`, JSON.stringify(newSession)); } useEffect(() => { if(!session || session.userAnswers[currentQuestionIndex].status !== 'not-visited') { return; } const newAnswers = [...session.userAnswers]; newAnswers[currentQuestionIndex].status = 'not-answered'; updateSessionState({ ...session, userAnswers: newAnswers }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentQuestionIndex, session]); const submitTest = useCallback(async () => { if (!session || !user || !firestore) { if (!user) { toast({ title: 'Not Signed In', description: 'Please sign in to save your results.', variant: 'destructive'}); } return; }; setIsSubmitting(true); try { const endTime = Date.now(); let correctAnswers = 0; let incorrectAnswers = 0; let skipped = 0; const performanceByCategory: { [key: string]: { correct: number; incorrect: number; skipped: number; total: number; type: 'subject' | 'topic' } } = {}; session.questions.forEach((q, i) => { const categoryName = q.topic || q.subject; const categoryType = q.topic ? 'topic' as const : 'subject' as const; if (!performanceByCategory[categoryName]) { performanceByCategory[categoryName] = { correct: 0, incorrect: 0, skipped: 0, total: 0, type: categoryType }; } performanceByCategory[categoryName].total++; const userAnswer = session.userAnswers[i]; if (userAnswer.status === 'not-visited' || userAnswer.status === 'not-answered' || userAnswer.status === 'marked-for-review') { skipped++; performanceByCategory[categoryName].skipped++; } else if (userAnswer.selectedOption === q.correctAnswerIndex) { correctAnswers++; performanceByCategory[categoryName].correct++; } else { incorrectAnswers++; performanceByCategory[categoryName].incorrect++; } }); const attempted = correctAnswers + incorrectAnswers; const accuracy = attempted > 0 ? (correctAnswers / attempted) * 100 : 0; let score = correctAnswers; if(session.config.negativeMarking && session.config.negativeMarkingRatio) { score -= incorrectAnswers * session.config.negativeMarkingRatio; } const performanceBreakdown: import('@/lib/types').PerformanceBreakdown[] = Object.entries(performanceByCategory).map(([category, stats]) => { const attemptedInCategory = stats.correct + stats.incorrect; return { category: category, categoryType: stats.type, accuracy: attemptedInCategory > 0 ? (stats.correct / attemptedInCategory) * 100 : 0, correctCount: stats.correct, incorrectCount: stats.incorrect, skippedCount: stats.skipped, totalCount: stats.total, }; }); const result: TestResult = { id: session.id, userId: user.uid, config: session.config, score, accuracy, totalQuestions: session.questions.length, correctAnswers, incorrectAnswers, skipped, takenAt: new Date(session.startTime).toISOString(), timeTaken: Math.floor((endTime - session.startTime) / 1000), performanceBreakdown, }; const finalSession: TestSession = { ...session, userId: user.uid, endTime: endTime, userAnswers: session.userAnswers.map(ua => ({...ua, selectedOption: ua.selectedOption === undefined ? null : ua.selectedOption})) }; const batch = writeBatch(firestore); const resultRef = doc(firestore, 'users', user.uid, 'testResults', session.id); batch.set(resultRef, result); const sessionRef = doc(firestore, 'users', user.uid, 'userTests', session.id); batch.set(sessionRef, finalSession); await batch.commit(); localStorage.removeItem(`test_session_${session.id}`); toast({ title: 'Test Submitted! 🎉', description: 'Your results have been saved. Redirecting...' }); router.replace(`/results/${session.id}`); } catch (error) { console.error("Failed to submit test:", error); toast({ title: "Submission Failed", description: "Could not save your test results. Please check your connection and try again.", variant: "destructive", }); setIsSubmitting(false); } }, [session, router, user, firestore, toast]); useEffect(() => { if (timeLeft === null) return; if (timeLeft <= 0) { submitTest(); return; } const timer = setInterval(() => setTimeLeft(t => (t ?? 0) - 1), 1000); return () => clearInterval(timer); }, [timeLeft, submitTest]); const updateAnswer = (questionIndex: number, selectedOption?: number, statusUpdate?: Partial) => { if (!session) return; const newAnswers = [...session.userAnswers]; const currentAnswer = newAnswers[questionIndex]; let newSession: TestSession; if (selectedOption !== undefined) { const newStatus = currentAnswer.status === 'marked-for-review' || currentAnswer.status === 'answered-and-marked-for-review' ? 'answered-and-marked-for-review' : 'answered'; newAnswers[questionIndex] = { ...currentAnswer, selectedOption, status: newStatus }; newSession = { ...session, userAnswers: newAnswers }; } else if (statusUpdate) { newAnswers[questionIndex] = { ...currentAnswer, ...statusUpdate }; newSession = { ...session, userAnswers: newAnswers }; } else { // Clear response const newStatus = currentAnswer.status === 'answered-and-marked-for-review' ? 'marked-for-review' : 'not-answered'; newAnswers[questionIndex] = { ...currentAnswer, selectedOption: null, status: newStatus }; newSession = { ...session, userAnswers: newAnswers }; } updateSessionState(newSession); }; const handleOptionChange = (value: string) => { updateAnswer(currentQuestionIndex, parseInt(value)); }; const handleMarkForReview = () => { const currentStatus = session?.userAnswers[currentQuestionIndex].status; let newStatus: UserAnswer['status']; if (currentStatus === 'answered') { newStatus = 'answered-and-marked-for-review'; } else if (currentStatus === 'answered-and-marked-for-review') { newStatus = 'answered'; } else if (currentStatus === 'marked-for-review') { newStatus = 'not-answered'; } else { // not-answered or not-visited newStatus = 'marked-for-review'; } updateAnswer(currentQuestionIndex, undefined, { status: newStatus }); }; const clearResponse = () => { updateAnswer(currentQuestionIndex, undefined, { selectedOption: null }); }; const handleQuestionSelect = (index: number) => { setCurrentQuestionIndex(index); }; const currentQuestion: Question | undefined = session?.questions[currentQuestionIndex]; const currentUserAnswer: UserAnswer | undefined = session?.userAnswers[currentQuestionIndex]; if (!isMounted) return
; if (!session || !currentQuestion) { useEffect(() => notFound(), []); return null; } const formatTime = (seconds: number) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; }; return (

Question {currentQuestionIndex + 1} of {session.questions.length}

{session.config.exam}
{timeLeft !== null && (
Time Left
{formatTime(timeLeft)}
)}
{currentQuestion.questionText}
{currentQuestion.options.map((option, index) => ( ))}
{currentQuestionIndex < session.questions.length - 1 ? ( ) : ( Ready to finish? This will end the test and calculate your score. You cannot go back after submitting. Cancel {isSubmitting ? ( <> Submitting... ) : ( 'Submit' )} )}
); }