@eshal-bot/chat-widget 0.2.31 → 0.2.33

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-DGutMRlI.esm.js";import"react";import"react-dom/client";import"react/jsx-runtime";import"react-dom";
1
+ export{S as default}from"./chat-widget-index-ChCWDRfs.esm.js";import"react";import"react-dom/client";import"react/jsx-runtime";import"react-dom";
@@ -1177,7 +1177,7 @@
1177
1177
  function lb(a, b) {
1178
1178
  return null == a || "http://www.w3.org/1999/xhtml" === a ? kb(b) : "http://www.w3.org/2000/svg" === a && "foreignObject" === b ? "http://www.w3.org/1999/xhtml" : a;
1179
1179
  }
1180
- var mb,
1180
+ var mb$1,
1181
1181
  nb = function (a) {
1182
1182
  return "undefined" !== typeof MSApp && MSApp.execUnsafeLocalFunction ? function (b, c, d, e) {
1183
1183
  MSApp.execUnsafeLocalFunction(function () {
@@ -1186,9 +1186,9 @@
1186
1186
  } : a;
1187
1187
  }(function (a, b) {
1188
1188
  if ("http://www.w3.org/2000/svg" !== a.namespaceURI || "innerHTML" in a) a.innerHTML = b;else {
1189
- mb = mb || document.createElement("div");
1190
- mb.innerHTML = "<svg>" + b.valueOf().toString() + "</svg>";
1191
- for (b = mb.firstChild; a.firstChild;) a.removeChild(a.firstChild);
1189
+ mb$1 = mb$1 || document.createElement("div");
1190
+ mb$1.innerHTML = "<svg>" + b.valueOf().toString() + "</svg>";
1191
+ for (b = mb$1.firstChild; a.firstChild;) a.removeChild(a.firstChild);
1192
1192
  for (; b.firstChild;) a.appendChild(b.firstChild);
1193
1193
  }
1194
1194
  });
@@ -9307,6 +9307,7 @@
9307
9307
  retrySend: "إعادة المحاولة"
9308
9308
  },
9309
9309
  es: {
9310
+ agentJoinedConversation: "{name} se ha unido a la conversación",
9310
9311
  connectionError: "Lo sentimos, estoy teniendo problemas de conexión en este momento. Inténtalo de nuevo.",
9311
9312
  micDenied: "No pudimos acceder a tu micrófono. Permite el acceso e inténtalo de nuevo.",
9312
9313
  micNotFound: "No se encontró ningún micrófono. Conecta uno e inténtalo de nuevo.",
@@ -9559,6 +9560,7 @@
9559
9560
  allowMicAccess: "Permetre accés al micròfon"
9560
9561
  },
9561
9562
  zh: {
9563
+ agentJoinedConversation: "{name} 已加入对话",
9562
9564
  connectionError: "抱歉,我现在连接遇到问题。请重试。",
9563
9565
  micDenied: "无法访问您的麦克风。请允许访问后重试。",
9564
9566
  micNotFound: "未检测到麦克风。请连接麦克风后重试。",
@@ -9727,6 +9729,7 @@
9727
9729
  allowMicAccess: "Tillad mikrofonadgang"
9728
9730
  },
9729
9731
  nl: {
9732
+ agentJoinedConversation: "{name} neemt deel aan het gesprek",
9730
9733
  connectionError: "Sorry, ik heb momenteel verbindingsproblemen. Probeer het opnieuw.",
9731
9734
  micDenied: "We konden geen toegang krijgen tot je microfoon. Geef toegang en probeer het opnieuw.",
9732
9735
  micNotFound: "Geen microfoon gevonden. Sluit een microfoon aan en probeer het opnieuw.",
@@ -9853,6 +9856,7 @@
9853
9856
  allowMicAccess: "Salli mikrofonin käyttö"
9854
9857
  },
9855
9858
  fr: {
9859
+ agentJoinedConversation: "{name} a rejoint la conversation",
9856
9860
  connectionError: "Désolé, je rencontre des problèmes de connexion en ce moment. Veuillez réessayer.",
9857
9861
  micDenied: "Nous n'avons pas pu accéder à votre microphone. Autorisez l'accès et réessayez.",
9858
9862
  micNotFound: "Aucun microphone détecté. Connectez-en un et réessayez.",
@@ -9895,6 +9899,7 @@
9895
9899
  allowMicAccess: "Autoriser l'accès au microphone"
9896
9900
  },
9897
9901
  de: {
9902
+ agentJoinedConversation: "{name} ist dem Gespräch beigetreten",
9898
9903
  connectionError: "Entschuldigung, ich habe gerade Verbindungsprobleme. Bitte versuchen Sie es erneut.",
9899
9904
  micDenied: "Wir konnten nicht auf Ihr Mikrofon zugreifen. Bitte erlauben Sie den Zugriff und versuchen Sie es erneut.",
9900
9905
  micNotFound: "Kein Mikrofon gefunden. Bitte schließen Sie eines an und versuchen Sie es erneut.",
@@ -10235,6 +10240,7 @@
10235
10240
  allowMicAccess: "Consenti l'accesso al microfono"
10236
10241
  },
10237
10242
  ja: {
10243
+ agentJoinedConversation: "{name}が会話に参加しました",
10238
10244
  connectionError: "申し訳ありません。現在接続に問題が発生しています。もう一度お試しください。",
10239
10245
  micDenied: "マイクにアクセスできませんでした。アクセスを許可してもう一度お試しください。",
10240
10246
  micNotFound: "マイクが見つかりません。マイクを接続してもう一度お試しください。",
@@ -10739,6 +10745,7 @@
10739
10745
  allowMicAccess: "Разрешить доступ к микрофону"
10740
10746
  },
10741
10747
  sr: {
10748
+ agentJoinedConversation: "{name} se pridružio/la razgovoru",
10742
10749
  connectionError: "Извините, тренутно имам проблема са повезивањем. Пробајте поново.",
10743
10750
  micDenied: "Нисмо могли да приступимо вашем микрофону. Дозволите приступ и пробајте поново.",
10744
10751
  micNotFound: "Микрофон није пронађен. Прикључите микрофон и пробајте поново.",
@@ -12080,11 +12087,43 @@
12080
12087
  }
12081
12088
  return fallbackIsRtl;
12082
12089
  }
