@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.
- package/dist/{chat-widget-MarkdownRenderer-ozg8BpuC.esm.js → chat-widget-MarkdownRenderer-u47jw-Lf.esm.js} +1 -1
- package/dist/chat-widget-index-ChCWDRfs.esm.js +20 -0
- package/dist/chat-widget.esm.js +1 -1
- package/dist/chat-widget.js +248 -59
- package/dist/chat-widget.min.js +6 -6
- package/dist/chat-widget.umd.js +8 -8
- package/package.json +1 -1
- package/dist/chat-widget-index-DGutMRlI.esm.js +0 -20
package/dist/chat-widget.esm.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{S as default}from"./chat-widget-index-
|
|
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";
|
package/dist/chat-widget.js
CHANGED
|
@@ -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)
|
|
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.
|
|
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,
|