Add Word Discovery game with auto-play TTS and Settings system

- New Word Discovery game with image support and practice phases
- Auto-play TTS on word appearance with speed control (0.7x-1.1x)
- Complete Settings page with TTS controls and debug interface
- Language standardization with BCP 47 codes (en-US, zh-CN, fr-FR)
- Media fallback handling for missing images and audio
- Settings Manager with voice selection and debug tools

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
StillHammer 2025-09-19 14:39:08 +08:00
parent cb0c3b01f8
commit 475006e912
15 changed files with 2033 additions and 4 deletions

314
css/settings.css Normal file
View File

@ -0,0 +1,314 @@
/* === SETTINGS PAGE STYLES === */
.settings-container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 30px;
}
.settings-section {
background: var(--card-background);
border-radius: var(--border-radius);
padding: 25px;
box-shadow: var(--shadow);
border: 1px solid var(--border-color);
}
.settings-section h3 {
font-size: 1.4em;
margin-bottom: 20px;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
/* === SETTING GROUPS === */
.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);
min-width: 140px;
}
.setting-group input[type="range"] {
flex: 1;
margin: 0 15px;
accent-color: var(--primary-color);
}
.setting-group select {
min-width: 200px;
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 14px;
background: white;
}
.setting-group span {
min-width: 40px;
text-align: center;
font-weight: 600;
color: var(--primary-color);
}
/* === DEBUG INFO === */
.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);
}
.info-item .value {
font-weight: 600;
color: var(--text-primary);
}
.info-item .value.small {
font-size: 0.85em;
max-width: 400px;
word-break: break-all;
}
/* === DEBUG CONTROLS === */
.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);
color: white;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.debug-btn:hover {
background: #2563eb;
transform: translateY(-2px);
box-shadow: var(--shadow);
}
.debug-btn:active {
transform: translateY(0);
}
/* === DEBUG OUTPUT === */
.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: var(--transition);
}
.clear-btn:hover {
background: #444;
border-color: #666;
}
/* === VOICE LIST === */
.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);
border-radius: 8px;
padding: 12px;
transition: var(--transition);
cursor: pointer;
}
.voice-item:hover {
background: #e9ecef;
border-color: var(--primary-color);
}
.voice-item.selected {
background: var(--primary-light);
border-color: var(--primary-color);
}
.voice-name {
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
}
.voice-lang {
font-size: 0.9em;
color: var(--text-secondary);
margin-bottom: 4px;
}
.voice-type {
font-size: 0.8em;
color: var(--accent-color);
font-weight: 500;
}
/* === BROWSER INFO === */
.browser-info .info-item {
align-items: flex-start;
}
.browser-info .value {
text-align: right;
max-width: 60%;
}
/* === LOG COLORS === */
.debug-log .success {
color: #4ade80;
}
.debug-log .error {
color: #f87171;
}
.debug-log .warning {
color: #fbbf24;
}
.debug-log .info {
color: #60a5fa;
}
.debug-log .timestamp {
color: #9ca3af;
font-size: 0.9em;
}
/* === RESPONSIVE === */
@media (max-width: 768px) {
.settings-container {
padding: 15px;
gap: 20px;
}
.settings-section {
padding: 20px;
}
.setting-group {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.setting-group label {
min-width: auto;
}
.setting-group input[type="range"] {
width: 100%;
margin: 10px 0;
}
.setting-group select {
width: 100%;
min-width: auto;
}
.debug-controls {
grid-template-columns: 1fr;
}
.info-item {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.voice-list {
grid-template-columns: 1fr;
}
}

View File

@ -7,6 +7,7 @@
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/navigation.css">
<link rel="stylesheet" href="css/games.css">
<link rel="stylesheet" href="css/settings.css">
</head>
<body>
<!-- Top Bar with Network Status -->
@ -45,9 +46,9 @@
📊 <span>Statistics</span>
<small>Coming soon</small>
</button>
<button class="option-card secondary" onclick="showComingSoon()">
<button class="option-card secondary" onclick="navigateTo('settings')">
⚙️ <span>Settings</span>
<small>Coming soon</small>
<small>Audio & Debug Tools</small>
</button>
<button class="option-card primary" onclick="showContentCreator()">
🏭 <span>Content Creator</span>
@ -91,12 +92,106 @@
Score: <span id="current-score">0</span>
</div>
</div>
<div class="game-container" id="game-container">
<!-- The game will be loaded here dynamically -->
</div>
</div>
<!-- Settings Page -->
<div class="page" id="settings-page">
<div class="page-header">
<button class="back-btn" onclick="goBack()">← Back</button>
<h2>⚙️ Settings & Debug Tools</h2>
</div>
<div class="settings-container">
<!-- Audio Settings Section -->
<div class="settings-section">
<h3>🔊 Audio Settings</h3>
<div class="setting-group">
<label>TTS Voice Speed:</label>
<input type="range" id="tts-rate" min="0.5" max="2" step="0.1" value="0.8">
<span id="tts-rate-value">0.8</span>
</div>
<div class="setting-group">
<label>TTS Volume:</label>
<input type="range" id="tts-volume" min="0" max="1" step="0.1" value="1">
<span id="tts-volume-value">1.0</span>
</div>
<div class="setting-group">
<label>Preferred Voice:</label>
<select id="tts-voice">
<option value="">Auto (System Default)</option>
</select>
</div>
</div>
<!-- TTS Debug Section -->
<div class="settings-section">
<h3>🔧 TTS Debug Tools</h3>
<div class="debug-info" id="tts-status">
<div class="info-item">
<span class="label">Browser Support:</span>
<span class="value" id="browser-support">Checking...</span>
</div>
<div class="info-item">
<span class="label">Available Voices:</span>
<span class="value" id="voice-count">Loading...</span>
</div>
<div class="info-item">
<span class="label">English Voices:</span>
<span class="value" id="english-voice-count">Loading...</span>
</div>
</div>
<div class="debug-controls">
<button class="debug-btn" onclick="testBasicTTS()">🔊 Test Basic TTS</button>
<button class="debug-btn" onclick="testWithCallbacks()">📞 Test with Callbacks</button>
<button class="debug-btn" onclick="testGameWords()">🎮 Test Game Words</button>
<button class="debug-btn" onclick="refreshVoices()">🔄 Refresh Voices</button>
</div>
<div class="debug-output" id="debug-output">
<h4>Debug Output:</h4>
<div class="debug-log" id="debug-log"></div>
<button class="clear-btn" onclick="clearDebugLog()">Clear Log</button>
</div>
</div>
<!-- Voice List Section -->
<div class="settings-section">
<h3>🎤 Available Voices</h3>
<div class="voice-list" id="voice-list">
Loading voices...
</div>
</div>
<!-- Browser Info Section -->
<div class="settings-section">
<h3>🌐 Browser Information</h3>
<div class="browser-info">
<div class="info-item">
<span class="label">User Agent:</span>
<span class="value small" id="user-agent"></span>
</div>
<div class="info-item">
<span class="label">Platform:</span>
<span class="value" id="platform"></span>
</div>
<div class="info-item">
<span class="label">Language:</span>
<span class="value" id="browser-language"></span>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Modal Coming Soon -->
@ -126,6 +221,7 @@
<script src="js/core/json-content-loader.js"></script>
<script src="js/core/content-game-compatibility.js"></script>
<script src="js/tools/content-creator.js"></script>
<script src="js/core/settings-manager.js"></script>
<script src="js/core/navigation.js"></script>
<script src="js/core/game-loader.js"></script>
<script>

View File

@ -7,6 +7,7 @@ window.ContentModules.ChineseLongStory = {
name: "The Dragon's Pearl - 龙珠传说",
description: "Long story with translation and pronunciation",
difficulty: "intermediate",
language: "zh-CN",
totalWords: 1200,
vocabulary: {

View File

@ -0,0 +1,59 @@
// Example content module with minimal data (no images, some missing pronunciation)
window.ContentModules = window.ContentModules || {};
window.ContentModules.ExampleMinimal = {
name: "Minimal Vocabulary Test",
description: "Test content with missing images and audio",
difficulty: "easy",
language: "en-US",
// Vocabulary with mixed availability of features
vocabulary: {
"hello": {
translation: "bonjour",
pronunciation: "həˈloʊ",
type: "greeting"
// No image
},
"goodbye": {
translation: "au revoir",
type: "greeting"
// No image, no pronunciation
},
"water": {
translation: "eau",
pronunciation: "ˈːtər",
type: "noun"
// No image
},
"food": {
translation: "nourriture",
type: "noun"
// No image, no pronunciation
},
"happy": {
translation: "heureux",
pronunciation: "ˈhæpi",
type: "adjective"
// No image
},
"sad": {
translation: "triste",
type: "adjective"
// No image, no pronunciation
}
},
// Backward compatibility for other games
sentences: [
{
english: "Hello, how are you?",
chinese: "Bonjour, comment allez-vous?",
prononciation: "həˈloʊ haʊ ɑr ju"
},
{
english: "I need some water",
chinese: "J'ai besoin d'eau",
prononciation: "aɪ nid sʌm ˈːtər"
}
]
};

View File

@ -0,0 +1,81 @@
// Example content module with image support for Word Discovery game
window.ContentModules = window.ContentModules || {};
window.ContentModules.ExampleWithImages = {
name: "Basic Vocabulary with Images",
description: "Simple English words with visual support for beginners",
difficulty: "easy",
language: "en-US",
// Vocabulary with image support
vocabulary: {
"apple": {
translation: "pomme",
pronunciation: "æpəl",
type: "noun",
image: "assets/images/vocabulary/apple.png",
audioFile: "assets/audio/vocabulary/apple.mp3"
},
"cat": {
translation: "chat",
pronunciation: "kæt",
type: "noun",
image: "assets/images/vocabulary/cat.png",
audioFile: "assets/audio/vocabulary/cat_broken.mp3" // Broken path to test fallback
},
"house": {
translation: "maison",
pronunciation: "haʊs",
type: "noun",
image: "assets/images/vocabulary/house.png"
},
"car": {
translation: "voiture",
pronunciation: "kɑr",
type: "noun",
image: "assets/images/vocabulary/car.png"
},
"tree": {
translation: "arbre",
pronunciation: "tri",
type: "noun",
image: "assets/images/vocabulary/tree.png"
},
"book": {
translation: "livre",
pronunciation: "bʊk",
type: "noun",
image: "assets/images/vocabulary/book.png"
},
"sun": {
translation: "soleil",
pronunciation: "sʌn",
type: "noun",
image: "assets/images/vocabulary/sun.png"
},
"dog": {
translation: "chien",
pronunciation: "dɔg",
type: "noun",
image: "assets/images/vocabulary/dog.png"
}
},
// Backward compatibility for other games
sentences: [
{
english: "The apple is red",
chinese: "La pomme est rouge",
prononciation: "ðə æpəl ɪz red"
},
{
english: "The cat is sleeping",
chinese: "Le chat dort",
prononciation: "ðə kæt ɪz slipɪŋ"
},
{
english: "I live in a house",
chinese: "J'habite dans une maison",
prononciation: "aɪ lɪv ɪn ə haʊs"
}
]
};

View File

@ -6,6 +6,7 @@ window.ContentModules.SBSLevel78New = {
name: "SBS Level 7-8 New",
description: "Side by Side Level 7-8 vocabulary with language-agnostic format",
difficulty: "intermediate",
language: "en-US",
vocabulary: {
// Housing and Places

View File

@ -7,6 +7,7 @@ window.ContentModules.StoryPrototypeOptimized = {
name: "The Magical Library (Optimized)",
description: "Adventure story with centralized vocabulary system",
difficulty: "intermediate",
language: "en-US",
// Centralized vocabulary - defined once, used everywhere
vocabulary: {

View File

@ -8,6 +8,7 @@ window.ContentModules.TestMinimalContent = {
name: "Test Minimal (2 mots)",
description: "Contenu minimal pour tester la compatibilité",
difficulty: "easy",
language: "en-US",
vocabulary: {
"hello": "bonjour",

View File

@ -7,6 +7,7 @@ window.ContentModules.TestMinimal = {
name: "Test Minimal (2 mots)",
description: "Contenu minimal pour tester la compatibilité - seulement 2 mots",
difficulty: "easy",
language: "en-US",
vocabulary: {
"hello": "bonjour",

View File

@ -7,6 +7,7 @@ window.ContentModules.TestRich = {
name: "Test Riche (complet)",
description: "Contenu riche pour tester la compatibilité maximale",
difficulty: "medium",
language: "en-US",
vocabulary: {
"apple": {

View File

@ -330,6 +330,7 @@ class ContentScanner {
'english_exemple.json', // Local JSON basic
'english_exemple_fixed.json', // Local JSON modular
'english_exemple_ultra_commented.json', // Local JSON ultra-modular
'example-with-images.js', // Local JS with image support for Word Discovery
// AJOUT: Fichiers générés par le système de conversion
'sbs-level-7-8-GENERATED-from-js.json',
'english-exemple-commented-GENERATED.json'

View File

@ -322,7 +322,8 @@ const GameLoader = {
'adventure-reader': 'AdventureReader',
'chinese-study': 'ChineseStudy',
'story-reader': 'StoryReader',
'word-storm': 'WordStorm'
'word-storm': 'WordStorm',
'word-discovery': 'WordDiscovery'
};
return names[gameType] || gameType;
},

View File

@ -97,6 +97,12 @@ const AppNavigation = {
name: 'Word Storm',
icon: '🌪️',
description: 'Catch falling words before they hit the ground!'
},
'word-discovery': {
enabled: true,
name: 'Word Discovery',
icon: '🔍',
description: 'Learn new words with images and interactive practice!'
}
},
content: {
@ -259,6 +265,9 @@ const AppNavigation = {
case 'play':
this.showGamePage(game, content);
break;
case 'settings':
this.showSettingsPage();
break;
default:
this.showHomePage();
}
@ -302,6 +311,22 @@ const AppNavigation = {
this.updateBreadcrumb();
},
showSettingsPage() {
logSh('⚙️ Displaying settings page', 'INFO');
this.hideAllPages();
document.getElementById('settings-page').classList.add('active');
this.currentPage = 'settings';
this.updateBreadcrumb();
// Initialize settings if SettingsManager is available
if (window.SettingsManager) {
// Ensure SettingsManager is initialized for this page
setTimeout(() => {
window.SettingsManager.init();
}, 100);
}
},
// Display games selection page
async showGamesPage(contentType) {
logSh(`🎮 Displaying games selection page for content: ${contentType}`, 'INFO');

397
js/core/settings-manager.js Normal file
View File

@ -0,0 +1,397 @@
// === 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;

1049
js/games/word-discovery.js Normal file

File diff suppressed because it is too large Load Diff