/** * VocabularyModule - Groups of 5 vocabulary exercise implementation * Implements DRSExerciseInterface for strict contract enforcement */ import DRSExerciseInterface from '../interfaces/DRSExerciseInterface.js'; class VocabularyModule extends DRSExerciseInterface { constructor(orchestrator, llmValidator, prerequisiteEngine, contextMemory) { super('VocabularyModule'); // Validate dependencies (llmValidator can be null since we use local validation) if (!orchestrator || !prerequisiteEngine || !contextMemory) { throw new Error('VocabularyModule requires orchestrator, prerequisiteEngine, and contextMemory'); } this.orchestrator = orchestrator; this.llmValidator = llmValidator; this.prerequisiteEngine = prerequisiteEngine; this.contextMemory = contextMemory; // Module state this.initialized = false; this.container = null; this.currentExerciseData = null; this.currentVocabularyGroup = []; this.currentWordIndex = 0; this.groupResults = []; this.isRevealed = false; // Configuration this.config = { groupSize: 5, masteryThreshold: 80, // 80% correct to consider mastered maxAttempts: 3, showPronunciation: true, randomizeOrder: true }; // Bind methods this._handleNextWord = this._handleNextWord.bind(this); this._handleRevealAnswer = this._handleRevealAnswer.bind(this); this._handleUserInput = this._handleUserInput.bind(this); this._handleDifficultySelection = this._handleDifficultySelection.bind(this); } /** * Initialize the exercise module * @param {Object} config - Exercise configuration * @param {Object} content - Exercise content data * @returns {Promise} */ async init(config = {}, content = {}) { if (this.initialized) return; console.log('๐Ÿ“š Initializing VocabularyModule...'); // Merge provided config with defaults this.config = { ...this.config, ...config }; // Store content for later use this.currentExerciseData = content; this.startTime = Date.now(); this.initialized = true; console.log('โœ… VocabularyModule initialized'); } /** * Render the exercise UI * @param {HTMLElement} container - Container element to render into * @returns {Promise} */ async render(container) { if (!this.initialized) { throw new Error('VocabularyModule must be initialized before rendering'); } // Use existing present() logic await this.present(container, this.currentExerciseData); } /** * Clean up and destroy the exercise * @returns {Promise} */ async destroy() { console.log('๐Ÿงน Destroying VocabularyModule...'); // Remove event listeners if (this.container) { const input = this.container.querySelector('.vocabulary-input'); const submitBtn = this.container.querySelector('.btn-submit'); const revealBtn = this.container.querySelector('.btn-reveal'); const nextBtn = this.container.querySelector('.btn-next'); const difficultyButtons = this.container.querySelectorAll('.difficulty-btn'); if (input) input.removeEventListener('input', this._handleUserInput); if (submitBtn) submitBtn.removeEventListener('click', this._handleUserInput); if (revealBtn) revealBtn.removeEventListener('click', this._handleRevealAnswer); if (nextBtn) nextBtn.removeEventListener('click', this._handleNextWord); difficultyButtons.forEach(btn => { btn.removeEventListener('click', this._handleDifficultySelection); }); // Clear container this.container.innerHTML = ''; this.container = null; } // Reset state this.currentVocabularyGroup = []; this.currentWordIndex = 0; this.groupResults = []; this.isRevealed = false; this.currentExerciseData = null; this.initialized = false; console.log('โœ… VocabularyModule destroyed'); } /** * Check if module can run with current prerequisites * @param {Array} prerequisites - List of learned vocabulary/concepts * @param {Object} chapterContent - Full chapter content * @returns {boolean} - True if module can run */ canRun(prerequisites, chapterContent) { // Vocabulary module can always run if there's vocabulary in the chapter const hasVocabulary = chapterContent && chapterContent.vocabulary && Object.keys(chapterContent.vocabulary).length > 0; return hasVocabulary; } /** * Present exercise UI and content * @param {HTMLElement} container - DOM container to render into * @param {Object} exerciseData - Specific exercise data to present * @returns {Promise} */ async present(container, exerciseData) { if (!this.initialized) { throw new Error('VocabularyModule must be initialized before use'); } this.container = container; this.currentExerciseData = exerciseData; // Extract and process all vocabulary const allVocabulary = exerciseData.vocabulary || []; // Pre-process all vocabulary items to extract clean translations allVocabulary.forEach(word => { let translation = word.translation; if (typeof translation === 'object' && translation !== null) { // Try different field names that might contain the translation translation = translation.user_language || translation.target_language || translation.translation || translation.meaning || translation.fr || translation.definition || Object.values(translation).find(val => typeof val === 'string' && val !== word.word) || JSON.stringify(translation); } word.cleanTranslation = translation; }); // Filter out already mastered words using PrerequisiteEngine const unmastedVocabulary = allVocabulary.filter(word => { if (!this.prerequisiteEngine || !this.prerequisiteEngine.isInitialized) { return true; // Include all words if PrerequisiteEngine not available } const isMastered = this.prerequisiteEngine.isMastered(word.word); if (isMastered) { console.log(`๐ŸŽฏ Skipping already mastered word: ${word.word}`); } return !isMastered; // Only include non-mastered words }); console.log(`๐Ÿ“š Filtered vocabulary: ${allVocabulary.length} total โ†’ ${unmastedVocabulary.length} unmastered words`); // Check if all words are already mastered if (unmastedVocabulary.length === 0) { this.container.innerHTML = `
๐ŸŽ‰

Vocabulary Mastered!

All vocabulary words in this chapter have been mastered.

${allVocabulary.length} Words Mastered
`; return; } // DYNAMIC MODE: Pick 5 random words from discovered words (no pre-calculation) const discoveredWords = unmastedVocabulary.filter(word => { return this.prerequisiteEngine.isDiscovered(word.word); }); console.log(`๐Ÿ“š Found ${discoveredWords.length} discovered words (out of ${unmastedVocabulary.length} unmastered)`); // Take up to 5 random discovered words const selectedWords = this._selectRandomWords(discoveredWords, this.config.groupSize); this.currentVocabularyGroup = selectedWords; this.currentWordIndex = 0; this.groupResults = []; this.isRevealed = false; console.log(`๐Ÿ“š Selected ${selectedWords.length} random words for this session:`, selectedWords.map(w => w.word)); if (this.config.randomizeOrder) { this._shuffleArray(this.currentVocabularyGroup); } console.log(`๐Ÿ“š Presenting vocabulary group (${this.currentVocabularyGroup.length} words)`); // Render initial UI await this._renderVocabularyExercise(); // Start with first word this._presentCurrentWord(); } /** * Validate user input with simple string matching (NO AI) * @param {string} userInput - User's response * @param {Object} context - Exercise context and expected answer * @returns {Promise} - Validation result with score and feedback */ async validate(userInput, context) { if (!userInput || !userInput.trim()) { return { score: 0, correct: false, feedback: "Please provide an answer.", timestamp: new Date().toISOString(), provider: 'local' }; } const currentWord = this.currentVocabularyGroup[this.currentWordIndex]; const expectedTranslation = currentWord.cleanTranslation || currentWord.translation; const userAnswer = userInput.trim(); // Simple string matching validation (NO AI) const isCorrect = this._checkTranslation(userAnswer, expectedTranslation); const result = { score: isCorrect ? 100 : 0, correct: isCorrect, feedback: isCorrect ? "Correct! Well done." : `Incorrect. The correct answer is: ${expectedTranslation}`, timestamp: new Date().toISOString(), provider: 'local', expectedAnswer: expectedTranslation, userAnswer: userAnswer }; // Record interaction in context memory this.contextMemory.recordInteraction({ type: 'vocabulary', subtype: 'translation', content: { vocabulary: [currentWord], word: currentWord.word, expectedTranslation }, userResponse: userAnswer, validation: result, context: { validationType: 'simple_string_match' } }); return result; } /** * Get current progress data * @returns {ProgressData} - Progress information for this module */ getProgress() { const totalWords = this.currentVocabularyGroup.length; const completedWords = this.groupResults.length; const correctWords = this.groupResults.filter(result => result.correct).length; return { type: 'vocabulary', totalWords, completedWords, correctWords, currentWordIndex: this.currentWordIndex, groupResults: this.groupResults, progressPercentage: totalWords > 0 ? Math.round((completedWords / totalWords) * 100) : 0, accuracyPercentage: completedWords > 0 ? Math.round((correctWords / completedWords) * 100) : 0 }; } /** * Clean up and prepare for unloading */ cleanup() { console.log('๐Ÿงน Cleaning up VocabularyModule...'); // Remove event listeners if (this.container) { this.container.innerHTML = ''; } // Reset state this.container = null; this.currentExerciseData = null; this.currentVocabularyGroup = []; this.currentWordIndex = 0; this.groupResults = []; this.isRevealed = false; console.log('โœ… VocabularyModule cleaned up'); } /** * Get module metadata * @returns {Object} - Module information */ getMetadata() { return { name: 'VocabularyModule', type: 'vocabulary', version: '1.0.0', description: 'Groups of 5 vocabulary exercises with LLM validation', capabilities: ['translation', 'pronunciation', 'spaced_repetition'], config: this.config }; } // Private Methods /** * Check translation with simple string matching and fuzzy logic * @param {string} userAnswer - User's answer * @param {string} expectedTranslation - Expected correct answer * @returns {boolean} - True if answer is acceptable * @private */ _checkTranslation(userAnswer, expectedTranslation) { if (!userAnswer || !expectedTranslation) return false; // Normalize both strings const normalizeString = (str) => { return str.toLowerCase() .trim() .replace(/[.,!?;:"'()]/g, '') // Remove punctuation .replace(/\s+/g, ' '); // Normalize whitespace }; const normalizedUser = normalizeString(userAnswer); const normalizedExpected = normalizeString(expectedTranslation); // Exact match after normalization if (normalizedUser === normalizedExpected) { return true; } // Split expected answer into alternatives (e.g., "shirt, t-shirt" or "shirt / t-shirt") const alternatives = normalizedExpected.split(/[,/|;]/).map(alt => alt.trim()); // Check if user answer matches any alternative for (const alternative of alternatives) { if (normalizedUser === alternative) { return true; } // Allow partial matches for single words if they're very close if (alternative.split(' ').length === 1 && normalizedUser.split(' ').length === 1) { const similarity = this._calculateSimilarity(normalizedUser, alternative); if (similarity > 0.85) { // 85% similarity threshold return true; } } } return false; } /** * Calculate string similarity using simple character comparison * @param {string} str1 - First string * @param {string} str2 - Second string * @returns {number} - Similarity score between 0 and 1 * @private */ _calculateSimilarity(str1, str2) { if (str1 === str2) return 1.0; if (str1.length === 0 || str2.length === 0) return 0.0; // Simple character-based similarity const longer = str1.length > str2.length ? str1 : str2; const shorter = str1.length > str2.length ? str2 : str1; if (longer.length === 0) return 1.0; const editDistance = this._levenshteinDistance(str1, str2); return (longer.length - editDistance) / longer.length; } /** * Calculate Levenshtein distance between two strings * @param {string} str1 - First string * @param {string} str2 - Second string * @returns {number} - Edit distance * @private */ _levenshteinDistance(str1, str2) { const matrix = []; for (let i = 0; i <= str2.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= str1.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= str2.length; i++) { for (let j = 1; j <= str1.length; j++) { if (str2.charAt(i - 1) === str1.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1 ); } } } return matrix[str2.length][str1.length]; } async _renderVocabularyExercise() { if (!this.container) return; const totalWords = this.currentVocabularyGroup.length; const progressPercentage = totalWords > 0 ? Math.round((this.currentWordIndex / totalWords) * 100) : 0; this.container.innerHTML = `

๐Ÿ“š Vocabulary Practice

Word ${this.currentWordIndex + 1} of ${totalWords}
`; // Add CSS styles this._addStyles(); } _updateProgressDisplay() { const progressText = document.getElementById('progress-text'); const progressFill = document.getElementById('progress-fill'); if (progressText && progressFill) { const totalWords = this.currentVocabularyGroup.length; const progressPercentage = totalWords > 0 ? Math.round((this.currentWordIndex / totalWords) * 100) : 0; // Update text (no group numbers in dynamic mode) progressText.textContent = `Word ${this.currentWordIndex + 1} of ${totalWords}`; // Update progress bar progressFill.style.width = `${progressPercentage}%`; } } _presentCurrentWord() { if (this.currentWordIndex >= this.currentVocabularyGroup.length) { this._showGroupResults(); return; } // Update progress display this._updateProgressDisplay(); const currentWord = this.currentVocabularyGroup[this.currentWordIndex]; const card = document.getElementById('vocabulary-card'); const controls = document.getElementById('exercise-controls'); if (!card || !controls) return; this.isRevealed = false; card.innerHTML = `

${currentWord.word}

${this.config.showPronunciation && currentWord.pronunciation ? `
[${currentWord.pronunciation}]
` : ''}
${currentWord.type || 'word'}
`; controls.innerHTML = `
`; // Add event listeners document.getElementById('tts-btn').onclick = () => this._handleTTS(); document.getElementById('reveal-btn').onclick = this._handleRevealAnswer; document.getElementById('submit-btn').onclick = this._handleUserInput; // Add click listener on the word itself for TTS const targetWord = document.getElementById('target-word-tts'); if (targetWord) { targetWord.onclick = () => { this._handleTTS(); this._highlightPronunciation(); }; } // Allow Enter key to submit const input = document.getElementById('translation-input'); input.addEventListener('keypress', (e) => { if (e.key === 'Enter') { this._handleUserInput(); } }); // Focus on input input.focus(); } async _handleUserInput() { const input = document.getElementById('translation-input'); const userInput = input ? input.value.trim() : ''; if (!userInput) { this._showFeedback('Please enter a translation.', 'warning'); return; } // Disable input during validation this._setInputEnabled(false); this._showFeedback('Checking answer...', 'info'); try { const validationResult = await this.validate(userInput, {}); // Store result this.groupResults[this.currentWordIndex] = { word: this.currentVocabularyGroup[this.currentWordIndex].word, userAnswer: userInput, correct: validationResult.correct, score: validationResult.score, feedback: validationResult.feedback, timestamp: new Date().toISOString() }; // Show result and difficulty selection this._showValidationResult(validationResult); } catch (error) { console.error('Validation error:', error); this._showFeedback('Error validating answer. Please try again.', 'error'); this._setInputEnabled(true); } } _handleRevealAnswer() { const revealedSection = document.getElementById('revealed-answer'); const answerSection = document.getElementById('answer-section'); if (revealedSection && answerSection) { revealedSection.style.display = 'block'; answerSection.style.display = 'none'; this.isRevealed = true; // Add click listener on revealed answer for TTS const answerTTS = document.getElementById('answer-tts'); if (answerTTS) { answerTTS.onclick = () => { this._handleTTS(); this._highlightPronunciation(); }; } // Auto-play TTS when answer is revealed setTimeout(() => { this._handleTTS(); this._highlightPronunciation(); }, 100); // Quick delay to let the answer appear // Don't mark as incorrect yet - wait for user self-assessment // The difficulty selection will determine the actual result this._showDifficultySelection(); } } _showValidationResult(validationResult) { const feedbackClass = validationResult.correct ? 'success' : 'error'; this._showFeedback(validationResult.feedback, feedbackClass); // Show correct answer if incorrect if (!validationResult.correct) { const revealedSection = document.getElementById('revealed-answer'); if (revealedSection) { revealedSection.style.display = 'block'; } } // Show difficulty selection setTimeout(() => { this._showDifficultySelection(); }, 2000); } _showDifficultySelection() { const controls = document.getElementById('exercise-controls'); if (!controls) return; controls.innerHTML = `
`; // Add event listeners for difficulty buttons document.querySelectorAll('.difficulty-btn').forEach(btn => { btn.onclick = async (e) => await this._handleDifficultySelection(e.target.dataset.difficulty); }); } async _handleDifficultySelection(difficulty) { const currentWord = this.currentVocabularyGroup[this.currentWordIndex]; // Create or update result based on user self-assessment const userAnswer = this.isRevealed ? '[revealed]' : (this.groupResults[this.currentWordIndex]?.userAnswer || '[self-assessed]'); // Convert difficulty to success/score based on spaced repetition logic const difficultyMapping = { 'again': { correct: false, score: 0 }, // Failed - need to see again soon 'hard': { correct: true, score: 60 }, // Passed but difficult 'good': { correct: true, score: 80 }, // Good understanding 'easy': { correct: true, score: 100 } // Perfect understanding }; const assessment = difficultyMapping[difficulty] || { correct: false, score: 0 }; // Create/update the result entry this.groupResults[this.currentWordIndex] = { word: currentWord.word, userAnswer: userAnswer, correct: assessment.correct, score: assessment.score, difficulty: difficulty, feedback: `Self-assessed as: ${difficulty}`, timestamp: new Date().toISOString(), wasRevealed: this.isRevealed }; // ALWAYS mark word as discovered (seen/introduced) - Enhanced with persistence const discoveryMetadata = { difficulty: difficulty, sessionId: this.orchestrator?.sessionId || 'unknown', moduleType: 'vocabulary', timestamp: new Date().toISOString(), wasRevealed: this.isRevealed, responseTime: Date.now() - (this.wordStartTime || Date.now()) }; try { await this.prerequisiteEngine.markWordDiscovered(currentWord.word, discoveryMetadata); console.log('๐Ÿ“š Enhanced persistence: Word discovered:', currentWord.word); } catch (error) { console.error('โŒ Error marking word discovered:', error); } // Mark word as mastered ONLY if good or easy - Enhanced with persistence if (['good', 'easy'].includes(difficulty)) { const masteryMetadata = { difficulty: difficulty, sessionId: this.orchestrator?.sessionId || 'unknown', moduleType: 'vocabulary', attempts: 1, // Single attempt with self-assessment correct: assessment.correct, scores: [assessment.score], masteryLevel: difficulty === 'easy' ? 2 : 1 }; try { await this.prerequisiteEngine.markWordMastered(currentWord.word, masteryMetadata); console.log('๐Ÿ† Enhanced persistence: Word mastered:', currentWord.word); } catch (error) { console.error('โŒ Error marking word mastered:', error); } // Legacy persistent storage removed - now using enhanced PrerequisiteEngine persistence } console.log(`Word "${currentWord.word}" marked as ${difficulty}`); // Move to next word this.currentWordIndex++; this._presentCurrentWord(); } _handleNextWord() { this.currentWordIndex++; this._presentCurrentWord(); } _handleTTS() { const currentWord = this.currentVocabularyGroup[this.currentWordIndex]; if (currentWord && currentWord.word) { this._speakWord(currentWord.word); } } async _speakWord(text, options = {}) { // Check if browser supports Speech Synthesis if ('speechSynthesis' in window) { try { // Cancel any ongoing speech window.speechSynthesis.cancel(); const utterance = new SpeechSynthesisUtterance(text); // Get language from chapter data, fallback to options or en-US const chapterLanguage = this.currentExerciseData?.language || 'en-US'; utterance.lang = options.lang || chapterLanguage; utterance.rate = options.rate || 0.8; utterance.pitch = options.pitch || 1; utterance.volume = options.volume || 1; // Wait for voices to be loaded before selecting one const voices = await this._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.default ) || voices.find(voice => voice.lang.startsWith(langPrefix)); if (matchingVoice) { utterance.voice = matchingVoice; console.log('๐Ÿ”Š Using voice:', matchingVoice.name, matchingVoice.lang); } else { console.warn(`๐Ÿ”Š No voice found for language: ${chapterLanguage}, available:`, voices.map(v => v.lang)); } } // Add event handlers utterance.onstart = () => { console.log('๐Ÿ”Š TTS started for:', text); this._updateTTSButton(true); }; utterance.onend = () => { console.log('๐Ÿ”Š TTS finished for:', text); this._updateTTSButton(false); }; utterance.onerror = (event) => { console.warn('๐Ÿ”Š TTS error:', event.error); this._updateTTSButton(false); }; // Speak the text window.speechSynthesis.speak(utterance); } catch (error) { console.warn('๐Ÿ”Š TTS failed:', error); this._fallbackTTS(text); } } else { console.warn('๐Ÿ”Š Speech Synthesis not supported in this browser'); this._fallbackTTS(text); } } /** * Get available speech synthesis voices, waiting for them to load if necessary * @returns {Promise} Array of available voices * @private */ _getVoices() { return new Promise((resolve) => { let voices = window.speechSynthesis.getVoices(); // If voices are already loaded, return them immediately if (voices.length > 0) { resolve(voices); return; } // Otherwise, wait for voiceschanged event const voicesChangedHandler = () => { voices = window.speechSynthesis.getVoices(); if (voices.length > 0) { window.speechSynthesis.removeEventListener('voiceschanged', voicesChangedHandler); resolve(voices); } }; window.speechSynthesis.addEventListener('voiceschanged', voicesChangedHandler); // Fallback timeout in case voices never load setTimeout(() => { window.speechSynthesis.removeEventListener('voiceschanged', voicesChangedHandler); resolve(window.speechSynthesis.getVoices()); }, 1000); }); } _updateTTSButton(isPlaying) { // Update main TTS button const ttsBtn = document.getElementById('tts-btn'); if (ttsBtn) { if (isPlaying) { ttsBtn.innerHTML = '๐Ÿ”„ Speaking...'; ttsBtn.disabled = true; } else { ttsBtn.innerHTML = '๐Ÿ”Š Listen'; ttsBtn.disabled = false; } } } _fallbackTTS(text) { // Fallback when TTS fails - show pronunciation if available const currentWord = this.currentVocabularyGroup[this.currentWordIndex]; if (currentWord && currentWord.pronunciation) { alert(`Pronunciation: /${currentWord.pronunciation}/`); } else { alert(`Word: ${text}\n(Text-to-speech not available)`); } } _highlightPronunciation() { // Highlight pronunciation when TTS is played const pronunciation = document.getElementById('pronunciation-display') || document.getElementById('pronunciation-reveal'); if (pronunciation) { // Add highlight class pronunciation.classList.add('pronunciation-highlight'); // Remove highlight after animation setTimeout(() => { pronunciation.classList.remove('pronunciation-highlight'); }, 2000); } } _showGroupResults() { const resultsContainer = document.getElementById('group-results'); const card = document.getElementById('vocabulary-card'); const controls = document.getElementById('exercise-controls'); if (!resultsContainer) return; const correctCount = this.groupResults.filter(result => result.correct).length; const totalCount = this.groupResults.length; const accuracy = totalCount > 0 ? Math.round((correctCount / totalCount) * 100) : 0; let resultClass = 'results-poor'; if (accuracy >= 80) resultClass = 'results-excellent'; else if (accuracy >= 60) resultClass = 'results-good'; // DYNAMIC MODE: Single "Continue" button - orchestrator decides next exercise const resultsHTML = `

๐Ÿ“Š Session Results

${accuracy}% Accuracy
${correctCount} / ${totalCount} correct
`; resultsContainer.innerHTML = resultsHTML; resultsContainer.style.display = 'block'; // Hide other sections if (card) card.style.display = 'none'; if (controls) controls.style.display = 'none'; // Add button listener const continueBtn = document.getElementById('continue-btn'); if (continueBtn) { continueBtn.onclick = () => { // Complete exercise and let orchestrator decide next step if (this.orchestrator && this.orchestrator.completeExercise) { this.orchestrator.completeExercise({ moduleType: 'vocabulary', results: this.groupResults, progress: this.getProgress() }); } else { console.log('โœ… Vocabulary session completed, orchestrator will decide next exercise'); // Fallback: use drsDebug if available if (window.drsDebug?.instance?.completeExercise) { window.drsDebug.instance.completeExercise(); } } }; } } _setInputEnabled(enabled) { const input = document.getElementById('translation-input'); const submitBtn = document.getElementById('submit-btn'); const revealBtn = document.getElementById('reveal-btn'); if (input) input.disabled = !enabled; if (submitBtn) submitBtn.disabled = !enabled; if (revealBtn) revealBtn.disabled = !enabled; } _showFeedback(message, type = 'info') { // Create or update feedback element let feedback = document.getElementById('feedback-message'); if (!feedback) { feedback = document.createElement('div'); feedback.id = 'feedback-message'; feedback.className = 'feedback-message'; const card = document.getElementById('vocabulary-card'); if (card) { card.appendChild(feedback); } } feedback.className = `feedback-message feedback-${type}`; feedback.textContent = message; feedback.style.display = 'block'; // Auto-hide info messages if (type === 'info') { setTimeout(() => { if (feedback) { feedback.style.display = 'none'; } }, 3000); } } /** * Select N random words from an array * @param {Array} words - Array of word objects * @param {number} count - Number of words to select * @returns {Array} - Random selection of words */ _selectRandomWords(words, count) { if (words.length <= count) { return [...words]; // Return all if not enough words } // Fisher-Yates shuffle and take first N const shuffled = [...words]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled.slice(0, count); } _shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } } _addStyles() { if (document.getElementById('vocabulary-module-styles')) return; const styles = document.createElement('style'); styles.id = 'vocabulary-module-styles'; styles.textContent = ` .vocabulary-exercise { max-width: 600px; margin: 0 auto; padding: 20px; } .exercise-header { text-align: center; margin-bottom: 30px; } .progress-info { margin-top: 10px; } .progress-bar { width: 100%; height: 8px; background-color: #e0e0e0; border-radius: 4px; margin-top: 5px; overflow: hidden; } .progress-fill { height: 100%; background: linear-gradient(90deg, #667eea, #764ba2); transition: width 0.3s ease; } .vocabulary-card { background: white; border-radius: 12px; padding: 30px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px; } .word-display { text-align: center; margin-bottom: 30px; } .target-word { font-size: 2.5em; color: #333; margin-bottom: 10px; font-weight: bold; } .target-word.clickable { cursor: pointer; transition: all 0.2s ease; } .target-word.clickable:hover { color: #667eea; transform: scale(1.05); } .pronunciation { font-style: italic; color: #666; margin-bottom: 5px; transition: all 0.3s ease; } .pronunciation-highlight { color: #667eea !important; font-weight: bold; font-size: 1.2em; animation: pulse 0.5s ease-in-out; } @keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } } .word-type { color: #888; font-size: 0.9em; text-transform: uppercase; } .translation-input { margin-bottom: 20px; } .translation-input label { display: block; margin-bottom: 8px; font-weight: bold; color: #555; } .translation-input input { width: 100%; padding: 12px; font-size: 1.1em; border: 2px solid #ddd; border-radius: 8px; box-sizing: border-box; } .translation-input input:focus { border-color: #667eea; outline: none; } .revealed-answer { background-color: #f8f9fa; padding: 15px; border-radius: 8px; margin-top: 15px; } .correct-translation { font-size: 1.2em; color: #28a745; margin-bottom: 5px; } .correct-translation.clickable { cursor: pointer; transition: all 0.2s ease; padding: 5px; border-radius: 4px; } .correct-translation.clickable:hover { background-color: #d4edda; transform: scale(1.02); } .pronunciation-text { font-style: italic; color: #666; transition: all 0.3s ease; } .exercise-controls { text-align: center; } .control-buttons { display: flex; gap: 15px; justify-content: center; } .difficulty-selection { text-align: center; } .difficulty-buttons { display: flex; gap: 8px; justify-content: center; flex-wrap: nowrap; margin-top: 15px; } .difficulty-btn { padding: 8px 12px; border: none; border-radius: 8px; cursor: pointer; font-size: 0.85em; transition: all 0.3s ease; } .group-results-content { background: white; border-radius: 12px; padding: 30px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); text-align: center; } .results-summary { margin-bottom: 30px; } .accuracy-display { margin-bottom: 10px; } .accuracy-number { font-size: 3em; font-weight: bold; display: block; } .results-excellent .accuracy-number { color: #28a745; } .results-good .accuracy-number { color: #ffc107; } .results-poor .accuracy-number { color: #dc3545; } .word-results { text-align: left; margin-bottom: 30px; } .word-result { display: flex; justify-content: space-between; align-items: center; padding: 10px; margin-bottom: 5px; border-radius: 8px; background-color: #f8f9fa; } .word-result.correct { background-color: #d4edda; } .word-result.incorrect { background-color: #f8d7da; } .feedback-message { padding: 10px; border-radius: 8px; margin-top: 15px; text-align: center; } .feedback-info { background-color: #d1ecf1; color: #0c5460; } .feedback-success { background-color: #d4edda; color: #155724; } .feedback-warning { background-color: #fff3cd; color: #856404; } .feedback-error { background-color: #f8d7da; color: #721c24; } .btn-primary { background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; padding: 12px 24px; border-radius: 8px; cursor: pointer; font-size: 1em; transition: all 0.3s ease; } .btn-primary:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); } .btn-secondary { background: #6c757d; color: white; border: none; padding: 12px 24px; border-radius: 8px; cursor: pointer; font-size: 1em; transition: all 0.3s ease; } .btn-secondary:hover { background: #5a6268; } .btn-success { background-color: #28a745; color: white; } .btn-warning { background-color: #ffc107; color: #212529; } .btn-error { background-color: #dc3545; color: white; } button:disabled { opacity: 0.6; cursor: not-allowed; } `; document.head.appendChild(styles); } // ======================================== // DRSExerciseInterface REQUIRED METHODS // ======================================== /** * Get exercise results and statistics * @returns {Object} - Results data */ getResults() { const correctWords = this.groupResults.filter(result => result.correct).length; const totalWords = this.groupResults.length; const score = totalWords > 0 ? Math.round((correctWords / totalWords) * 100) : 0; const timeSpent = this.startTime ? Date.now() - this.startTime : 0; return { score, attempts: totalWords, timeSpent, completed: this.currentWordIndex >= this.currentVocabularyGroup.length, details: { correctWords, totalWords, groupResults: this.groupResults, masteryPercentage: score } }; } /** * Handle user input during exercise * @param {Event} event - User input event * @param {*} data - Input data * @returns {void} */ handleUserInput(event, data) { // This method delegates to existing handlers // Already implemented through _handleUserInput, _handleDifficultySelection, etc. if (event && event.type) { switch (event.type) { case 'input': case 'change': this._handleUserInput(event); break; case 'click': if (event.target.classList.contains('difficulty-btn')) { this._handleDifficultySelection(event); } else if (event.target.classList.contains('btn-next')) { this._handleNextWord(event); } else if (event.target.classList.contains('btn-reveal')) { this._handleRevealAnswer(event); } break; } } } /** * Mark exercise as completed and save progress * @param {Object} results - Exercise results * @returns {Promise} */ async markCompleted(results) { console.log('๐Ÿ’พ Marking VocabularyModule as completed...'); // Mark all words in current group as mastered if score is high enough const { score, details } = results || this.getResults(); if (score >= this.config.masteryThreshold) { // Mark all words in group as mastered for (const word of this.currentVocabularyGroup) { if (this.prerequisiteEngine && this.prerequisiteEngine.isInitialized) { await this.prerequisiteEngine.markWordMastered(word.word, { score, timestamp: new Date().toISOString(), moduleType: 'vocabulary', attempts: details.totalWords }); } } console.log(`โœ… Marked ${this.currentVocabularyGroup.length} words as mastered`); } else { console.log(`โš ๏ธ Score ${score}% below mastery threshold ${this.config.masteryThreshold}%`); } // Save completion metadata if (this.contextMemory) { this.contextMemory.recordInteraction({ type: 'vocabulary', subtype: 'completion', content: { vocabulary: this.currentVocabularyGroup }, validation: results, context: { moduleType: 'vocabulary', dynamicMode: true } }); } console.log('โœ… VocabularyModule completion saved'); } /** * Get exercise type identifier * @returns {string} - Exercise type */ getExerciseType() { return 'vocabulary'; } /** * Get exercise configuration * @returns {Object} - Configuration object */ getExerciseConfig() { const wordCount = this.currentVocabularyGroup ? this.currentVocabularyGroup.length : 0; const estimatedTimePerWord = 0.5; // 30 seconds per word return { type: this.getExerciseType(), difficulty: wordCount <= 3 ? 'easy' : (wordCount <= 7 ? 'medium' : 'hard'), estimatedTime: Math.ceil(wordCount * estimatedTimePerWord), // in minutes prerequisites: [], // Vocabulary has no prerequisites metadata: { ...this.config, groupSize: this.config.groupSize, masteryThreshold: this.config.masteryThreshold, wordCount, dynamicMode: true // Flag to indicate dynamic word selection } }; } } export default VocabularyModule;