UNPKG

mcp-quiz-server

Version:

🧠 AI-Powered Quiz Management via Model Context Protocol (MCP) - Create, manage, and take quizzes directly from VS Code, Claude, and other AI agents.

1,003 lines 47.1 kB
import { Component } from './Component.js'; import { AppStore } from '../store/AppStore.js'; import { DOMUtils } from '../utils/index.js'; import { SettingsService } from '../services/SettingsService.js'; import { TimerService } from '../services/TimerService.js'; export class QuizContent extends Component { constructor() { super('#quiz-content'); this.currentQuestionIndex = 0; this.unsubscribe = null; this.timerUnsubscribe = null; this.currentSelectionTimeout = null; this.continueButton = null; this.viewModeToggleSetup = false; this.store = AppStore.getInstance(); this.settingsService = SettingsService.getInstance(); this.timerService = TimerService.getInstance(); this.welcomeScreen = document.querySelector('#welcome-screen'); this.quizContainer = document.querySelector('#quiz-container'); this.submitButton = document.querySelector('#submit-button'); this.prevButton = null; this.nextButton = null; } onMount() { this.unsubscribe = this.store.subscribe(state => this.onStateChange(state)); this.timerUnsubscribe = this.timerService.subscribe(timerState => this.onTimerStateChange(timerState)); } onUnmount() { this.unsubscribe?.(); this.timerUnsubscribe?.(); } bindEvents() { this.submitButton?.addEventListener('click', this.handleSubmit.bind(this)); this.prevButton = document.querySelector('#prev-btn'); this.nextButton = document.querySelector('#next-btn'); this.prevButton?.addEventListener('click', this.handlePreviousQuestion.bind(this)); this.nextButton?.addEventListener('click', this.handleNextQuestion.bind(this)); this.setupViewModeToggle(); document.querySelector('#reset-quiz-btn')?.addEventListener('click', () => { this.store.retakeQuiz(); }); document.querySelectorAll('#create-quiz-btn, #welcome-create-btn').forEach(btn => { btn.addEventListener('click', () => { DOMUtils.showToast('Quiz creation coming soon!', 'info'); }); }); this.setupClickToAdvance(); } setupViewModeToggle() { if (this.viewModeToggleSetup) { console.log('View mode toggle already setup, skipping...'); return; } const setupToggle = () => { const singleBtn = document.getElementById('view-mode-single'); const listBtn = document.getElementById('view-mode-list'); if (!singleBtn || !listBtn) { console.warn('View mode buttons not found, retrying in 100ms...'); setTimeout(setupToggle, 100); return; } console.log('Setting up view mode toggle buttons...'); const newSingleBtn = singleBtn.cloneNode(true); const newListBtn = listBtn.cloneNode(true); singleBtn.parentNode?.replaceChild(newSingleBtn, singleBtn); listBtn.parentNode?.replaceChild(newListBtn, listBtn); newSingleBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); console.log('Single mode button clicked'); try { this.store.setViewMode('single'); console.log('Single mode activated successfully'); } catch (error) { console.error('Error setting single mode:', error); } }); newListBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); console.log('List mode button clicked'); try { this.store.setViewMode('list'); console.log('List mode activated successfully'); } catch (error) { console.error('Error setting list mode:', error); } }); this.viewModeToggleSetup = true; console.log('✅ View mode toggle listeners attached successfully'); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', setupToggle); } else { setupToggle(); } } setupClickToAdvance() { this.quizContainer?.addEventListener('click', e => { const settings = this.settingsService.getSettings(); if (!settings.ui.clickToAdvance) return; const state = this.store.getState(); if (state.ui.viewMode === 'list') return; const target = e.target; if (target.closest('button') || target.closest('input') || target.closest('select')) { return; } this.handleNextQuestion(); }); } async handleSubmit() { if (!this.submitButton) return; const originalText = this.submitButton.textContent; this.submitButton.disabled = true; this.submitButton.textContent = 'Submitting...'; try { const result = await this.store.submitQuiz(); DOMUtils.showToast('🎉 Quiz completed successfully!', 'success'); this.submitButton.classList.add('animate-pulse'); setTimeout(() => { this.submitButton.classList.remove('animate-pulse'); }, 1000); this.store.showCelebration(); console.log('✅ Quiz submission successful, results modal should be visible'); } catch (error) { const message = error instanceof Error ? error.message : 'Failed to submit quiz'; DOMUtils.showToast(message, 'error'); console.error('❌ Quiz submission failed:', error); } finally { this.submitButton.disabled = false; this.submitButton.textContent = originalText; } } handlePreviousQuestion() { console.log('Previous question clicked, current index:', this.currentQuestionIndex); if (this.currentQuestionIndex > 0) { this.currentQuestionIndex--; console.log('Moving to question index:', this.currentQuestionIndex); this.scrollToCurrentQuestion(); this.updateNavigationButtons(); const state = this.store.getState(); if (state.currentQuiz && state.ui.viewMode === 'single') { this.renderQuiz(state.currentQuiz, state.userAnswers, state.ui.viewMode); } } } handleNextQuestion() { console.log('Next question clicked, current index:', this.currentQuestionIndex); const state = this.store.getState(); if (state.currentQuiz && this.currentQuestionIndex < state.currentQuiz.questions.length - 1) { this.currentQuestionIndex++; console.log('Moving to question index:', this.currentQuestionIndex); this.scrollToCurrentQuestion(); this.updateNavigationButtons(); if (state.ui.viewMode === 'single') { this.renderQuiz(state.currentQuiz, state.userAnswers, state.ui.viewMode); } } } scrollToCurrentQuestion() { const questions = this.quizContainer.querySelectorAll('.question-block'); const currentQuestion = questions[this.currentQuestionIndex]; if (currentQuestion) { currentQuestion.scrollIntoView({ behavior: 'smooth', block: 'center', }); questions.forEach(q => q.classList.remove('ring-2', 'ring-blue-400')); currentQuestion.classList.add('ring-2', 'ring-blue-400'); } } updateNavigationButtons() { if (!this.prevButton) { this.prevButton = document.querySelector('#prev-btn'); } if (!this.nextButton) { this.nextButton = document.querySelector('#next-btn'); } if (!this.prevButton || !this.nextButton) return; const state = this.store.getState(); const settings = this.settingsService.getSettings(); const totalQuestions = state.currentQuiz?.questions.length || 0; const isListMode = state.ui.viewMode === 'list'; const isSingleMode = state.ui.viewMode === 'single'; const isFirstQuestion = this.currentQuestionIndex === 0; const isLastQuestion = this.currentQuestionIndex === totalQuestions - 1; const navigationBar = document.querySelector('#quiz-navigation-bar'); if (isSingleMode) { if (navigationBar) { navigationBar.classList.add('hidden-single-mode'); navigationBar.classList.remove('fade-out-single-mode'); } console.log('🔇 Navigation bar hidden in single mode'); return; } if (navigationBar) { navigationBar.classList.remove('hidden-single-mode', 'fade-out-single-mode'); } if (settings.ui.hideAllNavButtons) { this.prevButton.style.display = 'none'; this.nextButton.style.display = 'none'; return; } this.prevButton.style.display = ''; this.nextButton.style.display = ''; if (isListMode) { this.prevButton.style.visibility = 'hidden'; this.nextButton.style.visibility = 'hidden'; } else { this.prevButton.style.visibility = 'visible'; this.nextButton.style.visibility = 'visible'; if (isFirstQuestion) { this.prevButton.disabled = true; this.prevButton.classList.add('disabled-fade'); this.prevButton.classList.remove('enabled-fade'); if (this.shouldHideNavButton('prev')) { this.prevButton.classList.add('disappear'); } } else { this.prevButton.disabled = false; this.prevButton.classList.add('enabled-fade'); this.prevButton.classList.remove('disabled-fade', 'disappear'); } if (isLastQuestion) { this.nextButton.disabled = true; this.nextButton.classList.add('disabled-fade'); this.nextButton.classList.remove('enabled-fade'); if (this.shouldHideNavButton('next')) { this.nextButton.classList.add('disappear'); } } else { this.nextButton.disabled = false; this.nextButton.classList.add('enabled-fade'); this.nextButton.classList.remove('disabled-fade', 'disappear'); } } console.log('🧭 Navigation buttons updated:', { mode: state.ui.viewMode, navigationBarVisible: !navigationBar?.classList.contains('hidden-single-mode'), prevVisible: this.prevButton.style.visibility !== 'hidden', nextVisible: this.nextButton.style.visibility !== 'hidden' }); } shouldHideNavButton(direction) { const settings = this.settingsService.getSettings(); return settings.ui.hideDisabledNavButtons; } onStateChange(state) { this.updateVisibility(state); this.updateQuizHeader(state); this.updateViewModeToggle(state); if (state.currentQuiz) { if (this.currentQuestionIndex >= state.currentQuiz.questions.length) { this.currentQuestionIndex = 0; } this.renderQuiz(state.currentQuiz, state.userAnswers, state.ui.viewMode); this.updateNavigationButtons(); } if (state.ui.showCelebration) { this.showCompletionCelebration(); } } onTimerStateChange(timerState) { if (timerState.isWarning) { const progressBar = document.querySelector('#quiz-progress-bar'); if (progressBar) { progressBar.classList.add('bg-red-500'); progressBar.classList.remove('bg-primary-500'); } } if (timerState.isExpired) { console.log('Timer expired, quiz should auto-submit'); } } updateVisibility(state) { const showWelcome = state.ui.showWelcome || !state.currentQuiz; this.welcomeScreen?.classList.toggle('hidden', !showWelcome); this.element.classList.toggle('hidden', showWelcome); this.updateSubmitBarVisibility(state, showWelcome); } updateSubmitBarVisibility(state, showWelcome) { const navigationBar = document.querySelector('#quiz-navigation-bar'); if (!navigationBar) return; const isSingleMode = state.ui.viewMode === 'single'; const shouldHideNavigationBar = showWelcome || !state.currentQuiz || state.ui.showResults || state.quiz.isCompleted || state.ui.showCelebration || isSingleMode; if (shouldHideNavigationBar) { if (isSingleMode && state.currentQuiz && !showWelcome && !state.ui.showResults && !state.quiz.isCompleted && !state.ui.showCelebration) { navigationBar.classList.add('hidden-single-mode'); navigationBar.classList.remove('fade-out-single-mode'); } else { navigationBar.style.display = 'none'; navigationBar.classList.remove('hidden-single-mode', 'fade-out-single-mode'); } } else { navigationBar.style.display = 'block'; navigationBar.classList.remove('hidden-single-mode', 'fade-out-single-mode'); } if (state.ui.submitBarVisible !== !shouldHideNavigationBar) { this.store.updateSubmitBarVisibility(); } console.log('🎯 Navigation bar visibility (ISSUE_019):', { showWelcome, hasQuiz: !!state.currentQuiz, showResults: state.ui.showResults, isCompleted: state.quiz.isCompleted, showCelebration: state.ui.showCelebration, isSingleMode, visible: !shouldHideNavigationBar, hiddenClass: navigationBar.classList.contains('hidden-single-mode') }); } updateQuizHeader(state) { if (!state.currentQuiz) return; const quizTitle = document.querySelector('#quiz-title'); const quizDescription = document.querySelector('#quiz-description'); const quizBreadcrumb = document.querySelector('#quiz-breadcrumb'); const quizProgressText = document.querySelector('#quiz-progress-text'); const quizProgressBar = document.querySelector('#quiz-progress-bar'); const quizCompletion = document.querySelector('#quiz-completion'); if (quizTitle) { quizTitle.textContent = state.currentQuiz.title; } if (quizDescription) { quizDescription.textContent = state.currentQuiz.description || ''; } if (quizBreadcrumb) { quizBreadcrumb.textContent = state.currentQuiz.title; } const answered = Object.keys(state.userAnswers).length; const total = state.currentQuiz.questions.length; const currentQuestion = this.currentQuestionIndex + 1; const completionPercentage = Math.round((answered / total) * 100); if (quizProgressText) { if (state.ui.viewMode === 'single') { quizProgressText.textContent = `Question ${currentQuestion} of ${total}`; } else { quizProgressText.textContent = `${answered} of ${total} answered`; } } if (quizProgressBar) { const progressWidth = state.ui.viewMode === 'single' ? (currentQuestion / total) * 100 : completionPercentage; quizProgressBar.style.width = `${progressWidth}%`; } if (quizCompletion) { const displayPercentage = state.ui.viewMode === 'single' ? Math.round((currentQuestion / total) * 100) : completionPercentage; quizCompletion.textContent = `${displayPercentage}%`; } } updateViewModeToggle(state) { const singleBtn = document.getElementById('view-mode-single'); const listBtn = document.getElementById('view-mode-list'); if (singleBtn && listBtn) { if (state.ui.viewMode === 'single') { singleBtn.classList.add('bg-white', 'shadow-sm'); singleBtn.classList.remove('text-gray-600'); listBtn.classList.remove('bg-white', 'shadow-sm'); listBtn.classList.add('text-gray-600'); } else { listBtn.classList.add('bg-white', 'shadow-sm'); listBtn.classList.remove('text-gray-600'); singleBtn.classList.remove('bg-white', 'shadow-sm'); singleBtn.classList.add('text-gray-600'); } } } render() { } renderQuiz(quiz, userAnswers, viewMode) { if (!this.quizContainer) return; if (viewMode === 'single') { this.renderSingleQuestionMode(quiz, userAnswers); } else { this.renderAllQuestionsMode(quiz, userAnswers); } this.bindQuestionEvents(); } renderSingleQuestionMode(quiz, userAnswers) { const currentQuestion = quiz.questions[this.currentQuestionIndex]; if (!currentQuestion) return; const questionHtml = this.renderQuestion(currentQuestion, this.currentQuestionIndex, userAnswers); const isAnswered = userAnswers[currentQuestion.id]; const settings = this.settingsService.getSettings(); const isLastQuestion = this.currentQuestionIndex >= quiz.questions.length - 1; let actionButtonHtml = ''; if (isLastQuestion) { actionButtonHtml = ` <div class="complete-quiz-container mt-6 text-center" style="${isAnswered ? '' : 'display: none;'}"> <button id="complete-quiz-btn" class="bg-green-600 hover:bg-green-700 text-white font-medium py-3 px-8 rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 shadow-lg"> <span class="flex items-center justify-center"> <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path> </svg> Complete Quiz </span> </button> </div> `; } else { actionButtonHtml = settings.ui.showContinueButton ? ` <div class="continue-button-container mt-6 text-center" style="${isAnswered ? '' : 'display: none;'}"> <button id="continue-btn" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"> <span class="flex items-center justify-center"> <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path> </svg> Continue </span> </button> </div> ` : ''; } this.quizContainer.innerHTML = ` <div class="single-question-view"> <div class="progress-indicator text-center mb-4 md:mb-6"> <span class="text-lg font-medium text-gray-700"> Question ${this.currentQuestionIndex + 1} of ${quiz.questions.length} </span> <div class="w-full bg-gray-200 rounded-full h-2 mt-2"> <div class="bg-blue-600 h-2 rounded-full transition-all duration-300" style="width: ${((this.currentQuestionIndex + 1) / quiz.questions.length) * 100}%"></div> </div> </div> ${questionHtml} ${actionButtonHtml} </div> `; this.bindContinueButton(); this.bindCompleteQuizButton(); } renderAllQuestionsMode(quiz, userAnswers) { const questionsHtml = quiz.questions .map((question, index) => this.renderQuestion(question, index, userAnswers)) .join(''); this.quizContainer.innerHTML = `<div class="all-questions-view space-y-4 md:space-y-6">${questionsHtml}</div>`; } renderQuestion(question, index, userAnswers) { const settings = this.settingsService.getSettings(); const state = this.store.getState(); const isSingleMode = state.ui.viewMode === 'single'; const useClickableCards = isSingleMode && settings.ui.clickToSelectCards; const useInstantSubmission = settings.quiz.instantSubmission; const optionsHtml = question.options .map(option => { const isSelected = userAnswers[question.id] === option; if (useInstantSubmission) { return ` <div class="quiz-option-card cursor-pointer p-4 rounded-xl border-2 transition-all duration-300 transform hover:scale-[1.02] hover:shadow-lg ${isSelected ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 shadow-md' : 'border-gray-200 dark:border-gray-600 hover:border-blue-300 dark:hover:border-blue-500'}" data-question-id="${question.id}" data-answer="${DOMUtils.escapeHtml(option)}" role="button" tabindex="0" aria-pressed="${isSelected}"> <div class="flex items-center justify-between"> <span class="text-gray-900 dark:text-gray-100 font-medium">${DOMUtils.escapeHtml(option)}</span> <div class="answer-indicator w-6 h-6 rounded-full border-2 transition-all duration-200 ${isSelected ? 'border-blue-500 bg-blue-500' : 'border-gray-300 dark:border-gray-500'}"> ${isSelected ? '<div class="w-2 h-2 bg-white rounded-full mx-auto mt-1"></div>' : ''} </div> </div> </div> `; } else if (useClickableCards) { return ` <div class="quiz-answer-card cursor-pointer p-4 rounded-xl border-2 transition-all duration-300 transform hover:scale-[1.02] hover:shadow-lg active:scale-[0.98] ${isSelected ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 shadow-md ring-2 ring-blue-200 dark:ring-blue-800' : 'border-gray-200 dark:border-gray-600 hover:border-blue-300 dark:hover:border-blue-500 hover:bg-gray-50 dark:hover:bg-gray-800'}" data-question-id="${question.id}" data-answer="${DOMUtils.escapeHtml(option)}" role="button" tabindex="0" aria-pressed="${isSelected}" aria-label="Select answer: ${DOMUtils.escapeHtml(option)}" style="min-height: 44px;"> <div class="flex items-center justify-between"> <span class="text-gray-900 dark:text-gray-100 font-medium leading-relaxed">${DOMUtils.escapeHtml(option)}</span> <div class="answer-indicator w-6 h-6 rounded-full border-2 transition-all duration-200 flex-shrink-0 ${isSelected ? 'border-blue-500 bg-blue-500' : 'border-gray-300 dark:border-gray-500'}"> ${isSelected ? '<div class="w-2 h-2 bg-white rounded-full mx-auto mt-1"></div>' : ''} </div> </div> </div> `; } else { const isChecked = isSelected ? 'checked' : ''; return ` <label class="quiz-option flex items-center p-3 rounded-lg border border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer transition-all duration-200"> <input type="radio" name="question-${question.id}" value="${DOMUtils.escapeHtml(option)}" class="text-blue-600 focus:ring-blue-500 mr-3" ${isChecked} > <span class="text-gray-700 dark:text-gray-300">${DOMUtils.escapeHtml(option)}</span> </label> `; } }) .join(''); return ` <div class="question-block p-4 md:p-6 bg-white rounded-lg border border-gray-200 shadow-sm"> <div class="flex items-start justify-between mb-3 md:mb-4"> <h3 class="text-lg font-medium text-gray-900"> ${index + 1}. ${DOMUtils.escapeHtml(question.question)} </h3> ${question.points ? ` <span class="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full ml-4 flex-shrink-0"> ${question.points} pts </span> ` : ''} </div> <div class="space-y-2 md:space-y-3"> ${optionsHtml} </div> </div> `; } bindQuestionEvents() { const settings = this.settingsService.getSettings(); const state = this.store.getState(); const isSingleMode = state.ui.viewMode === 'single'; if (settings.quiz.instantSubmission) { const optionCards = this.quizContainer.querySelectorAll('.quiz-option-card'); optionCards.forEach(card => { card.addEventListener('click', this.handleInstantSubmission.bind(this)); card.addEventListener('keydown', (e) => { const keyEvent = e; if (keyEvent.key === 'Enter' || keyEvent.key === ' ') { keyEvent.preventDefault(); this.handleInstantSubmission(e); } }); }); } else if (isSingleMode && settings.ui.clickToSelectCards) { const answerCards = this.quizContainer.querySelectorAll('.quiz-answer-card'); answerCards.forEach(card => { card.addEventListener('click', this.handleAnswerCardSelection.bind(this)); card.addEventListener('keydown', (e) => { const keyEvent = e; if (keyEvent.key === 'Enter' || keyEvent.key === ' ') { keyEvent.preventDefault(); this.handleAnswerCardSelection(e); } if (keyEvent.key === 'ArrowDown' || keyEvent.key === 'ArrowUp') { keyEvent.preventDefault(); this.handleCardKeyboardNavigation(keyEvent); } }); }); } else { const radioInputs = this.quizContainer.querySelectorAll('input[type="radio"]'); radioInputs.forEach(input => { input.addEventListener('change', this.handleAnswerSelection.bind(this)); }); } } bindContinueButton() { this.continueButton = document.getElementById('continue-btn'); if (this.continueButton) { this.continueButton.addEventListener('click', () => { this.handleNextQuestion(); }); this.continueButton.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.handleNextQuestion(); } }); } } bindCompleteQuizButton() { const completeQuizButton = document.getElementById('complete-quiz-btn'); if (completeQuizButton) { completeQuizButton.addEventListener('click', () => { console.log('🏁 Complete Quiz button clicked from single mode'); this.handleSubmit(); }); completeQuizButton.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); console.log('🏁 Complete Quiz button activated via keyboard'); this.handleSubmit(); } }); } } async handleInstantSubmission(event) { const target = event.target; const card = target.closest('.quiz-option-card'); if (!card) return; const questionId = card.dataset.questionId; const answer = card.dataset.answer; const settings = this.settingsService.getSettings(); if (!questionId || !answer) return; console.log('🚀 Instant submission:', questionId, answer); if (card.classList.contains('processing')) return; card.classList.add('processing'); this.store.updateAnswer(questionId, answer); const questionBlock = card.closest('.question-block'); if (questionBlock) { questionBlock.querySelectorAll('.quiz-option-card').forEach(otherCard => { otherCard.classList.remove('border-blue-500', 'bg-blue-50', 'dark:bg-blue-900/20', 'shadow-md'); otherCard.classList.add('border-gray-200', 'dark:border-gray-600'); otherCard.setAttribute('aria-pressed', 'false'); }); card.classList.remove('border-gray-200', 'dark:border-gray-600'); card.classList.add('border-blue-500', 'bg-blue-50', 'dark:bg-blue-900/20', 'shadow-md'); card.setAttribute('aria-pressed', 'true'); const indicator = card.querySelector('.answer-indicator'); if (indicator) { indicator.className = 'answer-indicator w-6 h-6 rounded-full border-2 border-blue-500 bg-blue-500 transition-all duration-200'; indicator.innerHTML = '<div class="w-2 h-2 bg-white rounded-full mx-auto mt-1"></div>'; } } if (settings.quiz.showImmediateFeedback && questionBlock) { this.showImmediateResult(questionId, answer, questionBlock); } const state = this.store.getState(); if (state.ui.viewMode === 'single') { setTimeout(() => { if (settings.quiz.allowAnswerChange && !card.classList.contains('final-answer')) { card.classList.remove('processing'); return; } if (state.currentQuiz && this.currentQuestionIndex >= state.currentQuiz.questions.length - 1) { const allAnswered = Object.keys(state.userAnswers).length >= state.currentQuiz.questions.length; if (allAnswered) { this.handleSubmit(); } } else { this.handleNextQuestion(); } }, settings.quiz.instantFeedbackDelay); } card.classList.remove('processing'); } handleAnswerCardSelection(event) { const target = event.target; const card = target.closest('.quiz-answer-card'); if (!card) return; const questionId = card.dataset.questionId; const answer = card.dataset.answer; const settings = this.settingsService.getSettings(); if (!questionId || !answer) return; console.log('🎯 Answer card selected:', questionId, answer); this.store.updateAnswer(questionId, answer); const questionBlock = card.closest('.question-block'); if (questionBlock) { questionBlock.querySelectorAll('.quiz-answer-card').forEach(otherCard => { otherCard.classList.remove('border-blue-500', 'bg-blue-50', 'dark:bg-blue-900/20', 'shadow-md', 'ring-2', 'ring-blue-200', 'dark:ring-blue-800'); otherCard.classList.add('border-gray-200', 'dark:border-gray-600'); otherCard.setAttribute('aria-pressed', 'false'); const indicator = otherCard.querySelector('.answer-indicator'); if (indicator) { indicator.className = 'answer-indicator w-6 h-6 rounded-full border-2 border-gray-300 dark:border-gray-500 transition-all duration-200 flex-shrink-0'; indicator.innerHTML = ''; } }); card.classList.remove('border-gray-200', 'dark:border-gray-600'); card.classList.add('border-blue-500', 'bg-blue-50', 'dark:bg-blue-900/20', 'shadow-md', 'ring-2', 'ring-blue-200', 'dark:ring-blue-800'); card.setAttribute('aria-pressed', 'true'); const indicator = card.querySelector('.answer-indicator'); if (indicator) { indicator.className = 'answer-indicator w-6 h-6 rounded-full border-2 border-blue-500 bg-blue-500 transition-all duration-200 flex-shrink-0'; indicator.innerHTML = '<div class="w-2 h-2 bg-white rounded-full mx-auto mt-1"></div>'; } card.classList.add('animate-pulse'); setTimeout(() => { card.classList.remove('animate-pulse'); }, 300); } if (settings.quiz.showImmediateFeedback && questionBlock) { this.showImmediateResult(questionId, answer, questionBlock); } if (settings.ui.announceSelections) { this.announceSelection(answer); } const state = this.store.getState(); if (state.ui.viewMode === 'single' && settings.ui.showContinueButton) { this.showContinueButton(); } this.showSelectionCheckmark(card); } handleCardKeyboardNavigation(event) { const currentCard = event.target; const questionBlock = currentCard.closest('.question-block'); if (!questionBlock) return; const allCards = Array.from(questionBlock.querySelectorAll('.quiz-answer-card')); const currentIndex = allCards.indexOf(currentCard); let nextIndex = currentIndex; if (event.key === 'ArrowDown') { nextIndex = (currentIndex + 1) % allCards.length; } else if (event.key === 'ArrowUp') { nextIndex = currentIndex === 0 ? allCards.length - 1 : currentIndex - 1; } if (nextIndex !== currentIndex) { const nextCard = allCards[nextIndex]; nextCard.focus(); allCards.forEach(card => card.classList.remove('ring-2', 'ring-gray-400')); nextCard.classList.add('ring-2', 'ring-gray-400'); setTimeout(() => { nextCard.classList.remove('ring-2', 'ring-gray-400'); }, 1000); } } handleAnswerSelection(event) { console.log('🎯 Answer selection triggered'); const input = event.target; const questionId = input.name.replace('question-', ''); console.log('📝 Question ID:', questionId, 'Answer:', input.value); this.store.updateAnswer(questionId, input.value); if (this.currentSelectionTimeout) { clearTimeout(this.currentSelectionTimeout); } const questionBlock = input.closest('.question-block'); const selectedLabel = input.closest('label'); const settings = this.settingsService.getSettings(); if (questionBlock && selectedLabel) { console.log('✨ Applying visual feedback to selected option'); questionBlock.querySelectorAll('label').forEach(label => { label.removeAttribute('data-selected-feedback'); }); selectedLabel.setAttribute('data-selected-feedback', 'true'); console.log('🎨 Applied data-selected-feedback:', selectedLabel.hasAttribute('data-selected-feedback')); questionBlock.classList.add('border-green-200'); this.showSelectionCheckmark(selectedLabel); selectedLabel.classList.add('just-selected'); setTimeout(() => { selectedLabel.classList.remove('just-selected'); }, 300); if (settings.quiz.showImmediateFeedback) { this.showImmediateResult(questionId, input.value, questionBlock); } if (settings.ui.announceSelections) { this.announceSelection(input.value); } const state = this.store.getState(); if (state.ui.viewMode === 'single' && settings.ui.showContinueButton) { this.showContinueButton(); } this.currentSelectionTimeout = setTimeout(() => { questionBlock.classList.remove('bg-green-50'); if (settings.ui.autoAdvanceQuestions && !settings.ui.showContinueButton) { if (state.ui.viewMode === 'single') { this.handleNextQuestion(); } } }, settings.ui.selectionFeedbackDuration); } } showSelectionCheckmark(selectedLabel) { const checkmark = document.createElement('div'); checkmark.className = 'selection-checkmark absolute -top-2 -right-2 bg-green-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm font-bold animate-bounce'; checkmark.innerHTML = '✓'; checkmark.style.zIndex = '10'; checkmark.setAttribute('aria-hidden', 'true'); selectedLabel.classList.add('relative'); selectedLabel.appendChild(checkmark); setTimeout(() => { if (checkmark.parentNode) { checkmark.parentNode.removeChild(checkmark); } }, 2000); } announceSelection(selectedValue) { let announcement = document.getElementById('selection-announcement'); if (!announcement) { announcement = document.createElement('div'); announcement.id = 'selection-announcement'; announcement.className = 'sr-only'; announcement.setAttribute('aria-live', 'polite'); announcement.setAttribute('aria-atomic', 'true'); announcement.setAttribute('role', 'status'); document.body.appendChild(announcement); } announcement.textContent = `Answer selected: ${selectedValue}`; } showContinueButton() { const continueContainer = this.quizContainer.querySelector('.continue-button-container'); const completeContainer = this.quizContainer.querySelector('.complete-quiz-container'); if (continueContainer) { continueContainer.style.display = 'block'; setTimeout(() => { if (this.continueButton) { this.continueButton.focus(); this.continueButton.classList.add('ring-2', 'ring-blue-500', 'ring-offset-2'); } }, 100); } else if (completeContainer) { completeContainer.style.display = 'block'; setTimeout(() => { const completeQuizButton = document.getElementById('complete-quiz-btn'); if (completeQuizButton) { completeQuizButton.focus(); completeQuizButton.classList.add('ring-2', 'ring-green-500', 'ring-offset-2'); } }, 100); } } showImmediateResult(questionId, userAnswer, questionBlock) { const state = this.store.getState(); const settings = this.settingsService.getSettings(); if (!state.currentQuiz) return; const question = state.currentQuiz.questions.find(q => q.id === questionId); if (!question) return; const isCorrect = question.correctAnswer === userAnswer; const existingFeedback = questionBlock.querySelector('.immediate-feedback'); if (existingFeedback) { existingFeedback.remove(); } const feedbackDiv = document.createElement('div'); feedbackDiv.className = 'immediate-feedback mt-4 p-3 rounded-lg border transition-all duration-300 animate-fadeIn'; if (isCorrect) { feedbackDiv.className += ' bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800'; } else { feedbackDiv.className += ' bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800'; } let feedbackContent = ''; if (isCorrect) { feedbackContent += ` <div class="flex items-center gap-2 mb-2"> <i data-lucide="check-circle" class="w-5 h-5 text-green-600 dark:text-green-400"></i> <span class="font-medium text-green-800 dark:text-green-200">Correct!</span> </div> `; if (settings.quiz.enableCoachingMode) { feedbackContent += `<p class="text-green-700 dark:text-green-300 text-sm">🎉 Well done! You've got it right.</p>`; } } else { feedbackContent += ` <div class="flex items-center gap-2 mb-2"> <i data-lucide="x-circle" class="w-5 h-5 text-red-600 dark:text-red-400"></i> <span class="font-medium text-red-800 dark:text-red-200">Not quite right</span> </div> `; feedbackContent += ` <p class="text-red-700 dark:text-red-300 text-sm mb-2"> <span class="font-medium">Correct answer:</span> ${DOMUtils.escapeHtml(question.correctAnswer)} </p> `; if (settings.quiz.enableCoachingMode) { feedbackContent += `<p class="text-red-700 dark:text-red-300 text-sm">💡 Don't worry! Learning is about making mistakes and improving.</p>`; } } if (settings.quiz.showExplanationsAfterAnswer && question.explanation) { feedbackContent += ` <div class="mt-3 pt-3 border-t border-current border-opacity-20"> <div class="flex items-start gap-2"> <i data-lucide="lightbulb" class="w-4 h-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0"></i> <div> <p class="font-medium text-blue-800 dark:text-blue-200 text-sm mb-1">Explanation</p> <p class="text-blue-700 dark:text-blue-300 text-sm leading-relaxed">${DOMUtils.escapeHtml(question.explanation)}</p> </div> </div> </div> `; } if (settings.quiz.enableCoachingMode && settings.quiz.coachingIntensity !== 'basic') { if (!isCorrect && settings.quiz.coachingIntensity === 'comprehensive') { feedbackContent += ` <div class="mt-3 pt-3 border-t border-current border-opacity-20"> <div class="flex items-start gap-2"> <i data-lucide="target" class="w-4 h-4 text-purple-600 dark:text-purple-400 mt-0.5 flex-shrink-0"></i> <div> <p class="font-medium text-purple-800 dark:text-purple-200 text-sm mb-1">Study Tip</p> <p class="text-purple-700 dark:text-purple-300 text-sm">Review this concept again to reinforce your understanding.</p> </div> </div> </div> `; } } feedbackDiv.innerHTML = feedbackContent; questionBlock.appendChild(feedbackDiv); this.reinitializeIcons(); if (settings.quiz.coachingIntensity !== 'comprehensive') { setTimeout(() => { if (feedbackDiv.parentNode) { feedbackDiv.style.opacity = '0'; setTimeout(() => { if (feedbackDiv.parentNode) { feedbackDiv.remove(); } }, 300); } }, 4000); } } showCompletionCelebration() { const celebration = document.createElement('div'); celebration.className = 'fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-30 pointer-events-none'; celebration.innerHTML = ` <div class="bg-white dark:bg-surface-800 rounded-2xl p-8 mx-4 text-center shadow-2xl transform scale-95 opacity-0 transition-all duration-500" id="celebration-content"> <div class="text-6xl mb-4">🎉</div> <h2 class="text-2xl font-bold text-gray-900 dark:text-surface-100 mb-2">Quiz Complete!</h2> <p class="text-gray-600 dark:text-surface-400">Great job! Let's see how you did...</p> <div class="mt-4 flex justify-center"> <div class="animate-spin h-6 w-6 border-2 border-blue-500 border-t-transparent rounded-full"></div> </div> </div> `; document.body.appendChild(celebration); requestAnimationFrame(() => { const content = celebration.querySelector('#celebration-content'); if (content) { content.style.transform = 'scale(1)'; content.style.opacity = '1'; } }); this.createConfettiEffect(); setTimeout(() => { celebration.style.opacity = '0'; setTimeout(() => { document.body.removeChild(celebration); }, 500); }, 2000); } createConfettiEffect() { const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f9ca24', '#6c5ce7']; const confettiCount = 50; for (let i = 0; i < confettiCount; i++) { const confetti = document.createElement('div'); confetti.className = 'fixed pointer-events-none z-40'; confetti.style.cssText = ` width: 10px; height: 10px; background: ${colors[Math.floor(Math.random() * colors.length)]}; left: ${Math.random() * 100}vw; top: -10px; opacity: 1; transform: rotate(${Math.random() * 360}deg); animation: confetti-fall ${2 + Math.random() * 3}s linear forwards; `; document.body.appendChild(confetti); setTimeout(() => { if (confetti.parentNode) { confetti.parentNode.removeChild(confetti); } }, 5000); } } reinitializeIcons() { if (window.lucide?.createIcons) { window.lucide.createIcons(); } } } //# sourceMappingURL=QuizContent.original.js.map