@eshal-bot/chat-widget 0.1.122 → 0.1.123

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-CbBUA9Lj.esm.js";import"react";import"react-dom/client";import"react-dom";import"react/jsx-runtime";
1
+ export{S as default}from"./chat-widget-index-Bn-k9Jjr.esm.js";import"react";import"react-dom/client";import"react-dom";import"react/jsx-runtime";
@@ -16627,6 +16627,11 @@
16627
16627
  };
16628
16628
  }, [isVoiceSessionActive, voiceStatus]);
16629
16629
  const normalizedDisclaimerLinkUrl = reactExports.useMemo(() => normalizeExternalUrl(disclaimerLinkUrl), [disclaimerLinkUrl]);
16630
+
16631
+ // EV-2351: the disclaimer's direction comes from its own text. Falls back to
16632
+ // the configured `isRtl` when the text has no strongly-directional character
16633
+ // (digits, punctuation, emoji only), same contract as the message bubbles.
16634
+ const disclaimerIsRtl = reactExports.useMemo(() => detectTextDirection(disclaimerText, isRtl), [disclaimerText, isRtl]);
16630
16635
  return /*#__PURE__*/jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, {
16631
16636
  children: [disclaimerEnabled && disclaimerText && disclaimerPosition === "top" && /*#__PURE__*/jsxRuntimeExports.jsx("div", {
16632
16637
  className: "flex justify-center",
@@ -16636,13 +16641,21 @@
16636
16641
  color: isDark ? "#d1d5db" : "#6b7280",
16637
16642
  fontFamily: fontFamily !== undefined && fontFamily !== null ? fontFamily : "Inter",
16638
16643
  fontSize: fontSize !== undefined && fontSize !== null ? "calc(".concat(fontSize, " - 2px)") : "12px",
16639
- direction: isRtl ? "rtl" : "ltr",
16644
+ // EV-2351: direction from the disclaimer's OWN text, not from
16645
+ // the configured default language. This is static configured
16646
+ // copy rather than message content, which is why the earlier
16647
+ // per-message passes never reached it - an English disclaimer
16648
+ // under an Arabic-default concierge rendered with its trailing
16649
+ // period pushed to the front (".Please verify important
16650
+ // information"). Same helper and same fallback as the message
16651
+ // bubbles below.
16652
+ direction: disclaimerIsRtl ? "rtl" : "ltr",
16640
16653
  textAlign: "center",
16641
16654
  borderColor: isDark ? "#374151" : "#e5e7eb"
16642
16655
  },
16643
16656
  children: /*#__PURE__*/jsxRuntimeExports.jsxs("p", {
16644
16657
  className: "leading-relaxed whitespace-pre-wrap break-words inline",
16645
- dir: isRtl ? "rtl" : "ltr",
16658
+ dir: disclaimerIsRtl ? "rtl" : "ltr",
16646
16659
  style: {
16647
16660
  color: isDark ? "#d1d5db" : "#6b7280",
16648
16661
  fontFamily: fontFamily !== undefined && fontFamily !== null ? fontFamily : "Inter",
@@ -17107,24 +17120,42 @@
17107
17120
  return;
17108
17121
  }
17109
17122
 
17110
- // presignRes.ok means decideAttachmentPolicy's LAST step ran -
17111
- // addSessionBytes already reserved this file's bytes server-side.
17112
- // Mark it so removeAttachment (and every error path below) knows to
17113
- // call /release rather than just correcting the client estimate.
17114
- patchAttachment(attachmentId, {
17115
- reservedBytes: file.size
17116
- });
17123
+ // How many bytes the SERVER is currently holding for this attachment.
17124
+ // Only ever non-zero when we know a reservation exists (EV-2791) -
17125
+ // releasing bytes the server never reserved silently hands the
17126
+ // conversation extra budget, which is as wrong as over-counting.
17127
+ let heldServerBytes = 0;
17128
+ const releaseHeld = () => {
17129
+ if (heldServerBytes > 0) {
17130
+ releaseSessionBytes({
17131
+ apiEndpoint,
17132
+ conversationId,
17133
+ size: heldServerBytes,
17134
+ authHeader
17135
+ });
17136
+ heldServerBytes = 0;
17137
+ patchAttachment(attachmentId, {
17138
+ reservedBytes: 0
17139
+ });
17140
+ }
17141
+ };
17142
+ const undoLocalEstimate = () => {
17143
+ sessionBytesRef.current.bytes = Math.max(0, sessionBytesRef.current.bytes - file.size);
17144
+ };
17117
17145
  let presign;
17118
17146
  try {
17119
17147
  presign = await presignRes.json();
17120
17148
  } catch (_unused5) {
17149
+ // Body unreadable, so we cannot tell whether a reservation was made.
17150
+ // Release the declared size - the server clamps at zero, and this
17151
+ // path needs the policy check to have already passed.
17121
17152
  releaseSessionBytes({
17122
17153
  apiEndpoint,
17123
17154
  conversationId,
17124
17155
  size: file.size,
17125
17156
  authHeader
17126
17157
  });
17127
- sessionBytesRef.current.bytes = Math.max(0, sessionBytesRef.current.bytes - file.size);
17158
+ undoLocalEstimate();
17128
17159
  patchAttachment(attachmentId, {
17129
17160
  reservedBytes: 0
17130
17161
  });
@@ -17132,6 +17163,20 @@
17132
17163
  return;
17133
17164
  }
17134
17165
 
17166
+ // `reserved` tells us whether /presign itself charged the session cap.
17167
+ // It is false on the NFS/multipart branch, where /multipart is the
17168
+ // reserving gate - reserving in both places is what double-counted the
17169
+ // "already used" figure (EV-2791). An older backend omits the field;
17170
+ // fall back to the legacy assumption so a widget deployed ahead of the
17171
+ // backend still releases correctly.
17172
+ const presignReserved = typeof presign.reserved === "boolean" ? presign.reserved : !presign.useMultipart;
17173
+ if (presignReserved) {
17174
+ heldServerBytes = file.size;
17175
+ patchAttachment(attachmentId, {
17176
+ reservedBytes: file.size
17177
+ });
17178
+ }
17179
+
17135
17180
  // Step B - upload bytes. Two paths:
17136
17181
  // * useMultipart=true → POST FormData to /multipart (NFS + any
17137
17182
  // backend without a direct-PUT endpoint).
@@ -17160,6 +17205,13 @@
17160
17205
  });
17161
17206
  uploadedReadUrl = multipartResp.url;
17162
17207
  uploadedStorageKey = multipartResp.storageKey;
17208
+ // /multipart is the reserving gate on this path, and it only
17209
+ // reserves once its own policy check passes - the bytes are held
17210
+ // from here on, so removing the chip must release them.
17211
+ heldServerBytes = file.size;
17212
+ patchAttachment(attachmentId, {
17213
+ reservedBytes: file.size
17214
+ });
17163
17215
  } else if (presign.uploadUrl) {
17164
17216
  await putWithProgress({
17165
17217
  url: presign.uploadUrl,
@@ -17186,16 +17238,8 @@
17186
17238
  });
17187
17239
  if (!confirmed.ok) {
17188
17240
  inflightRef.current.delete(attachmentId);
17189
- releaseSessionBytes({
17190
- apiEndpoint,
17191
- conversationId,
17192
- size: file.size,
17193
- authHeader
17194
- });
17195
- sessionBytesRef.current.bytes = Math.max(0, sessionBytesRef.current.bytes - file.size);
17196
- patchAttachment(attachmentId, {
17197
- reservedBytes: 0
17198
- });
17241
+ releaseHeld();
17242
+ undoLocalEstimate();
17199
17243
  errorPatch(attachmentId, confirmed.code, confirmed.message);
17200
17244
  return;
17201
17245
  }
@@ -17207,16 +17251,12 @@
17207
17251
  // Aborted means removeAttachment already released this reservation -
17208
17252
  // releasing again here would double-count the give-back.
17209
17253
  if (err && err.aborted) return;
17210
- releaseSessionBytes({
17211
- apiEndpoint,
17212
- conversationId,
17213
- size: file.size,
17214
- authHeader
17215
- });
17216
- sessionBytesRef.current.bytes = Math.max(0, sessionBytesRef.current.bytes - file.size);
17217
- patchAttachment(attachmentId, {
17218
- reservedBytes: 0
17219
- });
17254
+ // On the multipart path a failure means either the policy check
17255
+ // rejected (nothing reserved) or the write failed (the route released
17256
+ // its own reservation) - `heldServerBytes` is 0 either way, so
17257
+ // releaseHeld() correctly does nothing.
17258
+ releaseHeld();
17259
+ undoLocalEstimate();
17220
17260
  errorPatch(attachmentId, "network", "Upload failed. Please retry.");
17221
17261
  return;
17222
17262
  }
@@ -18190,6 +18230,15 @@
18190
18230
  useGoogleFont(resolvedHeaderFontFamily);
18191
18231
  const t = reactExports.useMemo(() => createTranslator(defaultLanguage), [defaultLanguage]);
18192
18232
  const isRtl = isRtlLocale(defaultLanguage);
18233
+
18234
+ // EV-2351: the subtitle is operator-authored copy, so its direction has to
18235
+ // come from the text itself rather than from the configured default
18236
+ // language. With Arabic as the default, an English subtitle inherited the
18237
+ // header's RTL container and rendered with its trailing punctuation pushed
18238
+ // to the front ("!Do what you love. We'll do the rest"). Falls back to the
18239
+ // configured direction when the text carries no strongly-directional
18240
+ // character, same contract as the message bubbles.
18241
+ const subtitleIsRtl = reactExports.useMemo(() => detectTextDirection(subtitle, isRtl), [subtitle, isRtl]);
18193
18242
  const agentStatus = reactExports.useMemo(() => computeAgentStatus({
18194
18243
  showStatus,
18195
18244
  statusMode,
@@ -18270,9 +18319,11 @@
18270
18319
  }), subtitle ? /*#__PURE__*/jsxRuntimeExports.jsx("p", {
18271
18320
  title: subtitle,
18272
18321
  className: "leading-snug truncate opacity-75",
18322
+ dir: subtitleIsRtl ? "rtl" : "ltr",
18273
18323
  style: {
18274
18324
  fontSize: "12px",
18275
- color: iconColor
18325
+ color: iconColor,
18326
+ textAlign: subtitleIsRtl ? "right" : "left"
18276
18327
  },
18277
18328
  children: subtitle
18278
18329
  }) : null]