Class_generator/src/DRS/exercise-modules/TextModule.js
StillHammer 05142bdfbc Implement comprehensive AI text report/export system
- 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>
2025-09-26 21:24:13 +08:00

1510 lines
52 KiB
JavaScript

/**
* TextModule - Reading comprehension exercises with AI validation
* Handles text passages with comprehension questions and contextual understanding
*/
import ExerciseModuleInterface from '../interfaces/ExerciseModuleInterface.js';
class TextModule extends ExerciseModuleInterface {
constructor(orchestrator, llmValidator, prerequisiteEngine, contextMemory) {
super();
// Validate dependencies
if (!orchestrator || !llmValidator || !prerequisiteEngine || !contextMemory) {
throw new Error('TextModule 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.currentText = null;
this.currentQuestion = null;
this.questionIndex = 0;
this.questionResults = [];
this.validationInProgress = false;
this.lastValidationResult = null;
this.aiAvailable = false;
// Configuration
this.config = {
requiredProvider: 'openai', // Prefer OpenAI for text analysis
model: 'gpt-4o-mini',
temperature: 0.2, // Slightly more creative for text comprehension
maxTokens: 800,
timeout: 45000, // Longer timeout for complex text analysis
questionsPerText: 3, // Default number of questions per text
showTextDuringQuestions: true, // Keep text visible during questions
allowReread: true // Allow re-reading the text
};
// 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._handleRereadText = this._handleRereadText.bind(this);
}
async init() {
if (this.initialized) return;
console.log('📖 Initializing TextModule...');
// Test AI connectivity - recommended for text comprehension
try {
const testResult = await this.llmValidator.testConnectivity();
if (testResult.success) {
console.log(`✅ AI connectivity verified for text analysis (providers: ${testResult.availableProviders?.join(', ') || testResult.provider})`);
this.aiAvailable = true;
} else {
console.warn('⚠️ AI connection failed - text comprehension will be limited:', testResult.error);
this.aiAvailable = false;
}
} catch (error) {
console.warn('⚠️ AI connectivity test failed - using basic text analysis:', error.message);
this.aiAvailable = false;
}
this.initialized = true;
console.log(`✅ TextModule initialized (AI: ${this.aiAvailable ? 'available for deep analysis' : 'limited - basic analysis only'})`);
}
/**
* Check if module can run with current prerequisites
* @param {Array} prerequisites - List of learned vocabulary/concepts
* @param {Object} chapterContent - Full chapter content
* @returns {boolean} - True if module can run
*/
canRun(prerequisites, chapterContent) {
// Check if there are texts and if prerequisites allow them
const texts = chapterContent?.texts || [];
if (texts.length === 0) return false;
// Find texts that can be unlocked with current prerequisites
const availableTexts = texts.filter(text => {
const unlockStatus = this.prerequisiteEngine.canUnlock('text', text);
return unlockStatus.canUnlock;
});
return availableTexts.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('TextModule must be initialized before use');
}
this.container = container;
this.currentExerciseData = exerciseData;
this.currentText = exerciseData.text;
this.questionIndex = 0;
this.questionResults = [];
this.validationInProgress = false;
this.lastValidationResult = null;
// Detect languages from chapter content
this._detectLanguages(exerciseData);
// Generate or extract questions
this.questions = await this._prepareQuestions(this.currentText);
console.log(`📖 Presenting text comprehension: "${this.currentText.title || 'Reading Exercise'}" (${this.questions.length} questions)`);
// Render initial UI
await this._renderTextExercise();
// Start with text reading phase
this._showTextReading();
}
/**
* Validate user input with AI for deep text 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.currentText || !this.currentQuestion) {
throw new Error('No text or question loaded for validation');
}
console.log(`📖 Validating text comprehension answer for question ${this.questionIndex + 1}`);
// Build comprehensive prompt for text comprehension
const prompt = this._buildTextComprehensionPrompt(userInput);
try {
// Use AI validation with structured response
const aiResponse = await this.llmValidator.iaEngine.validateEducationalContent(prompt, {
preferredProvider: this.config.requiredProvider,
temperature: this.config.temperature,
maxTokens: this.config.maxTokens,
timeout: this.config.timeout,
systemPrompt: `You are an expert reading comprehension evaluator. Focus on understanding and critical thinking, not just literal recall. ALWAYS respond in the exact format: [answer]yes/no [explanation]your detailed analysis here`
});
// Parse structured response
const parsedResult = this._parseStructuredResponse(aiResponse);
// Record interaction in context memory
this.contextMemory.recordInteraction({
type: 'text',
subtype: 'comprehension',
content: {
text: this.currentText,
question: this.currentQuestion,
textTitle: this.currentText.title || 'Reading Exercise',
textLength: this.currentText.content?.length || 0
},
userResponse: userInput.trim(),
validation: parsedResult,
context: {
languages: this.languages,
questionIndex: this.questionIndex,
totalQuestions: this.questions.length
}
});
return parsedResult;
} catch (error) {
console.error('❌ AI text comprehension validation failed:', error);
// Fallback to basic keyword analysis if AI fails
if (!this.aiAvailable) {
return this._performBasicTextValidation(userInput);
}
throw new Error(`Text 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: 'text',
textTitle: this.currentText?.title || 'Reading 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,
aiAnalysisAvailable: this.aiAvailable
};
}
/**
* Clean up and prepare for unloading
*/
cleanup() {
console.log('🧹 Cleaning up TextModule...');
// Remove event listeners
if (this.container) {
this.container.innerHTML = '';
}
// Reset state
this.container = null;
this.currentExerciseData = null;
this.currentText = null;
this.currentQuestion = null;
this.questionIndex = 0;
this.questionResults = [];
this.questions = null;
this.validationInProgress = false;
this.lastValidationResult = null;
console.log('✅ TextModule cleaned up');
}
/**
* Get module metadata
* @returns {Object} - Module information
*/
getMetadata() {
return {
name: 'TextModule',
type: 'text',
version: '1.0.0',
description: 'Reading comprehension exercises with AI-powered text analysis',
capabilities: ['text_comprehension', 'critical_thinking', 'contextual_analysis', 'ai_feedback'],
aiRequired: false, // Can work without AI but limited
config: this.config
};
}
// Private Methods
/**
* Detect languages from exercise data
* @private
*/
_detectLanguages(exerciseData) {
const chapterContent = this.currentExerciseData?.chapterContent;
if (chapterContent?.metadata?.userLanguage) {
this.languages.userLanguage = chapterContent.metadata.userLanguage;
}
if (chapterContent?.metadata?.targetLanguage) {
this.languages.targetLanguage = chapterContent.metadata.targetLanguage;
}
console.log(`🌍 Text languages detected: ${this.languages.userLanguage} -> ${this.languages.targetLanguage}`);
}
/**
* Prepare questions for the text
* @private
*/
async _prepareQuestions(text) {
// If text already has questions, use them
if (text.questions && text.questions.length > 0) {
return text.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'
}));
}
// Generate default comprehension questions
const defaultQuestions = [
{
id: 'main_idea',
question: `What is the main idea of this text?`,
type: 'open',
keywords: ['main', 'central', 'primary', 'key'],
difficulty: 'medium'
},
{
id: 'details',
question: `What are the key details mentioned in the text?`,
type: 'open',
keywords: ['details', 'specific', 'mentioned'],
difficulty: 'easy'
},
{
id: 'analysis',
question: `What can you infer or conclude from this text?`,
type: 'open',
keywords: ['infer', 'conclude', 'imply', 'suggest'],
difficulty: 'hard'
}
];
// Limit to configured number of questions
return defaultQuestions.slice(0, this.config.questionsPerText);
}
/**
* Build comprehensive prompt for text comprehension validation
* @private
*/
_buildTextComprehensionPrompt(userAnswer) {
const textContent = this.currentText.content || this.currentText.text || '';
const textTitle = this.currentText.title || 'Reading Text';
return `You are evaluating reading comprehension for a language learning exercise.
CRITICAL: You MUST respond in this EXACT format: [answer]yes/no [explanation]your detailed analysis here
TEXT PASSAGE:
Title: "${textTitle}"
Content: "${textContent}"
QUESTION: ${this.currentQuestion.question}
STUDENT RESPONSE: "${userAnswer}"
EVALUATION CONTEXT:
- Exercise Type: Reading comprehension
- Languages: ${this.languages.userLanguage} -> ${this.languages.targetLanguage}
- Question Type: ${this.currentQuestion.type}
- Question Difficulty: ${this.currentQuestion.difficulty}
- Question ${this.questionIndex + 1} of ${this.questions.length}
EVALUATION CRITERIA:
- [answer]yes if the student demonstrates understanding of the text in relation to the question
- [answer]no if the response shows lack of comprehension or is unrelated
- Focus on COMPREHENSION and UNDERSTANDING, not perfect language
- Accept paraphrasing and different perspectives if they show understanding
- Consider cultural context and language learning level
[explanation] should provide:
1. What the student understood correctly
2. What they might have missed or misunderstood
3. Encouragement and specific improvement suggestions
4. Connection to the broader text meaning
Format: [answer]yes/no [explanation]your comprehensive educational feedback here`;
}
/**
* Parse structured AI response for text 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 text comprehension response:', responseText.substring(0, 150) + '...');
// Extract [answer] - case insensitive
const answerMatch = responseText.match(/\[answer\](yes|no)/i);
if (!answerMatch) {
throw new Error('AI response missing [answer] format');
}
// Extract [explanation] - multiline support
const explanationMatch = responseText.match(/\[explanation\](.+)/s);
if (!explanationMatch) {
throw new Error('AI response missing [explanation] format');
}
const isCorrect = answerMatch[1].toLowerCase() === 'yes';
const explanation = explanationMatch[1].trim();
// Higher scores for text comprehension to encourage reading
const result = {
score: isCorrect ? 90 : 60, // More generous scoring for 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,
textComprehension: true
};
console.log(`✅ AI text comprehension parsed: ${result.answer} - Score: ${result.score}`);
return result;
} catch (error) {
console.error('❌ Failed to parse AI text comprehension response:', error);
console.error('Raw response:', aiResponse);
throw new Error(`AI response format invalid: ${error.message}`);
}
}
/**
* Perform basic text validation when AI is unavailable
* @private
*/
_performBasicTextValidation(userAnswer) {
console.log('🔍 Performing basic text 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 = 40; // Base score
if (answerLength > 20) score += 20; // Substantial answer
if (answerLength > 50) score += 10; // Detailed answer
if (hasKeywords) score += 20; // Contains relevant keywords
if (answerLength > 100) score += 10; // Very detailed
const isCorrect = score >= 70;
return {
score: Math.min(score, 100),
correct: isCorrect,
feedback: isCorrect
? "Good comprehension demonstrated! Your answer shows understanding of the text."
: "Your answer could be more detailed. Try to include more specific information from the text.",
timestamp: new Date().toISOString(),
provider: 'basic_text_analysis',
model: 'keyword_length_analysis',
cached: false,
mockGenerated: true,
textComprehension: true
};
}
/**
* Render the text exercise interface
* @private
*/
async _renderTextExercise() {
if (!this.container || !this.currentText) return;
const textTitle = this.currentText.title || 'Reading Exercise';
const textContent = this.currentText.content || this.currentText.text || '';
const wordCount = textContent.split(/\s+/).length;
this.container.innerHTML = `
<div class="text-exercise">
<div class="exercise-header">
<h2>📖 Reading Comprehension</h2>
<div class="text-info">
<span class="text-meta">
${this.questions?.length || 0} questions • ~${wordCount} words
${!this.aiAvailable ? ' • ⚠️ Limited analysis mode' : ' • 🧠 AI analysis'}
</span>
</div>
</div>
<div class="text-content">
<div class="text-passage-section" id="text-passage-section">
<div class="text-passage-card">
<div class="passage-header">
<h3>${textTitle}</h3>
<button id="reread-btn" class="btn btn-outline btn-sm" style="display: none;">
<span class="btn-icon">👁️</span>
<span class="btn-text">Re-read Text</span>
</button>
</div>
<div class="passage-content" id="passage-content">
${this._formatTextContent(textContent)}
</div>
<div class="reading-actions">
<button id="start-questions-btn" class="btn btn-primary">
<span class="btn-icon">❓</span>
<span class="btn-text">Start Questions</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="Write your answer here... Be specific and refer to the text."
rows="4"
autocomplete="off"
></textarea>
</div>
<div class="question-controls">
<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' : '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 Analysis' : '🔍 Analysis'}</h3>
<span class="analysis-model">${this.aiAvailable ? this.config.model : '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-text-btn" class="btn btn-success" style="display: none;">
Complete Text Exercise
</button>
</div>
</div>
<div class="text-results" id="text-results" style="display: none;">
<!-- Final results will be shown here -->
</div>
</div>
</div>
`;
// Add CSS styles
this._addStyles();
// Add event listeners
this._setupEventListeners();
}
/**
* Format text content with paragraphs and line breaks
* @private
*/
_formatTextContent(content) {
if (!content) return '';
// Split into paragraphs and format
return content
.split('\n\n')
.map(paragraph => `<p>${paragraph.replace(/\n/g, '<br>')}</p>`)
.join('');
}
/**
* Setup event listeners for text exercise
* @private
*/
_setupEventListeners() {
const startQuestionsBtn = document.getElementById('start-questions-btn');
const rereadBtn = document.getElementById('reread-btn');
const answerInput = document.getElementById('answer-input');
const validateBtn = document.getElementById('validate-answer-btn');
const retryBtn = document.getElementById('retry-answer-btn');
const nextBtn = document.getElementById('next-question-btn');
const finishBtn = document.getElementById('finish-text-btn');
// Start questions button
if (startQuestionsBtn) {
startQuestionsBtn.onclick = () => this._startQuestions();
}
// Re-read text button
if (rereadBtn) {
rereadBtn.onclick = this._handleRereadText;
}
// 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._completeTextExercise();
}
/**
* Show text reading phase
* @private
*/
_showTextReading() {
const textSection = document.getElementById('text-passage-section');
const questionsSection = document.getElementById('questions-section');
if (textSection) textSection.style.display = 'block';
if (questionsSection) questionsSection.style.display = 'none';
}
/**
* Start questions phase
* @private
*/
_startQuestions() {
const textSection = document.getElementById('text-passage-section');
const questionsSection = document.getElementById('questions-section');
const rereadBtn = document.getElementById('reread-btn');
if (textSection) {
textSection.style.display = this.config.showTextDuringQuestions ? 'block' : 'none';
}
if (questionsSection) questionsSection.style.display = 'block';
if (rereadBtn) rereadBtn.style.display = 'inline-block';
this._presentCurrentQuestion();
}
/**
* Present current question
* @private
*/
_presentCurrentQuestion() {
if (this.questionIndex >= this.questions.length) {
this._showTextResults();
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>
</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 your comprehension...' : '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()
};
// 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('❌ Text 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-text-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 ? '✅ Good Comprehension!' : '📚 Keep Learning!'}
</span>
<span class="comprehension-score">Score: ${result.score}/100</span>
</div>
<div class="explanation-text">${result.explanation || result.feedback}</div>
${result.textComprehension ? '<div class="analysis-note">💡 This analysis focuses on your understanding of the text\'s meaning and context.</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();
}
/**
* Handle re-read text
* @private
*/
_handleRereadText() {
const textSection = document.getElementById('text-passage-section');
if (textSection) {
textSection.style.display = 'block';
textSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
/**
* 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 text results
* @private
*/
_showTextResults() {
const resultsContainer = document.getElementById('text-results');
const questionsSection = document.getElementById('questions-section');
if (!resultsContainer) return;
const correctCount = this.questionResults.filter(result => result.correct).length;
const totalCount = this.questionResults.length;
const comprehensionRate = totalCount > 0 ? Math.round((correctCount / totalCount) * 100) : 0;
let resultClass = 'results-poor';
if (comprehensionRate >= 80) resultClass = 'results-excellent';
else if (comprehensionRate >= 60) resultClass = 'results-good';
const resultsHTML = `
<div class="text-results-content ${resultClass}">
<h3>📊 Reading 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} questions understood well
</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>
</div>
</div>
`).join('')}
</div>
<div class="results-actions">
<button id="complete-text-btn" class="btn btn-primary">Continue to Next Exercise</button>
<button id="review-text-btn" class="btn btn-outline">Review Text 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-text-btn').onclick = () => this._completeTextExercise();
document.getElementById('review-text-btn').onclick = () => this._reviewText();
}
/**
* Complete text exercise
* @private
*/
_completeTextExercise() {
// Mark text 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 textId = this.currentText.id || this.currentText.title || 'text_exercise';
const metadata = {
comprehensionRate: Math.round(comprehensionRate * 100),
questionsAnswered: this.questionResults.length,
correctAnswers: correctCount,
sessionId: this.orchestrator?.sessionId || 'unknown',
moduleType: 'text',
aiAnalysisUsed: this.aiAvailable
};
this.prerequisiteEngine.markPhraseMastered(textId, metadata);
// Also save to persistent storage
if (window.addMasteredItem && this.orchestrator?.bookId && this.orchestrator?.chapterId) {
window.addMasteredItem(
this.orchestrator.bookId,
this.orchestrator.chapterId,
'texts',
textId,
metadata
);
}
}
// Emit completion event
this.orchestrator._eventBus.emit('drs:exerciseCompleted', {
moduleType: 'text',
results: this.questionResults,
progress: this.getProgress()
}, 'TextModule');
}
/**
* Review text again
* @private
*/
_reviewText() {
this.questionIndex = 0;
this.questionResults = [];
this._showTextReading();
const resultsContainer = document.getElementById('text-results');
if (resultsContainer) resultsContainer.style.display = 'none';
}
/**
* Add CSS styles for text exercise
* @private
*/
_addStyles() {
if (document.getElementById('text-module-styles')) return;
const styles = document.createElement('style');
styles.id = 'text-module-styles';
styles.textContent = `
.text-exercise {
max-width: 900px;
margin: 0 auto;
padding: 20px;
display: grid;
gap: 20px;
}
.exercise-header {
text-align: center;
margin-bottom: 20px;
}
.text-info {
margin-top: 10px;
}
.text-meta {
color: #666;
font-size: 0.9em;
}
.text-content {
display: grid;
gap: 20px;
}
.text-passage-card, .question-card {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.passage-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #eee;
}
.passage-header h3 {
margin: 0;
color: #333;
font-size: 1.5em;
}
.passage-content {
line-height: 1.8;
font-size: 1.1em;
color: #444;
margin-bottom: 30px;
max-height: 400px;
overflow-y: auto;
}
.passage-content p {
margin-bottom: 1.2em;
}
.reading-actions {
text-align: center;
}
.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, #667eea, #764ba2);
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 #667eea;
}
.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;
}
.question-type {
background: #e3f2fd;
color: #1976d2;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85em;
font-weight: 500;
}
.difficulty-easy {
background: #e8f5e8;
color: #2e7d32;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85em;
font-weight: 500;
}
.difficulty-medium {
background: #fff3e0;
color: #f57c00;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85em;
font-weight: 500;
}
.difficulty-hard {
background: #ffebee;
color: #c62828;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85em;
font-weight: 500;
}
.answer-input-section {
margin-bottom: 20px;
}
.answer-input-section label {
display: block;
margin-bottom: 10px;
font-weight: 600;
color: #555;
}
.answer-input-section textarea {
width: 100%;
padding: 15px;
font-size: 1.05em;
border: 2px solid #ddd;
border-radius: 8px;
resize: vertical;
min-height: 100px;
box-sizing: border-box;
transition: border-color 0.3s ease;
font-family: inherit;
line-height: 1.5;
}
.answer-input-section textarea:focus {
outline: none;
border-color: #667eea;
}
.question-controls {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.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 #667eea;
}
.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 {
font-size: 0.9em;
color: #666;
font-style: italic;
padding-top: 10px;
border-top: 1px solid #eee;
}
.panel-actions {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.text-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;
}
.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;
}
.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, #667eea, #764ba2);
color: white;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover:not(:disabled) {
background: #5a6268;
}
.btn-outline {
background: transparent;
border: 2px solid #667eea;
color: #667eea;
}
.btn-outline:hover:not(:disabled) {
background: #667eea;
color: white;
}
.btn-success {
background: linear-gradient(135deg, #4caf50, #45a049);
color: white;
}
.btn-success:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(76, 175, 80, 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) {
.text-exercise {
padding: 15px;
}
.text-passage-card, .question-card, .explanation-panel {
padding: 20px;
}
.question-text {
font-size: 1.2em;
}
.comprehension-rate {
font-size: 2.5em;
}
.panel-actions, .results-actions {
flex-direction: column;
}
.passage-header {
flex-direction: column;
gap: 15px;
align-items: flex-start;
}
}
`;
document.head.appendChild(styles);
}
}
export default TextModule;