@xapp/chat-widget 1.104.0 → 1.105.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
@@ -328,7 +328,7 @@ function buildChatWidgetChannel(props) {
328
328
  // and the text on it are always a matched pair rather than one scraped
329
329
  // value and one guess.
330
330
  const primaryColor = (0, color_1.usableColorOr)(props.primaryColor, DEFAULT_PRIMARY);
331
- return Object.assign({ type: "chat-widget",
331
+ return Object.assign(Object.assign({ type: "chat-widget",
332
332
  // MCP by default. The widget runs on a public page, so homeowner mode
333
333
  // — which is unauthenticated and carries the appId in the url — is the
334
334
  // only one that can work there; business_owner needs a token, and a
@@ -339,7 +339,7 @@ function buildChatWidgetChannel(props) {
339
339
  mcp: {
340
340
  mode: "homeowner"
341
341
  }
342
- }, theme: buildChatTheme(primaryColor, props.secondaryColor), actionBar: buildActionBar({ apiUrl, formWidgetKey, phoneNumber }) }, (businessName ? { botName: businessName } : {}));
342
+ }, theme: buildChatTheme(primaryColor, props.secondaryColor), actionBar: buildActionBar({ apiUrl, formWidgetKey, phoneNumber }) }, (formWidgetKey ? { formWidgetKey } : {})), (businessName ? { botName: businessName } : {}));
343
343
  }
344
344
  /**
345
345
  * The dock of buttons that replaces the plain chat bubble.
@@ -3870,6 +3870,31 @@ function useVisualViewportProperties(enabled) {
3870
3870
  return trackVisualViewport();
3871
3871
  }, [enabled]);
3872
3872
  }
3873
+ /** The sessionStorage key the booking page writes the flag to. */
3874
+ var TEST_LEAD_STORAGE_KEY = "xa_test";
3875
+ /** The attribute sent with a lead submission. */
3876
+ var TEST_LEAD_ATTRIBUTE = "xaTest";
3877
+ /** True when the current browser session is a test-lead page view. */
3878
+ function isTestLeadSession() {
3879
+ try {
3880
+ return sessionStorage.getItem(TEST_LEAD_STORAGE_KEY) === "1";
3881
+ }
3882
+ catch (_a) {
3883
+ // Storage can be blocked by the browser; treat that as a real visit.
3884
+ return false;
3885
+ }
3886
+ }
3887
+ /**
3888
+ * Tags the attributes as a test lead when the session flag is set. A test must
3889
+ * never be counted as Google traffic, so any `rwg_token` is dropped as well.
3890
+ */
3891
+ function addTestLeadAttribute(attributes) {
3892
+ if (isTestLeadSession()) {
3893
+ attributes[TEST_LEAD_ATTRIBUTE] = true;
3894
+ delete attributes["rwg_token"];
3895
+ }
3896
+ return attributes;
3897
+ }
3873
3898
 
