diff --git a/src/bot/chatProcedure.ts b/src/bot/chatProcedure.ts index 580a870..4302eba 100644 --- a/src/bot/chatProcedure.ts +++ b/src/bot/chatProcedure.ts @@ -408,50 +408,67 @@ export class ChatProcedure { /** * Send a chat message in the meeting. - * Finds the chat input, types the message, and sends it. + * Finds the chat input (with retry), types the message, and sends it. */ async sendChatMessage(text: string): Promise { this._logger.info(`Sending chat message: ${text.substring(0, 60)}...`); - try { - // Find chat input - const inputSelectors = [ - '[data-tid="ckeditor-replyConversation"]', - 'div[role="textbox"][data-tid*="chat"]', - 'div[role="textbox"][aria-label*="message" i]', - 'div[role="textbox"][aria-label*="Nachricht" i]', - 'div[contenteditable="true"][data-tid*="message"]', - 'div[contenteditable="true"][aria-label*="message" i]', - ]; + const inputSelectors = [ + '[data-tid="ckeditor-replyConversation"]', + 'div[role="textbox"][data-tid*="chat"]', + 'div[role="textbox"][data-tid*="message"]', + 'div[role="textbox"][aria-label*="message" i]', + 'div[role="textbox"][aria-label*="Nachricht" i]', + 'div[contenteditable="true"][data-tid*="message"]', + 'div[contenteditable="true"][data-tid*="chat"]', + 'div[contenteditable="true"][aria-label*="message" i]', + '[aria-label*="Type a new message" i]', + '[aria-label*="Neue Nachricht eingeben" i]', + ]; - let input: any = null; - for (const selector of inputSelectors) { - input = await this._page.$(selector); - if (input) break; + const maxAttempts = 5; + const retryDelayMs = 600; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + let input: any = null; + for (const selector of inputSelectors) { + input = await this._page.$(selector); + if (input) { + const isVisible = await input.isVisible().catch(() => false); + if (isVisible) break; + await input.dispose(); + input = null; + } + } + + if (input) { + await input.click(); + await this._page.waitForTimeout(200); + + await this._page.keyboard.type(text, { delay: 10 }); + await this._page.waitForTimeout(200); + await this._page.keyboard.press('Enter'); + this._logger.info('Chat message sent'); + return true; + } + + if (attempt < maxAttempts) { + this._logger.info(`Chat input not found, retry ${attempt}/${maxAttempts} in ${retryDelayMs}ms`); + await this._page.waitForTimeout(retryDelayMs); + } + } catch (error) { + this._logger.error(`Error sending chat message (attempt ${attempt}):`, error); + if (attempt < maxAttempts) { + await this._page.waitForTimeout(retryDelayMs); + } else { + return false; + } } - - if (!input) { - this._logger.warn('Could not find chat input field'); - return false; - } - - // Click to focus - await input.click(); - await this._page.waitForTimeout(200); - - // Type the message - await this._page.keyboard.type(text, { delay: 10 }); - await this._page.waitForTimeout(200); - - // Press Enter to send - await this._page.keyboard.press('Enter'); - this._logger.info('Chat message sent'); - return true; - - } catch (error) { - this._logger.error('Error sending chat message:', error); - return false; } + + this._logger.warn('Could not find chat input field'); + return false; } /** diff --git a/src/bot/orchestrator.ts b/src/bot/orchestrator.ts index ee1cee2..0c37e50 100644 --- a/src/bot/orchestrator.ts +++ b/src/bot/orchestrator.ts @@ -72,6 +72,8 @@ export class BotOrchestrator { private _isShuttingDown: boolean = false; private _isDebugMode: boolean = false; private _keepAliveInterval: NodeJS.Timeout | null = null; + private _chatMessageQueue: string[] = []; + private _chatQueueProcessing: boolean = false; constructor( sessionId: string, @@ -699,10 +701,8 @@ export class BotOrchestrator { case 'sendChatMessage': const chatMsg = message as SendChatMessage; this._logger.info(`Gateway sendChatMessage received: ${chatMsg.text?.substring(0, 60)}...`); - try { - await this.sendChatMessageToMeeting(chatMsg.text); - } catch (chatErr) { - this._logger.error(`Failed to send chat message to meeting: ${chatErr}`); + if (chatMsg.text) { + this._enqueueChatMessage(chatMsg.text); } break; @@ -1234,7 +1234,10 @@ export class BotOrchestrator { this._logger.info(`Sending join greeting (chat + voice): ${greeting}`); - // Chat greeting + // Brief delay so chat input is ready after panel open (Teams DOM can lag) + await new Promise((r) => setTimeout(r, 800)); + + // Chat greeting (queued; retries if input not found) await this.sendChatMessageToMeeting(greeting); // Voice greeting — ask Gateway to generate TTS and send back playAudio @@ -1294,14 +1297,40 @@ export class BotOrchestrator { } /** - * Send a text message to the meeting chat. + * Enqueue a chat message for serialized sending (prevents interleaved text when + * multiple messages arrive in quick succession). + */ + private _enqueueChatMessage(text: string): void { + this._chatMessageQueue.push(text); + this._processChatQueue(); + } + + /** + * Process chat queue one message at a time to avoid interleaved typing. + */ + private async _processChatQueue(): Promise { + if (this._chatQueueProcessing || this._chatMessageQueue.length === 0) return; + this._chatQueueProcessing = true; + while (this._chatMessageQueue.length > 0) { + const text = this._chatMessageQueue.shift()!; + if (this._isShuttingDown || this._state !== 'in_meeting' || !this._chatProcedure) { + this._logger.warn('Cannot send chat message - not in meeting, skipping queued message'); + continue; + } + try { + await this._chatProcedure.sendChatMessage(text); + } catch (chatErr) { + this._logger.error(`Failed to send chat message to meeting: ${chatErr}`); + } + } + this._chatQueueProcessing = false; + } + + /** + * Send a text message to the meeting chat (enqueued for serialized delivery). */ async sendChatMessageToMeeting(text: string): Promise { - if (this._isShuttingDown || this._state !== 'in_meeting' || !this._chatProcedure) { - this._logger.warn('Cannot send chat message - not in meeting'); - return; - } - await this._chatProcedure.sendChatMessage(text); + this._enqueueChatMessage(text); } /**