@eshal-bot/chat-widget 0.1.101 → 0.1.103

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.
@@ -1 +1 @@
1
- export{S as default}from"./chat-widget-index-B5VwIIEZ.esm.js";import"react";import"react-dom/client";import"react-dom";import"react/jsx-runtime";
1
+ export{S as default}from"./chat-widget-index-2LNAzlMS.esm.js";import"react";import"react-dom/client";import"react-dom";import"react/jsx-runtime";
@@ -8183,6 +8183,10 @@
8183
8183
  // (the full follow-up question) on click. Kept here or it's whitelisted
8184
8184
  // out before render; click handlers fall back to `text` when absent.
8185
8185
  submitText: typeof p.submitText === 'string' ? p.submitText : undefined,
8186
+ // EV-2979: the server mints a per-chip promptId so click rows can
8187
+ // be joined to their impression row. This whitelist would strip it
8188
+ // otherwise. Optional - older payloads simply have none.
8189
+ promptId: typeof p.promptId === 'string' ? p.promptId : undefined,
8186
8190
  confidence: typeof p.confidence === 'number' ? p.confidence : undefined
8187
8191
  })).filter(p => p.text);
8188
8192
  return normalized.length ? normalized : undefined;
@@ -17252,23 +17256,36 @@
17252
17256
  scrollbarWidth: "none",
17253
17257
  msOverflowStyle: "none"
17254
17258
  },
17255
- children: suggestions.map((p, index) => /*#__PURE__*/jsxRuntimeExports.jsx("button", {
17256
- type: "button",
17257
- onClick: () =>
17258
- // EV-2842: pass the prompt object + position so the click
17259
- // handler can record telemetry; extra args are ignored by
17260
- // handlers that only read the submit value.
17261
- onSuggestionClick(p.submitText || p.text || p, p, index),
17262
- dir: "auto",
17263
- className: "inline-flex items-center rounded-full px-4 py-1.5 whitespace-nowrap flex-shrink-0 border-0 transition-transform duration-150 active:scale-95",
17264
- style: {
17265
- background: "linear-gradient(135deg, ".concat(promptPrimary, " 0%, ").concat(promptPrimary, "dd 100%)"),
17266
- color: promptTextColor,
17267
- fontFamily: promptFontFamily,
17268
- fontSize: promptFontSize
17269
- },
17270
- children: getPromptDisplayText(p, questionDisplay)
17271
- }, "".concat(p.text || p, "-").concat(index)))
17259
+ children: suggestions.map((p, index) => {
17260
+ // EV-2993: the chip follows the direction of the string it
17261
+ // actually DISPLAYS - which under EV-2977 is the natural
17262
+ // question (submitText), not the title-case label - falling
17263
+ // back to the configured widget direction only when the text
17264
+ // itself is direction-neutral ("123", a lone "?"). `dir="auto"`
17265
+ // alone was not enough: it hard-defaults neutral text to LTR
17266
+ // and it leaves `text-align` on the container's direction.
17267
+ const displayText = getPromptDisplayText(p, questionDisplay);
17268
+ const chipIsRtl = detectTextDirection(displayText, isRtl);
17269
+ return /*#__PURE__*/jsxRuntimeExports.jsx("button", {
17270
+ type: "button",
17271
+ onClick: () =>
17272
+ // EV-2842: pass the prompt object + position so the click
17273
+ // handler can record telemetry; extra args are ignored by
17274
+ // handlers that only read the submit value.
17275
+ onSuggestionClick(p.submitText || p.text || p, p, index),
17276
+ dir: chipIsRtl ? "rtl" : "ltr",
17277
+ className: "inline-flex items-center rounded-full px-4 py-1.5 whitespace-nowrap flex-shrink-0 border-0 transition-transform duration-150 active:scale-95 ".concat(chipIsRtl ? "text-right" : "text-left"),
17278
+ style: {
17279
+ direction: chipIsRtl ? "rtl" : "ltr",
17280
+ textAlign: chipIsRtl ? "right" : "left",
17281
+ background: "linear-gradient(135deg, ".concat(promptPrimary, " 0%, ").concat(promptPrimary, "dd 100%)"),
17282
+ color: promptTextColor,
17283
+ fontFamily: promptFontFamily,
17284
+ fontSize: promptFontSize
17285
+ },
17286
+ children: displayText
17287
+ }, "".concat(p.text || p, "-").concat(index));
17288
+ })
17272
17289
  }), /*#__PURE__*/jsxRuntimeExports.jsx("button", {
17273
17290
  type: "button",
17274
17291
  onClick: () => scroll("right"),
@@ -18799,6 +18816,162 @@
18799
18816
  });
