Chat history: extract Teams timestamp, classify pre-join messages as history, no AI trigger
Made-with: Cursor
This commit is contained in:
parent
7e62c2fc65
commit
2e2fbfe8ed
3 changed files with 59 additions and 14 deletions
|
|
@ -6,6 +6,7 @@ export interface ChatMessageEntry {
|
||||||
text: string;
|
text: string;
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
source: 'chat';
|
source: 'chat';
|
||||||
|
isHistory: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -20,7 +21,7 @@ export class ChatProcedure {
|
||||||
private _onChatMessage: (entry: ChatMessageEntry) => void;
|
private _onChatMessage: (entry: ChatMessageEntry) => void;
|
||||||
private _isSubscribed: boolean = false;
|
private _isSubscribed: boolean = false;
|
||||||
private _lastMessageText: string = '';
|
private _lastMessageText: string = '';
|
||||||
private _chatReadyAtMs: number = 0;
|
private _botJoinedAtMs: number = 0;
|
||||||
private _recentMessageKeyTimestamps: Map<string, number> = new Map();
|
private _recentMessageKeyTimestamps: Map<string, number> = new Map();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -33,6 +34,14 @@ export class ChatProcedure {
|
||||||
this._onChatMessage = onChatMessage;
|
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.
|
* Open the chat panel and start monitoring messages.
|
||||||
*/
|
*/
|
||||||
|
|
@ -121,6 +130,7 @@ export class ChatProcedure {
|
||||||
speaker: string;
|
speaker: string;
|
||||||
text: string;
|
text: string;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
|
teamsTimestamp?: string;
|
||||||
messageKey?: string;
|
messageKey?: string;
|
||||||
}) => {
|
}) => {
|
||||||
this._handleChatMessage(msg);
|
this._handleChatMessage(msg);
|
||||||
|
|
@ -156,8 +166,26 @@ export class ChatProcedure {
|
||||||
return noisePatterns.some(p => lower.includes(p));
|
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 {
|
function _extractChatMessage(el: HTMLElement): boolean {
|
||||||
// Strategy 1: Standard selectors
|
|
||||||
const messageSelectors = [
|
const messageSelectors = [
|
||||||
'[data-tid="chat-message"]',
|
'[data-tid="chat-message"]',
|
||||||
'.fui-ChatMessage',
|
'.fui-ChatMessage',
|
||||||
|
|
@ -203,10 +231,12 @@ export class ChatProcedure {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (text && text.length > 0) {
|
if (text && text.length > 0) {
|
||||||
|
const teamsTs = _extractTeamsTimestamp(messageEl) || _extractTeamsTimestamp(el);
|
||||||
(window as any).__onChatMessageEvent({
|
(window as any).__onChatMessageEvent({
|
||||||
speaker: author,
|
speaker: author,
|
||||||
text,
|
text,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: teamsTs || new Date().toISOString(),
|
||||||
|
teamsTimestamp: teamsTs,
|
||||||
messageKey: `${author}::${text}`,
|
messageKey: `${author}::${text}`,
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -239,10 +269,12 @@ export class ChatProcedure {
|
||||||
// Check if this looks like a time-stamped message (not just any two children)
|
// Check if this looks like a time-stamped message (not just any two children)
|
||||||
const hasTid = children[i].getAttribute('data-tid') || '';
|
const hasTid = children[i].getAttribute('data-tid') || '';
|
||||||
if (hasTid.includes('author') || hasTid.includes('name') || hasTid.includes('sender')) {
|
if (hasTid.includes('author') || hasTid.includes('name') || hasTid.includes('sender')) {
|
||||||
|
const teamsTs = _extractTeamsTimestamp(el);
|
||||||
(window as any).__onChatMessageEvent({
|
(window as any).__onChatMessageEvent({
|
||||||
speaker: candidateName,
|
speaker: candidateName,
|
||||||
text: candidateBody,
|
text: candidateBody,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: teamsTs || new Date().toISOString(),
|
||||||
|
teamsTimestamp: teamsTs,
|
||||||
messageKey: `${candidateName}::${candidateBody}`,
|
messageKey: `${candidateName}::${candidateBody}`,
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -325,19 +357,21 @@ export class ChatProcedure {
|
||||||
});
|
});
|
||||||
|
|
||||||
this._logger.info(`Chat MutationObserver set up (target: ${chatObserverTarget})`);
|
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.
|
* 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;
|
if (!this._isSubscribed || !msg.text) return;
|
||||||
|
|
||||||
const nowMs = Date.now();
|
const nowMs = Date.now();
|
||||||
if (this._chatReadyAtMs > 0 && nowMs < this._chatReadyAtMs) return;
|
|
||||||
|
|
||||||
// Dedup
|
// Dedup
|
||||||
if (msg.text === this._lastMessageText) return;
|
if (msg.text === this._lastMessageText) return;
|
||||||
|
|
@ -353,13 +387,22 @@ export class ChatProcedure {
|
||||||
}
|
}
|
||||||
this._lastMessageText = msg.text;
|
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({
|
this._onChatMessage({
|
||||||
speaker: msg.speaker,
|
speaker: msg.speaker,
|
||||||
text: msg.text,
|
text: msg.text,
|
||||||
timestamp: new Date(msg.timestamp),
|
timestamp: messageTimestamp,
|
||||||
source: 'chat',
|
source: 'chat',
|
||||||
|
isHistory,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1025,8 +1025,7 @@ export class BotOrchestrator {
|
||||||
this._page,
|
this._page,
|
||||||
this._logger,
|
this._logger,
|
||||||
(entry: ChatMessageEntry) => {
|
(entry: ChatMessageEntry) => {
|
||||||
// Send chat message to Gateway as a special transcript
|
this._sendChatMessage(entry.speaker, entry.text, entry.isHistory);
|
||||||
this._sendChatMessage(entry.speaker, entry.text);
|
|
||||||
this._callbacks.onTranscript({
|
this._callbacks.onTranscript({
|
||||||
speaker: entry.speaker,
|
speaker: entry.speaker,
|
||||||
text: entry.text,
|
text: entry.text,
|
||||||
|
|
@ -1203,6 +1202,7 @@ export class BotOrchestrator {
|
||||||
*/
|
*/
|
||||||
private async _enableChat(): Promise<void> {
|
private async _enableChat(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
this._chatProcedure!.setBotJoinedAt(Date.now());
|
||||||
await this._chatProcedure!.enableChatMonitoring();
|
await this._chatProcedure!.enableChatMonitoring();
|
||||||
await this._chatProcedure!.subscribeToChatMessages();
|
await this._chatProcedure!.subscribeToChatMessages();
|
||||||
this._logger.info('Chat monitoring enabled and subscribed');
|
this._logger.info('Chat monitoring enabled and subscribed');
|
||||||
|
|
@ -1252,7 +1252,7 @@ export class BotOrchestrator {
|
||||||
/**
|
/**
|
||||||
* Send a chat message event to the Gateway.
|
* 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 = {
|
const message: ChatMessage = {
|
||||||
type: 'chatMessage',
|
type: 'chatMessage',
|
||||||
sessionId: this._sessionId,
|
sessionId: this._sessionId,
|
||||||
|
|
@ -1260,6 +1260,7 @@ export class BotOrchestrator {
|
||||||
speaker,
|
speaker,
|
||||||
text,
|
text,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
isHistory,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
this._sendToGateway(message);
|
this._sendToGateway(message);
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ export interface ChatMessage {
|
||||||
speaker: string;
|
speaker: string;
|
||||||
text: string;
|
text: string;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
|
isHistory?: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue