/** * 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} */ 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} - 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 = `

๐Ÿ“š Grammar Practice

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

๐Ÿ“– ${ruleTitle}

${ruleType} ${this.currentGrammarRule.difficulty || 'medium'}
${this.currentGrammarRule.description || 'Grammar rule description not available.'}
${this.currentGrammarRule.examples && this.currentGrammarRule.examples.length > 0 ? `

๐Ÿ“ Examples:

    ${this.currentGrammarRule.examples.map(example => `
  • ${example}
  • ` ).join('')}
` : ''} ${this.currentGrammarRule.notes ? `

๐Ÿ’ก Important Notes:

${this.currentGrammarRule.notes}

` : ''}
`; // 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 = `
${exercise.question}
${exercise.options.map((option, index) => ` `).join('')}
`; // Hide text input for multiple choice if (answerInputSection) answerInputSection.style.display = 'none'; break; case 'fill_blank': content = `
${exercise.question}
Fill in the blank with the correct word or phrase.
`; break; case 'correction': content = `
${exercise.question}
Identify and correct the grammar error.
`; break; case 'transformation': content = `
${exercise.question}
Transform the sentence according to the grammar rule.
`; break; case 'conjugation': content = `
${exercise.question}
Conjugate the verb correctly.
`; break; case 'construction': content = `
${exercise.question}
Build a grammatically correct sentence.
`; break; default: content = `
${exercise.question}
`; break; } content += `
${this.exerciseTypes[exercise.type] || exercise.type} ${exercise.difficulty} ${exercise.points} points
`; 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 = ` โœ“ Hint Used `; } } /** * 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 ? ` ๐Ÿ“– Review Rule ` : ` โœ๏ธ Back to Exercise `; } } } /** * 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 = `
๐Ÿ”
${this.aiAvailable ? 'AI is analyzing your grammar...' : 'Checking your answer...'}
`; // 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 = `
${result.correct ? 'โœ…' : '๐Ÿ“š'} Analysis complete
`; } catch (error) { console.error('โŒ Grammar validation error:', error); // Show error status statusDiv.innerHTML = `
โš ๏ธ Error: ${error.message}
`; // Re-enable input for retry this._enableRetry(); } } /** * Show validation result in explanation panel * @private */ _showValidationResult(result) { const explanationPanel = document.getElementById('explanation-panel'); const explanationContent = document.getElementById('explanation-content'); const nextBtn = document.getElementById('next-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 = `
${result.correct ? 'โœ… Excellent Grammar!' : '๐Ÿ“š Keep Studying!'} Score: ${result.score}/100
Correct Answer: ${this.currentExercise.correctAnswer}
${result.explanation || result.feedback}
${result.grammarAnalysis ? '
๐Ÿ“š This analysis focuses on grammatical accuracy and rule application.
' : ''} ${this.hintUsed ? '
๐Ÿ’ก Remember: Using hints helps learning but reduces scores. Try to apply grammar rules independently next time!
' : ''} ${this.attempts > 1 ? '
๐ŸŽฏ Multiple attempts help reinforce learning. Review the rule and practice more!
' : ''}
`; // 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 = `

๐Ÿ“Š Grammar Practice Results

${accuracyRate}% Accuracy Rate
${correctCount} / ${totalCount} exercises completed correctly
Average Score: ${avgScore}/100 points
${this.exerciseResults.map((result, index) => `
Ex${index + 1} ${result.correct ? 'โœ…' : '๐Ÿ“š'} ${this.exerciseTypes[this.exercises[index]?.type] || 'Grammar'} Score: ${result.score}/100 Attempts: ${result.attempts} ${result.hintUsed ? '๐Ÿ’ก Hint' : ''}
`).join('')}
`; 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;