MAJOR ARCHITECTURE UPDATE - C++ Style Interface Enforcement 🔒 **Strict Interface System**: - Created DRSExerciseInterface (10 required methods) - Created ProgressSystemInterface (17 required methods) - Updated ImplementationValidator with 3-phase validation - Red screen errors for missing implementations 📚 **11/11 Exercise Modules Implemented**: ✅ VocabularyModule - Local flashcard validation ✅ TextAnalysisModule - AI text comprehension ✅ GrammarAnalysisModule - AI grammar correction ✅ TranslationModule - AI translation validation ✅ OpenResponseModule - AI open-ended responses ✅ PhraseModule - Phrase comprehension ✅ AudioModule - Audio listening exercises ✅ ImageModule - Visual comprehension ✅ GrammarModule - Grammar exercises ✅ TextModule - Reading comprehension ✅ WordDiscoveryModule - Vocabulary introduction 🎯 **Required Methods (All Modules)**: - Lifecycle: init(), render(), destroy() - Exercise: validate(), getResults(), handleUserInput() - Progress: markCompleted(), getProgress() - Metadata: getExerciseType(), getExerciseConfig() 📋 **Documentation**: - Updated CLAUDE.md with complete interface hierarchy - Created DRS_IMPLEMENTATION_PLAN.md (roadmap) - Documented enforcement rules and patterns 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2125 lines
75 KiB
JavaScript
2125 lines
75 KiB
JavaScript
/**
|
|
* GrammarModule - Grammar exercises with AI validation
|
|
* Handles grammar rules, sentence construction, and correction exercises
|
|
*/
|
|
|
|
import DRSExerciseInterface from '../interfaces/DRSExerciseInterface.js';
|
|
|
|
class GrammarModule extends DRSExerciseInterface {
|
|
constructor(orchestrator, llmValidator, prerequisiteEngine, contextMemory) {
|
|
super('GrammarModule');
|
|
|
|
// Validate dependencies
|
|
if (!orchestrator || !llmValidator || !prerequisiteEngine || !contextMemory) {
|
|
throw new Error('GrammarModule 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.currentGrammarRule = null;
|
|
this.currentExercise = null;
|
|
this.exerciseIndex = 0;
|
|
this.exerciseResults = [];
|
|
this.validationInProgress = false;
|
|
this.lastValidationResult = null;
|
|
this.aiAvailable = false;
|
|
this.hintUsed = false;
|
|
this.attempts = 0;
|
|
|
|
// Configuration
|
|
this.config = {
|
|
requiredProvider: 'openai', // Prefer OpenAI for grammar analysis
|
|
model: 'gpt-4o-mini',
|
|
temperature: 0.1, // Lower temperature for grammar accuracy
|
|
maxTokens: 600,
|
|
timeout: 30000, // Standard timeout for grammar
|
|
exercisesPerRule: 5, // Default number of exercises per grammar rule
|
|
maxAttempts: 3, // Maximum attempts per exercise
|
|
showHints: true, // Allow hints for grammar rules
|
|
showExplanations: true // Show rule explanations
|
|
};
|
|
|
|
// Languages configuration
|
|
this.languages = {
|
|
userLanguage: 'English',
|
|
targetLanguage: 'French'
|
|
};
|
|
|
|
// Grammar exercise types
|
|
this.exerciseTypes = {
|
|
'fill_blank': 'Fill in the blank',
|
|
'correction': 'Error correction',
|
|
'transformation': 'Sentence transformation',
|
|
'multiple_choice': 'Multiple choice',
|
|
'conjugation': 'Verb conjugation',
|
|
'construction': 'Sentence construction'
|
|
};
|
|
|
|
// Bind methods
|
|
this._handleUserInput = this._handleUserInput.bind(this);
|
|
this._handleNextExercise = this._handleNextExercise.bind(this);
|
|
this._handleRetry = this._handleRetry.bind(this);
|
|
this._handleShowHint = this._handleShowHint.bind(this);
|
|
this._handleShowRule = this._handleShowRule.bind(this);
|
|
}
|
|
|
|
async init() {
|
|
if (this.initialized) return;
|
|
|
|
console.log('📚 Initializing GrammarModule...');
|
|
|
|
// Test AI connectivity - highly recommended for grammar
|
|
try {
|
|
const testResult = await this.llmValidator.testConnectivity();
|
|
if (testResult.success) {
|
|
console.log(`✅ AI connectivity verified for grammar analysis (providers: ${testResult.availableProviders?.join(', ') || testResult.provider})`);
|
|
this.aiAvailable = true;
|
|
} else {
|
|
console.warn('⚠️ AI connection failed - grammar validation will be limited:', testResult.error);
|
|
this.aiAvailable = false;
|
|
}
|
|
} catch (error) {
|
|
console.warn('⚠️ AI connectivity test failed - using basic grammar validation:', error.message);
|
|
this.aiAvailable = false;
|
|
}
|
|
|
|
this.initialized = true;
|
|
console.log(`✅ GrammarModule initialized (AI: ${this.aiAvailable ? 'available for deep analysis' : 'limited - basic validation 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 grammar rules and if prerequisites allow them
|
|
const grammarRules = chapterContent?.grammar || [];
|
|
if (grammarRules.length === 0) return false;
|
|
|
|
// Find grammar rules that can be unlocked with current prerequisites
|
|
const availableRules = grammarRules.filter(rule => {
|
|
const unlockStatus = this.prerequisiteEngine.canUnlock('grammar', rule);
|
|
return unlockStatus.canUnlock;
|
|
});
|
|
|
|
return availableRules.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('GrammarModule must be initialized before use');
|
|
}
|
|
|
|
this.container = container;
|
|
this.currentExerciseData = exerciseData;
|
|
this.currentGrammarRule = exerciseData.grammar;
|
|
this.exerciseIndex = 0;
|
|
this.exerciseResults = [];
|
|
this.validationInProgress = false;
|
|
this.lastValidationResult = null;
|
|
this.hintUsed = false;
|
|
this.attempts = 0;
|
|
|
|
// Detect languages from chapter content
|
|
this._detectLanguages(exerciseData);
|
|
|
|
// Generate or extract exercises
|
|
this.exercises = await this._prepareExercises(this.currentGrammarRule);
|
|
|
|
console.log(`📚 Presenting grammar exercises: "${this.currentGrammarRule.title || 'Grammar Practice'}" (${this.exercises.length} exercises)`);
|
|
|
|
// Render initial UI
|
|
await this._renderGrammarExercise();
|
|
|
|
// Start with rule explanation
|
|
this._showRuleExplanation();
|
|
}
|
|
|
|
/**
|
|
* Validate user input with AI for deep grammar analysis
|
|
* @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.currentGrammarRule || !this.currentExercise) {
|
|
throw new Error('No grammar rule or exercise loaded for validation');
|
|
}
|
|
|
|
console.log(`📚 Validating grammar answer for exercise ${this.exerciseIndex + 1}`);
|
|
|
|
// Build comprehensive prompt for grammar validation
|
|
const prompt = this._buildGrammarPrompt(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 grammar teacher. Focus on grammatical accuracy, rule application, and language structure. ALWAYS respond in the exact format: [answer]yes/no [explanation]your detailed grammar analysis here`
|
|
});
|
|
|
|
// Parse structured response
|
|
const parsedResult = this._parseStructuredResponse(aiResponse);
|
|
|
|
// Apply penalties and bonuses
|
|
if (this.hintUsed) {
|
|
parsedResult.score = Math.max(parsedResult.score - 10, 30);
|
|
parsedResult.feedback += ` (Note: -10 points for using hint - try to apply grammar rules independently)`;
|
|
}
|
|
|
|
if (this.attempts > 1) {
|
|
const penalty = (this.attempts - 1) * 5;
|
|
parsedResult.score = Math.max(parsedResult.score - penalty, 20);
|
|
parsedResult.feedback += ` (Note: -${penalty} points for multiple attempts - review grammar rules carefully)`;
|
|
}
|
|
|
|
// Record interaction in context memory
|
|
this.contextMemory.recordInteraction({
|
|
type: 'grammar',
|
|
subtype: this.currentExercise.type,
|
|
content: {
|
|
rule: this.currentGrammarRule,
|
|
exercise: this.currentExercise,
|
|
ruleTitle: this.currentGrammarRule.title || 'Grammar Rule',
|
|
exerciseType: this.currentExercise.type,
|
|
difficulty: this.currentExercise.difficulty
|
|
},
|
|
userResponse: userInput.trim(),
|
|
validation: parsedResult,
|
|
context: {
|
|
languages: this.languages,
|
|
exerciseIndex: this.exerciseIndex,
|
|
totalExercises: this.exercises.length,
|
|
attempts: this.attempts,
|
|
hintUsed: this.hintUsed
|
|
}
|
|
});
|
|
|
|
return parsedResult;
|
|
|
|
} catch (error) {
|
|
console.error('❌ AI grammar validation failed:', error);
|
|
|
|
// Fallback to basic grammar validation if AI fails
|
|
if (!this.aiAvailable) {
|
|
return this._performBasicGrammarValidation(userInput);
|
|
}
|
|
|
|
throw new Error(`Grammar validation failed: ${error.message}. Please check your answer and try again.`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current progress data
|
|
* @returns {ProgressData} - Progress information for this module
|
|
*/
|
|
getProgress() {
|
|
const totalExercises = this.exercises ? this.exercises.length : 0;
|
|
const completedExercises = this.exerciseResults.length;
|
|
const correctAnswers = this.exerciseResults.filter(result => result.correct).length;
|
|
|
|
return {
|
|
type: 'grammar',
|
|
ruleTitle: this.currentGrammarRule?.title || 'Grammar Rule',
|
|
totalExercises,
|
|
completedExercises,
|
|
correctAnswers,
|
|
currentExerciseIndex: this.exerciseIndex,
|
|
exerciseResults: this.exerciseResults,
|
|
progressPercentage: totalExercises > 0 ? Math.round((completedExercises / totalExercises) * 100) : 0,
|
|
accuracyRate: completedExercises > 0 ? Math.round((correctAnswers / completedExercises) * 100) : 0,
|
|
currentAttempts: this.attempts,
|
|
hintUsed: this.hintUsed,
|
|
aiAnalysisAvailable: this.aiAvailable
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Clean up and prepare for unloading
|
|
*/
|
|
cleanup() {
|
|
console.log('🧹 Cleaning up GrammarModule...');
|
|
|
|
// Remove event listeners
|
|
if (this.container) {
|
|
this.container.innerHTML = '';
|
|
}
|
|
|
|
// Reset state
|
|
this.container = null;
|
|
this.currentExerciseData = null;
|
|
this.currentGrammarRule = null;
|
|
this.currentExercise = null;
|
|
this.exerciseIndex = 0;
|
|
this.exerciseResults = [];
|
|
this.exercises = null;
|
|
this.validationInProgress = false;
|
|
this.lastValidationResult = null;
|
|
this.hintUsed = false;
|
|
this.attempts = 0;
|
|
|
|
console.log('✅ GrammarModule cleaned up');
|
|
}
|
|
|
|
/**
|
|
* Get module metadata
|
|
* @returns {Object} - Module information
|
|
*/
|
|
getMetadata() {
|
|
return {
|
|
name: 'GrammarModule',
|
|
type: 'grammar',
|
|
version: '1.0.0',
|
|
description: 'Grammar exercises with AI-powered linguistic analysis',
|
|
capabilities: ['grammar_rules', 'sentence_construction', 'error_correction', 'linguistic_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(`🌍 Grammar languages detected: ${this.languages.userLanguage} -> ${this.languages.targetLanguage}`);
|
|
}
|
|
|
|
/**
|
|
* Prepare exercises for the grammar rule
|
|
* @private
|
|
*/
|
|
async _prepareExercises(grammarRule) {
|
|
// If grammar rule already has exercises, use them
|
|
if (grammarRule.exercises && grammarRule.exercises.length > 0) {
|
|
return grammarRule.exercises.map((ex, index) => ({
|
|
id: `ex${index + 1}`,
|
|
type: ex.type || 'fill_blank',
|
|
question: ex.question || ex.text || ex,
|
|
correctAnswer: ex.answer || ex.correctAnswer || '',
|
|
options: ex.options || [],
|
|
hint: ex.hint || '',
|
|
difficulty: ex.difficulty || 'medium',
|
|
points: ex.points || 10
|
|
}));
|
|
}
|
|
|
|
// Generate default grammar exercises based on rule type
|
|
const ruleType = grammarRule.type || 'general';
|
|
const defaultExercises = this._generateDefaultExercises(grammarRule, ruleType);
|
|
|
|
// Limit to configured number of exercises
|
|
return defaultExercises.slice(0, this.config.exercisesPerRule);
|
|
}
|
|
|
|
/**
|
|
* Generate default exercises for a grammar rule
|
|
* @private
|
|
*/
|
|
_generateDefaultExercises(grammarRule, ruleType) {
|
|
const ruleTitle = grammarRule.title || 'Grammar Rule';
|
|
const examples = grammarRule.examples || [];
|
|
|
|
const defaultExercises = [
|
|
{
|
|
id: 'fill1',
|
|
type: 'fill_blank',
|
|
question: `Complete the sentence following the ${ruleTitle} rule: "The student ____ to school every day."`,
|
|
correctAnswer: 'goes',
|
|
hint: `Use the correct form of the verb according to ${ruleTitle}`,
|
|
difficulty: 'easy',
|
|
points: 10
|
|
},
|
|
{
|
|
id: 'correction1',
|
|
type: 'correction',
|
|
question: `Correct the grammar error in this sentence: "He don't like coffee."`,
|
|
correctAnswer: `He doesn't like coffee.`,
|
|
hint: 'Check subject-verb agreement',
|
|
difficulty: 'medium',
|
|
points: 15
|
|
},
|
|
{
|
|
id: 'transform1',
|
|
type: 'transformation',
|
|
question: `Transform this sentence using ${ruleTitle}: "She is reading a book."`,
|
|
correctAnswer: 'She reads a book.',
|
|
hint: `Apply ${ruleTitle} to change the tense or form`,
|
|
difficulty: 'medium',
|
|
points: 15
|
|
},
|
|
{
|
|
id: 'choice1',
|
|
type: 'multiple_choice',
|
|
question: `Choose the correct option that follows ${ruleTitle}:`,
|
|
options: ['Option A', 'Option B', 'Option C'],
|
|
correctAnswer: 'Option A',
|
|
hint: `Remember the ${ruleTitle} rule`,
|
|
difficulty: 'easy',
|
|
points: 10
|
|
},
|
|
{
|
|
id: 'construction1',
|
|
type: 'construction',
|
|
question: `Create a sentence using ${ruleTitle} with the words: [student, study, library]`,
|
|
correctAnswer: 'The student studies in the library.',
|
|
hint: `Follow ${ruleTitle} for proper sentence construction`,
|
|
difficulty: 'hard',
|
|
points: 20
|
|
}
|
|
];
|
|
|
|
return defaultExercises;
|
|
}
|
|
|
|
/**
|
|
* Build comprehensive prompt for grammar validation
|
|
* @private
|
|
*/
|
|
_buildGrammarPrompt(userAnswer) {
|
|
const ruleTitle = this.currentGrammarRule.title || 'Grammar Rule';
|
|
const ruleDescription = this.currentGrammarRule.description || 'No description available';
|
|
const ruleExamples = this.currentGrammarRule.examples || [];
|
|
|
|
return `You are evaluating grammar for a language learning exercise.
|
|
|
|
CRITICAL: You MUST respond in this EXACT format: [answer]yes/no [explanation]your detailed grammar analysis here
|
|
|
|
GRAMMAR RULE:
|
|
Title: "${ruleTitle}"
|
|
Description: "${ruleDescription}"
|
|
Examples: ${ruleExamples.length > 0 ? ruleExamples.join(', ') : 'No examples provided'}
|
|
|
|
EXERCISE:
|
|
Type: ${this.currentExercise.type} (${this.exerciseTypes[this.currentExercise.type] || 'Grammar exercise'})
|
|
Question: "${this.currentExercise.question}"
|
|
Expected Answer: "${this.currentExercise.correctAnswer}"
|
|
${this.currentExercise.options.length > 0 ? `Options: ${this.currentExercise.options.join(', ')}` : ''}
|
|
|
|
STUDENT RESPONSE: "${userAnswer}"
|
|
|
|
EVALUATION CONTEXT:
|
|
- Exercise Type: Grammar practice
|
|
- Languages: ${this.languages.userLanguage} -> ${this.languages.targetLanguage}
|
|
- Exercise Difficulty: ${this.currentExercise.difficulty}
|
|
- Exercise ${this.exerciseIndex + 1} of ${this.exercises.length}
|
|
- Attempts: ${this.attempts}
|
|
- Hint Used: ${this.hintUsed}
|
|
|
|
EVALUATION CRITERIA:
|
|
- [answer]yes if the student's answer demonstrates correct grammar rule application
|
|
- [answer]no if the answer shows grammatical errors or rule misapplication
|
|
- Focus on GRAMMATICAL ACCURACY and RULE COMPREHENSION
|
|
- Accept alternative correct forms if they follow the grammar rule
|
|
- Be strict about grammar but consider language learning level
|
|
- For fill-in-the-blank: exact or grammatically equivalent answers
|
|
- For corrections: proper error identification and correction
|
|
- For transformations: accurate structural changes
|
|
- For multiple choice: exact match with correct option
|
|
- For construction: proper sentence structure following the rule
|
|
|
|
[explanation] should provide:
|
|
1. Whether the grammar rule was applied correctly
|
|
2. Specific grammatical analysis of the response
|
|
3. What errors were made (if any) and why they're incorrect
|
|
4. The correct grammatical form and rule explanation
|
|
5. Tips for remembering and applying this grammar rule
|
|
6. Encouragement and constructive feedback
|
|
|
|
Format: [answer]yes/no [explanation]your comprehensive grammar analysis here`;
|
|
}
|
|
|
|
/**
|
|
* Parse structured AI response for grammar validation
|
|
* @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 grammar 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();
|
|
|
|
// Grammar-specific scoring
|
|
const baseScore = isCorrect ? 92 : 45; // High standards for grammar
|
|
const result = {
|
|
score: baseScore,
|
|
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,
|
|
grammarAnalysis: true
|
|
};
|
|
|
|
console.log(`✅ AI grammar parsed: ${result.answer} - Score: ${result.score}`);
|
|
return result;
|
|
|
|
} catch (error) {
|
|
console.error('❌ Failed to parse AI grammar response:', error);
|
|
console.error('Raw response:', aiResponse);
|
|
|
|
throw new Error(`AI response format invalid: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Perform basic grammar validation when AI is unavailable
|
|
* @private
|
|
*/
|
|
_performBasicGrammarValidation(userAnswer) {
|
|
console.log('🔍 Performing basic grammar validation (AI unavailable)');
|
|
|
|
const correctAnswer = this.currentExercise.correctAnswer.trim().toLowerCase();
|
|
const userAnswerClean = userAnswer.trim().toLowerCase();
|
|
|
|
// Basic exact matching for grammar
|
|
const isExactMatch = userAnswerClean === correctAnswer;
|
|
|
|
// Simple similarity check
|
|
const similarity = this._calculateStringSimilarity(userAnswerClean, correctAnswer);
|
|
const isClose = similarity > 0.8;
|
|
|
|
let score = 20; // Base score for attempt
|
|
let feedback = '';
|
|
|
|
if (isExactMatch) {
|
|
score = 80;
|
|
feedback = "Correct! Your answer matches the expected grammar form.";
|
|
} else if (isClose) {
|
|
score = 60;
|
|
feedback = "Close! Your answer is similar to the correct form but may have minor grammar issues. The correct answer is: " + this.currentExercise.correctAnswer;
|
|
} else {
|
|
score = 30;
|
|
feedback = "Not quite right. The correct answer is: " + this.currentExercise.correctAnswer + ". Please review the grammar rule and try again.";
|
|
}
|
|
|
|
return {
|
|
score: score,
|
|
correct: isExactMatch,
|
|
feedback: feedback,
|
|
timestamp: new Date().toISOString(),
|
|
provider: 'basic_grammar_analysis',
|
|
model: 'string_matching',
|
|
cached: false,
|
|
mockGenerated: true,
|
|
grammarAnalysis: true,
|
|
limitedAnalysis: true
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculate string similarity (basic implementation)
|
|
* @private
|
|
*/
|
|
_calculateStringSimilarity(str1, str2) {
|
|
const longer = str1.length > str2.length ? str1 : str2;
|
|
const shorter = str1.length > str2.length ? str2 : str1;
|
|
|
|
if (longer.length === 0) return 1.0;
|
|
|
|
const editDistance = this._levenshteinDistance(longer, shorter);
|
|
return (longer.length - editDistance) / longer.length;
|
|
}
|
|
|
|
/**
|
|
* Calculate Levenshtein distance
|
|
* @private
|
|
*/
|
|
_levenshteinDistance(str1, str2) {
|
|
const matrix = [];
|
|
|
|
for (let i = 0; i <= str2.length; i++) {
|
|
matrix[i] = [i];
|
|
}
|
|
|
|
for (let j = 0; j <= str1.length; j++) {
|
|
matrix[0][j] = j;
|
|
}
|
|
|
|
for (let i = 1; i <= str2.length; i++) {
|
|
for (let j = 1; j <= str1.length; j++) {
|
|
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
|
|
matrix[i][j] = matrix[i - 1][j - 1];
|
|
} else {
|
|
matrix[i][j] = Math.min(
|
|
matrix[i - 1][j - 1] + 1,
|
|
matrix[i][j - 1] + 1,
|
|
matrix[i - 1][j] + 1
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return matrix[str2.length][str1.length];
|
|
}
|
|
|
|
/**
|
|
* Render the grammar exercise interface
|
|
* @private
|
|
*/
|
|
async _renderGrammarExercise() {
|
|
if (!this.container || !this.currentGrammarRule) return;
|
|
|
|
const ruleTitle = this.currentGrammarRule.title || 'Grammar Practice';
|
|
const ruleType = this.currentGrammarRule.type || 'general';
|
|
|
|
this.container.innerHTML = `
|
|
<div class="grammar-exercise">
|
|
<div class="exercise-header">
|
|
<h2>📚 Grammar Practice</h2>
|
|
<div class="grammar-info">
|
|
<span class="grammar-meta">
|
|
${this.exercises?.length || 0} exercises • ${ruleType} grammar
|
|
${!this.aiAvailable ? ' • ⚠️ Limited analysis mode' : ' • 🧠 AI grammar analysis'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grammar-content">
|
|
<div class="rule-explanation-section" id="rule-explanation-section">
|
|
<div class="rule-explanation-card">
|
|
<div class="rule-header">
|
|
<h3>📖 ${ruleTitle}</h3>
|
|
<div class="rule-stats">
|
|
<span class="rule-type">${ruleType}</span>
|
|
<span class="rule-difficulty">${this.currentGrammarRule.difficulty || 'medium'}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="rule-content">
|
|
<div class="rule-description">
|
|
${this.currentGrammarRule.description || 'Grammar rule description not available.'}
|
|
</div>
|
|
|
|
${this.currentGrammarRule.examples && this.currentGrammarRule.examples.length > 0 ? `
|
|
<div class="rule-examples">
|
|
<h4>📝 Examples:</h4>
|
|
<ul>
|
|
${this.currentGrammarRule.examples.map(example =>
|
|
`<li>${example}</li>`
|
|
).join('')}
|
|
</ul>
|
|
</div>
|
|
` : ''}
|
|
|
|
${this.currentGrammarRule.notes ? `
|
|
<div class="rule-notes">
|
|
<h4>💡 Important Notes:</h4>
|
|
<p>${this.currentGrammarRule.notes}</p>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
|
|
<div class="rule-actions">
|
|
<button id="start-exercises-btn" class="btn btn-success">
|
|
<span class="btn-icon">✏️</span>
|
|
<span class="btn-text">Start Grammar Exercises</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="exercises-section" id="exercises-section" style="display: none;">
|
|
<div class="exercise-card">
|
|
<div class="exercise-progress">
|
|
<div class="progress-indicator">
|
|
<span id="exercise-counter">Exercise 1 of ${this.exercises?.length || 0}</span>
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" id="progress-fill" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
<div class="attempt-info">
|
|
<span id="attempt-counter">Attempt 1 of ${this.config.maxAttempts}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="exercise-content" id="exercise-content">
|
|
<!-- Exercise content will be populated dynamically -->
|
|
</div>
|
|
|
|
<div class="answer-input-section" id="answer-input-section">
|
|
<label for="answer-input">Your Answer:</label>
|
|
<input
|
|
type="text"
|
|
id="answer-input"
|
|
placeholder="Enter your answer here..."
|
|
autocomplete="off"
|
|
/>
|
|
</div>
|
|
|
|
<div class="exercise-controls">
|
|
<button id="show-rule-btn" class="btn btn-outline">
|
|
<span class="btn-icon">📖</span>
|
|
<span class="btn-text">Review Rule</span>
|
|
</button>
|
|
${this.config.showHints ? `
|
|
<button id="show-hint-btn" class="btn btn-secondary">
|
|
<span class="btn-icon">💡</span>
|
|
<span class="btn-text">Show Hint</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' : 'Check Answer'}</span>
|
|
</button>
|
|
<div id="validation-status" class="validation-status"></div>
|
|
</div>
|
|
|
|
<div class="hint-panel" id="hint-panel" style="display: none;">
|
|
<div class="hint-content">
|
|
<h4>💡 Hint</h4>
|
|
<p id="hint-text"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="explanation-panel" id="explanation-panel" style="display: none;">
|
|
<div class="panel-header">
|
|
<h3>${this.aiAvailable ? '🤖 AI Grammar 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-exercise-btn" class="btn btn-primary" style="display: none;">
|
|
Next Exercise
|
|
</button>
|
|
<button id="retry-exercise-btn" class="btn btn-secondary" style="display: none;">
|
|
Try Again
|
|
</button>
|
|
<button id="finish-grammar-btn" class="btn btn-success" style="display: none;">
|
|
Complete Grammar Practice
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grammar-results" id="grammar-results" style="display: none;">
|
|
<!-- Final results will be shown here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Add CSS styles
|
|
this._addStyles();
|
|
|
|
// Add event listeners
|
|
this._setupEventListeners();
|
|
}
|
|
|
|
/**
|
|
* Setup event listeners for grammar exercise
|
|
* @private
|
|
*/
|
|
_setupEventListeners() {
|
|
const startExercisesBtn = document.getElementById('start-exercises-btn');
|
|
const showRuleBtn = document.getElementById('show-rule-btn');
|
|
const showHintBtn = document.getElementById('show-hint-btn');
|
|
const answerInput = document.getElementById('answer-input');
|
|
const validateBtn = document.getElementById('validate-answer-btn');
|
|
const retryBtn = document.getElementById('retry-exercise-btn');
|
|
const nextBtn = document.getElementById('next-exercise-btn');
|
|
const finishBtn = document.getElementById('finish-grammar-btn');
|
|
|
|
// Start exercises button
|
|
if (startExercisesBtn) {
|
|
startExercisesBtn.onclick = () => this._startExercises();
|
|
}
|
|
|
|
// Rule and hint buttons
|
|
if (showRuleBtn) {
|
|
showRuleBtn.onclick = this._handleShowRule;
|
|
}
|
|
|
|
if (showHintBtn) {
|
|
showHintBtn.onclick = this._handleShowHint;
|
|
}
|
|
|
|
// Answer input validation
|
|
if (answerInput) {
|
|
answerInput.addEventListener('input', () => {
|
|
const hasText = answerInput.value.trim().length > 0;
|
|
if (validateBtn) {
|
|
validateBtn.disabled = !hasText || this.validationInProgress;
|
|
}
|
|
});
|
|
|
|
// Allow Enter to validate
|
|
answerInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter' && !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._handleNextExercise;
|
|
if (finishBtn) finishBtn.onclick = () => this._completeGrammarExercise();
|
|
}
|
|
|
|
/**
|
|
* Show rule explanation phase
|
|
* @private
|
|
*/
|
|
_showRuleExplanation() {
|
|
const ruleSection = document.getElementById('rule-explanation-section');
|
|
const exercisesSection = document.getElementById('exercises-section');
|
|
|
|
if (ruleSection) ruleSection.style.display = 'block';
|
|
if (exercisesSection) exercisesSection.style.display = 'none';
|
|
}
|
|
|
|
/**
|
|
* Start exercises phase
|
|
* @private
|
|
*/
|
|
_startExercises() {
|
|
const ruleSection = document.getElementById('rule-explanation-section');
|
|
const exercisesSection = document.getElementById('exercises-section');
|
|
|
|
if (ruleSection) ruleSection.style.display = 'none';
|
|
if (exercisesSection) exercisesSection.style.display = 'block';
|
|
|
|
this._presentCurrentExercise();
|
|
}
|
|
|
|
/**
|
|
* Present current exercise
|
|
* @private
|
|
*/
|
|
_presentCurrentExercise() {
|
|
if (this.exerciseIndex >= this.exercises.length) {
|
|
this._showGrammarResults();
|
|
return;
|
|
}
|
|
|
|
this.currentExercise = this.exercises[this.exerciseIndex];
|
|
this.attempts = 0;
|
|
this.hintUsed = false;
|
|
|
|
const exerciseContent = document.getElementById('exercise-content');
|
|
const exerciseCounter = document.getElementById('exercise-counter');
|
|
const attemptCounter = document.getElementById('attempt-counter');
|
|
const progressFill = document.getElementById('progress-fill');
|
|
|
|
if (!exerciseContent || !this.currentExercise) return;
|
|
|
|
// Update progress
|
|
const progressPercentage = ((this.exerciseIndex + 1) / this.exercises.length) * 100;
|
|
if (progressFill) progressFill.style.width = `${progressPercentage}%`;
|
|
if (exerciseCounter) exerciseCounter.textContent = `Exercise ${this.exerciseIndex + 1} of ${this.exercises.length}`;
|
|
if (attemptCounter) attemptCounter.textContent = `Attempt 1 of ${this.config.maxAttempts}`;
|
|
|
|
// Display exercise based on type
|
|
this._displayExerciseByType(this.currentExercise);
|
|
|
|
// Clear previous answer and focus
|
|
const answerInput = document.getElementById('answer-input');
|
|
if (answerInput) {
|
|
answerInput.value = '';
|
|
answerInput.focus();
|
|
}
|
|
|
|
// Hide panels
|
|
const explanationPanel = document.getElementById('explanation-panel');
|
|
const hintPanel = document.getElementById('hint-panel');
|
|
if (explanationPanel) explanationPanel.style.display = 'none';
|
|
if (hintPanel) hintPanel.style.display = 'none';
|
|
}
|
|
|
|
/**
|
|
* Display exercise content by type
|
|
* @private
|
|
*/
|
|
_displayExerciseByType(exercise) {
|
|
const exerciseContent = document.getElementById('exercise-content');
|
|
const answerInputSection = document.getElementById('answer-input-section');
|
|
|
|
let content = '';
|
|
let inputType = 'text';
|
|
|
|
switch (exercise.type) {
|
|
case 'multiple_choice':
|
|
content = `
|
|
<div class="exercise-display exercise-multiple-choice">
|
|
<div class="exercise-question">${exercise.question}</div>
|
|
<div class="exercise-options">
|
|
${exercise.options.map((option, index) => `
|
|
<label class="option-label">
|
|
<input type="radio" name="grammar-option" value="${option}" />
|
|
<span class="option-text">${option}</span>
|
|
</label>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
// Hide text input for multiple choice
|
|
if (answerInputSection) answerInputSection.style.display = 'none';
|
|
break;
|
|
|
|
case 'fill_blank':
|
|
content = `
|
|
<div class="exercise-display exercise-fill-blank">
|
|
<div class="exercise-question">${exercise.question}</div>
|
|
<div class="exercise-instruction">Fill in the blank with the correct word or phrase.</div>
|
|
</div>
|
|
`;
|
|
break;
|
|
|
|
case 'correction':
|
|
content = `
|
|
<div class="exercise-display exercise-correction">
|
|
<div class="exercise-question">${exercise.question}</div>
|
|
<div class="exercise-instruction">Identify and correct the grammar error.</div>
|
|
</div>
|
|
`;
|
|
break;
|
|
|
|
case 'transformation':
|
|
content = `
|
|
<div class="exercise-display exercise-transformation">
|
|
<div class="exercise-question">${exercise.question}</div>
|
|
<div class="exercise-instruction">Transform the sentence according to the grammar rule.</div>
|
|
</div>
|
|
`;
|
|
break;
|
|
|
|
case 'conjugation':
|
|
content = `
|
|
<div class="exercise-display exercise-conjugation">
|
|
<div class="exercise-question">${exercise.question}</div>
|
|
<div class="exercise-instruction">Conjugate the verb correctly.</div>
|
|
</div>
|
|
`;
|
|
break;
|
|
|
|
case 'construction':
|
|
content = `
|
|
<div class="exercise-display exercise-construction">
|
|
<div class="exercise-question">${exercise.question}</div>
|
|
<div class="exercise-instruction">Build a grammatically correct sentence.</div>
|
|
</div>
|
|
`;
|
|
break;
|
|
|
|
default:
|
|
content = `
|
|
<div class="exercise-display">
|
|
<div class="exercise-question">${exercise.question}</div>
|
|
</div>
|
|
`;
|
|
break;
|
|
}
|
|
|
|
content += `
|
|
<div class="exercise-meta">
|
|
<span class="exercise-type">${this.exerciseTypes[exercise.type] || exercise.type}</span>
|
|
<span class="exercise-difficulty difficulty-${exercise.difficulty}">
|
|
${exercise.difficulty}
|
|
</span>
|
|
<span class="exercise-points">${exercise.points} points</span>
|
|
</div>
|
|
`;
|
|
|
|
exerciseContent.innerHTML = content;
|
|
|
|
// Show/hide input section based on exercise type
|
|
if (answerInputSection) {
|
|
answerInputSection.style.display = exercise.type === 'multiple_choice' ? 'none' : 'block';
|
|
}
|
|
|
|
// Add event listeners for multiple choice
|
|
if (exercise.type === 'multiple_choice') {
|
|
const radioInputs = document.querySelectorAll('input[name="grammar-option"]');
|
|
radioInputs.forEach(input => {
|
|
input.addEventListener('change', () => {
|
|
const validateBtn = document.getElementById('validate-answer-btn');
|
|
if (validateBtn) {
|
|
validateBtn.disabled = !input.checked || this.validationInProgress;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle show hint
|
|
* @private
|
|
*/
|
|
_handleShowHint() {
|
|
const hintPanel = document.getElementById('hint-panel');
|
|
const hintText = document.getElementById('hint-text');
|
|
const showHintBtn = document.getElementById('show-hint-btn');
|
|
|
|
if (!hintPanel || !hintText || !this.currentExercise.hint) return;
|
|
|
|
hintText.textContent = this.currentExercise.hint;
|
|
hintPanel.style.display = 'block';
|
|
this.hintUsed = true;
|
|
|
|
// Disable hint button after use
|
|
if (showHintBtn) {
|
|
showHintBtn.disabled = true;
|
|
showHintBtn.innerHTML = `
|
|
<span class="btn-icon">✓</span>
|
|
<span class="btn-text">Hint Used</span>
|
|
`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle show rule
|
|
* @private
|
|
*/
|
|
_handleShowRule() {
|
|
const ruleSection = document.getElementById('rule-explanation-section');
|
|
const exercisesSection = document.getElementById('exercises-section');
|
|
|
|
// Toggle visibility
|
|
if (ruleSection && exercisesSection) {
|
|
const isRuleVisible = ruleSection.style.display !== 'none';
|
|
ruleSection.style.display = isRuleVisible ? 'none' : 'block';
|
|
exercisesSection.style.display = isRuleVisible ? 'block' : 'none';
|
|
|
|
const showRuleBtn = document.getElementById('show-rule-btn');
|
|
if (showRuleBtn) {
|
|
showRuleBtn.innerHTML = isRuleVisible ? `
|
|
<span class="btn-icon">📖</span>
|
|
<span class="btn-text">Review Rule</span>
|
|
` : `
|
|
<span class="btn-icon">✏️</span>
|
|
<span class="btn-text">Back to Exercise</span>
|
|
`;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle user input validation
|
|
* @private
|
|
*/
|
|
async _handleUserInput() {
|
|
let userAnswer = '';
|
|
|
|
// Get answer based on exercise type
|
|
if (this.currentExercise.type === 'multiple_choice') {
|
|
const selectedOption = document.querySelector('input[name="grammar-option"]:checked');
|
|
if (!selectedOption) return;
|
|
userAnswer = selectedOption.value;
|
|
} else {
|
|
const answerInput = document.getElementById('answer-input');
|
|
if (!answerInput) return;
|
|
userAnswer = answerInput.value.trim();
|
|
if (!userAnswer) return;
|
|
}
|
|
|
|
const validateBtn = document.getElementById('validate-answer-btn');
|
|
const statusDiv = document.getElementById('validation-status');
|
|
|
|
if (!validateBtn || !statusDiv) return;
|
|
|
|
try {
|
|
// Increment attempts
|
|
this.attempts++;
|
|
const attemptCounter = document.getElementById('attempt-counter');
|
|
if (attemptCounter) {
|
|
attemptCounter.textContent = `Attempt ${this.attempts} of ${this.config.maxAttempts}`;
|
|
}
|
|
|
|
// Set validation in progress
|
|
this.validationInProgress = true;
|
|
validateBtn.disabled = true;
|
|
|
|
// Disable inputs
|
|
const answerInput = document.getElementById('answer-input');
|
|
const radioInputs = document.querySelectorAll('input[name="grammar-option"]');
|
|
if (answerInput) answerInput.disabled = true;
|
|
radioInputs.forEach(input => input.disabled = true);
|
|
|
|
// Show loading status
|
|
statusDiv.innerHTML = `
|
|
<div class="status-loading">
|
|
<div class="loading-spinner">🔍</div>
|
|
<span>${this.aiAvailable ? 'AI is analyzing your grammar...' : 'Checking your answer...'}</span>
|
|
</div>
|
|
`;
|
|
|
|
// Call validation
|
|
const result = await this.validate(userAnswer, {});
|
|
this.lastValidationResult = result;
|
|
|
|
// Store result if correct or max attempts reached
|
|
if (result.correct || this.attempts >= this.config.maxAttempts) {
|
|
this.exerciseResults[this.exerciseIndex] = {
|
|
exercise: this.currentExercise.question,
|
|
userAnswer: userAnswer,
|
|
correctAnswer: this.currentExercise.correctAnswer,
|
|
correct: result.correct,
|
|
score: result.score,
|
|
feedback: result.feedback,
|
|
attempts: this.attempts,
|
|
hintUsed: this.hintUsed,
|
|
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('❌ Grammar 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-exercise-btn');
|
|
const retryBtn = document.getElementById('retry-exercise-btn');
|
|
const finishBtn = document.getElementById('finish-grammar-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 Grammar!' : '📚 Keep Studying!'}
|
|
</span>
|
|
<span class="grammar-score">Score: ${result.score}/100</span>
|
|
</div>
|
|
<div class="correct-answer-display">
|
|
<strong>Correct Answer:</strong> ${this.currentExercise.correctAnswer}
|
|
</div>
|
|
<div class="explanation-text">${result.explanation || result.feedback}</div>
|
|
${result.grammarAnalysis ? '<div class="analysis-note">📚 This analysis focuses on grammatical accuracy and rule application.</div>' : ''}
|
|
${this.hintUsed ? '<div class="hint-note">💡 Remember: Using hints helps learning but reduces scores. Try to apply grammar rules independently next time!</div>' : ''}
|
|
${this.attempts > 1 ? '<div class="attempt-note">🎯 Multiple attempts help reinforce learning. Review the rule and practice more!</div>' : ''}
|
|
</div>
|
|
`;
|
|
|
|
// Show appropriate buttons
|
|
const isLastExercise = this.exerciseIndex >= this.exercises.length - 1;
|
|
const canRetry = !result.correct && this.attempts < this.config.maxAttempts;
|
|
|
|
if (nextBtn) nextBtn.style.display = (result.correct || this.attempts >= this.config.maxAttempts) && !isLastExercise ? 'inline-block' : 'none';
|
|
if (finishBtn) finishBtn.style.display = (result.correct || this.attempts >= this.config.maxAttempts) && isLastExercise ? 'inline-block' : 'none';
|
|
if (retryBtn) retryBtn.style.display = canRetry ? 'inline-block' : 'none';
|
|
|
|
// Scroll to explanation
|
|
explanationPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
}
|
|
|
|
/**
|
|
* Handle next exercise
|
|
* @private
|
|
*/
|
|
_handleNextExercise() {
|
|
this.exerciseIndex++;
|
|
this._presentCurrentExercise();
|
|
}
|
|
|
|
/**
|
|
* 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 or incorrect answer
|
|
* @private
|
|
*/
|
|
_enableRetry() {
|
|
this.validationInProgress = false;
|
|
|
|
const answerInput = document.getElementById('answer-input');
|
|
const validateBtn = document.getElementById('validate-answer-btn');
|
|
const radioInputs = document.querySelectorAll('input[name="grammar-option"]');
|
|
|
|
if (answerInput) {
|
|
answerInput.disabled = false;
|
|
answerInput.focus();
|
|
}
|
|
|
|
radioInputs.forEach(input => input.disabled = false);
|
|
|
|
if (validateBtn) {
|
|
validateBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show final grammar results
|
|
* @private
|
|
*/
|
|
_showGrammarResults() {
|
|
const resultsContainer = document.getElementById('grammar-results');
|
|
const exercisesSection = document.getElementById('exercises-section');
|
|
|
|
if (!resultsContainer) return;
|
|
|
|
const correctCount = this.exerciseResults.filter(result => result.correct).length;
|
|
const totalCount = this.exerciseResults.length;
|
|
const accuracyRate = totalCount > 0 ? Math.round((correctCount / totalCount) * 100) : 0;
|
|
const totalScore = this.exerciseResults.reduce((sum, result) => sum + result.score, 0);
|
|
const avgScore = totalCount > 0 ? Math.round(totalScore / totalCount) : 0;
|
|
|
|
let resultClass = 'results-poor';
|
|
if (accuracyRate >= 80) resultClass = 'results-excellent';
|
|
else if (accuracyRate >= 60) resultClass = 'results-good';
|
|
|
|
const resultsHTML = `
|
|
<div class="grammar-results-content ${resultClass}">
|
|
<h3>📊 Grammar Practice Results</h3>
|
|
<div class="results-summary">
|
|
<div class="accuracy-display">
|
|
<span class="accuracy-rate">${accuracyRate}%</span>
|
|
<span class="accuracy-label">Accuracy Rate</span>
|
|
</div>
|
|
<div class="exercises-summary">
|
|
${correctCount} / ${totalCount} exercises completed correctly
|
|
</div>
|
|
<div class="score-summary">
|
|
Average Score: ${avgScore}/100 points
|
|
</div>
|
|
</div>
|
|
|
|
<div class="exercise-breakdown">
|
|
${this.exerciseResults.map((result, index) => `
|
|
<div class="exercise-result ${result.correct ? 'correct' : 'incorrect'}">
|
|
<div class="exercise-summary">
|
|
<span class="exercise-num">Ex${index + 1}</span>
|
|
<span class="exercise-icon">${result.correct ? '✅' : '📚'}</span>
|
|
<span class="exercise-type">${this.exerciseTypes[this.exercises[index]?.type] || 'Grammar'}</span>
|
|
<span class="score">Score: ${result.score}/100</span>
|
|
<span class="attempts-info">Attempts: ${result.attempts}</span>
|
|
${result.hintUsed ? '<span class="hint-used">💡 Hint</span>' : ''}
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
|
|
<div class="results-actions">
|
|
<button id="complete-grammar-btn" class="btn btn-primary">Continue to Next Exercise</button>
|
|
<button id="practice-again-btn" class="btn btn-outline">Practice This Rule Again</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
resultsContainer.innerHTML = resultsHTML;
|
|
resultsContainer.style.display = 'block';
|
|
|
|
// Hide other sections
|
|
if (exercisesSection) exercisesSection.style.display = 'none';
|
|
|
|
// Add action listeners
|
|
document.getElementById('complete-grammar-btn').onclick = () => this._completeGrammarExercise();
|
|
document.getElementById('practice-again-btn').onclick = () => this._practiceAgain();
|
|
}
|
|
|
|
/**
|
|
* Complete grammar exercise
|
|
* @private
|
|
*/
|
|
_completeGrammarExercise() {
|
|
// Mark grammar rule as mastered if performance is good
|
|
const correctCount = this.exerciseResults.filter(result => result.correct).length;
|
|
const accuracyRate = correctCount / this.exerciseResults.length;
|
|
|
|
if (accuracyRate >= 0.7) { // 70% accuracy threshold for grammar
|
|
const ruleId = this.currentGrammarRule.id || this.currentGrammarRule.title || 'grammar_rule';
|
|
const metadata = {
|
|
accuracyRate: Math.round(accuracyRate * 100),
|
|
exercisesCompleted: this.exerciseResults.length,
|
|
correctExercises: correctCount,
|
|
totalAttempts: this.exerciseResults.reduce((sum, result) => sum + result.attempts, 0),
|
|
avgScore: Math.round(this.exerciseResults.reduce((sum, result) => sum + result.score, 0) / this.exerciseResults.length),
|
|
sessionId: this.orchestrator?.sessionId || 'unknown',
|
|
moduleType: 'grammar',
|
|
aiAnalysisUsed: this.aiAvailable,
|
|
ruleType: this.currentGrammarRule.type || 'general'
|
|
};
|
|
|
|
this.prerequisiteEngine.markGrammarMastered(ruleId, metadata);
|
|
|
|
// Also save to persistent storage
|
|
if (window.addMasteredItem && this.orchestrator?.bookId && this.orchestrator?.chapterId) {
|
|
window.addMasteredItem(
|
|
this.orchestrator.bookId,
|
|
this.orchestrator.chapterId,
|
|
'grammar',
|
|
ruleId,
|
|
metadata
|
|
);
|
|
}
|
|
}
|
|
|
|
// Emit completion event
|
|
this.orchestrator._eventBus.emit('drs:exerciseCompleted', {
|
|
moduleType: 'grammar',
|
|
results: this.exerciseResults,
|
|
progress: this.getProgress()
|
|
}, 'GrammarModule');
|
|
}
|
|
|
|
/**
|
|
* Practice grammar rule again
|
|
* @private
|
|
*/
|
|
_practiceAgain() {
|
|
this.exerciseIndex = 0;
|
|
this.exerciseResults = [];
|
|
this.attempts = 0;
|
|
this.hintUsed = false;
|
|
this._showRuleExplanation();
|
|
|
|
const resultsContainer = document.getElementById('grammar-results');
|
|
if (resultsContainer) resultsContainer.style.display = 'none';
|
|
}
|
|
|
|
/**
|
|
* Add CSS styles for grammar exercise
|
|
* @private
|
|
*/
|
|
_addStyles() {
|
|
if (document.getElementById('grammar-module-styles')) return;
|
|
|
|
const styles = document.createElement('style');
|
|
styles.id = 'grammar-module-styles';
|
|
styles.textContent = `
|
|
.grammar-exercise {
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
display: grid;
|
|
gap: 20px;
|
|
}
|
|
|
|
.exercise-header {
|
|
text-align: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.grammar-info {
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.grammar-meta {
|
|
color: #666;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.grammar-content {
|
|
display: grid;
|
|
gap: 20px;
|
|
}
|
|
|
|
.rule-explanation-card, .exercise-card {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 30px;
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.rule-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
padding-bottom: 15px;
|
|
border-bottom: 2px solid #eee;
|
|
}
|
|
|
|
.rule-header h3 {
|
|
margin: 0;
|
|
color: #333;
|
|
font-size: 1.5em;
|
|
}
|
|
|
|
.rule-stats {
|
|
display: flex;
|
|
gap: 15px;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.rule-type {
|
|
background: #e3f2fd;
|
|
color: #1976d2;
|
|
padding: 4px 12px;
|
|
border-radius: 20px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.rule-difficulty {
|
|
background: #f3e5f5;
|
|
color: #7b1fa2;
|
|
padding: 4px 12px;
|
|
border-radius: 20px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.rule-content {
|
|
line-height: 1.6;
|
|
color: #444;
|
|
}
|
|
|
|
.rule-description {
|
|
font-size: 1.1em;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.rule-examples {
|
|
margin-bottom: 20px;
|
|
padding: 20px;
|
|
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
|
|
border-radius: 10px;
|
|
border-left: 4px solid #28a745;
|
|
}
|
|
|
|
.rule-examples h4 {
|
|
margin: 0 0 15px 0;
|
|
color: #333;
|
|
}
|
|
|
|
.rule-examples ul {
|
|
margin: 0;
|
|
padding-left: 20px;
|
|
}
|
|
|
|
.rule-examples li {
|
|
margin-bottom: 8px;
|
|
font-weight: 500;
|
|
color: #555;
|
|
}
|
|
|
|
.rule-notes {
|
|
padding: 20px;
|
|
background: linear-gradient(135deg, #fff3cd, #ffeaa7);
|
|
border-radius: 10px;
|
|
border-left: 4px solid #ffc107;
|
|
}
|
|
|
|
.rule-notes h4 {
|
|
margin: 0 0 10px 0;
|
|
color: #333;
|
|
}
|
|
|
|
.rule-notes p {
|
|
margin: 0;
|
|
color: #555;
|
|
}
|
|
|
|
.rule-actions {
|
|
text-align: center;
|
|
margin-top: 25px;
|
|
}
|
|
|
|
.exercise-progress {
|
|
margin-bottom: 25px;
|
|
}
|
|
|
|
.progress-indicator {
|
|
text-align: center;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.attempt-info {
|
|
text-align: center;
|
|
color: #666;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.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, #6c5ce7, #a29bfe);
|
|
transition: width 0.5s ease;
|
|
}
|
|
|
|
.exercise-display {
|
|
margin-bottom: 25px;
|
|
padding: 20px;
|
|
background: linear-gradient(135deg, #f8f9ff, #e8f4fd);
|
|
border-radius: 10px;
|
|
border-left: 4px solid #6c5ce7;
|
|
}
|
|
|
|
.exercise-question {
|
|
font-size: 1.3em;
|
|
font-weight: 600;
|
|
color: #333;
|
|
margin-bottom: 15px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.exercise-instruction {
|
|
font-style: italic;
|
|
color: #666;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.exercise-options {
|
|
display: grid;
|
|
gap: 10px;
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.option-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 12px 15px;
|
|
background: white;
|
|
border-radius: 8px;
|
|
border: 2px solid #eee;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.option-label:hover {
|
|
border-color: #6c5ce7;
|
|
background: #f8f9ff;
|
|
}
|
|
|
|
.option-label input[type="radio"] {
|
|
margin: 0;
|
|
}
|
|
|
|
.option-text {
|
|
font-weight: 500;
|
|
color: #333;
|
|
}
|
|
|
|
.exercise-meta {
|
|
display: flex;
|
|
gap: 15px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.exercise-type {
|
|
background: #e3f2fd;
|
|
color: #1976d2;
|
|
padding: 4px 12px;
|
|
border-radius: 20px;
|
|
font-size: 0.85em;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.exercise-points {
|
|
background: #e8f5e8;
|
|
color: #2e7d32;
|
|
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 input[type="text"] {
|
|
width: 100%;
|
|
padding: 15px;
|
|
font-size: 1.05em;
|
|
border: 2px solid #ddd;
|
|
border-radius: 8px;
|
|
box-sizing: border-box;
|
|
transition: border-color 0.3s ease;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.answer-input-section input[type="text"]:focus {
|
|
outline: none;
|
|
border-color: #6c5ce7;
|
|
}
|
|
|
|
.exercise-controls {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.exercise-controls > div:first-child {
|
|
display: flex;
|
|
gap: 15px;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
}
|
|
|
|
.hint-panel {
|
|
margin-top: 20px;
|
|
padding: 20px;
|
|
background: linear-gradient(135deg, #fff3cd, #ffeaa7);
|
|
border-radius: 10px;
|
|
border-left: 4px solid #ffc107;
|
|
}
|
|
|
|
.hint-content h4 {
|
|
margin: 0 0 10px 0;
|
|
color: #333;
|
|
}
|
|
|
|
.hint-content p {
|
|
margin: 0;
|
|
color: #555;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.validation-status {
|
|
min-height: 30px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.status-loading, .status-complete, .status-error {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 12px 20px;
|
|
border-radius: 25px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.status-loading {
|
|
background: #e3f2fd;
|
|
color: #1976d2;
|
|
}
|
|
|
|
.status-complete {
|
|
background: #e8f5e8;
|
|
color: #2e7d32;
|
|
}
|
|
|
|
.status-error {
|
|
background: #ffebee;
|
|
color: #c62828;
|
|
}
|
|
|
|
.loading-spinner {
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
.explanation-panel {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 25px;
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
|
border-left: 4px solid #6c5ce7;
|
|
}
|
|
|
|
.panel-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
.panel-header h3 {
|
|
margin: 0;
|
|
color: #333;
|
|
}
|
|
|
|
.analysis-model {
|
|
font-size: 0.9em;
|
|
color: #666;
|
|
background: #f5f5f5;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.explanation-result {
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.explanation-result.correct {
|
|
border-left: 4px solid #4caf50;
|
|
background: linear-gradient(135deg, #f1f8e9, #e8f5e8);
|
|
}
|
|
|
|
.explanation-result.needs-improvement {
|
|
border-left: 4px solid #ff9800;
|
|
background: linear-gradient(135deg, #fff8e1, #fff3e0);
|
|
}
|
|
|
|
.result-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.result-indicator {
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.grammar-score {
|
|
font-size: 0.9em;
|
|
color: #666;
|
|
}
|
|
|
|
.correct-answer-display {
|
|
background: rgba(108, 92, 231, 0.1);
|
|
padding: 10px 15px;
|
|
border-radius: 8px;
|
|
margin-bottom: 15px;
|
|
font-weight: 500;
|
|
color: #333;
|
|
}
|
|
|
|
.explanation-text {
|
|
line-height: 1.6;
|
|
color: #333;
|
|
font-size: 1.05em;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.analysis-note, .hint-note, .attempt-note {
|
|
font-size: 0.9em;
|
|
color: #666;
|
|
font-style: italic;
|
|
padding-top: 10px;
|
|
border-top: 1px solid #eee;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.hint-note {
|
|
color: #f57c00;
|
|
}
|
|
|
|
.attempt-note {
|
|
color: #1976d2;
|
|
}
|
|
|
|
.panel-actions {
|
|
display: flex;
|
|
gap: 15px;
|
|
justify-content: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.grammar-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;
|
|
}
|
|
|
|
.accuracy-display {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.accuracy-rate {
|
|
font-size: 3em;
|
|
font-weight: bold;
|
|
display: block;
|
|
}
|
|
|
|
.results-excellent .accuracy-rate { color: #4caf50; }
|
|
.results-good .accuracy-rate { color: #ff9800; }
|
|
.results-poor .accuracy-rate { color: #f44336; }
|
|
|
|
.accuracy-label {
|
|
font-size: 1.2em;
|
|
color: #666;
|
|
}
|
|
|
|
.exercises-summary, .score-summary {
|
|
font-size: 1.1em;
|
|
color: #555;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.exercise-breakdown {
|
|
display: grid;
|
|
gap: 10px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.exercise-result {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.exercise-result.correct {
|
|
background: linear-gradient(135deg, #e8f5e8, #f1f8e9);
|
|
border-left: 4px solid #4caf50;
|
|
}
|
|
|
|
.exercise-result.incorrect {
|
|
background: linear-gradient(135deg, #fff8e1, #fff3e0);
|
|
border-left: 4px solid #ff9800;
|
|
}
|
|
|
|
.exercise-summary {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.exercise-num {
|
|
font-weight: bold;
|
|
color: #333;
|
|
}
|
|
|
|
.attempts-info, .hint-used {
|
|
font-size: 0.85em;
|
|
color: #666;
|
|
background: rgba(255,255,255,0.7);
|
|
padding: 4px 8px;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.hint-used {
|
|
color: #f57c00;
|
|
}
|
|
|
|
.results-actions {
|
|
display: flex;
|
|
gap: 15px;
|
|
justify-content: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.btn {
|
|
padding: 12px 24px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-size: 1em;
|
|
font-weight: 500;
|
|
transition: all 0.3s ease;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover:not(:disabled) {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 15px rgba(108, 92, 231, 0.3);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #6c757d;
|
|
color: white;
|
|
}
|
|
|
|
.btn-secondary:hover:not(:disabled) {
|
|
background: #5a6268;
|
|
}
|
|
|
|
.btn-outline {
|
|
background: transparent;
|
|
border: 2px solid #6c5ce7;
|
|
color: #6c5ce7;
|
|
}
|
|
|
|
.btn-outline:hover:not(:disabled) {
|
|
background: #6c5ce7;
|
|
color: white;
|
|
}
|
|
|
|
.btn-success {
|
|
background: linear-gradient(135deg, #00b894, #00cec9);
|
|
color: white;
|
|
}
|
|
|
|
.btn-success:hover:not(:disabled) {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 15px rgba(0, 184, 148, 0.3);
|
|
}
|
|
|
|
@keyframes spin {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.grammar-exercise {
|
|
padding: 15px;
|
|
}
|
|
|
|
.rule-explanation-card, .exercise-card, .explanation-panel {
|
|
padding: 20px;
|
|
}
|
|
|
|
.exercise-question {
|
|
font-size: 1.2em;
|
|
}
|
|
|
|
.accuracy-rate {
|
|
font-size: 2.5em;
|
|
}
|
|
|
|
.panel-actions, .results-actions {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.rule-header {
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.exercise-controls > div:first-child {
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
|
|
.exercise-summary {
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
}
|
|
`;
|
|
|
|
document.head.appendChild(styles);
|
|
}
|
|
|
|
// ========================================
|
|
// DRSExerciseInterface REQUIRED METHODS
|
|
// ========================================
|
|
|
|
async init(config = {}, content = {}) {
|
|
this.config = { ...this.config, ...config };
|
|
this.currentExerciseData = content;
|
|
this.startTime = Date.now();
|
|
this.initialized = true;
|
|
}
|
|
|
|
async render(container) {
|
|
if (!this.initialized) throw new Error('GrammarModule must be initialized before rendering');
|
|
await this.present(container, this.currentExerciseData);
|
|
}
|
|
|
|
async destroy() {
|
|
this.cleanup?.();
|
|
this.container = null;
|
|
this.initialized = false;
|
|
}
|
|
|
|
getResults() {
|
|
return {
|
|
score: this.progress?.averageScore ? Math.round(this.progress.averageScore * 100) : 0,
|
|
attempts: this.userResponses?.length || 0,
|
|
timeSpent: this.startTime ? Date.now() - this.startTime : 0,
|
|
completed: true,
|
|
details: { progress: this.progress, responses: this.userResponses }
|
|
};
|
|
}
|
|
|
|
handleUserInput(event, data) {
|
|
if (event?.type === 'input') this._handleInputChange?.(event);
|
|
if (event?.type === 'click' && event.target.id === 'submitButton') this._handleSubmit?.(event);
|
|
}
|
|
|
|
async markCompleted(results) {
|
|
const { score, details } = results || this.getResults();
|
|
if (this.contextMemory) {
|
|
this.contextMemory.recordInteraction({
|
|
type: 'grammar',
|
|
subtype: 'completion',
|
|
content: this.currentExerciseData,
|
|
validation: { score },
|
|
context: { moduleType: 'grammar' }
|
|
});
|
|
}
|
|
}
|
|
|
|
getExerciseType() {
|
|
return 'grammar';
|
|
}
|
|
|
|
getExerciseConfig() {
|
|
return {
|
|
type: this.getExerciseType(),
|
|
difficulty: this.currentExerciseData?.difficulty || 'medium',
|
|
estimatedTime: 5,
|
|
prerequisites: [],
|
|
metadata: { ...this.config, requiresAI: false }
|
|
};
|
|
}
|
|
|
|
}
|
|
|
|
export default GrammarModule; |