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.

365 lines (352 loc) 16.5 kB
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