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

πŸ’¬ Phrase Comprehension

${this.languages.userLanguage} β†’ ${this.languages.targetLanguage}
"${originalText}"
${pronunciation ? `
[${pronunciation}]
` : ''} ${!this.aiAvailable ? `
⚠️ AI validation unavailable - using mock mode
` : ''}
`; // 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 = `
🧠
AI is evaluating your answer...
`; // Call AI validation const result = await this.validate(userInput, {}); this.lastValidationResult = result; // Show result in explanation panel this._showExplanation(result); // Update status statusDiv.innerHTML = `
${result.correct ? 'βœ…' : '❌'} AI evaluation complete
`; } catch (error) { console.error('❌ Validation error:', error); // Show error status statusDiv.innerHTML = `
⚠️ Error: ${error.message}
`; // 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 = `
${result.correct ? 'βœ… Correct!' : '❌ Not quite right'} Score: ${result.score}/100
${result.explanation}
`; // 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;