- Add AIReportSystem.js for detailed AI response capture and report generation - Add AIReportInterface.js UI component for report access and export - Integrate AI reporting into LLMValidator and SmartPreviewOrchestrator - Add missing modules to Application.js configuration (unifiedDRS, smartPreviewOrchestrator) - Create missing content/chapters/sbs.json for book metadata - Enhance Application.js with debug logging for module loading - Add multi-format export capabilities (text, HTML, JSON) - Implement automatic learning insights extraction from AI feedback - Add session management and performance tracking for AI reports 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1949 lines
68 KiB
JavaScript
1949 lines
68 KiB
JavaScript
/**
|
|
* 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<void>}
|
|
*/
|
|
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<ValidationResult>} - 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 = `
|
|
<div class="image-exercise">
|
|
<div class="exercise-header">
|
|
<h2>🖼️ Visual Comprehension</h2>
|
|
<div class="image-info">
|
|
<span class="image-meta">
|
|
${this.questions?.length || 0} questions
|
|
${!this.aiAvailable ? ' • ⚠️ Limited analysis mode' : ' • 🧠 AI vision analysis'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="image-content">
|
|
<div class="image-viewer-section" id="image-viewer-section">
|
|
<div class="image-viewer-card">
|
|
<div class="viewer-header">
|
|
<h3>${imageTitle}</h3>
|
|
<div class="viewer-stats">
|
|
<span class="view-time">Viewing: <span id="view-timer">0s</span></span>
|
|
${this.config.allowZoom ? '<button id="zoom-btn" class="btn btn-outline btn-sm">🔍 Zoom</button>' : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="image-container" id="image-container">
|
|
${imageUrl ? `
|
|
<img
|
|
id="main-image"
|
|
src="${imageUrl}"
|
|
alt="${imageTitle}"
|
|
class="exercise-image"
|
|
onload="this.closest('.image-exercise').querySelector('#image-viewer-section')._handleImageLoad()"
|
|
/>
|
|
` : `
|
|
<div class="image-placeholder">
|
|
🖼️ Image placeholder (${imageTitle})
|
|
${imageDescription ? `<p class="placeholder-desc">${imageDescription}</p>` : ''}
|
|
</div>
|
|
`}
|
|
|
|
<div class="image-overlay" id="image-overlay" style="display: none;">
|
|
<div class="overlay-content">
|
|
<button id="close-overlay" class="btn btn-outline">✕ Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
${imageDescription ? `
|
|
<div class="image-description">
|
|
<h4>📝 Context</h4>
|
|
<p>${imageDescription}</p>
|
|
</div>
|
|
` : ''}
|
|
|
|
<div class="viewing-actions">
|
|
<button id="start-questions-btn" class="btn btn-success" style="display: none;">
|
|
<span class="btn-icon">❓</span>
|
|
<span class="btn-text">Start Questions</span>
|
|
</button>
|
|
<button id="observe-more-btn" class="btn btn-primary">
|
|
<span class="btn-icon">👁️</span>
|
|
<span class="btn-text">Observe Image</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="questions-section" id="questions-section" style="display: none;">
|
|
<div class="question-card">
|
|
<div class="question-progress">
|
|
<div class="progress-indicator">
|
|
<span id="question-counter">Question 1 of ${this.questions?.length || 0}</span>
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" id="progress-fill" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="question-content" id="question-content">
|
|
<!-- Question content will be populated dynamically -->
|
|
</div>
|
|
|
|
<div class="answer-input-section">
|
|
<label for="answer-input">Your Answer:</label>
|
|
<textarea
|
|
id="answer-input"
|
|
placeholder="Describe what you observe in the image..."
|
|
rows="5"
|
|
autocomplete="off"
|
|
></textarea>
|
|
</div>
|
|
|
|
<div class="question-controls">
|
|
<button id="look-again-btn" class="btn btn-outline">
|
|
<span class="btn-icon">👁️</span>
|
|
<span class="btn-text">Look at Image Again</span>
|
|
</button>
|
|
<button id="validate-answer-btn" class="btn btn-primary" disabled>
|
|
<span class="btn-icon">${this.aiAvailable ? '🧠' : '🔍'}</span>
|
|
<span class="btn-text">${this.aiAvailable ? 'Validate with AI Vision' : 'Validate Answer'}</span>
|
|
</button>
|
|
<div id="validation-status" class="validation-status"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="explanation-panel" id="explanation-panel" style="display: none;">
|
|
<div class="panel-header">
|
|
<h3>${this.aiAvailable ? '🤖 AI Vision Analysis' : '🔍 Analysis'}</h3>
|
|
<span class="analysis-model">${this.aiAvailable ? this.config.model + ' (Vision)' : 'Basic Analysis'}</span>
|
|
</div>
|
|
<div class="explanation-content" id="explanation-content">
|
|
<!-- Analysis will appear here -->
|
|
</div>
|
|
<div class="panel-actions">
|
|
<button id="next-question-btn" class="btn btn-primary" style="display: none;">
|
|
Next Question
|
|
</button>
|
|
<button id="retry-answer-btn" class="btn btn-secondary" style="display: none;">
|
|
Try Again
|
|
</button>
|
|
<button id="finish-image-btn" class="btn btn-success" style="display: none;">
|
|
Complete Image Exercise
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="image-results" id="image-results" style="display: none;">
|
|
<!-- Final results will be shown here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 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 = `
|
|
<button id="close-overlay" class="btn btn-outline close-btn">✕ Close</button>
|
|
<div class="zoomed-container"></div>
|
|
`;
|
|
|
|
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 = `
|
|
<div class="question-display">
|
|
<div class="question-text">${this.currentQuestion.question}</div>
|
|
<div class="question-meta">
|
|
<span class="question-type">${this.currentQuestion.type}</span>
|
|
<span class="question-difficulty difficulty-${this.currentQuestion.difficulty}">
|
|
${this.currentQuestion.difficulty}
|
|
</span>
|
|
<span class="focus-area focus-${this.currentQuestion.focusArea}">
|
|
${this.currentQuestion.focusArea}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 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 = `
|
|
<div class="status-loading">
|
|
<div class="loading-spinner">🔍</div>
|
|
<span>${this.aiAvailable ? 'AI is analyzing the image and your response...' : 'Analyzing your answer...'}</span>
|
|
</div>
|
|
`;
|
|
|
|
// 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 = `
|
|
<div class="status-complete">
|
|
<span class="result-icon">${result.correct ? '✅' : '👁️'}</span>
|
|
<span>Analysis complete</span>
|
|
</div>
|
|
`;
|
|
|
|
} catch (error) {
|
|
console.error('❌ Image validation error:', error);
|
|
|
|
// Show error status
|
|
statusDiv.innerHTML = `
|
|
<div class="status-error">
|
|
<span class="error-icon">⚠️</span>
|
|
<span>Error: ${error.message}</span>
|
|
</div>
|
|
`;
|
|
|
|
// 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 = `
|
|
<div class="explanation-result ${result.correct ? 'correct' : 'needs-improvement'}">
|
|
<div class="result-header">
|
|
<span class="result-indicator">
|
|
${result.correct ? '✅ Excellent Observation!' : '👁️ Keep Looking!'}
|
|
</span>
|
|
<span class="comprehension-score">Score: ${result.score}/100</span>
|
|
</div>
|
|
<div class="explanation-text">${result.explanation || result.feedback}</div>
|
|
${result.imageComprehension ? '<div class="analysis-note">🖼️ This analysis evaluates your visual observation skills and image interpretation abilities.</div>' : ''}
|
|
${result.visionAnalysis ? '<div class="vision-note">🤖 AI Vision analyzed both the image content and your response for comprehensive feedback.</div>' : ''}
|
|
${this.viewingTime >= this.config.minViewTime ? '<div class="time-bonus">⏱️ Observation time bonus applied for thorough viewing!</div>' : ''}
|
|
</div>
|
|
`;
|
|
|
|
// 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 = `
|
|
<div class="image-results-content ${resultClass}">
|
|
<h3>📊 Visual Comprehension Results</h3>
|
|
<div class="results-summary">
|
|
<div class="comprehension-display">
|
|
<span class="comprehension-rate">${comprehensionRate}%</span>
|
|
<span class="comprehension-label">Comprehension Rate</span>
|
|
</div>
|
|
<div class="questions-summary">
|
|
${correctCount} / ${totalCount} visual observations understood well
|
|
</div>
|
|
<div class="observation-stats">
|
|
<div class="stat-item">
|
|
<span class="stat-value">${this.viewingTime}s</span>
|
|
<span class="stat-label">Total Viewing Time</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-value">${this.viewingTime >= this.config.minViewTime ? '👍' : '⚡'}</span>
|
|
<span class="stat-label">${this.viewingTime >= this.config.minViewTime ? 'Thorough observation' : 'Quick observation'}</span>
|
|
</div>
|
|
${this.aiAvailable ? `
|
|
<div class="stat-item">
|
|
<span class="stat-value">🤖</span>
|
|
<span class="stat-label">AI Vision Analysis</span>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="question-breakdown">
|
|
${this.questionResults.map((result, index) => `
|
|
<div class="question-result ${result.correct ? 'understood' : 'needs-work'}">
|
|
<div class="question-summary">
|
|
<span class="question-num">Q${index + 1}</span>
|
|
<span class="comprehension-icon">${result.correct ? '✅' : '👁️'}</span>
|
|
<span class="score">Score: ${result.score}/100</span>
|
|
<span class="focus-area">${this.questions[index]?.focusArea || 'general'}</span>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
|
|
<div class="results-actions">
|
|
<button id="complete-image-btn" class="btn btn-primary">Continue to Next Exercise</button>
|
|
<button id="review-image-btn" class="btn btn-outline">Review Image Again</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
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; |