/** * TextBuffer Module * Manages text processing and sentence detection for the UI */ import { BaseModule } from './base-module.js'; import { moduleRegistry } from './module-registry.js'; class TextBufferModule extends BaseModule { constructor() { super('text-buffer', 'Text Buffer'); this.buffer = ''; this.sentenceEndRegex = /[.!?]\s+/g; this.onSentenceReadyCallback = null; this.processingLock = false; this.processingQueue = []; this.isProcessingActive = false; // Bind methods using parent's bindMethods utility this.bindMethods([ 'addText', 'processNextFromQueue', 'processSentences', 'processNextSentence', 'clear', 'getBuffer', 'getStatus', 'setOnSentenceReady' ]); } /** * Initialize the module * @returns {Promise} - Resolves with success status */ async initialize() { try { // Use parent's addEventListener for automatic cleanup this.addEventListener(document, 'ui:paragraph:complete', (event) => { if (this.processingQueue.length > 0 && !this.isProcessingActive) { console.log('TextBuffer: Previous paragraph complete, processing next from queue'); this.processNextFromQueue(); } }); // Use parent's addEventListener for automatic cleanup this.addEventListener(document, 'animation:complete', () => { if (this.processingQueue.length > 0 && !this.isProcessingActive) { console.log('TextBuffer: Animations complete, processing next from queue'); this.processNextFromQueue(); } }); this.reportProgress(100, "Text buffer ready"); return true; } catch (error) { console.error("Error initializing Text Buffer:", error); return false; } } /** * Set callback function for when a sentence is ready * @param {Function} callback - Function to call with the sentence and completion callback */ setOnSentenceReady(callback) { if (typeof callback === 'function') { this.onSentenceReadyCallback = callback; console.log("Text Buffer: Sentence ready callback set"); // Process any queued text immediately if (this.processingQueue.length > 0 && !this.isProcessingActive) { this.processNextFromQueue(); } } else { console.warn("Text Buffer: Invalid sentence ready callback provided"); } } /** * Add text to the buffer and process sentences * @param {string} text - Text to add to the buffer */ addText(text) { if (!text) return; console.log(`TextBuffer: Adding text: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}"`); // Add to processing queue instead of directly to buffer this.processingQueue.push(text); // Process the queue if not already processing if (!this.isProcessingActive && this.onSentenceReadyCallback) { this.processNextFromQueue(); } else { console.log('TextBuffer: Text queued for processing'); } } /** * Process the next text fragment from the queue */ processNextFromQueue() { if (this.processingQueue.length === 0 || this.isProcessingActive) { return; } this.isProcessingActive = true; const text = this.processingQueue.shift(); console.log(`TextBuffer: Processing next fragment from queue, remaining: ${this.processingQueue.length}`); // Add text to buffer this.buffer += text; // If we have a trailing newline as a complete sentence, add a period if (this.buffer.endsWith('\n') && !this.buffer.endsWith('.\n')) { const lastChar = this.buffer.charAt(this.buffer.length - 2); if (lastChar !== '.' && lastChar !== '!' && lastChar !== '?') { this.buffer = this.buffer.slice(0, -1) + '.\n'; } } // Process any complete sentences this.processSentences(); } /** * Process complete sentences in the buffer */ processSentences() { // Prevent concurrent processing if (this.processingLock) return; this.processingLock = true; try { // Check for sentence endings (including newlines as sentence endings) const sentenceEndings = [/[.!?]\s+/g, /[.!?]$/m, /\n/g]; let foundSentence = false; for (const pattern of sentenceEndings) { if (this.buffer.match(pattern)) { foundSentence = true; break; } } if (!foundSentence) { // No complete sentences yet this.processingLock = false; this.isProcessingActive = false; // Use parent's dispatchEvent method super.dispatchEvent('buffer:waiting', { remainingText: this.buffer, queueLength: this.processingQueue.length }); return; } // Process the next complete sentence this.processNextSentence(); } catch (error) { console.error("Error processing sentences:", error); this.processingLock = false; this.isProcessingActive = false; } } /** * Process the next sentence in the buffer */ processNextSentence() { // Check for different sentence endings const patterns = [/[.!?]\s+/, /[.!?]$/, /\n/]; let match = null; let endIndex = -1; // Try to find the first sentence ending for (const pattern of patterns) { match = this.buffer.match(pattern); if (match) { endIndex = match.index + match[0].length; break; } } if (endIndex === -1) { // No complete sentence found this.processingLock = false; this.isProcessingActive = false; return; } const sentence = this.buffer.substring(0, endIndex); // Remove the processed sentence from buffer this.buffer = this.buffer.substring(endIndex); console.log(`TextBuffer: Processing sentence: "${sentence.trim()}"`); // Use parent's dispatchEvent method super.dispatchEvent('buffer:sentence', { sentence: sentence, remaining: this.buffer.length }); // Call the callback if set if (this.onSentenceReadyCallback) { this.onSentenceReadyCallback(sentence, () => { // After processing is complete, check for more sentences this.processingLock = false; // Release lock immediately to allow processing of next sentence // Check if there are more sentences to process if (this.buffer.length > 0) { // Use requestAnimationFrame to prevent stack overflow and ensure UI update between sentences requestAnimationFrame(() => { this.processSentences(); }); } else if (this.processingQueue.length > 0) { // No more sentences in buffer but we have more text in the queue requestAnimationFrame(() => { this.isProcessingActive = false; this.processNextFromQueue(); }); } else { // All processed this.isProcessingActive = false; super.dispatchEvent('buffer:empty', {}); } }); } else { // No callback set, just release lock and continue processing this.processingLock = false; if (this.buffer.length > 0) { // Use requestAnimationFrame instead of setTimeout for better performance requestAnimationFrame(() => { this.processSentences(); }); } else if (this.processingQueue.length > 0) { requestAnimationFrame(() => { this.isProcessingActive = false; this.processNextFromQueue(); }); } else { this.isProcessingActive = false; } } } /** * Clear the text buffer */ clear() { this.buffer = ''; this.processingQueue = []; this.isProcessingActive = false; this.processingLock = false; } /** * Get the current buffer content * @returns {string} - Current buffer content */ getBuffer() { return this.buffer; } /** * Get the current processing status * @returns {Object} - Processing status object */ getStatus() { return { bufferLength: this.buffer.length, queueLength: this.processingQueue.length, isProcessing: this.isProcessingActive, isLocked: this.processingLock }; } } // Create the singleton instance const TextBuffer = new TextBufferModule(); // Register with the module registry moduleRegistry.register(TextBuffer); // Export the module export { TextBuffer }; // Keep a reference in window for loader system window.TextBuffer = TextBuffer;