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.

364 lines (315 loc) 11.4 kB
/** * @moduleName: TimerService * @version: 1.0.0 * @since: 2025-07-24 * @lastUpdated: 2025-07-24 * @projectSummary: Quiz timing functionality with configurable time limits and pressure mode * @techStack: TypeScript, DOM API, Local Storage * @dependency: None * @interModuleDependency: SettingsService.ts, AppStore.ts * @requirementsTraceability {@link Requirements.REQ_QUIZ_008} - Quiz timer functionality and time-based features * @requirementsTraceability {@link Requirements.REQ_UI_006} - Status bar and timer display integration * @requirementsTraceability {@link Requirements.REQ_CONFIG_002} - Timer configuration and settings management * @briefDescription: Manages quiz timers, time pressure features, and timing-related UI updates * @methods: startTimer, stopTimer, pauseTimer, getTimeRemaining, formatTime, isTimerEnabled * @contributors: GitHub Copilot * @examples: const timer = TimerService.getInstance(); timer.startTimer(300); // 5 minutes * @vulnerabilitiesAssessment: No security concerns - client-side timing only, validation on server */ export interface TimerSettings { enabled: boolean; duration: number; // in seconds warning: number; // seconds before end to show warning autoSubmit: boolean; // auto-submit when time expires } export interface TimerState { isRunning: boolean; timeRemaining: number; totalTime: number; isWarning: boolean; isExpired: boolean; } export class TimerService { private static instance: TimerService; private timerState: TimerState; private intervalId: NodeJS.Timeout | null = null; private callbacks: Array<(state: TimerState) => void> = []; private timerDisplay: HTMLElement | null = null; private timerContainer: HTMLElement | null = null; private warningThreshold: number = 60; // default warning threshold in seconds private timerToggle: HTMLInputElement | null = null; private constructor() { this.timerState = { isRunning: false, timeRemaining: 0, totalTime: 0, isWarning: false, isExpired: false, }; this.initializeDOM(); } /** * @description Gets the singleton instance of TimerService * @returns {TimerService} The singleton TimerService instance */ public static getInstance(): TimerService { if (!TimerService.instance) { TimerService.instance = new TimerService(); } return TimerService.instance; } private initializeDOM(): void { this.timerDisplay = document.querySelector('#timer-display'); this.timerContainer = document.querySelector('#quiz-timer'); // Setup timer toggle this.timerToggle = document.querySelector('#timer-toggle') as HTMLInputElement; if (this.timerToggle) { this.timerToggle.addEventListener('change', e => { const enabled = (e.target as HTMLInputElement).checked; this.toggleTimer(enabled); }); } } /** * @description Subscribes to timer state changes * @param {function} callback Function to call when timer state changes * @returns {function} Unsubscribe function to remove the callback */ public subscribe(callback: (state: TimerState) => void): () => void { this.callbacks.push(callback); return () => { const index = this.callbacks.indexOf(callback); if (index > -1) { this.callbacks.splice(index, 1); } }; } private notifySubscribers(): void { this.callbacks.forEach(callback => callback(this.timerState)); } /** * @description Starts the quiz timer with specified duration * @param {number} durationInSeconds Timer duration in seconds (default: 300) * @param {number} warningThreshold Warning threshold in seconds (default: 60) */ public startTimer(durationInSeconds: number = 300, warningThreshold: number = 60): void { this.stopTimer(); // Clear any existing timer this.warningThreshold = warningThreshold; this.timerState = { isRunning: true, timeRemaining: durationInSeconds, totalTime: durationInSeconds, isWarning: false, isExpired: false, }; this.showTimer(); this.updateDisplay(); this.intervalId = setInterval(() => { this.tick(); }, 1000); this.notifySubscribers(); } /** * @description Stops the timer and clears all intervals */ public stopTimer(): void { if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; } this.timerState.isRunning = false; this.hideTimer(); this.notifySubscribers(); } /** * @description Auto-starts timer based on settings (REQ-UI-012) - MODERNIZED * @param {SettingsService} settingsService Settings service instance */ public autoStartIfEnabled(settingsService: any): void { const settings = settingsService.getSettings(); if (settings.quiz.useTimer) { console.log('🚀 Auto-starting timer (modernized single setting)'); // Use configured duration or 15-minute default const timerDuration = settings.quiz.timerDuration || 900; // 15 minutes default this.startTimer(timerDuration, this.warningThreshold); } } public pauseTimer(): void { if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; } this.timerState.isRunning = false; this.notifySubscribers(); } public resumeTimer(): void { if (!this.timerState.isExpired && this.timerState.timeRemaining > 0) { this.timerState.isRunning = true; this.intervalId = setInterval(() => { this.tick(); }, 1000); this.notifySubscribers(); } } private tick(): void { this.timerState.timeRemaining -= 1; // Check for warning state using the configurable threshold this.timerState.isWarning = this.timerState.timeRemaining <= this.warningThreshold; // Check for expiration if (this.timerState.timeRemaining <= 0) { this.timerState.timeRemaining = 0; this.timerState.isExpired = true; this.timerState.isRunning = false; this.stopTimer(); // Auto-submit if enabled this.handleTimeExpiry(); } this.updateDisplay(); this.notifySubscribers(); } private handleTimeExpiry(): void { // Retrieve TimerSettings from localStorage or a settings service let autoSubmit = false; try { const settingsStr = localStorage.getItem('timerSettings'); if (settingsStr) { const settings: TimerSettings = JSON.parse(settingsStr); autoSubmit = settings.autoSubmit; } } catch (e) { // Fallback: do not auto-submit if settings are not available autoSubmit = false; } const submitButton = document.querySelector('#submit-button') as HTMLButtonElement; if (submitButton) { // Show time expired message this.showTimeExpiredMessage(); if (autoSubmit) { // Auto-submit after a short delay to let user see the message setTimeout(() => { submitButton.click(); }, 2000); } } } private showTimeExpiredMessage(): void { const message = document.createElement('div'); message.className = 'fixed top-20 left-1/2 transform -translate-x-1/2 bg-red-500 text-white px-6 py-3 rounded-lg shadow-lg z-50 animate-bounce'; message.textContent = "⏰ Time's up! " + (this.getAutoSubmitSetting() ? 'Submitting quiz...' : 'Please submit your quiz.'); document.body.appendChild(message); setTimeout(() => { if (document.body.contains(message)) { document.body.removeChild(message); } }, 3000); } private getAutoSubmitSetting(): boolean { try { const settingsStr = localStorage.getItem('timerSettings'); if (settingsStr) { const settings: TimerSettings = JSON.parse(settingsStr); return settings.autoSubmit; } } catch (e) { // Fallback } return false; } private updateDisplay(): void { if (this.timerDisplay) { this.timerDisplay.textContent = this.formatTime(this.timerState.timeRemaining); // Update styling based on state if (this.timerState.isWarning) { this.timerDisplay.classList.add('text-red-500', 'font-bold', 'animate-pulse'); } else { this.timerDisplay.classList.remove('text-red-500', 'font-bold', 'animate-pulse'); } } } private showTimer(): void { if (this.timerContainer) { this.timerContainer.classList.remove('hidden'); this.timerContainer.classList.add('flex'); } } private hideTimer(): void { if (this.timerContainer) { this.timerContainer.classList.add('hidden'); this.timerContainer.classList.remove('flex'); } } private toggleTimer(enabled: boolean): void { if (enabled) { // Start timer with default 5 minutes (300 seconds) and default warning threshold this.startTimer(300, this.warningThreshold); } else { this.stopTimer(); } } /** * @description Formats seconds into MM:SS format * @param {number} seconds Time in seconds to format * @returns {string} Formatted time string (MM:SS) */ public formatTime(seconds: number): string { const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`; } /** * @description Gets current timer state * @returns {TimerState} Copy of current timer state */ public getState(): TimerState { return { ...this.timerState }; } public isTimerEnabled(): boolean { // MODERNIZED: Remove DOM dependency, use settings service approach if available // This method is kept for compatibility but should use settings-based logic return this.timerToggle?.checked || false; } public getTimeRemaining(): number { return this.timerState.timeRemaining; } public getProgressPercentage(): number { if (this.timerState.totalTime === 0) return 0; return ( ((this.timerState.totalTime - this.timerState.timeRemaining) / this.timerState.totalTime) * 100 ); } // Additional utility methods for enhanced functionality public setWarningThreshold(seconds: number): void { this.warningThreshold = seconds; } public getWarningThreshold(): number { return this.warningThreshold; } public saveTimerSettings(settings: TimerSettings): void { try { localStorage.setItem('timerSettings', JSON.stringify(settings)); } catch (e) { console.warn('Failed to save timer settings:', e); } } public loadTimerSettings(): TimerSettings { try { const settingsStr = localStorage.getItem('timerSettings'); if (settingsStr) { return JSON.parse(settingsStr); } } catch (e) { console.warn('Failed to load timer settings:', e); } // Return default settings return { enabled: false, duration: 300, // 5 minutes warning: 60, // 1 minute warning autoSubmit: false, }; } }