diff --git a/src/games/FlashcardLearning.js b/src/games/FlashcardLearning.js index 305a502..36320fd 100644 --- a/src/games/FlashcardLearning.js +++ b/src/games/FlashcardLearning.js @@ -1364,19 +1364,22 @@ class FlashcardLearning extends Module { "> ${this._currentCard.displayFront} -
+ cursor: pointer; + transition: all 0.2s ease; + " title="Click to hear pronunciation"> ${this._currentCard.displayBack}
-
${this._currentCard.pronunciation || ''}
@@ -1388,6 +1391,23 @@ class FlashcardLearning extends Module { this._isRevealed = true; + // Add click listener on answer for TTS + const answerTTS = document.getElementById('answer-tts'); + if (answerTTS) { + answerTTS.addEventListener('click', () => { + this._playAudio(this._currentCard.front); + this._highlightPronunciation(); + }); + answerTTS.addEventListener('mouseenter', () => { + answerTTS.style.transform = 'scale(1.05)'; + answerTTS.style.color = '#6dd5fa'; + }); + answerTTS.addEventListener('mouseleave', () => { + answerTTS.style.transform = 'scale(1)'; + answerTTS.style.color = 'white'; + }); + } + // Add difficulty buttons in game-controls section const gameControls = document.querySelector('.game-controls'); if (gameControls) { @@ -1512,6 +1532,7 @@ class FlashcardLearning extends Module { // Always play audio for pronunciation, regardless of mode setTimeout(() => { this._playAudio(this._currentCard.front); + this._highlightPronunciation(); }, 200); }, 150); } @@ -1712,24 +1733,58 @@ class FlashcardLearning extends Module { speechSynthesis.cancel(); const utterance = new SpeechSynthesisUtterance(text); + + // Get language from chapter content, fallback to en-US + const chapterLanguage = this._content?.language || 'en-US'; + utterance.lang = chapterLanguage; utterance.rate = 0.8; utterance.pitch = 1.0; utterance.volume = 1.0; - // Try to use a good English voice + // Try to find a suitable voice for the language const voices = speechSynthesis.getVoices(); - const englishVoice = voices.find(voice => - voice.lang.startsWith('en') && voice.name.includes('Neural') - ) || voices.find(voice => voice.lang.startsWith('en')); + if (voices.length > 0) { + // Find voice matching the chapter language + const langPrefix = chapterLanguage.split('-')[0]; // e.g., "zh" from "zh-CN" + const matchingVoice = voices.find(voice => + voice.lang.startsWith(langPrefix) && (voice.name.includes('Neural') || voice.default) + ) || voices.find(voice => voice.lang.startsWith(langPrefix)); - if (englishVoice) { - utterance.voice = englishVoice; + if (matchingVoice) { + utterance.voice = matchingVoice; + console.log('🔊 Using voice:', matchingVoice.name, matchingVoice.lang); + } } speechSynthesis.speak(utterance); } } + _highlightPronunciation() { + // Highlight pronunciation when TTS is played + const pronunciation = document.getElementById('pronunciation-display'); + + if (pronunciation) { + // Store original styles + const originalColor = pronunciation.style.color; + const originalFontSize = pronunciation.style.fontSize; + + // Add highlight + pronunciation.style.color = '#6dd5fa'; + pronunciation.style.fontWeight = 'bold'; + pronunciation.style.fontSize = '22px'; + pronunciation.style.transform = 'scale(1.1)'; + + // Remove highlight after animation + setTimeout(() => { + pronunciation.style.color = originalColor; + pronunciation.style.fontWeight = 'normal'; + pronunciation.style.fontSize = originalFontSize; + pronunciation.style.transform = 'scale(1)'; + }, 2000); + } + } + _generatePronunciation(word) { if (!word || typeof word !== 'string') return ''; diff --git a/src/games/QuizGame.js b/src/games/QuizGame.js index 70b02b5..a6a2be3 100644 --- a/src/games/QuizGame.js +++ b/src/games/QuizGame.js @@ -190,7 +190,8 @@ class QuizGame extends Module { vocabulary.push({ english: word, translation: data.user_language, - type: data.type || 'unknown' + type: data.type || 'unknown', + pronunciation: data.pronunciation || '' }); } } @@ -396,6 +397,14 @@ class QuizGame extends Module { opacity: 0.7; } + .option-pronunciation { + font-size: 0.85rem; + font-style: italic; + color: #6c757d; + margin-top: 8px; + transition: all 0.3s ease; + } + .quiz-feedback { text-align: center; padding: 20px; @@ -692,9 +701,19 @@ class QuizGame extends Module { ? vocab.translation : vocab.english; + // Store the Chinese word and pronunciation for TTS + const chineseWord = vocab.english; // In our data structure, english is the Chinese word + const pronunciation = vocab.pronunciation || ''; + return ` -
+
${optionText} + ${pronunciation ? `
[${pronunciation}]
` : ''}
`; }).join(''); @@ -765,6 +784,16 @@ class QuizGame extends Module { return; } + // Play TTS when clicking on an option + const word = optionElement.dataset.word; + const pronunciation = optionElement.dataset.pronunciation; + if (word) { + this._playAudio(word); + if (pronunciation) { + this._highlightPronunciation(optionElement); + } + } + this._isAnswering = true; const question = this._questions[this._currentQuestion]; const selectedAnswer = optionElement.dataset.value; @@ -962,6 +991,62 @@ class QuizGame extends Module { this._eventBus.emit('game:resumed', { instanceId: this.name }, this.name); } + _playAudio(text) { + if ('speechSynthesis' in window) { + // Cancel any ongoing speech + speechSynthesis.cancel(); + + const utterance = new SpeechSynthesisUtterance(text); + + // Get language from chapter content, fallback to en-US + const chapterLanguage = this._content?.language || 'en-US'; + utterance.lang = chapterLanguage; + utterance.rate = 0.8; + utterance.pitch = 1.0; + utterance.volume = 1.0; + + // Try to find a suitable voice for the language + const voices = speechSynthesis.getVoices(); + if (voices.length > 0) { + // Find voice matching the chapter language + const langPrefix = chapterLanguage.split('-')[0]; // e.g., "zh" from "zh-CN" + const matchingVoice = voices.find(voice => + voice.lang.startsWith(langPrefix) && (voice.name.includes('Neural') || voice.default) + ) || voices.find(voice => voice.lang.startsWith(langPrefix)); + + if (matchingVoice) { + utterance.voice = matchingVoice; + console.log('🔊 Using voice:', matchingVoice.name, matchingVoice.lang); + } + } + + speechSynthesis.speak(utterance); + } + } + + _highlightPronunciation(optionElement) { + const pronunciation = optionElement.querySelector('.option-pronunciation'); + + if (pronunciation) { + // Store original styles + const originalColor = pronunciation.style.color; + const originalFontWeight = pronunciation.style.fontWeight; + + // Add highlight + pronunciation.style.color = '#007bff'; + pronunciation.style.fontWeight = 'bold'; + pronunciation.style.transform = 'scale(1.2)'; + pronunciation.style.transition = 'all 0.3s ease'; + + // Remove highlight after animation + setTimeout(() => { + pronunciation.style.color = originalColor; + pronunciation.style.fontWeight = originalFontWeight; + pronunciation.style.transform = 'scale(1)'; + }, 2000); + } + } + _showVictoryPopup({ gameTitle, currentScore, bestScore, isNewBest, stats }) { const popup = document.createElement('div'); popup.className = 'victory-popup';