@eshal-bot/chat-widget 0.1.101 → 0.1.102

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-BcPgyNKZ.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;
@@ -18799,6 +18803,162 @@
18799
18803
  });
18800
18804
  };
18801
18805
 
18806
+ /**
18807
+ * EV-2979 - auto-prompt chip CLICK telemetry (client half).
18808
+ *
18809
+ * Impressions are logged server-side at the chip-generation call sites;
18810
+ * clicks can only be observed on the client, so this module is the last
18811
+ * link in the funnel. It posts to the Express support proxy at
18812
+ * `POST /api/v1/support/chip-click`, which re-derives the organization
18813
+ * from the verified widget auth (EV-2388 - the client MUST NOT send an
18814
+ * organizationId) and forwards to knowledge-api.
18815
+ *
18816
+ * Hard rules, in priority order over "did the event land":
18817
+ * 1. Never block, delay or break the chip's real action. Callers must
18818
+ * not await this; every failure path resolves to `false` and never
18819
+ * throws.
18820
+ * 2. Never send PII beyond what the chip itself already contains
18821
+ * (its label, its submit text, its position, the conversation id).
18822
+ * 3. Be a silent no-op when there is nothing safe to send: no API base
18823
+ * URL, no conversation id, or telemetry disabled by config.
18824
+ *
18825
+ * Transport: `navigator.sendBeacon` is preferred because the browser
18826
+ * keeps it in flight across the re-render (and even a navigation) that
18827
+ * a chip click triggers. sendBeacon cannot set request headers, so the
18828
+ * in-app Bearer path (`getAuthToken` present, i.e. session auth) falls
18829
+ * back to `fetch(..., { keepalive: true })`, which has the same
18830
+ * survive-the-unload property but can carry Authorization. Mode B
18831
+ * embeds have no token - the HMAC signature is added by the
18832
+ * @eshal-bot/server proxy - so those take the sendBeacon path.
18833
+ */
18834
+
18835
+ const CHIP_CLICK_PATH = '/api/v1/support/chip-click';
18836
+
18837
+ /**
18838
+ * Builds the wire payload for a chip click.
18839
+ *
18840
+ * `chipText` is deliberately the chip's SHORT LABEL (`text`), not the
18841
+ * displayed question (`submitText`): impression rows store `chip_text`
18842
+ * from `text`, so using the label is what makes clicks joinable to
18843
+ * impressions. EV-2977 changed only what the chip DISPLAYS.
18844
+ *
18845
+ * @param {{chip?: object|string, submitText?: string, position?: number,
18846
+ * conversationId?: string, source?: string}} args
18847
+ * @returns {object|null} payload, or null when there is no usable chipText
18848
+ */
18849
+ function buildChipClickPayload() {
18850
+ let {
18851
+ chip,
18852
+ submitText,
18853
+ position,
18854
+ conversationId,
18855
+ source = 'widget'
18856
+ } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
18857
+ const isObject = chip && typeof chip === 'object';
18858
+ const label = isObject && typeof chip.text === 'string' ? chip.text : '';
18859
+ const submit = typeof submitText === 'string' && submitText ? submitText : isObject && typeof chip.submitText === 'string' ? chip.submitText : typeof chip === 'string' ? chip : '';
18860
+ const chipText = label || submit;
18861
+ if (!chipText) return null;
18862
+ const payload = {
18863
+ conversationId,
18864
+ chipText,
18865
+ submitText: submit || undefined,
18866
+ position: Number.isInteger(position) ? position : undefined,
18867
+ source
18868
+ };
18869
+
18870
+ // The chip payload is growing a `promptId` concurrently. Read it
18871
+ // defensively and simply omit the key until it exists - this must
18872
+ // never become a hard dependency.
18873
+ const promptId = isObject ? chip === null || chip === void 0 ? void 0 : chip.promptId : undefined;
18874
+ if (typeof promptId === 'string' && promptId) payload.promptId = promptId;
18875
+ return payload;
18876
+ }
18877
+
18878
+ /**
18879
+ * Fire-and-forget chip-click beacon.
18880
+ *
18881
+ * @param {object} args
18882
+ * @param {string} [args.apiBaseUrl] Support API base (same one /csat uses).
18883
+ * @param {string} [args.conversationId] Required; no conversation = no event.
18884
+ * @param {object|string} [args.chip] The chip object from the suggestions row.
18885
+ * @param {string} [args.submitText] What the click submits.
18886
+ * @param {number} [args.position] 0-based index within the strip.
18887
+ * @param {string} [args.source] Surface id, defaults to "widget".
18888
+ * @param {Function} [args.getAuthToken] Optional async () => token (in-app path).
18889
+ * @param {boolean} [args.enabled] Config kill-switch, defaults to true.
18890
+ * @returns {Promise<boolean>} true only if a request was actually dispatched.
18891
+ */
18892
+ async function sendChipClickBeacon() {
18893
+ let {
18894
+ apiBaseUrl,
18895
+ conversationId,
18896
+ chip,
18897
+ submitText,
18898
+ position,
18899
+ source = 'widget',
18900
+ getAuthToken,
18901
+ enabled = true
18902
+ } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
18903
+ try {
18904
+ if (enabled === false) return false;
18905
+ if (!apiBaseUrl || typeof apiBaseUrl !== 'string') return false;
18906
+ if (!conversationId || typeof conversationId !== 'string') return false;
18907
+ const payload = buildChipClickPayload({
18908
+ chip,
18909
+ submitText,
18910
+ position,
18911
+ conversationId,
18912
+ source
18913
+ });
18914
+ if (!payload) return false;
18915
+ const url = "".concat(apiBaseUrl.replace(/\/$/, '')).concat(CHIP_CLICK_PATH);
18916
+ const body = JSON.stringify(payload);
18917
+ let token;
18918
+ if (typeof getAuthToken === 'function') {
18919
+ try {
18920
+ token = await getAuthToken();
18921
+ } catch (_unused) {
18922
+ token = undefined;
18923
+ }
18924
+ }
18925
+
18926
+ // No Authorization header needed -> sendBeacon, the most
18927
+ // re-render/unload-proof transport available.
18928
+ if (!token && typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function' && typeof Blob !== 'undefined') {
18929
+ try {
18930
+ const queued = navigator.sendBeacon(url, new Blob([body], {
18931
+ type: 'application/json'
18932
+ }));
18933
+ if (queued) return true;
18934
+ // sendBeacon returns false when the user agent refuses to queue
18935
+ // (payload over the per-origin budget); fall through to fetch.
18936
+ } catch (_unused2) {
18937
+ // Fall through to fetch.
18938
+ }
18939
+ }
18940
+ if (typeof fetch !== 'function') return false;
18941
+ const headers = {
18942
+ 'Content-Type': 'application/json'
18943
+ };
18944
+ if (token) headers.Authorization = "Bearer ".concat(token);
18945
+
18946
+ // Not awaited by callers; keepalive keeps it alive past the
18947
+ // re-render the click causes.
18948
+ await fetch(url, {
18949
+ method: 'POST',
18950
+ headers,
18951
+ credentials: 'include',
18952
+ keepalive: true,
18953
+ body
18954
+ });
18955
+ return true;
18956
+ } catch (_unused3) {
18957
+ // Telemetry only - never surface to the user, never rethrow.
18958
+ return false;
18959
+ }
18960
+ }
18961
+
18802
18962
  const DEFAULT_AGENT_BUBBLE_COLOR = "#ffffff";
