/** * TranslationModule - AI-validated translation exercises * Presents translation challenges with intelligent AI feedback on accuracy and fluency * Implements DRSExerciseInterface for strict contract enforcement */ import DRSExerciseInterface from '../interfaces/DRSExerciseInterface.js'; class TranslationModule extends DRSExerciseInterface { constructor(orchestrator, llmValidator, prerequisiteEngine, contextMemory) { super('TranslationModule'); // Validate dependencies if (!orchestrator || !llmValidator || !prerequisiteEngine || !contextMemory) { throw new Error('TranslationModule 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.currentTranslations = []; this.translationIndex = 0; this.userTranslations = []; this.validationInProgress = false; this.lastValidationResult = null; // Configuration this.config = { requiredProvider: 'openai', // Translation needs nuanced understanding model: 'gpt-4o-mini', temperature: 0.2, // Lower for accuracy in translation maxTokens: 1000, timeout: 50000, translationsPerExercise: 4, // Number of phrases to translate supportedLanguagePairs: [ { from: 'French', to: 'English' }, { from: 'English', to: 'French' }, { from: 'Spanish', to: 'English' }, { from: 'English', to: 'Spanish' } ], defaultLanguagePair: { from: 'French', to: 'English' }, allowMultipleAttempts: true, accuracyThreshold: 0.7, // Minimum score for acceptable translation showContext: true // Show context/situation for translation }; // Progress tracking this.progress = { translationsCompleted: 0, translationsAccurate: 0, averageAccuracy: 0, averageFluency: 0, vocabularyLearned: new Set(), phrasesLearned: new Set(), culturalNotesLearned: new Set(), totalTimeSpent: 0, lastActivity: null, improvementAreas: { vocabulary: 0, grammar: 0, idioms: 0, cultural: 0, fluency: 0 } }; // UI elements cache this.elements = { sourceContainer: null, sourceText: null, translationArea: null, submitButton: null, feedbackContainer: null, progressIndicator: null, contextPanel: null, languageSelector: null }; } /** * Check if module can run with current content */ canRun(prerequisites, chapterContent) { // Check if we have translation content or can generate it const hasTranslationContent = chapterContent && (chapterContent.translations || chapterContent.phrases || chapterContent.texts); if (!hasTranslationContent) { console.log('❌ TranslationModule: No translation content available'); return false; } // Check if LLM validator is available if (!this.llmValidator || !this.llmValidator.isAvailable()) { console.log('❌ TranslationModule: LLM validator not available'); return false; } console.log('βœ… TranslationModule: Can run with available content'); return true; } /** * Present exercise UI and content */ async present(container, exerciseData) { console.log('🌍 TranslationModule: Starting presentation'); this.container = container; this.currentExerciseData = exerciseData; try { // Clear container container.innerHTML = ''; // Prepare translation content await this._prepareTranslations(); // Create UI layout this._createUI(); // Show first translation this._displayCurrentTranslation(); this.initialized = true; console.log('βœ… TranslationModule: Presentation ready'); } catch (error) { console.error('❌ TranslationModule presentation failed:', error); this._showError('Failed to load translation exercise. Please try again.'); } } /** * Validate user translation with AI */ async validate(userTranslation, context) { console.log('πŸ” TranslationModule: Validating translation'); if (this.validationInProgress) { console.log('⏳ Validation already in progress'); return this.lastValidationResult; } const currentTranslation = this.currentTranslations[this.translationIndex]; // Basic input validation if (!userTranslation || userTranslation.trim().length === 0) { return { success: false, score: 0, feedback: 'Please provide a translation for the text.', suggestions: ['Read the source text carefully', 'Consider the context', 'Translate the meaning, not just words'] }; } // Check minimum effort (not just source text copied) if (userTranslation.trim().toLowerCase() === currentTranslation.source.toLowerCase()) { return { success: false, score: 0, feedback: 'You\'ve copied the original text. Please provide a translation.', suggestions: [ 'Translate the text into the target language', 'Focus on conveying the meaning', 'Don\'t just copy the original' ] }; } this.validationInProgress = true; try { // Prepare context for translation validation const translationContext = { sourceText: currentTranslation.source, targetLanguage: currentTranslation.targetLanguage || this.config.defaultLanguagePair.to, sourceLanguage: currentTranslation.sourceLanguage || this.config.defaultLanguagePair.from, userTranslation: userTranslation.trim(), context: currentTranslation.context || '', difficulty: this.currentExerciseData.difficulty || 'medium', expectedTranslation: currentTranslation.expected || null, culturalNotes: currentTranslation.culturalNotes || null, ...context }; // Use LLMValidator for translation analysis const result = await this.llmValidator.validateTranslation( currentTranslation.source, userTranslation.trim(), translationContext ); // Process and enhance the result const enhancedResult = this._processTranslationResult(result, userTranslation, currentTranslation); // Store result and update progress this.lastValidationResult = enhancedResult; this._updateProgress(enhancedResult, currentTranslation); console.log('βœ… TranslationModule: Validation completed', enhancedResult); return enhancedResult; } catch (error) { console.error('❌ TranslationModule validation failed:', error); return { success: false, score: 0, feedback: 'Unable to analyze your translation due to a technical issue. Please try again.', error: error.message, suggestions: ['Check your internet connection', 'Try a simpler translation', 'Contact support if the problem persists'] }; } finally { this.validationInProgress = false; } } /** * Get current progress */ getProgress() { return { ...this.progress, currentTranslation: this.translationIndex + 1, totalTranslations: this.currentTranslations.length, moduleType: 'translation', completionRate: this._calculateCompletionRate(), accuracyRate: this.progress.translationsCompleted > 0 ? this.progress.translationsAccurate / this.progress.translationsCompleted : 0 }; } /** * Clean up resources */ cleanup() { // Remove event listeners if (this.elements.submitButton) { this.elements.submitButton.removeEventListener('click', this._handleSubmit.bind(this)); } if (this.elements.translationArea) { this.elements.translationArea.removeEventListener('input', this._handleInputChange.bind(this)); } // Clear references this.container = null; this.currentExerciseData = null; this.elements = {}; this.initialized = false; console.log('🧹 TranslationModule: Cleaned up'); } /** * Get module metadata */ getMetadata() { return { name: 'TranslationModule', version: '1.0.0', description: 'AI-validated translation exercises', author: 'DRS System', capabilities: [ 'translation-validation', 'cultural-context', 'fluency-assessment', 'vocabulary-learning', 'progress-tracking' ], requiredServices: ['llmValidator', 'orchestrator', 'prerequisiteEngine', 'contextMemory'], supportedLanguages: ['English', 'French', 'Spanish'], exerciseTypes: ['text-translation', 'phrase-translation', 'contextual-translation'] }; } // Private methods async _prepareTranslations() { // Extract or generate translation content if (this.currentExerciseData.translations && this.currentExerciseData.translations.length > 0) { // Use provided translations this.currentTranslations = this.currentExerciseData.translations.slice(0, this.config.translationsPerExercise); } else if (this.currentExerciseData.phrases && this.currentExerciseData.phrases.length > 0) { // Use phrases for translation this.currentTranslations = this.currentExerciseData.phrases.slice(0, this.config.translationsPerExercise); } else { // Generate translations using AI await this._generateTranslationsWithAI(); } // Normalize translation format this.currentTranslations = this.currentTranslations.map(translation => { if (typeof translation === 'string') { return { source: translation, sourceLanguage: this.config.defaultLanguagePair.from, targetLanguage: this.config.defaultLanguagePair.to, context: '', difficulty: this.currentExerciseData.difficulty || 'medium' }; } return { source: translation.source || translation.text || translation, sourceLanguage: translation.sourceLanguage || translation.from || this.config.defaultLanguagePair.from, targetLanguage: translation.targetLanguage || translation.to || this.config.defaultLanguagePair.to, context: translation.context || translation.situation || '', expected: translation.expected || translation.translation || null, culturalNotes: translation.culturalNotes || translation.notes || null, difficulty: translation.difficulty || this.currentExerciseData.difficulty || 'medium' }; }); console.log('🌍 Translation content prepared:', this.currentTranslations.length); } async _generateTranslationsWithAI() { // Use the orchestrator's IAEngine to generate translation exercises try { const difficulty = this.currentExerciseData.difficulty || 'medium'; const topic = this.currentExerciseData.topic || 'everyday situations'; const languagePair = this.currentExerciseData.languagePair || this.config.defaultLanguagePair; const prompt = `Generate 4 translation exercises from ${languagePair.from} to ${languagePair.to} for ${difficulty} level learners. Topic: ${topic} Requirements: - Create practical phrases/sentences for real-world situations - Include variety: greetings, daily activities, emotions, descriptions - Make them appropriate for ${difficulty} level - Include context when helpful - Focus on useful, commonly needed expressions Return ONLY valid JSON: [ { "source": "Bonjour, comment allez-vous aujourd'hui?", "context": "Polite greeting in a formal setting", "difficulty": "${difficulty}", "culturalNotes": "Use 'vous' for formal situations" } ]`; const sharedServices = this.orchestrator.getSharedServices(); if (sharedServices && sharedServices.iaEngine) { const result = await sharedServices.iaEngine.validateEducationalContent(prompt, { systemPrompt: 'You are a language learning expert. Create realistic translation exercises.', temperature: 0.4 }); if (result && result.content) { try { this.currentTranslations = JSON.parse(result.content); console.log('βœ… Generated translations with AI:', this.currentTranslations.length); return; } catch (parseError) { console.warn('Failed to parse AI-generated translations'); } } } } catch (error) { console.warn('Failed to generate translations with AI:', error); } // Fallback: basic translations this.currentTranslations = [ { source: "Bonjour, comment allez-vous?", context: "Formal greeting", difficulty: "easy", culturalNotes: "Use 'vous' for politeness" }, { source: "Je voudrais rΓ©server une table pour ce soir.", context: "Restaurant reservation", difficulty: "medium", culturalNotes: "Conditional form shows politeness" }, { source: "Pourriez-vous m'indiquer le chemin vers la gare?", context: "Asking for directions", difficulty: "medium", culturalNotes: "Very polite way to ask for help" }, { source: "J'ai eu beaucoup de mal Γ  comprendre cette explication.", context: "Expressing difficulty in understanding", difficulty: "hard", culturalNotes: "Idiomatic expression 'avoir du mal Γ '" } ]; } _createUI() { const container = this.container; container.innerHTML = `
Translation 1 of ${this.currentTranslations.length}
French β†’ English

πŸ”€ Text to Translate

🌍 Your Translation

πŸ’­ Tips: Focus on meaning, not word-for-word translation. Consider cultural context and natural expression.
`; // Cache elements this.elements = { sourceContainer: container.querySelector('#sourceContainer'), sourceText: container.querySelector('#sourceText'), translationArea: container.querySelector('#translationArea'), submitButton: container.querySelector('#submitButton'), feedbackContainer: container.querySelector('#feedbackContainer'), progressIndicator: container.querySelector('#progressIndicator'), contextPanel: container.querySelector('#contextPanel') }; // Add event listeners this.elements.translationArea.addEventListener('input', this._handleInputChange.bind(this)); this.elements.submitButton.addEventListener('click', this._handleSubmit.bind(this)); } _displayCurrentTranslation() { const translation = this.currentTranslations[this.translationIndex]; // Update language information document.getElementById('languageFrom').textContent = translation.sourceLanguage || 'French'; document.getElementById('languageTo').textContent = translation.targetLanguage || 'English'; // Update source text this.elements.sourceText.innerHTML = `
"${translation.source}"
`; // Update context if available if (translation.context) { document.getElementById('contextText').textContent = translation.context; this.elements.contextPanel.style.display = 'block'; } else { this.elements.contextPanel.style.display = 'none'; } // Update progress document.getElementById('currentTranslation').textContent = this.translationIndex + 1; document.getElementById('totalTranslations').textContent = this.currentTranslations.length; // Reset translation area this.elements.translationArea.value = ''; this.elements.translationArea.disabled = false; this.elements.submitButton.disabled = true; // Hide previous feedback this.elements.feedbackContainer.style.display = 'none'; // Focus on translation area setTimeout(() => this.elements.translationArea.focus(), 100); } _handleInputChange() { const userInput = this.elements.translationArea.value.trim(); this.elements.submitButton.disabled = userInput.length === 0; } async _handleSubmit() { const userTranslation = this.elements.translationArea.value.trim(); const currentTranslation = this.currentTranslations[this.translationIndex]; if (!userTranslation) { this._showValidationError('Please provide a translation.'); return; } // Disable UI during validation this.elements.translationArea.disabled = true; this.elements.submitButton.disabled = true; this.elements.submitButton.textContent = 'πŸ”„ Analyzing...'; try { // Validate with AI const result = await this.validate(userTranslation, { context: currentTranslation.context, expectedTranslation: currentTranslation.expected, culturalNotes: currentTranslation.culturalNotes }); // Show feedback this._displayFeedback(result, currentTranslation); // Store translation this.userTranslations[this.translationIndex] = { source: currentTranslation.source, userTranslation: userTranslation, result: result, timestamp: new Date().toISOString() }; } catch (error) { console.error('Validation error:', error); this._showValidationError('Failed to analyze your translation. Please try again.'); } // Re-enable UI this.elements.translationArea.disabled = false; this.elements.submitButton.textContent = 'πŸ” Check My Translation'; this._handleInputChange(); } _displayFeedback(result, translation) { const scoresPanel = document.getElementById('scoresPanel'); const feedbackContent = document.getElementById('feedbackContent'); const improvementSuggestions = document.getElementById('improvementSuggestions'); const culturalNotes = document.getElementById('culturalNotes'); const actionButtons = document.getElementById('actionButtons'); // Format scores let scoresHTML = `
Accuracy: ${Math.round((result.score || 0) * 100)}%
`; if (result.fluencyScore) { scoresHTML += `
Fluency: ${Math.round(result.fluencyScore * 100)}%
`; } scoresHTML += '
'; scoresPanel.innerHTML = scoresHTML; // Format main feedback let feedbackHTML = ''; if (result.success && result.score >= this.config.accuracyThreshold) { feedbackHTML = `
βœ… Excellent translation!
${result.feedback}
`; } else { feedbackHTML = `
πŸ“ Room for improvement:
${result.feedback}
`; } feedbackContent.innerHTML = feedbackHTML; // Show suggestions if (result.suggestions && result.suggestions.length > 0) { improvementSuggestions.innerHTML = `
πŸ’‘ Suggestions for improvement:
`; } // Show cultural notes let culturalHTML = ''; if (translation.culturalNotes) { culturalHTML += `
πŸ›οΈ Cultural Note: ${translation.culturalNotes}
`; } if (result.culturalContext) { culturalHTML += `
🌍 Cultural Context: ${result.culturalContext}
`; } culturalNotes.innerHTML = culturalHTML; // Create action buttons let buttonsHTML = ''; if (this.config.allowMultipleAttempts && result.score < this.config.accuracyThreshold) { buttonsHTML += ``; } if (this.translationIndex < this.currentTranslations.length - 1) { buttonsHTML += ``; } else { buttonsHTML += ``; } actionButtons.innerHTML = buttonsHTML; // Show feedback section this.elements.feedbackContainer.style.display = 'block'; this.elements.feedbackContainer.scrollIntoView({ behavior: 'smooth' }); } _nextTranslation() { if (this.translationIndex < this.currentTranslations.length - 1) { this.translationIndex++; this._displayCurrentTranslation(); } } _finishExercise() { // Calculate final statistics const totalAccurate = this.userTranslations.filter(t => t.result && t.result.score >= this.config.accuracyThreshold ).length; const averageAccuracy = this.userTranslations.reduce((sum, t) => sum + (t.result?.score || 0), 0) / this.userTranslations.length; const completionData = { moduleType: 'translation', translations: this.userTranslations, progress: this.getProgress(), finalStats: { totalTranslations: this.currentTranslations.length, accurateTranslations: totalAccurate, averageAccuracy: averageAccuracy, vocabularyLearned: this.progress.vocabularyLearned.size, phrasesLearned: this.progress.phrasesLearned.size, improvementAreas: this.progress.improvementAreas } }; // Use orchestrator to handle completion if (this.orchestrator && this.orchestrator.handleExerciseCompletion) { this.orchestrator.handleExerciseCompletion(completionData); } console.log('πŸŽ‰ TranslationModule: Exercise completed', completionData); } _processTranslationResult(result, userTranslation, translation) { // Enhance the raw LLM result const enhanced = { ...result, inputLength: userTranslation.length, sourceLength: translation.source.length, timestamp: new Date().toISOString(), moduleType: 'translation', languagePair: { from: translation.sourceLanguage, to: translation.targetLanguage } }; return enhanced; } _updateProgress(result, translation) { this.progress.translationsCompleted++; this.progress.lastActivity = new Date().toISOString(); // Count as accurate if above threshold if (result.score >= this.config.accuracyThreshold) { this.progress.translationsAccurate++; } // Update averages const totalAccuracy = this.progress.averageAccuracy * (this.progress.translationsCompleted - 1) + (result.score || 0); this.progress.averageAccuracy = totalAccuracy / this.progress.translationsCompleted; if (result.fluencyScore) { const totalFluency = this.progress.averageFluency * (this.progress.translationsCompleted - 1) + result.fluencyScore; this.progress.averageFluency = totalFluency / this.progress.translationsCompleted; } // Track vocabulary and phrases if (result.newVocabulary) { result.newVocabulary.forEach(word => this.progress.vocabularyLearned.add(word)); } if (translation.culturalNotes) { this.progress.culturalNotesLearned.add(translation.culturalNotes); } } _calculateCompletionRate() { if (!this.currentTranslations || this.currentTranslations.length === 0) return 0; return (this.translationIndex + 1) / this.currentTranslations.length; } _getScoreClass(score) { if (score >= 0.8) return 'excellent'; if (score >= 0.6) return 'good'; if (score >= 0.4) return 'fair'; return 'needs-work'; } _showError(message) { this.container.innerHTML = `

❌ Error

${message}

`; } _showValidationError(message) { // Show temporary error message const errorDiv = document.createElement('div'); errorDiv.className = 'validation-error'; errorDiv.textContent = message; errorDiv.style.cssText = 'color: #e74c3c; padding: 10px; margin: 10px 0; border: 1px solid #e74c3c; border-radius: 4px; background: #ffebee;'; this.elements.translationArea.parentNode.insertBefore(errorDiv, this.elements.translationArea); setTimeout(() => errorDiv.remove(), 5000); } // ======================================== // 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('TranslationModule must be initialized before rendering'); await this.present(container, this.currentExerciseData); } async destroy() { this.cleanup(); } getResults() { const totalPhrases = this.currentPhrases ? this.currentPhrases.length : 0; const translatedPhrases = this.userTranslations ? this.userTranslations.length : 0; const score = this.progress ? Math.round(this.progress.averageAccuracy * 100) : 0; return { score, attempts: translatedPhrases, timeSpent: this.startTime ? Date.now() - this.startTime : 0, completed: translatedPhrases >= totalPhrases, details: { totalPhrases, translatedPhrases, averageAccuracy: this.progress?.averageAccuracy || 0, translations: this.userTranslations || [] } }; } handleUserInput(event, data) { if (event && event.type === 'input') this._handleInputChange?.(event); if (event && 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: 'translation', subtype: 'completion', content: { phrases: this.currentPhrases }, userResponse: this.userTranslations, validation: { score, averageAccuracy: details.averageAccuracy }, context: { moduleType: 'translation', totalPhrases: details.totalPhrases } }); } } getExerciseType() { return 'translation'; } getExerciseConfig() { const phraseCount = this.currentPhrases ? this.currentPhrases.length : this.config?.phrasesPerExercise || 3; return { type: this.getExerciseType(), difficulty: this.currentExerciseData?.difficulty || 'medium', estimatedTime: phraseCount * 2, // 2 min per phrase prerequisites: [], metadata: { ...this.config, phraseCount, requiresAI: true } }; } } export default TranslationModule;