/** * AudioModule - Listening comprehension exercises with AI validation * Handles audio passages with listening questions and pronunciation practice */ import DRSExerciseInterface from '../interfaces/DRSExerciseInterface.js'; class AudioModule extends DRSExerciseInterface { constructor(orchestrator, llmValidator, prerequisiteEngine, contextMemory) { super('AudioModule'); // Validate dependencies if (!orchestrator || !llmValidator || !prerequisiteEngine || !contextMemory) { throw new Error('AudioModule requires all service dependencies'); } this.orchestrator = orchestrator; this.llmValidator = llmValidator; this.prerequisiteEngine = prerequisiteEngine; this.contextMemory = contextMemory; // Module state this.initialized = false; this.container = null; this.currentExerciseData = null; this.currentAudio = null; this.currentQuestion = null; this.questionIndex = 0; this.questionResults = []; this.validationInProgress = false; this.lastValidationResult = null; this.aiAvailable = false; this.audioElement = null; this.audioProgress = 0; this.playCount = 0; // Configuration this.config = { requiredProvider: 'openai', // Prefer OpenAI for audio analysis model: 'gpt-4o-mini', temperature: 0.2, maxTokens: 800, timeout: 45000, // Longer timeout for complex audio analysis questionsPerAudio: 3, // Default number of questions per audio maxPlaybacks: 5, // Maximum playbacks before penalty showTranscriptAfter: 3, // Show transcript after N playbacks allowReplay: true // Allow replaying the audio }; // Languages configuration this.languages = { userLanguage: 'English', targetLanguage: 'French' }; // Bind methods this._handleUserInput = this._handleUserInput.bind(this); this._handleNextQuestion = this._handleNextQuestion.bind(this); this._handleRetry = this._handleRetry.bind(this); this._handlePlayAudio = this._handlePlayAudio.bind(this); this._handleAudioProgress = this._handleAudioProgress.bind(this); this._handleAudioEnded = this._handleAudioEnded.bind(this); } async init() { if (this.initialized) return; console.log('🎧 Initializing AudioModule...'); // Test AI connectivity - recommended for audio comprehension try { const testResult = await this.llmValidator.testConnectivity(); if (testResult.success) { console.log(`βœ… AI connectivity verified for audio analysis (providers: ${testResult.availableProviders?.join(', ') || testResult.provider})`); this.aiAvailable = true; } else { console.warn('⚠️ AI connection failed - audio comprehension will be limited:', testResult.error); this.aiAvailable = false; } } catch (error) { console.warn('⚠️ AI connectivity test failed - using basic audio analysis:', error.message); this.aiAvailable = false; } this.initialized = true; console.log(`βœ… AudioModule initialized (AI: ${this.aiAvailable ? 'available for deep analysis' : 'limited - basic analysis only'})`); } /** * 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) { // Check if there are audio files and if prerequisites allow them const audios = chapterContent?.audios || []; if (audios.length === 0) return false; // Find audio files that can be unlocked with current prerequisites const availableAudios = audios.filter(audio => { const unlockStatus = this.prerequisiteEngine.canUnlock('audio', audio); return unlockStatus.canUnlock; }); return availableAudios.length > 0; } /** * 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('AudioModule must be initialized before use'); } this.container = container; this.currentExerciseData = exerciseData; this.currentAudio = exerciseData.audio; this.questionIndex = 0; this.questionResults = []; this.validationInProgress = false; this.lastValidationResult = null; this.audioProgress = 0; this.playCount = 0; // Detect languages from chapter content this._detectLanguages(exerciseData); // Generate or extract questions this.questions = await this._prepareQuestions(this.currentAudio); console.log(`🎧 Presenting audio comprehension: "${this.currentAudio.title || 'Listening Exercise'}" (${this.questions.length} questions)`); // Render initial UI await this._renderAudioExercise(); // Start with audio listening phase this._showAudioListening(); } /** * Validate user input with AI for deep audio comprehension * @param {string} userInput - User's response * @param {Object} context - Exercise context * @returns {Promise} - Validation result with score and feedback */ async validate(userInput, context) { if (!userInput || !userInput.trim()) { throw new Error('Please provide an answer'); } if (!this.currentAudio || !this.currentQuestion) { throw new Error('No audio or question loaded for validation'); } console.log(`🎧 Validating audio comprehension answer for question ${this.questionIndex + 1}`); // Build comprehensive prompt for audio comprehension const prompt = this._buildAudioComprehensionPrompt(userInput); try { // Use AI validation with structured response const aiResponse = await this.llmValidator.iaEngine.validateEducationalContent(prompt, { preferredProvider: this.config.requiredProvider, temperature: this.config.temperature, maxTokens: this.config.maxTokens, timeout: this.config.timeout, systemPrompt: `You are an expert listening comprehension evaluator. Focus on understanding audio content and critical listening skills. ALWAYS respond in the exact format: [answer]yes/no [explanation]your detailed analysis here` }); // Parse structured response const parsedResult = this._parseStructuredResponse(aiResponse); // Apply playback penalty if too many replays if (this.playCount > this.config.maxPlaybacks) { parsedResult.score = Math.max(parsedResult.score - 10, 40); parsedResult.feedback += ` (Note: Score reduced due to excessive playbacks - practice active listening on first attempts)`; } // Record interaction in context memory this.contextMemory.recordInteraction({ type: 'audio', subtype: 'comprehension', content: { audio: this.currentAudio, question: this.currentQuestion, audioTitle: this.currentAudio.title || 'Listening Exercise', audioDuration: this.currentAudio.duration || 0, playCount: this.playCount }, userResponse: userInput.trim(), validation: parsedResult, context: { languages: this.languages, questionIndex: this.questionIndex, totalQuestions: this.questions.length, playbacks: this.playCount } }); return parsedResult; } catch (error) { console.error('❌ AI audio comprehension validation failed:', error); // Fallback to basic keyword analysis if AI fails if (!this.aiAvailable) { return this._performBasicAudioValidation(userInput); } throw new Error(`Audio comprehension validation failed: ${error.message}. Please check your answer and try again.`); } } /** * Get current progress data * @returns {ProgressData} - Progress information for this module */ getProgress() { const totalQuestions = this.questions ? this.questions.length : 0; const completedQuestions = this.questionResults.length; const correctAnswers = this.questionResults.filter(result => result.correct).length; return { type: 'audio', audioTitle: this.currentAudio?.title || 'Listening Exercise', totalQuestions, completedQuestions, correctAnswers, currentQuestionIndex: this.questionIndex, questionResults: this.questionResults, progressPercentage: totalQuestions > 0 ? Math.round((completedQuestions / totalQuestions) * 100) : 0, comprehensionRate: completedQuestions > 0 ? Math.round((correctAnswers / completedQuestions) * 100) : 0, playbackCount: this.playCount, aiAnalysisAvailable: this.aiAvailable }; } /** * Clean up and prepare for unloading */ cleanup() { console.log('🧹 Cleaning up AudioModule...'); // Stop audio playback if (this.audioElement) { this.audioElement.pause(); this.audioElement.currentTime = 0; } // Remove event listeners if (this.container) { this.container.innerHTML = ''; } // Reset state this.container = null; this.currentExerciseData = null; this.currentAudio = null; this.currentQuestion = null; this.questionIndex = 0; this.questionResults = []; this.questions = null; this.validationInProgress = false; this.lastValidationResult = null; this.audioElement = null; this.audioProgress = 0; this.playCount = 0; console.log('βœ… AudioModule cleaned up'); } /** * Get module metadata * @returns {Object} - Module information */ getMetadata() { return { name: 'AudioModule', type: 'audio', version: '1.0.0', description: 'Listening comprehension exercises with AI-powered audio analysis', capabilities: ['audio_comprehension', 'active_listening', 'pronunciation_feedback', 'ai_feedback'], aiRequired: false, // Can work without AI but limited config: this.config }; } // Private Methods /** * Detect languages from exercise data * @private */ _detectLanguages(exerciseData) { const chapterContent = this.currentExerciseData?.chapterContent; if (chapterContent?.metadata?.userLanguage) { this.languages.userLanguage = chapterContent.metadata.userLanguage; } if (chapterContent?.metadata?.targetLanguage) { this.languages.targetLanguage = chapterContent.metadata.targetLanguage; } console.log(`🌍 Audio languages detected: ${this.languages.userLanguage} -> ${this.languages.targetLanguage}`); } /** * Prepare questions for the audio * @private */ async _prepareQuestions(audio) { // If audio already has questions, use them if (audio.questions && audio.questions.length > 0) { return audio.questions.map((q, index) => ({ id: `q${index + 1}`, question: q.question || q.text || q, type: q.type || 'open', expectedAnswer: q.answer || q.expectedAnswer, keywords: q.keywords || [], difficulty: q.difficulty || 'medium', requiresTranscript: q.requiresTranscript || false })); } // Generate default listening comprehension questions const defaultQuestions = [ { id: 'main_content', question: `What is the main topic or content of this audio?`, type: 'open', keywords: ['main', 'topic', 'about', 'content'], difficulty: 'medium', requiresTranscript: false }, { id: 'specific_details', question: `What specific details or information did you hear?`, type: 'open', keywords: ['details', 'specific', 'information', 'mentioned'], difficulty: 'easy', requiresTranscript: false }, { id: 'comprehension', question: `What can you understand or infer from the speaker's tone and context?`, type: 'open', keywords: ['tone', 'context', 'infer', 'understand', 'meaning'], difficulty: 'hard', requiresTranscript: false } ]; // Limit to configured number of questions return defaultQuestions.slice(0, this.config.questionsPerAudio); } /** * Build comprehensive prompt for audio comprehension validation * @private */ _buildAudioComprehensionPrompt(userAnswer) { const audioTitle = this.currentAudio.title || 'Listening Exercise'; const audioTranscript = this.currentAudio.transcript || ''; const audioDuration = this.currentAudio.duration || 'Unknown'; return `You are evaluating listening comprehension for a language learning exercise. CRITICAL: You MUST respond in this EXACT format: [answer]yes/no [explanation]your detailed analysis here AUDIO CONTENT: Title: "${audioTitle}" Duration: ${audioDuration} ${audioTranscript ? `Transcript: "${audioTranscript}"` : 'Transcript: Not available'} QUESTION: ${this.currentQuestion.question} STUDENT RESPONSE: "${userAnswer}" EVALUATION CONTEXT: - Exercise Type: Listening comprehension - Languages: ${this.languages.userLanguage} -> ${this.languages.targetLanguage} - Question Type: ${this.currentQuestion.type} - Question Difficulty: ${this.currentQuestion.difficulty} - Question ${this.questionIndex + 1} of ${this.questions.length} - Audio Playbacks: ${this.playCount} EVALUATION CRITERIA: - [answer]yes if the student demonstrates understanding of the audio content in relation to the question - [answer]no if the response shows lack of comprehension or is unrelated to the audio - Focus on LISTENING COMPREHENSION and UNDERSTANDING, not perfect language - Accept different interpretations if they show understanding of the audio - Consider that students may hear different details or focus on different aspects - Reward active listening and attention to audio-specific elements (tone, emphasis, pronunciation) [explanation] should provide: 1. What the student understood correctly from the audio 2. What they might have missed or misunderstood 3. Encouragement and specific improvement suggestions for listening skills 4. Connection to broader audio meaning and context 5. Tips for better listening comprehension Format: [answer]yes/no [explanation]your comprehensive educational feedback here`; } /** * Parse structured AI response for audio comprehension * @private */ _parseStructuredResponse(aiResponse) { try { let responseText = ''; // Extract text from AI response if (typeof aiResponse === 'string') { responseText = aiResponse; } else if (aiResponse.content) { responseText = aiResponse.content; } else if (aiResponse.text) { responseText = aiResponse.text; } else { responseText = JSON.stringify(aiResponse); } console.log('πŸ” Parsing AI audio comprehension response:', responseText.substring(0, 150) + '...'); // Extract [answer] - case insensitive const answerMatch = responseText.match(/\[answer\](yes|no)/i); if (!answerMatch) { throw new Error('AI response missing [answer] format'); } // Extract [explanation] - multiline support const explanationMatch = responseText.match(/\[explanation\](.+)/s); if (!explanationMatch) { throw new Error('AI response missing [explanation] format'); } const isCorrect = answerMatch[1].toLowerCase() === 'yes'; const explanation = explanationMatch[1].trim(); // Higher scores for audio comprehension to encourage listening const result = { score: isCorrect ? 85 : 55, // Slightly lower than text due to listening difficulty correct: isCorrect, feedback: explanation, answer: answerMatch[1].toLowerCase(), explanation: explanation, timestamp: new Date().toISOString(), provider: this.config.requiredProvider, model: this.config.model, cached: false, formatValid: true, audioComprehension: true }; console.log(`βœ… AI audio comprehension parsed: ${result.answer} - Score: ${result.score}`); return result; } catch (error) { console.error('❌ Failed to parse AI audio comprehension response:', error); console.error('Raw response:', aiResponse); throw new Error(`AI response format invalid: ${error.message}`); } } /** * Perform basic audio validation when AI is unavailable * @private */ _performBasicAudioValidation(userAnswer) { console.log('πŸ” Performing basic audio validation (AI unavailable)'); const answerLength = userAnswer.trim().length; const hasKeywords = this.currentQuestion.keywords?.some(keyword => userAnswer.toLowerCase().includes(keyword.toLowerCase()) ); // Basic scoring based on answer length and keyword presence let score = 35; // Lower base score for audio (harder without AI) if (answerLength > 15) score += 15; // Substantial answer if (answerLength > 40) score += 10; // Detailed answer if (hasKeywords) score += 20; // Contains relevant keywords if (answerLength > 80) score += 10; // Very detailed if (this.playCount <= 2) score += 10; // Bonus for fewer playbacks const isCorrect = score >= 65; return { score: Math.min(score, 100), correct: isCorrect, feedback: isCorrect ? "Good listening comprehension demonstrated! Your answer shows understanding of the audio content." : "Your answer could be more detailed. Try to listen for specific information and include more details from what you heard.", timestamp: new Date().toISOString(), provider: 'basic_audio_analysis', model: 'keyword_length_analysis', cached: false, mockGenerated: true, audioComprehension: true }; } /** * Render the audio exercise interface * @private */ async _renderAudioExercise() { if (!this.container || !this.currentAudio) return; const audioTitle = this.currentAudio.title || 'Listening Exercise'; const audioDuration = this.currentAudio.duration ? `${this.currentAudio.duration}s` : 'Unknown'; const audioUrl = this.currentAudio.url || this.currentAudio.src || ''; this.container.innerHTML = `

🎧 Listening Comprehension

${this.questions?.length || 0} questions β€’ ${audioDuration} ${!this.aiAvailable ? ' β€’ ⚠️ Limited analysis mode' : ' β€’ 🧠 AI analysis'}

${audioTitle}

Plays: 0 Duration: ${audioDuration}
${audioUrl ? ` ` : `
🎡 Audio file placeholder (${audioTitle})
`}
0:00 / ${audioDuration}
`; // Add CSS styles this._addStyles(); // Setup audio element this._setupAudioElement(); // Add event listeners this._setupEventListeners(); } /** * Setup audio element and controls * @private */ _setupAudioElement() { this.audioElement = document.getElementById('audio-element'); if (this.audioElement) { this.audioElement.addEventListener('loadedmetadata', () => { const totalTime = document.getElementById('total-time'); if (totalTime && this.audioElement.duration) { const minutes = Math.floor(this.audioElement.duration / 60); const seconds = Math.floor(this.audioElement.duration % 60); totalTime.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; } }); this.audioElement.addEventListener('timeupdate', this._handleAudioProgress); this.audioElement.addEventListener('ended', this._handleAudioEnded); } } /** * Setup event listeners for audio exercise * @private */ _setupEventListeners() { const playAudioBtn = document.getElementById('play-audio-btn'); const replayAudioBtn = document.getElementById('replay-audio-btn'); const startQuestionsBtn = document.getElementById('start-questions-btn'); const answerInput = document.getElementById('answer-input'); const validateBtn = document.getElementById('validate-answer-btn'); const retryBtn = document.getElementById('retry-answer-btn'); const nextBtn = document.getElementById('next-question-btn'); const finishBtn = document.getElementById('finish-audio-btn'); // Audio control buttons if (playAudioBtn) { playAudioBtn.onclick = this._handlePlayAudio; } if (replayAudioBtn) { replayAudioBtn.onclick = this._handlePlayAudio; } // Start questions button if (startQuestionsBtn) { startQuestionsBtn.onclick = () => this._startQuestions(); } // Answer input validation if (answerInput) { answerInput.addEventListener('input', () => { const hasText = answerInput.value.trim().length > 0; if (validateBtn) { validateBtn.disabled = !hasText || this.validationInProgress; } }); // Allow Ctrl+Enter to validate answerInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey) && !validateBtn.disabled) { e.preventDefault(); this._handleUserInput(); } }); } // Validate button if (validateBtn) { validateBtn.onclick = this._handleUserInput; } // Action buttons if (retryBtn) retryBtn.onclick = this._handleRetry; if (nextBtn) nextBtn.onclick = this._handleNextQuestion; if (finishBtn) finishBtn.onclick = () => this._completeAudioExercise(); } /** * Handle audio playback * @private */ _handlePlayAudio() { if (!this.audioElement) { // For demo mode without actual audio this._simulateAudioPlayback(); return; } const playBtn = document.getElementById('play-audio-btn'); const replayBtn = document.getElementById('replay-audio-btn'); const playIcon = document.getElementById('play-icon'); const playText = document.getElementById('play-text'); if (this.audioElement.paused) { this.audioElement.play(); this.playCount++; // Update play counter const playCounter = document.getElementById('play-counter'); if (playCounter) playCounter.textContent = this.playCount; // Update button states if (playIcon) playIcon.textContent = '⏸️'; if (playText) playText.textContent = 'Pause'; // Show transcript after enough plays if (this.playCount >= this.config.showTranscriptAfter) { const transcriptSection = document.getElementById('transcript-section'); if (transcriptSection) transcriptSection.style.display = 'block'; } // Show questions button after first play if (this.playCount === 1) { const startQuestionsBtn = document.getElementById('start-questions-btn'); if (startQuestionsBtn) startQuestionsBtn.style.display = 'inline-block'; } } else { this.audioElement.pause(); if (playIcon) playIcon.textContent = '▢️'; if (playText) playText.textContent = 'Resume'; } } /** * Simulate audio playback for demo mode * @private */ _simulateAudioPlayback() { this.playCount++; const playCounter = document.getElementById('play-counter'); if (playCounter) playCounter.textContent = this.playCount; // Simulate progress let progress = 0; const progressFill = document.getElementById('audio-progress'); const currentTime = document.getElementById('current-time'); const interval = setInterval(() => { progress += 2; if (progressFill) progressFill.style.width = `${progress}%`; if (currentTime) { const seconds = Math.floor((progress / 100) * 30); // Assume 30s demo const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; currentTime.textContent = `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; } if (progress >= 100) { clearInterval(interval); this._handleAudioEnded(); } }, 100); // Show transcript after enough plays if (this.playCount >= this.config.showTranscriptAfter) { const transcriptSection = document.getElementById('transcript-section'); if (transcriptSection) transcriptSection.style.display = 'block'; } // Show questions button after first play if (this.playCount === 1) { const startQuestionsBtn = document.getElementById('start-questions-btn'); if (startQuestionsBtn) startQuestionsBtn.style.display = 'inline-block'; } } /** * Handle audio progress updates * @private */ _handleAudioProgress() { if (!this.audioElement) return; const progress = (this.audioElement.currentTime / this.audioElement.duration) * 100; const progressFill = document.getElementById('audio-progress'); const currentTime = document.getElementById('current-time'); if (progressFill) progressFill.style.width = `${progress}%`; if (currentTime && this.audioElement.currentTime) { const minutes = Math.floor(this.audioElement.currentTime / 60); const seconds = Math.floor(this.audioElement.currentTime % 60); currentTime.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; } } /** * Handle audio playback end * @private */ _handleAudioEnded() { const playIcon = document.getElementById('play-icon'); const playText = document.getElementById('play-text'); if (playIcon) playIcon.textContent = '▢️'; if (playText) playText.textContent = 'Play Again'; // Reset audio to beginning if (this.audioElement) { this.audioElement.currentTime = 0; } } /** * Show audio listening phase * @private */ _showAudioListening() { const audioSection = document.getElementById('audio-player-section'); const questionsSection = document.getElementById('questions-section'); if (audioSection) audioSection.style.display = 'block'; if (questionsSection) questionsSection.style.display = 'none'; } /** * Start questions phase * @private */ _startQuestions() { const audioSection = document.getElementById('audio-player-section'); const questionsSection = document.getElementById('questions-section'); // Keep audio player visible but smaller if (audioSection) { audioSection.style.display = 'block'; audioSection.classList.add('minimized'); } if (questionsSection) questionsSection.style.display = 'block'; this._presentCurrentQuestion(); } /** * Present current question * @private */ _presentCurrentQuestion() { if (this.questionIndex >= this.questions.length) { this._showAudioResults(); return; } this.currentQuestion = this.questions[this.questionIndex]; const questionContent = document.getElementById('question-content'); const questionCounter = document.getElementById('question-counter'); const progressFill = document.getElementById('progress-fill'); if (!questionContent || !this.currentQuestion) return; // Update progress const progressPercentage = ((this.questionIndex + 1) / this.questions.length) * 100; if (progressFill) progressFill.style.width = `${progressPercentage}%`; if (questionCounter) questionCounter.textContent = `Question ${this.questionIndex + 1} of ${this.questions.length}`; // Display question questionContent.innerHTML = `
${this.currentQuestion.question}
${this.currentQuestion.type} ${this.currentQuestion.difficulty} ${this.currentQuestion.requiresTranscript ? 'πŸ“„ Transcript helpful' : ''}
`; // Clear previous answer and focus const answerInput = document.getElementById('answer-input'); if (answerInput) { answerInput.value = ''; answerInput.focus(); } // Hide explanation panel const explanationPanel = document.getElementById('explanation-panel'); if (explanationPanel) explanationPanel.style.display = 'none'; } /** * Handle user input validation * @private */ async _handleUserInput() { const answerInput = document.getElementById('answer-input'); const validateBtn = document.getElementById('validate-answer-btn'); const statusDiv = document.getElementById('validation-status'); if (!answerInput || !validateBtn || !statusDiv) return; const userAnswer = answerInput.value.trim(); if (!userAnswer) return; try { // Set validation in progress this.validationInProgress = true; validateBtn.disabled = true; answerInput.disabled = true; // Show loading status statusDiv.innerHTML = `
πŸ”
${this.aiAvailable ? 'AI is analyzing your comprehension...' : 'Analyzing your answer...'}
`; // Call validation const result = await this.validate(userAnswer, {}); this.lastValidationResult = result; // Store result this.questionResults[this.questionIndex] = { question: this.currentQuestion.question, userAnswer: userAnswer, correct: result.correct, score: result.score, feedback: result.feedback, timestamp: new Date().toISOString(), playbackCount: this.playCount }; // Show result this._showValidationResult(result); // Update status statusDiv.innerHTML = `
${result.correct ? 'βœ…' : 'πŸ‘‚'} Analysis complete
`; } catch (error) { console.error('❌ Audio validation error:', error); // Show error status statusDiv.innerHTML = `
⚠️ Error: ${error.message}
`; // Re-enable input for retry this._enableRetry(); } } /** * Show validation result in explanation panel * @private */ _showValidationResult(result) { const explanationPanel = document.getElementById('explanation-panel'); const explanationContent = document.getElementById('explanation-content'); const nextBtn = document.getElementById('next-question-btn'); const retryBtn = document.getElementById('retry-answer-btn'); const finishBtn = document.getElementById('finish-audio-btn'); if (!explanationPanel || !explanationContent) return; // Show panel explanationPanel.style.display = 'block'; // Set explanation content explanationContent.innerHTML = `
${result.correct ? 'βœ… Good Listening!' : 'πŸ‘‚ Keep Practicing!'} Score: ${result.score}/100
${result.explanation || result.feedback}
${result.audioComprehension ? '
🎧 This analysis focuses on your listening skills and understanding of audio content.
' : ''} ${this.playCount > this.config.maxPlaybacks ? '
πŸ’‘ Try to listen actively on the first few attempts for better comprehension scores.
' : ''}
`; // Show appropriate buttons const isLastQuestion = this.questionIndex >= this.questions.length - 1; if (nextBtn) nextBtn.style.display = isLastQuestion ? 'none' : 'inline-block'; if (finishBtn) finishBtn.style.display = isLastQuestion ? 'inline-block' : 'none'; if (retryBtn) retryBtn.style.display = result.correct ? 'none' : 'inline-block'; // Scroll to explanation explanationPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } /** * Handle next question * @private */ _handleNextQuestion() { this.questionIndex++; this._presentCurrentQuestion(); } /** * Handle retry * @private */ _handleRetry() { // Hide explanation and enable new input const explanationPanel = document.getElementById('explanation-panel'); const statusDiv = document.getElementById('validation-status'); if (explanationPanel) explanationPanel.style.display = 'none'; if (statusDiv) statusDiv.innerHTML = ''; this._enableRetry(); } /** * Enable retry after error * @private */ _enableRetry() { this.validationInProgress = false; const answerInput = document.getElementById('answer-input'); const validateBtn = document.getElementById('validate-answer-btn'); if (answerInput) { answerInput.disabled = false; answerInput.focus(); } if (validateBtn) { validateBtn.disabled = false; } } /** * Show final audio results * @private */ _showAudioResults() { const resultsContainer = document.getElementById('audio-results'); const questionsSection = document.getElementById('questions-section'); if (!resultsContainer) return; const correctCount = this.questionResults.filter(result => result.correct).length; const totalCount = this.questionResults.length; const comprehensionRate = totalCount > 0 ? Math.round((correctCount / totalCount) * 100) : 0; let resultClass = 'results-poor'; if (comprehensionRate >= 80) resultClass = 'results-excellent'; else if (comprehensionRate >= 60) resultClass = 'results-good'; const resultsHTML = `

πŸ“Š Listening Comprehension Results

${comprehensionRate}% Comprehension Rate
${correctCount} / ${totalCount} questions understood well
${this.playCount} Total Plays
${this.playCount <= this.config.maxPlaybacks ? 'πŸ‘' : '⚠️'} ${this.playCount <= this.config.maxPlaybacks ? 'Good listening' : 'Practice active listening'}
${this.questionResults.map((result, index) => `
Q${index + 1} ${result.correct ? 'βœ…' : 'πŸ‘‚'} Score: ${result.score}/100 Plays: ${result.playbackCount || this.playCount}
`).join('')}
`; resultsContainer.innerHTML = resultsHTML; resultsContainer.style.display = 'block'; // Hide other sections if (questionsSection) questionsSection.style.display = 'none'; // Add action listeners document.getElementById('complete-audio-btn').onclick = () => this._completeAudioExercise(); document.getElementById('replay-audio-btn').onclick = () => this._replayAudio(); } /** * Complete audio exercise * @private */ _completeAudioExercise() { // Mark audio as comprehended if performance is good const correctCount = this.questionResults.filter(result => result.correct).length; const comprehensionRate = correctCount / this.questionResults.length; if (comprehensionRate >= 0.6) { // 60% comprehension threshold const audioId = this.currentAudio.id || this.currentAudio.title || 'audio_exercise'; const metadata = { comprehensionRate: Math.round(comprehensionRate * 100), questionsAnswered: this.questionResults.length, correctAnswers: correctCount, totalPlaybacks: this.playCount, sessionId: this.orchestrator?.sessionId || 'unknown', moduleType: 'audio', aiAnalysisUsed: this.aiAvailable, listeningEfficiency: this.playCount <= this.config.maxPlaybacks ? 'good' : 'needs_improvement' }; this.prerequisiteEngine.markPhraseMastered(audioId, metadata); // Also save to persistent storage if (window.addMasteredItem && this.orchestrator?.bookId && this.orchestrator?.chapterId) { window.addMasteredItem( this.orchestrator.bookId, this.orchestrator.chapterId, 'audios', audioId, metadata ); } } // Emit completion event this.orchestrator._eventBus.emit('drs:exerciseCompleted', { moduleType: 'audio', results: this.questionResults, progress: this.getProgress() }, 'AudioModule'); } /** * Replay audio exercise * @private */ _replayAudio() { this.questionIndex = 0; this.questionResults = []; this.playCount = 0; this._showAudioListening(); const resultsContainer = document.getElementById('audio-results'); if (resultsContainer) resultsContainer.style.display = 'none'; // Reset UI elements const playCounter = document.getElementById('play-counter'); if (playCounter) playCounter.textContent = '0'; const startQuestionsBtn = document.getElementById('start-questions-btn'); if (startQuestionsBtn) startQuestionsBtn.style.display = 'none'; const transcriptSection = document.getElementById('transcript-section'); if (transcriptSection) transcriptSection.style.display = 'none'; } /** * Add CSS styles for audio exercise * @private */ _addStyles() { if (document.getElementById('audio-module-styles')) return; const styles = document.createElement('style'); styles.id = 'audio-module-styles'; styles.textContent = ` .audio-exercise { max-width: 900px; margin: 0 auto; padding: 20px; display: grid; gap: 20px; } .exercise-header { text-align: center; margin-bottom: 20px; } .audio-info { margin-top: 10px; } .audio-meta { color: #666; font-size: 0.9em; } .audio-content { display: grid; gap: 20px; } .audio-player-card, .question-card { background: white; border-radius: 12px; padding: 30px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); } .audio-player-section.minimized .audio-player-card { padding: 20px; background: #f8f9fa; border: 2px solid #e9ecef; } .player-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 2px solid #eee; } .player-header h3 { margin: 0; color: #333; font-size: 1.5em; } .player-stats { display: flex; gap: 15px; font-size: 0.9em; color: #666; } .play-count { font-weight: 600; } .audio-player { margin-bottom: 25px; } .audio-placeholder { text-align: center; padding: 40px; background: linear-gradient(135deg, #f8f9ff, #e8f4fd); border-radius: 10px; color: #666; font-size: 1.1em; margin-bottom: 20px; } .player-controls { display: flex; flex-direction: column; gap: 15px; align-items: center; } .play-btn { font-size: 1.1em; min-width: 140px; } .progress-container { width: 100%; max-width: 400px; text-align: center; } .progress-bar { width: 100%; height: 8px; background-color: #e0e0e0; border-radius: 4px; margin-bottom: 10px; overflow: hidden; } .progress-fill { height: 100%; background: linear-gradient(90deg, #28a745, #20c997); transition: width 0.3s ease; } .time-display { font-size: 0.9em; color: #666; font-family: monospace; } .transcript-section { margin-top: 20px; padding: 20px; background: linear-gradient(135deg, #fff9e6, #fff3cd); border-radius: 10px; border-left: 4px solid #ffc107; } .transcript-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .transcript-header h4 { margin: 0; color: #333; } .transcript-note { font-size: 0.8em; color: #666; background: rgba(255,255,255,0.7); padding: 4px 8px; border-radius: 12px; } .transcript-content { line-height: 1.6; color: #444; font-style: italic; } .listening-actions { text-align: center; margin-top: 20px; } .question-progress { margin-bottom: 25px; } .progress-indicator { text-align: center; } .question-display { margin-bottom: 25px; padding: 20px; background: linear-gradient(135deg, #f8f9ff, #e8f4fd); border-radius: 10px; border-left: 4px solid #6c5ce7; } .question-text { font-size: 1.3em; font-weight: 600; color: #333; margin-bottom: 15px; line-height: 1.4; } .question-meta { display: flex; gap: 15px; align-items: center; flex-wrap: wrap; } .question-type { background: #e3f2fd; color: #1976d2; padding: 4px 12px; border-radius: 20px; font-size: 0.85em; font-weight: 500; } .requires-transcript { background: #fff3e0; color: #f57c00; padding: 4px 12px; border-radius: 20px; font-size: 0.85em; font-weight: 500; } .difficulty-easy { background: #e8f5e8; color: #2e7d32; padding: 4px 12px; border-radius: 20px; font-size: 0.85em; font-weight: 500; } .difficulty-medium { background: #fff3e0; color: #f57c00; padding: 4px 12px; border-radius: 20px; font-size: 0.85em; font-weight: 500; } .difficulty-hard { background: #ffebee; color: #c62828; padding: 4px 12px; border-radius: 20px; font-size: 0.85em; font-weight: 500; } .answer-input-section { margin-bottom: 20px; } .answer-input-section label { display: block; margin-bottom: 10px; font-weight: 600; color: #555; } .answer-input-section textarea { width: 100%; padding: 15px; font-size: 1.05em; border: 2px solid #ddd; border-radius: 8px; resize: vertical; min-height: 100px; box-sizing: border-box; transition: border-color 0.3s ease; font-family: inherit; line-height: 1.5; } .answer-input-section textarea:focus { outline: none; border-color: #6c5ce7; } .question-controls { display: flex; flex-direction: column; align-items: center; gap: 15px; } .question-controls > div:first-child { display: flex; gap: 15px; flex-wrap: wrap; justify-content: center; } .validation-status { min-height: 30px; display: flex; align-items: center; justify-content: center; } .status-loading, .status-complete, .status-error { display: flex; align-items: center; gap: 10px; padding: 12px 20px; border-radius: 25px; font-weight: 500; } .status-loading { background: #e3f2fd; color: #1976d2; } .status-complete { background: #e8f5e8; color: #2e7d32; } .status-error { background: #ffebee; color: #c62828; } .loading-spinner { animation: spin 1s linear infinite; } .explanation-panel { background: white; border-radius: 12px; padding: 25px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); border-left: 4px solid #6c5ce7; } .panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #eee; } .panel-header h3 { margin: 0; color: #333; } .analysis-model { font-size: 0.9em; color: #666; background: #f5f5f5; padding: 4px 8px; border-radius: 4px; } .explanation-result { padding: 20px; border-radius: 8px; margin-bottom: 15px; } .explanation-result.correct { border-left: 4px solid #4caf50; background: linear-gradient(135deg, #f1f8e9, #e8f5e8); } .explanation-result.needs-improvement { border-left: 4px solid #ff9800; background: linear-gradient(135deg, #fff8e1, #fff3e0); } .result-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; font-weight: 600; } .result-indicator { font-size: 1.1em; } .comprehension-score { font-size: 0.9em; color: #666; } .explanation-text { line-height: 1.6; color: #333; font-size: 1.05em; margin-bottom: 10px; } .analysis-note, .playback-note { font-size: 0.9em; color: #666; font-style: italic; padding-top: 10px; border-top: 1px solid #eee; margin-top: 10px; } .panel-actions { display: flex; gap: 15px; justify-content: center; flex-wrap: wrap; } .audio-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; } .comprehension-display { margin-bottom: 15px; } .comprehension-rate { font-size: 3em; font-weight: bold; display: block; } .results-excellent .comprehension-rate { color: #4caf50; } .results-good .comprehension-rate { color: #ff9800; } .results-poor .comprehension-rate { color: #f44336; } .comprehension-label { font-size: 1.2em; color: #666; } .questions-summary { font-size: 1.1em; color: #555; margin-bottom: 20px; } .listening-stats { display: flex; justify-content: center; gap: 40px; margin-top: 20px; padding: 20px; background: #f8f9fa; border-radius: 10px; } .stat-item { text-align: center; } .stat-value { display: block; font-size: 1.5em; font-weight: bold; color: #333; } .stat-label { font-size: 0.9em; color: #666; } .question-breakdown { display: grid; gap: 10px; margin-bottom: 30px; } .question-result { display: flex; align-items: center; justify-content: space-between; padding: 15px; border-radius: 8px; background: #f8f9fa; } .question-result.understood { background: linear-gradient(135deg, #e8f5e8, #f1f8e9); border-left: 4px solid #4caf50; } .question-result.needs-work { background: linear-gradient(135deg, #fff8e1, #fff3e0); border-left: 4px solid #ff9800; } .question-summary { display: flex; align-items: center; gap: 15px; } .question-num { font-weight: bold; color: #333; } .playback-info { font-size: 0.85em; color: #666; background: rgba(255,255,255,0.7); padding: 4px 8px; border-radius: 10px; } .results-actions { display: flex; gap: 15px; justify-content: center; flex-wrap: wrap; } .btn { padding: 12px 24px; border: none; border-radius: 8px; cursor: pointer; font-size: 1em; font-weight: 500; transition: all 0.3s ease; display: inline-flex; align-items: center; gap: 8px; text-decoration: none; } .btn:disabled { opacity: 0.6; cursor: not-allowed; } .btn-primary { background: linear-gradient(135deg, #6c5ce7, #a29bfe); color: white; } .btn-primary:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(108, 92, 231, 0.3); } .btn-secondary { background: #6c757d; color: white; } .btn-secondary:hover:not(:disabled) { background: #5a6268; } .btn-outline { background: transparent; border: 2px solid #6c5ce7; color: #6c5ce7; } .btn-outline:hover:not(:disabled) { background: #6c5ce7; color: white; } .btn-success { background: linear-gradient(135deg, #00b894, #00cec9); color: white; } .btn-success:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(0, 184, 148, 0.3); } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @media (max-width: 768px) { .audio-exercise { padding: 15px; } .audio-player-card, .question-card, .explanation-panel { padding: 20px; } .question-text { font-size: 1.2em; } .comprehension-rate { font-size: 2.5em; } .panel-actions, .results-actions { flex-direction: column; } .player-header { flex-direction: column; gap: 15px; align-items: flex-start; } .listening-stats { flex-direction: column; gap: 20px; } .question-controls > div:first-child { flex-direction: column; align-items: center; } } `; document.head.appendChild(styles); } // ======================================== // DRSExerciseInterface REQUIRED METHODS // ======================================== async init(config = {}, content = {}) { this.config = { ...this.config, ...config }; this.currentExerciseData = content; this.startTime = Date.now(); this.initialized = true; } async render(container) { if (!this.initialized) throw new Error('AudioModule must be initialized before rendering'); await this.present(container, this.currentExerciseData); } async destroy() { this.cleanup?.(); this.container = null; this.initialized = false; } getResults() { return { score: this.progress?.averageScore ? Math.round(this.progress.averageScore * 100) : 0, attempts: this.userResponses?.length || 0, timeSpent: this.startTime ? Date.now() - this.startTime : 0, completed: true, details: { progress: this.progress, responses: this.userResponses } }; } handleUserInput(event, data) { if (event?.type === 'input') this._handleInputChange?.(event); if (event?.type === 'click' && event.target.id === 'submitButton') this._handleSubmit?.(event); } async markCompleted(results) { const { score, details } = results || this.getResults(); if (this.contextMemory) { this.contextMemory.recordInteraction({ type: 'audio', subtype: 'completion', content: this.currentExerciseData, validation: { score }, context: { moduleType: 'audio' } }); } } getExerciseType() { return 'audio'; } getExerciseConfig() { return { type: this.getExerciseType(), difficulty: this.currentExerciseData?.difficulty || 'medium', estimatedTime: 5, prerequisites: [], metadata: { ...this.config, requiresAI: false } }; } } export default AudioModule;