@xapp/chat-widget 1.100.0 → 1.102.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.js CHANGED
@@ -920,6 +920,15 @@ var ActionBarCta = function (_a) {
920
920
  * in `_mixins.scss`. Keep this in step with the `mobile()` mixin. XappMedia/chat-widget#1594.
921
921
  */
922
922
  var MOBILE_MEDIA_QUERY = "(max-width: 640px), (max-height: 480px) and (pointer: coarse)";
923
+ /**
924
+ * Whether the chat panel renders full-screen right now. False where matchMedia is missing
925
+ * (jsdom), matching ActionBar's assume-desktop fallback.
926
+ */
927
+ function isMobileViewport(win) {
928
+ if (win === void 0) { win = window; }
929
+ return typeof win.matchMedia === "function" && win.matchMedia(MOBILE_MEDIA_QUERY).matches;
930
+ }
931
+
923
932
  /**
924
933
  * ChatWidget.scss sizes the chat panel against this variable. Nothing used to set it, so it always
925
934
  * fell back to the hardcoded 80px in the SCSS - and the real mobile bar is taller than that once
@@ -2323,8 +2332,100 @@ function setVisualStatus(status) {
2323
2332
  };
2324
2333
  }
2325
2334
 
2326
- function execute(url, dispatch, visuals, behavior) {
2335
+ /**
2336
+ * Carries "this visitor just clicked a link in the chat" across a same-tab navigation.
2337
+ *
2338
+ * Persisted widget state already survives the page load, but it cannot tell a chat link
2339
+ * click from a visitor coming back to the site later. The param can; it is read on arrival
2340
+ * and stripped from the address bar so it is never bookmarked or shared.
2341
+ */
2342
+ var CHAT_CONTINUE_PARAM = "xa_chat";
2343
+ var CHAT_CONTINUE_VALUE = "resume";
2344
+ var CONTINUE_CTA_MESSAGE = "Continue your conversation";
2345
+ /**
2346
+ * Marks a link the widget is about to follow in this tab. Only same-origin links: widget
2347
+ * state lives in this origin's localStorage, so the widget on any other origin - a
2348
+ * subdomain included - has no conversation to continue.
2349
+ */
2350
+ function withChatContinueParam(url, currentOrigin) {
2351
+ try {
2352
+ var urlObj = new URL(url);
2353
+ if (urlObj.origin !== currentOrigin || urlObj.searchParams.get(CHAT_CONTINUE_PARAM) === CHAT_CONTINUE_VALUE) {
2354
+ return url;
2355
+ }
2356
+ urlObj.searchParams.set(CHAT_CONTINUE_PARAM, CHAT_CONTINUE_VALUE);
2357
+ return urlObj.toString();
2358
+ }
2359
+ catch (_) {
2360
+ return url;
2361
+ }
2362
+ }
2363
+ /**
2364
+ * True when this page was reached through a chat link. Pure, so it is safe inside a
2365
+ * render-time initializer that React may call twice (StrictMode); strip the param
2366
+ * separately with `stripChatContinueParam`, from an effect.
2367
+ */
2368
+ function hasChatContinueParam(win) {
2369
+ if (win === void 0) { win = window; }
2370
+ try {
2371
+ return new URL(win.location.href).searchParams.get(CHAT_CONTINUE_PARAM) === CHAT_CONTINUE_VALUE;
2372
+ }
2373
+ catch (_) {
2374
+ return false;
2375
+ }
2376
+ }
2377
+ /**
2378
+ * Removes the param from the address bar with replaceState, keeping the rest of the URL
2379
+ * and any SPA history state. A no-op when it is absent, so repeating it is harmless.
2380
+ */
2381
+ function stripChatContinueParam(win) {
2382
+ if (win === void 0) { win = window; }
2383
+ try {
2384
+ var url = new URL(win.location.href);
2385
+ if (!url.searchParams.has(CHAT_CONTINUE_PARAM)) {
2386
+ return;
2387
+ }
2388
+ url.searchParams.delete(CHAT_CONTINUE_PARAM);
2389
+ win.history.replaceState(win.history.state, "", url.toString());
2390
+ }
2391
+ catch (_) {
2392
+ // An address bar we cannot rewrite only leaves the param visible.
2393
+ }
2394
+ }
2395
+ /**
2396
+ * A conversation worth continuing: any message on screen. Join notices do not count - the
2397
+ * MCP connection posts one before the greeting, so a brand-new session has one too.
2398
+ */
2399
+ function hasConversation(chats) {
2400
+ return !!(chats === null || chats === void 0 ? void 0 : chats.some(function (chat) { return chat.type === "chat.msg"; }));
2401
+ }
2402
+ /**
2403
+ * The CTA to show, given whether there is a conversation to go back to.
2404
+ *
2405
+ * With one, the first-visit copy ("Need Help? Click Here") becomes a prompt to continue,
2406
+ * keeping the tenant's timing and styling. A tenant with no CTA gets none on an ordinary
2407
+ * page load; right after a chat link it does, immediately - on mobile that prompt is the
2408
+ * only sign the conversation is still there, because the panel stays closed over the page
2409
+ * they asked for.
2410
+ */
2411
+ function resolveCta(cta, options) {
2412
+ var arrivedViaChatLink = options.arrivedViaChatLink;
2413
+ if (!options.hasConversation || (!(cta === null || cta === void 0 ? void 0 : cta.message) && !arrivedViaChatLink)) {
2414
+ return cta;
2415
+ }
2416
+ var immediately = arrivedViaChatLink ? { delay: 0 } : {};
2417
+ var mobile = cta === null || cta === void 0 ? void 0 : cta.mobile;
2418
+ return __assign$1(__assign$1(__assign$1(__assign$1({}, cta), { message: CONTINUE_CTA_MESSAGE }), immediately), (mobile && {
2419
+ mobile: __assign$1(__assign$1(__assign$1({}, mobile), ((typeof mobile.message === "string" || arrivedViaChatLink) && { message: CONTINUE_CTA_MESSAGE })), immediately),
2420
+ }));
2421
+ }
2422
+
2423
+ /**
2424
+ * @internal Exported for unit testing; use `useOpenUrlCallback`.
2425
+ */
2426
+ function executeOpenUrl(url, dispatch, visuals, behavior, isMobile) {
2327
2427
  var _a, _b;
2428
+ if (isMobile === void 0) { isMobile = isMobileViewport(); }
2328
2429
  if (!dispatch || !visuals) {
2329
2430
  throw new Error("OpenUrl: No dispatch and/or visual.");
2330
2431
  }
@@ -2391,12 +2492,23 @@ function execute(url, dispatch, visuals, behavior) {
2391
2492
  window.open(url, "_blank");
2392
2493
  break;
2393
2494
  case "sameWindow":
2394
- if (!visuals.opened) {
2495
+ // Desktop: the next page reopens the chat beside the content, as before.
2496
+ // Mobile: the panel is full-screen, so reopening it would cover the page they
2497
+ // just asked for. Leave it closed; the param lets that page offer to continue.
2498
+ // Unconditional: callers can hand in a `visuals` captured before the chat opened
2499
+ // (ChatMarkdownMessage's click handler does), and skipping on a stale `opened`
2500
+ // leaves the persisted state open.
2501
+ if (isMobile) {
2502
+ dispatch(setVisualStatus({
2503
+ opened: false
2504
+ }));
2505
+ }
2506
+ else if (!visuals.opened) {
2395
2507
  dispatch(setVisualStatus({
2396
2508
  opened: true
2397
2509
  }));
2398
2510
  }
2399
- window.open(url, "_self");
2511
+ window.open(withChatContinueParam(url, window.location.origin), "_self");
2400
2512
  break;
2401
2513
  default:
2402
2514
  throwBadKind$1(type);
@@ -2408,7 +2520,7 @@ function useOpenUrlCallback() {
2408
2520
  if (!behavior) {
2409
2521
  behavior = (env === null || env === void 0 ? void 0 : env.urls) ? resolveUrlPolicy(url, env.urls) : defaultBehavior;
2410
2522
  }
2411
- execute(url, dispatch, visuals, behavior);
2523
+ executeOpenUrl(url, dispatch, visuals, behavior);
2412
2524
  }, [env]);
2413
2525
  }
2414
2526
 
@@ -4301,6 +4413,10 @@ var MCPChat = /** @class */ (function () {
4301
4413
  this.isInitializingSession = false;
4302
4414
  // Session validation state (silent handshake - no stream dispatch)
4303
4415
  this.isValidatingSession = false;
4416
+ // A conversation is on screen that the server should keep: restored from a previous page,
4417
+ // or confirmed by the server on this one. Sent as `continueConversation`, which also lets
4418
+ // the server resume a session idle longer than its default window.
4419
+ this.conversationOnScreen = false;
4304
4420
  // Tool call tracking for debug mode
4305
4421
  this.currentToolCalls = [];
4306
4422
  this.isReplaying = false;
@@ -4388,8 +4504,12 @@ var MCPChat = /** @class */ (function () {
4388
4504
  _a.label = 1;
4389
4505
  case 1:
4390
4506
  _a.trys.push([1, 3, 4, 5]);
4391
- return [4 /*yield*/, this.performSSERequest(this.buildRequestPayload({ resumeSession: true }))];
4507
+ // With a conversation on screen (e.g. after a same-tab link click), ask the
4508
+ // server to continue it: without the flag it clears the history and greets.
4509
+ return [4 /*yield*/, this.performSSERequest(this.buildRequestPayload(__assign$1({ resumeSession: true }, (this.conversationOnScreen && { continueConversation: true }))))];
4392
4510
  case 2:
4511
+ // With a conversation on screen (e.g. after a same-tab link click), ask the
4512
+ // server to continue it: without the flag it clears the history and greets.
4393
4513
  _a.sent();
4394
4514
  log("Session validation complete");
4395
4515
  return [3 /*break*/, 5];
@@ -4644,8 +4764,8 @@ var MCPChat = /** @class */ (function () {
4644
4764
  // Generic fallback
4645
4765
  return "Something went wrong. Please try again.";
4646
4766
  };
4647
- MCPChat.prototype.dispatchStreamStart = function (messageId) {
4648
- var bot = this.getBot(undefined);
4767
+ MCPChat.prototype.dispatchStreamStart = function (messageId, user) {
4768
+ var bot = this.getBot(user);
4649
4769
  var streamTimestamp = +new Date();
4650
4770
  log("[dispatchStreamStart] Stream started: ".concat(messageId, ", timestamp: ").concat(streamTimestamp));
4651
4771
  this.dispatch({
@@ -4658,30 +4778,25 @@ var MCPChat = /** @class */ (function () {
4658
4778
  },
4659
4779
  });
4660
4780
  };
4661
- MCPChat.prototype.dispatchStreamChunk = function (messageId, text) {
4781
+ MCPChat.prototype.dispatchStreamChunk = function (messageId, text, user) {
4662
4782
  this.dispatch({
4663
4783
  type: "chat",
4664
4784
  detail: {
4665
4785
  type: "chat.stream.chunk",
4666
- user: this.getBot(undefined),
4786
+ user: this.getBot(user),
4667
4787
  messageId: messageId,
4668
4788
  text: text,
4669
4789
  timestamp: +new Date(),
4670
4790
  },
4671
4791
  });
4672
4792
  };
4673
- MCPChat.prototype.dispatchStreamEnd = function (messageId, fullMessage, toolCalls) {
4793
+ MCPChat.prototype.dispatchStreamEnd = function (messageId, fullMessage, options) {
4794
+ if (options === void 0) { options = {}; }
4795
+ var toolCalls = options.toolCalls, user = options.user, preserveChips = options.preserveChips;
4674
4796
  log("Stream ended: ".concat(messageId));
4675
4797
  this.dispatch({
4676
4798
  type: "chat",
4677
- detail: {
4678
- type: "chat.stream.end",
4679
- user: this.getBot(undefined),
4680
- messageId: messageId,
4681
- msg: fullMessage,
4682
- toolCalls: toolCalls,
4683
- timestamp: +new Date(),
4684
- },
4799
+ detail: __assign$1(__assign$1({ type: "chat.stream.end", user: this.getBot(user), messageId: messageId, msg: fullMessage, toolCalls: toolCalls }, (preserveChips && { preserveChips: preserveChips })), { timestamp: +new Date() }),
4685
4800
  });
4686
4801
  };
4687
4802
  MCPChat.prototype.dispatchStreamAbort = function (messageId) {
@@ -4791,6 +4906,19 @@ var MCPChat = /** @class */ (function () {
4791
4906
  MCPChat.prototype.bufferStreamText = function (text) {
4792
4907
  this.streamBuffer += text;
4793
4908
  };
4909
+ /**
4910
+ * Dispatches whatever is buffered right now, without waiting on the throttle timer.
4911
+ *
4912
+ * Used when a bubble has to be closed mid-turn (`message_start`): the split has to be
4913
+ * synchronous, because frames arrive in order on one `onmessage` callback and deferring it
4914
+ * to a microtask would let the next `text` frame land in the bubble we are closing.
4915
+ */
4916
+ MCPChat.prototype.flushStreamBufferSync = function () {
4917
+ if (this.streamBuffer.length > 0 && this.streamingMessageIdForThrottle) {
4918
+ this.dispatchStreamChunk(this.streamingMessageIdForThrottle, this.streamBuffer);
4919
+ this.streamBuffer = "";
4920
+ }
4921
+ };
4794
4922
  /**
4795
4923
  * Flushes any remaining buffered text and stops the throttle timer.
4796
4924
  * Returns a promise that resolves when all buffered text has been dispatched.
@@ -5061,7 +5189,7 @@ var MCPChat = /** @class */ (function () {
5061
5189
  return [2 /*return*/];
5062
5190
  }
5063
5191
  log("[attemptSSERequest] Sending message: \"".concat(messageText, "\", userId: ").concat(this._userId, ", sessionId: ").concat(this._sessionId));
5064
- return [2 /*return*/, this.performSSERequest(this.buildRequestPayload({ message: messageText }))];
5192
+ return [2 /*return*/, this.performSSERequest(this.buildRequestPayload(__assign$1({ message: messageText }, (this.conversationOnScreen && { continueConversation: true }))))];
5065
5193
  });
5066
5194
  });
5067
5195
  };
@@ -5071,7 +5199,7 @@ var MCPChat = /** @class */ (function () {
5071
5199
  */
5072
5200
  MCPChat.prototype.performSSERequest = function (body) {
5073
5201
  return __awaiter$1(this, void 0, void 0, function () {
5074
- var endpoint, messageId, textChunks, hasError, httpStatusCode, sseError;
5202
+ var endpoint, messageId, bubbleId, bubbleUser, bubbleChunks, hasError, httpStatusCode, sseError;
5075
5203
  var _this = this;
5076
5204
  return __generator$1(this, function (_a) {
5077
5205
  endpoint = this.getEndpoint();
@@ -5084,7 +5212,8 @@ var MCPChat = /** @class */ (function () {
5084
5212
  this.abortController = new AbortController();
5085
5213
  messageId = uuid_1();
5086
5214
  this.currentMessageId = messageId;
5087
- textChunks = [];
5215
+ bubbleId = messageId;
5216
+ bubbleChunks = [];
5088
5217
  hasError = false;
5089
5218
  return [2 /*return*/, new Promise(function (resolve, reject) {
5090
5219
  // WARNING: Do not log headers object - it contains sensitive auth tokens
@@ -5115,6 +5244,7 @@ var MCPChat = /** @class */ (function () {
5115
5244
  });
5116
5245
  }); },
5117
5246
  onmessage: function (event) {
5247
+ var _a;
5118
5248
  // Guard: ignore events from stale/aborted requests
5119
5249
  if (_this.currentMessageId !== messageId) {
5120
5250
  log("[SSE] Ignoring stale event for messageId: ".concat(messageId, ", current: ").concat(_this.currentMessageId));
@@ -5141,10 +5271,15 @@ var MCPChat = /** @class */ (function () {
5141
5271
  case "connected":
5142
5272
  // Connection established
5143
5273
  log("[SSE] Connected event received, messageId: ".concat(messageId, ", isReturningUser: ").concat(data.isReturningUser, ", isReplay: ").concat(data.isReplay));
5274
+ _this.conversationOnScreen = true;
5144
5275
  // Track if this is a replay connection
5145
5276
  if (data.isReplay) {
5146
5277
  _this.isReplaying = true;
5147
5278
  }
5279
+ // A server may name the participant up front.
5280
+ if (data.user) {
5281
+ bubbleUser = data.user;
5282
+ }
5148
5283
  // Store identity from server response
5149
5284
  if (data.userId || data.sessionId) {
5150
5285
  if (data.userId) {
@@ -5164,18 +5299,35 @@ var MCPChat = /** @class */ (function () {
5164
5299
  if (data.isReplay) {
5165
5300
  // Replay connection — resume into the existing partial
5166
5301
  // message instead of creating a duplicate placeholder
5167
- _this.dispatchStreamResume(messageId);
5302
+ _this.dispatchStreamResume(bubbleId);
5168
5303
  }
5169
5304
  else {
5170
5305
  // Normal connection — create new streaming placeholder
5171
- _this.dispatchStreamStart(messageId);
5306
+ _this.dispatchStreamStart(bubbleId, bubbleUser);
5307
+ }
5308
+ _this.startStreamThrottle(bubbleId);
5309
+ break;
5310
+ case "session_resumed":
5311
+ // The server continued the conversation: no greeting follows,
5312
+ // so adopt the identity without opening a stream bubble.
5313
+ log("[SSE] Session resumed, sessionId: ".concat(data.sessionId, ", hasHistory: ").concat(data.hasHistory));
5314
+ _this.conversationOnScreen = true;
5315
+ if (data.userId) {
5316
+ _this._userId = data.userId;
5317
+ _this.userIdIsServerAssigned = true;
5318
+ }
5319
+ if (data.sessionId) {
5320
+ _this._sessionId = data.sessionId;
5321
+ _this.dispatch(setSessionId(_this._sessionId));
5172
5322
  }
5173
- _this.startStreamThrottle(messageId);
5323
+ storeIdentity(data.userId, data.sessionId);
5324
+ _this.stopTyping();
5174
5325
  break;
5175
5326
  case "session_expired":
5176
5327
  // Session no longer exists on server - need to start fresh
5177
5328
  log("[SSE] Session expired: ".concat(data.reason || "Session not found"));
5178
5329
  // Clear stored session (and lastSeq) but keep userId for returning user identification
5330
+ _this.conversationOnScreen = false;
5179
5331
  clearStoredSession(_this._sessionId);
5180
5332
  _this._sessionId = "";
5181
5333
  _this.lastSeq = undefined;
@@ -5188,14 +5340,40 @@ var MCPChat = /** @class */ (function () {
5188
5340
  _this.initializeNewSession();
5189
5341
  resolve();
5190
5342
  break;
5343
+ case "message_start": {
5344
+ // Close the bubble being streamed and open a new one, optionally
5345
+ // under a different participant. This is the only way a turn can
5346
+ // render as more than one message: the reducer creates a fresh
5347
+ // bubble per `chat.stream.start` and finalizes any still-streaming
5348
+ // one, so a server can make a handoff look like someone joining.
5349
+ // Servers that never send this are unaffected.
5350
+ log("[SSE] message_start \u2014 splitting bubble, user: ".concat((_a = data.user) === null || _a === void 0 ? void 0 : _a.displayName));
5351
+ _this.flushStreamBufferSync();
5352
+ _this.dispatchStreamEnd(bubbleId, { text: bubbleChunks.join("") }, {
5353
+ // Same debug-mode capture as `done`, scoped to this bubble:
5354
+ // stream.end replaces a bubble's toolCalls wholesale, so any
5355
+ // left in currentToolCalls would reappear on the next one.
5356
+ toolCalls: (_this.config.debugMode || _this._debugMode) ? __spreadArray$1([], _this.currentToolCalls, true) : undefined,
5357
+ user: bubbleUser,
5358
+ // Chips belong to the turn; its final end sets them.
5359
+ preserveChips: true,
5360
+ });
5361
+ _this.currentToolCalls = [];
5362
+ bubbleId = uuid_1();
5363
+ bubbleChunks = [];
5364
+ bubbleUser = data.user;
5365
+ _this.dispatchStreamStart(bubbleId, bubbleUser);
5366
+ _this.startStreamThrottle(bubbleId);
5367
+ break;
5368
+ }
5191
5369
  case "text":
5192
5370
  if (data.text) {
5193
- log("[SSE] Text chunk received (".concat(data.text.length, " chars), total chunks: ").concat(textChunks.length + 1, ", isReplay: ").concat(data.isReplay));
5194
- textChunks.push(data.text);
5371
+ log("[SSE] Text chunk received (".concat(data.text.length, " chars), chunks in this bubble: ").concat(bubbleChunks.length + 1, ", isReplay: ").concat(data.isReplay));
5372
+ bubbleChunks.push(data.text);
5195
5373
  if (data.isReplay || _this.isSleeping) {
5196
5374
  // Replayed text or tab hidden — dispatch immediately
5197
5375
  // (no throttle timer running in either case)
5198
- _this.dispatchStreamChunk(messageId, data.text);
5376
+ _this.dispatchStreamChunk(bubbleId, data.text, bubbleUser);
5199
5377
  }
5200
5378
  else {
5201
5379
  // Live text — buffer for throttled dispatch
@@ -5219,7 +5397,7 @@ var MCPChat = /** @class */ (function () {
5219
5397
  _this.currentToolCalls.push(toolCall);
5220
5398
  // Always dispatch tool start for user-friendly progress display
5221
5399
  _this.dispatchToolStart({
5222
- messageId: messageId,
5400
+ messageId: bubbleId,
5223
5401
  toolName: data.toolName,
5224
5402
  toolCallId: data.toolCallId,
5225
5403
  displayName: data.displayName,
@@ -5260,7 +5438,7 @@ var MCPChat = /** @class */ (function () {
5260
5438
  }
5261
5439
  // Always dispatch tool end for user-friendly progress display
5262
5440
  _this.dispatchToolEnd({
5263
- messageId: messageId,
5441
+ messageId: bubbleId,
5264
5442
  toolName: data.toolName,
5265
5443
  toolCallId: data.toolCallId,
5266
5444
  displayName: data.displayName,
@@ -5282,8 +5460,10 @@ var MCPChat = /** @class */ (function () {
5282
5460
  storeIdentity(undefined, data.response.sessionId);
5283
5461
  }
5284
5462
  // Wait for buffer to flush, then finalize
5285
- var accumulatedText = textChunks.join("");
5286
- log("[SSE] Done event received. Total chunks: ".concat(textChunks.length, ", accumulated text length: ").concat(accumulatedText.length));
5463
+ // The bubble's own text, not the whole turn's — a turn split by
5464
+ // `message_start` has already committed its earlier bubbles.
5465
+ var accumulatedText = bubbleChunks.join("");
5466
+ log("[SSE] Done event received. Chunks in this bubble: ".concat(bubbleChunks.length, ", accumulated text length: ").concat(accumulatedText.length));
5287
5467
  log("[SSE] First 100 chars of accumulated: ".concat(accumulatedText.substring(0, 100)));
5288
5468
  // Map suggestions to options (chips) format
5289
5469
  var suggestions = data.response.suggestions;
@@ -5299,8 +5479,14 @@ var MCPChat = /** @class */ (function () {
5299
5479
  if (sources === null || sources === void 0 ? void 0 : sources.length) {
5300
5480
  log("[SSE] Sources: ".concat(sources.length, " items"));
5301
5481
  }
5482
+ // `response.response` is request-scoped. For an unsplit turn it is
5483
+ // the whole answer, and the fallback serves servers that deliver it
5484
+ // only via `done`. Once `message_start` has fired it would copy every
5485
+ // earlier bubble's text into this one, so a split turn's bubbles
5486
+ // must stream their text as `text` frames.
5487
+ var isSplitTurn = bubbleId !== messageId;
5302
5488
  var finalMessage_1 = {
5303
- text: accumulatedText || data.response.response || "",
5489
+ text: accumulatedText || (isSplitTurn ? "" : data.response.response) || "",
5304
5490
  options: options,
5305
5491
  sources: sources,
5306
5492
  displays: data.response.displays,
@@ -5308,7 +5494,7 @@ var MCPChat = /** @class */ (function () {
5308
5494
  // Capture tool calls before flushing (debug mode — config or runtime toggle)
5309
5495
  var toolCalls_1 = (_this.config.debugMode || _this._debugMode) ? __spreadArray$1([], _this.currentToolCalls, true) : undefined;
5310
5496
  _this.flushStreamBuffer().then(function () {
5311
- _this.dispatchStreamEnd(messageId, finalMessage_1, toolCalls_1);
5497
+ _this.dispatchStreamEnd(bubbleId, finalMessage_1, { toolCalls: toolCalls_1, user: bubbleUser });
5312
5498
  _this.currentToolCalls = []; // Clear after dispatch
5313
5499
  resolve();
5314
5500
  });
@@ -5348,7 +5534,7 @@ var MCPChat = /** @class */ (function () {
5348
5534
  log("[MCPChat] SSE error during sleep - will request replay on wakeup");
5349
5535
  _this.needsReplayOnWakeup = true;
5350
5536
  if (_this.streamingMessageIdForThrottle) {
5351
- _this.dispatchStreamAbort(messageId);
5537
+ _this.dispatchStreamAbort(bubbleId);
5352
5538
  }
5353
5539
  _this.stopStreamThrottle();
5354
5540
  }
@@ -5392,6 +5578,7 @@ var MCPChat = /** @class */ (function () {
5392
5578
  MCPChat.prototype.setVisitorInfo = function (visitorInfo, sessionId, cb) {
5393
5579
  this.visitorInfo = visitorInfo;
5394
5580
  this._attributes = __assign$1({}, this.visitorInfo.attributes);
5581
+ this.conversationOnScreen = visitorInfo.hasConversation === true;
5395
5582
  // do not set currentUrl if localhost
5396
5583
  var href = new URL(window.location.href);
5397
5584
  if (href === null || href === void 0 ? void 0 : href.host.toLowerCase().startsWith("localhost")) {
@@ -12906,7 +13093,21 @@ var ChatTextMessage = function (props) {
12906
13093
  var wasStreamed = message.wasStreamed;
12907
13094
  var showTail = agentMessage && !props.sibling && !isStreaming && !wasStreamed;
12908
13095
  var hasToolCalls = props.toolCalls && props.toolCalls.length > 0;
12909
- return (require$$0.jsxs("div", { className: "chat-msg".concat(isStreaming ? " chat-msg--streaming" : ""), children: [hasToolCalls && (require$$0.jsx(ToolCallDisplay, { toolCalls: props.toolCalls, debugMode: props.debugMode })), require$$0.jsx("div", { className: "chat-text-container", children: require$$0.jsxs(ChatMessageBubble, { owner: agentMessage ? "others" : "mine", hasTail: showTail, children: [require$$0.jsx("span", { className: "message-sr-only", children: "at " + time + (agentMessage ? (isStreaming ? " the bot is typing" : " the bot said") : " the user said") }), parsedHtml ? (require$$0.jsx("span", { className: "chat-msg__content", dangerouslySetInnerHTML: { __html: parsedHtml } })) : (require$$0.jsx("span", { className: "chat-msg__content", children: message.msg.text })), isStreaming && require$$0.jsx("span", { className: "streaming-cursor", "aria-hidden": "true" })] }) })] }));
13096
+ // Delegated, because the links are inside dangerouslySetInnerHTML. Modified clicks stay
13097
+ // with the browser so ctrl/cmd-click still opens a background tab.
13098
+ var onLinkClick = props.onLinkClick;
13099
+ var handleContentClick = function (ev) {
13100
+ var _a, _b;
13101
+ if (!onLinkClick || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) {
13102
+ return;
13103
+ }
13104
+ var link = (_b = (_a = ev.target).closest) === null || _b === void 0 ? void 0 : _b.call(_a, "a");
13105
+ if (link && ev.currentTarget.contains(link)) {
13106
+ ev.preventDefault();
13107
+ onLinkClick(link.href);
13108
+ }
13109
+ };
13110
+ return (require$$0.jsxs("div", { className: "chat-msg".concat(isStreaming ? " chat-msg--streaming" : ""), children: [hasToolCalls && (require$$0.jsx(ToolCallDisplay, { toolCalls: props.toolCalls, debugMode: props.debugMode })), require$$0.jsx("div", { className: "chat-text-container", children: require$$0.jsxs(ChatMessageBubble, { owner: agentMessage ? "others" : "mine", hasTail: showTail, children: [require$$0.jsx("span", { className: "message-sr-only", children: "at " + time + (agentMessage ? (isStreaming ? " the bot is typing" : " the bot said") : " the user said") }), parsedHtml ? (require$$0.jsx("span", { className: "chat-msg__content", onClick: handleContentClick, dangerouslySetInnerHTML: { __html: parsedHtml } })) : (require$$0.jsx("span", { className: "chat-msg__content", children: message.msg.text })), isStreaming && require$$0.jsx("span", { className: "streaming-cursor", "aria-hidden": "true" })] }) })] }));
12910
13111
  };
12911
13112
 
12912
13113
  var ChatScheduleWidget = function (props) {
@@ -12949,14 +13150,21 @@ var avaKeys = ["text", "html", "card", "list"];
12949
13150
  * @returns
12950
13151
  */
12951
13152
  var ChatMessage = function (props) {
12952
- var _a, _b, _c;
13153
+ var _a, _b, _c, _d;
12953
13154
  var middleware = props.messageMiddleware || StandardMiddlewares;
12954
13155
  var chatConfig = React.useContext(ChatConfigContext);
12955
13156
  // console.log(`########### chatConfig: ${JSON.stringify(chatConfig, null, 2)}`);
12956
13157
  var ctx = props.middlewareContext;
12957
13158
  var user = props.agent;
13159
+ var chatDispatch = useChatDispatch();
13160
+ var visuals = reactRedux.useSelector(function (state) { return state.visuals; });
13161
+ // Text links follow the tenant's URL policy when it set one (utm_source, same-tab
13162
+ // continuation, ...). Tenants without one keep the plain new-tab anchor.
13163
+ var handleTextLinkClick = ((_a = chatConfig === null || chatConfig === void 0 ? void 0 : chatConfig.env) === null || _a === void 0 ? void 0 : _a.urls) && (ctx === null || ctx === void 0 ? void 0 : ctx.openUrl)
13164
+ ? function (href) { return ctx.openUrl(href, chatDispatch, visuals); }
13165
+ : undefined;
12958
13166
  // Get hideUserInfo from the agent info based on the message sender's nick
12959
- var agentInfo = (_a = props.agents) === null || _a === void 0 ? void 0 : _a[props.message.user.nick];
13167
+ var agentInfo = (_b = props.agents) === null || _b === void 0 ? void 0 : _b[props.message.user.nick];
12960
13168
  var hideUserInfo = (agentInfo === null || agentInfo === void 0 ? void 0 : agentInfo.hideUserInfo) || false;
12961
13169
  function renderByType() {
12962
13170
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
@@ -12975,7 +13183,7 @@ var ChatMessage = function (props) {
12975
13183
  var debugMode = (_d = (_c = (_b = (_a = chatConfig === null || chatConfig === void 0 ? void 0 : chatConfig.env) === null || _a === void 0 ? void 0 : _a.connection) === null || _b === void 0 ? void 0 : _b.mcp) === null || _c === void 0 ? void 0 : _c.debugMode) !== null && _d !== void 0 ? _d : false;
12976
13184
  // Show tool progress with avatar when no text yet, or above text when text exists
12977
13185
  var showToolProgressWithAvatar = hasToolCalls && !msg.text;
12978
- return (require$$0.jsxs(require$$0.Fragment, { children: [showToolProgressWithAvatar && (require$$0.jsx(ChatMessagePart, { showAvatar: true, user: user, avatarPosition: (_g = (_f = (_e = chatConfig === null || chatConfig === void 0 ? void 0 : chatConfig.env) === null || _e === void 0 ? void 0 : _e.theme) === null || _f === void 0 ? void 0 : _f.messages) === null || _g === void 0 ? void 0 : _g.avatarPosition, hideUserInfo: hideUserInfo, isStreaming: true, children: require$$0.jsx("div", { className: "chat-msg", children: require$$0.jsx(ToolCallDisplay, { toolCalls: toolCalls, debugMode: debugMode }) }) })), msg.text && (require$$0.jsx(ChatMessagePart, { showAvatar: avaKey === "text", user: user, avatarPosition: (_k = (_j = (_h = chatConfig === null || chatConfig === void 0 ? void 0 : chatConfig.env) === null || _h === void 0 ? void 0 : _h.theme) === null || _j === void 0 ? void 0 : _j.messages) === null || _k === void 0 ? void 0 : _k.avatarPosition, hideUserInfo: hideUserInfo, isStreaming: isStreaming_1 || wasStreamed, children: require$$0.jsx(ChatTextMessage, { message: props.message, sibling: props.sibling, user: user, toolCalls: hasToolCalls ? toolCalls : undefined, debugMode: debugMode }) })), msg.html &&
13186
+ return (require$$0.jsxs(require$$0.Fragment, { children: [showToolProgressWithAvatar && (require$$0.jsx(ChatMessagePart, { showAvatar: true, user: user, avatarPosition: (_g = (_f = (_e = chatConfig === null || chatConfig === void 0 ? void 0 : chatConfig.env) === null || _e === void 0 ? void 0 : _e.theme) === null || _f === void 0 ? void 0 : _f.messages) === null || _g === void 0 ? void 0 : _g.avatarPosition, hideUserInfo: hideUserInfo, isStreaming: true, children: require$$0.jsx("div", { className: "chat-msg", children: require$$0.jsx(ToolCallDisplay, { toolCalls: toolCalls, debugMode: debugMode }) }) })), msg.text && (require$$0.jsx(ChatMessagePart, { showAvatar: avaKey === "text", user: user, avatarPosition: (_k = (_j = (_h = chatConfig === null || chatConfig === void 0 ? void 0 : chatConfig.env) === null || _h === void 0 ? void 0 : _h.theme) === null || _j === void 0 ? void 0 : _j.messages) === null || _k === void 0 ? void 0 : _k.avatarPosition, hideUserInfo: hideUserInfo, isStreaming: isStreaming_1 || wasStreamed, children: require$$0.jsx(ChatTextMessage, { message: props.message, sibling: props.sibling, user: user, toolCalls: hasToolCalls ? toolCalls : undefined, debugMode: debugMode, onLinkClick: handleTextLinkClick }) })), msg.html &&
12979
13187
  require$$0.jsx(ChatMessagePart, { showAvatar: avaKey === "html", user: user, avatarPosition: (_o = (_m = (_l = chatConfig === null || chatConfig === void 0 ? void 0 : chatConfig.env) === null || _l === void 0 ? void 0 : _l.theme) === null || _m === void 0 ? void 0 : _m.messages) === null || _o === void 0 ? void 0 : _o.avatarPosition, hideUserInfo: hideUserInfo, children: require$$0.jsx(ChatMarkdownMessage, { message: props.message, sibling: props.sibling, onOpenUrl: (_p = props.middlewareContext) === null || _p === void 0 ? void 0 : _p.openUrl }) }), msg.displays && middleware && msg.displays.map(function (display, index) {
12980
13188
  if (display.type === "ScheduleButton") {
12981
13189
  return (require$$0.jsx(ChatScheduleWidget, { minimizeOnClick: props.minimizeOnClick, display: display }, index));
@@ -12999,7 +13207,7 @@ var ChatMessage = function (props) {
12999
13207
  }
13000
13208
  // Don't show timestamp/sources while streaming or if message has no content yet
13001
13209
  var isStreaming = props.message.isStreaming;
13002
- var hasContent = !!(((_b = props.message.msg) === null || _b === void 0 ? void 0 : _b.text) || ((_c = props.message.msg) === null || _c === void 0 ? void 0 : _c.html));
13210
+ var hasContent = !!(((_c = props.message.msg) === null || _c === void 0 ? void 0 : _c.text) || ((_d = props.message.msg) === null || _d === void 0 ? void 0 : _d.html));
13003
13211
  var showFooter = !isStreaming && hasContent;
13004
13212
  // empty
13005
13213
  return (require$$0.jsx("div", { className: "chat-msg-container-wrapper ".concat(isAgent(props.message.user.nick) ? "agent" : "visitor", " ").concat(props.sibling ? "sibling" : ""), children: require$$0.jsxs("div", { className: "chat-msg-container ".concat(getClassName(props.message)), children: [renderByType(), showFooter && renderTimestampAndSources()] }) }));
@@ -13009,9 +13217,6 @@ var ChatMessage = function (props) {
13009
13217
  * value must be like "60m", "2h" etc.
13010
13218
  */
13011
13219
  function convertToSeconds(s) {
13012
- if (!s) {
13013
- return 0;
13014
- }
13015
13220
  var groups = s.match(/(\d+)([dhms]?)/);
13016
13221
  if (!groups) {
13017
13222
  err("Invalid time string: ".concat(s));
@@ -13034,15 +13239,50 @@ function convertToSeconds(s) {
13034
13239
  }
13035
13240
  }
13036
13241
 
13037
- function checkSessionExpiration(duration, lastMessageTimestamp, _lastTimestamp) {
13038
- var durationMs = convertToSeconds(duration) * 1000;
13039
- // never if no session duration in config
13040
- if (!duration) {
13242
+ /**
13243
+ * How long a conversation must sit idle, on top of the calendar day changing, before it
13244
+ * expires when the widget config does not say.
13245
+ */
13246
+ var DEFAULT_SESSION_EXPIRATION = "4h";
13247
+ function isSameLocalDay(a, b) {
13248
+ var dayA = new Date(a);
13249
+ var dayB = new Date(b);
13250
+ return dayA.getFullYear() === dayB.getFullYear()
13251
+ && dayA.getMonth() === dayB.getMonth()
13252
+ && dayA.getDate() === dayB.getDate();
13253
+ }
13254
+ /**
13255
+ * A conversation expires only when BOTH the visitor's local calendar day has changed since
13256
+ * the last message AND at least `duration` has passed. So a visitor who browses the site
13257
+ * over an afternoon keeps one conversation, and one chatting at 11pm is not greeted afresh
13258
+ * at 1am - but tomorrow's visit starts clean.
13259
+ */
13260
+ function checkSessionExpiration(duration, lastMessageTimestamp, _lastTimestamp, now) {
13261
+ if (now === void 0) { now = Date.now(); }
13262
+ if (!lastMessageTimestamp) {
13041
13263
  return false;
13042
13264
  }
13043
- // TODO: Why? Let's use the time passed since the last message timestamp
13044
- // return lastTimestamp - lastMessageTimestamp > durationMs;
13045
- return +new Date() - lastMessageTimestamp > durationMs;
13265
+ var durationMs = convertToSeconds(duration || DEFAULT_SESSION_EXPIRATION) * 1000;
13266
+ return now - lastMessageTimestamp >= durationMs && !isSameLocalDay(lastMessageTimestamp, now);
13267
+ }
13268
+
13269
+ /**
13270
+ * Whether this page was reached through a same-tab link in the chat, read once on mount.
13271
+ *
13272
+ * The read is pure and the strip runs in an effect. StrictMode calls state initializers
13273
+ * twice in development, and React 18 (still allowed by our peer range) keeps the second
13274
+ * result - which an initializer that also stripped the param would get wrong. React 19
13275
+ * reuses the first result, so the test suite here cannot show that failure.
13276
+ */
13277
+ function useArrivedViaChatLink(disabled) {
13278
+ if (disabled === void 0) { disabled = false; }
13279
+ var arrived = React.useState(function () { return !disabled && hasChatContinueParam(); })[0];
13280
+ React.useEffect(function () {
13281
+ if (arrived) {
13282
+ stripChatContinueParam();
13283
+ }
13284
+ }, [arrived]);
13285
+ return arrived;
13046
13286
  }
13047
13287
 
13048
13288
  function useExternalScript(url) {
@@ -13163,6 +13403,7 @@ function useGreeting(active) {
13163
13403
  // Handle null context (e.g., in preview mode)
13164
13404
  var isAdmin = (_b = (_a = ctx === null || ctx === void 0 ? void 0 : ctx.env) === null || _a === void 0 ? void 0 : _a.isAdmin) !== null && _b !== void 0 ? _b : false;
13165
13405
  var sessionId = reactRedux.useSelector(function (state) { return state.sessionId; });
13406
+ var conversationOnScreen = reactRedux.useSelector(function (state) { return hasConversation(state.chats); });
13166
13407
  React.useEffect(function () {
13167
13408
  if (active) {
13168
13409
  if (snapshotRef.current !== curr) {
@@ -13173,7 +13414,8 @@ function useGreeting(active) {
13173
13414
  visitorId: curr.visitorId,
13174
13415
  userId: curr.userId,
13175
13416
  accessToken: curr.accessToken,
13176
- attributes: __assign$1(__assign$1({}, curr.attributes), { currentUrl: window.location.href })
13417
+ attributes: __assign$1(__assign$1({}, curr.attributes), { currentUrl: window.location.href }),
13418
+ hasConversation: conversationOnScreen
13177
13419
  }, sessionId));
13178
13420
  if (!isAdmin) {
13179
13421
  var timeoutId_1 = setTimeout(function () {
@@ -37029,7 +37271,7 @@ var ChatWidgetConnected = function (props) {
37029
37271
  * Exported for use by StaticChatWidgetContainer to avoid infinite loops.
37030
37272
  */
37031
37273
  var ChatWidgetUI = function (props) {
37032
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13;
37274
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14;
37033
37275
  var innerDispatch = useChatDispatch();
37034
37276
  var dispatch = useChatServerDispatch();
37035
37277
  // Publish the visual viewport's geometry so the panel can size around the soft keyboard.
@@ -37050,14 +37292,18 @@ var ChatWidgetUI = function (props) {
37050
37292
  // Preview mode can be enabled via `preview` prop (recommended) or legacy `mode="preview"`
37051
37293
  var previewMode = props.preview === true || mode === "preview";
37052
37294
  var modeClass = "widget-container--".concat(mode);
37053
- var canRefresh = (_c = (_b = props.config.header) === null || _b === void 0 ? void 0 : _b.actions) === null || _c === void 0 ? void 0 : _c.refresh;
37295
+ // Reached this page through a link in the chat.
37296
+ var arrivedViaChatLink = useArrivedViaChatLink(previewMode);
37297
+ var conversationOnScreen = hasConversation(chatState.chats);
37298
+ var chatButtonCta = React.useMemo(function () { var _a; return resolveCta((_a = props.config) === null || _a === void 0 ? void 0 : _a.cta, { hasConversation: conversationOnScreen, arrivedViaChatLink: arrivedViaChatLink }); }, [(_b = props.config) === null || _b === void 0 ? void 0 : _b.cta, conversationOnScreen, arrivedViaChatLink]);
37299
+ var canRefresh = (_d = (_c = props.config.header) === null || _c === void 0 ? void 0 : _c.actions) === null || _d === void 0 ? void 0 : _d.refresh;
37054
37300
  // can't minimize in docked mode, static mode, or preview mode.
37055
- var canMinimize = !dockedMode && !staticMode && !previewMode && ((_e = (_d = props.config.header) === null || _d === void 0 ? void 0 : _d.actions) === null || _e === void 0 ? void 0 : _e.minimize);
37056
- log("docked: ".concat(dockedMode, " static: ").concat(staticMode, " preview: ").concat(previewMode, " minimized: ").concat((_g = (_f = props.config.header) === null || _f === void 0 ? void 0 : _f.actions) === null || _g === void 0 ? void 0 : _g.minimize));
37301
+ var canMinimize = !dockedMode && !staticMode && !previewMode && ((_f = (_e = props.config.header) === null || _e === void 0 ? void 0 : _e.actions) === null || _f === void 0 ? void 0 : _f.minimize);
37302
+ log("docked: ".concat(dockedMode, " static: ").concat(staticMode, " preview: ").concat(previewMode, " minimized: ").concat((_h = (_g = props.config.header) === null || _g === void 0 ? void 0 : _g.actions) === null || _h === void 0 ? void 0 : _h.minimize));
37057
37303
  var canCancel;
37058
37304
  // To preserve legacy behavior, cancel needs a little more checks
37059
- if (typeof ((_j = (_h = props.config.header) === null || _h === void 0 ? void 0 : _h.actions) === null || _j === void 0 ? void 0 : _j.cancel) === "boolean") {
37060
- canCancel = !dockedMode && !staticMode && !previewMode && ((_l = (_k = props.config.header) === null || _k === void 0 ? void 0 : _k.actions) === null || _l === void 0 ? void 0 : _l.cancel);
37305
+ if (typeof ((_k = (_j = props.config.header) === null || _j === void 0 ? void 0 : _j.actions) === null || _k === void 0 ? void 0 : _k.cancel) === "boolean") {
37306
+ canCancel = !dockedMode && !staticMode && !previewMode && ((_m = (_l = props.config.header) === null || _l === void 0 ? void 0 : _l.actions) === null || _m === void 0 ? void 0 : _m.cancel);
37061
37307
  }
37062
37308
  else {
37063
37309
  canCancel = !dockedMode && !staticMode && !previewMode;
@@ -37068,16 +37314,16 @@ var ChatWidgetUI = function (props) {
37068
37314
  }
37069
37315
  // Our state - pull from storage
37070
37316
  // Parse autoOpenOnWidth once for use in both initial state and useEffect
37071
- var autoOpenOnWidthValue = ((_m = props.config) === null || _m === void 0 ? void 0 : _m.autoOpenOnWidth)
37317
+ var autoOpenOnWidthValue = ((_o = props.config) === null || _o === void 0 ? void 0 : _o.autoOpenOnWidth)
37072
37318
  ? parseInt(props.config.autoOpenOnWidth, 10)
37073
37319
  : NaN;
37074
37320
  var autoOpenOnWidthMatch = !isNaN(autoOpenOnWidthValue) && window.innerWidth >= autoOpenOnWidthValue;
37075
37321
  // When autoOpenOnWidth is configured, use width check instead of persisted visible state
37076
37322
  // to prevent greeting requests on viewports below the threshold
37077
- var _14 = React.useState((!canMinimize && !canCancel) ||
37323
+ var _15 = React.useState((!canMinimize && !canCancel) ||
37078
37324
  // !!get("visible") ||
37079
- (!isNaN(autoOpenOnWidthValue) ? autoOpenOnWidthMatch : chatState.visuals.visible)), visible = _14[0], setVisibleState = _14[1];
37080
- var _15 = React.useState(false); _15[0]; var setTypingState = _15[1]; // false initially - state kept for potential external observers
37325
+ (!isNaN(autoOpenOnWidthValue) ? autoOpenOnWidthMatch : chatState.visuals.visible)), visible = _15[0], setVisibleState = _15[1];
37326
+ var _16 = React.useState(false); _16[0]; var setTypingState = _16[1]; // false initially - state kept for potential external observers
37081
37327
  // Ref to track typing state for use in timeout callbacks
37082
37328
  var typingRef = React.useRef(false);
37083
37329
  // Timeout ref for debouncing "stop typing" events
@@ -37085,11 +37331,11 @@ var ChatWidgetUI = function (props) {
37085
37331
  // Track whether autoOpenOnWidth has already fired so it doesn't re-open after user closes
37086
37332
  var autoOpenFiredRef = React.useRef(autoOpenOnWidthMatch);
37087
37333
  var chatServer = React.useContext(ChatServerContext);
37088
- var patternsConfig = (_p = (_o = props.config) === null || _o === void 0 ? void 0 : _o.autoOpenOnPattern) === null || _p === void 0 ? void 0 : _p.patterns;
37334
+ var patternsConfig = (_q = (_p = props.config) === null || _p === void 0 ? void 0 : _p.autoOpenOnPattern) === null || _q === void 0 ? void 0 : _q.patterns;
37089
37335
  var currentUrl = window.location.href;
37090
37336
  var patternExist = patternsConfig && patternsConfig.length > 0;
37091
37337
  var patternMatches = patternsConfig === null || patternsConfig === void 0 ? void 0 : patternsConfig.some(function (pattern) { return currentUrl.includes(pattern); });
37092
- var configWidth = (_r = (_q = props.config) === null || _q === void 0 ? void 0 : _q.autoOpenOnPattern) === null || _r === void 0 ? void 0 : _r.minimumWidth;
37338
+ var configWidth = (_s = (_r = props.config) === null || _r === void 0 ? void 0 : _r.autoOpenOnPattern) === null || _s === void 0 ? void 0 : _s.minimumWidth;
37093
37339
  var currentWidth = window.innerWidth;
37094
37340
  var setVisible = React.useCallback(function (newVisible) {
37095
37341
  if (staticMode || previewMode) {
@@ -37124,7 +37370,7 @@ var ChatWidgetUI = function (props) {
37124
37370
  };
37125
37371
  // eslint-disable-next-line react-hooks/exhaustive-deps
37126
37372
  }, []);
37127
- var _16 = React.useState(!document.hidden), isTabVisible = _16[0], setIsTabVisible = _16[1];
37373
+ var _17 = React.useState(!document.hidden), isTabVisible = _17[0], setIsTabVisible = _17[1];
37128
37374
  React.useEffect(function () {
37129
37375
  var handleVisibilityChange = function () {
37130
37376
  setIsTabVisible(!document.hidden);
@@ -37348,8 +37594,8 @@ var ChatWidgetUI = function (props) {
37348
37594
  }));
37349
37595
  }
37350
37596
  // Action Bar state and handlers
37351
- var _17 = React.useState(false), formModalOpen = _17[0], setFormModalOpen = _17[1];
37352
- var _18 = React.useState(null), activeFormConfig = _18[0], setActiveFormConfig = _18[1];
37597
+ var _18 = React.useState(false), formModalOpen = _18[0], setFormModalOpen = _18[1];
37598
+ var _19 = React.useState(null), activeFormConfig = _19[0], setActiveFormConfig = _19[1];
37353
37599
  var handleFormButtonClick = React.useCallback(function (formButton) {
37354
37600
  // If form-widget is configured, use its openForm API
37355
37601
  // The form-widget script should already be pre-loaded
@@ -37416,7 +37662,7 @@ var ChatWidgetUI = function (props) {
37416
37662
  var messages = chatState && chatState.chats;
37417
37663
  log("Rendering - accountStatus: \"".concat(chatState.accountStatus, "\", connectionStatus: \"").concat(chatState.connection.connectionStatus, "\""));
37418
37664
  var config = props.config, onConnectionStatusChange = props.onConnectionStatusChange;
37419
- var actionBarEnabled = ((_s = config === null || config === void 0 ? void 0 : config.actionBar) === null || _s === void 0 ? void 0 : _s.enabled) === true;
37665
+ var actionBarEnabled = ((_t = config === null || config === void 0 ? void 0 : config.actionBar) === null || _t === void 0 ? void 0 : _t.enabled) === true;
37420
37666
  // The host page declares this; the widget cannot work it out. On the standalone
37421
37667
  // contact/booking page the form already *is* the page, which makes every button that
37422
37668
  // only opens one redundant. XappMedia/chat-widget#1626.
@@ -37440,7 +37686,7 @@ var ChatWidgetUI = function (props) {
37440
37686
  onConnectionStatusChange(connectionStatus);
37441
37687
  }
37442
37688
  }, [connectionStatus, onConnectionStatusChange]);
37443
- useExternalScript((_t = props.config) === null || _t === void 0 ? void 0 : _t.middlewareUrl);
37689
+ useExternalScript((_u = props.config) === null || _u === void 0 ? void 0 : _u.middlewareUrl);
37444
37690
  // Only reach for form.xapp.ai when something on this page can still open the form.
37445
37691
  //
37446
37692
  // Loading it is not free: the script mounts a second form-widget instance whose wrapper
@@ -37480,15 +37726,18 @@ var ChatWidgetUI = function (props) {
37480
37726
  };
37481
37727
  }, [actionBarVisible]);
37482
37728
  // This is a pseudo agent. It represent's the widget (shown in the header avatar for instance)
37483
- var widgetAgent = ((_u = chatState.agents["agent:robot"]) === null || _u === void 0 ? void 0 : _u.user) ||
37729
+ var widgetAgent = ((_v = chatState.agents["agent:robot"]) === null || _v === void 0 ? void 0 : _v.user) ||
37484
37730
  (config === null || config === void 0 ? void 0 : config.agent) || {
37485
37731
  nick: "agent:robot",
37486
37732
  avatarPath: config.avatarUrl,
37487
37733
  display_name: "Agent",
37488
37734
  };
37489
- return (require$$0.jsxs(require$$0.Fragment, { children: [require$$0.jsxs("div", { className: "widget-container ".concat(modeClass, " ").concat(getVisibilityClass()).concat(actionBarVisible ? " widget-container--with-action-bar" : ""), children: [require$$0.jsx(WidgetStylesheet, { theme: config === null || config === void 0 ? void 0 : config.theme }), require$$0.jsx(ChatHeader, { accountStatus: chatState.accountStatus, refreshOnClick: handleRestartClick, minimizeOnClick: handleMinimizeClick, cancelOnClick: handleCancelClick, agent: widgetAgent, canRefresh: canRefresh, canMinimize: canMinimize, canCancel: canCancel, config: config === null || config === void 0 ? void 0 : config.header, menuConfig: config.menu, debugMode: (_x = (_w = (_v = props.config) === null || _v === void 0 ? void 0 : _v.connection) === null || _w === void 0 ? void 0 : _w.mcp) === null || _x === void 0 ? void 0 : _x.debugMode, onSubmit: handleOnSubmit }), require$$0.jsx(MessageList, { visible: visible, queuePosition: chatState.queuePosition, isChatting: chatState.isChatting, isOffline: isOffline, messages: messages, agents: chatState.agents, agent: config === null || config === void 0 ? void 0 : config.agent, lastRatingRequestTimestamp: chatState.lastRatingRequestTimestamp, hasRating: chatState.hasRating, visitorId: chatState.visitorId, hasWsButton: !!chatState.wsButton, messageMiddleware: props.messageMiddleware, textTypingStatusEnabled: (_z = (_y = props.config) === null || _y === void 0 ? void 0 : _y.typingStatus) === null || _z === void 0 ? void 0 : _z.textTypingStatusEnabled, disableAutoScroll: props.disableAutoScroll, onSend: handleSendMessage, onWrite: handleWriteMessage, onOpenUrl: handleOpenUrl, minimizeOnClick: handleMinimizeClick }), require$$0.jsx("div", { className: "xa-spinner-container ".concat(visible && connectionStatus === "pending" ? "visible" : ""), children: require$$0.jsx("div", { className: "xa-spinner" }) }), connectionStatus === "offline" && require$$0.jsx(ServerOffline, {}), chatState.wsButton && visible && (require$$0.jsx(WsButton, { button: chatState.wsButton, onPress: handleWsButtonPress })), require$$0.jsx(ChatFooter, { isAdmin: config === null || config === void 0 ? void 0 : config.isAdmin, isChatting: chatState.isChatting, placeholder: (_0 = config === null || config === void 0 ? void 0 : config.input) === null || _0 === void 0 ? void 0 : _0.placeholder, sendButtonIcon: (_2 = (_1 = config === null || config === void 0 ? void 0 : config.footer) === null || _1 === void 0 ? void 0 : _1.sendButton) === null || _2 === void 0 ? void 0 : _2.icon, sendButtonIconHover: (_4 = (_3 = config === null || config === void 0 ? void 0 : config.footer) === null || _3 === void 0 ? void 0 : _3.sendButton) === null || _4 === void 0 ? void 0 : _4.iconHover, sendButtonIconDisabled: (_6 = (_5 = config === null || config === void 0 ? void 0 : config.footer) === null || _5 === void 0 ? void 0 : _5.sendButton) === null || _6 === void 0 ? void 0 : _6.iconDisabled, visible: visible, hasWsButton: !!chatState.wsButton, menuConfig: (_7 = props.config) === null || _7 === void 0 ? void 0 : _7.menu, footerConfig: (_8 = props.config) === null || _8 === void 0 ? void 0 : _8.footer, inputConfig: (_9 = props.config) === null || _9 === void 0 ? void 0 : _9.input, disabled: previewMode, onChange: handleOnChange, onSubmit: handleOnSubmit, onFileUpload: handleFileUpload }), require$$0.jsx("div", { className: "restartModal", ref: modalRef, onClick: handleRestartModalCloseClick, children: require$$0.jsx(ModalContent, { onClose: handleRestartModalCloseClick, onReset: handleReset }) })] }), actionBarVisible && config.actionBar ? (require$$0.jsxs(require$$0.Fragment, { children: [require$$0.jsx(ActionBar, { config: __assign$1(__assign$1({}, config.actionBar), {
37735
+ return (require$$0.jsxs(require$$0.Fragment, { children: [require$$0.jsxs("div", { className: "widget-container ".concat(modeClass, " ").concat(getVisibilityClass()).concat(actionBarVisible ? " widget-container--with-action-bar" : ""), children: [require$$0.jsx(WidgetStylesheet, { theme: config === null || config === void 0 ? void 0 : config.theme }), require$$0.jsx(ChatHeader, { accountStatus: chatState.accountStatus, refreshOnClick: handleRestartClick, minimizeOnClick: handleMinimizeClick, cancelOnClick: handleCancelClick, agent: widgetAgent, canRefresh: canRefresh, canMinimize: canMinimize, canCancel: canCancel, config: config === null || config === void 0 ? void 0 : config.header, menuConfig: config.menu, debugMode: (_y = (_x = (_w = props.config) === null || _w === void 0 ? void 0 : _w.connection) === null || _x === void 0 ? void 0 : _x.mcp) === null || _y === void 0 ? void 0 : _y.debugMode, onSubmit: handleOnSubmit }), require$$0.jsx(MessageList, { visible: visible, queuePosition: chatState.queuePosition, isChatting: chatState.isChatting, isOffline: isOffline, messages: messages, agents: chatState.agents, agent: config === null || config === void 0 ? void 0 : config.agent, lastRatingRequestTimestamp: chatState.lastRatingRequestTimestamp, hasRating: chatState.hasRating, visitorId: chatState.visitorId, hasWsButton: !!chatState.wsButton, messageMiddleware: props.messageMiddleware, textTypingStatusEnabled: (_0 = (_z = props.config) === null || _z === void 0 ? void 0 : _z.typingStatus) === null || _0 === void 0 ? void 0 : _0.textTypingStatusEnabled, disableAutoScroll: props.disableAutoScroll, onSend: handleSendMessage, onWrite: handleWriteMessage, onOpenUrl: handleOpenUrl, minimizeOnClick: handleMinimizeClick }), require$$0.jsx("div", { className: "xa-spinner-container ".concat(visible && connectionStatus === "pending" ? "visible" : ""), children: require$$0.jsx("div", { className: "xa-spinner" }) }), connectionStatus === "offline" && require$$0.jsx(ServerOffline, {}), chatState.wsButton && visible && (require$$0.jsx(WsButton, { button: chatState.wsButton, onPress: handleWsButtonPress })), require$$0.jsx(ChatFooter, { isAdmin: config === null || config === void 0 ? void 0 : config.isAdmin, isChatting: chatState.isChatting, placeholder: (_1 = config === null || config === void 0 ? void 0 : config.input) === null || _1 === void 0 ? void 0 : _1.placeholder, sendButtonIcon: (_3 = (_2 = config === null || config === void 0 ? void 0 : config.footer) === null || _2 === void 0 ? void 0 : _2.sendButton) === null || _3 === void 0 ? void 0 : _3.icon, sendButtonIconHover: (_5 = (_4 = config === null || config === void 0 ? void 0 : config.footer) === null || _4 === void 0 ? void 0 : _4.sendButton) === null || _5 === void 0 ? void 0 : _5.iconHover, sendButtonIconDisabled: (_7 = (_6 = config === null || config === void 0 ? void 0 : config.footer) === null || _6 === void 0 ? void 0 : _6.sendButton) === null || _7 === void 0 ? void 0 : _7.iconDisabled, visible: visible, hasWsButton: !!chatState.wsButton, menuConfig: (_8 = props.config) === null || _8 === void 0 ? void 0 : _8.menu, footerConfig: (_9 = props.config) === null || _9 === void 0 ? void 0 : _9.footer, inputConfig: (_10 = props.config) === null || _10 === void 0 ? void 0 : _10.input, disabled: previewMode, onChange: handleOnChange, onSubmit: handleOnSubmit, onFileUpload: handleFileUpload }), require$$0.jsx("div", { className: "restartModal", ref: modalRef, onClick: handleRestartModalCloseClick, children: require$$0.jsx(ModalContent, { onClose: handleRestartModalCloseClick, onReset: handleReset }) })] }), actionBarVisible && config.actionBar ? (require$$0.jsxs(require$$0.Fragment, { children: [require$$0.jsx(ActionBar, { config: __assign$1(__assign$1({}, config.actionBar), {
37490
37736
  // Use actionBar.cta if set, otherwise fall back to widget-level cta
37491
- cta: (_10 = config.actionBar.cta) !== null && _10 !== void 0 ? _10 : config.cta }), visible: visible, chatDisabled: config.disabled, formDisabled: isFormPage, hasUserInteracted: (_11 = chatState.visuals) === null || _11 === void 0 ? void 0 : _11.hasInteracted, onChatClick: chatButtonOnClick, onChatMinimize: handleMinimizeClick, onFormClick: handleFormButtonClick, onCtaDismiss: handleCtaDismiss }), formModalOpen && activeFormConfig && (require$$0.jsx(FormModal, { config: activeFormConfig, widgetEnv: config, onClose: handleFormModalClose }))] })) : (require$$0.jsx(ChatButton, { addClass: visible ? "visible" : "", onClick: chatButtonOnClick, config: config === null || config === void 0 ? void 0 : config.cta, imageUrl: (_12 = config === null || config === void 0 ? void 0 : config.chatButton) === null || _12 === void 0 ? void 0 : _12.imageUrl, visible: visible, hasInteracted: (_13 = chatState.visuals) === null || _13 === void 0 ? void 0 : _13.hasInteracted, onCtaDismiss: handleCtaDismiss })), require$$0.jsx(ErrorOverlay, { enableErrorOverlay: config === null || config === void 0 ? void 0 : config.enableErrorOverlay })] }));
37737
+ cta: resolveCta((_11 = config.actionBar.cta) !== null && _11 !== void 0 ? _11 : config.cta, {
37738
+ hasConversation: conversationOnScreen,
37739
+ arrivedViaChatLink: arrivedViaChatLink,
37740
+ }) }), visible: visible, chatDisabled: config.disabled, formDisabled: isFormPage, hasUserInteracted: (_12 = chatState.visuals) === null || _12 === void 0 ? void 0 : _12.hasInteracted, onChatClick: chatButtonOnClick, onChatMinimize: handleMinimizeClick, onFormClick: handleFormButtonClick, onCtaDismiss: handleCtaDismiss }), formModalOpen && activeFormConfig && (require$$0.jsx(FormModal, { config: activeFormConfig, widgetEnv: config, onClose: handleFormModalClose }))] })) : (require$$0.jsx(ChatButton, { addClass: visible ? "visible" : "", onClick: chatButtonOnClick, config: chatButtonCta, imageUrl: (_13 = config === null || config === void 0 ? void 0 : config.chatButton) === null || _13 === void 0 ? void 0 : _13.imageUrl, visible: visible, hasInteracted: (_14 = chatState.visuals) === null || _14 === void 0 ? void 0 : _14.hasInteracted, onCtaDismiss: handleCtaDismiss })), require$$0.jsx(ErrorOverlay, { enableErrorOverlay: config === null || config === void 0 ? void 0 : config.enableErrorOverlay })] }));
37492
37741
  };
37493
37742
  /**
37494
37743
  * Top-level wrapper that dispatches between preview mode and connected mode.
@@ -37948,8 +38197,11 @@ function update(state, action) {
37948
38197
  var finalMsg_1 = endDetail.msg;
37949
38198
  // Preserve tool calls from either the action or the existing message
37950
38199
  var toolCalls_1 = endDetail.toolCalls || endStreamingChat.toolCalls;
37951
- // Update chips from message options (same as chat.msg handler)
37952
- var chips = ((_w = finalMsg_1 === null || finalMsg_1 === void 0 ? void 0 : finalMsg_1.options) === null || _w === void 0 ? void 0 : _w.length) ? finalMsg_1.options : [];
38200
+ // Update chips from message options (same as chat.msg handler) — unless this closes
38201
+ // an intermediate bubble of a split turn, whose chips arrive with the final end.
38202
+ var chips = endDetail.preserveChips
38203
+ ? state.chips
38204
+ : ((_w = finalMsg_1 === null || finalMsg_1 === void 0 ? void 0 : finalMsg_1.options) === null || _w === void 0 ? void 0 : _w.length) ? finalMsg_1.options : [];
37953
38205
  return __assign$1(__assign$1({}, state), { lastTimestamp: endDetail === null || endDetail === void 0 ? void 0 : endDetail.timestamp, streamingMessageId: undefined, chips: chips, chats: state.chats.map(function (chat, index) {
37954
38206
  if (index === streamingIndex_4 && isChatMsgDetail(chat)) {
37955
38207
  return __assign$1(__assign$1({}, chat), { msg: finalMsg_1, isStreaming: false, wasStreamed: true, toolCalls: toolCalls_1 });