diff --git a/src/bot/chatProcedure.ts b/src/bot/chatProcedure.ts index 20ff466..580a870 100644 --- a/src/bot/chatProcedure.ts +++ b/src/bot/chatProcedure.ts @@ -6,6 +6,7 @@ export interface ChatMessageEntry { text: string; timestamp: Date; source: 'chat'; + isHistory: boolean; } /** @@ -20,7 +21,7 @@ export class ChatProcedure { private _onChatMessage: (entry: ChatMessageEntry) => void; private _isSubscribed: boolean = false; private _lastMessageText: string = ''; - private _chatReadyAtMs: number = 0; + private _botJoinedAtMs: number = 0; private _recentMessageKeyTimestamps: Map = new Map(); constructor( @@ -33,6 +34,14 @@ export class ChatProcedure { this._onChatMessage = onChatMessage; } + /** + * Set the bot's join timestamp so we can classify messages as history vs live. + */ + setBotJoinedAt(ms: number): void { + this._botJoinedAtMs = ms; + this._logger.info(`Chat history cutoff set: messages before ${new Date(ms).toISOString()} are history`); + } + /** * Open the chat panel and start monitoring messages. */ @@ -121,6 +130,7 @@ export class ChatProcedure { speaker: string; text: string; timestamp: string; + teamsTimestamp?: string; messageKey?: string; }) => { this._handleChatMessage(msg); @@ -156,8 +166,26 @@ export class ChatProcedure { return noisePatterns.some(p => lower.includes(p)); } + function _extractTeamsTimestamp(el: HTMLElement): string | undefined { + const timeSelectors = [ + 'time[datetime]', + '[data-tid="message-timestamp"] time', + '[data-tid*="timestamp"] time', + '[data-tid="message-timestamp"]', + ]; + for (const sel of timeSelectors) { + const timeEl = el.querySelector(sel) || el.closest?.('[data-tid*="message"]')?.querySelector(sel); + if (timeEl) { + const dt = timeEl.getAttribute('datetime'); + if (dt) return dt; + const title = timeEl.getAttribute('title'); + if (title) return title; + } + } + return undefined; + } + function _extractChatMessage(el: HTMLElement): boolean { - // Strategy 1: Standard selectors const messageSelectors = [ '[data-tid="chat-message"]', '.fui-ChatMessage', @@ -203,10 +231,12 @@ export class ChatProcedure { } if (text && text.length > 0) { + const teamsTs = _extractTeamsTimestamp(messageEl) || _extractTeamsTimestamp(el); (window as any).__onChatMessageEvent({ speaker: author, text, - timestamp: new Date().toISOString(), + timestamp: teamsTs || new Date().toISOString(), + teamsTimestamp: teamsTs, messageKey: `${author}::${text}`, }); return true; @@ -239,10 +269,12 @@ export class ChatProcedure { // Check if this looks like a time-stamped message (not just any two children) const hasTid = children[i].getAttribute('data-tid') || ''; if (hasTid.includes('author') || hasTid.includes('name') || hasTid.includes('sender')) { + const teamsTs = _extractTeamsTimestamp(el); (window as any).__onChatMessageEvent({ speaker: candidateName, text: candidateBody, - timestamp: new Date().toISOString(), + timestamp: teamsTs || new Date().toISOString(), + teamsTimestamp: teamsTs, messageKey: `${candidateName}::${candidateBody}`, }); return true; @@ -325,19 +357,21 @@ export class ChatProcedure { }); this._logger.info(`Chat MutationObserver set up (target: ${chatObserverTarget})`); - // Guard against Teams replaying historical thread entries as fresh mutations. - this._chatReadyAtMs = Date.now() + 12000; - this._logger.info('Chat monitor warmup active for 12s'); } /** * Handle a chat message event from the browser. */ - private _handleChatMessage(msg: { speaker: string; text: string; timestamp: string; messageKey?: string }): void { + private _handleChatMessage(msg: { + speaker: string; + text: string; + timestamp: string; + teamsTimestamp?: string; + messageKey?: string; + }): void { if (!this._isSubscribed || !msg.text) return; const nowMs = Date.now(); - if (this._chatReadyAtMs > 0 && nowMs < this._chatReadyAtMs) return; // Dedup if (msg.text === this._lastMessageText) return; @@ -353,13 +387,22 @@ export class ChatProcedure { } this._lastMessageText = msg.text; - this._logger.info(`Chat: [${msg.speaker}] ${msg.text}`); + // Classify as history (sent before bot joined) vs live + const messageTimestamp = new Date(msg.teamsTimestamp || msg.timestamp); + const messageMs = messageTimestamp.getTime(); + const isHistory = this._botJoinedAtMs > 0 && messageMs < this._botJoinedAtMs; + + this._logger.info( + `Chat${isHistory ? ' [HISTORY]' : ''}: [${msg.speaker}] ${msg.text}` + + (msg.teamsTimestamp ? ` (teams_ts=${msg.teamsTimestamp})` : ''), + ); this._onChatMessage({ speaker: msg.speaker, text: msg.text, - timestamp: new Date(msg.timestamp), + timestamp: messageTimestamp, source: 'chat', + isHistory, }); } diff --git a/src/bot/orchestrator.ts b/src/bot/orchestrator.ts index 28dc1ae..ee1cee2 100644 --- a/src/bot/orchestrator.ts +++ b/src/bot/orchestrator.ts @@ -1025,8 +1025,7 @@ export class BotOrchestrator { this._page, this._logger, (entry: ChatMessageEntry) => { - // Send chat message to Gateway as a special transcript - this._sendChatMessage(entry.speaker, entry.text); + this._sendChatMessage(entry.speaker, entry.text, entry.isHistory); this._callbacks.onTranscript({ speaker: entry.speaker, text: entry.text, @@ -1203,6 +1202,7 @@ export class BotOrchestrator { */ private async _enableChat(): Promise { try { + this._chatProcedure!.setBotJoinedAt(Date.now()); await this._chatProcedure!.enableChatMonitoring(); await this._chatProcedure!.subscribeToChatMessages(); this._logger.info('Chat monitoring enabled and subscribed'); @@ -1252,7 +1252,7 @@ export class BotOrchestrator { /** * Send a chat message event to the Gateway. */ - private _sendChatMessage(speaker: string, text: string): void { + private _sendChatMessage(speaker: string, text: string, isHistory: boolean = false): void { const message: ChatMessage = { type: 'chatMessage', sessionId: this._sessionId, @@ -1260,6 +1260,7 @@ export class BotOrchestrator { speaker, text, timestamp: new Date().toISOString(), + isHistory, }, }; this._sendToGateway(message); diff --git a/src/types/index.ts b/src/types/index.ts index 3d71dfb..c97e461 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -47,6 +47,7 @@ export interface ChatMessage { speaker: string; text: string; timestamp: string; + isHistory?: boolean; }; }