18800
18817
  };
18801
18818
 
18819
+ /**
18820
+ * EV-2979 - auto-prompt chip CLICK telemetry (client half).
18821
+ *
18822
+ * Impressions are logged server-side at the chip-generation call sites;
18823
+ * clicks can only be observed on the client, so this module is the last
18824
+ * link in the funnel. It posts to the Express support proxy at
18825
+ * `POST /api/v1/support/chip-click`, which re-derives the organization
18826
+ * from the verified widget auth (EV-2388 - the client MUST NOT send an
18827
+ * organizationId) and forwards to knowledge-api.
18828
+ *
18829
+ * Hard rules, in priority order over "did the event land":
18830
+ * 1. Never block, delay or break the chip's real action. Callers must
18831
+ * not await this; every failure path resolves to `false` and never
18832
+ * throws.
18833
+ * 2. Never send PII beyond what the chip itself already contains
18834
+ * (its label, its submit text, its position, the conversation id).
18835
+ * 3. Be a silent no-op when there is nothing safe to send: no API base
18836
+ * URL, no conversation id, or telemetry disabled by config.
18837
+ *
18838
+ * Transport: `navigator.sendBeacon` is preferred because the browser
18839
+ * keeps it in flight across the re-render (and even a navigation) that
18840
+ * a chip click triggers. sendBeacon cannot set request headers, so the
18841
+ * in-app Bearer path (`getAuthToken` present, i.e. session auth) falls
18842
+ * back to `fetch(..., { keepalive: true })`, which has the same
18843
+ * survive-the-unload property but can carry Authorization. Mode B
18844
+ * embeds have no token - the HMAC signature is added by the
18845
+ * @eshal-bot/server proxy - so those take the sendBeacon path.
18846
+ */
18847
+
18848
+ const CHIP_CLICK_PATH = '/api/v1/support/chip-click';
18849
+
18850
+ /**
18851
+ * Builds the wire payload for a chip click.
18852
+ *
18853
+ * `chipText` is deliberately the chip's SHORT LABEL (`text`), not the
18854
+ * displayed question (`submitText`): impression rows store `chip_text`
18855
+ * from `text`, so using the label is what makes clicks joinable to
18856
+ * impressions. EV-2977 changed only what the chip DISPLAYS.
18857
+ *
18858
+ * @param {{chip?: object|string, submitText?: string, position?: number,
18859
+ * conversationId?: string, source?: string}} args
18860
+ * @returns {object|null} payload, or null when there is no usable chipText
18861
+ */
18862
+ function buildChipClickPayload() {
18863
+ let {
18864
+ chip,
18865
+ submitText,
18866
+ position,
18867
+ conversationId,
18868
+ source = 'widget'
18869
+ } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
18870
+ const isObject = chip && typeof chip === 'object';
18871
+ const label = isObject && typeof chip.text === 'string' ? chip.text : '';
18872
+ const submit = typeof submitText === 'string' && submitText ? submitText : isObject && typeof chip.submitText === 'string' ? chip.submitText : typeof chip === 'string' ? chip : '';
18873
+ const chipText = label || submit;
18874
+ if (!chipText) return null;
18875
+ const payload = {
18876
+ conversationId,
18877
+ chipText,
18878
+ submitText: submit || undefined,
18879
+ position: Number.isInteger(position) ? position : undefined,
18880
+ source
18881
+ };
18882
+
18883
+ // The chip payload is growing a `promptId` concurrently. Read it
18884
+ // defensively and simply omit the key until it exists - this must
18885
+ // never become a hard dependency.
18886
+ const promptId = isObject ? chip === null || chip === void 0 ? void 0 : chip.promptId : undefined;
18887
+ if (typeof promptId === 'string' && promptId) payload.promptId = promptId;
18888
+ return payload;
18889
+ }
18890
+
18891
+ /**
18892
+ * Fire-and-forget chip-click beacon.
18893
+ *
18894
+ * @param {object} args
18895
+ * @param {string} [args.apiBaseUrl] Support API base (same one /csat uses).
18896
+ * @param {string} [args.conversationId] Required; no conversation = no event.
18897
+ * @param {object|string} [args.chip] The chip object from the suggestions row.
18898
+ * @param {string} [args.submitText] What the click submits.
18899
+ * @param {number} [args.position] 0-based index within the strip.
18900
+ * @param {string} [args.source] Surface id, defaults to "widget".
18901
+ * @param {Function} [args.getAuthToken] Optional async () => token (in-app path).
18902
+ * @param {boolean} [args.enabled] Config kill-switch, defaults to true.
18903
+ * @returns {Promise<boolean>} true only if a request was actually dispatched.
18904
+ */
18905
+ async function sendChipClickBeacon() {
18906
+ let {
18907
+ apiBaseUrl,
18908
+ conversationId,
18909
+ chip,
18910
+ submitText,
18911
+ position,
18912
+ source = 'widget',
18913
+ getAuthToken,
18914
+ enabled = true
18915
+ } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
18916
+ try {
18917
+ if (enabled === false) return false;
18918
+ if (!apiBaseUrl || typeof apiBaseUrl !== 'string') return false;
18919
+ if (!conversationId || typeof conversationId !== 'string') return false;
18920
+ const payload = buildChipClickPayload({
18921
+ chip,
18922
+ submitText,
18923
+ position,
18924
+ conversationId,
18925
+ source
18926
+ });
18927
+ if (!payload) return false;
18928
+ const url = "".concat(apiBaseUrl.replace(/\/$/, '')).concat(CHIP_CLICK_PATH);
18929
+ const body = JSON.stringify(payload);
18930
+ let token;
18931
+ if (typeof getAuthToken === 'function') {
18932
+ try {
18933
+ token = await getAuthToken();
18934
+ } catch (_unused) {
18935
+ token = undefined;
18936
+ }
18937
+ }
18938
+
18939
+ // No Authorization header needed -> sendBeacon, the most
18940
+ // re-render/unload-proof transport available.
18941
+ if (!token && typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function' && typeof Blob !== 'undefined') {
18942
+ try {
18943
+ const queued = navigator.sendBeacon(url, new Blob([body], {
18944
+ type: 'application/json'
18945
+ }));
18946
+ if (queued) return true;
18947
+ // sendBeacon returns false when the user agent refuses to queue
18948
+ // (payload over the per-origin budget); fall through to fetch.
18949
+ } catch (_unused2) {
18950
+ // Fall through to fetch.
18951
+ }
18952
+ }
18953
+ if (typeof fetch !== 'function') return false;
18954
+ const headers = {
18955
+ 'Content-Type': 'application/json'
18956
+ };
18957
+ if (token) headers.Authorization = "Bearer ".concat(token);
18958
+
18959
+ // Not awaited by callers; keepalive keeps it alive past the
18960
+ // re-render the click causes.
18961
+ await fetch(url, {
18962
+ method: 'POST',
18963
+ headers,
18964
+ credentials: 'include',
18965
+ keepalive: true,
18966
+ body
18967
+ });
18968
+ return true;
18969
+ } catch (_unused3) {
18970
+ // Telemetry only - never surface to the user, never rethrow.
18971
+ return false;
18972
+ }
18973
+ }
18974
+
18802
18975
  const DEFAULT_AGENT_BUBBLE_COLOR = "#ffffff";
