@eshal-bot/chat-widget 0.1.97 → 0.1.99

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-CHqB62M7.esm.js";import"react";import"react-dom/client";import"react-dom";import"react/jsx-runtime";
1
+ export{S as default}from"./chat-widget-index-BHNeh4MK.esm.js";import"react";import"react-dom/client";import"react-dom";import"react/jsx-runtime";
@@ -16546,13 +16546,43 @@
16546
16546
  message: backendMsg
16547
16547
  };
16548
16548
  }
16549
- function putWithProgress(_ref2) {
16549
+
16550
+ // Give back session budget the backend reserved at /presign admission time
16551
+ // (EV-2791) - fired when a chip is removed or its upload fails AFTER that
16552
+ // reservation succeeded, so a not-actually-sent file doesn't permanently
16553
+ // inflate the conversation's "used" total for the rest of the session TTL.
16554
+ // Fire-and-forget: a failed release just leaves the budget a bit tighter
16555
+ // than it truly is, never more permissive, so it's safe to ignore errors.
16556
+ function releaseSessionBytes(_ref2) {
16557
+ let {
16558
+ apiEndpoint,
16559
+ conversationId,
16560
+ size,
16561
+ authHeader
16562
+ } = _ref2;
16563
+ fetch(joinUrl(apiEndpoint, "/api/v1/chat-attachments/release"), {
16564
+ method: "POST",
16565
+ credentials: "include",
16566
+ headers: _objectSpread2({
16567
+ "Content-Type": "application/json"
16568
+ }, authHeader ? {
16569
+ Authorization: authHeader
16570
+ } : {}),
16571
+ body: JSON.stringify({
16572
+ conversationId,
16573
+ size
16574
+ })
16575
+ }).catch(() => {
16576
+ /* best-effort - see comment above */
16577
+ });
16578
+ }
16579
+ function putWithProgress(_ref3) {
16550
16580
  let {
16551
16581
  url,
16552
16582
  file,
16553
16583
  onProgress,
16554
16584
  registerXhr
16555
- } = _ref2;
16585
+ } = _ref3;
16556
16586
  return new Promise((resolve, reject) => {
16557
16587
  const xhr = new XMLHttpRequest();
16558
16588
  registerXhr(xhr);
@@ -16583,7 +16613,7 @@
16583
16613
  // `url` is consumed today (READ SAS URL that knowledge-api's
16584
16614
  // attachment_ingest fetches bytes from). Malformed / empty JSON
16585
16615
  // resolves to `{}` - the upload succeeded, the read URL is best-effort.
16586
- function postMultipartWithProgress(_ref3) {
16616
+ function postMultipartWithProgress(_ref4) {
16587
16617
  let {
16588
16618
  url,
16589
16619
  file,
@@ -16591,7 +16621,7 @@
16591
16621
  authHeader,
16592
16622
  onProgress,
16593
16623
  registerXhr
16594
- } = _ref3;
16624
+ } = _ref4;
16595
16625
  return new Promise((resolve, reject) => {
16596
16626
  const xhr = new XMLHttpRequest();
16597
16627
  registerXhr(xhr);
@@ -16636,14 +16666,14 @@
16636
16666
  xhr.send(form);
16637
16667
  });
16638
16668
  }
16639
- function useFileUpload(_ref4) {
16669
+ function useFileUpload(_ref5) {
16640
16670
  let {
16641
16671
  conversationId,
16642
16672
  config,
16643
16673
  apiEndpoint,
16644
16674
  authHeader,
16645
16675
  onError
16646
- } = _ref4;
16676
+ } = _ref5;
16647
16677
  const [attachments, setAttachments] = reactExports.useState([]);
16648
16678
  const inflightRef = reactExports.useRef(new Map());
16649
16679
  const idCounterRef = reactExports.useRef(0);
@@ -16746,6 +16776,9 @@
16746
16776
  })
16747
16777
  });
16748
16778
  } catch (err) {
16779
+ // Never reached the server - no reservation was ever made, so just
16780
+ // undo this attachment's optimistic client-side estimate (EV-2791).
16781
+ sessionBytesRef.current.bytes = Math.max(0, sessionBytesRef.current.bytes - file.size);
16749
16782
  errorPatch(attachmentId, "network", "Network error while requesting upload.");
16750
16783
  return;
16751
16784
  }
@@ -16759,13 +16792,34 @@
16759
16792
  } catch (_unused4) {
16760
16793
  /* body not JSON - keep defaults */
16761
16794
  }
16795
+ // decideAttachmentPolicy rejects BEFORE reserving budget, so there's
16796
+ // nothing to release server-side - only undo the client estimate.
16797
+ sessionBytesRef.current.bytes = Math.max(0, sessionBytesRef.current.bytes - file.size);
16762
16798
  errorPatch(attachmentId, backendCode, backendMsg);
16763
16799
  return;
16764
16800
  }