12090
+
12091
+ // A file the chat will not take is answered with a message from the agent,
12092
+ // as the other channels do, not with a warning chip under the composer. The
12093
+ // same wording and languages as the channels' lines
12094
+ // (backend channels/services/inboundAttachmentRejection.ts); other locales
12095
+ // read English. {max_mb}, {used_mb} and {types} are filled at the call site.
12096
+ const ATTACHMENT_REJECTION_TEXTS = {
12097
+ en: {
12098
+ attachmentLimitReached: "That file is too large. This chat can receive up to {max_mb} MB of files in total.",
12099
+ attachmentLimitReachedUsed: "That file is too large. This chat can receive up to {max_mb} MB of files in total, and {used_mb} MB is already used.",
12100
+ attachmentFileTooLarge: "Sorry, that file is too large. Files must be under {max_mb} MB.",
12101
+ attachmentTypeNotAllowed: "Sorry, we can't accept that type of file. You can send {types}."
12102
+ },
12103
+ ar: {
12104
+ attachmentLimitReached: "حجم الملف كبير جداً. يمكن لهذه المحادثة استقبال ملفات يصل مجموع حجمها إلى {max_mb} ميغابايت.",
12105
+ attachmentLimitReachedUsed: "حجم الملف كبير جداً. يمكن لهذه المحادثة استقبال ملفات يصل مجموع حجمها إلى {max_mb} ميغابايت، وقد تم استخدام {used_mb} ميغابايت.",
12106
+ attachmentFileTooLarge: "عذراً، حجم الملف كبير جداً. يجب أن يكون حجم الملف أقل من {max_mb} ميغابايت.",
12107
+ attachmentTypeNotAllowed: "عذراً، لا يمكننا استقبال هذا النوع من الملفات. يمكنك إرسال: {types}."
12108
+ },
12109
+ zh: {
12110
+ attachmentLimitReached: "文件过大。此聊天最多可接收共 {max_mb} MB 的文件。",
12111
+ attachmentLimitReachedUsed: "文件过大。此聊天最多可接收共 {max_mb} MB 的文件,目前已使用 {used_mb} MB。",
12112
+ attachmentFileTooLarge: "抱歉,文件过大。文件大小必须小于 {max_mb} MB。",
12113
+ attachmentTypeNotAllowed: "抱歉,无法接收此类文件。您可以发送:{types}。"
12114
+ },
12115
+ ja: {
12116
+ attachmentLimitReached: "ファイルが大きすぎます。このチャットで受け取れるファイルは合計 {max_mb} MB までです。",
12117
+ attachmentLimitReachedUsed: "ファイルが大きすぎます。このチャットで受け取れるファイルは合計 {max_mb} MB までで、すでに {used_mb} MB を使用しています。",
12118
+ attachmentFileTooLarge: "申し訳ありませんが、ファイルが大きすぎます。ファイルは {max_mb} MB 未満にしてください。",
12119
+ attachmentTypeNotAllowed: "申し訳ありませんが、この種類のファイルは受け付けていません。送信できるファイル: {types}"
12120
+ }
12121
+ };
12083
12122
  function translate(locale, key) {
12084
12123
  const normalized = normalizeLocale(locale);
12085
12124
  const dict = DICTIONARIES[normalized] || EN;
12086
12125
  const onboarding = ONBOARDING_PLACEHOLDERS[normalized];
12087
- return onboarding?.[key] ?? dict[key] ?? A11Y_LABELS[normalized]?.[key] ?? ONBOARDING_PLACEHOLDERS.en[key] ?? A11Y_LABELS.en[key] ?? EN[key] ?? key;
12126
+ return onboarding?.[key] ?? dict[key] ?? A11Y_LABELS[normalized]?.[key] ?? ATTACHMENT_REJECTION_TEXTS[normalized]?.[key] ?? ONBOARDING_PLACEHOLDERS.en[key] ?? A11Y_LABELS.en[key] ?? ATTACHMENT_REJECTION_TEXTS.en[key] ?? EN[key] ?? key;
12088
12127
  }
