Chat history: extract Teams timestamp, classify pre-join messages as history, no AI trigger

Made-with: Cursor
This commit is contained in:
ValueOn AG 2026-02-27 13:56:20 +01:00
parent 7e62c2fc65
commit 2e2fbfe8ed
3 changed files with 59 additions and 14 deletions

View file

@ -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,
});
}

View file

@ -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);

View file

@ -47,6 +47,7 @@ export interface ChatMessage {
speaker: string;
text: string;
timestamp: string;
isHistory?: boolean;
};
}