/** * SmartPreviewOrchestrator - Main controller for Dynamic Revision System * Manages dynamic loading/unloading of exercise modules and coordinates shared services */ import Module from '../core/Module.js'; const privateData = new WeakMap(); class SmartPreviewOrchestrator extends Module { constructor(name, dependencies, config) { super(name, ['eventBus', 'contentLoader']); // Validate dependencies if (!dependencies.eventBus) { throw new Error('SmartPreviewOrchestrator requires EventBus dependency'); } if (!dependencies.contentLoader) { throw new Error('SmartPreviewOrchestrator requires ContentLoader dependency'); } // Store dependencies and configuration this._eventBus = dependencies.eventBus; this._contentLoader = dependencies.contentLoader; this._config = config || {}; // Initialize private data privateData.set(this, { loadedModules: new Map(), availableModules: new Map(), currentModule: null, sharedServices: { llmValidator: null, prerequisiteEngine: null, contextMemory: null, aiReportInterface: null }, sessionState: { currentChapter: null, chapterContent: null, masteredVocabulary: new Set(), masteredPhrases: new Set(), masteredGrammar: new Set(), sessionProgress: {}, exerciseSequence: [], sequenceIndex: 0 }, moduleRegistry: { 'vocabulary': './exercise-modules/VocabularyModule.js', 'phrase': './exercise-modules/PhraseModule.js', 'text': './exercise-modules/TextModule.js', 'text-analysis': './exercise-modules/TextAnalysisModule.js', 'audio': './exercise-modules/AudioModule.js', 'image': './exercise-modules/ImageModule.js', 'grammar': './exercise-modules/GrammarModule.js', 'grammar-analysis': './exercise-modules/GrammarAnalysisModule.js', 'translation': './exercise-modules/TranslationModule.js' } }); Object.seal(this); } async init() { this._validateNotDestroyed(); try { console.log('๐ŸŽฏ Initializing Smart Preview Orchestrator...'); // Initialize shared services await this._initializeSharedServices(); // Set up event listeners this._setupEventListeners(); // Register available module types this._registerModuleTypes(); this._setInitialized(); console.log('โœ… Smart Preview Orchestrator initialized successfully'); } catch (error) { console.error('โŒ SmartPreviewOrchestrator initialization failed:', error); throw error; } } async destroy() { this._validateNotDestroyed(); try { console.log('๐Ÿงน Cleaning up Smart Preview Orchestrator...'); // Unload all loaded modules await this._unloadAllModules(); // Cleanup shared services await this._cleanupSharedServices(); // Remove event listeners this._eventBus.off('drs:startSession', this._handleStartSession, this.name); this._eventBus.off('drs:switchModule', this._handleSwitchModule, this.name); this._eventBus.off('drs:updateProgress', this._handleUpdateProgress, this.name); this._setDestroyed(); console.log('โœ… Smart Preview Orchestrator destroyed successfully'); } catch (error) { console.error('โŒ SmartPreviewOrchestrator cleanup failed:', error); throw error; } } // Public API Methods /** * Start a new revision session for a chapter * @param {string} bookId - Book identifier * @param {string} chapterId - Chapter identifier * @returns {Promise} - Success status */ async startRevisionSession(bookId, chapterId) { this._validateInitialized(); try { console.log(`๐Ÿš€ Starting revision session: ${bookId} - ${chapterId}`); // Load chapter content const chapterContent = await this._contentLoader.loadContent(chapterId); const data = privateData.get(this); data.sessionState.currentChapter = { bookId, chapterId }; data.sessionState.chapterContent = chapterContent; // Load existing progress from files if (window.getChapterProgress) { try { const savedProgress = await window.getChapterProgress(bookId, chapterId); // Populate session state with saved progress (handle both old and new format) if (savedProgress.masteredVocabulary) { const vocabItems = savedProgress.masteredVocabulary.map(entry => { return typeof entry === 'string' ? entry : entry.item; }); data.sessionState.masteredVocabulary = new Set(vocabItems); } if (savedProgress.masteredPhrases) { const phraseItems = savedProgress.masteredPhrases.map(entry => { return typeof entry === 'string' ? entry : entry.item; }); data.sessionState.masteredPhrases = new Set(phraseItems); } if (savedProgress.masteredGrammar) { const grammarItems = savedProgress.masteredGrammar.map(entry => { return typeof entry === 'string' ? entry : entry.item; }); data.sessionState.masteredGrammar = new Set(grammarItems); } console.log(`๐Ÿ“ Loaded existing progress: ${savedProgress.masteredVocabulary.length} vocab, ${savedProgress.masteredPhrases.length} phrases, mastery count: ${savedProgress.masteryCount}`); } catch (error) { console.warn('Failed to load existing progress:', error); } } // Initialize prerequisites await this._analyzePrerequisites(chapterContent); // Generate exercise sequence await this._generateExerciseSequence(); // Start AI reporting session if (data.sharedServices.llmValidator && data.sharedServices.aiReportInterface) { const sessionId = data.sharedServices.llmValidator.startReportSession({ bookId, chapterId, difficulty: this._config.difficulty || 'medium', exerciseTypes: Array.from(data.availableModules.keys()), totalExercises: data.sessionState.exerciseSequence.length }); // Notify the report interface data.sharedServices.aiReportInterface.onSessionStart({ bookId, chapterId, sessionId }); console.log(`๐Ÿ“Š Started AI report session: ${sessionId}`); } // Start with first available exercise await this._startNextExercise(); // Emit session started event this._eventBus.emit('drs:sessionStarted', { bookId, chapterId, totalExercises: data.sessionState.exerciseSequence.length, availableModules: Array.from(data.availableModules.keys()) }, this.name); return true; } catch (error) { console.error('โŒ Failed to start revision session:', error); this._eventBus.emit('drs:sessionError', { error: error.message }, this.name); return false; } } /** * Get available exercise modules based on current prerequisites * @returns {Array} - Available module names */ getAvailableModules() { this._validateInitialized(); const data = privateData.get(this); return Array.from(data.availableModules.keys()); } /** * Get shared services for external access * @returns {Object} - Shared services */ getSharedServices() { this._validateInitialized(); const data = privateData.get(this); return data.sharedServices; } /** * Switch to a different exercise module * @param {string} moduleType - Type of module to switch to * @returns {Promise} - Success status */ async switchToModule(moduleType) { this._validateInitialized(); try { const data = privateData.get(this); if (!data.availableModules.has(moduleType)) { throw new Error(`Module type ${moduleType} is not available`); } // Unload current module if (data.currentModule) { await this._unloadModule(data.currentModule); } // Load new module const module = await this._loadModule(moduleType); data.currentModule = moduleType; // Present exercise const exerciseData = await this._getExerciseData(moduleType); const container = document.getElementById('drs-exercise-container'); await module.present(container, exerciseData); this._eventBus.emit('drs:moduleActivated', { moduleType, exerciseData }, this.name); return true; } catch (error) { console.error(`โŒ Failed to switch to module ${moduleType}:`, error); return false; } } /** * Get current session progress * @returns {Object} - Progress information */ getSessionProgress() { this._validateInitialized(); const data = privateData.get(this); const state = data.sessionState; return { currentChapter: state.currentChapter, masteredVocabulary: state.masteredVocabulary.size, masteredPhrases: state.masteredPhrases.size, masteredGrammar: state.masteredGrammar.size, completedExercises: state.sequenceIndex, totalExercises: state.exerciseSequence.length, progressPercentage: Math.round((state.sequenceIndex / state.exerciseSequence.length) * 100) }; } // Private Methods async _initializeSharedServices() { console.log('๐Ÿ”ง Initializing shared services...'); const data = privateData.get(this); try { // Initialize LLMValidator (mock for now) const { default: LLMValidator } = await import('./services/LLMValidator.js'); data.sharedServices.llmValidator = new LLMValidator(this._config.llm || {}); // Initialize AIReportInterface const { default: AIReportInterface } = await import('../components/AIReportInterface.js'); data.sharedServices.aiReportInterface = new AIReportInterface( data.sharedServices.llmValidator, this._config.aiReporting || {} ); // Initialize PrerequisiteEngine const { default: PrerequisiteEngine } = await import('./services/PrerequisiteEngine.js'); data.sharedServices.prerequisiteEngine = new PrerequisiteEngine(); // Initialize ContextMemory const { default: ContextMemory } = await import('./services/ContextMemory.js'); data.sharedServices.contextMemory = new ContextMemory(); console.log('โœ… Shared services initialized'); } catch (error) { console.error('โŒ Failed to initialize shared services:', error); throw error; } } async _cleanupSharedServices() { const data = privateData.get(this); // Cleanup services if they have cleanup methods Object.values(data.sharedServices).forEach(service => { if (service && typeof service.cleanup === 'function') { service.cleanup(); } }); } _setupEventListeners() { this._eventBus.on('drs:startSession', this._handleStartSession.bind(this), this.name); this._eventBus.on('drs:switchModule', this._handleSwitchModule.bind(this), this.name); this._eventBus.on('drs:updateProgress', this._handleUpdateProgress.bind(this), this.name); this._eventBus.on('drs:exerciseCompleted', this._handleExerciseCompleted.bind(this), this.name); } _registerModuleTypes() { const data = privateData.get(this); // Register all available module types Object.keys(data.moduleRegistry).forEach(moduleType => { data.availableModules.set(moduleType, { path: data.moduleRegistry[moduleType], loaded: false, instance: null }); }); } async _loadModule(moduleType) { const data = privateData.get(this); const moduleInfo = data.availableModules.get(moduleType); if (!moduleInfo) { throw new Error(`Unknown module type: ${moduleType}`); } if (data.loadedModules.has(moduleType)) { return data.loadedModules.get(moduleType); } try { console.log(`๐Ÿ“ฆ Loading module: ${moduleType}`); // Dynamic import of module const modulePath = moduleInfo.path.startsWith('./') ? moduleInfo.path : `./${moduleInfo.path}`; const { default: ModuleClass } = await import(modulePath); // Create instance with shared services const moduleInstance = new ModuleClass( this, // orchestrator reference data.sharedServices.llmValidator, data.sharedServices.prerequisiteEngine, data.sharedServices.contextMemory ); // Initialize module await moduleInstance.init(); data.loadedModules.set(moduleType, moduleInstance); moduleInfo.loaded = true; moduleInfo.instance = moduleInstance; console.log(`โœ… Module loaded: ${moduleType}`); return moduleInstance; } catch (error) { console.error(`โŒ Failed to load module ${moduleType}:`, error); throw error; } } async _unloadModule(moduleType) { const data = privateData.get(this); const module = data.loadedModules.get(moduleType); if (module) { try { await module.cleanup(); data.loadedModules.delete(moduleType); const moduleInfo = data.availableModules.get(moduleType); if (moduleInfo) { moduleInfo.loaded = false; moduleInfo.instance = null; } console.log(`๐Ÿ“ค Module unloaded: ${moduleType}`); } catch (error) { console.error(`โŒ Error unloading module ${moduleType}:`, error); } } } async _unloadAllModules() { const data = privateData.get(this); const moduleTypes = Array.from(data.loadedModules.keys()); for (const moduleType of moduleTypes) { await this._unloadModule(moduleType); } } async _analyzePrerequisites(chapterContent) { const data = privateData.get(this); // Use PrerequisiteEngine to analyze chapter content const prerequisites = data.sharedServices.prerequisiteEngine.analyzeChapter(chapterContent); console.log('๐Ÿ“Š Prerequisites analyzed:', prerequisites); } async _generateExerciseSequence() { const data = privateData.get(this); // Generate exercise sequence based on content and mastery const chapterContent = data.sessionState.chapterContent; const masteredVocab = data.sessionState.masteredVocabulary; const masteredPhrases = data.sessionState.masteredPhrases; // Filter content to focus on non-mastered items const allVocab = Object.keys(chapterContent.vocabulary || {}); const allPhrases = Object.keys(chapterContent.phrases || {}); const unmasteredVocab = allVocab.filter(word => !masteredVocab.has(word)); const unmasteredPhrases = allPhrases.filter(phrase => !masteredPhrases.has(phrase)); console.log(`๐Ÿ“Š Content analysis:`); console.log(` ๐Ÿ“š Vocabulary: ${unmasteredVocab.length}/${allVocab.length} unmastered`); console.log(` ๐Ÿ’ฌ Phrases: ${unmasteredPhrases.length}/${allPhrases.length} unmastered`); const sequence = []; // Create vocabulary groups (focus on unmastered, but include some mastered for review) const vocabGroupSize = 5; const vocabGroups = Math.ceil(unmasteredVocab.length / vocabGroupSize); for (let i = 0; i < vocabGroups; i++) { sequence.push({ type: 'vocabulary', subtype: 'group', groupSize: vocabGroupSize, groupIndex: i, adaptive: true // Mark as adaptive sequence }); } // Add unmastered phrases (prioritize new content) unmasteredPhrases.forEach((phrase, index) => { if (index < 10) { // Limit to 10 phrases per session sequence.push({ type: 'phrase', subtype: 'individual', index: allPhrases.indexOf(phrase), adaptive: true }); } }); // Add some review items if we have extra capacity if (sequence.length < 15) { const reviewVocab = [...masteredVocab].slice(0, 3); const reviewPhrases = [...masteredPhrases].slice(0, 2); reviewVocab.forEach(word => { sequence.push({ type: 'vocabulary', subtype: 'review', word: word, adaptive: true }); }); reviewPhrases.forEach(phrase => { sequence.push({ type: 'phrase', subtype: 'review', index: allPhrases.indexOf(phrase), adaptive: true }); }); } // Shuffle for variety for (let i = sequence.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [sequence[i], sequence[j]] = [sequence[j], sequence[i]]; } const adaptiveInfo = unmasteredVocab.length === 0 && unmasteredPhrases.length === 0 ? ' (Review mode - all content mastered!)' : ' (Adaptive - focusing on unmastered content)'; console.log(`๐Ÿง  Generated adaptive sequence: ${sequence.length} exercises${adaptiveInfo}`); data.sessionState.exerciseSequence = sequence; data.sessionState.sequenceIndex = 0; } async _startNextExercise() { const data = privateData.get(this); const sequence = data.sessionState.exerciseSequence; const currentIndex = data.sessionState.sequenceIndex; if (currentIndex >= sequence.length) { // End AI reporting session if (data.sharedServices.llmValidator && data.sharedServices.aiReportInterface) { const sessionStats = this.getSessionProgress(); // End the report session data.sharedServices.llmValidator.endReportSession(); // Notify the report interface data.sharedServices.aiReportInterface.onSessionEnd({ exerciseCount: sequence.length, averageScore: sessionStats.averageScore || 0, completedAt: new Date() }); console.log('๐Ÿ“Š Ended AI report session'); } // Session complete - mark as completed and save const currentChapter = data.sessionState.currentChapter; if (currentChapter && window.markChapterCompleted) { try { await window.markChapterCompleted(currentChapter.bookId, currentChapter.chapterId); console.log(`๐Ÿ† Chapter marked as completed: ${currentChapter.bookId}/${currentChapter.chapterId}`); } catch (error) { console.warn('Failed to mark chapter as completed:', error); } } this._eventBus.emit('drs:sessionComplete', this.getSessionProgress(), this.name); return; } const exercise = sequence[currentIndex]; await this.switchToModule(exercise.type); } async _getExerciseData(moduleType) { const data = privateData.get(this); const chapterContent = data.sessionState.chapterContent; const sequence = data.sessionState.exerciseSequence; const currentExercise = sequence[data.sessionState.sequenceIndex]; // Generate exercise data based on module type and current exercise parameters switch (moduleType) { case 'vocabulary': return this._generateVocabularyExerciseData(chapterContent, currentExercise); case 'phrase': return this._generatePhraseExerciseData(chapterContent, currentExercise); case 'text': return this._generateTextExerciseData(chapterContent, currentExercise); default: return { type: moduleType, content: chapterContent }; } } _generateVocabularyExerciseData(chapterContent, exercise) { const vocabulary = chapterContent.vocabulary || {}; const vocabArray = Object.entries(vocabulary); const startIndex = exercise.groupIndex * exercise.groupSize; const endIndex = Math.min(startIndex + exercise.groupSize, vocabArray.length); const vocabGroup = vocabArray.slice(startIndex, endIndex); return { type: 'vocabulary', subtype: exercise.subtype, groupIndex: exercise.groupIndex, vocabulary: vocabGroup.map(([word, data]) => ({ word, translation: data.user_language, pronunciation: data.pronunciation, type: data.type })) }; } _generatePhraseExerciseData(chapterContent, exercise) { const phrases = chapterContent.phrases || {}; const phraseEntries = Object.entries(phrases); const phraseIndex = exercise.index || 0; // Check if phrase exists at this index if (phraseIndex >= phraseEntries.length) { console.warn(`โš ๏ธ Phrase at index ${phraseIndex} not found (total: ${phraseEntries.length})`); return null; } const [phraseText, phraseData] = phraseEntries[phraseIndex]; // Create phrase object for compatibility const phrase = { id: `phrase_${phraseIndex}`, english: phraseText, text: phraseText, translation: phraseData.user_language, user_language: phraseData.user_language, pronunciation: phraseData.pronunciation, context: phraseData.context || 'general', ...phraseData }; // Verify prerequisites for this phrase const data = privateData.get(this); const unlockStatus = data.sharedServices.prerequisiteEngine.canUnlock('phrase', phrase); return { type: 'phrase', subtype: exercise.subtype, phrase: phrase, phraseIndex: phraseIndex, totalPhrases: phraseEntries.length, unlockStatus: unlockStatus, chapterContent: chapterContent, // For language detection metadata: { userLanguage: chapterContent.metadata?.userLanguage || 'English', targetLanguage: chapterContent.metadata?.targetLanguage || 'French' } }; } _generateTextExerciseData(chapterContent, exercise) { const texts = chapterContent.texts || []; const textIndex = exercise.textIndex || 0; const text = texts[textIndex]; return { type: 'text', subtype: exercise.subtype, text, sentenceIndex: exercise.sentenceIndex || 0 }; } // Event Handlers async _handleStartSession(event) { const { bookId, chapterId } = event.data; await this.startRevisionSession(bookId, chapterId); } async _handleSwitchModule(event) { const { moduleType } = event.data; await this.switchToModule(moduleType); } async _handleUpdateProgress(event) { const data = privateData.get(this); const { type, item, mastered } = event.data; const currentChapter = data.sessionState.currentChapter; if (type === 'vocabulary' && mastered) { data.sessionState.masteredVocabulary.add(item); // Save to persistent storage with metadata if (currentChapter && window.addMasteredItem) { try { const metadata = { exerciseType: 'vocabulary', sessionId: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, moduleType: 'VocabularyModule', sequenceIndex: data.sessionState.sequenceIndex }; await window.addMasteredItem(currentChapter.bookId, currentChapter.chapterId, 'vocabulary', item, metadata); } catch (error) { console.warn('Failed to save vocabulary progress:', error); } } } else if (type === 'phrase' && mastered) { data.sessionState.masteredPhrases.add(item); // Save to persistent storage with metadata if (currentChapter && window.addMasteredItem) { try { const metadata = { exerciseType: 'phrase', sessionId: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, moduleType: 'PhraseModule', sequenceIndex: data.sessionState.sequenceIndex, aiValidated: true }; await window.addMasteredItem(currentChapter.bookId, currentChapter.chapterId, 'phrases', item, metadata); } catch (error) { console.warn('Failed to save phrase progress:', error); } } } else if (type === 'grammar' && mastered) { data.sessionState.masteredGrammar.add(item); // Save to persistent storage with metadata if (currentChapter && window.addMasteredItem) { try { const metadata = { exerciseType: 'grammar', sessionId: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, moduleType: 'GrammarModule', sequenceIndex: data.sessionState.sequenceIndex }; await window.addMasteredItem(currentChapter.bookId, currentChapter.chapterId, 'grammar', item, metadata); } catch (error) { console.warn('Failed to save grammar progress:', error); } } } this._eventBus.emit('drs:progressUpdated', this.getSessionProgress(), this.name); } async _handleExerciseCompleted(event) { const data = privateData.get(this); data.sessionState.sequenceIndex++; // Move to next exercise await this._startNextExercise(); } /** * Get mastery progress from PrerequisiteEngine * @returns {Object} - Progress statistics */ getMasteryProgress() { this._validateInitialized(); const data = privateData.get(this); if (!data.sharedServices.prerequisiteEngine) { return { vocabulary: { mastered: 0, total: 0, percentage: 0 }, phrases: { mastered: 0, total: 0, percentage: 0 }, grammar: { mastered: 0, total: 0, percentage: 0 } }; } return data.sharedServices.prerequisiteEngine.getMasteryProgress(); } /** * Get list of mastered words from PrerequisiteEngine * @returns {Array} - Array of mastered words */ getMasteredWords() { this._validateInitialized(); const data = privateData.get(this); if (!data.sharedServices.prerequisiteEngine) { return []; } return Array.from(data.sharedServices.prerequisiteEngine.masteredWords); } } export default SmartPreviewOrchestrator;