import Module from '../core/Module.js'; import ttsService from '../services/TTSService.js'; class SettingsDebug extends Module { constructor(name, dependencies, config) { super(name, ['eventBus', 'router']); if (!dependencies.eventBus || !dependencies.router) { throw new Error('SettingsDebug requires EventBus and Router dependencies'); } this._eventBus = dependencies.eventBus; this._router = dependencies.router; this._config = config || {}; // Internal state this._container = null; this._debugMessages = []; this._availableVoices = []; this._ttsSettings = { rate: 0.85, pitch: 1.0, volume: 1.0, voicesByLanguage: {} // e.g., { 'en-US': 'Google US English', 'zh-CN': 'Google ๆ™ฎ้€š่ฏ' } }; Object.seal(this); } async init() { this._validateNotDestroyed(); this._loadTTSSettings(); this._injectCSS(); this._setupEventListeners(); this._exposePublicAPI(); this._setInitialized(); } async destroy() { this._validateNotDestroyed(); if (this._container) { this._container.innerHTML = ''; } this._removeInjectedCSS(); this._eventBus.off('settings:show', this._handleShowSettings.bind(this), this.name); this._setDestroyed(); } // Public API show(container) { this._validateInitialized(); this._container = container; this._render(); this._loadVoices(); this._updateBrowserInfo(); this._addDebugMessage('Settings/Debug panel opened', 'info'); } hide() { this._validateInitialized(); if (this._container) { this._container.innerHTML = ''; this._container = null; } } // Private methods _setupEventListeners() { this._eventBus.on('settings:show', this._handleShowSettings.bind(this), this.name); this._eventBus.on('router:navigate', this._handleNavigation.bind(this), this.name); this._eventBus.on('navigation:settings', this._handleNavigationSettings.bind(this), this.name); } _handleShowSettings(event) { if (event.data && event.data.container) { this.show(event.data.container); } } _handleNavigation(event) { if (event.data && event.data.path !== '/settings') { this.hide(); } } _handleNavigationSettings(event) { // Find the main container or create one let container = document.getElementById('main-content'); if (!container) { container = document.querySelector('main') || document.body; } this.show(container); } _loadTTSSettings() { try { // Load settings from TTSService const defaults = ttsService.getDefaults(); this._ttsSettings.rate = defaults.rate; this._ttsSettings.volume = defaults.volume; this._ttsSettings.pitch = defaults.pitch; // Also try to load from localStorage for voicesByLanguage const saved = localStorage.getItem('tts-settings'); if (saved) { const savedSettings = JSON.parse(saved); if (savedSettings.voicesByLanguage) { this._ttsSettings.voicesByLanguage = savedSettings.voicesByLanguage; } } } catch (e) { this._addDebugMessage(`Failed to load TTS settings: ${e.message}`, 'warning'); } } _saveTTSSettings() { try { // Apply settings to TTSService ttsService.setDefaults({ rate: this._ttsSettings.rate, volume: this._ttsSettings.volume, pitch: this._ttsSettings.pitch }); // Apply preferred voices to TTSService ttsService.setPreferredVoices(this._ttsSettings.voicesByLanguage); // Save to localStorage localStorage.setItem('tts-settings', JSON.stringify(this._ttsSettings)); this._addDebugMessage('TTS settings saved and applied globally', 'success'); } catch (e) { this._addDebugMessage(`Failed to save TTS settings: ${e.message}`, 'error'); } } _injectCSS() { if (document.getElementById('settings-debug-styles')) return; const styleSheet = document.createElement('style'); styleSheet.id = 'settings-debug-styles'; styleSheet.textContent = ` .settings-container { max-width: 1000px; margin: 0 auto; padding: 20px; display: flex; flex-direction: column; gap: 30px; } .settings-section { background: var(--card-background, #fff); border-radius: 12px; padding: 25px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); border: 1px solid var(--border-color, #e5e7eb); } .settings-section h3 { font-size: 1.4em; margin-bottom: 20px; color: var(--text-primary, #111827); display: flex; align-items: center; gap: 8px; } .setting-group { display: flex; align-items: center; justify-content: space-between; margin-bottom: 15px; padding: 12px 0; border-bottom: 1px solid #f0f0f0; } .setting-group:last-child { border-bottom: none; margin-bottom: 0; } .setting-group label { font-weight: 500; color: var(--text-primary, #111827); min-width: 140px; } .setting-group input[type="range"] { flex: 1; margin: 0 15px; accent-color: var(--primary-color, #3b82f6); } .setting-group select { min-width: 200px; padding: 8px 12px; border: 1px solid var(--border-color, #e5e7eb); border-radius: 6px; font-size: 14px; background: white; } .setting-group span { min-width: 40px; text-align: center; font-weight: 600; color: var(--primary-color, #3b82f6); } .debug-info { background: #f8f9fa; border-radius: 8px; padding: 15px; margin-bottom: 20px; } .info-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #e9ecef; } .info-item:last-child { border-bottom: none; } .info-item .label { font-weight: 500; color: var(--text-secondary, #6b7280); } .info-item .value { font-weight: 600; color: var(--text-primary, #111827); } .info-item .value.small { font-size: 0.85em; max-width: 400px; word-break: break-all; } .debug-controls { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-bottom: 20px; } .debug-btn { padding: 12px 16px; border: none; border-radius: 8px; background: var(--primary-color, #3b82f6); color: white; font-weight: 500; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; gap: 8px; } .debug-btn:hover { background: #2563eb; transform: translateY(-2px); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); } .debug-btn:active { transform: translateY(0); } .debug-output { background: #1a1a1a; border-radius: 8px; padding: 15px; color: #e0e0e0; } .debug-output h4 { color: #ffffff; margin-bottom: 10px; font-size: 1.1em; } .debug-log { min-height: 120px; max-height: 300px; overflow-y: auto; font-family: 'Courier New', monospace; font-size: 13px; line-height: 1.4; white-space: pre-wrap; word-break: break-word; background: #000000; padding: 10px; border-radius: 4px; margin-bottom: 10px; } .debug-log:empty::before { content: "No debug output yet. Click test buttons above."; color: #888; font-style: italic; } .clear-btn { padding: 6px 12px; border: 1px solid #555; border-radius: 4px; background: #333; color: #fff; font-size: 12px; cursor: pointer; transition: all 0.3s ease; } .clear-btn:hover { background: #444; border-color: #666; } .voice-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; max-height: 300px; overflow-y: auto; } .voice-item { background: #f8f9fa; border: 1px solid var(--border-color, #e5e7eb); border-radius: 8px; padding: 12px; transition: all 0.3s ease; cursor: pointer; } .voice-item:hover { background: #e9ecef; border-color: var(--primary-color, #3b82f6); } .voice-item.selected { background: var(--primary-light, #dbeafe); border-color: var(--primary-color, #3b82f6); } .voice-name { font-weight: 600; color: var(--text-primary, #111827); margin-bottom: 4px; } .voice-lang { font-size: 0.9em; color: var(--text-secondary, #6b7280); margin-bottom: 4px; } .voice-type { font-size: 0.8em; color: var(--accent-color, #f59e0b); font-weight: 500; } .debug-log .success { color: #4ade80; } .debug-log .error { color: #f87171; } .debug-log .warning { color: #fbbf24; } .debug-log .info { color: #60a5fa; } @media (max-width: 768px) { .settings-container { padding: 15px; gap: 20px; } .setting-group { flex-direction: column; align-items: flex-start; gap: 10px; } .setting-group input[type="range"] { width: 100%; margin: 10px 0; } .debug-controls { grid-template-columns: 1fr; } .voice-list { grid-template-columns: 1fr; } } `; document.head.appendChild(styleSheet); } _removeInjectedCSS() { const styleSheet = document.getElementById('settings-debug-styles'); if (styleSheet) { styleSheet.remove(); } } _render() { if (!this._container) return; this._container.innerHTML = `

Settings & Debug

๐Ÿ”ง System Information

Application Status: Running
Modules Loaded: 0
EventBus Status: Active
Current Route: /settings
Browser Support: Checking...

๐Ÿ”Š Text-to-Speech Settings (Applied globally to all games)

${this._ttsSettings.rate}
${this._ttsSettings.pitch}
${this._ttsSettings.volume}

๐ŸŽค Voice Selection by Language

๐ŸŽค Voice Information

Total Voices: 0
English Voices: 0

๐Ÿงช Debug Controls

Debug Log

๐ŸŒ Browser Information

User Agent:
Platform:
Language:
`; this._setupControlListeners(); this._updateSystemInfo(); } _setupControlListeners() { // TTS Rate slider const rateSlider = document.getElementById('tts-rate'); if (rateSlider) { rateSlider.addEventListener('input', (e) => { this._ttsSettings.rate = parseFloat(e.target.value); document.getElementById('tts-rate-value').textContent = this._ttsSettings.rate; this._saveTTSSettings(); }); } // Pitch slider const pitchSlider = document.getElementById('tts-pitch'); if (pitchSlider) { pitchSlider.addEventListener('input', (e) => { this._ttsSettings.pitch = parseFloat(e.target.value); document.getElementById('tts-pitch-value').textContent = this._ttsSettings.pitch; this._saveTTSSettings(); }); } // Volume slider const volumeSlider = document.getElementById('tts-volume'); if (volumeSlider) { volumeSlider.addEventListener('input', (e) => { this._ttsSettings.volume = parseFloat(e.target.value); document.getElementById('tts-volume-value').textContent = this._ttsSettings.volume; this._saveTTSSettings(); }); } // Voice selection const voiceSelect = document.getElementById('tts-voice'); if (voiceSelect) { voiceSelect.addEventListener('change', (e) => { this._ttsSettings.selectedVoice = e.target.value; this._saveTTSSettings(); }); } } _updateSystemInfo() { // Get application instance from global if (window.app) { const status = window.app.getStatus(); const moduleLoader = window.app.getCore()?.moduleLoader; if (status) { document.getElementById('app-status').textContent = status.status; } if (moduleLoader) { const moduleStatus = moduleLoader.getStatus(); document.getElementById('modules-count').textContent = moduleStatus?.loaded?.length || 0; } } // Current route document.getElementById('current-route').textContent = window.location.pathname || '/'; // EventBus status const eventBusStatus = this._eventBus ? 'Active' : 'Inactive'; document.getElementById('eventbus-status').textContent = eventBusStatus; } _updateBrowserInfo() { const elements = { 'user-agent': navigator.userAgent, 'platform': navigator.platform, 'browser-language': navigator.language }; Object.entries(elements).forEach(([id, value]) => { const element = document.getElementById(id); if (element) { element.textContent = value; } }); this._checkBrowserSupport(); } _checkBrowserSupport() { const checks = [ { name: 'speechSynthesis', available: 'speechSynthesis' in window }, { name: 'SpeechSynthesisUtterance', available: 'SpeechSynthesisUtterance' in window }, { name: 'getVoices', available: speechSynthesis && typeof speechSynthesis.getVoices === 'function' }, { name: 'speak', available: speechSynthesis && typeof speechSynthesis.speak === 'function' } ]; const support = checks.every(check => check.available); const supportElement = document.getElementById('browser-support'); if (supportElement) { supportElement.textContent = support ? 'โœ… Full Support' : 'โŒ Limited Support'; supportElement.style.color = support ? '#22C55E' : '#EF4444'; } this._addDebugMessage(`Browser TTS Support: ${support ? 'Full' : 'Limited'}`, support ? 'success' : 'warning'); return support; } async _loadVoices() { try { // Use TTSService to get voices this._availableVoices = await ttsService.getVoices(); this._updateVoiceInfo(); this._populateVoiceByLanguageSelectors(); this._displayVoiceList(); } catch (error) { this._addDebugMessage(`Failed to load voices: ${error.message}`, 'error'); } } _updateVoiceInfo() { const voiceCountElement = document.getElementById('voice-count'); const englishVoiceCountElement = document.getElementById('english-voice-count'); if (voiceCountElement) { voiceCountElement.textContent = this._availableVoices.length; } const englishVoices = this._availableVoices.filter(voice => voice.lang.startsWith('en')); if (englishVoiceCountElement) { englishVoiceCountElement.textContent = englishVoices.length; } } _populateVoiceByLanguageSelectors() { const container = document.getElementById('voice-language-selectors'); if (!container) return; // Group voices by language prefix const voicesByLang = {}; this._availableVoices.forEach(voice => { const langPrefix = voice.lang.split('-')[0]; // Group by prefix (e.g., 'en' for all English variants: en-US, en-GB, etc.) if (!voicesByLang[langPrefix]) { voicesByLang[langPrefix] = []; } voicesByLang[langPrefix].push(voice); }); // Only show these main languages const supportedLanguages = ['en', 'zh', 'fr', 'ja']; // Language names mapping const langNames = { 'en': 'English', 'zh': 'Chinese (ไธญๆ–‡)', 'fr': 'French (Franรงais)', 'ja': 'Japanese (ๆ—ฅๆœฌ่ชž)' }; // Create selectors for each supported language container.innerHTML = ''; const availableLangs = supportedLanguages.filter(lang => voicesByLang[lang] && voicesByLang[lang].length > 0); availableLangs.forEach(langPrefix => { const voices = voicesByLang[langPrefix]; const langName = langNames[langPrefix]; const selectorGroup = document.createElement('div'); selectorGroup.className = 'setting-group'; selectorGroup.innerHTML = ` `; const select = selectorGroup.querySelector('select'); // Populate voice options for this language voices.forEach(voice => { const option = document.createElement('option'); option.value = voice.name; const quality = voice.localService ? '๐ŸŸข' : '๐Ÿ”ต'; option.textContent = `${quality} ${voice.name} (${voice.lang})`; // Check if this voice is selected for any variant of this language const savedVoice = this._ttsSettings.voicesByLanguage[voice.lang]; if (savedVoice === voice.name) { option.selected = true; } select.appendChild(option); }); // Add change listener select.addEventListener('change', (e) => { const selectedVoiceName = e.target.value; // Store the voice for all variants of this language voices.forEach(voice => { if (selectedVoiceName === '') { delete this._ttsSettings.voicesByLanguage[voice.lang]; } else { // Set the selected voice for ALL variants of this language this._ttsSettings.voicesByLanguage[voice.lang] = selectedVoiceName; } }); this._saveTTSSettings(); this._addDebugMessage(`Voice for ${langName} set to: ${selectedVoiceName || 'Auto'}`, 'success'); }); container.appendChild(selectorGroup); }); } _displayVoiceList() { const voiceListElement = document.getElementById('voice-list'); if (!voiceListElement) return; if (this._availableVoices.length === 0) { voiceListElement.innerHTML = '
No voices available
'; return; } voiceListElement.innerHTML = ''; this._availableVoices.forEach(voice => { const voiceItem = document.createElement('div'); voiceItem.className = 'voice-item'; voiceItem.innerHTML = `
${voice.name}
${voice.lang}
${voice.localService ? 'Local' : 'Remote'}
`; voiceItem.addEventListener('click', () => { this._testVoice(voice); document.querySelectorAll('.voice-item').forEach(item => item.classList.remove('selected')); voiceItem.classList.add('selected'); }); voiceListElement.appendChild(voiceItem); }); } _testVoice(voice) { try { const utterance = new SpeechSynthesisUtterance('Hello, this is a voice test'); utterance.voice = voice; utterance.rate = this._ttsSettings.rate; utterance.volume = this._ttsSettings.volume; utterance.onstart = () => { this._addDebugMessage(`Testing voice: ${voice.name}`, 'info'); }; utterance.onerror = (event) => { this._addDebugMessage(`Voice test error: ${event.error}`, 'error'); }; speechSynthesis.speak(utterance); } catch (error) { this._addDebugMessage(`Voice test failed: ${error.message}`, 'error'); } } _addDebugMessage(message, type = 'info') { const timestamp = new Date().toLocaleTimeString(); const logEntry = `[${timestamp}] ${message}`; this._debugMessages.push({ message: logEntry, type }); this._updateDebugDisplay(); console.log(`[Settings] ${logEntry}`); } _updateDebugDisplay() { const debugLogElement = document.getElementById('debug-log'); if (!debugLogElement) return; const lastEntries = this._debugMessages.slice(-50); debugLogElement.innerHTML = lastEntries .map(entry => `${entry.message}`) .join('\n'); debugLogElement.scrollTop = debugLogElement.scrollHeight; } // Public test methods (exposed via window.settingsDebug) testBasicTTS() { this._addDebugMessage('Testing basic TTS...', 'info'); this._speak('Hello world, this is a basic test') .then(() => this._addDebugMessage('โœ… Basic TTS test completed', 'success')) .catch(error => this._addDebugMessage(`โŒ Basic TTS test failed: ${error.message}`, 'error')); } testGameWords() { this._addDebugMessage('Testing game vocabulary words...', 'info'); const words = ['apple', 'cat', 'house', 'car', 'tree', 'book', 'sun', 'dog']; let index = 0; const speakNext = () => { if (index >= words.length) { this._addDebugMessage('โœ… Game words test completed', 'success'); return; } const word = words[index++]; this._speak(word) .then(() => { this._addDebugMessage(`โœ… Spoke: ${word}`, 'info'); setTimeout(speakNext, 500); }) .catch(error => { this._addDebugMessage(`โŒ Failed to speak ${word}: ${error.message}`, 'error'); setTimeout(speakNext, 500); }); }; speakNext(); } refreshVoices() { this._addDebugMessage('Refreshing voice list...', 'info'); this._loadVoices(); this._addDebugMessage('โœ… Voice list refreshed', 'success'); } testSystem() { this._addDebugMessage('Testing system components...', 'info'); // Test EventBus if (this._eventBus) { this._addDebugMessage('โœ… EventBus: Active', 'success'); } else { this._addDebugMessage('โŒ EventBus: Not found', 'error'); } // Test Router if (this._router) { this._addDebugMessage('โœ… Router: Active', 'success'); } else { this._addDebugMessage('โŒ Router: Not found', 'error'); } // Test Application if (window.app) { this._addDebugMessage('โœ… Application: Running', 'success'); } else { this._addDebugMessage('โŒ Application: Not found', 'error'); } this._addDebugMessage('System test completed', 'info'); this._updateSystemInfo(); } clearDebugLog() { this._debugMessages = []; this._updateDebugDisplay(); this._addDebugMessage('Debug log cleared', 'info'); } testCurrentSettings() { this._addDebugMessage(`Testing TTS with current settings (rate: ${this._ttsSettings.rate}, pitch: ${this._ttsSettings.pitch}, volume: ${this._ttsSettings.volume})...`, 'info'); const testText = 'Hello! This is a test of your current text-to-speech settings. You can adjust the rate, pitch, and volume to your preference.'; this._speak(testText) .then(() => this._addDebugMessage('โœ… TTS test completed successfully', 'success')) .catch(error => this._addDebugMessage(`โŒ TTS test failed: ${error.message}`, 'error')); } testVoiceForLanguage(langPrefix) { this._addDebugMessage(`Testing voice for language: ${langPrefix}...`, 'info'); // Get test phrases for supported languages const testPhrases = { 'en': 'Hello! This is a test of the English voice.', 'zh': 'ไฝ ๅฅฝ๏ผ่ฟ™ๆ˜ฏไธญๆ–‡่ฏญ้Ÿณ็š„ๆต‹่ฏ•ใ€‚', 'fr': 'Bonjour ! Ceci est un test de la voix franรงaise.', 'ja': 'ใ“ใ‚“ใซใกใฏ๏ผใ“ใ‚Œใฏๆ—ฅๆœฌ่ชž้Ÿณๅฃฐใฎใƒ†ใ‚นใƒˆใงใ™ใ€‚' }; const testPhrase = testPhrases[langPrefix] || 'Hello, this is a voice test.'; const language = langPrefix + '-' + langPrefix.toUpperCase(); // e.g., 'en-EN' this._speak(testPhrase, { lang: language }) .then(() => this._addDebugMessage(`โœ… ${langPrefix} voice test completed`, 'success')) .catch(error => this._addDebugMessage(`โŒ ${langPrefix} voice test failed: ${error.message}`, 'error')); } resetToDefaults() { this._addDebugMessage('Resetting TTS settings to defaults...', 'info'); // Reset to default values this._ttsSettings.rate = 0.85; this._ttsSettings.pitch = 1.0; this._ttsSettings.volume = 1.0; this._ttsSettings.voicesByLanguage = {}; // Update UI const rateSlider = document.getElementById('tts-rate'); const pitchSlider = document.getElementById('tts-pitch'); const volumeSlider = document.getElementById('tts-volume'); if (rateSlider) { rateSlider.value = this._ttsSettings.rate; document.getElementById('tts-rate-value').textContent = this._ttsSettings.rate; } if (pitchSlider) { pitchSlider.value = this._ttsSettings.pitch; document.getElementById('tts-pitch-value').textContent = this._ttsSettings.pitch; } if (volumeSlider) { volumeSlider.value = this._ttsSettings.volume; document.getElementById('tts-volume-value').textContent = this._ttsSettings.volume; } // Reset all language voice selectors document.querySelectorAll('.voice-lang-select').forEach(select => { select.value = ''; }); // Save settings this._saveTTSSettings(); this._addDebugMessage('โœ… Settings reset to defaults (rate: 0.85, pitch: 1.0, volume: 1.0, all voices: Auto)', 'success'); } _exposePublicAPI() { // Expose API for debug buttons to use window.settingsDebug = { testBasicTTS: () => this.testBasicTTS(), testGameWords: () => this.testGameWords(), refreshVoices: () => this.refreshVoices(), testSystem: () => this.testSystem(), clearDebugLog: () => this.clearDebugLog(), testCurrentSettings: () => this.testCurrentSettings(), resetToDefaults: () => this.resetToDefaults(), testVoiceForLanguage: (langPrefix) => this.testVoiceForLanguage(langPrefix) }; } async _speak(text, options = {}) { try { const language = options.lang || 'en-US'; const ttsOptions = { rate: options.rate || this._ttsSettings.rate, volume: options.volume || this._ttsSettings.volume, pitch: this._ttsSettings.pitch }; await ttsService.speak(text, language, ttsOptions); } catch (error) { throw error; } } } export default SettingsDebug;