18803
18976
  const DEFAULT_USER_MESSAGE_COLOR = "#f5f5f5";
18804
18977
  const DEFAULT_TEXT_COLOR = "#000000";
@@ -18915,6 +19088,9 @@
18915
19088
  // (submitText) instead of the title-case label. Display-only;
18916
19089
  // kill-switch via behavior.autoPromptQuestionDisplay (default on).
18917
19090
  autoPromptQuestionDisplay = true,
19091
+ // EV-2979: fire-and-forget click telemetry for auto-prompt chips.
19092
+ // Kill-switch via behavior.autoPromptClickTelemetry (default on).
19093
+ autoPromptClickTelemetry = true,
18918
19094
  allowFeedback = true,
18919
19095
  provideCitations = true,
18920
19096
  onFeedback,
@@ -18951,37 +19127,23 @@
18951
19127
  (_panelRef$current = panelRef.current) === null || _panelRef$current === void 0 || _panelRef$current.focus();
18952
19128
  }, []);
18953
19129
 
18954
- // EV-2842 - chip click telemetry. Fire-and-forget POST to the
18955
- // HMAC-verified support proxy (same auth pattern as the CSAT calls
19130
+ // EV-2842 / EV-2979 - chip click telemetry. Fire-and-forget POST to
19131
+ // the verified support proxy (same auth pattern as the CSAT calls
18956
19132
  // below). A telemetry failure must never block or delay submitting