12089
12128
  function createTranslator(locale) {
12090
12129
  const normalized = normalizeLocale(locale);
@@ -12160,14 +12199,16 @@
12160
12199
  * The widget holds its own copy of every /support turn, under its own ids, so
12161
12200
  * on reload (and on the event stream's backfill) it merges from the server
12162
12201
  * only what reached the conversation another way: what a person sent from the
12163
- * Inbox (contentType human_reply, handoff_ack) and a Skill's reply to a staff
12202
+ * Inbox (contentType human_reply, handoff_ack), the away and holding lines
12203
+ * sent while the visitor waits for a person (handoff_wait, EV-4803; backend
12204
+ * services/widgetHandoffLines.ts) and a Skill's reply to a staff
12164
12205
  * turn (staffTurn: Instruct Skill, or a hand-back that resumes the Skill;
12165
12206
  * backend services/staffTurn.ts). Dev, 15 Oct 2026: a Skill reply sent while
12166
12207
  * the Testing lab was closed never showed there, because only a person's
12167
12208
  * replies were merged. Mirrors packages/frontend/src/lib/messages/repliedOutsideStream.ts.
12168
12209
  */
12169
12210
  function isRepliedOutsideStream(m) {
12170
- return m?.contentType === 'human_reply' || m?.contentType === 'handoff_ack' || m?.staffTurn === true;
12211
+ return m?.contentType === 'human_reply' || m?.contentType === 'handoff_ack' || m?.contentType === 'handoff_wait' || m?.staffTurn === true;
12171
12212
  }
12172
12213
 
12173
12214
  /**
@@ -12179,6 +12220,62 @@
12179
12220
  return m?.responder === 'ai' || m?.responder === 'human' ? m.responder : null;
12180
12221
  }
12181
12222
 
12223
+ // EV-4048: human-agent identification in the widget transcript.
12224
+ //
12225
+ // A message pushed by a human agent (inbox reply / takeover) carries
12226
+ // metadata.senderName; AI replies do not. For each such message we expose the
12227
+ // agent's name (rendered above the bubble) and flag the first message from a
12228
+ // given agent in the thread so the list can insert a "<Name> has joined the
12229
+ // conversation" system line. Derived client-side so it also works for the
12230
+ // history fetch and SSE backfill, with no extra backend event.
12231
+
12232
+ function getHumanSenderName(message) {
12233
+ if (!message || message.role === 'user') return null;
12234
+ const name = message.metadata?.senderName;
12235
+ return typeof name === 'string' && name.trim() ? name.trim() : null;
12236
+ }
12237
+
12238
+ // EV-4803: an agent who takes the chat in the Inbox (assign to self) joins
12239
+ // before writing anything. The widget hears an agent_joined event and keeps
12240
+ // { name, afterMessageId }; the line shows there, and that agent's first
12241
+ // reply then skips its own line. Live only: on a reload the line falls back
12242
+ // to the first reply, as before.
12243
+ function computeHumanAgentAnnotations(messages, liveJoined = []) {
12244
+ const result = new Map();
12245
+ if (!Array.isArray(messages)) return result;
12246
+ const seen = new Set((Array.isArray(liveJoined) ? liveJoined : []).map(j => j?.name).filter(Boolean));
12247
+ for (const message of messages) {
12248
+ const name = getHumanSenderName(message);
12249
+ if (!name || !message.id) continue;
12250
+ const joined = !seen.has(name);
12251
+ seen.add(name);
12252
+ result.set(message.id, {
12253
+ name,
12254
+ joined
12255
+ });
12256
+ }
12257
+ return result;
12258
+ }
12259
+
12260
+ /**
12261
+ * EV-4803: live joins by the message they follow (id -> names in order). A
12262
+ * join whose message is gone (or that came before any) goes after the last.
12263
+ */
12264
+ function liveJoinsByMessage(liveJoined, messages = []) {
12265
+ const result = new Map();
12266
+ const list = Array.isArray(messages) ? messages : [];
12267
+ const lastId = list[list.length - 1]?.id;
12268
+ for (const join of Array.isArray(liveJoined) ? liveJoined : []) {
12269
+ if (!join?.name) continue;
12270
+ const at = join.afterMessageId && list.some(m => m.id === join.afterMessageId) ? join.afterMessageId : lastId;
12271
+ if (!at) continue;
12272
+ const names = result.get(at) || [];
12273
+ names.push(join.name);
12274
+ result.set(at, names);
12275
+ }
12276
+ return result;
12277
+ }
12278
+
12182
12279
  /**
12183
12280
  * Clean CJK (Chinese, Japanese, Korean) spaces
12184
12281
  */
@@ -12997,6 +13094,15 @@
12997
13094
  // the live csat_request event, or by /history when the widget was not
12998
13095
  // listening at the time; the chat window shows its rating card for it.
12999
13096
  const [csatRequest, setCsatRequest] = reactExports.useState(null);
13097
+ // EV-4803: agents who joined live (assigned the chat to themselves in the
13098
+ // Inbox) before writing: [{ name, afterMessageId }]. MessageList shows
13099
+ // "<name> has joined the conversation" after that message.
13100
+ const [liveJoined, setLiveJoined] = reactExports.useState([]);
13101
+ // The stream handler outlives renders; this is the list it joins after.
13102
+ const messagesNowRef = reactExports.useRef(messages);
13103
+ reactExports.useEffect(() => {
13104
+ messagesNowRef.current = messages;
13105
+ }, [messages]);
13000
13106
  const isHandedOffRef = reactExports.useRef(false);
13001
13107
  const markHandedOff = reactExports.useCallback(() => {
13002
13108
  isHandedOffRef.current = true;
@@ -13208,6 +13314,19 @@
13208
13314
  setCsatRequest(data.csat);
13209
13315
  return;
13210
13316
  }
13317
+ if (data.type === 'agent_joined' && data.name) {
13318
+ const name = String(data.name).trim();
13319
+ if (!name) return;
13320
+ const now = messagesNowRef.current || [];
13321
+ // Already wrote here: their first reply carries the line.
13322
+ if (now.some(m => getHumanSenderName(m) === name)) return;
13323
+ const afterMessageId = now[now.length - 1]?.id || null;
13324
+ setLiveJoined(joined => joined.some(j => j.name === name) ? joined : [...joined, {
13325
+ name,
13326
+ afterMessageId
13327
+ }]);
13328
+ return;
13329
+ }
13211
13330
  if (data.type === 'conversation_erased') {
13212
13331
  es.close();
13213
13332
  if (handoffEventSourceRef.current === es) handoffEventSourceRef.current = null;
@@ -15435,6 +15554,7 @@
15435
15554
  setBidiMessages([]);
15436
15555
  // The rating request belonged to the conversation being left.
15437
15556
  setCsatRequest(null);
15557
+ setLiveJoined([]);
15438
15558
 
15439
15559
  // Drop the localStorage transcript for the conversation we're abandoning
15440
15560
  // BEFORE we rotate to a fresh sessionId, otherwise the old bucket leaks.
@@ -15685,7 +15805,9 @@
15685
15805
  resetConversation,
15686
15806
  updateMessageFeedback,
15687
15807
  // Rating request from the Inbox (null when none is open).
15688
- csatRequest
15808
+ csatRequest,
15809
+ // EV-4803: agents who joined live, for the "has joined" line.
15810
+ liveJoined
15689
15811
  };
15690
15812
  };
15691
15813
 
@@ -18368,37 +18490,6 @@
18368
18490
  });