3874
3899
  /**
3875
3900
  * Quicker configurable uuid
@@ -3893,6 +3918,7 @@ function uuid() {
3893
3918
  var IconButton_1 = IconButton;
3894
3919
  var RichInput_1 = RichInput;
3895
3920
  var SuggestionsList_1 = SuggestionsList;
3921
+ var addTestLeadAttribute_1 = addTestLeadAttribute;
3896
3922
  var findItemByIndex_1 = findItemByIndex;
3897
3923
  var getAttribution_1 = getAttribution;
3898
3924
  var getItemsLength_1 = getItemsLength;
@@ -6362,6 +6388,8 @@ var StentorDirectChat = /** @class */ (function () {
6362
6388
  if (origin) {
6363
6389
  attributes["origin"] = origin;
6364
6390
  }
6391
+ // A test lead from Studio's "send yourself a test lead" booking page
6392
+ addTestLeadAttribute_1(attributes);
6365
6393
  // Add current time for accurate date/time responses
6366
6394
  attributes["currentTime"] = new Date().toISOString();
6367
6395
  now = new Date().getTime();
@@ -7590,6 +7618,8 @@ var StentorRouterChat = /** @class */ (function () {
7590
7618
  if (origin) {
7591
7619
  attributes["origin"] = origin;
7592
7620
  }
7621
+ // A test lead from Studio's "send yourself a test lead" booking page
7622
+ addTestLeadAttribute_1(attributes);
7593
7623
  // Add current time for accurate date/time responses
7594
7624
  attributes["currentTime"] = new Date().toISOString();
7595
7625
  request = requestFromMessage(message, userId, this.isNewSession, sessionId, accessToken, attributes, this.visitorInfo);
@@ -37292,12 +37322,25 @@ var ChatWidgetConnected = function (props) {
37292
37322
  }
37293
37323
  return (jsx(ChatConfigContext.Provider, { value: config, children: jsx(ChatServerContext.Provider, { value: chatServer, children: jsx(ChatWidgetUI, __assign$1({}, props, { config: rawConfig })) }) }));
37294
37324
  };
37325
+ var HOST_FORM_WIDGET_CHECK_MS = 500;
37326
+ /** About 15s: long enough for a slow host script, short enough not to poll forever. */
37327
+ var HOST_FORM_WIDGET_MAX_CHECKS = 30;
37328
+ /** The form widget key a Form/Book button carries on its own formConfig, if any. */
37329
+ function formButtonKey(button) {
37330
+ var _a, _b;
37331
+ return opensFormWidget(button)
37332
+ ? ((_b = (_a = button.formConfig) === null || _a === void 0 ? void 0 : _a.api) === null || _b === void 0 ? void 0 : _b.key) || undefined
37333
+ : undefined;
37334
+ }
37335
+ function isHostFormWidgetOnPage() {
37336
+ return isFormWidgetScriptLoaded() || isFormWidgetReady();
37337
+ }
37295
37338
  /**
37296
37339
  * Inner ChatWidget component that renders the actual UI.
37297
37340
  * Exported for use by StaticChatWidgetContainer to avoid infinite loops.
37298
37341
  */
37299
37342
  var ChatWidgetUI = function (props) {
37300
- 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, _15, _16, _17, _18, _19, _20;
37343
+ 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, _15, _16, _17, _18, _19, _20, _21, _22, _23;
37301
37344
  var innerDispatch = useChatDispatch();
37302
37345
  var dispatch = useChatServerDispatch();
37303
37346
  // Publish the visual viewport's geometry so the panel can size around the soft keyboard.
@@ -37328,21 +37371,21 @@ var ChatWidgetUI = function (props) {
37328
37371
  var popOutEnabled = dockedMode && ((_c = props.config.popOut) === null || _c === void 0 ? void 0 : _c.enabled) === true;
37329
37372
  var popOutMinWidth = (_e = (_d = props.config.popOut) === null || _d === void 0 ? void 0 : _d.minWidth) !== null && _e !== void 0 ? _e : DEFAULT_POP_OUT_MIN_WIDTH;
37330
37373
  var anchorRef = useRef(null);
37331
- var _21 = useState(false), anchorOffScreen = _21[0], setAnchorOffScreen = _21[1];
37374
+ var _24 = useState(false), anchorOffScreen = _24[0], setAnchorOffScreen = _24[1];
37332
37375
  // A returning visitor's chat often sits below the fold at load. Following them before they
37333
37376
  // have even reached it would put a floating window in front of someone who never scrolled
37334
37377
  // past anything, so the chat has to have been on screen once first.
37335
- var _22 = useState(false), anchorSeen = _22[0], setAnchorSeen = _22[1];
37378
+ var _25 = useState(false), anchorSeen = _25[0], setAnchorSeen = _25[1];
37336
37379
  // Set when the visitor puts the floating window away: it stays put until their own
37337
37380
  // scrolling brings the chat's place on the page back into view.
37338
- var _23 = useState(false), popOutDismissed = _23[0], setPopOutDismissed = _23[1];
37381
+ var _26 = useState(false), popOutDismissed = _26[0], setPopOutDismissed = _26[1];
37339
37382
  // On a phone the chat offers its entry point rather than covering the page; this is set when
37340
37383
  // the visitor taps that and asks for the conversation.
37341
- var _24 = useState(false), mobileOpened = _24[0], setMobileOpened = _24[1];
37384
+ var _27 = useState(false), mobileOpened = _27[0], setMobileOpened = _27[1];
37342
37385
  // Tracked rather than read at render: a visitor who narrows the window or rotates a tablet
37343
37386
  // while the chat is popped out would otherwise keep a floating window over most of a phone
37344
37387
  // screen until some unrelated re-render happened along.
37345
- var _25 = useState(typeof window === "undefined" ? 0 : window.innerWidth), viewportWidth = _25[0], setViewportWidth = _25[1];
37388
+ var _28 = useState(typeof window === "undefined" ? 0 : window.innerWidth), viewportWidth = _28[0], setViewportWidth = _28[1];
37346
37389
  useEffect(function () {
37347
37390
  if (!popOutEnabled || typeof window === "undefined") {
37348
37391
  return undefined;
@@ -37508,10 +37551,10 @@ var ChatWidgetUI = function (props) {
37508
37551
  var autoOpenOnWidthMatch = !isNaN(autoOpenOnWidthValue) && window.innerWidth >= autoOpenOnWidthValue;
37509
37552
  // When autoOpenOnWidth is configured, use width check instead of persisted visible state
37510
37553
  // to prevent greeting requests on viewports below the threshold
37511
- var _26 = useState((!canMinimize && !canCancel) ||
37554
+ var _29 = useState((!canMinimize && !canCancel) ||
37512
37555
  // !!get("visible") ||
37513
- (!isNaN(autoOpenOnWidthValue) ? autoOpenOnWidthMatch : chatState.visuals.visible)), visible = _26[0], setVisibleState = _26[1];
37514
- var _27 = useState(false); _27[0]; var setTypingState = _27[1]; // false initially - state kept for potential external observers
37556
+ (!isNaN(autoOpenOnWidthValue) ? autoOpenOnWidthMatch : chatState.visuals.visible)), visible = _29[0], setVisibleState = _29[1];
37557
+ var _30 = useState(false); _30[0]; var setTypingState = _30[1]; // false initially - state kept for potential external observers
37515
37558
  // Ref to track typing state for use in timeout callbacks
37516
37559
  var typingRef = useRef(false);
37517
37560
  // Timeout ref for debouncing "stop typing" events
@@ -37558,7 +37601,7 @@ var ChatWidgetUI = function (props) {
37558
37601
  };
37559
37602
  // eslint-disable-next-line react-hooks/exhaustive-deps
37560
37603
  }, []);
37561
- var _28 = useState(!document.hidden), isTabVisible = _28[0], setIsTabVisible = _28[1];
37604
+ var _31 = useState(!document.hidden), isTabVisible = _31[0], setIsTabVisible = _31[1];
37562
37605
  useEffect(function () {
37563
37606
  var handleVisibilityChange = function () {
37564
37607
  setIsTabVisible(!document.hidden);
@@ -37782,8 +37825,8 @@ var ChatWidgetUI = function (props) {
37782
37825
  }));
37783
37826
  }
37784
37827
  // Action Bar state and handlers
37785
- var _29 = useState(false), formModalOpen = _29[0], setFormModalOpen = _29[1];
37786
- var _30 = useState(null), activeFormConfig = _30[0], setActiveFormConfig = _30[1];
37828
+ var _32 = useState(false), formModalOpen = _32[0], setFormModalOpen = _32[1];
37829
+ var _33 = useState(null), activeFormConfig = _33[0], setActiveFormConfig = _33[1];
37787
37830
  var handleFormButtonClick = useCallback(function (formButton) {
37788
37831
  // If form-widget is configured, use its openForm API
37789
37832
  // The form-widget script should already be pre-loaded
@@ -37855,14 +37898,51 @@ var ChatWidgetUI = function (props) {
37855
37898
  // contact/booking page the form already *is* the page, which makes every button that
37856
37899
  // only opens one redundant. XappMedia/chat-widget#1626.
37857
37900
  var isFormPage = useIsFormPage();
37901
+ // Studio can save Form/Book buttons with no form widget key anywhere. They can only open the
37902
+ // FormModal fallback, which then says "Form widget key not configured" - so drop them, unless
37903
+ // the host page installed the form widget itself. One key anywhere is enough: a page has one
37904
+ // form widget and every Form/Book button opens it through the same global.
37905
+ var configuredButtons = (_2 = (_1 = config === null || config === void 0 ? void 0 : config.actionBar) === null || _1 === void 0 ? void 0 : _1.buttons) !== null && _2 !== void 0 ? _2 : [];
37906
+ var hasKeylessFormButton = !(config === null || config === void 0 ? void 0 : config.formWidgetKey) &&
37907
+ configuredButtons.some(opensFormWidget) &&
37908
+ !configuredButtons.some(formButtonKey);
37909
+ var _34 = useState(function () { return hasKeylessFormButton && isHostFormWidgetOnPage(); }), hostFormWidget = _34[0], setHostFormWidget = _34[1];
37910
+ // The host's form-widget script loads independently of ours and can arrive after the first
37911
+ // render, so keep looking for a while rather than deciding once. This only runs when there is
37912
+ // no key anywhere, and with no key we never preload - so the script this finds by its fixed id
37913
+ // cannot be our own.
37914
+ useEffect(function () {
37915
+ if (!hasKeylessFormButton || hostFormWidget) {
37916
+ return undefined;
37917
+ }
37918
+ var checks = 0;
37919
+ var interval = setInterval(function () {
37920
+ checks++;
37921
+ if (isHostFormWidgetOnPage()) {
37922
+ setHostFormWidget(true);
37923
+ clearInterval(interval);
37924
+ }
37925
+ else if (checks >= HOST_FORM_WIDGET_MAX_CHECKS) {
37926
+ clearInterval(interval);
37927
+ }
37928
+ }, HOST_FORM_WIDGET_CHECK_MS);
37929
+ return function () { return clearInterval(interval); };
37930
+ }, [hasKeylessFormButton, hostFormWidget]);
37931
+ var actionBarConfig = useMemo(function () {
37932
+ var actionBar = config === null || config === void 0 ? void 0 : config.actionBar;
37933
+ if (!actionBar || !hasKeylessFormButton || hostFormWidget) {
37934
+ return actionBar;
37935
+ }
37936
+ return __assign$1(__assign$1({}, actionBar), { buttons: actionBar.buttons.filter(function (button) { return !opensFormWidget(button); }) });
37937
+ }, [config === null || config === void 0 ? void 0 : config.actionBar, hasKeylessFormButton, hostFormWidget]);
37858
37938
  var actionBarButtons = useMemo(function () {
37859
- return actionBarEnabled && (config === null || config === void 0 ? void 0 : config.actionBar)
37860
- ? visibleActionBarButtons(config.actionBar.buttons, {
37939
+ return actionBarEnabled && actionBarConfig
37940
+ ? visibleActionBarButtons(actionBarConfig.buttons, {
37861
37941
  chatDisabled: config.disabled,
37862
37942
  formDisabled: isFormPage,
37863
37943
  })
37864
37944
  : [];
37865
- }, [actionBarEnabled, config === null || config === void 0 ? void 0 : config.actionBar, config === null || config === void 0 ? void 0 : config.disabled, isFormPage]);
37945
+ }, [actionBarEnabled, actionBarConfig, config === null || config === void 0 ? void 0 : config.disabled, isFormPage]);
37866
37946
  // A bar that filters down to nothing is a bare strip of chrome. Treat it as if the bar
37867
37947
  // were off, so the visitor still gets the plain chat button rather than an empty shell.
37868
37948
  var actionBarVisible = actionBarEnabled && actionBarButtons.length > 0;
@@ -37874,7 +37954,7 @@ var ChatWidgetUI = function (props) {
37874
37954
  onConnectionStatusChange(connectionStatus);
37875
37955
  }
37876
37956
  }, [connectionStatus, onConnectionStatusChange]);
37877
- useExternalScript((_1 = props.config) === null || _1 === void 0 ? void 0 : _1.middlewareUrl);
37957
+ useExternalScript((_3 = props.config) === null || _3 === void 0 ? void 0 : _3.middlewareUrl);
37878
37958
  // Only reach for form.xapp.ai when something on this page can still open the form.
37879
37959
  //
37880
37960
  // Loading it is not free: the script mounts a second form-widget instance whose wrapper
@@ -37884,11 +37964,16 @@ var ChatWidgetUI = function (props) {
37884
37964
  //
37885
37965
  // With no bar configured the side button IS the entry point, so that case still loads.
37886
37966
  var formWidgetNeeded = !isFormPage && (!actionBarEnabled || actionBarButtons.some(opensFormWidget));
37967
+ // The top-level key is the documented place for it, but an App can carry the key only on
37968
+ // a Form/Book button's formConfig - buildChatWidgetChannel did exactly that. Reading only
37969
+ // the top-level key left the script unloaded, so every click waited out
37970
+ // openFormWidgetWhenReady before the modal fallback found the button's key.
37971
+ var formWidgetKey = (_4 = config === null || config === void 0 ? void 0 : config.formWidgetKey) !== null && _4 !== void 0 ? _4 : actionBarButtons.map(formButtonKey).find(Boolean);
37887
37972
  useEffect(function () {
37888
- if ((config === null || config === void 0 ? void 0 : config.formWidgetKey) && formWidgetNeeded) {
37889
- preloadFormWidgetScript(config.formWidgetKey);
37973
+ if (formWidgetKey && formWidgetNeeded) {
37974
+ preloadFormWidgetScript(formWidgetKey);
37890
37975
  }
37891
- }, [config === null || config === void 0 ? void 0 : config.formWidgetKey, formWidgetNeeded]);
37976
+ }, [formWidgetKey, formWidgetNeeded]);
37892
37977
  // Set body attribute when actionBar is enabled (used by CSS to hide form-widget side button on mobile)
37893
37978
  // Uses reference counting to handle multiple widget instances
37894
37979
  useEffect(function () {
@@ -37914,23 +37999,23 @@ var ChatWidgetUI = function (props) {
37914
37999
  };
37915
38000
  }, [actionBarVisible]);
37916
38001
  // This is a pseudo agent. It represent's the widget (shown in the header avatar for instance)
37917
- var widgetAgent = ((_2 = chatState.agents["agent:robot"]) === null || _2 === void 0 ? void 0 : _2.user) ||
38002
+ var widgetAgent = ((_5 = chatState.agents["agent:robot"]) === null || _5 === void 0 ? void 0 : _5.user) ||
37918
38003
  (config === null || config === void 0 ? void 0 : config.agent) || {
37919
38004
  nick: "agent:robot",
37920
38005
  avatarPath: config.avatarUrl,
37921
38006
  display_name: "Agent",
37922
38007
  };
37923
- var widgetContainer = (jsxs("div", { className: "widget-container ".concat(modeClass, " ").concat(getVisibilityClass()).concat(actionBarVisible ? " widget-container--with-action-bar" : "").concat(poppedOut ? " widget-container--popped" : "").concat(collapsedOnMobile ? " widget-container--collapsed" : ""), children: [jsx(WidgetStylesheet, { theme: config === null || config === void 0 ? void 0 : config.theme }), !headerHidden && (jsx(ChatHeader, { accountStatus: chatState.accountStatus, refreshOnClick: handleRestartClick, minimizeOnClick: poppedOut ? handlePopOutDismiss : handleMinimizeClick, cancelOnClick: handleCancelClick, agent: widgetAgent, canRefresh: canRefresh, canMinimize: poppedOut || canMinimize, canCancel: canCancel, config: config === null || config === void 0 ? void 0 : config.header, menuConfig: config.menu, debugMode: (_5 = (_4 = (_3 = props.config) === null || _3 === void 0 ? void 0 : _3.connection) === null || _4 === void 0 ? void 0 : _4.mcp) === null || _5 === void 0 ? void 0 : _5.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: (_7 = (_6 = props.config) === null || _6 === void 0 ? void 0 : _6.typingStatus) === null || _7 === void 0 ? void 0 : _7.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: (_8 = config === null || config === void 0 ? void 0 : config.input) === null || _8 === void 0 ? void 0 : _8.placeholder, sendButtonIcon: (_10 = (_9 = config === null || config === void 0 ? void 0 : config.footer) === null || _9 === void 0 ? void 0 : _9.sendButton) === null || _10 === void 0 ? void 0 : _10.icon, sendButtonIconHover: (_12 = (_11 = config === null || config === void 0 ? void 0 : config.footer) === null || _11 === void 0 ? void 0 : _11.sendButton) === null || _12 === void 0 ? void 0 : _12.iconHover, sendButtonIconDisabled: (_14 = (_13 = config === null || config === void 0 ? void 0 : config.footer) === null || _13 === void 0 ? void 0 : _13.sendButton) === null || _14 === void 0 ? void 0 : _14.iconDisabled, visible: visible, hasWsButton: !!chatState.wsButton, menuConfig: menuConfig, footerConfig: (_15 = props.config) === null || _15 === void 0 ? void 0 : _15.footer, inputConfig: (_16 = props.config) === null || _16 === void 0 ? void 0 : _16.input, disabled: previewMode, onChange: handleOnChange, onSubmit: handleOnSubmit, onFileUpload: handleFileUpload }), jsx("div", { className: "restartModal", ref: modalRef, onClick: handleRestartModalCloseClick, children: jsx(ModalContent, { onClose: handleRestartModalCloseClick, onReset: handleReset }) })] }));
38008
+ var widgetContainer = (jsxs("div", { className: "widget-container ".concat(modeClass, " ").concat(getVisibilityClass()).concat(actionBarVisible ? " widget-container--with-action-bar" : "").concat(poppedOut ? " widget-container--popped" : "").concat(collapsedOnMobile ? " widget-container--collapsed" : ""), children: [jsx(WidgetStylesheet, { theme: config === null || config === void 0 ? void 0 : config.theme }), !headerHidden && (jsx(ChatHeader, { accountStatus: chatState.accountStatus, refreshOnClick: handleRestartClick, minimizeOnClick: poppedOut ? handlePopOutDismiss : handleMinimizeClick, cancelOnClick: handleCancelClick, agent: widgetAgent, canRefresh: canRefresh, canMinimize: poppedOut || canMinimize, canCancel: canCancel, config: config === null || config === void 0 ? void 0 : config.header, menuConfig: config.menu, debugMode: (_8 = (_7 = (_6 = props.config) === null || _6 === void 0 ? void 0 : _6.connection) === null || _7 === void 0 ? void 0 : _7.mcp) === null || _8 === void 0 ? void 0 : _8.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: (_10 = (_9 = props.config) === null || _9 === void 0 ? void 0 : _9.typingStatus) === null || _10 === void 0 ? void 0 : _10.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: (_11 = config === null || config === void 0 ? void 0 : config.input) === null || _11 === void 0 ? void 0 : _11.placeholder, sendButtonIcon: (_13 = (_12 = config === null || config === void 0 ? void 0 : config.footer) === null || _12 === void 0 ? void 0 : _12.sendButton) === null || _13 === void 0 ? void 0 : _13.icon, sendButtonIconHover: (_15 = (_14 = config === null || config === void 0 ? void 0 : config.footer) === null || _14 === void 0 ? void 0 : _14.sendButton) === null || _15 === void 0 ? void 0 : _15.iconHover, sendButtonIconDisabled: (_17 = (_16 = config === null || config === void 0 ? void 0 : config.footer) === null || _16 === void 0 ? void 0 : _16.sendButton) === null || _17 === void 0 ? void 0 : _17.iconDisabled, visible: visible, hasWsButton: !!chatState.wsButton, menuConfig: menuConfig, footerConfig: (_18 = props.config) === null || _18 === void 0 ? void 0 : _18.footer, inputConfig: (_19 = props.config) === null || _19 === void 0 ? void 0 : _19.input, disabled: previewMode, onChange: handleOnChange, onSubmit: handleOnSubmit, onFileUpload: handleFileUpload }), jsx("div", { className: "restartModal", ref: modalRef, onClick: handleRestartModalCloseClick, children: jsx(ModalContent, { onClose: handleRestartModalCloseClick, onReset: handleReset }) })] }));
37924
38009
  return (jsxs(Fragment, { children: [popOutEnabled ? (
37925
38010
  // The anchor keeps the chat's place in the page, so the layout does not jump when
37926
38011
  // the chat pops out. The chat itself lives in popOutHost, which the effect above
37927
38012
  // moves between this anchor and the body.
37928
- jsx("div", { className: "widget-anchor", ref: anchorRef, children: popOutHost && createPortal(widgetContainer, popOutHost) })) : (widgetContainer), actionBarVisible && config.actionBar && !(collapsedOnMobile && mobileEntry === "button") ? (jsxs(Fragment, { children: [jsx(ActionBar, { config: __assign$1(__assign$1({}, config.actionBar), {
38013
+ jsx("div", { className: "widget-anchor", ref: anchorRef, children: popOutHost && createPortal(widgetContainer, popOutHost) })) : (widgetContainer), actionBarVisible && actionBarConfig && !(collapsedOnMobile && mobileEntry === "button") ? (jsxs(Fragment, { children: [jsx(ActionBar, { config: __assign$1(__assign$1({}, actionBarConfig), {
37929
38014
  // Use actionBar.cta if set, otherwise fall back to widget-level cta
37930
- cta: resolveCta((_17 = config.actionBar.cta) !== null && _17 !== void 0 ? _17 : config.cta, {
38015
+ cta: resolveCta((_20 = actionBarConfig.cta) !== null && _20 !== void 0 ? _20 : config.cta, {
37931
38016
  hasConversation: conversationOnScreen,
37932
38017
  arrivedViaChatLink: arrivedViaChatLink,
37933
- }) }), visible: collapsedOnMobile ? false : visible, chatDisabled: config.disabled, formDisabled: isFormPage, hasUserInteracted: (_18 = chatState.visuals) === null || _18 === void 0 ? void 0 : _18.hasInteracted, onChatClick: collapsedOnMobile ? handleMobileOpen : chatButtonOnClick, onChatMinimize: handleMinimizeClick, onFormClick: handleFormButtonClick, onCtaDismiss: handleCtaDismiss }), formModalOpen && activeFormConfig && (jsx(FormModal, { config: activeFormConfig, widgetEnv: config, onClose: handleFormModalClose }))] })) : (jsx(ChatButton, { addClass: visible && !collapsedOnMobile ? "visible" : "", onClick: collapsedOnMobile ? handleMobileOpen : chatButtonOnClick, config: chatButtonCta, imageUrl: (_19 = config === null || config === void 0 ? void 0 : config.chatButton) === null || _19 === void 0 ? void 0 : _19.imageUrl, visible: collapsedOnMobile ? false : visible, hasInteracted: (_20 = chatState.visuals) === null || _20 === void 0 ? void 0 : _20.hasInteracted, onCtaDismiss: handleCtaDismiss })), jsx(ErrorOverlay, { enableErrorOverlay: config === null || config === void 0 ? void 0 : config.enableErrorOverlay })] }));
38018
+ }) }), visible: collapsedOnMobile ? false : visible, chatDisabled: config.disabled, formDisabled: isFormPage, hasUserInteracted: (_21 = chatState.visuals) === null || _21 === void 0 ? void 0 : _21.hasInteracted, onChatClick: collapsedOnMobile ? handleMobileOpen : chatButtonOnClick, onChatMinimize: handleMinimizeClick, onFormClick: handleFormButtonClick, onCtaDismiss: handleCtaDismiss }), formModalOpen && activeFormConfig && (jsx(FormModal, { config: activeFormConfig, widgetEnv: config, onClose: handleFormModalClose }))] })) : (jsx(ChatButton, { addClass: visible && !collapsedOnMobile ? "visible" : "", onClick: collapsedOnMobile ? handleMobileOpen : chatButtonOnClick, config: chatButtonCta, imageUrl: (_22 = config === null || config === void 0 ? void 0 : config.chatButton) === null || _22 === void 0 ? void 0 : _22.imageUrl, visible: collapsedOnMobile ? false : visible, hasInteracted: (_23 = chatState.visuals) === null || _23 === void 0 ? void 0 : _23.hasInteracted, onCtaDismiss: handleCtaDismiss })), jsx(ErrorOverlay, { enableErrorOverlay: config === null || config === void 0 ? void 0 : config.enableErrorOverlay })] }));
37934
38019
  };
37935
38020
  /**
37936
38021
  * Top-level wrapper that dispatches between preview mode and connected mode.