18957
- // the chip's text, so this is never awaited and swallows everything.
18958
- const reportChipClick = reactExports.useCallback(async (submitValue, prompt, index) => {
18959
- try {
18960
- if (!apiBaseUrl) return;
18961
- const headers = {
18962
- "Content-Type": "application/json"
18963
- };
18964
- if (typeof getAuthToken === "function") {
18965
- const token = await getAuthToken();
18966
- if (token) headers["Authorization"] = "Bearer ".concat(token);
18967
- }
18968
- await fetch("".concat(apiBaseUrl.replace(/\/$/, ""), "/api/v1/support/chip-click"), {
18969
- method: "POST",
18970
- headers,
18971
- credentials: "include",
18972
- keepalive: true,
18973
- body: JSON.stringify({
18974
- conversationId,
18975
- chipText: prompt && prompt.text || String(submitValue !== null && submitValue !== void 0 ? submitValue : ""),
18976
- submitText: String(submitValue !== null && submitValue !== void 0 ? submitValue : ""),
18977
- position: Number.isInteger(index) ? index : undefined,
18978
- source: "widget"
18979
- })
18980
- });
18981
- } catch (_unused) {
18982
- // Telemetry only - never surface to the user.
18983
- }
18984
- }, [apiBaseUrl, getAuthToken, conversationId]);
19133
+ // the chip's text, so this is never awaited and sendChipClickBeacon
19134
+ // swallows everything internally.
19135
+ const reportChipClick = reactExports.useCallback((submitValue, prompt, index) => {
19136
+ void sendChipClickBeacon({
19137
+ apiBaseUrl,
19138
+ conversationId,
19139
+ chip: prompt,
19140
+ submitText: submitValue == null ? undefined : String(submitValue),
19141
+ position: index,
19142
+ source: "widget",
19143
+ getAuthToken,
19144
+ enabled: autoPromptClickTelemetry
19145
+ });
19146
+ }, [apiBaseUrl, getAuthToken, conversationId, autoPromptClickTelemetry]);
18985
19147
  const stableOnPromptSuggestionClick = reactExports.useCallback((text, prompt, index) => {
18986
19148
  // EV-2842: record the chip click (not awaited). Only fires when a
18987
19149
  // prompt OBJECT rode along - i.e. a real auto-prompt chip row -
@@ -19003,7 +19165,7 @@
19003
19165
  if (!raw) return null;
19004
19166
  const parsed = JSON.parse(raw);
19005
19167
  if (typeof (parsed === null || parsed === void 0 ? void 0 : parsed.width) === "number" && typeof (parsed === null || parsed === void 0 ? void 0 : parsed.height) === "number" && parsed.width >= _WIDGET_MIN_W && parsed.height >= _WIDGET_MIN_H) return parsed;
19006
- } catch (_unused2) {/* fall through */}
19168
+ } catch (_unused) {/* fall through */}
19007
19169
  return null;
19008
19170
  });
19009
19171
  const resizeAxisRef = reactExports.useRef(null);
@@ -19051,7 +19213,7 @@
19051
19213
  if (!widgetSize) return;
19052
19214
  try {
19053
19215
  window.localStorage.setItem(_WIDGET_SIZE_KEY, JSON.stringify(widgetSize));
19054
- } catch (_unused3) {/* ignore */}
19216
+ } catch (_unused2) {/* ignore */}
19055
19217
  }, [widgetSize]);
19056
19218
  const handleResizeStart = (axis, e) => {
19057
19219
  e.preventDefault();
@@ -19079,7 +19241,7 @@
19079
19241
  try {
19080
19242
  var _getSession$csatSubmi, _getSession;
19081
19243
  return (_getSession$csatSubmi = (_getSession = getSession(organizationId)) === null || _getSession === void 0 ? void 0 : _getSession.csatSubmitted) !== null && _getSession$csatSubmi !== void 0 ? _getSession$csatSubmi : false;
19082
- } catch (_unused4) {
19244
+ } catch (_unused3) {
19083
19245
  return false;
19084
19246
  }
19085
19247
  });
@@ -19098,7 +19260,7 @@
19098
19260
  const submitted = (_getSession$csatSubmi2 = (_getSession2 = getSession(organizationId)) === null || _getSession2 === void 0 ? void 0 : _getSession2.csatSubmitted) !== null && _getSession$csatSubmi2 !== void 0 ? _getSession$csatSubmi2 : false;
19099
19261
  setCsatSubmitted(submitted);
19100
19262
  if (submitted) setShowCsat(false);
19101
- } catch (_unused5) {}
19263
+ } catch (_unused4) {}
19102
19264
  }, [conversationId]);
19103
19265
 
19104
19266
  // Auto-scroll hook
@@ -20080,7 +20242,7 @@
20080
20242
 
20081
20243
  // Map API response to widget props (use props as overrides if provided)
