<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Math Mock Test</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
/* Apply box-sizing globally for consistent layout behavior */
*, *::before, *::after {
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background-color: #f0f2f5;
margin: 0;
padding: 0;
overflow-x: hidden; /* Prevent horizontal scroll due to drawer positioning */
}
textarea::-webkit-scrollbar {
width: 8px;
}
textarea::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
textarea::-webkit-scrollbar-thumb {
background: #888;
border-radius: 10px;
}
textarea::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Input and Result pages container for full width */
.page-container {
width: 100%;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
}
.page-card {
background-color: white;
padding: 2rem;
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
width: 100%; /* Full width on mobile */
max-width: 768px; /* Max width for larger screens */
}
/* Mocktest page specific styles for full width without scrolling */
#mocktest-page {
width: 100%;
height: 100vh;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
#mocktest-page .mocktest-content-wrapper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: white;
overflow: hidden; /* Manage overflow of content within wrapper */
}
/* Sliding Panel styles */
#question-panel {
position: fixed;
top: 0;
right: 0;
width: 80%; /* Adjust width for mobile */
max-width: 300px; /* Max width for larger screens */
height: 100vh;
background-color: #f8fafc;
transform: translateX(100%);
transition: transform 0.3s ease-in-out;
z-index: 1000;
box-shadow: -4px 0 10px rgba(0, 0, 0, 0.2);
padding: 1rem;
display: flex;
flex-direction: column;
}
#question-panel.open {
transform: translateX(0);
}
/* Overlay for when panel is open */
#panel-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
display: none;
}
#panel-overlay.active {
display: block;
}
/* Chart container for responsiveness */
.chart-container {
position: relative;
height: 300px;
width: 100%;
}
/* Container for horizontal bar chart to manage its scrolling */
.horizontal-chart-wrapper {
overflow-x: auto;
width: 100%;
padding-bottom: 10px;
}
#timePerQuestionChartCanvas {
min-height: 200px;
}
/* Specific styles for feedback modal */
#question-feedback-modal {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.75);
display: none;
align-items: center;
justify-content: center;
padding: 1rem;
z-index: 2000;
}
#question-feedback-modal .modal-content {
background-color: white;
padding: 1.5rem;
border-radius: 0.5rem;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
width: 100%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
position: relative;
}
#question-feedback-modal .modal-close-btn {
position: absolute;
top: 10px;
right: 10px;
padding: 8px;
background: none;
border: none;
cursor: pointer;
color: #4a5568;
font-size: 1.5rem;
}
/* Adjust top bar spacing to ensure icons are not cut off */
.mocktest-content-wrapper .top-bar-icons {
display: flex;
align-items: center;
padding-right: 0.5rem;
}
/* Ensure question text doesn't overflow horizontally */
.mocktest-content-wrapper .question-text-area {
word-wrap: break-word;
overflow-wrap: break-word;
padding-left: 1rem;
padding-right: 1rem;
}
</style>
</head>
<body class="flex flex-col min-h-screen">
<div id="input-page" class="page-container">
<div class="page-card">
<h1 class="text-2xl font-bold text-center mb-6 text-gray-800">
<span class="text-green-600">Powered by</span> Joharul Hasan
</h1>
<p class="text-center text-gray-600 mb-8">
Paste your MCQ questions below and generate an interactive quiz
</p>
<div class="mb-6">
<label for="question-input" class="block text-gray-700 text-lg font-medium mb-3">Enter MCQ Questions</label>
<textarea id="question-input" rows="15"
class="w-full p-4 border-2 border-blue-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent text-gray-800 resize-y"
placeholder="Paste your questions here. Example format:
8. Shatabdi Express left Delhi 40 min late. After covering half the distance, it increased its usual speed by 1⁄6 and reached Chandigarh on scheduled time. Find usual time for the journey.
(a) 560 min
(b) 520 min
(c) 540 min
(d) 420 min
Answer: (a) 560 min
14. A boy goes to school at 3 km/hr and returns at 2 km/hr. If he takes 5 hours total, find the distance to school.
(a) 5 km
(b) 6 km
(c) 7 km
(d) 8 km
Answer: (b) 6 km
"></textarea>
</div>
<button id="start-mocktest-btn"
class="w-full bg-gradient-to-r from-blue-500 to-purple-600 text-white font-semibold py-3 px-6 rounded-lg shadow-lg hover:from-blue-600 hover:to-purple-700 transition duration-300 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75">
Start Mock Test
</button>
</div>
</div>
<div id="mocktest-page" class="hidden">
<!-- Mock test content will be injected here by JavaScript -->
</div>
<div id="result-page" class="hidden page-container">
<!-- Result summary will be injected here by JavaScript -->
</div>
<!-- Custom Modal for Alerts (instead of alert()) -->
<div id="custom-alert-modal" class="hidden fixed inset-0 bg-gray-900 bg-opacity-75 flex items-center justify-center p-4 z-50">
<div class="bg-white p-6 rounded-lg shadow-xl text-center w-full max-w-sm">
<p id="custom-alert-message" class="text-lg font-semibold text-gray-800 mb-6"></p>
<button id="custom-alert-ok-btn"
class="bg-blue-500 text-white font-semibold py-2 px-6 rounded-lg shadow hover:bg-blue-600 transition duration-200">
OK
</button>
</div>
</div>
<!-- Panel Overlay -->
<div id="panel-overlay" class="hidden"></div>
<!-- Question Number Panel (Sliding Drawer) -->
<div id="question-panel" class="">
<!-- Time Left summary at the top of the panel -->
<div class="mb-4 w-full bg-white p-4 rounded-lg shadow-inner">
<div class="flex justify-between items-center mb-2">
<div class="flex items-center text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Time Left
</div>
<span id="total-time-left-panel" class="font-semibold text-blue-600">00:00:00</span>
</div>
</div>
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-semibold text-gray-800">Questions</h3>
<button id="close-panel-btn" class="text-gray-500 hover:text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Attempted, Unattempted, Marked at the top of the panel, under Time Left -->
<div class="mb-4 w-full bg-white p-4 rounded-lg shadow-inner flex flex-col items-center">
<div class="flex justify-between items-center w-full mb-2">
<div class="flex items-center text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Attempted
</div>
<span id="attempted-count-panel" class="font-semibold text-green-600">0</span>
</div>
<div class="flex justify-between items-center w-full mb-2">
<div class="flex items-center text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Unattempted
</div>
<span id="unattempted-count-panel" class="font-semibold text-red-600">0</span>
</div>
<div class="flex justify-between items-center w-full">
<div class="flex items-center text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.324 1.108l1.519 4.674c.3.921-.755 1.688-1.539 1.108l-3.976-2.888a1 1 0 00-1.108 0l-3.976 2.888c-.784.58-1.838-.187-1.539-1.108l1.519-4.674a1 1 0 00-.324-1.108L2.245 9.401c-.783-.57-.381-1.81.588-1.81h4.915a1 1 0 00.95-.69l1.519-4.674z" />
</svg>
Marked
</div>
<span id="marked-count-panel" class="font-semibold text-orange-500">0</span>
</div>
</div>
<div id="panel-question-grid" class="grid grid-cols-4 gap-2 overflow-y-auto flex-grow">
<!-- Question numbers will be injected here -->
</div>
<!-- Submit Test button at the bottom of the panel -->
<button id="submit-test-panel-btn"
class="mt-4 w-full bg-red-600 text-white font-semibold py-3 px-6 rounded-lg shadow-lg hover:bg-red-700 transition duration-300 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-opacity-75">
SUBMIT TEST
</button>
</div>
<!-- Question Feedback Modal -->
<div id="question-feedback-modal" class="hidden">
<div class="modal-content">
<button class="modal-close-btn" onclick="document.getElementById('question-feedback-modal').style.display='none'">
×
</button>
<h3 id="feedback-question-title" class="text-xl font-bold text-gray-800 mb-4"></h3>
<p id="feedback-question-text" class="text-lg text-gray-700 mb-2"></p>
<div id="feedback-status-tag" class="text-sm font-semibold px-2 py-1 rounded-full mb-4 inline-block"></div>
<div id="feedback-time-taken" class="flex items-center text-sm ml-2 mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span id="feedback-time-value" class="font-medium"></span>
</div>
<div id="feedback-options-container" class="space-y-2 mb-4">
<!-- Options will be injected here -->
</div>
<div class="flex justify-between mt-4">
<button id="feedback-prev-btn" class="bg-gray-300 text-gray-800 font-semibold py-2 px-4 rounded-lg hover:bg-gray-400 transition">Previous</button>
<button id="feedback-next-btn" class="bg-blue-600 text-white font-semibold py-2 px-4 rounded-lg hover:bg-blue-700 transition">Next</button>
</div>
</div>
</div>
<script>
// --- Custom Alert Function ---
function showAlert(message) {
const modal = document.getElementById('custom-alert-modal');
const msgElement = document.getElementById('custom-alert-message');
const okBtn = document.getElementById('custom-alert-ok-btn');
msgElement.textContent = message;
modal.classList.remove('hidden');
okBtn.onclick = () => {
modal.classList.add('hidden');
};
}
// --- DOM Elements ---
const inputPage = document.getElementById('input-page');
const mocktestPage = document.getElementById('mocktest-page');
const resultPage = document.getElementById('result-page');
const startMockTestBtn = document.getElementById('start-mocktest-btn');
const questionInput = document.getElementById('question-input');
const questionPanel = document.getElementById('question-panel');
const closePanelBtn = document.getElementById('close-panel-btn');
const panelOverlay = document.getElementById('panel-overlay');
const submitTestPanelBtn = document.getElementById('submit-test-panel-btn');
// For feedback modal
const questionFeedbackModal = document.getElementById('question-feedback-modal');
const feedbackQuestionTitle = document.getElementById('feedback-question-title');
const feedbackQuestionText = document.getElementById('feedback-question-text');
const feedbackStatusTag = document.getElementById('feedback-status-tag');
const feedbackTimeValue = document.getElementById('feedback-time-value');
const feedbackOptionsContainer = document.getElementById('feedback-options-container');
const feedbackPrevBtn = document.getElementById('feedback-prev-btn');
const feedbackNextBtn = document.getElementById('feedback-next-btn');
// --- Global Variables ---
let questions = [];
let currentQuestionIndex = 0;
let questionStartTimes = [];
let questionEndTimes = [];
let totalTestStartTime;
let totalTestEndTime;
let selectedAnswers = [];
let totalTimerInterval;
let currentQuestionTimerInterval;
let filteredQuestionIndices = [];
let currentFeedbackIndex = 0;
// --- Constants for Scoring ---
const CORRECT_MARKS = 2;
const WRONG_MARKS = -0.5;
const TOTAL_TEST_DURATION_MINUTES = 60;
const ESTIMATED_EXAM_QUESTIONS = 25;
// --- Utility Functions ---
/**
* Parses the raw text input into an array of question objects.
* @param {string} rawText - The raw text input from the textarea.
* @returns {Array<Object>} An array of parsed question objects.
*/
function parseQuestions(rawText) {
const lines = rawText.split('\n').map(line => line.trim()).filter(line => line.length > 0);
const parsed = [];
let currentQuestion = null;
let optionsCounter = 0;
lines.forEach(line => {
const questionMatch = line.match(/^(\d+)\.\s(.+)/);
if (questionMatch) {
if (currentQuestion) {
parsed.push(currentQuestion);
}
currentQuestion = {
originalNumber: parseInt(questionMatch[1]),
question: questionMatch[2].trim(),
options: [],
answer: ''
};
optionsCounter = 0;
} else if (line.match(/^\([a-d]\)\s/) && currentQuestion && optionsCounter < 4) {
currentQuestion.options.push(line);
optionsCounter++;
} else if (line.startsWith('Answer:')) {
if (currentQuestion) {
const match = line.match(/\(([a-d])\)/);
if (match && match[1]) {
currentQuestion.answer = match[1];
}
}
} else {
if (currentQuestion && currentQuestion.options.length === 0 && !currentQuestion.answer) {
currentQuestion.question += ' ' + line;
}
}
});
if (currentQuestion) {
parsed.push(currentQuestion);
}
return parsed;
}
/**
* Formats total seconds into a human-readable string like "35sec" or "1minute 25sec".
* Does not include milliseconds or decimal points.
* @param {number} totalSeconds - The total number of seconds.
* @returns {string} Formatted time string.
*/
function formatSecondsToWords(totalSeconds) {
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
let result = '';
if (minutes > 0) {
result += `${minutes}minute`;
if (minutes > 1) result += 's'; // Pluralize minutes
}
if (seconds > 0) {
if (result.length > 0) result += ' '; // Add space if minutes exist
result += `${seconds}sec`;
} else if (totalSeconds === 0 && result.length === 0) {
result = '0sec'; // Display 0sec if total is 0
}
return result;
}
/**
* Formats a time in seconds into HH:MM:SS format.
* @param {number} totalSeconds - The total number of seconds.
* @returns {string} Formatted time string (HH:MM:SS).
*/
function formatHoursMinutesSeconds(totalSeconds) {
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const pad = (num) => String(num).padStart(2, '0');
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
}
/**
* Determines the color for time display based on duration and attempt status.
* @param {number} seconds - Time duration in seconds.
* @param {number} questionIndex - The internal 0-based index of the question.
* @returns {string} Tailwind CSS color class (e.g., 'text-green-600').
*/
function getTimeColorClass(seconds, questionIndex) {
// Check if the question was unattempted
if (selectedAnswers[questionIndex] === null) {
return 'text-gray-500'; // Always grey for unattempted
}
// Otherwise, use time-based color
if (seconds <= 60) {
return 'text-green-600';
} else if (seconds <= 90) {
return 'text-yellow-600';
} else {
return 'text-red-600';
}
}
/**
* Returns hex color code for chart based on duration and attempt status.
* @param {number} seconds - Time duration in seconds.
* @param {number} questionIndex - The internal 0-based index of the question.
* @returns {string} Hex color code.
*/
function getChartColor(seconds, questionIndex) {
if (selectedAnswers[questionIndex] === null) {
return '#9CA3AF'; // Tailwind gray-400 equivalent for bars
}
if (seconds <= 60) {
return '#10B981'; // Green
} else if (seconds <= 90) {
return '#F59E0B'; // Yellow
} else {
return '#EF4444'; // Red
}
}
/**
* Returns darker hex color code for chart border based on duration and attempt status.
* @param {number} seconds - Time duration in seconds.
* @param {number} questionIndex - The internal 0-based index of the question.
* @returns {string} Hex color code.
*/
function getChartBorderColor(seconds, questionIndex) {
if (selectedAnswers[questionIndex] === null) {
return '#6B7280'; // Tailwind gray-600 equivalent
}
if (seconds <= 60) {
return '#059669'; // Darker Green
} else if (seconds <= 90) {
return '#D97706'; // Darker Yellow
} else {
return '#DC2626'; // Darker Red
}
}
/**
* Starts the timer for the current question and updates the display.
*/
function startQuestionTimer() {
if (questionStartTimes[currentQuestionIndex] === undefined) {
questionStartTimes[currentQuestionIndex] = Date.now();
}
if (currentQuestionTimerInterval) {
clearInterval(currentQuestionTimerInterval);
}
currentQuestionTimerInterval = setInterval(() => {
const elapsed = Math.floor((Date.now() - questionStartTimes[currentQuestionIndex]) / 1000);
const questionTimerDisplay = document.getElementById('question-timer');
if (questionTimerDisplay) {
questionTimerDisplay.textContent = formatSecondsToWords(elapsed); // New format here
}
}, 1000);
}
/**
* Stops the timer for the current question and records the end time.
*/
function stopQuestionTimer() {
if (currentQuestionTimerInterval) {
clearInterval(currentQuestionTimerInterval);
if (questionEndTimes[currentQuestionIndex] === undefined) {
questionEndTimes[currentQuestionIndex] = Date.now();
}
}
}
/**
* Starts the total test timer.
*/
function startTotalTestTimer() {
totalTestStartTime = Date.now();
totalTimerInterval = setInterval(() => {
const elapsed = Math.floor((Date.now() - totalTestStartTime) / 1000);
const totalTestDurationSeconds = TOTAL_TEST_DURATION_MINUTES * 60;
const timeLeft = Math.max(0, totalTestDurationSeconds - elapsed);
// Update time left in panel
const totalTimerDisplayPanel = document.getElementById('total-time-left-panel');
if (totalTimerDisplayPanel) {
totalTimerDisplayPanel.textContent = formatHoursMinutesSeconds(timeLeft);
}
if (timeLeft <= 0) {
showAlert("Time's up! The test has been submitted automatically.");
submitTest();
}
}, 1000);
}
/**
* Stops the total test timer.
*/
function stopTotalTestTimer() {
if (totalTimerInterval) {
clearInterval(totalTimerInterval);
totalTestEndTime = Date.now();
}
}
/**
* Toggles the visibility of the question number side panel.
*/
function toggleQuestionPanel() {
questionPanel.classList.toggle('open');
panelOverlay.classList.toggle('active');
// Update panel summary when it opens
if(questionPanel.classList.contains('open')) {
updateMockTestSummary();
}
}
/**
* Displays a question on the mock test page.
* @param {number} index - The internal 0-based index of the question to display.
*/
function displayQuestion(index) {
// Close panel if it's currently open
if (questionPanel.classList.contains('open')) {
toggleQuestionPanel();
}
// Stop timer for previous question if it was running and record its end time
if (currentQuestionIndex !== index && questionStartTimes[currentQuestionIndex] !== undefined) {
stopQuestionTimer();
}
if (index < 0 || index >= questions.length) {
showAlert("Question index out of bounds.");
return;
}
currentQuestionIndex = index;
const question = questions[currentQuestionIndex];
const optionLetters = ['a', 'b', 'c', 'd'];
let optionsHtml = '';
question.options.forEach((optionText, idx) => {
const optionValue = optionLetters[idx];
const isSelected = selectedAnswers[currentQuestionIndex] === optionValue;
optionsHtml += `
<div class="flex items-center mb-3">
<input type="radio" id="option-${currentQuestionIndex}-${optionValue}" name="question-${currentQuestionIndex}" value="${optionValue}"
class="hidden peer" ${isSelected ? 'checked' : ''}>
<label for="option-${currentQuestionIndex}-${optionValue}"
class="flex items-center w-full p-4 border rounded-lg cursor-pointer
peer-checked:bg-blue-100 peer-checked:border-blue-500 peer-checked:text-blue-700
hover:bg-gray-50 transition duration-200"
ondblclick="handleOptionDblClick(event, ${currentQuestionIndex}, '${optionValue}')"
onclick="handleOptionClick(event, ${currentQuestionIndex}, '${optionValue}')">
<span class="text-gray-800 text-lg">${optionText}</span>
</label>
</div>
`;
});
const elapsedQuestionTime = questionStartTimes[currentQuestionIndex] ?
Math.floor((Date.now() - questionStartTimes[currentQuestionIndex]) / 1000) : 0;
mocktestPage.innerHTML = `
<div class="mocktest-content-wrapper">
<!-- Top Bar -->
<div class="flex items-center justify-between p-4 bg-gray-100 border-b border-gray-200 shadow-sm">
<div class="flex items-center text-gray-700 font-medium">
<span class="mr-2 text-xl">${question.originalNumber}</span>
<div class="flex items-center text-sm ml-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span id="question-timer">${formatSecondsToWords(elapsedQuestionTime)}</span> <!-- New format -->
</div>
<span class="ml-4 px-2 py-1 rounded-full text-green-700 bg-green-200 text-xs font-semibold">+ 2.0</span>
<span class="ml-2 px-2 py-1 rounded-full text-red-700 bg-red-200 text-xs font-semibold">- 0.5</span>
</div>
<div class="flex items-center top-bar-icons">
<!-- Nav icon for panel on right side of top -->
<button id="open-panel-btn-top"
class="text-gray-500 hover:text-gray-700 p-2 rounded-full hover:bg-gray-200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
<!-- Question Body and Buttons -->
<div class="flex-grow p-4 overflow-y-auto flex flex-col">
<div class="mb-6 question-text-area">
<p class="text-xl text-gray-900 mb-6">${question.originalNumber}. ${question.question}</p>
<div>
${optionsHtml}
</div>
</div>
<!-- Action Buttons below options -->
<div class="flex flex-wrap justify-center gap-3 mt-4 mb-6">
<button id="mark-next-btn"
class="flex-1 min-w-[120px] max-w-[180px] bg-gray-200 text-gray-800 font-semibold py-3 px-4 rounded-lg shadow-sm hover:bg-gray-300 transition duration-200 focus:outline-none">
Mark & Next
</button>
<button id="save-next-btn"
class="flex-1 min-w-[120px] max-w-[180px] bg-blue-600 text-white font-semibold py-3 px-4 rounded-lg shadow-md hover:bg-blue-700 transition duration-300 focus:outline-none">
Save & Next
</button>
</div>
</div>
</div>
`;
// Attach event listeners for the newly created elements
document.getElementById('mark-next-btn').onclick = () => {
moveToNextQuestion();
};
document.getElementById('save-next-btn').onclick = () => {
moveToNextQuestion();
};
document.getElementById('open-panel-btn-top').onclick = toggleQuestionPanel;
startQuestionTimer();
updateMockTestSummary();
renderQuestionPanel();
}
/**
* Handles double-click on an option label to clear selection.
* @param {Event} event - The double click event.
* @param {number} qIndex - The internal 0-based index of the question.
* @param {string} optionValue - The value of the option (e.g., 'a').
*/
function handleOptionDblClick(event, qIndex, optionValue) {
event.preventDefault();
if (selectedAnswers[qIndex] === optionValue) {
selectedAnswers[qIndex] = null;
const radioInput = document.getElementById(`option-${qIndex}-${optionValue}`);
if (radioInput) {
radioInput.checked = false;
}
updateMockTestSummary();
}
}
/**
* Handles single-click on an option label to select.
* @param {Event} event - The click event.
* @param {number} qIndex - The internal 0-based index of the question.
* @param {string} optionValue - The value of the option (e.g., 'a').
*/
function handleOptionClick(event, qIndex, optionValue) {
const radioInput = document.getElementById(`option-${qIndex}-${optionValue}`);
if (radioInput && !radioInput.checked) {
radioInput.checked = true;
selectedAnswers[qIndex] = optionValue;
updateMockTestSummary();
}
}
/**
* Shows the question feedback modal with details for a specific question.
* This function is now overloaded to handle direct calls or filtered calls.
* @param {number} displayIndex - The index of the question to display, either internal or index within filteredQuestionIndices.
* @param {boolean} fromFilter - True if called from a filtered list (Correct, Incorrect, etc.).
*/
function showQuestionFeedback(displayIndex, fromFilter = false) {
let questionIndexToShow;
if (fromFilter) {
questionIndexToShow = filteredQuestionIndices[displayIndex];
currentFeedbackIndex = displayIndex;
} else {
questionIndexToShow = displayIndex;
filteredQuestionIndices = [questionIndexToShow];
currentFeedbackIndex = 0;
}
const question = questions[questionIndexToShow];
const selectedAnswer = selectedAnswers[questionIndexToShow];
const correctAnswer = question.answer;
const timeTaken = window.questionTimeDurations[questionIndexToShow] || 0; // Retrieve actual time
feedbackQuestionTitle.textContent = `Question ${question.originalNumber} Feedback`;
feedbackQuestionText.textContent = question.question;
// Set Attempted/Unattempted tag
if (selectedAnswer !== null) {
feedbackStatusTag.textContent = "Attempted";
feedbackStatusTag.className = "text-sm font-semibold px-2 py-1 rounded-full inline-block bg-green-200 text-green-700 mb-4";
} else {
feedbackStatusTag.textContent = "Unattempted";
feedbackStatusTag.className = "text-sm font-semibold px-2 py-1 rounded-full inline-block bg-red-200 text-red-700 mb-4";
}
// Update time taken stamp in feedback modal - Use new formatSecondsToWords
feedbackTimeValue.textContent = formatSecondsToWords(timeTaken);
feedbackTimeValue.className = `font-medium ${getTimeColorClass(timeTaken, questionIndexToShow)}`; // Pass questionIndex for unattempted color logic
feedbackOptionsContainer.innerHTML = '';
const optionLetters = ['a', 'b', 'c', 'd'];
question.options.forEach((optionText, idx) => {
const optionValue = optionLetters[idx];
let colorClass = 'text-gray-800';
let borderColorClass = 'border-gray-200';
if (optionValue === correctAnswer) {
colorClass = 'bg-green-100 text-green-700 font-semibold';
borderColorClass = 'border-green-500';
} else if (selectedAnswer !== null && optionValue === selectedAnswer && optionValue !== correctAnswer) {
colorClass = 'bg-red-100 text-red-700 font-semibold';
borderColorClass = 'border-red-500';
}
const optionElement = document.createElement('p');
optionElement.className = `p-2 border rounded-md ${colorClass} ${borderColorClass}`;
optionElement.innerHTML = `<span>(${optionValue})</span> ${optionText.substring(optionText.indexOf(')') + 1).trim()}`;
feedbackOptionsContainer.appendChild(optionElement);
});
// Update Next/Previous button visibility
feedbackPrevBtn.disabled = currentFeedbackIndex === 0;
feedbackNextBtn.disabled = currentFeedbackIndex === filteredQuestionIndices.length - 1;
questionFeedbackModal.style.display = 'flex';
}
/**
* Navigates to the previous question in the filtered feedback list.
*/
function navigateFeedbackPrev() {
if (currentFeedbackIndex > 0) {
showQuestionFeedback(currentFeedbackIndex - 1, true);
}
}
/**
* Navigates to the next question in the filtered feedback list.
*/
function navigateFeedbackNext() {
if (currentFeedbackIndex < filteredQuestionIndices.length - 1) {
showQuestionFeedback(currentFeedbackIndex + 1, true);
}
}
/**
* Moves to the next question or submits the test if it's the last question.
*/
function moveToNextQuestion() {
stopQuestionTimer();
if (currentQuestionIndex < questions.length - 1) {
displayQuestion(currentQuestionIndex + 1);
} else {
submitTest();
}
}
/**
* Updates the attempted and unattempted counts on the mock test page and panel.
*/
function updateMockTestSummary() {
const attemptedCount = selectedAnswers.filter(a => a !== null).length;
const unattemptedCount = questions.length - attemptedCount;
// Update panel summary
const attemptedDisplayPanel = document.getElementById('attempted-count-panel');
const unattemptedDisplayPanel = document.getElementById('unattempted-count-panel');
const markedDisplayPanel = document.getElementById('marked-count-panel');
if (attemptedDisplayPanel) attemptedDisplayPanel.textContent = attemptedCount;
if (unattemptedDisplayPanel) unattemptedDisplayPanel.textContent = unattemptedCount;
if (markedDisplayPanel) markedDisplayPanel.textContent = 0;
renderQuestionPanel();
}
/**
* Renders the question number grid inside the sliding panel.
*/
function renderQuestionPanel() {
const panelQuestionGrid = document.getElementById('panel-question-grid');
if (panelQuestionGrid) {
panelQuestionGrid.innerHTML = questions.map((q, i) => {
let buttonClass = 'bg-gray-300 text-gray-800 hover:bg-gray-400';
if (i === currentQuestionIndex) {
buttonClass = 'bg-blue-600 text-white shadow-md';
} else if (selectedAnswers[i] !== null) {
buttonClass = 'bg-green-500 text-white';
}
return `
<button class="w-12 h-12 rounded-full flex items-center justify-center font-semibold text-base
${buttonClass} transition duration-200"
onclick="displayQuestion(${i})">
${q.originalNumber}
</button>
`;
}).join('');
}
}
/**
* Calculates the score and displays the result summary.
*/
function submitTest() {
stopTotalTestTimer();
stopQuestionTimer();
let correctCount = 0;
let incorrectCount = 0;
let score = 0;
questions.forEach((question, index) => {
const selectedAnswer = selectedAnswers[index];
if (selectedAnswer) {
if (selectedAnswer === question.answer) {
correctCount++;
score += CORRECT_MARKS;
} else {
incorrectCount++;
score += WRONG_MARKS;
}
}
});
const totalQuestions = questions.length;
const attemptedQuestions = correctCount + incorrectCount;
const unattemptedQuestions = totalQuestions - attemptedQuestions;
const accuracy = attemptedQuestions > 0 ? (correctCount / attemptedQuestions) * 100 : 0;
const totalTimeTakenSeconds = Math.floor((totalTestEndTime - totalTestStartTime) / 1000);
// This array is used by `showQuestionFeedback` and `renderTimePerQuestionChart`
window.questionTimeDurations = questions.map((_, i) => {
const start = questionStartTimes[i];
const end = questionEndTimes[i];
// If question was never started, or if it's the current question on submission,
// or if it was started and ended, calculate time spent.
// It will now store actual time, even for unattempted questions if time was spent.
if (start === undefined) { // Never visited
return 0;
}
if (end === undefined) { // Visited, but not navigated away from (e.g., test submitted while on it)
return Math.floor((Date.now() - start) / 1000);
}
return Math.floor((end - start) / 1000);
});
// Calculate Avg Time per Question and Estimated Exam Time
// Adjusted logic for avg time: if no questions attempted, avg is 0.
const avgTimePerQuestionSeconds = attemptedQuestions > 0 ? totalTimeTakenSeconds / attemptedQuestions : 0;
const estimatedExamTimeSeconds = avgTimePerQuestionSeconds * ESTIMATED_EXAM_QUESTIONS;
inputPage.classList.add('hidden');
mocktestPage.classList.add('hidden');
questionPanel.classList.remove('open');
panelOverlay.classList.remove('active');
resultPage.classList.remove('hidden');
resultPage.innerHTML = `
<div class="page-card">
<h2 class="text-3xl font-bold text-center mb-8 text-gray-800">Mock Test Result Summary</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div class="bg-blue-100 p-6 rounded-lg text-center">
<p class="text-2xl font-bold text-blue-700">${score.toFixed(2)} / ${totalQuestions * CORRECT_MARKS}</p>
<p class="text-gray-600">Total Score</p>
<div class="w-full bg-gray-200 rounded-full h-2.5 mt-2">
<div class="bg-blue-600 h-2.5 rounded-full" style="width: ${(score / (totalQuestions * CORRECT_MARKS)) * 100}%"></div>
</div>
</div>
<div class="bg-green-100 p-6 rounded-lg text-center">
<p class="text-2xl font-bold text-green-700">${accuracy.toFixed(2)}%</p>
<p class="text-gray-600">Accuracy</p>
<div class="w-full bg-gray-200 rounded-full h-2.5 mt-2">
<div class="bg-green-600 h-2.5 rounded-full" style="width: ${accuracy}%"></div>
</div>
</div>
</div>
<div class="bg-gray-50 p-6 rounded-lg mb-8">
<h3 class="text-xl font-semibold text-gray-800 mb-4">Question Breakdown</h3>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 text-center">
<div class="cursor-pointer hover:bg-gray-100 p-2 rounded-lg" onclick="showFeedbackFiltered('correct')">
<p class="text-3xl font-bold text-green-600">${correctCount}</p>
<p class="text-gray-600">Correct</p>
</div>
<div class="cursor-pointer hover:bg-gray-100 p-2 rounded-lg" onclick="showFeedbackFiltered('incorrect')">
<p class="text-3xl font-bold text-red-600">${incorrectCount}</p>
<p class="text-gray-600">Incorrect</p>
</div>
<div class="cursor-pointer hover:bg-gray-100 p-2 rounded-lg" onclick="showFeedbackFiltered('unattempted')">
<p class="text-3xl font-bold text-yellow-600">${unattemptedQuestions}</p>
<p class="text-gray-600">Unattempted</p>
</div>
<div class="cursor-pointer hover:bg-gray-100 p-2 rounded-lg" onclick="showFeedbackFiltered('attempted')">
<p class="text-3xl font-bold text-indigo-600">${attemptedQuestions}</p>
<p class="text-gray-600">Attempted</p>
</div>
</div>
<div class="chart-container mt-8">
<canvas id="questionStatusChart"></canvas>
</div>
</div>
<div class="bg-gray-50 p-6 rounded-lg mb-8">
<h3 class="text-xl font-semibold text-gray-800 mb-4">Time Analysis</h3>
<div class="flex items-center justify-between mb-3">
<p class="text-gray-700">Total Time Taken:</p>
<p class="font-bold text-lg text-purple-700">${formatHoursMinutesSeconds(totalTimeTakenSeconds)}</p>
</div>
<!-- New Time Analysis Calculations -->
<div class="flex items-center justify-between mb-2 p-2 rounded-lg bg-blue-50">
<p class="text-gray-700">Avg Time per Question:</p>
<p class="font-bold text-md text-blue-800">${formatSecondsToWords(avgTimePerQuestionSeconds)}</p>
</div>
<div class="flex items-center justify-between mb-3 p-2 rounded-lg bg-purple-50">
<p class="text-gray-700">Estimated Exam Time (25 Qs):</p>
<p class="font-bold text-md text-purple-800">${formatSecondsToWords(estimatedExamTimeSeconds)}</p>
</div>
<p class="text-gray-700 mb-2 mt-4">Time per Question Breakdown:</p>
<ul class="list-disc list-inside text-gray-700 max-h-48 overflow-y-auto border p-2 rounded-md">
${questions.map((q, i) => {
const time = window.questionTimeDurations[i];
const colorClass = getTimeColorClass(time, i); // Pass index
return `
<li class="flex justify-between items-center py-1 cursor-pointer hover:bg-gray-100" onclick="showQuestionFeedback(${i})">
<span>Question ${q.originalNumber}:</span>
<span class="font-medium ${colorClass}">${formatSecondsToWords(time)}</span> <!-- New format -->
</li>
`;
}).join('')}
</ul>
<div class="horizontal-chart-wrapper mt-8">
<canvas id="timePerQuestionChartCanvas"></canvas>
</div>
</div>
<div class="text-center">
<button id="restart-test-btn"
class="bg-blue-600 text-white font-semibold py-3 px-8 rounded-lg shadow-lg hover:bg-blue-700 transition duration-300 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75">
Restart Test
</button>
</div>
</div>
`;
// Render Charts
renderQuestionStatusChart(correctCount, incorrectCount, unattemptedQuestions);
renderTimePerQuestionChart(window.questionTimeDurations);
document.getElementById('restart-test-btn').onclick = resetApp;
// Attach event listeners for feedback modal navigation
feedbackPrevBtn.onclick = navigateFeedbackPrev;
feedbackNextBtn.onclick = navigateFeedbackNext;
}
/**
* Renders the question status doughnut chart.
* @param {number} correctCount
* @param {number} incorrectCount
* @param {number} unattemptedCount
*/
function renderQuestionStatusChart(correctCount, incorrectCount, unattemptedCount) {
const ctx = document.getElementById('questionStatusChart').getContext('2d');
if (window.questionStatusChartInstance) {
window.questionStatusChartInstance.destroy();
}
window.questionStatusChartInstance = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Correct', 'Incorrect', 'Unattempted'],
datasets: [{
data: [correctCount, incorrectCount, unattemptedCount],
backgroundColor: ['#10B981', '#EF4444', '#F59E0B'],
hoverOffset: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
animation: {
animateScale: true,
animateRotate: true
},
plugins: {
legend: {
position: 'bottom',
},
title: {
display: true,
text: 'Question Status Breakdown'
}
}
}
});
}
/**
* Renders the time per question horizontal bar chart.
* @param {Array<number>} timeDurations - Array of time taken for each question in seconds.
*/
function renderTimePerQuestionChart(timeDurations) {
const ctx = document.getElementById('timePerQuestionChartCanvas').getContext('2d');
const labels = questions.map((q) => `Q${q.originalNumber}`);
if (window.timePerQuestionChartInstance) {
window.timePerQuestionChartInstance.destroy();
}
const calculatedHeight = Math.max(300, labels.length * 30);
ctx.canvas.style.height = `${calculatedHeight}px`;
window.timePerQuestionChartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Time Taken', // Label changed to be more general
data: timeDurations,
backgroundColor: timeDurations.map((time, i) => getChartColor(time, i)),
borderColor: timeDurations.map((time, i) => getChartBorderColor(time, i)),
borderWidth: 1
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
onClick: (evt, elements) => {
if (elements.length > 0) {
const clickedIndex = elements[0].index;
showQuestionFeedback(clickedIndex);
}
},
plugins: {
legend: {
display: false,
},
title: {
display: true,
text: 'Time Taken Per Question'
},
tooltip: {
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.x !== null) {
label += formatSecondsToWords(context.parsed.x); // New format in tooltip
}
return label;
}
}
}
},
scales: {
x: {
beginAtZero: true,
title: {
display: true,
text: 'Time' // Text changed to be more general
},
ticks: {
callback: function(value, index, ticks) {
return formatSecondsToWords(value); // New format for X-axis ticks
}
}
},
y: {
title: {
display: true,
text: 'Question Number'
},
ticks: {
autoSkip: false,
maxRotation: 0,
minRotation: 0,
color: (context) => {
const value = timeDurations[context.index];
return getChartColor(value, context.index);
}
}
}
}
}
});
}
/**
* Resets the app to the initial input page state.
*/
function resetApp() {
questions = [];
currentQuestionIndex = 0;
questionStartTimes = [];
questionEndTimes = [];
totalTestStartTime = null;
totalTestEndTime = null;
selectedAnswers = [];
clearInterval(totalTimerInterval);
clearInterval(currentQuestionTimerInterval);
filteredQuestionIndices = [];
currentFeedbackIndex = 0;
if (window.questionStatusChartInstance) {
window.questionStatusChartInstance.destroy();
window.questionStatusChartInstance = null;
}
if (window.timePerQuestionChartInstance) {
window.timePerQuestionChartInstance.destroy();
window.timePerQuestionChartInstance = null;
}
questionInput.value = '';
inputPage.classList.remove('hidden');
mocktestPage.classList.add('hidden');
resultPage.classList.add('hidden');
questionPanel.classList.remove('open');
panelOverlay.classList.remove('active');
}
// --- Event Listeners ---
startMockTestBtn.addEventListener('click', () => {
const rawQuestions = questionInput.value.trim();
if (!rawQuestions) {
showAlert("Please paste your Math questions before starting the test.");
return;
}
questions = parseQuestions(rawQuestions);
if (questions.length === 0) {
showAlert("Could not parse any questions. Please ensure they follow the specified format.");
return;
}
selectedAnswers = new Array(questions.length).fill(null);
questionStartTimes = new Array(questions.length).fill(undefined);
questionEndTimes = new Array(questions.length).fill(undefined);
inputPage.classList.add('hidden');
mocktestPage.classList.remove('hidden');
startTotalTestTimer();
displayQuestion(0);
});
// Event listeners for panel
closePanelBtn.addEventListener('click', toggleQuestionPanel);
panelOverlay.addEventListener('click', toggleQuestionPanel);
submitTestPanelBtn.addEventListener('click', submitTest);
// Event listeners for feedback modal navigation
feedbackPrevBtn.addEventListener('click', navigateFeedbackPrev);
feedbackNextBtn.addEventListener('click', navigateFeedbackNext);
/**
* Filters questions and opens feedback modal with the first filtered question.
* @param {string} filterType - 'correct', 'incorrect', 'unattempted', 'attempted'
*/
function showFeedbackFiltered(filterType) {
filteredQuestionIndices = [];
questions.forEach((q, i) => {
const isAttempted = selectedAnswers[i] !== null;
const isCorrect = isAttempted && selectedAnswers[i] === q.answer;
if (filterType === 'correct' && isCorrect) {
filteredQuestionIndices.push(i);
} else if (filterType === 'incorrect' && isAttempted && !isCorrect) {
filteredQuestionIndices.push(i);
} else if (filterType === 'unattempted' && !isAttempted) {
filteredQuestionIndices.push(i);
} else if (filterType === 'attempted' && isAttempted) {
filteredQuestionIndices.push(i);
}
});
if (filteredQuestionIndices.length > 0) {
showQuestionFeedback(0, true);
} else {
showAlert(`No ${filterType} questions to show.`);
}
}
</script>
</body>
</html>
0 Comments