18369
18491
  };
18370
18492
 
18371
- // EV-4048: human-agent identification in the widget transcript.
18372
- //
18373
- // A message pushed by a human agent (inbox reply / takeover) carries
18374
- // metadata.senderName; AI replies do not. For each such message we expose the
18375
- // agent's name (rendered above the bubble) and flag the first message from a
18376
- // given agent in the thread so the list can insert a "<Name> has joined the
18377
- // conversation" system line. Derived client-side so it also works for the
18378
- // history fetch and SSE backfill, with no extra backend event.
18379
-
18380
- function getHumanSenderName(message) {
18381
- if (!message || message.role === 'user') return null;
18382
- const name = message.metadata?.senderName;
18383
- return typeof name === 'string' && name.trim() ? name.trim() : null;
18384
- }
18385
- function computeHumanAgentAnnotations(messages) {
18386
- const result = new Map();
18387
- if (!Array.isArray(messages)) return result;
18388
- const seen = new Set();
18389
- for (const message of messages) {
18390
- const name = getHumanSenderName(message);
18391
- if (!name || !message.id) continue;
18392
- const joined = !seen.has(name);
18393
- seen.add(name);
18394
- result.set(message.id, {
18395
- name,
18396
- joined
18397
- });
18398
- }
18399
- return result;
18400
- }
18401
-
18402
18493
  /**
18403
18494
  * Framework-free A2UI core shared by the three A2uiBlock renderers - the
18404
18495
  * agent-sdk, the widget and the frontend Testing Lab (EV-4798 MERN-19).
@@ -18454,6 +18545,8 @@
18454
18545
 
18455
18546
  const MessageList = ({
18456
18547
  messages = [],
18548
+ // EV-4803: agents who joined live: [{ name, afterMessageId }].
18549
+ liveJoined = [],
18457
18550
  isDark,
18458
18551
  primaryColor,
18459
18552
  isLoading,
@@ -18633,7 +18726,19 @@
18633
18726
  // the configured `isRtl` when the text has no strongly-directional character
18634
18727
  // (digits, punctuation, emoji only), same contract as the message bubbles.
18635
18728
  // EV-4048: agent name + first-join system line for human-agent replies.
18636
- const humanAgentAnnotations = reactExports.useMemo(() => computeHumanAgentAnnotations(messages), [messages]);
18729
+ const humanAgentAnnotations = reactExports.useMemo(() => computeHumanAgentAnnotations(messages, liveJoined), [messages, liveJoined]);
18730
+ const liveJoins = reactExports.useMemo(() => liveJoinsByMessage(liveJoined, messages), [liveJoined, messages]);
18731
+ const joinedLine = (name, key) => /*#__PURE__*/jsxRuntimeExports.jsx("div", {
18732
+ role: "status",
18733
+ "data-testid": "agent-joined-line",
18734
+ style: {
18735
+ textAlign: "center",
18736
+ fontSize: "12px",
18737
+ opacity: 0.7,
18738
+ margin: "8px 0"
18739
+ },
18740
+ children: translate(defaultLanguage, "agentJoinedConversation").replace("{name}", name)
18741
+ }, key);
18637
18742
  const disclaimerIsRtl = reactExports.useMemo(() => detectTextDirection(disclaimerText, isRtl), [disclaimerText, isRtl]);
