@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.js
CHANGED
|
@@ -2993,7 +2993,9 @@ function setSessionId(sessionId) {
|
|
|
2993
2993
|
|
|
2994
2994
|
// Endpoint path constants
|
|
2995
2995
|
var HOMEOWNER_ENDPOINT_PATH = "/chat/stream";
|
|
2996
|
+
var HOMEOWNER_CLOSE_ENDPOINT_PATH = "/chat/close";
|
|
2996
2997
|
var BUSINESS_OWNER_ENDPOINT_PATH = "/api/chat/stream";
|
|
2998
|
+
var BUSINESS_OWNER_CLOSE_ENDPOINT_PATH = "/api/chat/close";
|
|
2997
2999
|
// Default retry configuration for MCP connections
|
|
2998
3000
|
var DEFAULT_MCP_RETRY_CONFIG = [
|
|
2999
3001
|
{ retry: 0, delay: 0, text: "" }, // Initial attempt (silent)
|
|
@@ -3147,6 +3149,22 @@ function clearStoredLastSeq(sessionId) {
|
|
|
3147
3149
|
// sessionStorage may be unavailable
|
|
3148
3150
|
}
|
|
3149
3151
|
}
|
|
3152
|
+
/**
|
|
3153
|
+
* Returns the current local time as an ISO 8601 string with the local timezone offset.
|
|
3154
|
+
* Unlike toISOString(), this preserves the user's local time zone (e.g. "2024-01-15T14:30:00-04:00")
|
|
3155
|
+
* so the agent server can correctly determine time-of-day context (morning greetings, etc.).
|
|
3156
|
+
*/
|
|
3157
|
+
function getLocalISOString(date) {
|
|
3158
|
+
if (date === void 0) { date = new Date(); }
|
|
3159
|
+
var offsetMinutes = -date.getTimezoneOffset();
|
|
3160
|
+
var sign = offsetMinutes >= 0 ? "+" : "-";
|
|
3161
|
+
var pad = function (n) { return String(Math.abs(n)).padStart(2, "0"); };
|
|
3162
|
+
var offsetHours = Math.floor(Math.abs(offsetMinutes) / 60);
|
|
3163
|
+
var offsetMins = Math.abs(offsetMinutes) % 60;
|
|
3164
|
+
return ("".concat(date.getFullYear(), "-").concat(pad(date.getMonth() + 1), "-").concat(pad(date.getDate())) +
|
|
3165
|
+
"T".concat(pad(date.getHours()), ":").concat(pad(date.getMinutes()), ":").concat(pad(date.getSeconds())) +
|
|
3166
|
+
"".concat(sign).concat(pad(offsetHours), ":").concat(pad(offsetMins)));
|
|
3167
|
+
}
|
|
3150
3168
|
/**
|
|
3151
3169
|
* Gets the current page context from browser globals.
|
|
3152
3170
|
* If an override is provided, uses that instead (useful for testing).
|
|
@@ -3155,7 +3173,7 @@ function clearStoredLastSeq(sessionId) {
|
|
|
3155
3173
|
* may contain sensitive data (tokens, PII). Only origin + pathname are sent.
|
|
3156
3174
|
*/
|
|
3157
3175
|
function getPageContext(override) {
|
|
3158
|
-
var localTime =
|
|
3176
|
+
var localTime = getLocalISOString();
|
|
3159
3177
|
// If override provided, merge with defaults (override wins)
|
|
3160
3178
|
// Overrides are typically for testing and may include full URLs
|
|
3161
3179
|
if (override && Object.keys(override).length > 0) {
|
|
@@ -3207,12 +3225,15 @@ var MCPChat = /** @class */ (function () {
|
|
|
3207
3225
|
this.isValidatingSession = false;
|
|
3208
3226
|
// Tool call tracking for debug mode
|
|
3209
3227
|
this.currentToolCalls = [];
|
|
3210
|
-
// TODO: isReplaying and lastSeq are tracked for future replay support.
|
|
3211
|
-
// Wire up replay requests in wakeup() once the server endpoint supports it.
|
|
3212
3228
|
this.isReplaying = false;
|
|
3229
|
+
// Sleep/wakeup state for tab backgrounding recovery
|
|
3230
|
+
this.isSleeping = false;
|
|
3231
|
+
this.isRequestingReplay = false;
|
|
3232
|
+
this.needsReplayOnWakeup = false;
|
|
3213
3233
|
// True when userId was assigned by the server (via connected SSE event),
|
|
3214
3234
|
// meaning it should not be overwritten by the default client fingerprint.
|
|
3215
3235
|
this.userIdIsServerAssigned = false;
|
|
3236
|
+
this.sentMessageCount = 0;
|
|
3216
3237
|
this.config = config;
|
|
3217
3238
|
this.options = options;
|
|
3218
3239
|
this.configurableMessages = options === null || options === void 0 ? void 0 : options.configurableMessages;
|
|
@@ -3248,6 +3269,7 @@ var MCPChat = /** @class */ (function () {
|
|
|
3248
3269
|
}
|
|
3249
3270
|
MCPChat.prototype.init = function (dispatch) {
|
|
3250
3271
|
this.dispatch = dispatch;
|
|
3272
|
+
this.sessionStartTime = Date.now();
|
|
3251
3273
|
var mode = this.config.mode || "homeowner";
|
|
3252
3274
|
log("MCPChat initialized with mode: ".concat(mode));
|
|
3253
3275
|
// Warn if not using HTTPS with authentication
|
|
@@ -3328,7 +3350,9 @@ var MCPChat = /** @class */ (function () {
|
|
|
3328
3350
|
// Page context
|
|
3329
3351
|
pageContext: getPageContext(this.config.pageContextOverride),
|
|
3330
3352
|
// Client timestamp for latency measurement and clock drift detection
|
|
3331
|
-
clientTime: new Date().toISOString()
|
|
3353
|
+
clientTime: new Date().toISOString(),
|
|
3354
|
+
// Last sequence number for replay on reconnect
|
|
3355
|
+
lastSeq: this.lastSeq }, additionalData);
|
|
3332
3356
|
};
|
|
3333
3357
|
/**
|
|
3334
3358
|
* Initializes a new session by calling the endpoint with { newSession: true }.
|
|
@@ -3401,6 +3425,14 @@ var MCPChat = /** @class */ (function () {
|
|
|
3401
3425
|
// homeowner mode - serverUrl should already include appId
|
|
3402
3426
|
return "".concat(serverUrl).concat(HOMEOWNER_ENDPOINT_PATH);
|
|
3403
3427
|
};
|
|
3428
|
+
MCPChat.prototype.getCloseEndpoint = function () {
|
|
3429
|
+
var serverUrl = this.config.serverUrl.replace(/\/+$/, "");
|
|
3430
|
+
var mode = this.config.mode || "homeowner";
|
|
3431
|
+
if (mode === "business_owner") {
|
|
3432
|
+
return "".concat(serverUrl).concat(BUSINESS_OWNER_CLOSE_ENDPOINT_PATH);
|
|
3433
|
+
}
|
|
3434
|
+
return "".concat(serverUrl).concat(HOMEOWNER_CLOSE_ENDPOINT_PATH);
|
|
3435
|
+
};
|
|
3404
3436
|
MCPChat.prototype.sendNewMessage = function (msg, user) {
|
|
3405
3437
|
log("SERVER: new message: ".concat(JSON.stringify(msg)));
|
|
3406
3438
|
this.dispatch({
|
|
@@ -3551,6 +3583,30 @@ var MCPChat = /** @class */ (function () {
|
|
|
3551
3583
|
},
|
|
3552
3584
|
});
|
|
3553
3585
|
};
|
|
3586
|
+
MCPChat.prototype.dispatchStreamAbort = function (messageId) {
|
|
3587
|
+
log("[dispatchStreamAbort] Stream aborted: ".concat(messageId));
|
|
3588
|
+
this.dispatch({
|
|
3589
|
+
type: "chat",
|
|
3590
|
+
detail: {
|
|
3591
|
+
type: "chat.stream.abort",
|
|
3592
|
+
user: this.getBot(undefined),
|
|
3593
|
+
messageId: messageId,
|
|
3594
|
+
timestamp: Date.now(),
|
|
3595
|
+
},
|
|
3596
|
+
});
|
|
3597
|
+
};
|
|
3598
|
+
MCPChat.prototype.dispatchStreamResume = function (messageId) {
|
|
3599
|
+
log("[dispatchStreamResume] Stream resumed: ".concat(messageId));
|
|
3600
|
+
this.dispatch({
|
|
3601
|
+
type: "chat",
|
|
3602
|
+
detail: {
|
|
3603
|
+
type: "chat.stream.resume",
|
|
3604
|
+
user: this.getBot(undefined),
|
|
3605
|
+
messageId: messageId,
|
|
3606
|
+
timestamp: Date.now(),
|
|
3607
|
+
},
|
|
3608
|
+
});
|
|
3609
|
+
};
|
|
3554
3610
|
MCPChat.prototype.dispatchToolStart = function (params) {
|
|
3555
3611
|
this.dispatch({
|
|
3556
3612
|
type: "chat",
|
|
@@ -3864,7 +3920,10 @@ var MCPChat = /** @class */ (function () {
|
|
|
3864
3920
|
attempt++;
|
|
3865
3921
|
return [3 /*break*/, 3];
|
|
3866
3922
|
case 14:
|
|
3867
|
-
if (
|
|
3923
|
+
if (success) {
|
|
3924
|
+
this.sentMessageCount++;
|
|
3925
|
+
}
|
|
3926
|
+
else {
|
|
3868
3927
|
log("All MCP retry attempts exhausted");
|
|
3869
3928
|
}
|
|
3870
3929
|
return [2 /*return*/];
|
|
@@ -3916,6 +3975,10 @@ var MCPChat = /** @class */ (function () {
|
|
|
3916
3975
|
: {})),
|
|
3917
3976
|
body: JSON.stringify(body),
|
|
3918
3977
|
signal: _this.abortController.signal,
|
|
3978
|
+
// Keep the connection open when the tab is hidden. Without this,
|
|
3979
|
+
// fetchEventSource closes and re-POSTs on visibility change, which
|
|
3980
|
+
// creates duplicate streaming messages and out-of-order responses.
|
|
3981
|
+
openWhenHidden: true,
|
|
3919
3982
|
onopen: function (response) { return __awaiter$1(_this, void 0, void 0, function () {
|
|
3920
3983
|
var error;
|
|
3921
3984
|
return __generator$1(this, function (_a) {
|
|
@@ -3932,6 +3995,11 @@ var MCPChat = /** @class */ (function () {
|
|
|
3932
3995
|
});
|
|
3933
3996
|
}); },
|
|
3934
3997
|
onmessage: function (event) {
|
|
3998
|
+
// Guard: ignore events from stale/aborted requests
|
|
3999
|
+
if (_this.currentMessageId !== messageId) {
|
|
4000
|
+
log("[SSE] Ignoring stale event for messageId: ".concat(messageId, ", current: ").concat(_this.currentMessageId));
|
|
4001
|
+
return;
|
|
4002
|
+
}
|
|
3935
4003
|
log("SSE event: ".concat(event.event, ", data: ").concat(event.data));
|
|
3936
4004
|
var eventType = event.event || "message";
|
|
3937
4005
|
var data = {};
|
|
@@ -3971,11 +4039,17 @@ var MCPChat = /** @class */ (function () {
|
|
|
3971
4039
|
storeIdentity(data.userId, data.sessionId);
|
|
3972
4040
|
log("[SSE] Identity from connected - userId: ".concat(data.userId, ", sessionId: ").concat(data.sessionId));
|
|
3973
4041
|
}
|
|
3974
|
-
// Always start streaming - server sends greeting for both new and returning users
|
|
3975
|
-
// (resumeSession requests always get a personalized greeting)
|
|
3976
4042
|
_this.currentToolCalls = [];
|
|
3977
4043
|
_this.stopTyping();
|
|
3978
|
-
|
|
4044
|
+
if (data.isReplay) {
|
|
4045
|
+
// Replay connection — resume into the existing partial
|
|
4046
|
+
// message instead of creating a duplicate placeholder
|
|
4047
|
+
_this.dispatchStreamResume(messageId);
|
|
4048
|
+
}
|
|
4049
|
+
else {
|
|
4050
|
+
// Normal connection — create new streaming placeholder
|
|
4051
|
+
_this.dispatchStreamStart(messageId);
|
|
4052
|
+
}
|
|
3979
4053
|
_this.startStreamThrottle(messageId);
|
|
3980
4054
|
break;
|
|
3981
4055
|
case "session_expired":
|
|
@@ -3998,9 +4072,9 @@ var MCPChat = /** @class */ (function () {
|
|
|
3998
4072
|
if (data.text) {
|
|
3999
4073
|
log("[SSE] Text chunk received (".concat(data.text.length, " chars), total chunks: ").concat(textChunks.length + 1, ", isReplay: ").concat(data.isReplay));
|
|
4000
4074
|
textChunks.push(data.text);
|
|
4001
|
-
if (data.isReplay) {
|
|
4002
|
-
// Replayed text — dispatch immediately
|
|
4003
|
-
//
|
|
4075
|
+
if (data.isReplay || _this.isSleeping) {
|
|
4076
|
+
// Replayed text or tab hidden — dispatch immediately
|
|
4077
|
+
// (no throttle timer running in either case)
|
|
4004
4078
|
_this.dispatchStreamChunk(messageId, data.text);
|
|
4005
4079
|
}
|
|
4006
4080
|
else {
|
|
@@ -4135,7 +4209,20 @@ var MCPChat = /** @class */ (function () {
|
|
|
4135
4209
|
if (_this.currentMessageId === messageId && !hasError) {
|
|
4136
4210
|
hasError = true;
|
|
4137
4211
|
_this.isReplaying = false;
|
|
4138
|
-
_this.
|
|
4212
|
+
if (_this.isSleeping) {
|
|
4213
|
+
// Connection dropped while tab is hidden — don't treat as
|
|
4214
|
+
// normal error. Finalize partial message and request replay
|
|
4215
|
+
// when the user returns.
|
|
4216
|
+
log("[MCPChat] SSE error during sleep - will request replay on wakeup");
|
|
4217
|
+
_this.needsReplayOnWakeup = true;
|
|
4218
|
+
if (_this.streamingMessageIdForThrottle) {
|
|
4219
|
+
_this.dispatchStreamAbort(messageId);
|
|
4220
|
+
}
|
|
4221
|
+
_this.stopStreamThrottle();
|
|
4222
|
+
}
|
|
4223
|
+
else {
|
|
4224
|
+
_this.stopStreamThrottle();
|
|
4225
|
+
}
|
|
4139
4226
|
sseError = error instanceof Error ? error : new Error(String(error));
|
|
4140
4227
|
// Get status code if available
|
|
4141
4228
|
var statusCode = (error === null || error === void 0 ? void 0 : error.statusCode) || httpStatusCode;
|
|
@@ -4204,23 +4291,138 @@ var MCPChat = /** @class */ (function () {
|
|
|
4204
4291
|
}
|
|
4205
4292
|
};
|
|
4206
4293
|
MCPChat.prototype.sleep = function () {
|
|
4207
|
-
this.
|
|
4208
|
-
this.
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
this.
|
|
4294
|
+
this.isSleeping = true;
|
|
4295
|
+
if (this.streamingMessageIdForThrottle) {
|
|
4296
|
+
// Active stream in progress — keep SSE connection alive so the
|
|
4297
|
+
// response completes naturally while the tab is hidden.
|
|
4298
|
+
this.needsReplayOnWakeup = false;
|
|
4299
|
+
log("[MCPChat] sleep() - stream in progress, keeping SSE connection alive");
|
|
4300
|
+
// Flush any buffered text to Redux immediately
|
|
4301
|
+
if (this.streamBuffer.length > 0) {
|
|
4302
|
+
this.dispatchStreamChunk(this.streamingMessageIdForThrottle, this.streamBuffer);
|
|
4303
|
+
this.streamBuffer = "";
|
|
4304
|
+
}
|
|
4305
|
+
// Stop the visual throttle timer (browsers throttle timers anyway)
|
|
4306
|
+
// but keep streamingMessageIdForThrottle so incoming chunks can be dispatched
|
|
4307
|
+
if (this.streamThrottleTimer) {
|
|
4308
|
+
clearInterval(this.streamThrottleTimer);
|
|
4309
|
+
this.streamThrottleTimer = undefined;
|
|
4310
|
+
}
|
|
4311
|
+
if (this.visibilityHandler) {
|
|
4312
|
+
document.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
4313
|
+
this.visibilityHandler = undefined;
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
else {
|
|
4317
|
+
// No active stream — safe to abort the idle connection
|
|
4318
|
+
this.stopStreamThrottle();
|
|
4319
|
+
this.isReplaying = false;
|
|
4320
|
+
if (this.abortController) {
|
|
4321
|
+
this.abortController.abort();
|
|
4322
|
+
this.abortController = undefined;
|
|
4323
|
+
}
|
|
4212
4324
|
}
|
|
4213
4325
|
};
|
|
4214
4326
|
MCPChat.prototype.wakeup = function () {
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4327
|
+
this.isSleeping = false;
|
|
4328
|
+
if (this.streamingMessageIdForThrottle && this.abortController) {
|
|
4329
|
+
// Stream is still in progress — restart the visual throttle
|
|
4330
|
+
log("[MCPChat] wakeup() - stream still in progress, restarting throttle");
|
|
4331
|
+
// Flush accumulated buffer before startStreamThrottle (which resets it)
|
|
4332
|
+
if (this.streamBuffer.length > 0) {
|
|
4333
|
+
this.dispatchStreamChunk(this.streamingMessageIdForThrottle, this.streamBuffer);
|
|
4334
|
+
this.streamBuffer = "";
|
|
4335
|
+
}
|
|
4336
|
+
this.startStreamThrottle(this.streamingMessageIdForThrottle);
|
|
4337
|
+
}
|
|
4338
|
+
else if (this.needsReplayOnWakeup) {
|
|
4339
|
+
this.needsReplayOnWakeup = false;
|
|
4340
|
+
if (this.lastSeq !== undefined && this._sessionId) {
|
|
4341
|
+
// Connection dropped during sleep — request replay of missed events.
|
|
4342
|
+
// Don't set "online" yet — requestReplay is fire-and-forget and the
|
|
4343
|
+
// connection status will be updated by the SSE onopen handler.
|
|
4344
|
+
log("[MCPChat] wakeup() - connection lost during sleep, requesting replay");
|
|
4345
|
+
this.requestReplay();
|
|
4346
|
+
return;
|
|
4347
|
+
}
|
|
4348
|
+
// Cannot replay — missing lastSeq or sessionId
|
|
4349
|
+
err("[MCPChat] wakeup() - cannot replay, missing lastSeq (".concat(this.lastSeq, ") or sessionId (").concat(this._sessionId, ")"));
|
|
4350
|
+
}
|
|
4219
4351
|
if (this._sessionId) {
|
|
4220
4352
|
log("[MCPChat] Wakeup - session exists, lastSeq: ".concat(this.lastSeq));
|
|
4221
4353
|
this.setConnectionStatus("online");
|
|
4222
4354
|
}
|
|
4223
4355
|
};
|
|
4356
|
+
MCPChat.prototype.requestReplay = function () {
|
|
4357
|
+
return __awaiter$1(this, void 0, void 0, function () {
|
|
4358
|
+
var error_5;
|
|
4359
|
+
return __generator$1(this, function (_a) {
|
|
4360
|
+
switch (_a.label) {
|
|
4361
|
+
case 0:
|
|
4362
|
+
if (this.isRequestingReplay) {
|
|
4363
|
+
log("[MCPChat] Replay already in progress, skipping");
|
|
4364
|
+
return [2 /*return*/];
|
|
4365
|
+
}
|
|
4366
|
+
this.isRequestingReplay = true;
|
|
4367
|
+
this.typing();
|
|
4368
|
+
_a.label = 1;
|
|
4369
|
+
case 1:
|
|
4370
|
+
_a.trys.push([1, 3, 4, 5]);
|
|
4371
|
+
// lastSeq is already included by buildRequestPayload
|
|
4372
|
+
return [4 /*yield*/, this.performSSERequest(this.buildRequestPayload())];
|
|
4373
|
+
case 2:
|
|
4374
|
+
// lastSeq is already included by buildRequestPayload
|
|
4375
|
+
_a.sent();
|
|
4376
|
+
log("[MCPChat] Replay request completed");
|
|
4377
|
+
return [3 /*break*/, 5];
|
|
4378
|
+
case 3:
|
|
4379
|
+
error_5 = _a.sent();
|
|
4380
|
+
err("[MCPChat] Replay request failed: ".concat(error_5));
|
|
4381
|
+
this.setConnectionStatus("offline");
|
|
4382
|
+
return [3 /*break*/, 5];
|
|
4383
|
+
case 4:
|
|
4384
|
+
this.stopTyping();
|
|
4385
|
+
this.isRequestingReplay = false;
|
|
4386
|
+
return [7 /*endfinally*/];
|
|
4387
|
+
case 5: return [2 /*return*/];
|
|
4388
|
+
}
|
|
4389
|
+
});
|
|
4390
|
+
});
|
|
4391
|
+
};
|
|
4392
|
+
/**
|
|
4393
|
+
* Sends a best-effort close notification to the agent server.
|
|
4394
|
+
* Uses navigator.sendBeacon() so the request survives page unload.
|
|
4395
|
+
* Called from the beforeunload event handler and the widget close button.
|
|
4396
|
+
*/
|
|
4397
|
+
MCPChat.prototype.notifyDisconnecting = function (reason) {
|
|
4398
|
+
if (reason === void 0) { reason = "close_tab"; }
|
|
4399
|
+
if (!this._sessionId) {
|
|
4400
|
+
return;
|
|
4401
|
+
}
|
|
4402
|
+
var endpoint = this.getCloseEndpoint();
|
|
4403
|
+
var payload = JSON.stringify({
|
|
4404
|
+
sessionId: this._sessionId,
|
|
4405
|
+
userId: this._userId || undefined,
|
|
4406
|
+
reason: reason,
|
|
4407
|
+
messageCount: this.sentMessageCount,
|
|
4408
|
+
durationMs: this.sessionStartTime !== undefined ? Date.now() - this.sessionStartTime : 0,
|
|
4409
|
+
});
|
|
4410
|
+
log("[MCPChat] notifyDisconnecting: reason=".concat(reason, ", endpoint=").concat(endpoint));
|
|
4411
|
+
// For widget_close, the page is not unloading so fetch() can be used — it supports
|
|
4412
|
+
// auth headers (needed for business_owner mode) unlike sendBeacon.
|
|
4413
|
+
if (reason === "widget_close" && typeof fetch !== "undefined") {
|
|
4414
|
+
var headers = { "Content-Type": "application/json" };
|
|
4415
|
+
if (this.config.authToken) {
|
|
4416
|
+
headers["Authorization"] = "Bearer ".concat(this.config.authToken);
|
|
4417
|
+
}
|
|
4418
|
+
fetch(endpoint, { method: "POST", headers: headers, body: payload, keepalive: true });
|
|
4419
|
+
return;
|
|
4420
|
+
}
|
|
4421
|
+
if (typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
4422
|
+
var blob = new Blob([payload], { type: "application/json" });
|
|
4423
|
+
navigator.sendBeacon(endpoint, blob);
|
|
4424
|
+
}
|
|
4425
|
+
};
|
|
4224
4426
|
MCPChat.prototype.startSession = function (sessionId) {
|
|
4225
4427
|
// userId priority:
|
|
4226
4428
|
// 1. Existing server-assigned userId (from localStorage or previous connected event)
|
|
@@ -35568,7 +35770,7 @@ var ChatWidgetUI = function (props) {
|
|
|
35568
35770
|
var handleBeforeUnload = function () {
|
|
35569
35771
|
log("beforeunload - notifying router of user disconnecting");
|
|
35570
35772
|
if (chatServer.notifyDisconnecting) {
|
|
35571
|
-
chatServer.notifyDisconnecting();
|
|
35773
|
+
chatServer.notifyDisconnecting("close_tab");
|
|
35572
35774
|
}
|
|
35573
35775
|
};
|
|
35574
35776
|
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
@@ -35722,6 +35924,9 @@ var ChatWidgetUI = function (props) {
|
|
|
35722
35924
|
}
|
|
35723
35925
|
/** Called when cancel is clicked */
|
|
35724
35926
|
function handleCancelClick() {
|
|
35927
|
+
if (chatServer === null || chatServer === void 0 ? void 0 : chatServer.notifyDisconnecting) {
|
|
35928
|
+
chatServer.notifyDisconnecting("widget_close");
|
|
35929
|
+
}
|
|
35725
35930
|
// First reset to clear all state
|
|
35726
35931
|
innerDispatch(reset());
|
|
35727
35932
|
// Then set hasInteracted to prevent CTA from showing after cancel
|
|
@@ -36076,6 +36281,20 @@ function resetReducer(state) {
|
|
|
36076
36281
|
function isChatSystemMessage(chat) {
|
|
36077
36282
|
return chat.type === "chat.systemmessage";
|
|
36078
36283
|
}
|
|
36284
|
+
/**
|
|
36285
|
+
* Find the LAST streaming message index. We must search from the end because
|
|
36286
|
+
* orphan isStreaming messages from interrupted streams may exist earlier in the
|
|
36287
|
+
* array. The active streaming message is always the most recently inserted one.
|
|
36288
|
+
*/
|
|
36289
|
+
function findLastStreamingIndex(chats) {
|
|
36290
|
+
for (var i = chats.length - 1; i >= 0; i--) {
|
|
36291
|
+
var chat = chats[i];
|
|
36292
|
+
if (isChatMsgDetail(chat) && chat.isStreaming) {
|
|
36293
|
+
return i;
|
|
36294
|
+
}
|
|
36295
|
+
}
|
|
36296
|
+
return -1;
|
|
36297
|
+
}
|
|
36079
36298
|
// todo: create reducer (requires redux-thunk dependency)
|
|
36080
36299
|
function appendMessageToState(state, msg) {
|
|
36081
36300
|
state.chats = joinMessages(state.chats, msg);
|
|
@@ -36193,8 +36412,19 @@ function update(state, action) {
|
|
|
36193
36412
|
case "chat.typing":
|
|
36194
36413
|
var agent = state.agents[action.detail.user.nick];
|
|
36195
36414
|
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)) });
|
|
36196
|
-
case "chat.stream.start":
|
|
36197
|
-
//
|
|
36415
|
+
case "chat.stream.start": {
|
|
36416
|
+
// Clean up any orphan streaming messages from interrupted streams
|
|
36417
|
+
// before creating the new placeholder.
|
|
36418
|
+
var cleanedChats = newState.chats.map(function (chat) {
|
|
36419
|
+
if (isChatMsgDetail(chat) && chat.isStreaming) {
|
|
36420
|
+
return __assign(__assign({}, chat), { isStreaming: false, wasStreamed: true });
|
|
36421
|
+
}
|
|
36422
|
+
return chat;
|
|
36423
|
+
});
|
|
36424
|
+
// Add placeholder message with streaming state.
|
|
36425
|
+
// Use joinMessages (insertSorted by timestamp) instead of append to
|
|
36426
|
+
// guarantee correct ordering even if events arrive out of order
|
|
36427
|
+
// (e.g., after an SSE abort/retry during tab backgrounding).
|
|
36198
36428
|
var streamingMsg = {
|
|
36199
36429
|
type: "chat.msg",
|
|
36200
36430
|
user: action.detail.user,
|
|
@@ -36204,13 +36434,15 @@ function update(state, action) {
|
|
|
36204
36434
|
},
|
|
36205
36435
|
isStreaming: true,
|
|
36206
36436
|
};
|
|
36207
|
-
|
|
36437
|
+
var newChats = joinMessages(cleanedChats, streamingMsg);
|
|
36438
|
+
return __assign(__assign({}, newState), { chats: newChats, streamingMessageId: action.detail.messageId });
|
|
36439
|
+
}
|
|
36208
36440
|
case "chat.stream.chunk": {
|
|
36209
36441
|
// Find the streaming message by ID and append text
|
|
36210
36442
|
var chunkDetail = action.detail;
|
|
36211
36443
|
if (state.streamingMessageId === chunkDetail.messageId) {
|
|
36212
36444
|
// Find the streaming message by ID (not by index - other messages could be inserted)
|
|
36213
|
-
var streamingIndex_1 = state.chats
|
|
36445
|
+
var streamingIndex_1 = findLastStreamingIndex(state.chats);
|
|
36214
36446
|
if (streamingIndex_1 >= 0) {
|
|
36215
36447
|
var chunkText_1 = chunkDetail.text;
|
|
36216
36448
|
return __assign(__assign({}, state), { lastTimestamp: chunkDetail === null || chunkDetail === void 0 ? void 0 : chunkDetail.timestamp, chats: state.chats.map(function (chat, index) {
|
|
@@ -36230,7 +36462,7 @@ function update(state, action) {
|
|
|
36230
36462
|
var toolStartDetail_1 = action.detail;
|
|
36231
36463
|
if (state.streamingMessageId === toolStartDetail_1.messageId) {
|
|
36232
36464
|
// Find the streaming message by ID (not by index - other messages could be inserted)
|
|
36233
|
-
var streamingIndex_2 = state.chats
|
|
36465
|
+
var streamingIndex_2 = findLastStreamingIndex(state.chats);
|
|
36234
36466
|
if (streamingIndex_2 >= 0) {
|
|
36235
36467
|
return __assign(__assign({}, state), { lastTimestamp: toolStartDetail_1.timestamp, chats: state.chats.map(function (chat, index) {
|
|
36236
36468
|
if (index === streamingIndex_2 && isChatMsgDetail(chat)) {
|
|
@@ -36260,7 +36492,7 @@ function update(state, action) {
|
|
|
36260
36492
|
var toolEndDetail_1 = action.detail;
|
|
36261
36493
|
if (state.streamingMessageId === toolEndDetail_1.messageId) {
|
|
36262
36494
|
// Find the streaming message by ID (not by index - other messages could be inserted)
|
|
36263
|
-
var streamingIndex_3 = state.chats
|
|
36495
|
+
var streamingIndex_3 = findLastStreamingIndex(state.chats);
|
|
36264
36496
|
if (streamingIndex_3 >= 0) {
|
|
36265
36497
|
return __assign(__assign({}, state), { lastTimestamp: toolEndDetail_1.timestamp, chats: state.chats.map(function (chat, index) {
|
|
36266
36498
|
var _a;
|
|
@@ -36289,7 +36521,7 @@ function update(state, action) {
|
|
|
36289
36521
|
var endDetail = action.detail;
|
|
36290
36522
|
if (state.streamingMessageId === endDetail.messageId) {
|
|
36291
36523
|
// Find the streaming message by ID (not by index - other messages could be inserted)
|
|
36292
|
-
var streamingIndex_4 = state.chats
|
|
36524
|
+
var streamingIndex_4 = findLastStreamingIndex(state.chats);
|
|
36293
36525
|
var endStreamingChat = streamingIndex_4 >= 0 ? state.chats[streamingIndex_4] : undefined;
|
|
36294
36526
|
// Verify it's the right message using type guard
|
|
36295
36527
|
if (streamingIndex_4 >= 0 && endStreamingChat && isChatMsgDetail(endStreamingChat)) {
|
|
@@ -36308,6 +36540,51 @@ function update(state, action) {
|
|
|
36308
36540
|
}
|
|
36309
36541
|
return state;
|
|
36310
36542
|
}
|
|
36543
|
+
case "chat.stream.abort": {
|
|
36544
|
+
// Finalize a streaming message interrupted by connection loss during sleep.
|
|
36545
|
+
// Keeps whatever partial text is already in the message.
|
|
36546
|
+
var abortDetail = action.detail;
|
|
36547
|
+
if (state.streamingMessageId === abortDetail.messageId) {
|
|
36548
|
+
var streamingIndex_5 = findLastStreamingIndex(state.chats);
|
|
36549
|
+
if (streamingIndex_5 >= 0) {
|
|
36550
|
+
return __assign(__assign({}, state), { lastTimestamp: abortDetail.timestamp, streamingMessageId: undefined, chats: state.chats.map(function (chat, index) {
|
|
36551
|
+
if (index === streamingIndex_5 && isChatMsgDetail(chat)) {
|
|
36552
|
+
return __assign(__assign({}, chat), { isStreaming: false, wasStreamed: true });
|
|
36553
|
+
}
|
|
36554
|
+
return chat;
|
|
36555
|
+
}) });
|
|
36556
|
+
}
|
|
36557
|
+
}
|
|
36558
|
+
return state;
|
|
36559
|
+
}
|
|
36560
|
+
case "chat.stream.resume": {
|
|
36561
|
+
// Re-enable streaming on the last aborted bot message after replay reconnect.
|
|
36562
|
+
// Targets the most recent wasStreamed message that is NOT currently streaming
|
|
36563
|
+
// (i.e., the one finalized by chat.stream.abort). Updates streamingMessageId
|
|
36564
|
+
// so replayed chunks route to the existing message.
|
|
36565
|
+
// Guard: only resume if no stream is currently active.
|
|
36566
|
+
var resumeDetail = action.detail;
|
|
36567
|
+
if (state.streamingMessageId) {
|
|
36568
|
+
return state;
|
|
36569
|
+
}
|
|
36570
|
+
var lastBotIndex_1 = -1;
|
|
36571
|
+
for (var i = state.chats.length - 1; i >= 0; i--) {
|
|
36572
|
+
var chat = state.chats[i];
|
|
36573
|
+
if (isChatMsgDetail(chat) && chat.wasStreamed && !chat.isStreaming) {
|
|
36574
|
+
lastBotIndex_1 = i;
|
|
36575
|
+
break;
|
|
36576
|
+
}
|
|
36577
|
+
}
|
|
36578
|
+
if (lastBotIndex_1 >= 0) {
|
|
36579
|
+
return __assign(__assign({}, state), { lastTimestamp: resumeDetail.timestamp, streamingMessageId: resumeDetail.messageId, chats: state.chats.map(function (chat, index) {
|
|
36580
|
+
if (index === lastBotIndex_1 && isChatMsgDetail(chat)) {
|
|
36581
|
+
return __assign(__assign({}, chat), { isStreaming: true, wasStreamed: false });
|
|
36582
|
+
}
|
|
36583
|
+
return chat;
|
|
36584
|
+
}) });
|
|
36585
|
+
}
|
|
36586
|
+
return state;
|
|
36587
|
+
}
|
|
36311
36588
|
default:
|
|
36312
36589
|
return state;
|
|
36313
36590
|
}
|