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