18638
18743
  return /*#__PURE__*/jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, {
18639
18744
  children: [disclaimerEnabled && disclaimerText && disclaimerPosition === "top" && /*#__PURE__*/jsxRuntimeExports.jsx("div", {
@@ -18688,17 +18793,7 @@
18688
18793
  // confirmation / form cards, so a reload cannot re-arm them.
18689
18794
  jsxRuntimeExports.jsxs(A2uiAnsweredContext.Provider, {
18690
18795
  value: message.role === 'assistant' ? findNextUserAnswer(messages, index) : undefined,
18691
- children: [humanAgent?.joined && /*#__PURE__*/jsxRuntimeExports.jsx("div", {
18692
- role: "status",
18693
- "data-testid": "agent-joined-line",
18694
- style: {
18695
- textAlign: "center",
18696
- fontSize: "12px",
18697
- opacity: 0.7,
18698
- margin: "8px 0"
18699
- },
18700
- children: translate(defaultLanguage, "agentJoinedConversation").replace("{name}", humanAgent.name)
18701
- }), humanAgent && /*#__PURE__*/jsxRuntimeExports.jsx("div", {
18796
+ children: [humanAgent?.joined && joinedLine(humanAgent.name), humanAgent && /*#__PURE__*/jsxRuntimeExports.jsx("div", {
18702
18797
  "data-testid": "agent-sender-name",
18703
18798
  style: {
18704
18799
  fontSize: "12px",
@@ -18746,7 +18841,7 @@
18746
18841
  headerTextColor: headerTextColor,
18747
18842
  onRetryMessage: onRetryMessage,
18748
18843
  defaultLanguage: defaultLanguage
18749
- })]
18844
+ }), (liveJoins.get(message.id) || []).map(name => joinedLine(name, `live-join-${name}`))]
18750
18845
  }, message.id)
18751
18846
  );
18752
18847
  }), (() => {
@@ -18840,6 +18935,54 @@
18840
18935
  });
18841
18936
  };
18842
18937
 
18938
+ // A file the chat will not take (over the conversation's upload limit, over
18939
+ // the per-file cap, or a type it does not accept) is answered with a message
18940
+ // from the agent, as the other channels do, instead of a warning chip under
18941
+ // the composer.
18942
+
18943
+ /** The rejections answered with a message; anything else (a network failure) keeps its chip so it can be retried. */
18944
+ const MESSAGE_REJECTION_CODES = new Set(["size_exceeded", "file_too_large", "mime_not_allowed"]);
18945
+
18946
+ // The short names a customer knows, for "You can send PDF, JPG, PNG".
18947
+ const TYPE_LABELS = [["application/pdf", "PDF"], ["image/jpeg", "JPG"], ["image/png", "PNG"], ["image/gif", "GIF"], ["image/webp", "WEBP"], ["application/msword", "DOC"], ["application/vnd.openxmlformats-officedocument.wordprocessingml.document", "DOCX"], ["application/vnd.ms-excel", "XLS"], ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "XLSX"], ["text/csv", "CSV"], ["text/plain", "TXT"]];
18948
+ function allowedTypesLabel(allowedMimes) {
18949
+ const allowed = new Set((allowedMimes || []).map(m => String(m).toLowerCase()));
18950
+ const labels = TYPE_LABELS.filter(([mime]) => allowed.has(mime)).map(([, label]) => label);
18951
+ return labels.join(", ");
18952
+ }
18953
+ const fill = (template, vars) => template.replace(/\{(\w+)\}/g, (_m, k) => vars[k] ?? "");
18954
+ const mb = bytes => {
18955
+ const v = Math.max(0, Number(bytes) || 0) / (1024 * 1024);
18956
+ return v >= 10 ? String(Math.round(v)) : String(Math.round(v * 10) / 10);
18957
+ };
18958
+
18959
+ /**
18960
+ * The agent's reply to a rejected file, in the widget's language.
18961
+ * `rejection`: { code, usedBytes } from useFileUpload; `config`: the upload
18962
+ * policy (fileUploadMaxSizeMB, fileUploadMaxFileMB, fileUploadAllowedMimes).
18963
+ */
18964
+ function attachmentRejectionText(t, rejection, config = {}) {
18965
+ const maxMb = String(config.fileUploadMaxSizeMB ?? "");
18966
+ if (rejection.code === "mime_not_allowed") {
18967
+ const types = allowedTypesLabel(config.fileUploadAllowedMimes);
18968
+ return fill(t("attachmentTypeNotAllowed"), {
18969
+ types: types || "PDF"
18970
+ });
18971
+ }
18972
+ if (rejection.code === "file_too_large") {
18973
+ return fill(t("attachmentFileTooLarge"), {
18974
+ max_mb: String(config.fileUploadMaxFileMB ?? maxMb)
18975
+ });
18976
+ }
18977
+ const used = Number(rejection.usedBytes) || 0;
18978
+ return used > 0 ? fill(t("attachmentLimitReachedUsed"), {
18979
+ max_mb: maxMb,
18980
+ used_mb: mb(used)
18981
+ }) : fill(t("attachmentLimitReached"), {
18982
+ max_mb: maxMb
18983
+ });
18984
+ }
18985
+
18843
18986
  /**
18844
18987
  * useFileUpload - attachment upload flow for the production widget.
18845
18988
  *
@@ -18875,7 +19018,11 @@
18875
19018
  case "size_exceeded":
18876
19019
  case "mime_not_allowed":
18877
19020
  case "storage_unavailable":
19021
+ case "file_too_large":
18878
19022
  return code;
19023
+ // The server's name for "over this conversation's upload limit".
19024
+ case "session_exceeded":
19025
+ return "size_exceeded";
18879
19026
  default:
18880
19027
  return "unknown";
18881
19028
  }
@@ -19053,7 +19200,11 @@
19053
19200
  config,
19054
19201
  apiEndpoint,
19055
19202
  authHeader,
19056
- onError
19203
+ onError,
19204
+ // Called instead of showing an error chip when the chat will not take the
19205
+ // file (MESSAGE_REJECTION_CODES): { code, filename, size, usedBytes }. The
19206
+ // widget answers with a message from the agent, as the other channels do.
19207
+ onRejected
19057
19208
  }) {
19058
19209
  const [attachments, setAttachments] = reactExports.useState([]);
19059
19210
  const inflightRef = reactExports.useRef(new Map());
@@ -19087,6 +19238,19 @@
19087
19238
  } : a));
19088
19239
  }, []);
19089
19240
  const errorPatch = reactExports.useCallback((id, code, message) => {
19241
+ // The server refused the file for the conversation's policy: no chip,
19242
+ // the agent says so instead.
19243
+ if (onRejected && MESSAGE_REJECTION_CODES.has(code)) {
19244
+ const target = attachmentsRef.current.find(a => a.id === id);
19245
+ setAttachments(prev => prev.filter(a => a.id !== id));
19246
+ onRejected({
19247
+ code,
19248
+ filename: target?.filename,
19249
+ size: target?.size,
19250
+ usedBytes: Math.max(0, sessionBytesRef.current.bytes)
19251
+ });
19252
+ return;
19253
+ }
19090
19254
  const patch = {
19091
19255
  status: "error",
19092
19256
  error: {
@@ -19103,7 +19267,7 @@
19103
19267
  if (target && onError) onError(target);
19104
19268
  return patched;
19105
19269
  });
19106
- }, [onError]);
19270
+ }, [onError, onRejected]);
19107
19271
  const validateFile = reactExports.useCallback((file, currentCount) => {
19108
19272
  if (!config?.fileUploadEnabled) {
19109
19273
  return {
@@ -19343,6 +19507,15 @@
19343
19507
  const currentCount = attachmentsRef.current.filter(a => a.status !== "error").length;
19344
19508
  const check = validateFile(file, currentCount);
19345
19509
  const id = nextId();
19510
+ if (!check.ok && onRejected && MESSAGE_REJECTION_CODES.has(check.code)) {
19511
+ onRejected({
19512
+ code: check.code,
19513
+ filename: file.name,
19514
+ size: file.size,
19515
+ usedBytes: Math.max(0, sessionBytesRef.current.bytes)
19516
+ });
19517
+ continue;
19518
+ }
19346
19519
  if (!check.ok) {
19347
19520
  const rejected = {
19348
19521
  id,
@@ -19381,7 +19554,7 @@
19381
19554
  sessionBytesRef.current.bytes += file.size;
19382
19555
  void doUpload(id, file);
19383
19556
  }
19384
- }, [validateFile, doUpload, nextId, onError]);
19557
+ }, [validateFile, doUpload, nextId, onError, onRejected]);
19385
19558
  const removeAttachment = reactExports.useCallback(id => {
19386
19559
  const xhr = inflightRef.current.get(id);
19387
19560
  if (xhr) {
@@ -19702,7 +19875,10 @@
19702
19875
  chatFeatures,
19703
19876
  apiEndpoint,
19704
19877
  organizationId,
19705
- conversationId
19878
+ conversationId,
19879
+ // Adds an agent message (useChatState.addAssistantMessage): a file the
19880
+ // chat will not take is answered this way, as on the other channels.
19881
+ onSynthesizeAssistantMessage
19706
19882
  }) => {
19707
19883
  const t = reactExports.useMemo(() => createTranslator(defaultLanguage), [defaultLanguage]);
19708
19884
  const poweredByParts = reactExports.useMemo(() => getPoweredByParts(defaultLanguage), [defaultLanguage]);
@@ -19736,7 +19912,14 @@
19736
19912
  conversationId,
19737
19913
  config: uploadConfig,
19738
19914
  apiEndpoint,
19739
- authHeader: undefined
19915
+ authHeader: undefined,
19916
+ // Over the upload limit, over the per-file cap or a type the chat does
19917
+ // not take: a message from the agent, not a warning chip.
19918
+ onRejected: onSynthesizeAssistantMessage ? rejection => onSynthesizeAssistantMessage(attachmentRejectionText(t, rejection, {
19919
+ ...uploadConfig,
19920
+ // The platform's per-file cap (backend CHAT_ATTACHMENT_PLATFORM_MAX_MB).
19921
+ fileUploadMaxFileMB: chatFeatures?.fileUploadMaxFileMB ?? 25
19922
+ })) : undefined
19740
19923
  });
19741
19924
  const fileInputRef = reactExports.useRef(null);
19742
19925
 
@@ -21645,6 +21828,8 @@
21645
21828
  // A rating an agent asked for from the Inbox: { format, prompt } or null.
21646
21829
  // Shown whether or not the automatic survey (csatEnabled) is on.
21647
21830
  csatRequest = null,
21831
+ // EV-4803: agents who joined live: [{ name, afterMessageId }].
21832
+ liveJoined = [],
21648
21833
  onPromptSuggestionClick,
21649
21834
  onboardingEnabled = false,
21650
21835
  onResetConversation,
@@ -22425,6 +22610,7 @@
22425
22610
  children: t("sessionExpiringSoon")
22426
22611
  }), /*#__PURE__*/jsxRuntimeExports.jsx(MessageList, {
22427
22612
  messages: messages,
22613
+ liveJoined: liveJoined,
22428
22614
  isDark: isDark,
22429
22615
  primaryColor: primaryColor,
22430
22616
  isLoading: isLoading,
@@ -22500,6 +22686,7 @@
22500
22686
  })]
