- 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>
1510 lines
52 KiB
JavaScript
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; |