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.

304 lines 10.6 kB
export class TourService { constructor() { this.tours = new Map(); this.currentTour = null; this.currentStepIndex = 0; this.tourModal = null; this.overlay = null; this.spotlight = null; this.callbacks = []; this.initializeTours(); this.createOverlayElements(); } static getInstance() { if (!TourService.instance) { TourService.instance = new TourService(); } return TourService.instance; } initializeTours() { this.registerTour({ id: 'welcome', name: 'Welcome to Quiz Server', description: 'Quick introduction to the quiz application', autoStart: true, showOnFirstVisit: true, steps: [ { id: 'welcome', title: '🎉 Welcome to Quiz Server!', content: 'Take interactive quizzes with real-time feedback. This quick tour will show you the key features.', position: 'center', showSkip: true, showPrevious: false, showNext: true, video: '/demos/welcome-overview.webm', }, { id: 'quiz-list', title: '📋 Quiz Library', content: 'Browse available quizzes by category and difficulty. Click any quiz to start!', target: '#quiz-list', position: 'right', showSkip: true, showPrevious: true, showNext: true, }, { id: 'settings', title: '⚙️ Customize Experience', content: 'Access settings to personalize your quiz experience - themes, timer, navigation preferences.', target: '[data-tour="settings-button"]', position: 'bottom', showSkip: true, showPrevious: true, showNext: true, action: () => { const settingsBtn = document.querySelector('[data-tour="settings-button"]'); settingsBtn?.classList.add('tour-pulse'); setTimeout(() => settingsBtn?.classList.remove('tour-pulse'), 2000); }, }, { id: 'timer', title: '⏱️ Quiz Timer', content: 'Enable the timer for timed quizzes. Perfect for practice sessions and competitive challenges.', target: '#timer-toggle', position: 'left', showSkip: true, showPrevious: true, showNext: true, }, { id: 'ready', title: "🚀 You're All Set!", content: 'Ready to test your knowledge? Pick a quiz from the sidebar and start learning!', position: 'center', showSkip: false, showPrevious: true, showNext: false, }, ], }); this.registerTour({ id: 'features', name: 'Key Features Tour', description: 'Overview of main application features', autoStart: false, showOnFirstVisit: false, steps: [ { id: 'navigation', title: '🧭 Smart Navigation', content: 'Use Previous/Next buttons or keyboard arrows to navigate through questions.', target: '.nav-buttons', position: 'bottom', showSkip: true, showPrevious: false, showNext: true, }, { id: 'view-modes', title: '👁️ View Modes', content: 'Switch between single question focus or see all questions at once.', target: '[data-tour="view-toggle"]', position: 'top', showSkip: true, showPrevious: true, showNext: false, }, ], }); } createOverlayElements() { this.overlay = document.createElement('div'); this.overlay.id = 'tour-overlay'; this.overlay.className = 'tour-overlay hidden'; this.spotlight = document.createElement('div'); this.spotlight.id = 'tour-spotlight'; this.spotlight.className = 'tour-spotlight'; document.body.appendChild(this.overlay); document.body.appendChild(this.spotlight); } registerTour(tour) { this.tours.set(tour.id, tour); } startTour(tourId) { if (this.isToursDisabled()) { return false; } const tour = this.tours.get(tourId); if (!tour || this.hasCompletedTour(tourId)) { return false; } this.currentTour = tour; this.currentStepIndex = 0; this.showStep(0); this.notifySubscribers(); return true; } showStep(stepIndex) { if (!this.currentTour || stepIndex < 0 || stepIndex >= this.currentTour.steps.length) { return; } const step = this.currentTour.steps[stepIndex]; this.currentStepIndex = stepIndex; this.overlay?.classList.remove('hidden'); if (step.target) { this.highlightElement(step.target); } else { this.hideSpotlight(); } if (step.action) { step.action(); } if (this.tourModal) { this.tourModal.showStep(step, this.getTourState()); } } highlightElement(selector) { const element = document.querySelector(selector); if (!element || !this.spotlight) return; const rect = element.getBoundingClientRect(); const padding = 8; this.spotlight.style.top = `${rect.top - padding}px`; this.spotlight.style.left = `${rect.left - padding}px`; this.spotlight.style.width = `${rect.width + padding * 2}px`; this.spotlight.style.height = `${rect.height + padding * 2}px`; this.spotlight.classList.remove('hidden'); element.classList.add('tour-highlighted'); setTimeout(() => { element.classList.remove('tour-highlighted'); }, 3000); } hideSpotlight() { this.spotlight?.classList.add('hidden'); } nextStep() { if (!this.currentTour || this.currentStepIndex >= this.currentTour.steps.length - 1) { this.completeTour(); return false; } this.showStep(this.currentStepIndex + 1); this.notifySubscribers(); return true; } previousStep() { if (!this.currentTour || this.currentStepIndex <= 0) { return false; } this.showStep(this.currentStepIndex - 1); this.notifySubscribers(); return true; } skipTour() { this.endTour(false); } completeTour() { this.endTour(true); } endTour(completed) { if (!this.currentTour) return; if (completed) { this.markTourAsCompleted(this.currentTour.id); } this.overlay?.classList.add('hidden'); this.hideSpotlight(); this.currentTour = null; this.currentStepIndex = 0; if (this.tourModal) { this.tourModal.hide(); } this.notifySubscribers(); } shouldShowWelcomeTour() { if (this.isToursDisabled()) { return false; } return !this.hasCompletedTour('welcome') && !this.hasSeenApp(); } isToursDisabled() { if (localStorage.getItem('disable-tours') === 'true') { return true; } if (typeof window !== 'undefined' && (window.__PLAYWRIGHT__ || window.__TEST_MODE__ || navigator.webdriver || window.Cypress)) { return true; } return false; } hasCompletedTour(tourId) { const completed = localStorage.getItem(`tour-completed-${tourId}`); return completed === 'true'; } markTourAsCompleted(tourId) { localStorage.setItem(`tour-completed-${tourId}`, 'true'); localStorage.setItem('app-first-visit', 'false'); } hasSeenApp() { return localStorage.getItem('app-first-visit') === 'false'; } getTourState() { if (!this.currentTour) { return { isActive: false, currentTour: null, currentStep: 0, totalSteps: 0, canGoBack: false, canGoNext: false, canSkip: false, }; } const step = this.currentTour.steps[this.currentStepIndex]; return { isActive: true, currentTour: this.currentTour.id, currentStep: this.currentStepIndex + 1, totalSteps: this.currentTour.steps.length, canGoBack: step.showPrevious && this.currentStepIndex > 0, canGoNext: step.showNext && this.currentStepIndex < this.currentTour.steps.length - 1, canSkip: step.showSkip, }; } subscribe(callback) { this.callbacks.push(callback); return () => { const index = this.callbacks.indexOf(callback); if (index > -1) { this.callbacks.splice(index, 1); } }; } notifySubscribers() { const state = this.getTourState(); this.callbacks.forEach(callback => { try { callback(state); } catch (error) { console.error('Error in tour state callback:', error); } }); } setTourModal(modal) { this.tourModal = modal; } getAvailableTours() { return Array.from(this.tours.values()); } resetTourProgress() { this.tours.forEach((_, tourId) => { localStorage.removeItem(`tour-completed-${tourId}`); }); localStorage.removeItem('app-first-visit'); } } export const tourService = TourService.getInstance(); //# sourceMappingURL=TourService.js.map