LiamKhoaLe's picture
Enh quiz mode #2
4d8a8da
// ────────────────────────────── static/quiz.js ──────────────────────────────
(function() {
// Quiz state
let currentQuiz = null;
let currentQuestionIndex = 0;
let quizAnswers = {};
let quizTimer = null;
let timeRemaining = 0;
let quizSetupStep = 1;
// DOM elements
const quizLink = document.getElementById('quiz-link');
const quizSetupModal = document.getElementById('quiz-setup-modal');
const quizModal = document.getElementById('quiz-modal');
const quizResultsModal = document.getElementById('quiz-results-modal');
const quizSetupForm = document.getElementById('quiz-setup-form');
const quizQuestionsInput = document.getElementById('quiz-questions-input');
const quizTimeLimit = document.getElementById('quiz-time-limit');
const quizDocumentList = document.getElementById('quiz-document-list');
const quizPrevStep = document.getElementById('quiz-prev-step');
const quizNextStep = document.getElementById('quiz-next-step');
const quizCancel = document.getElementById('quiz-cancel');
const quizSubmit = document.getElementById('quiz-submit');
const quizTimerElement = document.getElementById('quiz-timer');
const quizProgressFill = document.getElementById('quiz-progress-fill');
const quizProgressText = document.getElementById('quiz-progress-text');
const quizQuestion = document.getElementById('quiz-question');
const quizAnswers = document.getElementById('quiz-answers');
const quizPrev = document.getElementById('quiz-prev');
const quizNext = document.getElementById('quiz-next');
const quizSubmitBtn = document.getElementById('quiz-submit');
const quizResultsContent = document.getElementById('quiz-results-content');
const quizResultsClose = document.getElementById('quiz-results-close');
// Initialize
init();
function init() {
setupEventListeners();
}
function setupEventListeners() {
// Quiz link
if (quizLink) {
quizLink.addEventListener('click', (e) => {
e.preventDefault();
openQuizSetup();
});
}
// Quiz setup form
if (quizSetupForm) {
quizSetupForm.addEventListener('submit', handleQuizSetupSubmit);
}
// Quiz setup navigation
if (quizNextStep) {
quizNextStep.addEventListener('click', nextQuizStep);
}
if (quizPrevStep) {
quizPrevStep.addEventListener('click', prevQuizStep);
}
if (quizCancel) {
quizCancel.addEventListener('click', closeQuizSetup);
}
// Quiz navigation
if (quizPrev) {
quizPrev.addEventListener('click', prevQuestion);
}
if (quizNext) {
quizNext.addEventListener('click', nextQuestion);
}
if (quizSubmitBtn) {
quizSubmitBtn.addEventListener('click', submitQuiz);
}
// Quiz results
if (quizResultsClose) {
quizResultsClose.addEventListener('click', closeQuizResults);
}
// Close modals on outside click
document.addEventListener('click', (e) => {
if (e.target.classList.contains('modal')) {
closeAllQuizModals();
}
});
}
async function openQuizSetup() {
const user = window.__sb_get_user();
if (!user) {
alert('Please sign in to create a quiz');
window.__sb_show_auth_modal();
return;
}
const currentProject = window.__sb_get_current_project && window.__sb_get_current_project();
if (!currentProject) {
alert('Please select a project first');
return;
}
// Load available documents
await loadQuizDocuments();
// Reset form
quizSetupStep = 1;
updateQuizSetupStep();
// Show modal
quizSetupModal.classList.remove('hidden');
}
async function loadQuizDocuments() {
const user = window.__sb_get_user();
const currentProject = window.__sb_get_current_project && window.__sb_get_current_project();
if (!user || !currentProject) return;
try {
const res = await fetch(`/files?user_id=${encodeURIComponent(user.user_id)}&project_id=${encodeURIComponent(currentProject.project_id)}`);
if (!res.ok) return;
const data = await res.json();
const files = data.files || [];
// Clear existing documents
quizDocumentList.innerHTML = '';
if (files.length === 0) {
quizDocumentList.innerHTML = '<div class="muted">No documents available. Please upload documents first.</div>';
return;
}
// Create document checkboxes
files.forEach((file, index) => {
const item = document.createElement('div');
item.className = 'document-checkbox-item';
item.innerHTML = `
<input type="checkbox" id="quiz-doc-${index}" value="${file.filename}" checked>
<label for="quiz-doc-${index}">${file.filename}</label>
`;
quizDocumentList.appendChild(item);
});
} catch (error) {
console.error('Failed to load documents:', error);
quizDocumentList.innerHTML = '<div class="muted">Failed to load documents.</div>';
}
}
function updateQuizSetupStep() {
// Hide all steps
document.querySelectorAll('.quiz-step').forEach(step => {
step.style.display = 'none';
});
// Show current step
const currentStep = document.getElementById(`quiz-step-${quizSetupStep}`);
if (currentStep) {
currentStep.style.display = 'block';
}
// Update navigation buttons
quizPrevStep.style.display = quizSetupStep > 1 ? 'inline-flex' : 'none';
quizNextStep.style.display = quizSetupStep < 3 ? 'inline-flex' : 'none';
quizSubmit.style.display = quizSetupStep === 3 ? 'inline-flex' : 'none';
}
function nextQuizStep() {
if (quizSetupStep < 3) {
quizSetupStep++;
updateQuizSetupStep();
}
}
function prevQuizStep() {
if (quizSetupStep > 1) {
quizSetupStep--;
updateQuizSetupStep();
}
}
function closeQuizSetup() {
quizSetupModal.classList.add('hidden');
quizSetupStep = 1;
updateQuizSetupStep();
}
async function handleQuizSetupSubmit(e) {
e.preventDefault();
const user = window.__sb_get_user();
const currentProject = window.__sb_get_current_project && window.__sb_get_current_project();
if (!user || !currentProject) {
showNotification('Please sign in and select a project', 'error');
return;
}
// Get form data
const questionsInput = quizQuestionsInput.value.trim();
const timeLimit = parseInt(quizTimeLimit.value) || 0;
// Get selected documents
const selectedDocs = Array.from(quizDocumentList.querySelectorAll('input[type="checkbox"]:checked'))
.map(input => input.value);
if (selectedDocs.length === 0) {
showNotification('Please select at least one document', 'warning');
return;
}
if (!questionsInput) {
showNotification('Please specify how many questions you want', 'warning');
return;
}
// Validate questions input
if (questionsInput.length < 10) {
showNotification('Please provide more details about your question requirements', 'warning');
return;
}
// Show loading with progress updates
showLoading('Creating quiz...');
updateLoadingProgress('Parsing your requirements...', 20);
try {
// Create quiz
const formData = new FormData();
formData.append('user_id', user.user_id);
formData.append('project_id', currentProject.project_id);
formData.append('questions_input', questionsInput);
formData.append('time_limit', timeLimit.toString());
formData.append('documents', JSON.stringify(selectedDocs));
updateLoadingProgress('Analyzing documents...', 40);
const response = await fetch('/quiz/create', {
method: 'POST',
body: formData
});
const data = await response.json();
if (response.ok) {
updateLoadingProgress('Generating questions...', 80);
// Simulate processing time for better UX
await new Promise(resolve => setTimeout(resolve, 1000));
hideLoading();
closeQuizSetup();
// Start quiz
currentQuiz = data.quiz;
currentQuestionIndex = 0;
quizAnswers = {};
timeRemaining = timeLimit * 60; // Convert to seconds
showNotification(`Quiz created successfully! ${currentQuiz.questions.length} questions generated.`, 'success');
startQuiz();
} else {
hideLoading();
showNotification(data.detail || 'Failed to create quiz', 'error');
}
} catch (error) {
hideLoading();
console.error('Quiz creation failed:', error);
showNotification('Failed to create quiz. Please try again.', 'error');
}
}
function startQuiz() {
// Show quiz modal
quizModal.classList.remove('hidden');
// Start timer if time limit is set
if (timeRemaining > 0) {
startQuizTimer();
} else {
quizTimerElement.textContent = 'No time limit';
}
// Show first question
showQuestion(0);
}
function startQuizTimer() {
updateTimerDisplay();
quizTimer = setInterval(() => {
timeRemaining--;
updateTimerDisplay();
if (timeRemaining <= 0) {
clearInterval(quizTimer);
timeUp();
}
}, 1000);
}
function updateTimerDisplay() {
const minutes = Math.floor(timeRemaining / 60);
const seconds = timeRemaining % 60;
const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
quizTimerElement.textContent = `Time: ${timeString}`;
// Add warning classes
quizTimerElement.classList.remove('warning', 'danger');
if (timeRemaining <= 60) {
quizTimerElement.classList.add('danger');
} else if (timeRemaining <= 300) { // 5 minutes
quizTimerElement.classList.add('warning');
}
}
function timeUp() {
alert('Time Up!');
submitQuiz();
}
function showQuestion(index) {
if (!currentQuiz || !currentQuiz.questions || index >= currentQuiz.questions.length) {
return;
}
const question = currentQuiz.questions[index];
currentQuestionIndex = index;
// Update progress
const progress = ((index + 1) / currentQuiz.questions.length) * 100;
quizProgressFill.style.width = `${progress}%`;
quizProgressText.textContent = `Question ${index + 1} of ${currentQuiz.questions.length}`;
// Show question
quizQuestion.innerHTML = `
<h3>Question ${index + 1}</h3>
<p>${question.question}</p>
`;
// Show answers
if (question.type === 'mcq') {
showMCQAnswers(question);
} else if (question.type === 'self_reflect') {
showSelfReflectAnswer(question);
}
// Update navigation
quizPrev.disabled = index === 0;
quizNext.style.display = index < currentQuiz.questions.length - 1 ? 'inline-flex' : 'none';
quizSubmitBtn.style.display = index === currentQuiz.questions.length - 1 ? 'inline-flex' : 'none';
}
function showMCQAnswers(question) {
quizAnswers.innerHTML = '';
question.options.forEach((option, index) => {
const optionDiv = document.createElement('div');
optionDiv.className = 'quiz-answer-option';
optionDiv.innerHTML = `
<input type="radio" name="question-${currentQuestionIndex}" value="${index}" id="option-${currentQuestionIndex}-${index}">
<label for="option-${currentQuestionIndex}-${index}">${option}</label>
`;
// Check if already answered
if (quizAnswers[currentQuestionIndex] !== undefined) {
const radio = optionDiv.querySelector('input[type="radio"]');
radio.checked = quizAnswers[currentQuestionIndex] === index;
if (radio.checked) {
optionDiv.classList.add('selected');
}
}
// Add click handler
optionDiv.addEventListener('click', () => {
// Remove selection from other options
quizAnswers.querySelectorAll('.quiz-answer-option').forEach(opt => {
opt.classList.remove('selected');
});
// Select this option
optionDiv.classList.add('selected');
const radio = optionDiv.querySelector('input[type="radio"]');
radio.checked = true;
// Save answer
quizAnswers[currentQuestionIndex] = index;
});
quizAnswers.appendChild(optionDiv);
});
}
function showSelfReflectAnswer(question) {
quizAnswers.innerHTML = `
<textarea class="quiz-text-answer" id="self-reflect-${currentQuestionIndex}" placeholder="Enter your answer here...">${quizAnswers[currentQuestionIndex] || ''}</textarea>
`;
const textarea = quizAnswers.querySelector('textarea');
textarea.addEventListener('input', (e) => {
quizAnswers[currentQuestionIndex] = e.target.value;
});
}
function prevQuestion() {
if (currentQuestionIndex > 0) {
showQuestion(currentQuestionIndex - 1);
}
}
function nextQuestion() {
if (currentQuestionIndex < currentQuiz.questions.length - 1) {
showQuestion(currentQuestionIndex + 1);
}
}
async function submitQuiz() {
if (quizTimer) {
clearInterval(quizTimer);
}
// Validate that all questions are answered
const unansweredQuestions = [];
for (let i = 0; i < currentQuiz.questions.length; i++) {
if (quizAnswers[i] === undefined || quizAnswers[i] === null || quizAnswers[i] === '') {
unansweredQuestions.push(i + 1);
}
}
if (unansweredQuestions.length > 0) {
const confirmSubmit = confirm(`You have ${unansweredQuestions.length} unanswered questions (${unansweredQuestions.join(', ')}). Do you want to submit anyway?`);
if (!confirmSubmit) {
return;
}
}
// Show loading with progress
showLoading('Submitting quiz...');
updateLoadingProgress('Processing your answers...', 30);
try {
const user = window.__sb_get_user();
const currentProject = window.__sb_get_current_project && window.__sb_get_current_project();
updateLoadingProgress('Marking your answers...', 60);
const formData = new FormData();
formData.append('user_id', user.user_id);
formData.append('project_id', currentProject.project_id);
formData.append('quiz_id', currentQuiz.quiz_id);
formData.append('answers', JSON.stringify(quizAnswers));
const response = await fetch('/quiz/submit', {
method: 'POST',
body: formData
});
const data = await response.json();
if (response.ok) {
updateLoadingProgress('Generating feedback...', 90);
// Simulate processing time for better UX
await new Promise(resolve => setTimeout(resolve, 1500));
hideLoading();
closeQuizModal();
showQuizResults(data.results);
} else {
hideLoading();
showNotification(data.detail || 'Failed to submit quiz', 'error');
}
} catch (error) {
hideLoading();
console.error('Quiz submission failed:', error);
showNotification('Failed to submit quiz. Please try again.', 'error');
}
}
function showQuizResults(results) {
// Show results summary
const totalQuestions = results.questions.length;
const correctAnswers = results.questions.filter(q => q.status === 'correct').length;
const partialAnswers = results.questions.filter(q => q.status === 'partial').length;
const incorrectAnswers = results.questions.filter(q => q.status === 'incorrect').length;
const score = Math.round((correctAnswers + partialAnswers * 0.5) / totalQuestions * 100);
// Determine performance level
let performanceLevel = 'Needs Improvement';
let performanceColor = 'var(--error)';
if (score >= 90) {
performanceLevel = 'Excellent';
performanceColor = 'var(--success)';
} else if (score >= 80) {
performanceLevel = 'Good';
performanceColor = 'var(--accent)';
} else if (score >= 70) {
performanceLevel = 'Satisfactory';
performanceColor = 'var(--warning)';
}
quizResultsContent.innerHTML = `
<div class="quiz-result-summary">
<div class="quiz-result-stat quiz-result-score">
<div class="quiz-result-stat-value" style="color: ${performanceColor}">${score}%</div>
<div class="quiz-result-stat-label">Score</div>
<div class="quiz-result-performance">${performanceLevel}</div>
</div>
<div class="quiz-result-stat">
<div class="quiz-result-stat-value" style="color: var(--success)">${correctAnswers}</div>
<div class="quiz-result-stat-label">Correct</div>
</div>
<div class="quiz-result-stat">
<div class="quiz-result-stat-value" style="color: var(--warning)">${partialAnswers}</div>
<div class="quiz-result-stat-label">Partial</div>
</div>
<div class="quiz-result-stat">
<div class="quiz-result-stat-value" style="color: var(--error)">${incorrectAnswers}</div>
<div class="quiz-result-stat-label">Incorrect</div>
</div>
</div>
<div class="quiz-result-questions">
${results.questions.map((question, index) => `
<div class="quiz-result-question">
<div class="quiz-result-question-header">
<div class="quiz-result-question-title">Question ${index + 1}</div>
<div class="quiz-result-question-status ${question.status}">${question.status}</div>
</div>
<div class="quiz-result-question-text">${question.question}</div>
${question.type === 'mcq' ? `
<div class="quiz-result-answer">
<div class="quiz-result-answer-label">Your Answer:</div>
<div class="quiz-result-answer-content ${question.status === 'correct' ? 'correct' : question.status === 'incorrect' ? 'incorrect' : ''}">${question.options[question.user_answer] || 'No answer'}</div>
</div>
${question.status === 'incorrect' ? `
<div class="quiz-result-answer">
<div class="quiz-result-answer-label">Correct Answer:</div>
<div class="quiz-result-answer-content correct">${question.options[question.correct_answer]}</div>
</div>
` : ''}
` : `
<div class="quiz-result-answer">
<div class="quiz-result-answer-label">Your Answer:</div>
<div class="quiz-result-answer-content">${question.user_answer || 'No answer'}</div>
</div>
`}
${question.explanation ? `
<div class="quiz-result-explanation">
<strong>Explanation:</strong> ${question.explanation}
</div>
` : ''}
</div>
`).join('')}
</div>
`;
quizResultsModal.classList.remove('hidden');
// Show success notification
showNotification(`Quiz completed! You scored ${score}% (${performanceLevel})`, 'success');
}
function closeQuizModal() {
quizModal.classList.add('hidden');
}
function closeQuizResults() {
quizResultsModal.classList.add('hidden');
}
function closeAllQuizModals() {
closeQuizSetup();
closeQuizModal();
closeQuizResults();
}
function showLoading(message = 'Loading...') {
const loadingOverlay = document.getElementById('loading-overlay');
const loadingMessage = document.getElementById('loading-message');
if (loadingOverlay && loadingMessage) {
loadingMessage.textContent = message;
loadingOverlay.classList.remove('hidden');
}
}
function updateLoadingProgress(message, progress) {
const loadingOverlay = document.getElementById('loading-overlay');
const loadingMessage = document.getElementById('loading-message');
const loadingProgress = document.getElementById('loading-progress');
if (loadingOverlay && loadingMessage) {
loadingMessage.textContent = message;
if (loadingProgress) {
loadingProgress.style.width = `${progress}%`;
}
}
}
function hideLoading() {
const loadingOverlay = document.getElementById('loading-overlay');
if (loadingOverlay) {
loadingOverlay.classList.add('hidden');
}
}
function showNotification(message, type = 'info') {
// Create notification element
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.innerHTML = `
<div class="notification-content">
<span class="notification-message">${message}</span>
<button class="notification-close">&times;</button>
</div>
`;
// Add to page
document.body.appendChild(notification);
// Auto remove after 5 seconds
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 5000);
// Close button
notification.querySelector('.notification-close').addEventListener('click', () => {
notification.remove();
});
}
// Expose functions globally
window.__sb_open_quiz_setup = openQuizSetup;
window.__sb_close_quiz_setup = closeQuizSetup;
})();