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>
980 lines
32 KiB
JavaScript
980 lines
32 KiB
JavaScript
/**
|
|
* PhraseModule - Individual phrase comprehension with mandatory AI validation
|
|
* Uses GPT-4-mini only, no fallbacks, structured response format
|
|
*/
|
|
|
|
import DRSExerciseInterface from '../interfaces/DRSExerciseInterface.js';
|
|
|
|
class PhraseModule extends DRSExerciseInterface {
|
|
constructor(orchestrator, llmValidator, prerequisiteEngine, contextMemory) {
|
|
super('PhraseModule');
|
|
|
|
// Validate dependencies
|
|
if (!orchestrator || !llmValidator || !prerequisiteEngine || !contextMemory) {
|
|
throw new Error('PhraseModule 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.currentPhrase = null;
|
|
this.validationInProgress = false;
|
|
this.lastValidationResult = null;
|
|
|
|
// Configuration - AI ONLY, no fallbacks
|
|
this.config = {
|
|
requiredProvider: 'openai', // GPT-4-mini only
|
|
model: 'gpt-4o-mini',
|
|
temperature: 0.1, // Very low for consistent evaluation
|
|
maxTokens: 500,
|
|
timeout: 30000,
|
|
noFallback: true // Critical: No mocks allowed
|
|
};
|
|
|
|
// Languages configuration
|
|
this.languages = {
|
|
userLanguage: 'English', // User's native language
|
|
targetLanguage: 'French' // Target learning language
|
|
};
|
|
|
|
// Bind methods
|
|
this._handleUserInput = this._handleUserInput.bind(this);
|
|
this._handleRetry = this._handleRetry.bind(this);
|
|
this._handleNextPhrase = this._handleNextPhrase.bind(this);
|
|
}
|
|
|
|
async init() {
|
|
if (this.initialized) return;
|
|
|
|
console.log('💬 Initializing PhraseModule...');
|
|
|
|
// Test AI connectivity - recommandé mais pas obligatoire
|
|
try {
|
|
const testResult = await this.llmValidator.testConnectivity();
|
|
if (testResult.success) {
|
|
console.log(`✅ AI connectivity verified (providers: ${testResult.availableProviders?.join(', ') || testResult.provider})`);
|
|
this.aiAvailable = true;
|
|
} else {
|
|
console.warn('⚠️ AI connection failed - will use mock validation:', testResult.error);
|
|
this.aiAvailable = false;
|
|
}
|
|
} catch (error) {
|
|
console.warn('⚠️ AI connectivity test failed - will use mock validation:', error.message);
|
|
this.aiAvailable = false;
|
|
}
|
|
|
|
this.initialized = true;
|
|
console.log(`✅ PhraseModule initialized (AI: ${this.aiAvailable ? 'available' : 'disabled - using mock mode'})`);
|
|
}
|
|
|
|
/**
|
|
* 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 phrases and if prerequisites allow them
|
|
const phrases = chapterContent?.phrases || [];
|
|
if (phrases.length === 0) return false;
|
|
|
|
// Find phrases that can be unlocked with current prerequisites
|
|
const availablePhrases = phrases.filter(phrase => {
|
|
const unlockStatus = this.prerequisiteEngine.canUnlock('phrase', phrase);
|
|
return unlockStatus.canUnlock;
|
|
});
|
|
|
|
return availablePhrases.length > 0;
|
|
}
|
|
|
|
/**
|
|
* Present exercise UI and content
|
|
* @param {HTMLElement} container - DOM container to render into
|
|
* @param {Object} exerciseData - Specific exercise data to present
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async present(container, exerciseData) {
|
|
if (!this.initialized) {
|
|
throw new Error('PhraseModule must be initialized before use');
|
|
}
|
|
|
|
this.container = container;
|
|
this.currentExerciseData = exerciseData;
|
|
this.currentPhrase = exerciseData.phrase;
|
|
this.validationInProgress = false;
|
|
this.lastValidationResult = null;
|
|
|
|
// Detect languages from chapter content
|
|
this._detectLanguages(exerciseData);
|
|
|
|
console.log(`💬 Presenting phrase exercise: "${this.currentPhrase?.english || this.currentPhrase?.text}"`);
|
|
|
|
// Render initial UI
|
|
await this._renderPhraseExercise();
|
|
}
|
|
|
|
/**
|
|
* Validate user input with mandatory AI (GPT-4-mini)
|
|
* @param {string} userInput - User's response
|
|
* @param {Object} context - Exercise context
|
|
* @returns {Promise<ValidationResult>} - Structured validation result
|
|
*/
|
|
async validate(userInput, context) {
|
|
if (!userInput || !userInput.trim()) {
|
|
throw new Error('Please provide an answer');
|
|
}
|
|
|
|
if (!this.currentPhrase) {
|
|
throw new Error('No phrase loaded for validation');
|
|
}
|
|
|
|
console.log(`🧠 AI validation: "${this.currentPhrase.english}" -> "${userInput}"`);
|
|
|
|
// Build structured prompt for GPT-4-mini
|
|
const prompt = this._buildStructuredPrompt(userInput);
|
|
|
|
try {
|
|
// Direct call to IAEngine with strict parameters
|
|
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 a language learning evaluator. ALWAYS respond in the exact format: [answer]yes/no [explanation]your explanation here`
|
|
});
|
|
|
|
// Parse structured response
|
|
const parsedResult = this._parseStructuredResponse(aiResponse);
|
|
|
|
// Record interaction in context memory
|
|
this.contextMemory.recordInteraction({
|
|
type: 'phrase',
|
|
subtype: 'comprehension',
|
|
content: {
|
|
phrase: this.currentPhrase,
|
|
originalText: this.currentPhrase.english || this.currentPhrase.text,
|
|
targetLanguage: this.languages.targetLanguage
|
|
},
|
|
userResponse: userInput.trim(),
|
|
validation: parsedResult,
|
|
context: { languages: this.languages }
|
|
});
|
|
|
|
return parsedResult;
|
|
|
|
} catch (error) {
|
|
console.error('❌ AI validation failed:', error);
|
|
|
|
// No fallback allowed - throw error to user
|
|
throw new Error(`AI validation failed: ${error.message}. Please check connection and retry.`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current progress data
|
|
* @returns {ProgressData} - Progress information for this module
|
|
*/
|
|
getProgress() {
|
|
return {
|
|
type: 'phrase',
|
|
currentPhrase: this.currentPhrase?.english || 'None',
|
|
validationStatus: this.validationInProgress ? 'validating' : 'ready',
|
|
lastResult: this.lastValidationResult,
|
|
aiProvider: this.config.requiredProvider,
|
|
languages: this.languages
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Clean up and prepare for unloading
|
|
*/
|
|
cleanup() {
|
|
console.log('🧹 Cleaning up PhraseModule...');
|
|
|
|
// Remove event listeners
|
|
if (this.container) {
|
|
this.container.innerHTML = '';
|
|
}
|
|
|
|
// Reset state
|
|
this.container = null;
|
|
this.currentExerciseData = null;
|
|
this.currentPhrase = null;
|
|
this.validationInProgress = false;
|
|
this.lastValidationResult = null;
|
|
|
|
console.log('✅ PhraseModule cleaned up');
|
|
}
|
|
|
|
// Private Methods
|
|
|
|
/**
|
|
* Detect languages from exercise data
|
|
* @private
|
|
*/
|
|
_detectLanguages(exerciseData) {
|
|
// Try to detect from chapter content or use defaults
|
|
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;
|
|
}
|
|
|
|
// Fallback detection from phrase content
|
|
if (this.currentPhrase?.user_language) {
|
|
this.languages.targetLanguage = this.currentPhrase.user_language;
|
|
}
|
|
|
|
console.log(`🌍 Languages detected: ${this.languages.userLanguage} -> ${this.languages.targetLanguage}`);
|
|
}
|
|
|
|
/**
|
|
* Build structured prompt for GPT-4-mini
|
|
* @private
|
|
*/
|
|
_buildStructuredPrompt(userAnswer) {
|
|
const originalText = this.currentPhrase.english || this.currentPhrase.text || '';
|
|
const expectedTranslation = this.currentPhrase.user_language || this.currentPhrase.translation || '';
|
|
|
|
return `You are evaluating a ${this.languages.userLanguage}/${this.languages.targetLanguage} phrase comprehension exercise.
|
|
|
|
CRITICAL: You MUST respond in this EXACT format: [answer]yes/no [explanation]your explanation here
|
|
|
|
Evaluate this student response:
|
|
- Original phrase (${this.languages.userLanguage}): "${originalText}"
|
|
- Expected meaning (${this.languages.targetLanguage}): "${expectedTranslation}"
|
|
- Student answer: "${userAnswer}"
|
|
- Context: Individual phrase comprehension exercise
|
|
|
|
Rules:
|
|
- [answer]yes if the student captured the essential meaning (even if not word-perfect)
|
|
- [answer]no if the meaning is wrong, missing, or completely off-topic
|
|
- [explanation] must be encouraging, educational, and constructive
|
|
- Focus on comprehension, not perfect translation
|
|
|
|
Format: [answer]yes/no [explanation]your detailed feedback here`;
|
|
}
|
|
|
|
/**
|
|
* Parse structured AI response
|
|
* @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 response:', responseText.substring(0, 200) + '...');
|
|
|
|
// 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();
|
|
|
|
const result = {
|
|
score: isCorrect ? 85 : 45, // High score for yes, low for no
|
|
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
|
|
};
|
|
|
|
console.log(`✅ AI response parsed: ${result.answer} - "${result.explanation.substring(0, 50)}..."`);
|
|
return result;
|
|
|
|
} catch (error) {
|
|
console.error('❌ Failed to parse AI response:', error);
|
|
console.error('Raw response:', aiResponse);
|
|
|
|
throw new Error(`AI response format invalid: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render the phrase exercise interface
|
|
* @private
|
|
*/
|
|
async _renderPhraseExercise() {
|
|
if (!this.container || !this.currentPhrase) return;
|
|
|
|
const originalText = this.currentPhrase.english || this.currentPhrase.text || 'No phrase text';
|
|
const pronunciation = this.currentPhrase.pronunciation || '';
|
|
|
|
this.container.innerHTML = `
|
|
<div class="phrase-exercise">
|
|
<div class="exercise-header">
|
|
<h2>💬 Phrase Comprehension</h2>
|
|
<div class="language-info">
|
|
<span class="source-lang">${this.languages.userLanguage}</span>
|
|
<span class="arrow">→</span>
|
|
<span class="target-lang">${this.languages.targetLanguage}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="phrase-content">
|
|
<div class="phrase-card">
|
|
<div class="phrase-display">
|
|
<div class="phrase-text">"${originalText}"</div>
|
|
${pronunciation ? `<div class="phrase-pronunciation">[${pronunciation}]</div>` : ''}
|
|
${!this.aiAvailable ? `
|
|
<div class="ai-status-warning">
|
|
⚠️ AI validation unavailable - using mock mode
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
|
|
<div class="comprehension-input">
|
|
<label for="comprehension-input">
|
|
What does this phrase mean in ${this.languages.targetLanguage}?
|
|
</label>
|
|
<textarea
|
|
id="comprehension-input"
|
|
placeholder="Enter your understanding of this phrase..."
|
|
rows="3"
|
|
autocomplete="off"
|
|
></textarea>
|
|
</div>
|
|
|
|
<div class="phrase-controls">
|
|
<button id="validate-btn" class="btn btn-primary" disabled>
|
|
<span class="btn-icon">${this.aiAvailable ? '🧠' : '🎭'}</span>
|
|
<span class="btn-text">${this.aiAvailable ? 'Validate with AI' : 'Validate (Mock Mode)'}</span>
|
|
</button>
|
|
<div id="validation-status" class="validation-status"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="explanation-panel" id="explanation-panel" style="display: none;">
|
|
<div class="panel-header">
|
|
<h3>🤖 AI Explanation</h3>
|
|
<span class="ai-model">${this.config.model}</span>
|
|
</div>
|
|
<div class="explanation-content" id="explanation-content">
|
|
<!-- AI explanation will appear here -->
|
|
</div>
|
|
<div class="panel-actions">
|
|
<button id="next-phrase-btn" class="btn btn-primary" style="display: none;">
|
|
Continue to Next Exercise
|
|
</button>
|
|
<button id="retry-btn" class="btn btn-secondary" style="display: none;">
|
|
Try Another Answer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Add CSS styles
|
|
this._addStyles();
|
|
|
|
// Add event listeners
|
|
this._setupEventListeners();
|
|
}
|
|
|
|
/**
|
|
* Setup event listeners
|
|
* @private
|
|
*/
|
|
_setupEventListeners() {
|
|
const input = document.getElementById('comprehension-input');
|
|
const validateBtn = document.getElementById('validate-btn');
|
|
const retryBtn = document.getElementById('retry-btn');
|
|
const nextBtn = document.getElementById('next-phrase-btn');
|
|
|
|
// Enable validate button when input has text
|
|
if (input) {
|
|
input.addEventListener('input', () => {
|
|
const hasText = input.value.trim().length > 0;
|
|
if (validateBtn) {
|
|
validateBtn.disabled = !hasText || this.validationInProgress;
|
|
}
|
|
});
|
|
|
|
// Allow Enter to validate (with Shift+Enter for new line)
|
|
input.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter' && !e.shiftKey && !validateBtn.disabled) {
|
|
e.preventDefault();
|
|
this._handleUserInput();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Validate button
|
|
if (validateBtn) {
|
|
validateBtn.onclick = this._handleUserInput;
|
|
}
|
|
|
|
// Retry button
|
|
if (retryBtn) {
|
|
retryBtn.onclick = this._handleRetry;
|
|
}
|
|
|
|
// Next button
|
|
if (nextBtn) {
|
|
nextBtn.onclick = this._handleNextPhrase;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle user input validation
|
|
* @private
|
|
*/
|
|
async _handleUserInput() {
|
|
const input = document.getElementById('comprehension-input');
|
|
const validateBtn = document.getElementById('validate-btn');
|
|
const statusDiv = document.getElementById('validation-status');
|
|
|
|
if (!input || !validateBtn || !statusDiv) return;
|
|
|
|
const userInput = input.value.trim();
|
|
if (!userInput) return;
|
|
|
|
try {
|
|
// Set validation in progress
|
|
this.validationInProgress = true;
|
|
validateBtn.disabled = true;
|
|
input.disabled = true;
|
|
|
|
// Show loading status
|
|
statusDiv.innerHTML = `
|
|
<div class="status-loading">
|
|
<div class="loading-spinner">🧠</div>
|
|
<span>AI is evaluating your answer...</span>
|
|
</div>
|
|
`;
|
|
|
|
// Call AI validation
|
|
const result = await this.validate(userInput, {});
|
|
this.lastValidationResult = result;
|
|
|
|
// Show result in explanation panel
|
|
this._showExplanation(result);
|
|
|
|
// Update status
|
|
statusDiv.innerHTML = `
|
|
<div class="status-complete">
|
|
<span class="result-icon">${result.correct ? '✅' : '❌'}</span>
|
|
<span>AI evaluation complete</span>
|
|
</div>
|
|
`;
|
|
|
|
} catch (error) {
|
|
console.error('❌ Validation error:', error);
|
|
|
|
// Show error status
|
|
statusDiv.innerHTML = `
|
|
<div class="status-error">
|
|
<span class="error-icon">⚠️</span>
|
|
<span>Error: ${error.message}</span>
|
|
</div>
|
|
`;
|
|
|
|
// Re-enable input for retry
|
|
this._enableRetry();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show AI explanation in dedicated panel
|
|
* @private
|
|
*/
|
|
_showExplanation(result) {
|
|
const explanationPanel = document.getElementById('explanation-panel');
|
|
const explanationContent = document.getElementById('explanation-content');
|
|
const nextBtn = document.getElementById('next-phrase-btn');
|
|
const retryBtn = document.getElementById('retry-btn');
|
|
|
|
if (!explanationPanel || !explanationContent) return;
|
|
|
|
// Show panel
|
|
explanationPanel.style.display = 'block';
|
|
|
|
// Set explanation content (read-only)
|
|
explanationContent.innerHTML = `
|
|
<div class="explanation-result ${result.correct ? 'correct' : 'incorrect'}">
|
|
<div class="result-header">
|
|
<span class="result-indicator">${result.correct ? '✅ Correct!' : '❌ Not quite right'}</span>
|
|
<span class="ai-confidence">Score: ${result.score}/100</span>
|
|
</div>
|
|
<div class="explanation-text">${result.explanation}</div>
|
|
</div>
|
|
`;
|
|
|
|
// Show appropriate buttons
|
|
if (nextBtn) nextBtn.style.display = result.correct ? 'inline-block' : 'none';
|
|
if (retryBtn) retryBtn.style.display = result.correct ? 'none' : 'inline-block';
|
|
|
|
// Scroll to explanation
|
|
explanationPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
}
|
|
|
|
/**
|
|
* Enable retry after error
|
|
* @private
|
|
*/
|
|
_enableRetry() {
|
|
this.validationInProgress = false;
|
|
|
|
const input = document.getElementById('comprehension-input');
|
|
const validateBtn = document.getElementById('validate-btn');
|
|
|
|
if (input) {
|
|
input.disabled = false;
|
|
input.focus();
|
|
}
|
|
|
|
if (validateBtn) {
|
|
validateBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle retry button
|
|
* @private
|
|
*/
|
|
_handleRetry() {
|
|
// Hide explanation panel and enable new input
|
|
const explanationPanel = document.getElementById('explanation-panel');
|
|
const statusDiv = document.getElementById('validation-status');
|
|
|
|
if (explanationPanel) explanationPanel.style.display = 'none';
|
|
if (statusDiv) statusDiv.innerHTML = '';
|
|
|
|
this._enableRetry();
|
|
}
|
|
|
|
/**
|
|
* Handle next phrase button
|
|
* @private
|
|
*/
|
|
_handleNextPhrase() {
|
|
// Mark phrase as completed and continue to next exercise
|
|
if (this.currentPhrase && this.lastValidationResult) {
|
|
const phraseId = this.currentPhrase.id || this.currentPhrase.english || 'unknown';
|
|
const metadata = {
|
|
difficulty: this.lastValidationResult.correct ? 'easy' : 'hard',
|
|
sessionId: this.orchestrator?.sessionId || 'unknown',
|
|
moduleType: 'phrase',
|
|
aiScore: this.lastValidationResult.score,
|
|
correct: this.lastValidationResult.correct,
|
|
provider: this.lastValidationResult.provider || 'openai'
|
|
};
|
|
|
|
this.prerequisiteEngine.markPhraseMastered(phraseId, metadata);
|
|
|
|
// Also save to persistent storage if phrase was correctly understood
|
|
if (this.lastValidationResult.correct && window.addMasteredItem &&
|
|
this.orchestrator?.bookId && this.orchestrator?.chapterId) {
|
|
window.addMasteredItem(
|
|
this.orchestrator.bookId,
|
|
this.orchestrator.chapterId,
|
|
'phrases',
|
|
phraseId,
|
|
metadata
|
|
);
|
|
}
|
|
}
|
|
|
|
// Emit completion event to orchestrator
|
|
this.orchestrator._eventBus.emit('drs:exerciseCompleted', {
|
|
moduleType: 'phrase',
|
|
result: this.lastValidationResult,
|
|
progress: this.getProgress()
|
|
}, 'PhraseModule');
|
|
}
|
|
|
|
/**
|
|
* Add CSS styles for phrase exercise
|
|
* @private
|
|
*/
|
|
_addStyles() {
|
|
if (document.getElementById('phrase-module-styles')) return;
|
|
|
|
const styles = document.createElement('style');
|
|
styles.id = 'phrase-module-styles';
|
|
styles.textContent = `
|
|
.phrase-exercise {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
display: grid;
|
|
gap: 20px;
|
|
}
|
|
|
|
.exercise-header {
|
|
text-align: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.language-info {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
margin-top: 10px;
|
|
font-size: 0.9em;
|
|
color: #666;
|
|
}
|
|
|
|
.phrase-content {
|
|
display: grid;
|
|
gap: 20px;
|
|
}
|
|
|
|
.phrase-card {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 30px;
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.phrase-display {
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
padding: 20px;
|
|
background: linear-gradient(135deg, #f8f9ff, #e8f4fd);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.phrase-text {
|
|
font-size: 1.8em;
|
|
font-weight: 600;
|
|
color: #2c3e50;
|
|
margin-bottom: 10px;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.phrase-pronunciation {
|
|
font-style: italic;
|
|
color: #666;
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.comprehension-input {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.comprehension-input label {
|
|
display: block;
|
|
margin-bottom: 10px;
|
|
font-weight: 600;
|
|
color: #555;
|
|
}
|
|
|
|
.comprehension-input textarea {
|
|
width: 100%;
|
|
padding: 15px;
|
|
font-size: 1.1em;
|
|
border: 2px solid #ddd;
|
|
border-radius: 8px;
|
|
resize: vertical;
|
|
min-height: 80px;
|
|
box-sizing: border-box;
|
|
transition: border-color 0.3s ease;
|
|
}
|
|
|
|
.comprehension-input textarea:focus {
|
|
outline: none;
|
|
border-color: #667eea;
|
|
}
|
|
|
|
.phrase-controls {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.validation-status {
|
|
min-height: 30px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.status-loading, .status-complete, .status-error {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px 20px;
|
|
border-radius: 20px;
|
|
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;
|
|
}
|
|
|
|
@keyframes spin {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.explanation-panel {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 25px;
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
|
border-left: 4px solid #667eea;
|
|
}
|
|
|
|
.panel-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
.panel-header h3 {
|
|
margin: 0;
|
|
color: #333;
|
|
}
|
|
|
|
.ai-model {
|
|
font-size: 0.9em;
|
|
color: #666;
|
|
background: #f5f5f5;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.explanation-content {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.explanation-result.correct {
|
|
border-left: 4px solid #4caf50;
|
|
background: linear-gradient(135deg, #f1f8e9, #e8f5e8);
|
|
}
|
|
|
|
.explanation-result.incorrect {
|
|
border-left: 4px solid #f44336;
|
|
background: linear-gradient(135deg, #fff3e0, #ffebee);
|
|
}
|
|
|
|
.explanation-result {
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.result-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.result-indicator {
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.ai-confidence {
|
|
font-size: 0.9em;
|
|
color: #666;
|
|
}
|
|
|
|
.explanation-text {
|
|
line-height: 1.6;
|
|
color: #333;
|
|
font-size: 1.05em;
|
|
}
|
|
|
|
.panel-actions {
|
|
display: flex;
|
|
gap: 15px;
|
|
justify-content: center;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover:not(:disabled) {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #6c757d;
|
|
color: white;
|
|
}
|
|
|
|
.btn-secondary:hover:not(:disabled) {
|
|
background: #5a6268;
|
|
}
|
|
|
|
.ai-status-warning {
|
|
background: linear-gradient(135deg, #fff3cd, #ffeaa7);
|
|
border: 1px solid #ffc107;
|
|
border-radius: 8px;
|
|
padding: 10px 15px;
|
|
margin: 10px 0;
|
|
text-align: center;
|
|
font-size: 0.9em;
|
|
color: #856404;
|
|
font-weight: 500;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.phrase-exercise {
|
|
padding: 15px;
|
|
}
|
|
|
|
.phrase-card, .explanation-panel {
|
|
padding: 20px;
|
|
}
|
|
|
|
.phrase-text {
|
|
font-size: 1.5em;
|
|
}
|
|
|
|
.panel-actions {
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
`;
|
|
|
|
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('PhraseModule 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: 'phrase',
|
|
subtype: 'completion',
|
|
content: this.currentExerciseData,
|
|
validation: { score },
|
|
context: { moduleType: 'phrase' }
|
|
});
|
|
}
|
|
}
|
|
|
|
getExerciseType() {
|
|
return 'phrase';
|
|
}
|
|
|
|
getExerciseConfig() {
|
|
return {
|
|
type: this.getExerciseType(),
|
|
difficulty: this.currentExerciseData?.difficulty || 'medium',
|
|
estimatedTime: 5,
|
|
prerequisites: [],
|
|
metadata: { ...this.config, requiresAI: false }
|
|
};
|
|
}
|
|
|
|
}
|
|
|
|
export default PhraseModule; |