/** * ImageModule - Visual comprehension exercises with AI validation * Handles image content with visual analysis questions and description exercises */ import ExerciseModuleInterface from '../interfaces/ExerciseModuleInterface.js'; class ImageModule extends ExerciseModuleInterface { constructor(orchestrator, llmValidator, prerequisiteEngine, contextMemory) { super(); // Validate dependencies if (!orchestrator || !llmValidator || !prerequisiteEngine || !contextMemory) { throw new Error('ImageModule 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.currentImage = null; this.currentQuestion = null; this.questionIndex = 0; this.questionResults = []; this.validationInProgress = false; this.lastValidationResult = null; this.aiAvailable = false; this.imageLoaded = false; this.viewingTime = 0; this.startViewTime = null; // Configuration this.config = { requiredProvider: 'openai', // Prefer OpenAI for image analysis model: 'gpt-4o-mini', // Model that supports vision temperature: 0.2, maxTokens: 800, timeout: 45000, // Longer timeout for image analysis questionsPerImage: 3, // Default number of questions per image minViewTime: 5, // Minimum viewing time in seconds for bonus showImageDuringQuestions: true, // Keep image visible during questions allowZoom: true // Allow image zooming }; // 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._handleImageLoad = this._handleImageLoad.bind(this); this._handleImageZoom = this._handleImageZoom.bind(this); this._startViewTimer = this._startViewTimer.bind(this); this._stopViewTimer = this._stopViewTimer.bind(this); } async init() { if (this.initialized) return; console.log('๐Ÿ–ผ๏ธ Initializing ImageModule...'); // Test AI connectivity - required for image analysis try { const testResult = await this.llmValidator.testConnectivity(); if (testResult.success) { console.log(`โœ… AI connectivity verified for image analysis (providers: ${testResult.availableProviders?.join(', ') || testResult.provider})`); this.aiAvailable = true; } else { console.warn('โš ๏ธ AI connection failed - image comprehension will be very limited:', testResult.error); this.aiAvailable = false; } } catch (error) { console.warn('โš ๏ธ AI connectivity test failed - using basic image analysis:', error.message); this.aiAvailable = false; } this.initialized = true; console.log(`โœ… ImageModule initialized (AI: ${this.aiAvailable ? 'available for vision 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 images and if prerequisites allow them const images = chapterContent?.images || []; if (images.length === 0) return false; // Find images that can be unlocked with current prerequisites const availableImages = images.filter(image => { const unlockStatus = this.prerequisiteEngine.canUnlock('image', image); return unlockStatus.canUnlock; }); return availableImages.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('ImageModule must be initialized before use'); } this.container = container; this.currentExerciseData = exerciseData; this.currentImage = exerciseData.image; this.questionIndex = 0; this.questionResults = []; this.validationInProgress = false; this.lastValidationResult = null; this.imageLoaded = false; this.viewingTime = 0; this.startViewTime = null; // Detect languages from chapter content this._detectLanguages(exerciseData); // Generate or extract questions this.questions = await this._prepareQuestions(this.currentImage); console.log(`๐Ÿ–ผ๏ธ Presenting image comprehension: "${this.currentImage.title || 'Visual Exercise'}" (${this.questions.length} questions)`); // Render initial UI await this._renderImageExercise(); // Start with image viewing phase this._showImageViewing(); } /** * Validate user input with AI for deep image 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.currentImage || !this.currentQuestion) { throw new Error('No image or question loaded for validation'); } console.log(`๐Ÿ–ผ๏ธ Validating image comprehension answer for question ${this.questionIndex + 1}`); // Build comprehensive prompt for image comprehension const prompt = this._buildImageComprehensionPrompt(userInput); try { // Use AI validation with structured response - includes image if available const aiResponse = await this.llmValidator.iaEngine.validateEducationalContent(prompt, { preferredProvider: this.config.requiredProvider, temperature: this.config.temperature, maxTokens: this.config.maxTokens, timeout: this.config.timeout, imageUrl: this.currentImage.url || this.currentImage.src, // Include image for vision analysis systemPrompt: `You are an expert visual comprehension evaluator. Analyze both the image content and the student's response. Focus on visual observation skills and interpretation. ALWAYS respond in the exact format: [answer]yes/no [explanation]your detailed analysis here` }); // Parse structured response const parsedResult = this._parseStructuredResponse(aiResponse); // Apply viewing time bonus if (this.viewingTime >= this.config.minViewTime) { parsedResult.score = Math.min(parsedResult.score + 5, 100); parsedResult.feedback += ` (Bonus: +5 points for thorough image observation)`; } // Record interaction in context memory this.contextMemory.recordInteraction({ type: 'image', subtype: 'comprehension', content: { image: this.currentImage, question: this.currentQuestion, imageTitle: this.currentImage.title || 'Visual Exercise', imageDescription: this.currentImage.description || '', viewingTime: this.viewingTime }, userResponse: userInput.trim(), validation: parsedResult, context: { languages: this.languages, questionIndex: this.questionIndex, totalQuestions: this.questions.length, observationTime: this.viewingTime } }); return parsedResult; } catch (error) { console.error('โŒ AI image comprehension validation failed:', error); // Fallback to basic keyword analysis if AI fails if (!this.aiAvailable) { return this._performBasicImageValidation(userInput); } throw new Error(`Image 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: 'image', imageTitle: this.currentImage?.title || 'Visual 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, observationTime: this.viewingTime, aiAnalysisAvailable: this.aiAvailable }; } /** * Clean up and prepare for unloading */ cleanup() { console.log('๐Ÿงน Cleaning up ImageModule...'); // Stop view timer this._stopViewTimer(); // Remove event listeners if (this.container) { this.container.innerHTML = ''; } // Reset state this.container = null; this.currentExerciseData = null; this.currentImage = null; this.currentQuestion = null; this.questionIndex = 0; this.questionResults = []; this.questions = null; this.validationInProgress = false; this.lastValidationResult = null; this.imageLoaded = false; this.viewingTime = 0; this.startViewTime = null; console.log('โœ… ImageModule cleaned up'); } /** * Get module metadata * @returns {Object} - Module information */ getMetadata() { return { name: 'ImageModule', type: 'image', version: '1.0.0', description: 'Visual comprehension exercises with AI-powered image analysis', capabilities: ['image_comprehension', 'visual_analysis', 'description_skills', 'ai_vision', 'ai_feedback'], aiRequired: true, // Highly recommended for image analysis 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(`๐ŸŒ Image languages detected: ${this.languages.userLanguage} -> ${this.languages.targetLanguage}`); } /** * Prepare questions for the image * @private */ async _prepareQuestions(image) { // If image already has questions, use them if (image.questions && image.questions.length > 0) { return image.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', focusArea: q.focusArea || 'general' // general, details, interpretation, description })); } // Generate default visual comprehension questions const defaultQuestions = [ { id: 'description', question: `Describe what you see in this image in detail.`, type: 'open', keywords: ['see', 'image', 'shows', 'contains', 'depicts'], difficulty: 'easy', focusArea: 'description' }, { id: 'details', question: `What specific details or objects can you identify in the image?`, type: 'open', keywords: ['details', 'objects', 'specific', 'identify', 'notice'], difficulty: 'medium', focusArea: 'details' }, { id: 'interpretation', question: `What do you think is happening in this image? What story does it tell?`, type: 'open', keywords: ['happening', 'story', 'context', 'situation', 'interpret'], difficulty: 'hard', focusArea: 'interpretation' } ]; // Limit to configured number of questions return defaultQuestions.slice(0, this.config.questionsPerImage); } /** * Build comprehensive prompt for image comprehension validation * @private */ _buildImageComprehensionPrompt(userAnswer) { const imageTitle = this.currentImage.title || 'Visual Exercise'; const imageDescription = this.currentImage.description || 'No description provided'; const imageUrl = this.currentImage.url || this.currentImage.src || ''; return `You are evaluating visual comprehension for a language learning exercise. CRITICAL: You MUST respond in this EXACT format: [answer]yes/no [explanation]your detailed analysis here IMAGE CONTENT: Title: "${imageTitle}" Description: "${imageDescription}" ${imageUrl ? `Image URL: ${imageUrl}` : 'Image: Available for visual analysis'} QUESTION: ${this.currentQuestion.question} STUDENT RESPONSE: "${userAnswer}" EVALUATION CONTEXT: - Exercise Type: Visual comprehension - Languages: ${this.languages.userLanguage} -> ${this.languages.targetLanguage} - Question Type: ${this.currentQuestion.type} - Question Difficulty: ${this.currentQuestion.difficulty} - Focus Area: ${this.currentQuestion.focusArea} - Question ${this.questionIndex + 1} of ${this.questions.length} - Observation Time: ${this.viewingTime}s EVALUATION CRITERIA: - [answer]yes if the student demonstrates good visual observation and understanding - [answer]no if the response shows poor observation or is unrelated to the image - Focus on VISUAL COMPREHENSION and OBSERVATION SKILLS, not perfect language - Accept different perspectives and interpretations if they show visual understanding - Reward specific details, accurate observations, and thoughtful interpretation - Consider the student's language learning level and cultural context - For description questions: reward comprehensive and accurate descriptions - For detail questions: reward specific and accurate observations - For interpretation questions: reward thoughtful analysis and context understanding [explanation] should provide: 1. What the student observed correctly from the image 2. What important visual elements they might have missed 3. How well their interpretation matches the image content 4. Encouragement and specific suggestions for better visual observation 5. Tips for developing visual comprehension skills 6. Recognition of cultural or contextual insights if present Format: [answer]yes/no [explanation]your comprehensive educational feedback here`; } /** * Parse structured AI response for image 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 image 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(); // Standard scores for image comprehension const result = { score: isCorrect ? 88 : 58, // Good scores for visual comprehension 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, imageComprehension: true, visionAnalysis: true }; console.log(`โœ… AI image comprehension parsed: ${result.answer} - Score: ${result.score}`); return result; } catch (error) { console.error('โŒ Failed to parse AI image comprehension response:', error); console.error('Raw response:', aiResponse); throw new Error(`AI response format invalid: ${error.message}`); } } /** * Perform basic image validation when AI is unavailable * @private */ _performBasicImageValidation(userAnswer) { console.log('๐Ÿ” Performing basic image 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 = 30; // Lower base score for images (much harder without AI vision) if (answerLength > 20) score += 15; // Substantial answer if (answerLength > 50) score += 10; // Detailed answer if (hasKeywords) score += 20; // Contains relevant keywords if (answerLength > 100) score += 10; // Very detailed if (this.viewingTime >= this.config.minViewTime) score += 10; // Good observation time const isCorrect = score >= 60; return { score: Math.min(score, 100), correct: isCorrect, feedback: isCorrect ? "Good visual observation! Your description shows attention to detail and understanding of the image." : "Try to observe the image more carefully and include more specific details about what you see. Look for objects, people, actions, colors, and settings.", timestamp: new Date().toISOString(), provider: 'basic_image_analysis', model: 'keyword_length_analysis', cached: false, mockGenerated: true, imageComprehension: true, limitedAnalysis: true }; } /** * Render the image exercise interface * @private */ async _renderImageExercise() { if (!this.container || !this.currentImage) return; const imageTitle = this.currentImage.title || 'Visual Exercise'; const imageDescription = this.currentImage.description || ''; const imageUrl = this.currentImage.url || this.currentImage.src || ''; this.container.innerHTML = `

๐Ÿ–ผ๏ธ Visual Comprehension

${this.questions?.length || 0} questions ${!this.aiAvailable ? ' โ€ข โš ๏ธ Limited analysis mode' : ' โ€ข ๐Ÿง  AI vision analysis'}

${imageTitle}

Viewing: 0s ${this.config.allowZoom ? '' : ''}
${imageUrl ? ` ${imageTitle} ` : `
๐Ÿ–ผ๏ธ Image placeholder (${imageTitle}) ${imageDescription ? `

${imageDescription}

` : ''}
`}
${imageDescription ? `

๐Ÿ“ Context

${imageDescription}

` : ''}
`; // Add CSS styles this._addStyles(); // Add event listeners this._setupEventListeners(); // Start observation timer after a brief delay setTimeout(() => { this._startViewTimer(); const observeBtn = document.getElementById('observe-more-btn'); if (observeBtn) observeBtn.style.display = 'none'; const startQuestionsBtn = document.getElementById('start-questions-btn'); if (startQuestionsBtn) startQuestionsBtn.style.display = 'inline-block'; }, 2000); } /** * Setup event listeners for image exercise * @private */ _setupEventListeners() { const startQuestionsBtn = document.getElementById('start-questions-btn'); const observeMoreBtn = document.getElementById('observe-more-btn'); const lookAgainBtn = document.getElementById('look-again-btn'); const zoomBtn = document.getElementById('zoom-btn'); const closeOverlay = document.getElementById('close-overlay'); 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-image-btn'); // Navigation buttons if (startQuestionsBtn) { startQuestionsBtn.onclick = () => this._startQuestions(); } if (observeMoreBtn) { observeMoreBtn.onclick = () => this._continueObservation(); } if (lookAgainBtn) { lookAgainBtn.onclick = () => this._showImageViewing(); } // Image interaction if (zoomBtn) { zoomBtn.onclick = this._handleImageZoom; } if (closeOverlay) { closeOverlay.onclick = () => { const overlay = document.getElementById('image-overlay'); if (overlay) overlay.style.display = 'none'; }; } // Handle image load const mainImage = document.getElementById('main-image'); if (mainImage) { mainImage.addEventListener('load', this._handleImageLoad); } // 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._completeImageExercise(); } /** * Handle image load event * @private */ _handleImageLoad() { this.imageLoaded = true; console.log('๐Ÿ–ผ๏ธ Image loaded successfully'); } /** * Handle image zoom functionality * @private */ _handleImageZoom() { const overlay = document.getElementById('image-overlay'); const mainImage = document.getElementById('main-image'); if (!overlay || !mainImage) return; const overlayContent = overlay.querySelector('.overlay-content'); const zoomedImage = mainImage.cloneNode(true); zoomedImage.className = 'zoomed-image'; zoomedImage.id = 'zoomed-image'; // Clear previous content and add zoomed image overlayContent.innerHTML = `
`; overlayContent.querySelector('.zoomed-container').appendChild(zoomedImage); overlay.style.display = 'flex'; // Re-attach close event overlayContent.querySelector('#close-overlay').onclick = () => { overlay.style.display = 'none'; }; } /** * Start view timer * @private */ _startViewTimer() { if (this.startViewTime) return; // Already started this.startViewTime = Date.now(); const timer = document.getElementById('view-timer'); const updateTimer = () => { if (this.startViewTime) { this.viewingTime = Math.floor((Date.now() - this.startViewTime) / 1000); if (timer) timer.textContent = `${this.viewingTime}s`; setTimeout(updateTimer, 1000); } }; updateTimer(); } /** * Stop view timer * @private */ _stopViewTimer() { if (this.startViewTime) { this.viewingTime = Math.floor((Date.now() - this.startViewTime) / 1000); this.startViewTime = null; } } /** * Continue observation phase * @private */ _continueObservation() { // Just hide the button and show start questions const observeBtn = document.getElementById('observe-more-btn'); const startQuestionsBtn = document.getElementById('start-questions-btn'); if (observeBtn) observeBtn.style.display = 'none'; if (startQuestionsBtn) startQuestionsBtn.style.display = 'inline-block'; } /** * Show image viewing phase * @private */ _showImageViewing() { const imageSection = document.getElementById('image-viewer-section'); const questionsSection = document.getElementById('questions-section'); if (imageSection) imageSection.style.display = 'block'; if (questionsSection) questionsSection.style.display = 'none'; // Resume timer if needed if (!this.startViewTime) { this._startViewTimer(); } } /** * Start questions phase * @private */ _startQuestions() { const imageSection = document.getElementById('image-viewer-section'); const questionsSection = document.getElementById('questions-section'); // Keep image visible but minimized if configured if (imageSection) { imageSection.style.display = this.config.showImageDuringQuestions ? 'block' : 'none'; if (this.config.showImageDuringQuestions) { imageSection.classList.add('minimized'); } } if (questionsSection) questionsSection.style.display = 'block'; // Stop continuous timer - we'll track total time this._stopViewTimer(); this._presentCurrentQuestion(); } /** * Present current question * @private */ _presentCurrentQuestion() { if (this.questionIndex >= this.questions.length) { this._showImageResults(); 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.focusArea}
`; // 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 the image and your response...' : '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(), observationTime: this.viewingTime }; // Show result this._showValidationResult(result); // Update status statusDiv.innerHTML = `
${result.correct ? 'โœ…' : '๐Ÿ‘๏ธ'} Analysis complete
`; } catch (error) { console.error('โŒ Image 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-image-btn'); if (!explanationPanel || !explanationContent) return; // Show panel explanationPanel.style.display = 'block'; // Set explanation content explanationContent.innerHTML = `
${result.correct ? 'โœ… Excellent Observation!' : '๐Ÿ‘๏ธ Keep Looking!'} Score: ${result.score}/100
${result.explanation || result.feedback}
${result.imageComprehension ? '
๐Ÿ–ผ๏ธ This analysis evaluates your visual observation skills and image interpretation abilities.
' : ''} ${result.visionAnalysis ? '
๐Ÿค– AI Vision analyzed both the image content and your response for comprehensive feedback.
' : ''} ${this.viewingTime >= this.config.minViewTime ? '
โฑ๏ธ Observation time bonus applied for thorough viewing!
' : ''}
`; // 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 image results * @private */ _showImageResults() { const resultsContainer = document.getElementById('image-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; const avgObservationTime = this.questionResults.length > 0 ? Math.round(this.questionResults.reduce((sum, r) => sum + (r.observationTime || 0), 0) / this.questionResults.length) : 0; let resultClass = 'results-poor'; if (comprehensionRate >= 80) resultClass = 'results-excellent'; else if (comprehensionRate >= 60) resultClass = 'results-good'; const resultsHTML = `

๐Ÿ“Š Visual Comprehension Results

${comprehensionRate}% Comprehension Rate
${correctCount} / ${totalCount} visual observations understood well
${this.viewingTime}s Total Viewing Time
${this.viewingTime >= this.config.minViewTime ? '๐Ÿ‘' : 'โšก'} ${this.viewingTime >= this.config.minViewTime ? 'Thorough observation' : 'Quick observation'}
${this.aiAvailable ? `
๐Ÿค– AI Vision Analysis
` : ''}
${this.questionResults.map((result, index) => `
Q${index + 1} ${result.correct ? 'โœ…' : '๐Ÿ‘๏ธ'} Score: ${result.score}/100 ${this.questions[index]?.focusArea || 'general'}
`).join('')}
`; resultsContainer.innerHTML = resultsHTML; resultsContainer.style.display = 'block'; // Hide other sections if (questionsSection) questionsSection.style.display = 'none'; // Add action listeners document.getElementById('complete-image-btn').onclick = () => this._completeImageExercise(); document.getElementById('review-image-btn').onclick = () => this._reviewImage(); } /** * Complete image exercise * @private */ _completeImageExercise() { // Mark image 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 imageId = this.currentImage.id || this.currentImage.title || 'image_exercise'; const metadata = { comprehensionRate: Math.round(comprehensionRate * 100), questionsAnswered: this.questionResults.length, correctAnswers: correctCount, observationTime: this.viewingTime, sessionId: this.orchestrator?.sessionId || 'unknown', moduleType: 'image', aiAnalysisUsed: this.aiAvailable, visionAnalysisUsed: this.aiAvailable, observationQuality: this.viewingTime >= this.config.minViewTime ? 'thorough' : 'quick' }; this.prerequisiteEngine.markPhraseMastered(imageId, metadata); // Also save to persistent storage if (window.addMasteredItem && this.orchestrator?.bookId && this.orchestrator?.chapterId) { window.addMasteredItem( this.orchestrator.bookId, this.orchestrator.chapterId, 'images', imageId, metadata ); } } // Emit completion event this.orchestrator._eventBus.emit('drs:exerciseCompleted', { moduleType: 'image', results: this.questionResults, progress: this.getProgress() }, 'ImageModule'); } /** * Review image again * @private */ _reviewImage() { this.questionIndex = 0; this.questionResults = []; this.viewingTime = 0; this.startViewTime = null; this._showImageViewing(); const resultsContainer = document.getElementById('image-results'); if (resultsContainer) resultsContainer.style.display = 'none'; // Reset UI elements const viewTimer = document.getElementById('view-timer'); if (viewTimer) viewTimer.textContent = '0s'; const startQuestionsBtn = document.getElementById('start-questions-btn'); if (startQuestionsBtn) startQuestionsBtn.style.display = 'none'; const observeBtn = document.getElementById('observe-more-btn'); if (observeBtn) observeBtn.style.display = 'inline-block'; } /** * Add CSS styles for image exercise * @private */ _addStyles() { if (document.getElementById('image-module-styles')) return; const styles = document.createElement('style'); styles.id = 'image-module-styles'; styles.textContent = ` .image-exercise { max-width: 1000px; margin: 0 auto; padding: 20px; display: grid; gap: 20px; } .exercise-header { text-align: center; margin-bottom: 20px; } .image-info { margin-top: 10px; } .image-meta { color: #666; font-size: 0.9em; } .image-content { display: grid; gap: 20px; } .image-viewer-card, .question-card { background: white; border-radius: 12px; padding: 30px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); } .image-viewer-section.minimized .image-viewer-card { padding: 20px; background: #f8f9fa; border: 2px solid #e9ecef; } .viewer-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 2px solid #eee; } .viewer-header h3 { margin: 0; color: #333; font-size: 1.5em; } .viewer-stats { display: flex; gap: 15px; align-items: center; font-size: 0.9em; color: #666; } .view-time { font-weight: 600; color: #17a2b8; } .image-container { position: relative; margin-bottom: 25px; text-align: center; } .exercise-image { max-width: 100%; max-height: 400px; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); cursor: pointer; transition: transform 0.3s ease; } .exercise-image:hover { transform: scale(1.02); } .image-placeholder { text-align: center; padding: 60px 40px; background: linear-gradient(135deg, #f8f9ff, #e8f4fd); border-radius: 10px; color: #666; font-size: 1.2em; border: 2px dashed #ddd; margin-bottom: 20px; } .placeholder-desc { margin-top: 15px; font-size: 0.9em; color: #888; font-style: italic; } .image-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.9); z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 20px; } .overlay-content { position: relative; max-width: 90vw; max-height: 90vh; display: flex; flex-direction: column; align-items: center; } .close-btn { position: absolute; top: -50px; right: 0; z-index: 1001; background: rgba(255,255,255,0.9); border: 2px solid #ddd; } .zoomed-container { max-width: 100%; max-height: 100%; overflow: auto; } .zoomed-image { max-width: none; max-height: 80vh; border-radius: 10px; } .image-description { margin-top: 20px; padding: 20px; background: linear-gradient(135deg, #f8f9fa, #e9ecef); border-radius: 10px; border-left: 4px solid #6c757d; } .image-description h4 { margin: 0 0 10px 0; color: #333; } .image-description p { margin: 0; line-height: 1.6; color: #555; } .viewing-actions { text-align: center; margin-top: 20px; display: flex; gap: 15px; justify-content: center; flex-wrap: wrap; } .question-progress { margin-bottom: 25px; } .progress-indicator { text-align: center; } .progress-bar { width: 100%; height: 8px; background-color: #e0e0e0; border-radius: 4px; margin-top: 10px; overflow: hidden; } .progress-fill { height: 100%; background: linear-gradient(90deg, #fd79a8, #e84393); transition: width 0.5s ease; } .question-display { margin-bottom: 25px; padding: 20px; background: linear-gradient(135deg, #f8f9ff, #e8f4fd); border-radius: 10px; border-left: 4px solid #fd79a8; } .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; } .focus-general { background: #f3e5f5; color: #7b1fa2; padding: 4px 12px; border-radius: 20px; font-size: 0.85em; font-weight: 500; } .focus-description { background: #e8f5e8; color: #2e7d32; padding: 4px 12px; border-radius: 20px; font-size: 0.85em; font-weight: 500; } .focus-details { background: #fff3e0; color: #f57c00; padding: 4px 12px; border-radius: 20px; font-size: 0.85em; font-weight: 500; } .focus-interpretation { background: #ffebee; color: #c62828; 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: 120px; 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: #fd79a8; } .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 #fd79a8; } .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, .vision-note, .time-bonus { font-size: 0.9em; color: #666; font-style: italic; padding-top: 10px; border-top: 1px solid #eee; margin-top: 10px; } .vision-note { color: #1976d2; } .time-bonus { color: #2e7d32; } .panel-actions { display: flex; gap: 15px; justify-content: center; flex-wrap: wrap; } .image-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; } .observation-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; } .focus-area { 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, #fd79a8, #e84393); color: white; } .btn-primary:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(253, 121, 168, 0.3); } .btn-secondary { background: #6c757d; color: white; } .btn-secondary:hover:not(:disabled) { background: #5a6268; } .btn-outline { background: transparent; border: 2px solid #fd79a8; color: #fd79a8; } .btn-outline:hover:not(:disabled) { background: #fd79a8; 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); } .btn-sm { padding: 8px 16px; font-size: 0.9em; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @media (max-width: 768px) { .image-exercise { padding: 15px; } .image-viewer-card, .question-card, .explanation-panel { padding: 20px; } .exercise-image { max-height: 300px; } .question-text { font-size: 1.2em; } .comprehension-rate { font-size: 2.5em; } .panel-actions, .results-actions { flex-direction: column; } .viewer-header { flex-direction: column; gap: 15px; align-items: flex-start; } .observation-stats { flex-direction: column; gap: 20px; } .question-controls > div:first-child { flex-direction: column; align-items: center; } .viewing-actions { flex-direction: column; } } `; document.head.appendChild(styles); } } export default ImageModule;