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;
|
||||
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<string, number> = 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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ export interface ChatMessage {
|
|||
speaker: string;
|
||||
text: string;
|
||||
timestamp: string;
|
||||
isHistory?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue