"use client" import { useEffect, useState, use, useRef } from "react" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Clock, Flag, ChevronLeft, ChevronRight, Pause as PauseIcon, MoreVertical, Calculator as CalculatorIcon, FlaskConical, Edit3, XCircle, Play, Shield, AlertTriangle, Moon, Sun } from "lucide-react" import Image from "next/image" import { useCBTStore, QuestionStatus } from "@/store/cbtStore" import { useExamTimer } from "@/hooks/useExamTimer" import ReactMarkdown from "react-markdown" import remarkMath from "remark-math" import rehypeKatex from "rehype-katex" import rehypeRaw from "rehype-raw" import "katex/dist/katex.min.css" import { Calculator } from "@/components/CBT/Calculator" import { Scratchpad } from "@/components/CBT/Scratchpad" import { PeriodicTable } from "@/components/CBT/PeriodicTable" import { ExamResult } from "@/components/CBT/ExamResult" // Dummy data for testing the CBT client engine const DUMMY_QUESTIONS = Array.from({ length: 20 }).map((_, i) => ({ id: `q_${i+1}`, number: i + 1, subject: "Physics", text_en: `A solid cylinder of mass M and radius R is rolling without slipping down an inclined plane of angle θ. Question ${i+1}. What is the minimum coefficient of static friction μ_s required to prevent slipping?`, text_hi: `द्रव्यमान M और त्रिज्या R का एक ठोस बेलन कोण θ के एक आनत तल पर बिना फिसले लुढ़क रहा है। प्रश्न ${i+1}...`, options: ['(1/3) tan θ', '(2/3) tan θ', '(1/2) tan θ', 'tan θ'] })); export default function CBTExamPage({ params }: { params: Promise<{ examId: string }> }) { const resolvedParams = use(params); const examId = resolvedParams.examId; const [mounted, setMounted] = useState(false); const [examType, setExamType] = useState('Test'); const [showCalc, setShowCalc] = useState(false); const [showScratchpad, setShowScratchpad] = useState(false); const [showPeriodicTable, setShowPeriodicTable] = useState(false); const [isPaused, setIsPaused] = useState(false); const [showSubmitConfirm, setShowSubmitConfirm] = useState(false); const [accessError, setAccessError] = useState(null); const [localTheme, setLocalTheme] = useState('light'); useEffect(() => { if (typeof window !== 'undefined') { if (document.documentElement.classList.contains('dark')) { setLocalTheme('dark'); } else { setLocalTheme('light'); } } }, []); const handleThemeToggle = () => { if (document.documentElement.classList.contains('dark')) { document.documentElement.classList.remove('dark'); localStorage.setItem('theme', 'light'); setLocalTheme('light'); } else { document.documentElement.classList.add('dark'); localStorage.setItem('theme', 'dark'); setLocalTheme('dark'); } }; // Anti-cheat state const [violationCount, setViolationCount] = useState(0); const [showWarning, setShowWarning] = useState(false); const hasShownWarning = useRef(false); const hasAutoSubmitted = useRef(false); const { initExam, startExam, questions, answers, currentQuestionIndex, selectAnswer, clearResponse, markForReview, goToQuestion, addTimeSpent, submitExam, getAttemptPayload, submitAttemptToServer, resetExamState, status, attemptId, } = useCBTStore(); const { formattedTime, isTimedOut } = useExamTimer(); const [questionStartTime, setQuestionStartTime] = useState(Date.now()); // Initialize Exam on mount useEffect(() => { async function fetchExamData() { try { const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; const testRes = await fetch(`${apiUrl}/api/tests/${examId}?user_id=b0e17378-864d-439e-8806-708dc48a891b`); if (!testRes.ok) throw new Error("Failed to fetch test structure"); const testData = await testRes.json(); // Strict boundary checking const now = new Date(); if (testData.scheduled_date && new Date(testData.scheduled_date) > now) { setAccessError(`This exam is scheduled for ${new Date(testData.scheduled_date).toLocaleString()}. You cannot start it yet.`); setMounted(true); return; } if (testData.end_date && new Date(testData.end_date) < now) { setAccessError(`This exam expired on ${new Date(testData.end_date).toLocaleString()} and is no longer available.`); setMounted(true); return; } setExamType(testData.type); // Extract all question IDs from sections const questionIds: string[] = []; Object.values(testData.sections).forEach((section: any) => { questionIds.push(...section.questions); }); const qRes = await fetch(`${apiUrl}/api/tests/${examId}/questions`); if (!qRes.ok) throw new Error("Failed to fetch questions in bulk"); const allQuestionsData = await qRes.json(); // Map questions by ID for fast lookup const questionsMap = new Map(); allQuestionsData.forEach((q: any) => { questionsMap.set(q.id, q); }); // Construct fetchedQuestions retaining the exact order of questionIds const fetchedQuestions = questionIds.map((id, idx) => { const qData = questionsMap.get(id); if (!qData) return null; // Fallback in case of mismatch return { id: qData.id, number: idx + 1, subject: qData.subject, type: qData.type, text_en: qData.question_text, text_hi: qData.question_text_hi || qData.question_text, options: Object.values(qData.options || {}), options_hi: Object.values(qData.options_hi || qData.options || {}), }; }).filter(Boolean) as any[]; initExam(examId, fetchedQuestions, testData.total_time * 60); setMounted(true); } catch (err) { console.error("Failed to load exam from network, falling back to dummy data:", err); initExam(examId, DUMMY_QUESTIONS, 10800); setMounted(true); } } fetchExamData(); }, [examId, initExam]); // Track time spent per question useEffect(() => { if (status === 'IN_PROGRESS') { setQuestionStartTime(Date.now()); } }, [currentQuestionIndex, status]); // Anti-Cheat: Tab switching listener useEffect(() => { if (status !== 'IN_PROGRESS') return; const handleVisibilityChange = () => { if (document.visibilityState === 'hidden') { setViolationCount((prev) => prev + 1); } }; document.addEventListener("visibilitychange", handleVisibilityChange); return () => document.removeEventListener("visibilitychange", handleVisibilityChange); }, [status]); // Handle side-effects of violation count changing useEffect(() => { if (violationCount === 1 && !hasShownWarning.current) { hasShownWarning.current = true; setShowWarning(true); } else if (violationCount >= 2 && status !== 'SUBMITTED' && status !== 'TIMED_OUT' && !hasAutoSubmitted.current) { hasAutoSubmitted.current = true; submitExam('SUBMITTED'); submitAttemptToServer('b0e17378-864d-439e-8806-708dc48a891b') .then(() => { window.alert("Test Auto-Submitted due to multiple tab switching violations."); }) .catch(err => { console.error("Auto-submit fetch error:", err); window.alert("Test Auto-Submitted due to multiple tab switching violations."); }); } }, [violationCount, status, submitExam, submitAttemptToServer]); const startTest = async () => { try { if (document.documentElement.requestFullscreen) { await document.documentElement.requestFullscreen(); } } catch (e) { console.warn("Fullscreen API failed", e); } startExam(); }; const handleNext = () => { if (!currentQuestion) return; const timeSpent = Math.floor((Date.now() - questionStartTime) / 1000); addTimeSpent(currentQuestion.id, timeSpent); if (currentQuestionIndex < questions.length - 1) { goToQuestion(currentQuestionIndex + 1); } else { goToQuestion(0); } } const handlePrev = () => { if (!currentQuestion) return; const timeSpent = Math.floor((Date.now() - questionStartTime) / 1000); addTimeSpent(currentQuestion.id, timeSpent); if (currentQuestionIndex > 0) { goToQuestion(currentQuestionIndex - 1); } } const handleGridClick = (index: number) => { if (!currentQuestion) return; const timeSpent = Math.floor((Date.now() - questionStartTime) / 1000); addTimeSpent(currentQuestion.id, timeSpent); goToQuestion(index); } const handleSubmit = () => { setShowSubmitConfirm(true); } const confirmSubmit = async () => { setShowSubmitConfirm(false); submitExam('SUBMITTED'); const payload = getAttemptPayload('b0e17378-864d-439e-8806-708dc48a891b'); console.log("EXAM SUBMITTED. PAYLOAD:", JSON.stringify(payload, null, 2)); await submitAttemptToServer('b0e17378-864d-439e-8806-708dc48a891b'); } if (!mounted || questions.length === 0) { return
Initializing CBT Engine
} if (accessError) { return (

Access Denied

{accessError}

); } if (status === 'NOT_STARTED') { return (

Secure Exam Environment

Target: {questions.some(q => q.subject.toLowerCase() === 'biology') ? 'NEET' : 'JEE'} Pattern Test

  • The test will open in Full Screen Mode.
  • Do not switch tabs or exit full screen.
  • First violation will result in a strict warning.
  • Second violation will automatically submit your exam to prevent cheating.
  • Ensure your internet connection is stable before starting.
Disclaimer: Only the score obtained in your first attempt will be considered for growth tracking and performance evaluation. Any subsequent attempts are intended solely for practice and will not be included in the official assessment.

Preferred Theme

Choose dark or light mode for the exam

); } if (status === 'SUBMITTED' || status === 'TIMED_OUT') { const payload = getAttemptPayload('b0e17378-864d-439e-8806-708dc48a891b'); if (!payload) return
Loading results...
; return ( { resetExamState(); window.location.reload(); } : () => {}} /> ) } const currentQuestion = questions[currentQuestionIndex]; const currentState = currentQuestion ? answers[currentQuestion.id] : null // Helper to ensure math options missing $ delimiters get rendered properly const renderLatex = (text: string) => { if (typeof text !== 'string') return text; // If it contains backslash but no dollar signs, we might want to wrap the whole thing in $ $ if (text.includes('\\') && !text.includes('$')) { return `$${text}$`; } return text; }; const handleOptionSelect = (qId: string, letter: string, type: string) => { if (type === 'MCQ_Multiple' || type === 'Multiple_Choice') { const currentSelected = currentState?.selected_answer || ""; const selectedArray = currentSelected.split(',').filter(Boolean); if (selectedArray.includes(letter)) { const newArray = selectedArray.filter((l: string) => l !== letter).sort(); if (newArray.length > 0) { selectAnswer(qId, newArray.join(',')); } else { clearResponse(qId); } } else { selectedArray.push(letter); selectAnswer(qId, selectedArray.sort().join(',')); } } else { selectAnswer(qId, letter); } } // Effect to close overlay tools on Escape const paletteStats = Object.values(answers || {}).reduce( (acc, val) => { acc[val.status]++; return acc; }, { UNVISITED: 0, NOT_ANSWERED: 0, ANSWERED: 0, MARKED: 0, ANSWERED_MARKED: 0 } as Record ); // Group questions by subject for the side panel const questionsBySubject = questions.reduce((acc, q, index) => { if (!acc[q.subject]) { acc[q.subject] = []; } acc[q.subject].push({ ...q, originalIndex: index }); return acc; }, {} as Record); return (
{/* Warning Modal */} {showWarning && (

Warning!

You switched tabs or left the secure window.

This is your final warning. Doing this again will automatically submit your exam and your score will be evaluated as is.

)}
PragatiVeer PragatiVeer

AITS Full Syllabus Test

{currentQuestion.subject}

{formattedTime}
{/* Main Question Area - Configured for Bilingual Split Pane */}
Question {currentQuestion.number} English
+4 / -1
{currentQuestion.text_en}
{(currentQuestion.type === 'MCQ_Multiple' || currentQuestion.type === 'Multiple_Choice') && (

This question has MULTIPLE CORRECT OPTIONS. Please select all that apply.

)}
{currentQuestion.type === 'Numerical' ? ( selectAnswer(currentQuestion.id, e.target.value)} /> ) : currentQuestion.type === 'Subjective' ? (