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
JavaScript
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