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.
365 lines (352 loc) • 16.5 kB
JavaScript
import { SettingsService } from '../services/SettingsService';
import { DOMUtils } from '../utils/index';
export class QuizStartModal {
constructor(options) {
this.startButton = null;
this.cancelButton = null;
this.element = null;
this.quiz = options.quiz;
this.onStart = options.onStart;
this.onCancel = options.onCancel;
this.settingsService = SettingsService.getInstance();
}
static show(options) {
const modal = new QuizStartModal(options);
modal.mount();
modal.appear();
return modal;
}
mount() {
if (this.element)
return;
this.element = document.createElement('div');
this.element.id = 'quiz-start-overlay';
this.element.style.display = 'none';
this.render();
this.bindEvents();
document.body.appendChild(this.element);
}
unmount() {
if (this.element && this.element.parentNode) {
document.removeEventListener('keydown', this.handleEscapeKey);
this.element.parentNode.removeChild(this.element);
this.element = null;
}
}
render() {
const estimatedTime = this.calculateEstimatedTime();
const categoryColor = this.getCategoryColor(this.quiz.category);
const difficultyInfo = this.getDifficultyInfo(this.quiz.difficulty);
if (!this.element)
return;
this.element.innerHTML = `
<div class="quiz-start-overlay fixed inset-0 z-[10000] flex items-center justify-center bg-black bg-opacity-60 backdrop-blur-sm opacity-0 transition-all duration-300" id="quiz-start-inner-overlay">
<div class="quiz-start-modal bg-white dark:bg-gray-800 rounded-3xl shadow-2xl max-w-lg w-11/12 mx-4 overflow-hidden transform scale-95 transition-all duration-300" id="quiz-start-modal">
<!-- Category Header with Gradient -->
<div class="quiz-category-header h-2 ${categoryColor}"></div>
<!-- Main Content -->
<div class="p-8">
<!-- Quiz Badge -->
<div class="flex items-center justify-center mb-6">
<div class="quiz-category-badge inline-flex items-center px-4 py-2 rounded-full text-sm font-medium ${this.getCategoryBadgeStyle(this.quiz.category)}">
<span class="mr-2">${this.getCategoryIcon(this.quiz.category)}</span>
${this.quiz.category || 'General'}
</div>
</div>
<!-- Quiz Title -->
<div class="text-center mb-6">
<h2 class="quiz-title text-3xl font-bold text-gray-900 dark:text-white mb-3 leading-tight">
${DOMUtils.escapeHtml(this.quiz.title)}
</h2>
${this.quiz.description
? `
<p class="quiz-description text-gray-600 dark:text-gray-300 text-lg leading-relaxed">
${DOMUtils.escapeHtml(this.quiz.description)}
</p>
`
: ''}
</div>
<!-- Quiz Metadata -->
<div class="quiz-meta-grid grid grid-cols-2 gap-4 mb-8">
<div class="meta-item text-center p-4 bg-gray-50 dark:bg-gray-700 rounded-xl">
<div class="meta-icon text-2xl mb-2">📝</div>
<div class="meta-value text-xl font-semibold text-gray-900 dark:text-white">
${this.quiz.questions?.length || 0}
</div>
<div class="meta-label text-sm text-gray-600 dark:text-gray-400">Questions</div>
</div>
<div class="meta-item text-center p-4 bg-gray-50 dark:bg-gray-700 rounded-xl">
<div class="meta-icon text-2xl mb-2">⏱️</div>
<div class="meta-value text-xl font-semibold text-gray-900 dark:text-white">
~${estimatedTime}
</div>
<div class="meta-label text-sm text-gray-600 dark:text-gray-400">Minutes</div>
</div>
</div>
<!-- Difficulty Indicator -->
<div class="difficulty-section mb-8">
<div class="flex items-center justify-center mb-3">
<span class="text-sm font-medium text-gray-600 dark:text-gray-400 mr-3">Difficulty:</span>
<div class="difficulty-badge inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${difficultyInfo.style}">
<span class="mr-1">${difficultyInfo.icon}</span>
${this.quiz.difficulty || 'Medium'}
</div>
</div>
<div class="difficulty-meter flex justify-center">
${this.renderDifficultyMeter()}
</div>
</div>
<!-- Timer Configuration Section -->
<div class="timer-config-section p-6 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 border-t border-b border-blue-200 dark:border-blue-700/50">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center">
<span class="text-lg mr-2">⏱️</span>
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Timer Settings</h3>
</div>
<div class="text-sm text-gray-500 dark:text-gray-400">Optional</div>
</div>
<div class="space-y-4">
<!-- Use Timer (Modern Simplified Interface) -->
<label class="flex items-center justify-between p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<div class="flex items-center">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Use Timer</span>
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">Auto-starts with quiz</span>
</div>
<input type="checkbox" id="quiz-use-timer" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
</label>
<!-- Timer Duration -->
<div class="p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Timer Duration</label>
<select id="quiz-timer-duration" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-blue-500 focus:border-blue-500">
<option value="300">5 minutes</option>
<option value="600">10 minutes</option>
<option value="900">15 minutes</option>
<option value="1200">20 minutes</option>
<option value="1800">30 minutes</option>
<option value="3600">1 hour</option>
<option value="0">No limit</option>
</select>
</div>
</div>
</div>
<!-- Call to Action -->
<div class="cta-section text-center">
<button id="start-quiz-btn" class="start-quiz-btn w-full bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-semibold py-4 px-8 rounded-xl transition-all duration-300 transform hover:scale-105 hover:shadow-lg mb-4 group">
<span class="flex items-center justify-center">
<span class="btn-icon mr-3 text-xl transition-transform group-hover:scale-110">🚀</span>
<span class="btn-text text-lg">Begin Quiz</span>
<span class="ml-3 opacity-0 group-hover:opacity-100 transition-opacity">→</span>
</span>
</button>
<button id="cancel-quiz-btn" class="cancel-btn text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 font-medium py-2 px-4 rounded-lg transition-colors duration-200">
Maybe Later
</button>
</div>
</div>
</div>
</div>
`;
}
bindEvents() {
if (!this.element)
return;
this.startButton = this.element.querySelector('#start-quiz-btn');
this.cancelButton = this.element.querySelector('#cancel-quiz-btn');
const useTimerCheckbox = this.element.querySelector('#quiz-use-timer');
const timerDurationSelect = this.element.querySelector('#quiz-timer-duration');
this.loadTimerSettings(useTimerCheckbox, timerDurationSelect);
useTimerCheckbox?.addEventListener('change', () => {
this.saveTimerSettings(useTimerCheckbox, timerDurationSelect);
});
timerDurationSelect?.addEventListener('change', () => {
this.saveTimerSettings(useTimerCheckbox, timerDurationSelect);
});
this.startButton?.addEventListener('click', () => {
this.triggerStartAnimation();
setTimeout(() => {
this.onStart(this.quiz);
this.hide();
}, 600);
});
this.cancelButton?.addEventListener('click', () => {
this.hide();
this.onCancel();
});
document.addEventListener('keydown', this.handleEscapeKey.bind(this));
const overlay = this.element?.querySelector('#quiz-start-inner-overlay');
overlay?.addEventListener('click', e => {
if (e.target === overlay) {
this.hide();
this.onCancel();
}
});
}
handleEscapeKey(event) {
if (event.key === 'Escape') {
this.hide();
this.onCancel();
}
}
calculateEstimatedTime() {
const questionCount = this.quiz.questions?.length || 0;
return Math.max(1, Math.ceil(questionCount * 1.5));
}
getCategoryColor(category) {
const colors = {
Technology: 'bg-gradient-to-r from-blue-500 to-cyan-500',
Science: 'bg-gradient-to-r from-green-500 to-emerald-500',
History: 'bg-gradient-to-r from-amber-500 to-orange-500',
Arts: 'bg-gradient-to-r from-purple-500 to-pink-500',
Sports: 'bg-gradient-to-r from-red-500 to-rose-500',
General: 'bg-gradient-to-r from-gray-500 to-slate-500',
};
return colors[category || 'General'] || colors['General'];
}
getCategoryBadgeStyle(category) {
const styles = {
Technology: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
Science: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
History: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200',
Arts: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
Sports: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
General: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200',
};
return styles[category || 'General'] || styles['General'];
}
getCategoryIcon(category) {
const icons = {
Technology: '💻',
Science: '🔬',
History: '📚',
Arts: '🎨',
Sports: '⚽',
General: '🧠',
};
return icons[category || 'General'] || icons['General'];
}
getDifficultyInfo(difficulty) {
const info = {
Beginner: {
icon: '🌟',
style: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
},
Easy: {
icon: '✨',
style: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
},
Medium: {
icon: '🔥',
style: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
},
Hard: {
icon: '⚡',
style: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
},
Expert: {
icon: '💎',
style: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
},
};
return info[difficulty || 'Medium'] || info['Medium'];
}
renderDifficultyMeter() {
const difficulty = this.quiz.difficulty || 'Medium';
const levels = ['Beginner', 'Easy', 'Medium', 'Hard', 'Expert'];
const currentLevel = levels.indexOf(difficulty);
return levels
.map((level, index) => {
const isActive = index <= currentLevel;
const isCurrentLevel = index === currentLevel;
return `
<div class="meter-dot w-3 h-3 rounded-full mx-1 transition-all duration-300 ${isActive
? isCurrentLevel
? 'bg-blue-500 scale-125 shadow-lg'
: 'bg-blue-400'
: 'bg-gray-300 dark:bg-gray-600'}"></div>
`;
})
.join('');
}
triggerStartAnimation() {
if (this.startButton) {
this.startButton.classList.add('animate-pulse', 'scale-95');
this.startButton.innerHTML = `
<span class="flex items-center justify-center">
<div class="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent mr-3"></div>
<span>Starting Quiz...</span>
</span>
`;
}
}
appear() {
if (!this.element)
return;
this.element.style.display = 'block';
this.element.style.position = 'fixed';
this.element.style.inset = '0';
this.element.style.zIndex = '10000';
this.element.style.visibility = 'visible';
this.element.style.opacity = '1';
requestAnimationFrame(() => {
const overlay = this.element?.querySelector('#quiz-start-inner-overlay');
const modal = this.element?.querySelector('#quiz-start-modal');
if (overlay && modal) {
overlay.classList.remove('opacity-0');
modal.classList.remove('scale-95');
modal.classList.add('scale-100');
}
});
}
hide() {
if (!this.element)
return;
const overlay = this.element.querySelector('#quiz-start-inner-overlay');
const modal = this.element.querySelector('#quiz-start-modal');
if (overlay && modal) {
overlay.classList.add('opacity-0');
modal.classList.add('scale-95');
modal.classList.remove('scale-100');
setTimeout(() => {
this.unmount();
}, 300);
}
}
onUnmount() {
document.removeEventListener('keydown', this.handleEscapeKey);
}
loadTimerSettings(useTimerCheckbox, timerDurationSelect) {
try {
const settings = this.settingsService.getSettings();
if (useTimerCheckbox) {
useTimerCheckbox.checked = settings.quiz?.useTimer || false;
}
if (timerDurationSelect) {
timerDurationSelect.value = settings.quiz?.timerDuration?.toString() || '900';
}
}
catch (error) {
console.warn('Failed to load timer settings:', error);
}
}
saveTimerSettings(useTimerCheckbox, timerDurationSelect) {
try {
const useTimer = useTimerCheckbox?.checked || false;
const timerDuration = parseInt(timerDurationSelect?.value || '900');
this.settingsService.updateQuizSettings({
useTimer,
timerDuration,
});
console.log('💾 Timer settings saved:', {
useTimer,
timerDuration,
});
window.dispatchEvent(new CustomEvent('timerSettingsChanged', {
detail: { useTimer, timerDuration },
}));
}
catch (error) {
console.error('Failed to save timer settings:', error);
}
}
}
//# sourceMappingURL=QuizStartModal.js.map