16801
+
16802
+ // presignRes.ok means decideAttachmentPolicy's LAST step ran -
16803
+ // addSessionBytes already reserved this file's bytes server-side.
16804
+ // Mark it so removeAttachment (and every error path below) knows to
16805
+ // call /release rather than just correcting the client estimate.
16806
+ patchAttachment(attachmentId, {
16807
+ reservedBytes: file.size
16808
+ });
16765
16809
  let presign;
16766
16810
  try {
16767
16811
  presign = await presignRes.json();
16768
16812
  } catch (_unused5) {
16813
+ releaseSessionBytes({
16814
+ apiEndpoint,
16815
+ conversationId,
16816
+ size: file.size,
16817
+ authHeader
16818
+ });
16819
+ sessionBytesRef.current.bytes = Math.max(0, sessionBytesRef.current.bytes - file.size);
16820
+ patchAttachment(attachmentId, {
16821
+ reservedBytes: 0
16822
+ });
16769
16823
  errorPatch(attachmentId, "unknown", "Malformed presign response.");
16770
16824
  return;
16771
16825
  }
@@ -16824,6 +16878,16 @@
16824
16878
  });
16825
16879
  if (!confirmed.ok) {
16826
16880
  inflightRef.current.delete(attachmentId);
16881
+ releaseSessionBytes({
16882
+ apiEndpoint,
16883
+ conversationId,
16884
+ size: file.size,
16885
+ authHeader
16886
+ });
16887
+ sessionBytesRef.current.bytes = Math.max(0, sessionBytesRef.current.bytes - file.size);
16888
+ patchAttachment(attachmentId, {
16889
+ reservedBytes: 0
16890
+ });
16827
16891
  errorPatch(attachmentId, confirmed.code, confirmed.message);
16828
16892
  return;
16829
16893
  }
@@ -16832,7 +16896,19 @@
16832
16896
  }
16833
16897
  } catch (err) {
16834
16898
  inflightRef.current.delete(attachmentId);
16899
+ // Aborted means removeAttachment already released this reservation -
16900
+ // releasing again here would double-count the give-back.
16835
16901
  if (err && err.aborted) return;
16902
+ releaseSessionBytes({
16903
+ apiEndpoint,
16904
+ conversationId,
16905
+ size: file.size,
16906
+ authHeader
16907
+ });
16908
+ sessionBytesRef.current.bytes = Math.max(0, sessionBytesRef.current.bytes - file.size);
16909
+ patchAttachment(attachmentId, {
16910
+ reservedBytes: 0
16911
+ });
16836
16912
  errorPatch(attachmentId, "network", "Upload failed. Please retry.");
16837
16913
  return;
16838
16914
  }
@@ -16875,7 +16951,11 @@
16875
16951
  size: file.size,
16876
16952
  mimeType: file.type,
16877
16953
  status: "uploading",
16878
- progress: 0
16954
+ progress: 0,
16955
+ // Set once /presign confirms the server actually reserved this
16956
+ // file's bytes (EV-2791) - removeAttachment uses it to decide
16957
+ // whether a removal needs to call /release.
16958
+ reservedBytes: 0
16879
16959
  };
16880
16960
  setAttachments(prev => [...prev, entry]);
16881
16961
  // Reserve session budget at ADMISSION, not upload completion -
@@ -16892,8 +16972,22 @@
16892
16972
  xhr.abort();
16893
16973
  inflightRef.current.delete(id);
16894
16974
  }
16975
+ // EV-2791: a chip removed before Send had its bytes reserved server-side
16976
+ // at /presign admission time - give them back, or the session's "used"
16977
+ // total stays inflated by every file the user attached-then-removed,
16978
+ // for the rest of the 24h session TTL.
16979
+ const removed = attachmentsRef.current.find(a => a.id === id);
16980
+ if (removed && removed.reservedBytes > 0) {
16981
+ releaseSessionBytes({
16982
+ apiEndpoint,
16983
+ conversationId,
16984
+ size: removed.reservedBytes,
16985
+ authHeader
16986
+ });
16987
+ sessionBytesRef.current.bytes = Math.max(0, sessionBytesRef.current.bytes - removed.reservedBytes);
16988
+ }
16895
16989
  setAttachments(prev => prev.filter(a => a.id !== id));