22501
22687
  })
22502
22688
  }), !(onboardingActive && !onboardingCompleted) && /*#__PURE__*/jsxRuntimeExports.jsx(ChatInput, {
22689
+ onSynthesizeAssistantMessage: onSynthesizeAssistantMessage,
22503
22690
  onStopGeneration: onStopGeneration,
22504
22691
  inputValue: inputValue,
22505
22692
  setInputValue: setInputValue,
@@ -22876,7 +23063,7 @@
22876
23063
  * value by its own test (that constant sat at 0.1.113 while this package was
22877
23064
  * at 0.2.2 precisely because nothing enforced it - EV-2431).
22878
23065
  */
22879
- const WIDGET_VERSION = '0.2.31';
23066
+ const WIDGET_VERSION = '0.2.33';
22880
23067
 
22881
23068
  /**
22882
23069
  * API utility functions
@@ -23297,7 +23484,8 @@
23297
23484
  widgetFilter,
23298
23485
  setWidgetFilter,
23299
23486
  addAssistantMessage,
23300
- csatRequest
23487
+ csatRequest,
23488
+ liveJoined
23301
23489
  } = useChatState(widgetConfig ? {
23302
23490
  welcomeMessage: widgetConfig.welcomeMessage,
23303
23491
  // Per the EV-3813 note below, this hook produces customer-facing
@@ -23521,6 +23709,7 @@
23521
23709
  csatFormat: widgetConfig.csatFormat,
23522
23710
  csatPrompt: widgetConfig.csatPrompt,
23523
23711
  csatRequest: csatRequest,
23712
+ liveJoined: liveJoined,
23524
23713
  onboardingEnabled: widgetConfig.onboardingEnabled,
23525
23714
  onResetConversation: resetConversation,
23526
23715
  sessionExpiringSoon: sessionExpiringSoon,