20082
20244
  const widgetConfig = reactExports.useMemo(() => {
20083
- var _agentConfig$config, _cfg$identity, _cfg$branding, _cfg$typography, _cfg$messages, _cfg$voice, _cfg$onboarding, _cfg$disclaimer, _cfg$csat, _cfg$inactivity, _cfg$behavior, _cfg$security, _cfg$layout, _ref2, _messages$bundles$loc, _messages$bundles, _messages$bundles$en, _messages$bundles2, _ref3, _ref4, _security$domains, _ref5, _ref6, _ref7, _branding$chatbotAvat, _branding$chatbotAvat2, _ref8, _ref9, _ref0, _ref1, _ref10, _ref11, _ref12, _ref13, _typography$headerTex, _identity$subtitle, _identity$showStatus, _identity$statusMode, _identity$officeHours, _ref14, _ref15, _ref16, _ref17, _ref18, _ref19, _inactivity$timeoutVa, _inactivity$timeoutUn, _ref20, _ref21, _ref22, _ref23, _ref24, _ref25, _ref26, _ref27, _csat$enabled, _csat$format, _csat$prompt, _csat$triggerType, _csat$idleTimeoutMins, _csat$followUpEnabled, _csat$followUpPrompt, _behavior$autoPromptE, _behavior$autoPromptQ, _behavior$allowFeedba, _behavior$provideCita, _agentConfig$organiza, _cfg$chatFeatures;
20245
+ var _agentConfig$config, _cfg$identity, _cfg$branding, _cfg$typography, _cfg$messages, _cfg$voice, _cfg$onboarding, _cfg$disclaimer, _cfg$csat, _cfg$inactivity, _cfg$behavior, _cfg$security, _cfg$layout, _ref2, _messages$bundles$loc, _messages$bundles, _messages$bundles$en, _messages$bundles2, _ref3, _ref4, _security$domains, _ref5, _ref6, _ref7, _branding$chatbotAvat, _branding$chatbotAvat2, _ref8, _ref9, _ref0, _ref1, _ref10, _ref11, _ref12, _ref13, _typography$headerTex, _identity$subtitle, _identity$showStatus, _identity$statusMode, _identity$officeHours, _ref14, _ref15, _ref16, _ref17, _ref18, _ref19, _inactivity$timeoutVa, _inactivity$timeoutUn, _ref20, _ref21, _ref22, _ref23, _ref24, _ref25, _ref26, _ref27, _csat$enabled, _csat$format, _csat$prompt, _csat$triggerType, _csat$idleTimeoutMins, _csat$followUpEnabled, _csat$followUpPrompt, _behavior$autoPromptE, _behavior$autoPromptQ, _behavior$autoPromptC, _behavior$allowFeedba, _behavior$provideCita, _agentConfig$organiza, _cfg$chatFeatures;
20084
20246
  if (!agentConfig) {
20085
20247
  return null;
20086
20248
  }
@@ -20180,6 +20342,11 @@
20180
20342
  // (submit payload untouched). Kill-switch per deployment: set
20181
20343
  // behavior.autoPromptQuestionDisplay=false to revert to labels.
20182
20344
  autoPromptQuestionDisplay: (_behavior$autoPromptQ = behavior.autoPromptQuestionDisplay) !== null && _behavior$autoPromptQ !== void 0 ? _behavior$autoPromptQ : true,
20345
+ // EV-2979: fire-and-forget chip-click telemetry (the click half of
20346
+ // the impression funnel). Default on; set
20347
+ // behavior.autoPromptClickTelemetry=false to silence it for a
20348
+ // deployment that does not want click events recorded.
20349
+ autoPromptClickTelemetry: (_behavior$autoPromptC = behavior.autoPromptClickTelemetry) !== null && _behavior$autoPromptC !== void 0 ? _behavior$autoPromptC : true,
20183
20350
  allowFeedback: (_behavior$allowFeedba = behavior.allowFeedback) !== null && _behavior$allowFeedba !== void 0 ? _behavior$allowFeedba : true,
20184
20351
  // behavior.provideCitations - when false, hide the Sources panel on
20185
20352
  // every message and tell the backend to skip citation generation.
@@ -20478,6 +20645,7 @@
20478
20645
  onResetConversation: resetConversation,
20479
20646
  autoPromptEnabled: widgetConfig.autoPromptEnabled,
20480
20647
  autoPromptQuestionDisplay: widgetConfig.autoPromptQuestionDisplay,
20648
+ autoPromptClickTelemetry: widgetConfig.autoPromptClickTelemetry,
20481
20649
  allowFeedback: widgetConfig.allowFeedback,
20482
20650
  provideCitations: widgetConfig.provideCitations,
20483
20651
  onFeedback: handleMessageFeedback,