16896
- }, []);
16990
+ }, [apiEndpoint, authHeader, conversationId]);
16897
16991
  const clearAll = reactExports.useCallback(() => {
16898
16992
  for (const xhr of inflightRef.current.values()) xhr.abort();
16899
16993
  inflightRef.current.clear();
@@ -17075,7 +17169,11 @@
17075
17169
  },
17076
17170
  children: suggestions.map((p, index) => /*#__PURE__*/jsxRuntimeExports.jsx("button", {
17077
17171
  type: "button",
17078
- onClick: () => onSuggestionClick(p.submitText || p.text || p),
17172
+ onClick: () =>
17173
+ // EV-2842: pass the prompt object + position so the click
17174
+ // handler can record telemetry; extra args are ignored by
17175
+ // handlers that only read the submit value.
17176
+ onSuggestionClick(p.submitText || p.text || p, p, index),
17079
17177
  dir: "auto",
17080
17178
  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",
17081
17179
  style: {
@@ -18758,9 +18856,48 @@
18758
18856
  var _panelRef$current;
18759
18857
  (_panelRef$current = panelRef.current) === null || _panelRef$current === void 0 || _panelRef$current.focus();
18760
18858
  }, []);
18761
- const stableOnPromptSuggestionClick = reactExports.useCallback(text => {
18859
+
18860
+ // EV-2842 - chip click telemetry. Fire-and-forget POST to the
18861
+ // HMAC-verified support proxy (same auth pattern as the CSAT calls
18862
+ // below). A telemetry failure must never block or delay submitting
18863
+ // the chip's text, so this is never awaited and swallows everything.
18864
+ const reportChipClick = reactExports.useCallback(async (submitValue, prompt, index) => {
18865
+ try {
18866
+ if (!apiBaseUrl) return;
18867
+ const headers = {
18868
+ "Content-Type": "application/json"
18869
+ };
18870
+ if (typeof getAuthToken === "function") {
18871
+ const token = await getAuthToken();
18872
+ if (token) headers["Authorization"] = "Bearer ".concat(token);
18873
+ }
18874
+ await fetch("".concat(apiBaseUrl.replace(/\/$/, ""), "/api/v1/support/chip-click"), {
18875
+ method: "POST",
18876
+ headers,
18877
+ credentials: "include",
18878
+ keepalive: true,
18879
+ body: JSON.stringify({
18880
+ conversationId,
18881
+ chipText: prompt && prompt.text || String(submitValue !== null && submitValue !== void 0 ? submitValue : ""),
18882
+ submitText: String(submitValue !== null && submitValue !== void 0 ? submitValue : ""),
18883
+ position: Number.isInteger(index) ? index : undefined,
18884
+ source: "widget"
18885
+ })
18886
+ });
18887
+ } catch (_unused) {
18888
+ // Telemetry only - never surface to the user.
18889
+ }
18890
+ }, [apiBaseUrl, getAuthToken, conversationId]);
18891
+ const stableOnPromptSuggestionClick = reactExports.useCallback((text, prompt, index) => {
18892
+ // EV-2842: record the chip click (not awaited). Only fires when a
18893
+ // prompt OBJECT rode along - i.e. a real auto-prompt chip row -
18894
+ // so A2UI buttons / onboarding handlers that call this with just
18895
+ // a string are not miscounted as chip clicks.
18896
+ if (prompt && typeof prompt === "object") {
18897
+ reportChipClick(text, prompt, index);
18898
+ }
18762
18899
  if (onPromptSuggestionClick) onPromptSuggestionClick(text);else onDirectSend === null || onDirectSend === void 0 || onDirectSend(text);
18763
- }, [onPromptSuggestionClick, onDirectSend]);
18900
+ }, [onPromptSuggestionClick, onDirectSend, reportChipClick]);
18764
18901
 
18765
18902
  // ─── Resizable widget ────────────────────────────────────────────
18766
18903
  const anchorRight = launcherPosition.includes("right");
@@ -18772,7 +18909,7 @@
18772
18909
  if (!raw) return null;
18773
18910
  const parsed = JSON.parse(raw);
18774
18911
  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;
18775
- } catch (_unused) {/* fall through */}
18912
+ } catch (_unused2) {/* fall through */}
18776
18913
  return null;
18777
18914
  });
18778
18915
  const resizeAxisRef = reactExports.useRef(null);
@@ -18820,7 +18957,7 @@
18820
18957
  if (!widgetSize) return;
18821
18958
  try {
18822
18959
  window.localStorage.setItem(_WIDGET_SIZE_KEY, JSON.stringify(widgetSize));
18823
- } catch (_unused2) {/* ignore */}
18960
+ } catch (_unused3) {/* ignore */}
18824
18961
  }, [widgetSize]);
18825
18962
  const handleResizeStart = (axis, e) => {
18826
18963
  e.preventDefault();
@@ -18848,7 +18985,7 @@
18848
18985
  try {
18849
18986
  var _getSession$csatSubmi, _getSession;
18850
18987
  return (_getSession$csatSubmi = (_getSession = getSession(organizationId)) === null || _getSession === void 0 ? void 0 : _getSession.csatSubmitted) !== null && _getSession$csatSubmi !== void 0 ? _getSession$csatSubmi : false;
18851
- } catch (_unused3) {
18988
+ } catch (_unused4) {
18852
18989
  return false;
18853
18990
  }
18854
18991
  });
@@ -18867,7 +19004,7 @@
18867
19004
  const submitted = (_getSession$csatSubmi2 = (_getSession2 = getSession(organizationId)) === null || _getSession2 === void 0 ? void 0 : _getSession2.csatSubmitted) !== null && _getSession$csatSubmi2 !== void 0 ? _getSession$csatSubmi2 : false;
18868
19005
  setCsatSubmitted(submitted);
18869
19006
  if (submitted) setShowCsat(false);
18870
- } catch (_unused4) {}
19007
+ } catch (_unused5) {}
18871
19008
  }, [conversationId]);
18872
19009
 
18873
19010
  // Auto-scroll hook