Major Changes: - Moved legacy system to Legacy/ folder for archival - Built new modular architecture with strict separation of concerns - Created core system: Module, EventBus, ModuleLoader, Router - Added Application bootstrap with auto-start functionality - Implemented development server with ES6 modules support - Created comprehensive documentation and project context - Converted SBS-7-8 content to JSON format - Copied all legacy games and content to new structure New Architecture Features: - Sealed modules with WeakMap private data - Strict dependency injection system - Event-driven communication only - Inviolable responsibility patterns - Auto-initialization without commands - Component-based UI foundation ready Technical Stack: - Vanilla JS/HTML/CSS only - ES6 modules with proper imports/exports - HTTP development server (no file:// protocol) - Modular CSS with component scoping - Comprehensive error handling and debugging Ready for Phase 2: Converting legacy modules to new architecture 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
397 lines
14 KiB
JavaScript
397 lines
14 KiB
JavaScript
// === SETTINGS MANAGER FOR TTS AND DEBUG ===
|
|
|
|
const SettingsManager = {
|
|
// TTS Settings
|
|
ttsSettings: {
|
|
rate: 0.8,
|
|
volume: 1.0,
|
|
selectedVoice: null
|
|
},
|
|
|
|
availableVoices: [],
|
|
debugMessages: [],
|
|
|
|
init() {
|
|
this.loadSettings();
|
|
this.initTTSSettings();
|
|
this.setupEventListeners();
|
|
this.checkBrowserSupport();
|
|
this.loadVoices();
|
|
this.updateBrowserInfo();
|
|
},
|
|
|
|
// === SETTINGS PERSISTENCE ===
|
|
loadSettings() {
|
|
const saved = localStorage.getItem('tts-settings');
|
|
if (saved) {
|
|
try {
|
|
this.ttsSettings = { ...this.ttsSettings, ...JSON.parse(saved) };
|
|
} catch (e) {
|
|
console.warn('Failed to load TTS settings:', e);
|
|
}
|
|
}
|
|
},
|
|
|
|
saveSettings() {
|
|
localStorage.setItem('tts-settings', JSON.stringify(this.ttsSettings));
|
|
},
|
|
|
|
// === TTS SETTINGS INITIALIZATION ===
|
|
initTTSSettings() {
|
|
const rateSlider = document.getElementById('tts-rate');
|
|
const volumeSlider = document.getElementById('tts-volume');
|
|
const voiceSelect = document.getElementById('tts-voice');
|
|
|
|
if (rateSlider) {
|
|
rateSlider.value = this.ttsSettings.rate;
|
|
document.getElementById('tts-rate-value').textContent = this.ttsSettings.rate;
|
|
}
|
|
|
|
if (volumeSlider) {
|
|
volumeSlider.value = this.ttsSettings.volume;
|
|
document.getElementById('tts-volume-value').textContent = this.ttsSettings.volume;
|
|
}
|
|
},
|
|
|
|
setupEventListeners() {
|
|
// 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.saveSettings();
|
|
});
|
|
}
|
|
|
|
// 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.saveSettings();
|
|
});
|
|
}
|
|
|
|
// Voice selection
|
|
const voiceSelect = document.getElementById('tts-voice');
|
|
if (voiceSelect) {
|
|
voiceSelect.addEventListener('change', (e) => {
|
|
this.ttsSettings.selectedVoice = e.target.value;
|
|
this.saveSettings();
|
|
});
|
|
}
|
|
},
|
|
|
|
// === BROWSER SUPPORT CHECK ===
|
|
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';
|
|
}
|
|
|
|
return support;
|
|
},
|
|
|
|
// === VOICE MANAGEMENT ===
|
|
loadVoices() {
|
|
const loadVoicesImpl = () => {
|
|
this.availableVoices = speechSynthesis.getVoices();
|
|
this.updateVoiceInfo();
|
|
this.populateVoiceSelect();
|
|
this.displayVoiceList();
|
|
};
|
|
|
|
// Try immediately
|
|
loadVoicesImpl();
|
|
|
|
// Also try after a delay (some browsers load voices asynchronously)
|
|
setTimeout(loadVoicesImpl, 100);
|
|
|
|
// Listen for voice changes
|
|
if (speechSynthesis.onvoiceschanged !== undefined) {
|
|
speechSynthesis.onvoiceschanged = loadVoicesImpl;
|
|
}
|
|
},
|
|
|
|
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;
|
|
}
|
|
},
|
|
|
|
populateVoiceSelect() {
|
|
const voiceSelect = document.getElementById('tts-voice');
|
|
if (!voiceSelect) return;
|
|
|
|
// Clear existing options except the first one
|
|
voiceSelect.innerHTML = '<option value="">Auto (System Default)</option>';
|
|
|
|
const englishVoices = this.availableVoices.filter(voice => voice.lang.startsWith('en'));
|
|
|
|
englishVoices.forEach(voice => {
|
|
const option = document.createElement('option');
|
|
option.value = voice.name;
|
|
option.textContent = `${voice.name} (${voice.lang})`;
|
|
if (voice.name === this.ttsSettings.selectedVoice) {
|
|
option.selected = true;
|
|
}
|
|
voiceSelect.appendChild(option);
|
|
});
|
|
},
|
|
|
|
displayVoiceList() {
|
|
const voiceListElement = document.getElementById('voice-list');
|
|
if (!voiceListElement) return;
|
|
|
|
if (this.availableVoices.length === 0) {
|
|
voiceListElement.innerHTML = '<div style="text-align: center; color: #666;">No voices available</div>';
|
|
return;
|
|
}
|
|
|
|
voiceListElement.innerHTML = '';
|
|
this.availableVoices.forEach(voice => {
|
|
const voiceItem = document.createElement('div');
|
|
voiceItem.className = 'voice-item';
|
|
voiceItem.innerHTML = `
|
|
<div class="voice-name">${voice.name}</div>
|
|
<div class="voice-lang">${voice.lang}</div>
|
|
<div class="voice-type">${voice.localService ? 'Local' : 'Remote'}</div>
|
|
`;
|
|
|
|
voiceItem.addEventListener('click', () => {
|
|
// Test this voice
|
|
this.testVoice(voice);
|
|
|
|
// Update selection visually
|
|
document.querySelectorAll('.voice-item').forEach(item => item.classList.remove('selected'));
|
|
voiceItem.classList.add('selected');
|
|
});
|
|
|
|
voiceListElement.appendChild(voiceItem);
|
|
});
|
|
},
|
|
|
|
// === TTS TESTING FUNCTIONS ===
|
|
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');
|
|
}
|
|
},
|
|
|
|
// === DEBUG FUNCTIONS ===
|
|
addDebugMessage(message, type = 'info') {
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
const logEntry = `[${timestamp}] ${message}`;
|
|
|
|
this.debugMessages.push({ message: logEntry, type });
|
|
this.updateDebugDisplay();
|
|
|
|
// Also log to console
|
|
console.log(`[Settings] ${logEntry}`);
|
|
},
|
|
|
|
updateDebugDisplay() {
|
|
const debugLogElement = document.getElementById('debug-log');
|
|
if (!debugLogElement) return;
|
|
|
|
const lastEntries = this.debugMessages.slice(-50); // Keep last 50 entries
|
|
debugLogElement.innerHTML = lastEntries
|
|
.map(entry => `<span class="${entry.type}">${entry.message}</span>`)
|
|
.join('\n');
|
|
|
|
// Scroll to bottom
|
|
debugLogElement.scrollTop = debugLogElement.scrollHeight;
|
|
},
|
|
|
|
clearDebugLog() {
|
|
this.debugMessages = [];
|
|
this.updateDebugDisplay();
|
|
},
|
|
|
|
// === LANGUAGE DETECTION ===
|
|
detectContentLanguage() {
|
|
// Try to get language from current content in Word Discovery or other games
|
|
if (window.currentGameContent && window.currentGameContent.language) {
|
|
this.addDebugMessage(`Auto-detected language: ${window.currentGameContent.language}`, 'info');
|
|
return window.currentGameContent.language;
|
|
}
|
|
|
|
// Fallback: try to detect from navigation context
|
|
if (window.AppNavigation && window.AppNavigation.scannedContent) {
|
|
const currentContentKey = window.AppNavigation.selectedContent;
|
|
if (currentContentKey) {
|
|
const contentInfo = window.AppNavigation.scannedContent.found.find(
|
|
content => content.id === currentContentKey
|
|
);
|
|
if (contentInfo && contentInfo.language) {
|
|
this.addDebugMessage(`Language from navigation: ${contentInfo.language}`, 'info');
|
|
return contentInfo.language;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.addDebugMessage('No content language detected, using en-US default', 'warning');
|
|
return 'en-US';
|
|
},
|
|
|
|
// === BROWSER INFO ===
|
|
updateBrowserInfo() {
|
|
const userAgentElement = document.getElementById('user-agent');
|
|
const platformElement = document.getElementById('platform');
|
|
const languageElement = document.getElementById('browser-language');
|
|
|
|
if (userAgentElement) {
|
|
userAgentElement.textContent = navigator.userAgent;
|
|
}
|
|
|
|
if (platformElement) {
|
|
platformElement.textContent = navigator.platform;
|
|
}
|
|
|
|
if (languageElement) {
|
|
languageElement.textContent = navigator.language;
|
|
}
|
|
},
|
|
|
|
// === PUBLIC TTS API ===
|
|
speak(text, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
if (!('speechSynthesis' in window)) {
|
|
reject(new Error('Speech synthesis not supported'));
|
|
return;
|
|
}
|
|
|
|
const utterance = new SpeechSynthesisUtterance(text);
|
|
|
|
// Apply settings
|
|
utterance.rate = options.rate || this.ttsSettings.rate;
|
|
utterance.volume = options.volume || this.ttsSettings.volume;
|
|
|
|
// Auto-detect language from content if available
|
|
const autoLang = this.detectContentLanguage() || 'en-US';
|
|
utterance.lang = options.lang || autoLang;
|
|
|
|
// Apply selected voice if available
|
|
if (this.ttsSettings.selectedVoice) {
|
|
const selectedVoice = this.availableVoices.find(
|
|
voice => voice.name === this.ttsSettings.selectedVoice
|
|
);
|
|
if (selectedVoice) {
|
|
utterance.voice = selectedVoice;
|
|
}
|
|
}
|
|
|
|
utterance.onend = () => {
|
|
this.addDebugMessage(`Spoke: "${text}"`, 'success');
|
|
resolve();
|
|
};
|
|
|
|
utterance.onerror = (event) => {
|
|
this.addDebugMessage(`Speech error: ${event.error}`, 'error');
|
|
reject(new Error(event.error));
|
|
};
|
|
|
|
speechSynthesis.speak(utterance);
|
|
} catch (error) {
|
|
this.addDebugMessage(`Speech failed: ${error.message}`, 'error');
|
|
reject(error);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// === GLOBAL TTS TEST FUNCTIONS ===
|
|
function testBasicTTS() {
|
|
SettingsManager.addDebugMessage('Testing basic TTS...', 'info');
|
|
SettingsManager.speak('Hello world, this is a basic test')
|
|
.then(() => SettingsManager.addDebugMessage('✅ Basic TTS test completed', 'success'))
|
|
.catch(error => SettingsManager.addDebugMessage(`❌ Basic TTS test failed: ${error.message}`, 'error'));
|
|
}
|
|
|
|
function testWithCallbacks() {
|
|
SettingsManager.addDebugMessage('Testing TTS with detailed callbacks...', 'info');
|
|
SettingsManager.speak('Apple, cat, house, car')
|
|
.then(() => SettingsManager.addDebugMessage('✅ Callback TTS test completed', 'success'))
|
|
.catch(error => SettingsManager.addDebugMessage(`❌ Callback TTS test failed: ${error.message}`, 'error'));
|
|
}
|
|
|
|
function testGameWords() {
|
|
SettingsManager.addDebugMessage('Testing game vocabulary words...', 'info');
|
|
const words = ['apple', 'cat', 'house', 'car', 'tree', 'book', 'sun', 'dog'];
|
|
|
|
let index = 0;
|
|
function speakNext() {
|
|
if (index >= words.length) {
|
|
SettingsManager.addDebugMessage('✅ Game words test completed', 'success');
|
|
return;
|
|
}
|
|
|
|
const word = words[index++];
|
|
SettingsManager.speak(word)
|
|
.then(() => {
|
|
SettingsManager.addDebugMessage(`✅ Spoke: ${word}`, 'info');
|
|
setTimeout(speakNext, 500);
|
|
})
|
|
.catch(error => {
|
|
SettingsManager.addDebugMessage(`❌ Failed to speak ${word}: ${error.message}`, 'error');
|
|
setTimeout(speakNext, 500);
|
|
});
|
|
}
|
|
|
|
speakNext();
|
|
}
|
|
|
|
function refreshVoices() {
|
|
SettingsManager.addDebugMessage('Refreshing voice list...', 'info');
|
|
SettingsManager.loadVoices();
|
|
SettingsManager.addDebugMessage('✅ Voice list refreshed', 'success');
|
|
}
|
|
|
|
function clearDebugLog() {
|
|
SettingsManager.clearDebugLog();
|
|
}
|
|
|
|
// Initialize when page loads
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', () => SettingsManager.init());
|
|
} else {
|
|
SettingsManager.init();
|
|
}
|
|
|
|
// Export for use in games
|
|
window.SettingsManager = SettingsManager; |