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>
845 lines
32 KiB
JavaScript
845 lines
32 KiB
JavaScript
/**
|
|
* 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 = `
|
|
<div class="translation-module">
|
|
<div class="progress-indicator" id="progressIndicator">
|
|
Translation <span id="currentTranslation">1</span> of <span id="totalTranslations">${this.currentTranslations.length}</span>
|
|
</div>
|
|
|
|
<div class="language-info" id="languageInfo">
|
|
<span class="language-from" id="languageFrom">French</span>
|
|
<span class="arrow">→</span>
|
|
<span class="language-to" id="languageTo">English</span>
|
|
</div>
|
|
|
|
<div class="source-section" id="sourceContainer">
|
|
<h3>🔤 Text to Translate</h3>
|
|
<div class="source-text" id="sourceText"></div>
|
|
<div class="context-panel" id="contextPanel" style="display: none;">
|
|
<strong>💡 Context:</strong> <span id="contextText"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="translation-section">
|
|
<h3>🌍 Your Translation</h3>
|
|
<div class="translation-container">
|
|
<textarea
|
|
id="translationArea"
|
|
placeholder="Write your translation here..."
|
|
rows="4"
|
|
></textarea>
|
|
<div class="translation-tips">
|
|
<strong>💭 Tips:</strong> Focus on meaning, not word-for-word translation. Consider cultural context and natural expression.
|
|
</div>
|
|
</div>
|
|
<button id="submitButton" class="submit-translation" disabled>
|
|
🔍 Check My Translation
|
|
</button>
|
|
</div>
|
|
|
|
<div class="feedback-section" id="feedbackContainer" style="display: none;">
|
|
<h3>📊 Translation Analysis</h3>
|
|
<div class="scores-panel" id="scoresPanel"></div>
|
|
<div class="feedback-content" id="feedbackContent"></div>
|
|
<div class="improvement-suggestions" id="improvementSuggestions"></div>
|
|
<div class="cultural-notes" id="culturalNotes"></div>
|
|
<div class="action-buttons" id="actionButtons"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 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 = `
|
|
<div class="source-content">"${translation.source}"</div>
|
|
`;
|
|
|
|
// 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 = `
|
|
<div class="translation-scores">
|
|
<div class="score-item">
|
|
<span class="score-label">Accuracy:</span>
|
|
<span class="score-value ${this._getScoreClass(result.score)}">${Math.round((result.score || 0) * 100)}%</span>
|
|
</div>
|
|
`;
|
|
|
|
if (result.fluencyScore) {
|
|
scoresHTML += `
|
|
<div class="score-item">
|
|
<span class="score-label">Fluency:</span>
|
|
<span class="score-value ${this._getScoreClass(result.fluencyScore)}">${Math.round(result.fluencyScore * 100)}%</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
scoresHTML += '</div>';
|
|
scoresPanel.innerHTML = scoresHTML;
|
|
|
|
// Format main feedback
|
|
let feedbackHTML = '';
|
|
if (result.success && result.score >= this.config.accuracyThreshold) {
|
|
feedbackHTML = `
|
|
<div class="feedback-text excellent">
|
|
<strong>✅ Excellent translation!</strong><br>
|
|
${result.feedback}
|
|
</div>
|
|
`;
|
|
} else {
|
|
feedbackHTML = `
|
|
<div class="feedback-text needs-improvement">
|
|
<strong>📝 Room for improvement:</strong><br>
|
|
${result.feedback}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
feedbackContent.innerHTML = feedbackHTML;
|
|
|
|
// Show suggestions
|
|
if (result.suggestions && result.suggestions.length > 0) {
|
|
improvementSuggestions.innerHTML = `
|
|
<div class="suggestions-panel">
|
|
<strong>💡 Suggestions for improvement:</strong>
|
|
<ul>
|
|
${result.suggestions.map(s => `<li>${s}</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Show cultural notes
|
|
let culturalHTML = '';
|
|
if (translation.culturalNotes) {
|
|
culturalHTML += `
|
|
<div class="cultural-info">
|
|
<strong>🏛️ Cultural Note:</strong> ${translation.culturalNotes}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (result.culturalContext) {
|
|
culturalHTML += `
|
|
<div class="cultural-context">
|
|
<strong>🌍 Cultural Context:</strong> ${result.culturalContext}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
culturalNotes.innerHTML = culturalHTML;
|
|
|
|
// Create action buttons
|
|
let buttonsHTML = '';
|
|
|
|
if (this.config.allowMultipleAttempts && result.score < this.config.accuracyThreshold) {
|
|
buttonsHTML += `<button class="retry-button" onclick="this.closest('.translation-module').querySelector('#translationArea').focus()">🔄 Try Again</button>`;
|
|
}
|
|
|
|
if (this.translationIndex < this.currentTranslations.length - 1) {
|
|
buttonsHTML += `<button class="next-button" onclick="this._nextTranslation()">➡️ Next Translation</button>`;
|
|
} else {
|
|
buttonsHTML += `<button class="finish-button" onclick="this._finishExercise()">🎉 Finish Exercise</button>`;
|
|
}
|
|
|
|
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 = `
|
|
<div class="error-message">
|
|
<h3>❌ Error</h3>
|
|
<p>${message}</p>
|
|
<button onclick="location.reload()">🔄 Try Again</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
_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; |