Files

318 lines
10 KiB
JavaScript

/**
* TextBuffer Module
* Manages text processing and sentence detection for the UI
*/
import { BaseModule } from './base-module.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;
this.paragraphCounter = 0;
this.currentTextBlockId = 0;
this.markupParser = null;
this.dependencies = ['markup-parser'];
// Bind methods using parent's bindMethods utility
this.bindMethods([
'addBlock',
'addBlocks',
'processNextFromQueue',
'processSentences',
'processNextSentence',
'clear',
'getBuffer',
'getStatus',
'setOnSentenceReady'
]);
}
/**
* Initialize the module
* @returns {Promise<boolean>} - Resolves with success status
*/
async initialize() {
try {
this.markupParser = this.getModule('markup-parser');
if (!this.markupParser) {
console.warn("TextBuffer: Markup parser not found, using plain paragraph splitting");
}
// 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 an already parsed render block to the processing queue.
* Engine protocols should prefer this over re-serializing tags into text markup.
* @param {Object} block - Parsed paragraph/media/heading block
*/
addBlock(block) {
if (!block || !block.type) return;
if (block.type === 'paragraph') {
const paragraphId = block.id || `paragraph-${this.paragraphCounter + 1}`;
this.processingQueue.push({
...block,
id: paragraphId,
paragraphIndex: this.paragraphCounter,
textBlockId: this.currentTextBlockId,
text: String(block.text || '').trim(),
layoutText: block.layoutText || block.text || ''
});
this.paragraphCounter += 1;
} else {
this.processingQueue.push({
...block,
id: block.id || `${block.type}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
});
if (block.type === 'image') {
this.currentTextBlockId += 1;
}
}
if (!this.isProcessingActive && this.onSentenceReadyCallback) {
this.processNextFromQueue();
} else {
console.log(`TextBuffer: ${block.type} block queued for processing`);
}
}
addBlocks(blocks = []) {
if (!Array.isArray(blocks)) return;
blocks.forEach(block => this.addBlock(block));
}
/**
* Process the next text fragment from the queue
*/
processNextFromQueue() {
if (this.processingQueue.length === 0 || this.isProcessingActive) {
return;
}
this.isProcessingActive = true;
const paragraph = this.processingQueue.shift();
console.log(`TextBuffer: Processing next fragment from queue, remaining: ${this.processingQueue.length}`);
this.buffer = paragraph.text;
super.dispatchEvent('buffer:sentence', {
sentence: paragraph.text,
paragraph,
remaining: this.processingQueue.length
});
if (this.onSentenceReadyCallback) {
this.onSentenceReadyCallback(paragraph, () => {});
this.buffer = '';
this.isProcessingActive = false;
this.processingLock = false;
if (this.processingQueue.length > 0) {
queueMicrotask(() => this.processNextFromQueue());
} else {
super.dispatchEvent('buffer:empty', {});
}
} else {
this.isProcessingActive = false;
}
}
/**
* Process complete sentences in the buffer
*/
processSentences() {
// If already processing, don't start another processing cycle
if (this.processingLock) {
return;
}
this.processingLock = true;
// If the buffer is empty, release the lock and check queue
if (this.buffer.length === 0) {
this.processingLock = false;
// If no more text to process, end processing
if (this.processingQueue.length === 0) {
this.isProcessingActive = false;
return;
}
// Process the next text fragment
this.processNextFromQueue();
return;
}
// Process the next sentence
this.processNextSentence();
}
/**
* 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;
this.paragraphCounter = 0;
this.currentTextBlockId = 0;
}
/**
* 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();
// Export the module
export { TextBuffer };