18803
18963
  const DEFAULT_USER_MESSAGE_COLOR = "#f5f5f5";
18804
18964
  const DEFAULT_TEXT_COLOR = "#000000";
@@ -18915,6 +19075,9 @@
18915
19075
  // (submitText) instead of the title-case label. Display-only;
18916
19076
  // kill-switch via behavior.autoPromptQuestionDisplay (default on).
18917
19077
  autoPromptQuestionDisplay = true,
19078
+ // EV-2979: fire-and-forget click telemetry for auto-prompt chips.
19079
+ // Kill-switch via behavior.autoPromptClickTelemetry (default on).
19080
+ autoPromptClickTelemetry = true,
18918
19081
  allowFeedback = true,
18919
19082
  provideCitations = true,
18920
19083
  onFeedback,
@@ -18951,37 +19114,23 @@
18951
19114
  (_panelRef$current = panelRef.current) === null || _panelRef$current === void 0 || _panelRef$current.focus();
18952
19115
  }, []);
18953
19116
 
18954
- // EV-2842 - chip click telemetry. Fire-and-forget POST to the
18955
- // HMAC-verified support proxy (same auth pattern as the CSAT calls
19117
+ // EV-2842 / EV-2979 - chip click telemetry. Fire-and-forget POST to
19118
+ // the verified support proxy (same auth pattern as the CSAT calls
18956
19119
  // 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]);
