Class_generator/src/DRS/exercise-modules/PhraseModule.js
StillHammer 194d65cd76 Implement strict DRS interface system for all 11 exercise modules
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>
2025-10-08 13:43:25 +08:00

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;