@xapp/chat-widget 1.88.0 → 1.91.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.css +1 -1
- package/dist/index.es.js +316 -30
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +316 -30
- package/dist/index.js.map +1 -1
- package/dist/store/ChatAction.d.ts +0 -0
- package/dist/xapp/ChatServer.d.ts +0 -0
- package/dist/xapp/MCPChat.d.ts +0 -0
- package/dist/xapp-chat-widget.js +4 -4
- package/dist/xapp-chat-widget.js.map +1 -1
- package/package.json +6 -6
package/dist/index.es.js
CHANGED
|
@@ -1899,7 +1899,15 @@ function sendGreeting(PRECHAT_FORM_ENABLED) {
|
|
|
1899
1899
|
}
|
|
1900
1900
|
|
|
1901
1901
|
function sendMessage(msg) {
|
|
1902
|
-
return function (chatServer) { return function () {
|
|
1902
|
+
return function (chatServer) { return function (dispatch, getState) {
|
|
1903
|
+
var state = getState();
|
|
1904
|
+
// Don't allow visitor to send message if offline
|
|
1905
|
+
if (state.accountStatus === "offline" && !state.isChatting)
|
|
1906
|
+
return;
|
|
1907
|
+
// Write user message to Redux before server send so it appears above streaming response
|
|
1908
|
+
if (msg.msg) {
|
|
1909
|
+
dispatch(writeMessage(msg.msg, state.visitor, +new Date()));
|
|
1910
|
+
}
|
|
1903
1911
|
chatServer.sendChatMsgRequest(msg, function () { });
|
|
1904
1912
|
}; };
|
|
1905
1913
|
}
|
|
@@ -2984,7 +2992,9 @@ function setSessionId(sessionId) {
|
|
|
2984
2992
|
|
|
2985
2993
|
// Endpoint path constants
|
|
2986
2994
|
var HOMEOWNER_ENDPOINT_PATH = "/chat/stream";
|
|
2995
|
+
var HOMEOWNER_CLOSE_ENDPOINT_PATH = "/chat/close";
|
|
2987
2996
|
var BUSINESS_OWNER_ENDPOINT_PATH = "/api/chat/stream";
|
|
2997
|
+
var BUSINESS_OWNER_CLOSE_ENDPOINT_PATH = "/api/chat/close";
|
|
2988
2998
|
// Default retry configuration for MCP connections
|
|
2989
2999
|
var DEFAULT_MCP_RETRY_CONFIG = [
|
|
2990
3000
|
{ retry: 0, delay: 0, text: "" }, // Initial attempt (silent)
|
|
@@ -3138,6 +3148,22 @@ function clearStoredLastSeq(sessionId) {
|
|
|
3138
3148
|
// sessionStorage may be unavailable
|
|
3139
3149
|
}
|
|
3140
3150
|
}
|
|
3151
|
+
/**
|
|
3152
|
+
* Returns the current local time as an ISO 8601 string with the local timezone offset.
|
|
3153
|
+
* Unlike toISOString(), this preserves the user's local time zone (e.g. "2024-01-15T14:30:00-04:00")
|
|
3154
|
+
* so the agent server can correctly determine time-of-day context (morning greetings, etc.).
|
|
3155
|
+
*/
|
|
3156
|
+
function getLocalISOString(date) {
|
|
3157
|
+
if (date === void 0) { date = new Date(); }
|
|
3158
|
+
var offsetMinutes = -date.getTimezoneOffset();
|
|
3159
|
+
var sign = offsetMinutes >= 0 ? "+" : "-";
|
|
3160
|
+
var pad = function (n) { return String(Math.abs(n)).padStart(2, "0"); };
|
|
3161
|
+
var offsetHours = Math.floor(Math.abs(offsetMinutes) / 60);
|
|
3162
|
+
var offsetMins = Math.abs(offsetMinutes) % 60;
|
|
3163
|
+
return ("".concat(date.getFullYear(), "-").concat(pad(date.getMonth() + 1), "-").concat(pad(date.getDate())) +
|
|
3164
|
+
"T".concat(pad(date.getHours()), ":").concat(pad(date.getMinutes()), ":").concat(pad(date.getSeconds())) +
|
|
3165
|
+
"".concat(sign).concat(pad(offsetHours), ":").concat(pad(offsetMins)));
|
|
3166
|
+
}
|
|
3141
3167
|
/**
|
|
3142
3168
|
* Gets the current page context from browser globals.
|
|
3143
3169
|
* If an override is provided, uses that instead (useful for testing).
|
|
@@ -3146,7 +3172,7 @@ function clearStoredLastSeq(sessionId) {
|
|
|
3146
3172
|
* may contain sensitive data (tokens, PII). Only origin + pathname are sent.
|
|
3147
3173
|
*/
|
|
3148
3174
|
function getPageContext(override) {
|
|
3149
|
-
var localTime =
|
|
3175
|
+
var localTime = getLocalISOString();
|
|
3150
3176
|
// If override provided, merge with defaults (override wins)
|
|
3151
3177
|
// Overrides are typically for testing and may include full URLs
|
|
3152
3178
|
if (override && Object.keys(override).length > 0) {
|
|
@@ -3198,12 +3224,15 @@ var MCPChat = /** @class */ (function () {
|
|
|
3198
3224
|
this.isValidatingSession = false;
|
|
3199
3225
|
// Tool call tracking for debug mode
|
|
3200
3226
|
this.currentToolCalls = [];
|
|
3201
|
-
// TODO: isReplaying and lastSeq are tracked for future replay support.
|
|
3202
|
-
// Wire up replay requests in wakeup() once the server endpoint supports it.
|
|
3203
3227
|
this.isReplaying = false;
|
|
3228
|
+
// Sleep/wakeup state for tab backgrounding recovery
|
|
3229
|
+
this.isSleeping = false;
|
|
3230
|
+
this.isRequestingReplay = false;
|
|
3231
|
+
this.needsReplayOnWakeup = false;
|
|
3204
3232
|
// True when userId was assigned by the server (via connected SSE event),
|
|
3205
3233
|
// meaning it should not be overwritten by the default client fingerprint.
|
|
3206
3234
|
this.userIdIsServerAssigned = false;
|
|
3235
|
+
this.sentMessageCount = 0;
|
|
3207
3236
|
this.config = config;
|
|
3208
3237
|
this.options = options;
|
|
3209
3238
|
this.configurableMessages = options === null || options === void 0 ? void 0 : options.configurableMessages;
|
|
@@ -3239,6 +3268,7 @@ var MCPChat = /** @class */ (function () {
|
|
|
3239
3268
|
}
|
|
3240
3269
|
MCPChat.prototype.init = function (dispatch) {
|
|
3241
3270
|
this.dispatch = dispatch;
|
|
3271
|
+
this.sessionStartTime = Date.now();
|
|
3242
3272
|
var mode = this.config.mode || "homeowner";
|
|
3243
3273
|
log("MCPChat initialized with mode: ".concat(mode));
|
|
3244
3274
|
// Warn if not using HTTPS with authentication
|
|
@@ -3319,7 +3349,9 @@ var MCPChat = /** @class */ (function () {
|
|
|
3319
3349
|
// Page context
|
|
3320
3350
|
pageContext: getPageContext(this.config.pageContextOverride),
|
|
3321
3351
|
// Client timestamp for latency measurement and clock drift detection
|
|
3322
|
-
clientTime: new Date().toISOString()
|
|
3352
|
+
clientTime: new Date().toISOString(),
|
|
3353
|
+
// Last sequence number for replay on reconnect
|
|
3354
|
+
lastSeq: this.lastSeq }, additionalData);
|
|
3323
3355
|
};
|
|
3324
3356
|
/**
|
|
3325
3357
|
* Initializes a new session by calling the endpoint with { newSession: true }.
|
|
@@ -3392,6 +3424,14 @@ var MCPChat = /** @class */ (function () {
|
|
|
3392
3424
|
// homeowner mode - serverUrl should already include appId
|
|
3393
3425
|
return "".concat(serverUrl).concat(HOMEOWNER_ENDPOINT_PATH);
|
|
3394
3426
|
};
|
|
3427
|
+
MCPChat.prototype.getCloseEndpoint = function () {
|
|
3428
|
+
var serverUrl = this.config.serverUrl.replace(/\/+$/, "");
|
|
3429
|
+
var mode = this.config.mode || "homeowner";
|
|
3430
|
+
if (mode === "business_owner") {
|
|
3431
|
+
return "".concat(serverUrl).concat(BUSINESS_OWNER_CLOSE_ENDPOINT_PATH);
|
|
3432
|
+
}
|
|
3433
|
+
return "".concat(serverUrl).concat(HOMEOWNER_CLOSE_ENDPOINT_PATH);
|
|
3434
|
+
};
|
|
3395
3435
|
MCPChat.prototype.sendNewMessage = function (msg, user) {
|
|
3396
3436
|
log("SERVER: new message: ".concat(JSON.stringify(msg)));
|
|
3397
3437
|
this.dispatch({
|
|
@@ -3542,6 +3582,30 @@ var MCPChat = /** @class */ (function () {
|
|
|
3542
3582
|
},
|
|
3543
3583
|
});
|
|
3544
3584
|
};
|
|
3585
|
+
MCPChat.prototype.dispatchStreamAbort = function (messageId) {
|
|
3586
|
+
log("[dispatchStreamAbort] Stream aborted: ".concat(messageId));
|
|
3587
|
+
this.dispatch({
|
|
3588
|
+
type: "chat",
|
|
3589
|
+
detail: {
|
|
3590
|
+
type: "chat.stream.abort",
|
|
3591
|
+
user: this.getBot(undefined),
|
|
3592
|
+
messageId: messageId,
|
|
3593
|
+
timestamp: Date.now(),
|
|
3594
|
+
},
|
|
3595
|
+
});
|
|
3596
|
+
};
|
|
3597
|
+
MCPChat.prototype.dispatchStreamResume = function (messageId) {
|
|
3598
|
+
log("[dispatchStreamResume] Stream resumed: ".concat(messageId));
|
|
3599
|
+
this.dispatch({
|
|
3600
|
+
type: "chat",
|
|
3601
|
+
detail: {
|
|
3602
|
+
type: "chat.stream.resume",
|
|
3603
|
+
user: this.getBot(undefined),
|
|
3604
|
+
messageId: messageId,
|
|
3605
|
+
timestamp: Date.now(),
|
|
3606
|
+
},
|
|
3607
|
+
});
|
|
3608
|
+
};
|
|
3545
3609
|
MCPChat.prototype.dispatchToolStart = function (params) {
|
|
3546
3610
|
this.dispatch({
|
|
3547
3611
|
type: "chat",
|
|
@@ -3855,7 +3919,10 @@ var MCPChat = /** @class */ (function () {
|
|
|
3855
3919
|
attempt++;
|
|
3856
3920
|
return [3 /*break*/, 3];
|
|
3857
3921
|
case 14:
|
|
3858
|
-
if (
|
|
3922
|
+
if (success) {
|
|
3923
|
+
this.sentMessageCount++;
|
|
3924
|
+
}
|
|
3925
|
+
else {
|
|
3859
3926
|
log("All MCP retry attempts exhausted");
|
|
3860
3927
|
}
|
|
3861
3928
|
return [2 /*return*/];
|
|
@@ -3907,6 +3974,10 @@ var MCPChat = /** @class */ (function () {
|
|
|
3907
3974
|
: {})),
|
|
3908
3975
|
body: JSON.stringify(body),
|
|
3909
3976
|
signal: _this.abortController.signal,
|
|
3977
|
+
// Keep the connection open when the tab is hidden. Without this,
|
|
3978
|
+
// fetchEventSource closes and re-POSTs on visibility change, which
|
|
3979
|
+
// creates duplicate streaming messages and out-of-order responses.
|
|
3980
|
+
openWhenHidden: true,
|
|
3910
3981
|
onopen: function (response) { return __awaiter$1(_this, void 0, void 0, function () {
|
|
3911
3982
|
var error;
|
|
3912
3983
|
return __generator$1(this, function (_a) {
|
|
@@ -3923,6 +3994,11 @@ var MCPChat = /** @class */ (function () {
|
|
|
3923
3994
|
});
|
|
3924
3995
|
}); },
|
|
3925
3996
|
onmessage: function (event) {
|
|
3997
|
+
// Guard: ignore events from stale/aborted requests
|
|
3998
|
+
if (_this.currentMessageId !== messageId) {
|
|
3999
|
+
log("[SSE] Ignoring stale event for messageId: ".concat(messageId, ", current: ").concat(_this.currentMessageId));
|
|
4000
|
+
return;
|
|
4001
|
+
}
|
|
3926
4002
|
log("SSE event: ".concat(event.event, ", data: ").concat(event.data));
|
|
3927
4003
|
var eventType = event.event || "message";
|
|
3928
4004
|
var data = {};
|
|
@@ -3962,11 +4038,17 @@ var MCPChat = /** @class */ (function () {
|
|
|
3962
4038
|
storeIdentity(data.userId, data.sessionId);
|
|
3963
4039
|
log("[SSE] Identity from connected - userId: ".concat(data.userId, ", sessionId: ").concat(data.sessionId));
|
|
3964
4040
|
}
|
|
3965
|
-
// Always start streaming - server sends greeting for both new and returning users
|
|
3966
|
-
// (resumeSession requests always get a personalized greeting)
|
|
3967
4041
|
_this.currentToolCalls = [];
|
|
3968
4042
|
_this.stopTyping();
|
|
3969
|
-
|
|
4043
|
+
if (data.isReplay) {
|
|
4044
|
+
// Replay connection — resume into the existing partial
|
|
4045
|
+
// message instead of creating a duplicate placeholder
|
|
4046
|
+
_this.dispatchStreamResume(messageId);
|
|
4047
|
+
}
|
|
4048
|
+
else {
|
|
4049
|
+
// Normal connection — create new streaming placeholder
|
|
4050
|
+
_this.dispatchStreamStart(messageId);
|
|
4051
|
+
}
|
|
3970
4052
|
_this.startStreamThrottle(messageId);
|
|
3971
4053
|
break;
|
|
3972
4054
|
case "session_expired":
|
|
@@ -3989,9 +4071,9 @@ var MCPChat = /** @class */ (function () {
|
|
|
3989
4071
|
if (data.text) {
|
|
3990
4072
|
log("[SSE] Text chunk received (".concat(data.text.length, " chars), total chunks: ").concat(textChunks.length + 1, ", isReplay: ").concat(data.isReplay));
|
|
3991
4073
|
textChunks.push(data.text);
|
|
3992
|
-
if (data.isReplay) {
|
|
3993
|
-
// Replayed text — dispatch immediately
|
|
3994
|
-
//
|
|
4074
|
+
if (data.isReplay || _this.isSleeping) {
|
|
4075
|
+
// Replayed text or tab hidden — dispatch immediately
|
|
4076
|
+
// (no throttle timer running in either case)
|
|
3995
4077
|
_this.dispatchStreamChunk(messageId, data.text);
|
|
3996
4078
|
}
|
|
3997
4079
|
else {
|
|
@@ -4088,6 +4170,7 @@ var MCPChat = /** @class */ (function () {
|
|
|
4088
4170
|
text: accumulatedText || data.response.response || "",
|
|
4089
4171
|
options: options,
|
|
4090
4172
|
sources: sources,
|
|
4173
|
+
displays: data.response.displays,
|
|
4091
4174
|
};
|
|
4092
4175
|
// Capture tool calls before flushing (debug mode)
|
|
4093
4176
|
var toolCalls_1 = _this.config.debugMode ? __spreadArray$1([], _this.currentToolCalls, true) : undefined;
|
|
@@ -4125,7 +4208,20 @@ var MCPChat = /** @class */ (function () {
|
|
|
4125
4208
|
if (_this.currentMessageId === messageId && !hasError) {
|
|
4126
4209
|
hasError = true;
|
|
4127
4210
|
_this.isReplaying = false;
|
|
4128
|
-
_this.
|
|
4211
|
+
if (_this.isSleeping) {
|
|
4212
|
+
// Connection dropped while tab is hidden — don't treat as
|
|
4213
|
+
// normal error. Finalize partial message and request replay
|
|
4214
|
+
// when the user returns.
|
|
4215
|
+
log("[MCPChat] SSE error during sleep - will request replay on wakeup");
|
|
4216
|
+
_this.needsReplayOnWakeup = true;
|
|
4217
|
+
if (_this.streamingMessageIdForThrottle) {
|
|
4218
|
+
_this.dispatchStreamAbort(messageId);
|
|
4219
|
+
}
|
|
4220
|
+
_this.stopStreamThrottle();
|
|
4221
|
+
}
|
|
4222
|
+
else {
|
|
4223
|
+
_this.stopStreamThrottle();
|
|
4224
|
+
}
|
|
4129
4225
|
sseError = error instanceof Error ? error : new Error(String(error));
|
|
4130
4226
|
// Get status code if available
|
|
4131
4227
|
var statusCode = (error === null || error === void 0 ? void 0 : error.statusCode) || httpStatusCode;
|
|
@@ -4194,23 +4290,138 @@ var MCPChat = /** @class */ (function () {
|
|
|
4194
4290
|
}
|
|
4195
4291
|
};
|
|
4196
4292
|
MCPChat.prototype.sleep = function () {
|
|
4197
|
-
this.
|
|
4198
|
-
this.
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
this.
|
|
4293
|
+
this.isSleeping = true;
|
|
4294
|
+
if (this.streamingMessageIdForThrottle) {
|
|
4295
|
+
// Active stream in progress — keep SSE connection alive so the
|
|
4296
|
+
// response completes naturally while the tab is hidden.
|
|
4297
|
+
this.needsReplayOnWakeup = false;
|
|
4298
|
+
log("[MCPChat] sleep() - stream in progress, keeping SSE connection alive");
|
|
4299
|
+
// Flush any buffered text to Redux immediately
|
|
4300
|
+
if (this.streamBuffer.length > 0) {
|
|
4301
|
+
this.dispatchStreamChunk(this.streamingMessageIdForThrottle, this.streamBuffer);
|
|
4302
|
+
this.streamBuffer = "";
|
|
4303
|
+
}
|
|
4304
|
+
// Stop the visual throttle timer (browsers throttle timers anyway)
|
|
4305
|
+
// but keep streamingMessageIdForThrottle so incoming chunks can be dispatched
|
|
4306
|
+
if (this.streamThrottleTimer) {
|
|
4307
|
+
clearInterval(this.streamThrottleTimer);
|
|
4308
|
+
this.streamThrottleTimer = undefined;
|
|
4309
|
+
}
|
|
4310
|
+
if (this.visibilityHandler) {
|
|
4311
|
+
document.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
4312
|
+
this.visibilityHandler = undefined;
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
else {
|
|
4316
|
+
// No active stream — safe to abort the idle connection
|
|
4317
|
+
this.stopStreamThrottle();
|
|
4318
|
+
this.isReplaying = false;
|
|
4319
|
+
if (this.abortController) {
|
|
4320
|
+
this.abortController.abort();
|
|
4321
|
+
this.abortController = undefined;
|
|
4322
|
+
}
|
|
4202
4323
|
}
|
|
4203
4324
|
};
|
|
4204
4325
|
MCPChat.prototype.wakeup = function () {
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4326
|
+
this.isSleeping = false;
|
|
4327
|
+
if (this.streamingMessageIdForThrottle && this.abortController) {
|
|
4328
|
+
// Stream is still in progress — restart the visual throttle
|
|
4329
|
+
log("[MCPChat] wakeup() - stream still in progress, restarting throttle");
|
|
4330
|
+
// Flush accumulated buffer before startStreamThrottle (which resets it)
|
|
4331
|
+
if (this.streamBuffer.length > 0) {
|
|
4332
|
+
this.dispatchStreamChunk(this.streamingMessageIdForThrottle, this.streamBuffer);
|
|
4333
|
+
this.streamBuffer = "";
|
|
4334
|
+
}
|
|
4335
|
+
this.startStreamThrottle(this.streamingMessageIdForThrottle);
|
|
4336
|
+
}
|
|
4337
|
+
else if (this.needsReplayOnWakeup) {
|
|
4338
|
+
this.needsReplayOnWakeup = false;
|
|
4339
|
+
if (this.lastSeq !== undefined && this._sessionId) {
|
|
4340
|
+
// Connection dropped during sleep — request replay of missed events.
|
|
4341
|
+
// Don't set "online" yet — requestReplay is fire-and-forget and the
|
|
4342
|
+
// connection status will be updated by the SSE onopen handler.
|
|
4343
|
+
log("[MCPChat] wakeup() - connection lost during sleep, requesting replay");
|
|
4344
|
+
this.requestReplay();
|
|
4345
|
+
return;
|
|
4346
|
+
}
|
|
4347
|
+
// Cannot replay — missing lastSeq or sessionId
|
|
4348
|
+
err("[MCPChat] wakeup() - cannot replay, missing lastSeq (".concat(this.lastSeq, ") or sessionId (").concat(this._sessionId, ")"));
|
|
4349
|
+
}
|
|
4209
4350
|
if (this._sessionId) {
|
|
4210
4351
|
log("[MCPChat] Wakeup - session exists, lastSeq: ".concat(this.lastSeq));
|
|
4211
4352
|
this.setConnectionStatus("online");
|
|
4212
4353
|
}
|
|
4213
4354
|
};
|
|
4355
|
+
MCPChat.prototype.requestReplay = function () {
|
|
4356
|
+
return __awaiter$1(this, void 0, void 0, function () {
|
|
4357
|
+
var error_5;
|
|
4358
|
+
return __generator$1(this, function (_a) {
|
|
4359
|
+
switch (_a.label) {
|
|
4360
|
+
case 0:
|
|
4361
|
+
if (this.isRequestingReplay) {
|
|
4362
|
+
log("[MCPChat] Replay already in progress, skipping");
|
|
4363
|
+
return [2 /*return*/];
|
|
4364
|
+
}
|
|
4365
|
+
this.isRequestingReplay = true;
|
|
4366
|
+
this.typing();
|
|
4367
|
+
_a.label = 1;
|
|
4368
|
+
case 1:
|
|
4369
|
+
_a.trys.push([1, 3, 4, 5]);
|
|
4370
|
+
// lastSeq is already included by buildRequestPayload
|
|
4371
|
+
return [4 /*yield*/, this.performSSERequest(this.buildRequestPayload())];
|
|
4372
|
+
case 2:
|
|
4373
|
+
// lastSeq is already included by buildRequestPayload
|
|
4374
|
+
_a.sent();
|
|
4375
|
+
log("[MCPChat] Replay request completed");
|
|
4376
|
+
return [3 /*break*/, 5];
|
|
4377
|
+
case 3:
|
|
4378
|
+
error_5 = _a.sent();
|
|
4379
|
+
err("[MCPChat] Replay request failed: ".concat(error_5));
|
|
4380
|
+
this.setConnectionStatus("offline");
|
|
4381
|
+
return [3 /*break*/, 5];
|
|
4382
|
+
case 4:
|
|
4383
|
+
this.stopTyping();
|
|
4384
|
+
this.isRequestingReplay = false;
|
|
4385
|
+
return [7 /*endfinally*/];
|
|
4386
|
+
case 5: return [2 /*return*/];
|
|
4387
|
+
}
|
|
4388
|
+
});
|
|
4389
|
+
});
|
|
4390
|
+
};
|
|
4391
|
+
/**
|
|
4392
|
+
* Sends a best-effort close notification to the agent server.
|
|
4393
|
+
* Uses navigator.sendBeacon() so the request survives page unload.
|
|
4394
|
+
* Called from the beforeunload event handler and the widget close button.
|
|
4395
|
+
*/
|
|
4396
|
+
MCPChat.prototype.notifyDisconnecting = function (reason) {
|
|
4397
|
+
if (reason === void 0) { reason = "close_tab"; }
|
|
4398
|
+
if (!this._sessionId) {
|
|
4399
|
+
return;
|
|
4400
|
+
}
|
|
4401
|
+
var endpoint = this.getCloseEndpoint();
|
|
4402
|
+
var payload = JSON.stringify({
|
|
4403
|
+
sessionId: this._sessionId,
|
|
4404
|
+
userId: this._userId || undefined,
|
|
4405
|
+
reason: reason,
|
|
4406
|
+
messageCount: this.sentMessageCount,
|
|
4407
|
+
durationMs: this.sessionStartTime !== undefined ? Date.now() - this.sessionStartTime : 0,
|
|
4408
|
+
});
|
|
4409
|
+
log("[MCPChat] notifyDisconnecting: reason=".concat(reason, ", endpoint=").concat(endpoint));
|
|
4410
|
+
// For widget_close, the page is not unloading so fetch() can be used — it supports
|
|
4411
|
+
// auth headers (needed for business_owner mode) unlike sendBeacon.
|
|
4412
|
+
if (reason === "widget_close" && typeof fetch !== "undefined") {
|
|
4413
|
+
var headers = { "Content-Type": "application/json" };
|
|
4414
|
+
if (this.config.authToken) {
|
|
4415
|
+
headers["Authorization"] = "Bearer ".concat(this.config.authToken);
|
|
4416
|
+
}
|
|
4417
|
+
fetch(endpoint, { method: "POST", headers: headers, body: payload, keepalive: true });
|
|
4418
|
+
return;
|
|
4419
|
+
}
|
|
4420
|
+
if (typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
4421
|
+
var blob = new Blob([payload], { type: "application/json" });
|
|
4422
|
+
navigator.sendBeacon(endpoint, blob);
|
|
4423
|
+
}
|
|
4424
|
+
};
|
|
4214
4425
|
MCPChat.prototype.startSession = function (sessionId) {
|
|
4215
4426
|
// userId priority:
|
|
4216
4427
|
// 1. Existing server-assigned userId (from localStorage or previous connected event)
|
|
@@ -35558,7 +35769,7 @@ var ChatWidgetUI = function (props) {
|
|
|
35558
35769
|
var handleBeforeUnload = function () {
|
|
35559
35770
|
log("beforeunload - notifying router of user disconnecting");
|
|
35560
35771
|
if (chatServer.notifyDisconnecting) {
|
|
35561
|
-
chatServer.notifyDisconnecting();
|
|
35772
|
+
chatServer.notifyDisconnecting("close_tab");
|
|
35562
35773
|
}
|
|
35563
35774
|
};
|
|
35564
35775
|
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
@@ -35712,6 +35923,9 @@ var ChatWidgetUI = function (props) {
|
|
|
35712
35923
|
}
|
|
35713
35924
|
/** Called when cancel is clicked */
|
|
35714
35925
|
function handleCancelClick() {
|
|
35926
|
+
if (chatServer === null || chatServer === void 0 ? void 0 : chatServer.notifyDisconnecting) {
|
|
35927
|
+
chatServer.notifyDisconnecting("widget_close");
|
|
35928
|
+
}
|
|
35715
35929
|
// First reset to clear all state
|
|
35716
35930
|
innerDispatch(reset());
|
|
35717
35931
|
// Then set hasInteracted to prevent CTA from showing after cancel
|
|
@@ -36066,6 +36280,20 @@ function resetReducer(state) {
|
|
|
36066
36280
|
function isChatSystemMessage(chat) {
|
|
36067
36281
|
return chat.type === "chat.systemmessage";
|
|
36068
36282
|
}
|
|
36283
|
+
/**
|
|
36284
|
+
* Find the LAST streaming message index. We must search from the end because
|
|
36285
|
+
* orphan isStreaming messages from interrupted streams may exist earlier in the
|
|
36286
|
+
* array. The active streaming message is always the most recently inserted one.
|
|
36287
|
+
*/
|
|
36288
|
+
function findLastStreamingIndex(chats) {
|
|
36289
|
+
for (var i = chats.length - 1; i >= 0; i--) {
|
|
36290
|
+
var chat = chats[i];
|
|
36291
|
+
if (isChatMsgDetail(chat) && chat.isStreaming) {
|
|
36292
|
+
return i;
|
|
36293
|
+
}
|
|
36294
|
+
}
|
|
36295
|
+
return -1;
|
|
36296
|
+
}
|
|
36069
36297
|
// todo: create reducer (requires redux-thunk dependency)
|
|
36070
36298
|
function appendMessageToState(state, msg) {
|
|
36071
36299
|
state.chats = joinMessages(state.chats, msg);
|
|
@@ -36183,8 +36411,19 @@ function update(state, action) {
|
|
|
36183
36411
|
case "chat.typing":
|
|
36184
36412
|
var agent = state.agents[action.detail.user.nick];
|
|
36185
36413
|
return __assign(__assign({}, state), { lastTimestamp: (_v = action.detail) === null || _v === void 0 ? void 0 : _v.timestamp, agents: __assign(__assign({}, state.agents), (_d = {}, _d[action.detail.user.nick] = __assign(__assign({}, agent), { typing: action.detail.typing }), _d)) });
|
|
36186
|
-
case "chat.stream.start":
|
|
36187
|
-
//
|
|
36414
|
+
case "chat.stream.start": {
|
|
36415
|
+
// Clean up any orphan streaming messages from interrupted streams
|
|
36416
|
+
// before creating the new placeholder.
|
|
36417
|
+
var cleanedChats = newState.chats.map(function (chat) {
|
|
36418
|
+
if (isChatMsgDetail(chat) && chat.isStreaming) {
|
|
36419
|
+
return __assign(__assign({}, chat), { isStreaming: false, wasStreamed: true });
|
|
36420
|
+
}
|
|
36421
|
+
return chat;
|
|
36422
|
+
});
|
|
36423
|
+
// Add placeholder message with streaming state.
|
|
36424
|
+
// Use joinMessages (insertSorted by timestamp) instead of append to
|
|
36425
|
+
// guarantee correct ordering even if events arrive out of order
|
|
36426
|
+
// (e.g., after an SSE abort/retry during tab backgrounding).
|
|
36188
36427
|
var streamingMsg = {
|
|
36189
36428
|
type: "chat.msg",
|
|
36190
36429
|
user: action.detail.user,
|
|
@@ -36194,13 +36433,15 @@ function update(state, action) {
|
|
|
36194
36433
|
},
|
|
36195
36434
|
isStreaming: true,
|
|
36196
36435
|
};
|
|
36197
|
-
|
|
36436
|
+
var newChats = joinMessages(cleanedChats, streamingMsg);
|
|
36437
|
+
return __assign(__assign({}, newState), { chats: newChats, streamingMessageId: action.detail.messageId });
|
|
36438
|
+
}
|
|
36198
36439
|
case "chat.stream.chunk": {
|
|
36199
36440
|
// Find the streaming message by ID and append text
|
|
36200
36441
|
var chunkDetail = action.detail;
|
|
36201
36442
|
if (state.streamingMessageId === chunkDetail.messageId) {
|
|
36202
36443
|
// Find the streaming message by ID (not by index - other messages could be inserted)
|
|
36203
|
-
var streamingIndex_1 = state.chats
|
|
36444
|
+
var streamingIndex_1 = findLastStreamingIndex(state.chats);
|
|
36204
36445
|
if (streamingIndex_1 >= 0) {
|
|
36205
36446
|
var chunkText_1 = chunkDetail.text;
|
|
36206
36447
|
return __assign(__assign({}, state), { lastTimestamp: chunkDetail === null || chunkDetail === void 0 ? void 0 : chunkDetail.timestamp, chats: state.chats.map(function (chat, index) {
|
|
@@ -36220,7 +36461,7 @@ function update(state, action) {
|
|
|
36220
36461
|
var toolStartDetail_1 = action.detail;
|
|
36221
36462
|
if (state.streamingMessageId === toolStartDetail_1.messageId) {
|
|
36222
36463
|
// Find the streaming message by ID (not by index - other messages could be inserted)
|
|
36223
|
-
var streamingIndex_2 = state.chats
|
|
36464
|
+
var streamingIndex_2 = findLastStreamingIndex(state.chats);
|
|
36224
36465
|
if (streamingIndex_2 >= 0) {
|
|
36225
36466
|
return __assign(__assign({}, state), { lastTimestamp: toolStartDetail_1.timestamp, chats: state.chats.map(function (chat, index) {
|
|
36226
36467
|
if (index === streamingIndex_2 && isChatMsgDetail(chat)) {
|
|
@@ -36250,7 +36491,7 @@ function update(state, action) {
|
|
|
36250
36491
|
var toolEndDetail_1 = action.detail;
|
|
36251
36492
|
if (state.streamingMessageId === toolEndDetail_1.messageId) {
|
|
36252
36493
|
// Find the streaming message by ID (not by index - other messages could be inserted)
|
|
36253
|
-
var streamingIndex_3 = state.chats
|
|
36494
|
+
var streamingIndex_3 = findLastStreamingIndex(state.chats);
|
|
36254
36495
|
if (streamingIndex_3 >= 0) {
|
|
36255
36496
|
return __assign(__assign({}, state), { lastTimestamp: toolEndDetail_1.timestamp, chats: state.chats.map(function (chat, index) {
|
|
36256
36497
|
var _a;
|
|
@@ -36279,7 +36520,7 @@ function update(state, action) {
|
|
|
36279
36520
|
var endDetail = action.detail;
|
|
36280
36521
|
if (state.streamingMessageId === endDetail.messageId) {
|
|
36281
36522
|
// Find the streaming message by ID (not by index - other messages could be inserted)
|
|
36282
|
-
var streamingIndex_4 = state.chats
|
|
36523
|
+
var streamingIndex_4 = findLastStreamingIndex(state.chats);
|
|
36283
36524
|
var endStreamingChat = streamingIndex_4 >= 0 ? state.chats[streamingIndex_4] : undefined;
|
|
36284
36525
|
// Verify it's the right message using type guard
|
|
36285
36526
|
if (streamingIndex_4 >= 0 && endStreamingChat && isChatMsgDetail(endStreamingChat)) {
|
|
@@ -36298,6 +36539,51 @@ function update(state, action) {
|
|
|
36298
36539
|
}
|
|
36299
36540
|
return state;
|
|
36300
36541
|
}
|
|
36542
|
+
case "chat.stream.abort": {
|
|
36543
|
+
// Finalize a streaming message interrupted by connection loss during sleep.
|
|
36544
|
+
// Keeps whatever partial text is already in the message.
|
|
36545
|
+
var abortDetail = action.detail;
|
|
36546
|
+
if (state.streamingMessageId === abortDetail.messageId) {
|
|
36547
|
+
var streamingIndex_5 = findLastStreamingIndex(state.chats);
|
|
36548
|
+
if (streamingIndex_5 >= 0) {
|
|
36549
|
+
return __assign(__assign({}, state), { lastTimestamp: abortDetail.timestamp, streamingMessageId: undefined, chats: state.chats.map(function (chat, index) {
|
|
36550
|
+
if (index === streamingIndex_5 && isChatMsgDetail(chat)) {
|
|
36551
|
+
return __assign(__assign({}, chat), { isStreaming: false, wasStreamed: true });
|
|
36552
|
+
}
|
|
36553
|
+
return chat;
|
|
36554
|
+
}) });
|
|
36555
|
+
}
|
|
36556
|
+
}
|
|
36557
|
+
return state;
|
|
36558
|
+
}
|
|
36559
|
+
case "chat.stream.resume": {
|
|
36560
|
+
// Re-enable streaming on the last aborted bot message after replay reconnect.
|
|
36561
|
+
// Targets the most recent wasStreamed message that is NOT currently streaming
|
|
36562
|
+
// (i.e., the one finalized by chat.stream.abort). Updates streamingMessageId
|
|
36563
|
+
// so replayed chunks route to the existing message.
|
|
36564
|
+
// Guard: only resume if no stream is currently active.
|
|
36565
|
+
var resumeDetail = action.detail;
|
|
36566
|
+
if (state.streamingMessageId) {
|
|
36567
|
+
return state;
|
|
36568
|
+
}
|
|
36569
|
+
var lastBotIndex_1 = -1;
|
|
36570
|
+
for (var i = state.chats.length - 1; i >= 0; i--) {
|
|
36571
|
+
var chat = state.chats[i];
|
|
36572
|
+
if (isChatMsgDetail(chat) && chat.wasStreamed && !chat.isStreaming) {
|
|
36573
|
+
lastBotIndex_1 = i;
|
|
36574
|
+
break;
|
|
36575
|
+
}
|
|
36576
|
+
}
|
|
36577
|
+
if (lastBotIndex_1 >= 0) {
|
|
36578
|
+
return __assign(__assign({}, state), { lastTimestamp: resumeDetail.timestamp, streamingMessageId: resumeDetail.messageId, chats: state.chats.map(function (chat, index) {
|
|
36579
|
+
if (index === lastBotIndex_1 && isChatMsgDetail(chat)) {
|
|
36580
|
+
return __assign(__assign({}, chat), { isStreaming: true, wasStreamed: false });
|
|
36581
|
+
}
|
|
36582
|
+
return chat;
|
|
36583
|
+
}) });
|
|
36584
|
+
}
|
|
36585
|
+
return state;
|
|
36586
|
+
}
|
|
36301
36587
|
default:
|
|
36302
36588
|
return state;
|
|
36303
36589
|
}
|