@xapp/chat-widget 1.101.0 → 1.103.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.es.js CHANGED
@@ -919,6 +919,15 @@ var ActionBarCta = function (_a) {
919
919
  * in `_mixins.scss`. Keep this in step with the `mobile()` mixin. XappMedia/chat-widget#1594.
920
920
  */
921
921
  var MOBILE_MEDIA_QUERY = "(max-width: 640px), (max-height: 480px) and (pointer: coarse)";
922
+ /**
923
+ * Whether the chat panel renders full-screen right now. False where matchMedia is missing
924
+ * (jsdom), matching ActionBar's assume-desktop fallback.
925
+ */
926
+ function isMobileViewport(win) {
927
+ if (win === void 0) { win = window; }
928
+ return typeof win.matchMedia === "function" && win.matchMedia(MOBILE_MEDIA_QUERY).matches;
929
+ }
930
+
922
931
  /**
923
932
  * ChatWidget.scss sizes the chat panel against this variable. Nothing used to set it, so it always
924
933
  * fell back to the hardcoded 80px in the SCSS - and the real mobile bar is taller than that once
@@ -2322,8 +2331,100 @@ function setVisualStatus(status) {
2322
2331
  };
2323
2332
  }
2324
2333
 
2325
- function execute(url, dispatch, visuals, behavior) {
2334
+ /**
2335
+ * Carries "this visitor just clicked a link in the chat" across a same-tab navigation.
2336
+ *
2337
+ * Persisted widget state already survives the page load, but it cannot tell a chat link
2338
+ * click from a visitor coming back to the site later. The param can; it is read on arrival
2339
+ * and stripped from the address bar so it is never bookmarked or shared.
2340
+ */
2341
+ var CHAT_CONTINUE_PARAM = "xa_chat";
2342
+ var CHAT_CONTINUE_VALUE = "resume";
2343
+ var CONTINUE_CTA_MESSAGE = "Continue your conversation";
2344
+ /**
2345
+ * Marks a link the widget is about to follow in this tab. Only same-origin links: widget
2346
+ * state lives in this origin's localStorage, so the widget on any other origin - a
2347
+ * subdomain included - has no conversation to continue.
2348
+ */
2349
+ function withChatContinueParam(url, currentOrigin) {
2350
+ try {
2351
+ var urlObj = new URL(url);
2352
+ if (urlObj.origin !== currentOrigin || urlObj.searchParams.get(CHAT_CONTINUE_PARAM) === CHAT_CONTINUE_VALUE) {
2353
+ return url;
2354
+ }
2355
+ urlObj.searchParams.set(CHAT_CONTINUE_PARAM, CHAT_CONTINUE_VALUE);
2356
+ return urlObj.toString();
2357
+ }
2358
+ catch (_) {
2359
+ return url;
2360
+ }
2361
+ }
2362
+ /**
2363
+ * True when this page was reached through a chat link. Pure, so it is safe inside a
2364
+ * render-time initializer that React may call twice (StrictMode); strip the param
2365
+ * separately with `stripChatContinueParam`, from an effect.
2366
+ */
2367
+ function hasChatContinueParam(win) {
2368
+ if (win === void 0) { win = window; }
2369
+ try {
2370
+ return new URL(win.location.href).searchParams.get(CHAT_CONTINUE_PARAM) === CHAT_CONTINUE_VALUE;
2371
+ }
2372
+ catch (_) {
2373
+ return false;
2374
+ }
2375
+ }
2376
+ /**
2377
+ * Removes the param from the address bar with replaceState, keeping the rest of the URL
2378
+ * and any SPA history state. A no-op when it is absent, so repeating it is harmless.
2379
+ */
2380
+ function stripChatContinueParam(win) {
2381
+ if (win === void 0) { win = window; }
2382
+ try {
2383
+ var url = new URL(win.location.href);
2384
+ if (!url.searchParams.has(CHAT_CONTINUE_PARAM)) {
2385
+ return;
2386
+ }
2387
+ url.searchParams.delete(CHAT_CONTINUE_PARAM);
2388
+ win.history.replaceState(win.history.state, "", url.toString());
2389
+ }
2390
+ catch (_) {
2391
+ // An address bar we cannot rewrite only leaves the param visible.
2392
+ }
2393
+ }
2394
+ /**
2395
+ * A conversation worth continuing: any message on screen. Join notices do not count - the
2396
+ * MCP connection posts one before the greeting, so a brand-new session has one too.
2397
+ */
2398
+ function hasConversation(chats) {
2399
+ return !!(chats === null || chats === void 0 ? void 0 : chats.some(function (chat) { return chat.type === "chat.msg"; }));
2400
+ }
2401
+ /**
2402
+ * The CTA to show, given whether there is a conversation to go back to.
2403
+ *
2404
+ * With one, the first-visit copy ("Need Help? Click Here") becomes a prompt to continue,
2405
+ * keeping the tenant's timing and styling. A tenant with no CTA gets none on an ordinary
2406
+ * page load; right after a chat link it does, immediately - on mobile that prompt is the
2407
+ * only sign the conversation is still there, because the panel stays closed over the page
2408
+ * they asked for.
2409
+ */
2410
+ function resolveCta(cta, options) {
2411
+ var arrivedViaChatLink = options.arrivedViaChatLink;
2412
+ if (!options.hasConversation || (!(cta === null || cta === void 0 ? void 0 : cta.message) && !arrivedViaChatLink)) {
2413
+ return cta;
2414
+ }
2415
+ var immediately = arrivedViaChatLink ? { delay: 0 } : {};
2416
+ var mobile = cta === null || cta === void 0 ? void 0 : cta.mobile;
2417
+ return __assign$1(__assign$1(__assign$1(__assign$1({}, cta), { message: CONTINUE_CTA_MESSAGE }), immediately), (mobile && {
2418
+ mobile: __assign$1(__assign$1(__assign$1({}, mobile), ((typeof mobile.message === "string" || arrivedViaChatLink) && { message: CONTINUE_CTA_MESSAGE })), immediately),
2419
+ }));
2420
+ }
2421
+
2422
+ /**
2423
+ * @internal Exported for unit testing; use `useOpenUrlCallback`.
2424
+ */
2425
+ function executeOpenUrl(url, dispatch, visuals, behavior, isMobile) {
2326
2426
  var _a, _b;
2427
+ if (isMobile === void 0) { isMobile = isMobileViewport(); }
2327
2428
  if (!dispatch || !visuals) {
2328
2429
  throw new Error("OpenUrl: No dispatch and/or visual.");
2329
2430
  }
@@ -2390,12 +2491,23 @@ function execute(url, dispatch, visuals, behavior) {
2390
2491
  window.open(url, "_blank");
2391
2492
  break;
2392
2493
  case "sameWindow":
2393
- if (!visuals.opened) {
2494
+ // Desktop: the next page reopens the chat beside the content, as before.
2495
+ // Mobile: the panel is full-screen, so reopening it would cover the page they
2496
+ // just asked for. Leave it closed; the param lets that page offer to continue.
2497
+ // Unconditional: callers can hand in a `visuals` captured before the chat opened
2498
+ // (ChatMarkdownMessage's click handler does), and skipping on a stale `opened`
2499
+ // leaves the persisted state open.
2500
+ if (isMobile) {
2501
+ dispatch(setVisualStatus({
2502
+ opened: false
2503
+ }));
2504
+ }
2505
+ else if (!visuals.opened) {
2394
2506
  dispatch(setVisualStatus({
2395
2507
  opened: true
2396
2508
  }));
2397
2509
  }
2398
- window.open(url, "_self");
2510
+ window.open(withChatContinueParam(url, window.location.origin), "_self");
2399
2511
  break;
2400
2512
  default:
2401
2513
  throwBadKind$1(type);
@@ -2407,7 +2519,7 @@ function useOpenUrlCallback() {
2407
2519
  if (!behavior) {
2408
2520
  behavior = (env === null || env === void 0 ? void 0 : env.urls) ? resolveUrlPolicy(url, env.urls) : defaultBehavior;
2409
2521
  }
2410
- execute(url, dispatch, visuals, behavior);
2522
+ executeOpenUrl(url, dispatch, visuals, behavior);
2411
2523
  }, [env]);
2412
2524
  }
2413
2525
 
@@ -4300,6 +4412,10 @@ var MCPChat = /** @class */ (function () {
4300
4412
  this.isInitializingSession = false;
4301
4413
  // Session validation state (silent handshake - no stream dispatch)
4302
4414
  this.isValidatingSession = false;
4415
+ // A conversation is on screen that the server should keep: restored from a previous page,
4416
+ // or confirmed by the server on this one. Sent as `continueConversation`, which also lets
4417
+ // the server resume a session idle longer than its default window.
4418
+ this.conversationOnScreen = false;
4303
4419
  // Tool call tracking for debug mode
4304
4420
  this.currentToolCalls = [];
4305
4421
  this.isReplaying = false;
@@ -4387,8 +4503,12 @@ var MCPChat = /** @class */ (function () {
4387
4503
  _a.label = 1;
4388
4504
  case 1:
4389
4505
  _a.trys.push([1, 3, 4, 5]);
4390
- return [4 /*yield*/, this.performSSERequest(this.buildRequestPayload({ resumeSession: true }))];
4506
+ // With a conversation on screen (e.g. after a same-tab link click), ask the
4507
+ // server to continue it: without the flag it clears the history and greets.
4508
+ return [4 /*yield*/, this.performSSERequest(this.buildRequestPayload(__assign$1({ resumeSession: true }, (this.conversationOnScreen && { continueConversation: true }))))];
4391
4509
  case 2:
4510
+ // With a conversation on screen (e.g. after a same-tab link click), ask the
4511
+ // server to continue it: without the flag it clears the history and greets.
4392
4512
  _a.sent();
4393
4513
  log("Session validation complete");
4394
4514
  return [3 /*break*/, 5];
@@ -5068,7 +5188,7 @@ var MCPChat = /** @class */ (function () {
5068
5188
  return [2 /*return*/];
5069
5189
  }
5070
5190
  log("[attemptSSERequest] Sending message: \"".concat(messageText, "\", userId: ").concat(this._userId, ", sessionId: ").concat(this._sessionId));
5071
- return [2 /*return*/, this.performSSERequest(this.buildRequestPayload({ message: messageText }))];
5191
+ return [2 /*return*/, this.performSSERequest(this.buildRequestPayload(__assign$1({ message: messageText }, (this.conversationOnScreen && { continueConversation: true }))))];
5072
5192
  });
5073
5193
  });
5074
5194
  };
@@ -5150,6 +5270,7 @@ var MCPChat = /** @class */ (function () {
5150
5270
  case "connected":
5151
5271
  // Connection established
5152
5272
  log("[SSE] Connected event received, messageId: ".concat(messageId, ", isReturningUser: ").concat(data.isReturningUser, ", isReplay: ").concat(data.isReplay));
5273
+ _this.conversationOnScreen = true;
5153
5274
  // Track if this is a replay connection
5154
5275
  if (data.isReplay) {
5155
5276
  _this.isReplaying = true;
@@ -5185,10 +5306,27 @@ var MCPChat = /** @class */ (function () {
5185
5306
  }
5186
5307
  _this.startStreamThrottle(bubbleId);
5187
5308
  break;
5309
+ case "session_resumed":
5310
+ // The server continued the conversation: no greeting follows,
5311
+ // so adopt the identity without opening a stream bubble.
5312
+ log("[SSE] Session resumed, sessionId: ".concat(data.sessionId, ", hasHistory: ").concat(data.hasHistory));
5313
+ _this.conversationOnScreen = true;
5314
+ if (data.userId) {
5315
+ _this._userId = data.userId;
5316
+ _this.userIdIsServerAssigned = true;
5317
+ }
5318
+ if (data.sessionId) {
5319
+ _this._sessionId = data.sessionId;
5320
+ _this.dispatch(setSessionId(_this._sessionId));
5321
+ }
5322
+ storeIdentity(data.userId, data.sessionId);
5323
+ _this.stopTyping();
5324
+ break;
5188
5325
  case "session_expired":
5189
5326
  // Session no longer exists on server - need to start fresh
5190
5327
  log("[SSE] Session expired: ".concat(data.reason || "Session not found"));
5191
5328
  // Clear stored session (and lastSeq) but keep userId for returning user identification
5329
+ _this.conversationOnScreen = false;
5192
5330
  clearStoredSession(_this._sessionId);
5193
5331
  _this._sessionId = "";
5194
5332
  _this.lastSeq = undefined;
@@ -5439,6 +5577,7 @@ var MCPChat = /** @class */ (function () {
5439
5577
  MCPChat.prototype.setVisitorInfo = function (visitorInfo, sessionId, cb) {
5440
5578
  this.visitorInfo = visitorInfo;
5441
5579
  this._attributes = __assign$1({}, this.visitorInfo.attributes);
5580
+ this.conversationOnScreen = visitorInfo.hasConversation === true;
5442
5581
  // do not set currentUrl if localhost
5443
5582
  var href = new URL(window.location.href);
5444
5583
  if (href === null || href === void 0 ? void 0 : href.host.toLowerCase().startsWith("localhost")) {
@@ -12953,7 +13092,21 @@ var ChatTextMessage = function (props) {
12953
13092
  var wasStreamed = message.wasStreamed;
12954
13093
  var showTail = agentMessage && !props.sibling && !isStreaming && !wasStreamed;
12955
13094
  var hasToolCalls = props.toolCalls && props.toolCalls.length > 0;
12956
- return (jsxs("div", { className: "chat-msg".concat(isStreaming ? " chat-msg--streaming" : ""), children: [hasToolCalls && (jsx(ToolCallDisplay, { toolCalls: props.toolCalls, debugMode: props.debugMode })), jsx("div", { className: "chat-text-container", children: jsxs(ChatMessageBubble, { owner: agentMessage ? "others" : "mine", hasTail: showTail, children: [jsx("span", { className: "message-sr-only", children: "at " + time + (agentMessage ? (isStreaming ? " the bot is typing" : " the bot said") : " the user said") }), parsedHtml ? (jsx("span", { className: "chat-msg__content", dangerouslySetInnerHTML: { __html: parsedHtml } })) : (jsx("span", { className: "chat-msg__content", children: message.msg.text })), isStreaming && jsx("span", { className: "streaming-cursor", "aria-hidden": "true" })] }) })] }));
13095
+ // Delegated, because the links are inside dangerouslySetInnerHTML. Modified clicks stay
13096
+ // with the browser so ctrl/cmd-click still opens a background tab.
13097
+ var onLinkClick = props.onLinkClick;
13098
+ var handleContentClick = function (ev) {
13099
+ var _a, _b;
13100
+ if (!onLinkClick || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) {
13101
+ return;
13102
+ }
13103
+ var link = (_b = (_a = ev.target).closest) === null || _b === void 0 ? void 0 : _b.call(_a, "a");
13104
+ if (link && ev.currentTarget.contains(link)) {
13105
+ ev.preventDefault();
13106
+ onLinkClick(link.href);
13107
+ }
13108
+ };
13109
+ return (jsxs("div", { className: "chat-msg".concat(isStreaming ? " chat-msg--streaming" : ""), children: [hasToolCalls && (jsx(ToolCallDisplay, { toolCalls: props.toolCalls, debugMode: props.debugMode })), jsx("div", { className: "chat-text-container", children: jsxs(ChatMessageBubble, { owner: agentMessage ? "others" : "mine", hasTail: showTail, children: [jsx("span", { className: "message-sr-only", children: "at " + time + (agentMessage ? (isStreaming ? " the bot is typing" : " the bot said") : " the user said") }), parsedHtml ? (jsx("span", { className: "chat-msg__content", onClick: handleContentClick, dangerouslySetInnerHTML: { __html: parsedHtml } })) : (jsx("span", { className: "chat-msg__content", children: message.msg.text })), isStreaming && jsx("span", { className: "streaming-cursor", "aria-hidden": "true" })] }) })] }));
12957
13110
  };
12958
13111
 
12959
13112
  var ChatScheduleWidget = function (props) {
@@ -12996,14 +13149,21 @@ var avaKeys = ["text", "html", "card", "list"];
12996
13149
  * @returns
12997
13150
  */
12998
13151
  var ChatMessage = function (props) {
12999
- var _a, _b, _c;
13152
+ var _a, _b, _c, _d;
13000
13153
  var middleware = props.messageMiddleware || StandardMiddlewares;
13001
13154
  var chatConfig = useContext(ChatConfigContext);
13002
13155
  // console.log(`########### chatConfig: ${JSON.stringify(chatConfig, null, 2)}`);
13003
13156
  var ctx = props.middlewareContext;
13004
13157
  var user = props.agent;
13158
+ var chatDispatch = useChatDispatch();
13159
+ var visuals = useSelector(function (state) { return state.visuals; });
13160
+ // Text links follow the tenant's URL policy when it set one (utm_source, same-tab
13161
+ // continuation, ...). Tenants without one keep the plain new-tab anchor.
13162
+ 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)
13163
+ ? function (href) { return ctx.openUrl(href, chatDispatch, visuals); }
13164
+ : undefined;
13005
13165
  // Get hideUserInfo from the agent info based on the message sender's nick
13006
- var agentInfo = (_a = props.agents) === null || _a === void 0 ? void 0 : _a[props.message.user.nick];
13166
+ var agentInfo = (_b = props.agents) === null || _b === void 0 ? void 0 : _b[props.message.user.nick];
13007
13167
  var hideUserInfo = (agentInfo === null || agentInfo === void 0 ? void 0 : agentInfo.hideUserInfo) || false;
13008
13168
  function renderByType() {
13009
13169
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
@@ -13022,7 +13182,7 @@ var ChatMessage = function (props) {
13022
13182
  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;
13023
13183
  // Show tool progress with avatar when no text yet, or above text when text exists
13024
13184
  var showToolProgressWithAvatar = hasToolCalls && !msg.text;
13025
- return (jsxs(Fragment, { children: [showToolProgressWithAvatar && (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: jsx("div", { className: "chat-msg", children: jsx(ToolCallDisplay, { toolCalls: toolCalls, debugMode: debugMode }) }) })), msg.text && (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: jsx(ChatTextMessage, { message: props.message, sibling: props.sibling, user: user, toolCalls: hasToolCalls ? toolCalls : undefined, debugMode: debugMode }) })), msg.html &&
13185
+ return (jsxs(Fragment, { children: [showToolProgressWithAvatar && (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: jsx("div", { className: "chat-msg", children: jsx(ToolCallDisplay, { toolCalls: toolCalls, debugMode: debugMode }) }) })), msg.text && (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: jsx(ChatTextMessage, { message: props.message, sibling: props.sibling, user: user, toolCalls: hasToolCalls ? toolCalls : undefined, debugMode: debugMode, onLinkClick: handleTextLinkClick }) })), msg.html &&
13026
13186
  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: 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) {
13027
13187
  if (display.type === "ScheduleButton") {
13028
13188
  return (jsx(ChatScheduleWidget, { minimizeOnClick: props.minimizeOnClick, display: display }, index));
@@ -13046,7 +13206,7 @@ var ChatMessage = function (props) {
13046
13206
  }
13047
13207
  // Don't show timestamp/sources while streaming or if message has no content yet
13048
13208
  var isStreaming = props.message.isStreaming;
13049
- 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));
13209
+ 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));
13050
13210
  var showFooter = !isStreaming && hasContent;
13051
13211
  // empty
13052
13212
  return (jsx("div", { className: "chat-msg-container-wrapper ".concat(isAgent(props.message.user.nick) ? "agent" : "visitor", " ").concat(props.sibling ? "sibling" : ""), children: jsxs("div", { className: "chat-msg-container ".concat(getClassName(props.message)), children: [renderByType(), showFooter && renderTimestampAndSources()] }) }));
@@ -13056,9 +13216,6 @@ var ChatMessage = function (props) {
13056
13216
  * value must be like "60m", "2h" etc.
13057
13217
  */
13058
13218
  function convertToSeconds(s) {
13059
- if (!s) {
13060
- return 0;
13061
- }
13062
13219
  var groups = s.match(/(\d+)([dhms]?)/);
13063
13220
  if (!groups) {
13064
13221
  err("Invalid time string: ".concat(s));
@@ -13081,15 +13238,50 @@ function convertToSeconds(s) {
13081
13238
  }
13082
13239
  }
13083
13240
 
13084
- function checkSessionExpiration(duration, lastMessageTimestamp, _lastTimestamp) {
13085
- var durationMs = convertToSeconds(duration) * 1000;
13086
- // never if no session duration in config
13087
- if (!duration) {
13241
+ /**
13242
+ * How long a conversation must sit idle, on top of the calendar day changing, before it
13243
+ * expires when the widget config does not say.
13244
+ */
13245
+ var DEFAULT_SESSION_EXPIRATION = "4h";
13246
+ function isSameLocalDay(a, b) {
13247
+ var dayA = new Date(a);
13248
+ var dayB = new Date(b);
13249
+ return dayA.getFullYear() === dayB.getFullYear()
13250
+ && dayA.getMonth() === dayB.getMonth()
13251
+ && dayA.getDate() === dayB.getDate();
13252
+ }
13253
+ /**
13254
+ * A conversation expires only when BOTH the visitor's local calendar day has changed since
13255
+ * the last message AND at least `duration` has passed. So a visitor who browses the site
13256
+ * over an afternoon keeps one conversation, and one chatting at 11pm is not greeted afresh
13257
+ * at 1am - but tomorrow's visit starts clean.
13258
+ */
13259
+ function checkSessionExpiration(duration, lastMessageTimestamp, _lastTimestamp, now) {
13260
+ if (now === void 0) { now = Date.now(); }
13261
+ if (!lastMessageTimestamp) {
13088
13262
  return false;
13089
13263
  }
13090
- // TODO: Why? Let's use the time passed since the last message timestamp
13091
- // return lastTimestamp - lastMessageTimestamp > durationMs;
13092
- return +new Date() - lastMessageTimestamp > durationMs;
13264
+ var durationMs = convertToSeconds(duration || DEFAULT_SESSION_EXPIRATION) * 1000;
13265
+ return now - lastMessageTimestamp >= durationMs && !isSameLocalDay(lastMessageTimestamp, now);
13266
+ }
13267
+
13268
+ /**
13269
+ * Whether this page was reached through a same-tab link in the chat, read once on mount.
13270
+ *
13271
+ * The read is pure and the strip runs in an effect. StrictMode calls state initializers
13272
+ * twice in development, and React 18 (still allowed by our peer range) keeps the second
13273
+ * result - which an initializer that also stripped the param would get wrong. React 19
13274
+ * reuses the first result, so the test suite here cannot show that failure.
13275
+ */
13276
+ function useArrivedViaChatLink(disabled) {
13277
+ if (disabled === void 0) { disabled = false; }
13278
+ var arrived = useState(function () { return !disabled && hasChatContinueParam(); })[0];
13279
+ useEffect(function () {
13280
+ if (arrived) {
13281
+ stripChatContinueParam();
13282
+ }
13283
+ }, [arrived]);
13284
+ return arrived;
13093
13285
  }
13094
13286
 
13095
13287
  function useExternalScript(url) {
@@ -13210,6 +13402,7 @@ function useGreeting(active) {
13210
13402
  // Handle null context (e.g., in preview mode)
13211
13403
  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;
13212
13404
  var sessionId = useSelector(function (state) { return state.sessionId; });
13405
+ var conversationOnScreen = useSelector(function (state) { return hasConversation(state.chats); });
13213
13406
  useEffect(function () {
13214
13407
  if (active) {
13215
13408
  if (snapshotRef.current !== curr) {
@@ -13220,7 +13413,8 @@ function useGreeting(active) {
13220
13413
  visitorId: curr.visitorId,
13221
13414
  userId: curr.userId,
13222
13415
  accessToken: curr.accessToken,
13223
- attributes: __assign$1(__assign$1({}, curr.attributes), { currentUrl: window.location.href })
13416
+ attributes: __assign$1(__assign$1({}, curr.attributes), { currentUrl: window.location.href }),
13417
+ hasConversation: conversationOnScreen
13224
13418
  }, sessionId));
13225
13419
  if (!isAdmin) {
13226
13420
  var timeoutId_1 = setTimeout(function () {
@@ -37076,7 +37270,7 @@ var ChatWidgetConnected = function (props) {
37076
37270
  * Exported for use by StaticChatWidgetContainer to avoid infinite loops.
37077
37271
  */
37078
37272
  var ChatWidgetUI = function (props) {
37079
- 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;
37273
+ 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;
37080
37274
  var innerDispatch = useChatDispatch();
37081
37275
  var dispatch = useChatServerDispatch();
37082
37276
  // Publish the visual viewport's geometry so the panel can size around the soft keyboard.
@@ -37097,14 +37291,27 @@ var ChatWidgetUI = function (props) {
37097
37291
  // Preview mode can be enabled via `preview` prop (recommended) or legacy `mode="preview"`
37098
37292
  var previewMode = props.preview === true || mode === "preview";
37099
37293
  var modeClass = "widget-container--".concat(mode);
37100
- var canRefresh = (_c = (_b = props.config.header) === null || _b === void 0 ? void 0 : _b.actions) === null || _c === void 0 ? void 0 : _c.refresh;
37294
+ // Reached this page through a link in the chat.
37295
+ var arrivedViaChatLink = useArrivedViaChatLink(previewMode);
37296
+ var conversationOnScreen = hasConversation(chatState.chats);
37297
+ var chatButtonCta = 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]);
37298
+ // Only an embedded widget may drop its header. In normal mode the header holds the
37299
+ // minimize and close buttons, so hiding it would leave a floating window with no way out.
37300
+ var headerHidden = ((_c = props.config.header) === null || _c === void 0 ? void 0 : _c.hidden) === true && (dockedMode || staticMode);
37301
+ // The header is the only place a HEADER_LEFT / HEADER_RIGHT menu button renders. Without it
37302
+ // the menu would be unreachable, so hand the button to the footer instead.
37303
+ var menuConfig = useMemo(function () {
37304
+ return headerHidden && props.config.menu && props.config.menu.menuButtonLocation !== "FOOTER"
37305
+ ? __assign$1(__assign$1({}, props.config.menu), { menuButtonLocation: "FOOTER" }) : props.config.menu;
37306
+ }, [headerHidden, props.config.menu]);
37307
+ var canRefresh = (_e = (_d = props.config.header) === null || _d === void 0 ? void 0 : _d.actions) === null || _e === void 0 ? void 0 : _e.refresh;
37101
37308
  // can't minimize in docked mode, static mode, or preview mode.
37102
- 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);
37103
- 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));
37309
+ var canMinimize = !dockedMode && !staticMode && !previewMode && ((_g = (_f = props.config.header) === null || _f === void 0 ? void 0 : _f.actions) === null || _g === void 0 ? void 0 : _g.minimize);
37310
+ log("docked: ".concat(dockedMode, " static: ").concat(staticMode, " preview: ").concat(previewMode, " minimized: ").concat((_j = (_h = props.config.header) === null || _h === void 0 ? void 0 : _h.actions) === null || _j === void 0 ? void 0 : _j.minimize));
37104
37311
  var canCancel;
37105
37312
  // To preserve legacy behavior, cancel needs a little more checks
37106
- if (typeof ((_j = (_h = props.config.header) === null || _h === void 0 ? void 0 : _h.actions) === null || _j === void 0 ? void 0 : _j.cancel) === "boolean") {
37107
- 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);
37313
+ if (typeof ((_l = (_k = props.config.header) === null || _k === void 0 ? void 0 : _k.actions) === null || _l === void 0 ? void 0 : _l.cancel) === "boolean") {
37314
+ canCancel = !dockedMode && !staticMode && !previewMode && ((_o = (_m = props.config.header) === null || _m === void 0 ? void 0 : _m.actions) === null || _o === void 0 ? void 0 : _o.cancel);
37108
37315
  }
37109
37316
  else {
37110
37317
  canCancel = !dockedMode && !staticMode && !previewMode;
@@ -37115,16 +37322,16 @@ var ChatWidgetUI = function (props) {
37115
37322
  }
37116
37323
  // Our state - pull from storage
37117
37324
  // Parse autoOpenOnWidth once for use in both initial state and useEffect
37118
- var autoOpenOnWidthValue = ((_m = props.config) === null || _m === void 0 ? void 0 : _m.autoOpenOnWidth)
37325
+ var autoOpenOnWidthValue = ((_p = props.config) === null || _p === void 0 ? void 0 : _p.autoOpenOnWidth)
37119
37326
  ? parseInt(props.config.autoOpenOnWidth, 10)
37120
37327
  : NaN;
37121
37328
  var autoOpenOnWidthMatch = !isNaN(autoOpenOnWidthValue) && window.innerWidth >= autoOpenOnWidthValue;
37122
37329
  // When autoOpenOnWidth is configured, use width check instead of persisted visible state
37123
37330
  // to prevent greeting requests on viewports below the threshold
37124
- var _14 = useState((!canMinimize && !canCancel) ||
37331
+ var _15 = useState((!canMinimize && !canCancel) ||
37125
37332
  // !!get("visible") ||
37126
- (!isNaN(autoOpenOnWidthValue) ? autoOpenOnWidthMatch : chatState.visuals.visible)), visible = _14[0], setVisibleState = _14[1];
37127
- var _15 = useState(false); _15[0]; var setTypingState = _15[1]; // false initially - state kept for potential external observers
37333
+ (!isNaN(autoOpenOnWidthValue) ? autoOpenOnWidthMatch : chatState.visuals.visible)), visible = _15[0], setVisibleState = _15[1];
37334
+ var _16 = useState(false); _16[0]; var setTypingState = _16[1]; // false initially - state kept for potential external observers
37128
37335
  // Ref to track typing state for use in timeout callbacks
37129
37336
  var typingRef = useRef(false);
37130
37337
  // Timeout ref for debouncing "stop typing" events
@@ -37132,11 +37339,11 @@ var ChatWidgetUI = function (props) {
37132
37339
  // Track whether autoOpenOnWidth has already fired so it doesn't re-open after user closes
37133
37340
  var autoOpenFiredRef = useRef(autoOpenOnWidthMatch);
37134
37341
  var chatServer = useContext(ChatServerContext);
37135
- var patternsConfig = (_p = (_o = props.config) === null || _o === void 0 ? void 0 : _o.autoOpenOnPattern) === null || _p === void 0 ? void 0 : _p.patterns;
37342
+ var patternsConfig = (_r = (_q = props.config) === null || _q === void 0 ? void 0 : _q.autoOpenOnPattern) === null || _r === void 0 ? void 0 : _r.patterns;
37136
37343
  var currentUrl = window.location.href;
37137
37344
  var patternExist = patternsConfig && patternsConfig.length > 0;
37138
37345
  var patternMatches = patternsConfig === null || patternsConfig === void 0 ? void 0 : patternsConfig.some(function (pattern) { return currentUrl.includes(pattern); });
37139
- var configWidth = (_r = (_q = props.config) === null || _q === void 0 ? void 0 : _q.autoOpenOnPattern) === null || _r === void 0 ? void 0 : _r.minimumWidth;
37346
+ var configWidth = (_t = (_s = props.config) === null || _s === void 0 ? void 0 : _s.autoOpenOnPattern) === null || _t === void 0 ? void 0 : _t.minimumWidth;
37140
37347
  var currentWidth = window.innerWidth;
37141
37348
  var setVisible = useCallback(function (newVisible) {
37142
37349
  if (staticMode || previewMode) {
@@ -37171,7 +37378,7 @@ var ChatWidgetUI = function (props) {
37171
37378
  };
37172
37379
  // eslint-disable-next-line react-hooks/exhaustive-deps
37173
37380
  }, []);
37174
- var _16 = useState(!document.hidden), isTabVisible = _16[0], setIsTabVisible = _16[1];
37381
+ var _17 = useState(!document.hidden), isTabVisible = _17[0], setIsTabVisible = _17[1];
37175
37382
  useEffect(function () {
37176
37383
  var handleVisibilityChange = function () {
37177
37384
  setIsTabVisible(!document.hidden);
@@ -37395,8 +37602,8 @@ var ChatWidgetUI = function (props) {
37395
37602
  }));
37396
37603
  }
37397
37604
  // Action Bar state and handlers
37398
- var _17 = useState(false), formModalOpen = _17[0], setFormModalOpen = _17[1];
37399
- var _18 = useState(null), activeFormConfig = _18[0], setActiveFormConfig = _18[1];
37605
+ var _18 = useState(false), formModalOpen = _18[0], setFormModalOpen = _18[1];
37606
+ var _19 = useState(null), activeFormConfig = _19[0], setActiveFormConfig = _19[1];
37400
37607
  var handleFormButtonClick = useCallback(function (formButton) {
37401
37608
  // If form-widget is configured, use its openForm API
37402
37609
  // The form-widget script should already be pre-loaded
@@ -37463,7 +37670,7 @@ var ChatWidgetUI = function (props) {
37463
37670
  var messages = chatState && chatState.chats;
37464
37671
  log("Rendering - accountStatus: \"".concat(chatState.accountStatus, "\", connectionStatus: \"").concat(chatState.connection.connectionStatus, "\""));
37465
37672
  var config = props.config, onConnectionStatusChange = props.onConnectionStatusChange;
37466
- var actionBarEnabled = ((_s = config === null || config === void 0 ? void 0 : config.actionBar) === null || _s === void 0 ? void 0 : _s.enabled) === true;
37673
+ var actionBarEnabled = ((_u = config === null || config === void 0 ? void 0 : config.actionBar) === null || _u === void 0 ? void 0 : _u.enabled) === true;
37467
37674
  // The host page declares this; the widget cannot work it out. On the standalone
37468
37675
  // contact/booking page the form already *is* the page, which makes every button that
37469
37676
  // only opens one redundant. XappMedia/chat-widget#1626.
@@ -37487,7 +37694,7 @@ var ChatWidgetUI = function (props) {
37487
37694
  onConnectionStatusChange(connectionStatus);
37488
37695
  }
37489
37696
  }, [connectionStatus, onConnectionStatusChange]);
37490
- useExternalScript((_t = props.config) === null || _t === void 0 ? void 0 : _t.middlewareUrl);
37697
+ useExternalScript((_v = props.config) === null || _v === void 0 ? void 0 : _v.middlewareUrl);
37491
37698
  // Only reach for form.xapp.ai when something on this page can still open the form.
37492
37699
  //
37493
37700
  // Loading it is not free: the script mounts a second form-widget instance whose wrapper
@@ -37527,15 +37734,18 @@ var ChatWidgetUI = function (props) {
37527
37734
  };
37528
37735
  }, [actionBarVisible]);
37529
37736
  // This is a pseudo agent. It represent's the widget (shown in the header avatar for instance)
37530
- var widgetAgent = ((_u = chatState.agents["agent:robot"]) === null || _u === void 0 ? void 0 : _u.user) ||
37737
+ var widgetAgent = ((_w = chatState.agents["agent:robot"]) === null || _w === void 0 ? void 0 : _w.user) ||
37531
37738
  (config === null || config === void 0 ? void 0 : config.agent) || {
37532
37739
  nick: "agent:robot",
37533
37740
  avatarPath: config.avatarUrl,
37534
37741
  display_name: "Agent",
37535
37742
  };
37536
- return (jsxs(Fragment, { children: [jsxs("div", { className: "widget-container ".concat(modeClass, " ").concat(getVisibilityClass()).concat(actionBarVisible ? " widget-container--with-action-bar" : ""), children: [jsx(WidgetStylesheet, { theme: config === null || config === void 0 ? void 0 : config.theme }), 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 }), 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 }), jsx("div", { className: "xa-spinner-container ".concat(visible && connectionStatus === "pending" ? "visible" : ""), children: jsx("div", { className: "xa-spinner" }) }), connectionStatus === "offline" && jsx(ServerOffline, {}), chatState.wsButton && visible && (jsx(WsButton, { button: chatState.wsButton, onPress: handleWsButtonPress })), 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 }), jsx("div", { className: "restartModal", ref: modalRef, onClick: handleRestartModalCloseClick, children: jsx(ModalContent, { onClose: handleRestartModalCloseClick, onReset: handleReset }) })] }), actionBarVisible && config.actionBar ? (jsxs(Fragment, { children: [jsx(ActionBar, { config: __assign$1(__assign$1({}, config.actionBar), {
37743
+ return (jsxs(Fragment, { children: [jsxs("div", { className: "widget-container ".concat(modeClass, " ").concat(getVisibilityClass()).concat(actionBarVisible ? " widget-container--with-action-bar" : ""), children: [jsx(WidgetStylesheet, { theme: config === null || config === void 0 ? void 0 : config.theme }), !headerHidden && (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: (_z = (_y = (_x = props.config) === null || _x === void 0 ? void 0 : _x.connection) === null || _y === void 0 ? void 0 : _y.mcp) === null || _z === void 0 ? void 0 : _z.debugMode, onSubmit: handleOnSubmit })), 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: (_1 = (_0 = props.config) === null || _0 === void 0 ? void 0 : _0.typingStatus) === null || _1 === void 0 ? void 0 : _1.textTypingStatusEnabled, disableAutoScroll: props.disableAutoScroll, onSend: handleSendMessage, onWrite: handleWriteMessage, onOpenUrl: handleOpenUrl, minimizeOnClick: handleMinimizeClick }), jsx("div", { className: "xa-spinner-container ".concat(visible && connectionStatus === "pending" ? "visible" : ""), children: jsx("div", { className: "xa-spinner" }) }), connectionStatus === "offline" && jsx(ServerOffline, {}), chatState.wsButton && visible && (jsx(WsButton, { button: chatState.wsButton, onPress: handleWsButtonPress })), jsx(ChatFooter, { isAdmin: config === null || config === void 0 ? void 0 : config.isAdmin, isChatting: chatState.isChatting, placeholder: (_2 = config === null || config === void 0 ? void 0 : config.input) === null || _2 === void 0 ? void 0 : _2.placeholder, sendButtonIcon: (_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.icon, sendButtonIconHover: (_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.iconHover, sendButtonIconDisabled: (_8 = (_7 = config === null || config === void 0 ? void 0 : config.footer) === null || _7 === void 0 ? void 0 : _7.sendButton) === null || _8 === void 0 ? void 0 : _8.iconDisabled, visible: visible, hasWsButton: !!chatState.wsButton, menuConfig: menuConfig, 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 }), jsx("div", { className: "restartModal", ref: modalRef, onClick: handleRestartModalCloseClick, children: jsx(ModalContent, { onClose: handleRestartModalCloseClick, onReset: handleReset }) })] }), actionBarVisible && config.actionBar ? (jsxs(Fragment, { children: [jsx(ActionBar, { config: __assign$1(__assign$1({}, config.actionBar), {
37537
37744
  // Use actionBar.cta if set, otherwise fall back to widget-level cta
37538
- 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 && (jsx(FormModal, { config: activeFormConfig, widgetEnv: config, onClose: handleFormModalClose }))] })) : (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 })), jsx(ErrorOverlay, { enableErrorOverlay: config === null || config === void 0 ? void 0 : config.enableErrorOverlay })] }));
37745
+ cta: resolveCta((_11 = config.actionBar.cta) !== null && _11 !== void 0 ? _11 : config.cta, {
37746
+ hasConversation: conversationOnScreen,
37747
+ arrivedViaChatLink: arrivedViaChatLink,
37748
+ }) }), 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 && (jsx(FormModal, { config: activeFormConfig, widgetEnv: config, onClose: handleFormModalClose }))] })) : (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 })), jsx(ErrorOverlay, { enableErrorOverlay: config === null || config === void 0 ? void 0 : config.enableErrorOverlay })] }));
37539
37749
  };
37540
37750
  /**
37541
37751
  * Top-level wrapper that dispatches between preview mode and connected mode.