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';