19120
+ // the chip's text, so this is never awaited and sendChipClickBeacon
19121
+ // swallows everything internally.
19122
+ const reportChipClick = reactExports.useCallback((submitValue, prompt, index) => {
19123
+ void sendChipClickBeacon({
19124
+ apiBaseUrl,
19125
+ conversationId,
19126
+ chip: prompt,
19127
+ submitText: submitValue == null ? undefined : String(submitValue),
19128
+ position: index,
19129
+ source: "widget",
19130
+ getAuthToken,
19131
+ enabled: autoPromptClickTelemetry
19132
+ });
19133
+ }, [apiBaseUrl, getAuthToken, conversationId, autoPromptClickTelemetry]);
18985
19134
  const stableOnPromptSuggestionClick = reactExports.useCallback((text, prompt, index) => {
18986
19135
  // EV-2842: record the chip click (not awaited). Only fires when a
18987
19136
  // prompt OBJECT rode along - i.e. a real auto-prompt chip row -
@@ -19003,7 +19152,7 @@
19003
19152
  if (!raw) return null;
19004
19153
  const parsed = JSON.parse(raw);
19005
19154
  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 */}
19155
+ } catch (_unused) {/* fall through */}
19007
19156
  return null;
19008
19157
  });
19009
19158
  const resizeAxisRef = reactExports.useRef(null);
@@ -19051,7 +19200,7 @@
19051
19200
  if (!widgetSize) return;
19052
19201
  try {
19053
19202
  window.localStorage.setItem(_WIDGET_SIZE_KEY, JSON.stringify(widgetSize));
19054
- } catch (_unused3) {/* ignore */}
19203
+ } catch (_unused2) {/* ignore */}
19055
19204
  }, [widgetSize]);
19056
19205
  const handleResizeStart = (axis, e) => {
19057
19206
  e.preventDefault();
@@ -19079,7 +19228,7 @@
19079
19228
  try {
19080
19229
  var _getSession$csatSubmi, _getSession;
19081
19230
  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) {
19231
+ } catch (_unused3) {
19083
19232
  return false;
19084
19233
  }
19085
19234
  });
@@ -19098,7 +19247,7 @@
19098
19247
  const submitted = (_getSession$csatSubmi2 = (_getSession2 = getSession(organizationId)) === null || _getSession2 === void 0 ? void 0 : _getSession2.csatSubmitted) !== null && _getSession$csatSubmi2 !== void 0 ? _getSession$csatSubmi2 : false;
19099
19248
  setCsatSubmitted(submitted);
19100
19249
  if (submitted) setShowCsat(false);
19101
- } catch (_unused5) {}
19250
+ } catch (_unused4) {}
19102
19251
  }, [conversationId]);
19103
19252
 
19104
19253
  // Auto-scroll hook
@@ -20080,7 +20229,7 @@
20080
20229
 
20081
20230
  // Map API response to widget props (use props as overrides if provided)
20082
20231
  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;
20232
+ 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
20233
  if (!agentConfig) {
20085
20234
  return null;
20086
20235
  }
@@ -20180,6 +20329,11 @@
20180
20329
  // (submit payload untouched). Kill-switch per deployment: set
20181
20330
  // behavior.autoPromptQuestionDisplay=false to revert to labels.
20182
20331
  autoPromptQuestionDisplay: (_behavior$autoPromptQ = behavior.autoPromptQuestionDisplay) !== null && _behavior$autoPromptQ !== void 0 ? _behavior$autoPromptQ : true,
20332
+ // EV-2979: fire-and-forget chip-click telemetry (the click half of
20333
+ // the impression funnel). Default on; set
20334
+ // behavior.autoPromptClickTelemetry=false to silence it for a
20335
+ // deployment that does not want click events recorded.
20336
+ autoPromptClickTelemetry: (_behavior$autoPromptC = behavior.autoPromptClickTelemetry) !== null && _behavior$autoPromptC !== void 0 ? _behavior$autoPromptC : true,
20183
20337
  allowFeedback: (_behavior$allowFeedba = behavior.allowFeedback) !== null && _behavior$allowFeedba !== void 0 ? _behavior$allowFeedba : true,
20184
20338
  // behavior.provideCitations - when false, hide the Sources panel on
20185
20339
  // every message and tell the backend to skip citation generation.
@@ -20478,6 +20632,7 @@
20478
20632
  onResetConversation: resetConversation,
20479
20633
  autoPromptEnabled: widgetConfig.autoPromptEnabled,
20480
20634
  autoPromptQuestionDisplay: widgetConfig.autoPromptQuestionDisplay,
20635
+ autoPromptClickTelemetry: widgetConfig.autoPromptClickTelemetry,
20481
20636
  allowFeedback: widgetConfig.allowFeedback,
20482
20637
  provideCitations: widgetConfig.provideCitations,
20483
20638
  onFeedback: handleMessageFeedback,