@xapp/chat-widget 1.90.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 +306 -29
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +306 -29
- 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.css +1 -1
- package/dist/xapp-chat-widget.js +4 -4
- package/dist/xapp-chat-widget.js.map +1 -1
- package/package.json +4 -4
package/dist/index.es.js
CHANGED
|
@@ -2992,7 +2992,9 @@ function setSessionId(sessionId) {
|
|
|
2992
2992
|
|
|
2993
2993
|
// Endpoint path constants
|
|
2994
2994
|
var HOMEOWNER_ENDPOINT_PATH = "/chat/stream";
|
|
2995
|
+
var HOMEOWNER_CLOSE_ENDPOINT_PATH = "/chat/close";
|
|
2995
2996
|
var BUSINESS_OWNER_ENDPOINT_PATH = "/api/chat/stream";
|
|
2997
|
+
var BUSINESS_OWNER_CLOSE_ENDPOINT_PATH = "/api/chat/close";
|
|
2996
2998
|
// Default retry configuration for MCP connections
|
|
2997
2999
|
var DEFAULT_MCP_RETRY_CONFIG = [
|
|
2998
3000
|
{ retry: 0, delay: 0, text: "" }, // Initial attempt (silent)
|
|
@@ -3146,6 +3148,22 @@ function clearStoredLastSeq(sessionId) {
|
|
|
3146
3148
|
// sessionStorage may be unavailable
|
|
3147
3149
|
}
|
|
3148
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
|
+
}
|
|
3149
3167
|
/**
|
|
3150
3168
|
* Gets the current page context from browser globals.
|
|
3151
3169
|
* If an override is provided, uses that instead (useful for testing).
|
|
@@ -3154,7 +3172,7 @@ function clearStoredLastSeq(sessionId) {
|
|
|
3154
3172
|
* may contain sensitive data (tokens, PII). Only origin + pathname are sent.
|
|
3155
3173
|
*/
|
|
3156
3174
|
function getPageContext(override) {
|
|
3157
|
-
var localTime =
|
|
3175
|
+
var localTime = getLocalISOString();
|
|
3158
3176
|
// If override provided, merge with defaults (override wins)
|
|
3159
3177
|
// Overrides are typically for testing and may include full URLs
|
|
3160
3178
|
if (override && Object.keys(override).length > 0) {
|
|
@@ -3206,12 +3224,15 @@ var MCPChat = /** @class */ (function () {
|
|
|
3206
3224
|
this.isValidatingSession = false;
|
|
3207
3225
|
// Tool call tracking for debug mode
|
|
3208
3226
|
this.currentToolCalls = [];
|
|
3209
|
-
// TODO: isReplaying and lastSeq are tracked for future replay support.
|
|
3210
|
-
// Wire up replay requests in wakeup() once the server endpoint supports it.
|
|
3211
3227
|
this.isReplaying = false;
|
|
3228
|
+
// Sleep/wakeup state for tab backgrounding recovery
|
|
3229
|
+
this.isSleeping = false;
|
|
3230
|
+
this.isRequestingReplay = false;
|
|
3231
|
+
this.needsReplayOnWakeup = false;
|
|
3212
3232
|
// True when userId was assigned by the server (via connected SSE event),
|
|
3213
3233
|
// meaning it should not be overwritten by the default client fingerprint.
|
|
3214
3234
|
this.userIdIsServerAssigned = false;
|
|
3235
|
+
this.sentMessageCount = 0;
|
|
3215
3236
|
this.config = config;
|
|
3216
3237
|
this.options = options;
|
|
3217
3238
|
this.configurableMessages = options === null || options === void 0 ? void 0 : options.configurableMessages;
|
|
@@ -3247,6 +3268,7 @@ var MCPChat = /** @class */ (function () {
|
|
|
3247
3268
|
}
|
|
3248
3269
|
MCPChat.prototype.init = function (dispatch) {
|
|
3249
3270
|
this.dispatch = dispatch;
|
|
3271
|
+
this.sessionStartTime = Date.now();
|
|
3250
3272
|
var mode = this.config.mode || "homeowner";
|
|
3251
3273
|
log("MCPChat initialized with mode: ".concat(mode));
|
|
3252
3274
|
// Warn if not using HTTPS with authentication
|
|
@@ -3327,7 +3349,9 @@ var MCPChat = /** @class */ (function () {
|
|
|
3327
3349
|
// Page context
|
|
3328
3350
|
pageContext: getPageContext(this.config.pageContextOverride),
|
|
3329
3351
|
// Client timestamp for latency measurement and clock drift detection
|
|
3330
|
-
clientTime: new Date().toISOString()
|
|
3352
|
+
clientTime: new Date().toISOString(),
|
|
3353
|
+
// Last sequence number for replay on reconnect
|
|
3354
|
+
lastSeq: this.lastSeq }, additionalData);
|
|
3331
3355
|
};
|
|
3332
3356
|
/**
|
|
3333
3357
|
* Initializes a new session by calling the endpoint with { newSession: true }.
|
|
@@ -3400,6 +3424,14 @@ var MCPChat = /** @class */ (function () {
|
|
|
3400
3424
|
// homeowner mode - serverUrl should already include appId
|
|
3401
3425
|
return "".concat(serverUrl).concat(HOMEOWNER_ENDPOINT_PATH);
|
|
3402
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
|
+
};
|
|
3403
3435
|
MCPChat.prototype.sendNewMessage = function (msg, user) {
|
|
3404
3436
|
log("SERVER: new message: ".concat(JSON.stringify(msg)));
|
|
3405
3437
|
this.dispatch({
|
|
@@ -3550,6 +3582,30 @@ var MCPChat = /** @class */ (function () {
|
|
|
3550
3582
|
},
|
|
3551
3583
|
});
|
|
3552
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
|
+
};
|
|
3553
3609
|
MCPChat.prototype.dispatchToolStart = function (params) {
|
|
3554
3610
|
this.dispatch({
|
|
3555
3611
|
type: "chat",
|
|
@@ -3863,7 +3919,10 @@ var MCPChat = /** @class */ (function () {
|
|
|
3863
3919
|
attempt++;
|
|
3864
3920
|
return [3 /*break*/, 3];
|
|
3865
3921
|
case 14:
|
|
3866
|
-
if (
|
|
3922
|
+
if (success) {
|
|
3923
|
+
this.sentMessageCount++;
|
|
3924
|
+
}
|
|
3925
|
+
else {
|
|
3867
3926
|
log("All MCP retry attempts exhausted");
|
|
3868
3927
|
}
|
|
3869
3928
|
return [2 /*return*/];
|
|
@@ -3915,6 +3974,10 @@ var MCPChat = /** @class */ (function () {
|
|
|
3915
3974
|
: {})),
|
|
3916
3975
|
body: JSON.stringify(body),
|
|
3917
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,
|
|
3918
3981
|
onopen: function (response) { return __awaiter$1(_this, void 0, void 0, function () {
|
|
3919
3982
|
var error;
|
|
3920
3983
|
return __generator$1(this, function (_a) {
|
|
@@ -3931,6 +3994,11 @@ var MCPChat = /** @class */ (function () {
|
|
|
3931
3994
|
});
|
|
3932
3995
|
}); },
|
|
3933
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
|
+
}
|
|
3934
4002
|
log("SSE event: ".concat(event.event, ", data: ").concat(event.data));
|
|
3935
4003
|
var eventType = event.event || "message";
|
|
3936
4004
|
var data = {};
|
|
@@ -3970,11 +4038,17 @@ var MCPChat = /** @class */ (function () {
|
|
|
3970
4038
|
storeIdentity(data.userId, data.sessionId);
|
|
3971
4039
|
log("[SSE] Identity from connected - userId: ".concat(data.userId, ", sessionId: ").concat(data.sessionId));
|
|
3972
4040
|
}
|
|
3973
|
-
// Always start streaming - server sends greeting for both new and returning users
|
|
3974
|
-
// (resumeSession requests always get a personalized greeting)
|
|
3975
4041
|
_this.currentToolCalls = [];
|
|
3976
4042
|
_this.stopTyping();
|
|
3977
|
-
|
|
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
|
+
}
|
|
3978
4052
|
_this.startStreamThrottle(messageId);
|
|
3979
4053
|
break;
|
|
3980
4054
|
case "session_expired":
|
|
@@ -3997,9 +4071,9 @@ var MCPChat = /** @class */ (function () {
|
|
|
3997
4071
|
if (data.text) {
|
|
3998
4072
|
log("[SSE] Text chunk received (".concat(data.text.length, " chars), total chunks: ").concat(textChunks.length + 1, ", isReplay: ").concat(data.isReplay));
|
|
3999
4073
|
textChunks.push(data.text);
|
|
4000
|
-
if (data.isReplay) {
|
|
4001
|
-
// Replayed text — dispatch immediately
|
|
4002
|
-
//
|
|
4074
|
+
if (data.isReplay || _this.isSleeping) {
|
|
4075
|
+
// Replayed text or tab hidden — dispatch immediately
|
|
4076
|
+
// (no throttle timer running in either case)
|
|
4003
4077
|
_this.dispatchStreamChunk(messageId, data.text);
|
|
4004
4078
|
}
|
|
4005
4079
|
else {
|
|
@@ -4134,7 +4208,20 @@ var MCPChat = /** @class */ (function () {
|
|
|
4134
4208
|
if (_this.currentMessageId === messageId && !hasError) {
|
|
4135
4209
|
hasError = true;
|
|
4136
4210
|
_this.isReplaying = false;
|
|
4137
|
-
_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
|
+
}
|
|
4138
4225
|
sseError = error instanceof Error ? error : new Error(String(error));
|
|
4139
4226
|
// Get status code if available
|
|
4140
4227
|
var statusCode = (error === null || error === void 0 ? void 0 : error.statusCode) || httpStatusCode;
|
|
@@ -4203,23 +4290,138 @@ var MCPChat = /** @class */ (function () {
|
|
|
4203
4290
|
}
|
|
4204
4291
|
};
|
|
4205
4292
|
MCPChat.prototype.sleep = function () {
|
|
4206
|
-
this.
|
|
4207
|
-
this.
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
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
|
+
}
|
|
4211
4323
|
}
|
|
4212
4324
|
};
|
|
4213
4325
|
MCPChat.prototype.wakeup = function () {
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
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
|
+
}
|
|
4218
4350
|
if (this._sessionId) {
|
|
4219
4351
|
log("[MCPChat] Wakeup - session exists, lastSeq: ".concat(this.lastSeq));
|
|
4220
4352
|
this.setConnectionStatus("online");
|
|
4221
4353
|
}
|
|
4222
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
|
+
};
|
|
4223
4425
|
MCPChat.prototype.startSession = function (sessionId) {
|
|
4224
4426
|
// userId priority:
|
|
4225
4427
|
// 1. Existing server-assigned userId (from localStorage or previous connected event)
|
|
@@ -35567,7 +35769,7 @@ var ChatWidgetUI = function (props) {
|
|
|
35567
35769
|
var handleBeforeUnload = function () {
|
|
35568
35770
|
log("beforeunload - notifying router of user disconnecting");
|
|
35569
35771
|
if (chatServer.notifyDisconnecting) {
|
|
35570
|
-
chatServer.notifyDisconnecting();
|
|
35772
|
+
chatServer.notifyDisconnecting("close_tab");
|
|
35571
35773
|
}
|
|
35572
35774
|
};
|
|
35573
35775
|
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
@@ -35721,6 +35923,9 @@ var ChatWidgetUI = function (props) {
|
|
|
35721
35923
|
}
|
|
35722
35924
|
/** Called when cancel is clicked */
|
|
35723
35925
|
function handleCancelClick() {
|
|
35926
|
+
if (chatServer === null || chatServer === void 0 ? void 0 : chatServer.notifyDisconnecting) {
|
|
35927
|
+
chatServer.notifyDisconnecting("widget_close");
|
|
35928
|
+
}
|
|
35724
35929
|
// First reset to clear all state
|
|
35725
35930
|
innerDispatch(reset());
|
|
35726
35931
|
// Then set hasInteracted to prevent CTA from showing after cancel
|
|
@@ -36075,6 +36280,20 @@ function resetReducer(state) {
|
|
|
36075
36280
|
function isChatSystemMessage(chat) {
|
|
36076
36281
|
return chat.type === "chat.systemmessage";
|
|
36077
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
|
+
}
|
|
36078
36297
|
// todo: create reducer (requires redux-thunk dependency)
|
|
36079
36298
|
function appendMessageToState(state, msg) {
|
|
36080
36299
|
state.chats = joinMessages(state.chats, msg);
|
|
@@ -36192,8 +36411,19 @@ function update(state, action) {
|
|
|
36192
36411
|
case "chat.typing":
|
|
36193
36412
|
var agent = state.agents[action.detail.user.nick];
|
|
36194
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)) });
|
|
36195
|
-
case "chat.stream.start":
|
|
36196
|
-
//
|
|
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).
|
|
36197
36427
|
var streamingMsg = {
|
|
36198
36428
|
type: "chat.msg",
|
|
36199
36429
|
user: action.detail.user,
|
|
@@ -36203,13 +36433,15 @@ function update(state, action) {
|
|
|
36203
36433
|
},
|
|
36204
36434
|
isStreaming: true,
|
|
36205
36435
|
};
|
|
36206
|
-
|
|
36436
|
+
var newChats = joinMessages(cleanedChats, streamingMsg);
|
|
36437
|
+
return __assign(__assign({}, newState), { chats: newChats, streamingMessageId: action.detail.messageId });
|
|
36438
|
+
}
|
|
36207
36439
|
case "chat.stream.chunk": {
|
|
36208
36440
|
// Find the streaming message by ID and append text
|
|
36209
36441
|
var chunkDetail = action.detail;
|
|
36210
36442
|
if (state.streamingMessageId === chunkDetail.messageId) {
|
|
36211
36443
|
// Find the streaming message by ID (not by index - other messages could be inserted)
|
|
36212
|
-
var streamingIndex_1 = state.chats
|
|
36444
|
+
var streamingIndex_1 = findLastStreamingIndex(state.chats);
|
|
36213
36445
|
if (streamingIndex_1 >= 0) {
|
|
36214
36446
|
var chunkText_1 = chunkDetail.text;
|
|
36215
36447
|
return __assign(__assign({}, state), { lastTimestamp: chunkDetail === null || chunkDetail === void 0 ? void 0 : chunkDetail.timestamp, chats: state.chats.map(function (chat, index) {
|
|
@@ -36229,7 +36461,7 @@ function update(state, action) {
|
|
|
36229
36461
|
var toolStartDetail_1 = action.detail;
|
|
36230
36462
|
if (state.streamingMessageId === toolStartDetail_1.messageId) {
|
|
36231
36463
|
// Find the streaming message by ID (not by index - other messages could be inserted)
|
|
36232
|
-
var streamingIndex_2 = state.chats
|
|
36464
|
+
var streamingIndex_2 = findLastStreamingIndex(state.chats);
|
|
36233
36465
|
if (streamingIndex_2 >= 0) {
|
|
36234
36466
|
return __assign(__assign({}, state), { lastTimestamp: toolStartDetail_1.timestamp, chats: state.chats.map(function (chat, index) {
|
|
36235
36467
|
if (index === streamingIndex_2 && isChatMsgDetail(chat)) {
|
|
@@ -36259,7 +36491,7 @@ function update(state, action) {
|
|
|
36259
36491
|
var toolEndDetail_1 = action.detail;
|
|
36260
36492
|
if (state.streamingMessageId === toolEndDetail_1.messageId) {
|
|
36261
36493
|
// Find the streaming message by ID (not by index - other messages could be inserted)
|
|
36262
|
-
var streamingIndex_3 = state.chats
|
|
36494
|
+
var streamingIndex_3 = findLastStreamingIndex(state.chats);
|
|
36263
36495
|
if (streamingIndex_3 >= 0) {
|
|
36264
36496
|
return __assign(__assign({}, state), { lastTimestamp: toolEndDetail_1.timestamp, chats: state.chats.map(function (chat, index) {
|
|
36265
36497
|
var _a;
|
|
@@ -36288,7 +36520,7 @@ function update(state, action) {
|
|
|
36288
36520
|
var endDetail = action.detail;
|
|
36289
36521
|
if (state.streamingMessageId === endDetail.messageId) {
|
|
36290
36522
|
// Find the streaming message by ID (not by index - other messages could be inserted)
|
|
36291
|
-
var streamingIndex_4 = state.chats
|
|
36523
|
+
var streamingIndex_4 = findLastStreamingIndex(state.chats);
|
|
36292
36524
|
var endStreamingChat = streamingIndex_4 >= 0 ? state.chats[streamingIndex_4] : undefined;
|
|
36293
36525
|
// Verify it's the right message using type guard
|
|
36294
36526
|
if (streamingIndex_4 >= 0 && endStreamingChat && isChatMsgDetail(endStreamingChat)) {
|
|
@@ -36307,6 +36539,51 @@ function update(state, action) {
|
|
|
36307
36539
|
}
|
|
36308
36540
|
return state;
|
|
36309
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
|
+
}
|
|
36310
36587
|
default:
|
|
36311
36588
|
return state;
|
|
36312
36589
|
}
|