@pindai-ai/chat-widget 3.0.0 → 3.0.2
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/pindai-chat-widget.css +1 -1
- package/dist/pindai-chat-widget.js +193 -138
- package/dist/pindai-chat-widget.js.map +1 -1
- package/dist/pindai-chat-widget.umd.js +8 -6
- package/dist/pindai-chat-widget.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/i18n.js +2 -0
- package/src/main.js +123 -5
- package/src/style.css +95 -1
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
var d = (u, e,
|
|
1
|
+
var d = (u, e, i) => new Promise((t, s) => {
|
|
2
2
|
var a = (l) => {
|
|
3
3
|
try {
|
|
4
|
-
r(
|
|
4
|
+
r(i.next(l));
|
|
5
5
|
} catch (o) {
|
|
6
6
|
s(o);
|
|
7
7
|
}
|
|
8
8
|
}, n = (l) => {
|
|
9
9
|
try {
|
|
10
|
-
r(
|
|
10
|
+
r(i.throw(l));
|
|
11
11
|
} catch (o) {
|
|
12
12
|
s(o);
|
|
13
13
|
}
|
|
14
|
-
}, r = (l) => l.done ?
|
|
15
|
-
r((
|
|
14
|
+
}, r = (l) => l.done ? t(l.value) : Promise.resolve(l.value).then(a, n);
|
|
15
|
+
r((i = i.apply(u, e)).next());
|
|
16
16
|
});
|
|
17
17
|
const g = {
|
|
18
18
|
en: {
|
|
@@ -65,7 +65,8 @@ const g = {
|
|
|
65
65
|
humanAgentLabel: "Support Agent",
|
|
66
66
|
actionIndicator: "Performing action...",
|
|
67
67
|
thinkingIndicator: "Thinking...",
|
|
68
|
-
poweredBy: "Powered by"
|
|
68
|
+
poweredBy: "Powered by",
|
|
69
|
+
waitingForAgent: "Waiting for a human agent..."
|
|
69
70
|
},
|
|
70
71
|
id: {
|
|
71
72
|
// Widget UI
|
|
@@ -117,7 +118,8 @@ const g = {
|
|
|
117
118
|
humanAgentLabel: "Agen Dukungan",
|
|
118
119
|
actionIndicator: "Sedang memproses...",
|
|
119
120
|
thinkingIndicator: "Sedang berpikir...",
|
|
120
|
-
poweredBy: "Dibuat dengan"
|
|
121
|
+
poweredBy: "Dibuat dengan",
|
|
122
|
+
waitingForAgent: "Menunggu agen manusia..."
|
|
121
123
|
}
|
|
122
124
|
};
|
|
123
125
|
class y {
|
|
@@ -136,13 +138,13 @@ class y {
|
|
|
136
138
|
* @param {object} params - Parameters to substitute in the translation
|
|
137
139
|
* @returns {string} Translated string
|
|
138
140
|
*/
|
|
139
|
-
t(e,
|
|
141
|
+
t(e, i = {}) {
|
|
140
142
|
var s;
|
|
141
|
-
let
|
|
142
|
-
return Object.keys(
|
|
143
|
+
let t = ((s = g[this.locale]) == null ? void 0 : s[e]) || g.en[e] || e;
|
|
144
|
+
return Object.keys(i).forEach((a) => {
|
|
143
145
|
const n = new RegExp(`\\{${a}\\}`, "g");
|
|
144
|
-
|
|
145
|
-
}),
|
|
146
|
+
t = t.replace(n, i[a]);
|
|
147
|
+
}), t;
|
|
146
148
|
}
|
|
147
149
|
/**
|
|
148
150
|
* Change the current locale
|
|
@@ -169,12 +171,12 @@ class y {
|
|
|
169
171
|
class w {
|
|
170
172
|
constructor(e) {
|
|
171
173
|
this.agentId = e.agentId || null, this.embedSecret = e.embedSecret || null, this.apiBaseUrl = e.apiBaseUrl ? e.apiBaseUrl.replace(/\/$/, "") : null;
|
|
172
|
-
const
|
|
173
|
-
if (!
|
|
174
|
+
const i = e.webhookUrl || e.n8nUrl, t = !!(this.agentId && this.embedSecret && this.apiBaseUrl);
|
|
175
|
+
if (!t && !!!i)
|
|
174
176
|
throw new Error(
|
|
175
177
|
'PindaiChatWidget: Provide either (agentId + embedSecret + apiBaseUrl) for Pindai Agent-API mode, or "webhookUrl" for generic webhook mode.'
|
|
176
178
|
);
|
|
177
|
-
this.webhookUrl =
|
|
179
|
+
this.webhookUrl = t ? `${this.apiBaseUrl}/v1/chat/${this.agentId}/message` : i, this._agentMode = t, this._embedToken = null, this.mode = e.mode || "widget", this.locale = e.locale || "id", this.i18n = new y(this.locale), this.title = e.title || this.i18n.t("title"), this.initialMessage = e.initialMessage || this.i18n.t("initialMessage"), this.launcherIconUrl = e.launcherIconUrl || this._getDefaultLauncherIcon(), this.logoUrl = e.logoUrl || "https://pindai.ai/logo.png", this.showLogo = e.showLogo !== !1, this.avatarUrl = e.avatarUrl || null, this.accentColor = e.accentColor || "#2563eb", this.launcherColor = e.launcherColor || this.accentColor, this.sendButtonColor = e.sendButtonColor || this.accentColor, this.theme = e.theme || "light", this.buttonAlignment = e.buttonAlignment || "bottom-right";
|
|
178
180
|
const a = e.bubbleMessages || (e.bubbleText ? [e.bubbleText] : null);
|
|
179
181
|
this.bubbleMessages = a && a.length > 0 ? a : null, this.bubbleDelay = typeof e.bubbleDelay == "number" ? e.bubbleDelay : 3e3, this.bubbleInterval = typeof e.bubbleInterval == "number" ? e.bubbleInterval : 5e3, this.showBubbleOnce = e.showBubbleOnce !== !1, this._bubbleDismissed = !1, this._bubbleMsgIndex = 0, this._bubbleRotateTimer = null, this._bubbleDelayTimer = null, this.customFooter = e.customFooter || null, this.showBranding = e.showBranding !== !1, this.showActionIndicators = e.showActionIndicators !== !1, this.visitorName = e.visitorName || null, this.visitorEmail = e.visitorEmail || null, this.enableStreaming = this._agentMode && e.enableStreaming !== !1, this.enableFileUpload = e.enableFileUpload !== !1, this.allowedFileTypes = e.allowedFileTypes || [
|
|
180
182
|
"image/jpeg",
|
|
@@ -193,7 +195,20 @@ class w {
|
|
|
193
195
|
this.i18n.t("quickReply4")
|
|
194
196
|
], this.enableHistory = e.enableHistory !== !1, this.maxHistoryItems = e.maxHistoryItems || 50;
|
|
195
197
|
const n = this._agentMode ? this.agentId : this.webhookUrl;
|
|
196
|
-
this.historyKey = `pindai-chat-history-${n}`, this.stateKey = `pindai-chat-state-${n}`, this.maxRetries = e.maxRetries || 3, this.retryDelay = e.retryDelay || 1e3, this.requestTimeout = e.requestTimeout || 3e4, this.rateLimit = e.rateLimit || 5, this.rateLimitWindow = e.rateLimitWindow || 6e4, this.messageTimes = [], this.container = null, this.launcher = null, this.bubblePopup = null, this.chatWindow = null, this.messageList = null, this.input = null, this.button = null, this.closeButton = null, this.pollingUrl = this._agentMode ? `${this.apiBaseUrl}/v1/chat/${this.agentId}/messages` : e.pollingUrl || null, this.pollingEnabled = !!(this.pollingUrl || e.pollingUrl), this.pollingInterval = e.pollingInterval || 3e3, this._pollingTimer = null, this._lastMessageTimestamp = null
|
|
198
|
+
this.historyKey = `pindai-chat-history-${n}`, this.stateKey = `pindai-chat-state-${n}`, this.maxRetries = e.maxRetries || 3, this.retryDelay = e.retryDelay || 1e3, this.requestTimeout = e.requestTimeout || 3e4, this.rateLimit = e.rateLimit || 5, this.rateLimitWindow = e.rateLimitWindow || 6e4, this.messageTimes = [], this.container = null, this.launcher = null, this.bubblePopup = null, this.chatWindow = null, this.messageList = null, this.input = null, this.button = null, this.closeButton = null, this.pollingUrl = this._agentMode ? `${this.apiBaseUrl}/v1/chat/${this.agentId}/messages` : e.pollingUrl || null, this.pollingEnabled = !!(this.pollingUrl || e.pollingUrl), this.pollingInterval = e.pollingInterval || 3e3, this._pollingTimer = null, this._lastMessageTimestamp = null;
|
|
199
|
+
const r = this._agentMode ? `pindai-session-id-${this.agentId}` : null, l = r ? (() => {
|
|
200
|
+
try {
|
|
201
|
+
return localStorage.getItem(r);
|
|
202
|
+
} catch (o) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
})() : null;
|
|
206
|
+
if (this.sessionId = l || `web-session-${Date.now()}-${Math.random()}`, r && !l)
|
|
207
|
+
try {
|
|
208
|
+
localStorage.setItem(r, this.sessionId);
|
|
209
|
+
} catch (o) {
|
|
210
|
+
}
|
|
211
|
+
this.isLoading = !1, this.isOpen = !1, this.isOnline = navigator.onLine, this.loadState(), this._setupOfflineDetection(), this._applyTheme(), this.mode === "fullscreen" ? this._initChatWindow() : this._initLauncher();
|
|
197
212
|
}
|
|
198
213
|
// ─────────────────────────────────────────────────────────────────────────
|
|
199
214
|
// Auth helpers (Agent-API mode)
|
|
@@ -202,10 +217,10 @@ class w {
|
|
|
202
217
|
* Convert hex string to Uint8Array
|
|
203
218
|
*/
|
|
204
219
|
_hexToBytes(e) {
|
|
205
|
-
const
|
|
206
|
-
for (let
|
|
207
|
-
t
|
|
208
|
-
return
|
|
220
|
+
const i = new Uint8Array(e.length / 2);
|
|
221
|
+
for (let t = 0; t < e.length; t += 2)
|
|
222
|
+
i[t / 2] = parseInt(e.slice(t, t + 2), 16);
|
|
223
|
+
return i;
|
|
209
224
|
}
|
|
210
225
|
/**
|
|
211
226
|
* Generate HMAC-SHA256 embed token using native crypto.subtle
|
|
@@ -220,8 +235,8 @@ class w {
|
|
|
220
235
|
{ name: "HMAC", hash: "SHA-256" },
|
|
221
236
|
!1,
|
|
222
237
|
["sign"]
|
|
223
|
-
),
|
|
224
|
-
return this._embedToken = Array.from(new Uint8Array(
|
|
238
|
+
), i = new TextEncoder().encode(`${this.agentId}:${this.sessionId}`), t = yield crypto.subtle.sign("HMAC", e, i);
|
|
239
|
+
return this._embedToken = Array.from(new Uint8Array(t)).map((s) => s.toString(16).padStart(2, "0")).join(""), this._embedToken;
|
|
225
240
|
});
|
|
226
241
|
}
|
|
227
242
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -231,13 +246,13 @@ class w {
|
|
|
231
246
|
if (this.theme === "dark")
|
|
232
247
|
document.documentElement.setAttribute("data-pindai-theme", "dark");
|
|
233
248
|
else if (this.theme === "auto") {
|
|
234
|
-
const e = window.matchMedia("(prefers-color-scheme: dark)"),
|
|
249
|
+
const e = window.matchMedia("(prefers-color-scheme: dark)"), i = (t) => {
|
|
235
250
|
document.documentElement.setAttribute(
|
|
236
251
|
"data-pindai-theme",
|
|
237
|
-
|
|
252
|
+
t.matches ? "dark" : "light"
|
|
238
253
|
);
|
|
239
254
|
};
|
|
240
|
-
|
|
255
|
+
i(e), e.addEventListener("change", i);
|
|
241
256
|
}
|
|
242
257
|
}
|
|
243
258
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -259,12 +274,12 @@ class w {
|
|
|
259
274
|
}, this.bubbleDelay);
|
|
260
275
|
}
|
|
261
276
|
this.launcher = document.createElement("div"), this.launcher.className = "n8n-chat-launcher", this.launcher.style.backgroundColor = this.launcherColor, this.launcher.setAttribute("role", "button"), this.launcher.setAttribute("aria-label", this.i18n.t("ariaOpenChat")), this.launcher.setAttribute("tabindex", "0");
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
277
|
+
const i = document.createElement("img");
|
|
278
|
+
i.src = this.launcherIconUrl, i.alt = "", i.onerror = () => {
|
|
279
|
+
i.onerror = null, i.src = this._getDefaultLauncherIcon();
|
|
265
280
|
};
|
|
266
|
-
const
|
|
267
|
-
|
|
281
|
+
const t = document.createElement("span");
|
|
282
|
+
t.className = "n8n-chat-unread-badge", t.style.display = "none", t.textContent = "0", this.launcher.appendChild(i), this.launcher.appendChild(t), e.appendChild(this.launcher), document.body.appendChild(e), this.launcher.addEventListener("click", () => this.toggleChatWindow()), this.launcher.addEventListener("keydown", (s) => {
|
|
268
283
|
(s.key === "Enter" || s.key === " ") && (s.preventDefault(), this.toggleChatWindow());
|
|
269
284
|
});
|
|
270
285
|
}
|
|
@@ -291,7 +306,7 @@ class w {
|
|
|
291
306
|
// Chat Window
|
|
292
307
|
// ─────────────────────────────────────────────────────────────────────────
|
|
293
308
|
_initChatWindow() {
|
|
294
|
-
this.container = document.createElement("div"), this.container.className = `n8n-chat-widget ${this.mode === "fullscreen" ? "n8n-chat-widget--fullscreen" : ""}`, this.container.setAttribute("role", "dialog"), this.container.setAttribute("aria-modal", "true"), this.container.setAttribute("aria-label", this.title);
|
|
309
|
+
this.container = document.createElement("div"), this.container.className = `n8n-chat-widget ${this.mode === "fullscreen" ? "n8n-chat-widget--fullscreen" : ""}`, this.container.setAttribute("role", "dialog"), this.container.setAttribute("aria-modal", "true"), this.container.setAttribute("aria-label", this.title), this.container.style.setProperty("--pindai-primary", this.accentColor), this.container.style.setProperty("--pindai-primary-dark", this.accentColor);
|
|
295
310
|
const e = this._buildFooterHtml();
|
|
296
311
|
this.container.innerHTML = `
|
|
297
312
|
<div class="n8n-chat-header">
|
|
@@ -320,11 +335,11 @@ class w {
|
|
|
320
335
|
</button>
|
|
321
336
|
</div>
|
|
322
337
|
${this.enableFileUpload ? '<div class="n8n-chat-file-preview" style="display: none;"></div>' : ""}
|
|
323
|
-
`, this._injectHeaderImage(), this.mode === "widget" ? document.body.appendChild(this.container) : (document.body.innerHTML = "", document.body.appendChild(this.container), document.body.style.margin = "0"), this.messageList = this.container.querySelector(".n8n-chat-messages"), this.input = this.container.querySelector('input[type="text"]'), this.button = this.container.querySelector(".n8n-chat-send-btn"), this.closeButton = this.container.querySelector(".n8n-chat-close-btn"), this.button.addEventListener("click", (
|
|
324
|
-
|
|
325
|
-
}), this.input.addEventListener("keypress", (
|
|
326
|
-
|
|
327
|
-
}), this.mode === "fullscreen" ? this.closeButton.style.display = "none" : this.closeButton.addEventListener("click", () => this.toggleChatWindow()), this.enableFileUpload && this.container.querySelector('input[type="file"]').addEventListener("change", (
|
|
338
|
+
`, this._injectHeaderImage(), this.mode === "widget" ? document.body.appendChild(this.container) : (document.body.innerHTML = "", document.body.appendChild(this.container), document.body.style.margin = "0"), this.messageList = this.container.querySelector(".n8n-chat-messages"), this.input = this.container.querySelector('input[type="text"]'), this.button = this.container.querySelector(".n8n-chat-send-btn"), this.closeButton = this.container.querySelector(".n8n-chat-close-btn"), this.button.addEventListener("click", (i) => {
|
|
339
|
+
i.preventDefault(), this.sendMessage();
|
|
340
|
+
}), this.input.addEventListener("keypress", (i) => {
|
|
341
|
+
i.key === "Enter" && (i.preventDefault(), this.sendMessage());
|
|
342
|
+
}), this.mode === "fullscreen" ? this.closeButton.style.display = "none" : this.closeButton.addEventListener("click", () => this.toggleChatWindow()), this.enableFileUpload && this.container.querySelector('input[type="file"]').addEventListener("change", (t) => this._handleFileSelect(t)), this._setupKeyboardNavigation(), this.loadHistory(), this.messageList.children.length === 0 && this.addMessage(this.initialMessage, "ai");
|
|
328
343
|
}
|
|
329
344
|
/**
|
|
330
345
|
* Inject the header avatar/logo as a real DOM element so we can attach
|
|
@@ -335,17 +350,17 @@ class w {
|
|
|
335
350
|
_injectHeaderImage() {
|
|
336
351
|
const e = this.container.querySelector(".n8n-chat-header-content");
|
|
337
352
|
if (!e) return;
|
|
338
|
-
const
|
|
353
|
+
const i = e.querySelector(".n8n-chat-title");
|
|
339
354
|
if (this.avatarUrl) {
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}, e.insertBefore(
|
|
355
|
+
const t = document.createElement("img");
|
|
356
|
+
t.className = "n8n-chat-header-avatar", t.alt = "", t.src = this.avatarUrl, t.onerror = () => {
|
|
357
|
+
t.onerror = null, t.remove();
|
|
358
|
+
}, e.insertBefore(t, i);
|
|
344
359
|
} else if (this.showLogo) {
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}, e.insertBefore(
|
|
360
|
+
const t = document.createElement("img");
|
|
361
|
+
t.className = "n8n-chat-logo", t.alt = "", t.src = this.logoUrl, t.onerror = () => {
|
|
362
|
+
t.onerror = null, t.remove();
|
|
363
|
+
}, e.insertBefore(t, i);
|
|
349
364
|
}
|
|
350
365
|
}
|
|
351
366
|
/**
|
|
@@ -373,6 +388,28 @@ class w {
|
|
|
373
388
|
_escapeHtml(e) {
|
|
374
389
|
return String(e).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
375
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* Safe markdown renderer for AI messages.
|
|
393
|
+
* Supports: bold, italic, inline code, fenced code blocks, headings,
|
|
394
|
+
* unordered/ordered lists, links. Always escapes HTML first to prevent XSS.
|
|
395
|
+
*/
|
|
396
|
+
_renderMarkdown(e) {
|
|
397
|
+
const i = [];
|
|
398
|
+
let t = String(e).replace(/```([\s\S]*?)```/g, (s, a) => {
|
|
399
|
+
const n = i.length;
|
|
400
|
+
return i.push(`<pre><code>${this._escapeHtml(a.replace(/^\n/, "").replace(/\n$/, ""))}</code></pre>`), `\0CODE${n}\0`;
|
|
401
|
+
});
|
|
402
|
+
return t = this._escapeHtml(t), t = t.replace(/`([^`]+)`/g, "<code>$1</code>"), t = t.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>"), t = t.replace(/__([^_]+)__/g, "<strong>$1</strong>"), t = t.replace(/\*([^*]+)\*/g, "<em>$1</em>"), t = t.replace(/_([^_]+)_/g, "<em>$1</em>"), t = t.replace(/^### (.+)$/gm, "<h5>$1</h5>"), t = t.replace(/^## (.+)$/gm, "<h4>$1</h4>"), t = t.replace(/^# (.+)$/gm, "<h3>$1</h3>"), t = t.replace(/^(?:[*-] .+\n?)+/gm, (s) => `<ul>${s.trim().split(`
|
|
403
|
+
`).map(
|
|
404
|
+
(n) => `<li>${n.replace(/^[*-] /, "")}</li>`
|
|
405
|
+
).join("")}</ul>`), t = t.replace(/^(?:\d+\. .+\n?)+/gm, (s) => `<ol>${s.trim().split(`
|
|
406
|
+
`).map(
|
|
407
|
+
(n) => `<li>${n.replace(/^\d+\. /, "")}</li>`
|
|
408
|
+
).join("")}</ol>`), t = t.replace(
|
|
409
|
+
/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g,
|
|
410
|
+
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>'
|
|
411
|
+
), t = t.replace(/\n/g, "<br>"), t = t.replace(/\x00CODE(\d+)\x00/g, (s, a) => i[parseInt(a)]), t;
|
|
412
|
+
}
|
|
376
413
|
// ─────────────────────────────────────────────────────────────────────────
|
|
377
414
|
// Toggle open/close
|
|
378
415
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -391,8 +428,8 @@ class w {
|
|
|
391
428
|
// Timestamp formatting
|
|
392
429
|
// ─────────────────────────────────────────────────────────────────────────
|
|
393
430
|
_formatTimestamp(e) {
|
|
394
|
-
const
|
|
395
|
-
return
|
|
431
|
+
const t = /* @__PURE__ */ new Date() - e;
|
|
432
|
+
return t < 6e4 ? this.i18n.t("justNow") : t < 36e5 ? this.i18n.t("minutesAgo", { minutes: Math.floor(t / 6e4) }) : e.toLocaleTimeString(this.locale === "id" ? "id-ID" : "en-US", {
|
|
396
433
|
hour: "2-digit",
|
|
397
434
|
minute: "2-digit"
|
|
398
435
|
});
|
|
@@ -400,13 +437,13 @@ class w {
|
|
|
400
437
|
// ─────────────────────────────────────────────────────────────────────────
|
|
401
438
|
// Messages
|
|
402
439
|
// ─────────────────────────────────────────────────────────────────────────
|
|
403
|
-
addMessage(e,
|
|
440
|
+
addMessage(e, i, t = /* @__PURE__ */ new Date()) {
|
|
404
441
|
const s = document.createElement("div");
|
|
405
|
-
s.className = `n8n-chat-bubble n8n-chat-${
|
|
442
|
+
s.className = `n8n-chat-bubble n8n-chat-${i}-message`;
|
|
406
443
|
const a = document.createElement("div");
|
|
407
|
-
a.className = "n8n-chat-message-text", a.textContent = e;
|
|
444
|
+
a.className = "n8n-chat-message-text", i === "ai" ? a.innerHTML = this._renderMarkdown(e) : a.textContent = e;
|
|
408
445
|
const n = document.createElement("div");
|
|
409
|
-
n.className = "n8n-chat-message-timestamp", n.textContent = this._formatTimestamp(
|
|
446
|
+
n.className = "n8n-chat-message-timestamp", n.textContent = this._formatTimestamp(t), n.setAttribute("data-timestamp", t.toISOString()), s.appendChild(a), s.appendChild(n), this.messageList.appendChild(s), this.messageList.scrollTop = this.messageList.scrollHeight, this.saveToHistory(e, i, t), !this.isOpen && i === "ai" && this._incrementUnread();
|
|
410
447
|
}
|
|
411
448
|
/**
|
|
412
449
|
* Create a streaming message bubble (text filled incrementally via SSE).
|
|
@@ -415,69 +452,69 @@ class w {
|
|
|
415
452
|
_createStreamingBubble() {
|
|
416
453
|
const e = document.createElement("div");
|
|
417
454
|
e.className = "n8n-chat-bubble n8n-chat-ai-message n8n-chat-bubble--streaming";
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
const
|
|
421
|
-
|
|
455
|
+
const i = document.createElement("div");
|
|
456
|
+
i.className = "n8n-chat-message-text", i.textContent = "";
|
|
457
|
+
const t = document.createElement("span");
|
|
458
|
+
t.className = "n8n-chat-stream-cursor";
|
|
422
459
|
const s = document.createElement("div");
|
|
423
|
-
return s.className = "n8n-chat-message-timestamp", s.textContent = this._formatTimestamp(/* @__PURE__ */ new Date()), e.appendChild(
|
|
460
|
+
return s.className = "n8n-chat-message-timestamp", s.textContent = this._formatTimestamp(/* @__PURE__ */ new Date()), e.appendChild(i), e.appendChild(t), e.appendChild(s), this.messageList.appendChild(e), this.messageList.scrollTop = this.messageList.scrollHeight, { bubble: e, textNode: i, cursor: t, timeNode: s };
|
|
424
461
|
}
|
|
425
|
-
addMessageWithoutSaving(e,
|
|
462
|
+
addMessageWithoutSaving(e, i, t) {
|
|
426
463
|
const s = document.createElement("div");
|
|
427
|
-
s.className = `n8n-chat-bubble n8n-chat-${
|
|
464
|
+
s.className = `n8n-chat-bubble n8n-chat-${i}-message`;
|
|
428
465
|
const a = document.createElement("div");
|
|
429
|
-
a.className = "n8n-chat-message-text", a.textContent = e;
|
|
466
|
+
a.className = "n8n-chat-message-text", i === "ai" ? a.innerHTML = this._renderMarkdown(e) : a.textContent = e;
|
|
430
467
|
const n = document.createElement("div");
|
|
431
|
-
n.className = "n8n-chat-message-timestamp", n.textContent = this._formatTimestamp(
|
|
468
|
+
n.className = "n8n-chat-message-timestamp", n.textContent = this._formatTimestamp(t), n.setAttribute("data-timestamp", t.toISOString()), s.appendChild(a), s.appendChild(n), this.messageList.appendChild(s), this.messageList.scrollTop = this.messageList.scrollHeight;
|
|
432
469
|
}
|
|
433
470
|
// ─────────────────────────────────────────────────────────────────────────
|
|
434
471
|
// Typing indicator
|
|
435
472
|
// ─────────────────────────────────────────────────────────────────────────
|
|
436
473
|
_showTypingIndicator(e) {
|
|
437
|
-
let
|
|
474
|
+
let i = this.messageList.querySelector(".n8n-chat-typing-indicator");
|
|
438
475
|
if (e) {
|
|
439
|
-
if (!
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
this.showActionIndicators ?
|
|
443
|
-
<div class="n8n-chat-typing-dots">${
|
|
476
|
+
if (!i) {
|
|
477
|
+
i = document.createElement("div"), i.className = "n8n-chat-bubble n8n-chat-ai-message n8n-chat-typing-indicator";
|
|
478
|
+
const t = "<span></span><span></span><span></span>";
|
|
479
|
+
this.showActionIndicators ? i.innerHTML = `
|
|
480
|
+
<div class="n8n-chat-typing-dots">${t}</div>
|
|
444
481
|
<span class="n8n-chat-typing-label">${this.i18n.t("thinkingIndicator")}</span>
|
|
445
|
-
` :
|
|
482
|
+
` : i.innerHTML = t, i.setAttribute("aria-label", this.i18n.t("typingIndicator")), this.messageList.appendChild(i), this.messageList.scrollTop = this.messageList.scrollHeight;
|
|
446
483
|
}
|
|
447
|
-
} else
|
|
484
|
+
} else i && i.remove();
|
|
448
485
|
}
|
|
449
486
|
// ─────────────────────────────────────────────────────────────────────────
|
|
450
487
|
// File upload
|
|
451
488
|
// ─────────────────────────────────────────────────────────────────────────
|
|
452
489
|
_handleFileSelect(e) {
|
|
453
|
-
Array.from(e.target.files).forEach((
|
|
454
|
-
if (!this.allowedFileTypes.includes(
|
|
455
|
-
this.addMessage(this.i18n.t("fileTypeNotSupported", { filename:
|
|
490
|
+
Array.from(e.target.files).forEach((t) => {
|
|
491
|
+
if (!this.allowedFileTypes.includes(t.type)) {
|
|
492
|
+
this.addMessage(this.i18n.t("fileTypeNotSupported", { filename: t.name }), "ai");
|
|
456
493
|
return;
|
|
457
494
|
}
|
|
458
|
-
if (
|
|
495
|
+
if (t.size > this.maxFileSize) {
|
|
459
496
|
const s = this.maxFileSize / 1024 / 1024;
|
|
460
|
-
this.addMessage(this.i18n.t("fileTooLarge", { filename:
|
|
497
|
+
this.addMessage(this.i18n.t("fileTooLarge", { filename: t.name, maxSize: s }), "ai");
|
|
461
498
|
return;
|
|
462
499
|
}
|
|
463
500
|
if (this.uploadedFiles.length >= this.maxFiles) {
|
|
464
501
|
this.addMessage(this.i18n.t("maxFilesExceeded", { maxFiles: this.maxFiles }), "ai");
|
|
465
502
|
return;
|
|
466
503
|
}
|
|
467
|
-
this.uploadedFiles.push(
|
|
504
|
+
this.uploadedFiles.push(t), this._renderFilePreview(t);
|
|
468
505
|
}), e.target.value = "";
|
|
469
506
|
}
|
|
470
507
|
_renderFilePreview(e) {
|
|
471
|
-
const
|
|
472
|
-
if (!
|
|
473
|
-
|
|
474
|
-
const
|
|
475
|
-
|
|
508
|
+
const i = this.container.querySelector(".n8n-chat-file-preview");
|
|
509
|
+
if (!i) return;
|
|
510
|
+
i.style.display = "flex";
|
|
511
|
+
const t = document.createElement("div");
|
|
512
|
+
t.className = "n8n-chat-file-item", t.innerHTML = `
|
|
476
513
|
<span class="n8n-chat-file-name">${this._escapeHtml(e.name)}</span>
|
|
477
514
|
<button class="n8n-chat-file-remove" aria-label="${this.i18n.t("ariaRemoveFile")}">×</button>
|
|
478
|
-
`,
|
|
479
|
-
this.uploadedFiles = this.uploadedFiles.filter((s) => s.name !== e.name),
|
|
480
|
-
}),
|
|
515
|
+
`, t.querySelector(".n8n-chat-file-remove").addEventListener("click", () => {
|
|
516
|
+
this.uploadedFiles = this.uploadedFiles.filter((s) => s.name !== e.name), t.remove(), this.uploadedFiles.length === 0 && (i.style.display = "none");
|
|
517
|
+
}), i.appendChild(t);
|
|
481
518
|
}
|
|
482
519
|
// ─────────────────────────────────────────────────────────────────────────
|
|
483
520
|
// Send message — entry point
|
|
@@ -488,8 +525,8 @@ class w {
|
|
|
488
525
|
if (!(!e && this.uploadedFiles.length === 0 || this.isLoading)) {
|
|
489
526
|
try {
|
|
490
527
|
this._checkRateLimit();
|
|
491
|
-
} catch (
|
|
492
|
-
this.addMessage(
|
|
528
|
+
} catch (i) {
|
|
529
|
+
this.addMessage(i.message, "ai");
|
|
493
530
|
return;
|
|
494
531
|
}
|
|
495
532
|
if (!this.isOnline) {
|
|
@@ -501,16 +538,16 @@ class w {
|
|
|
501
538
|
if (this._agentMode && this.enableStreaming && this.uploadedFiles.length === 0)
|
|
502
539
|
yield this._sendWithStreaming(e);
|
|
503
540
|
else {
|
|
504
|
-
const
|
|
505
|
-
this._showTypingIndicator(!1), this.addMessage(
|
|
541
|
+
const i = yield this._sendMessageWithRetry(e, this.uploadedFiles);
|
|
542
|
+
this._showTypingIndicator(!1), this.addMessage(i, "ai"), this.showQuickReplies && this.quickReplies.length > 0 && this._renderQuickReplies();
|
|
506
543
|
}
|
|
507
|
-
} catch (
|
|
508
|
-
this._showTypingIndicator(!1), this.addMessage(this._getErrorMessage(
|
|
544
|
+
} catch (i) {
|
|
545
|
+
this._showTypingIndicator(!1), this.addMessage(this._getErrorMessage(i), "ai");
|
|
509
546
|
} finally {
|
|
510
547
|
if (this.isLoading = !1, this.button.disabled = !1, this.input.disabled = !1, this.input.focus(), this.uploadedFiles.length > 0) {
|
|
511
548
|
this.uploadedFiles = [];
|
|
512
|
-
const
|
|
513
|
-
|
|
549
|
+
const i = this.container.querySelector(".n8n-chat-file-preview");
|
|
550
|
+
i && (i.innerHTML = "", i.style.display = "none");
|
|
514
551
|
}
|
|
515
552
|
}
|
|
516
553
|
}
|
|
@@ -521,13 +558,13 @@ class w {
|
|
|
521
558
|
// ─────────────────────────────────────────────────────────────────────────
|
|
522
559
|
_sendWithStreaming(e) {
|
|
523
560
|
return d(this, null, function* () {
|
|
524
|
-
const
|
|
561
|
+
const i = yield this._generateEmbedToken(), t = new URLSearchParams({
|
|
525
562
|
message: e,
|
|
526
563
|
sessionId: this.sessionId,
|
|
527
|
-
embedToken:
|
|
564
|
+
embedToken: i
|
|
528
565
|
});
|
|
529
|
-
this.visitorName &&
|
|
530
|
-
const s = `${this.apiBaseUrl}/v1/chat/${this.agentId}/stream?${
|
|
566
|
+
this.visitorName && t.set("visitor_name", this.visitorName), this.visitorEmail && t.set("visitor_email", this.visitorEmail);
|
|
567
|
+
const s = `${this.apiBaseUrl}/v1/chat/${this.agentId}/stream?${t}`;
|
|
531
568
|
return new Promise((a, n) => {
|
|
532
569
|
this._showTypingIndicator(!1);
|
|
533
570
|
const { bubble: r, textNode: l, cursor: o, timeNode: c } = this._createStreamingBubble();
|
|
@@ -536,7 +573,7 @@ class w {
|
|
|
536
573
|
p.onmessage = (f) => {
|
|
537
574
|
try {
|
|
538
575
|
const m = JSON.parse(f.data);
|
|
539
|
-
m.delta && (h += m.delta, l.textContent = h, this.messageList.scrollTop = this.messageList.scrollHeight), m.type === "done" && (p.close(), o.remove(), r.classList.remove("n8n-chat-bubble--streaming"), c.textContent = this._formatTimestamp(/* @__PURE__ */ new Date()), b = m.status || "active", this.saveToHistory(h, "ai"), b && b !== "active" && (this._startPolling(), this.addMessage(this.i18n.t("humanAgentJoined"), "ai")), this.showQuickReplies && this.quickReplies.length > 0 && this._renderQuickReplies(), a(h));
|
|
576
|
+
m.delta && (h += m.delta, l.textContent = h, this.messageList.scrollTop = this.messageList.scrollHeight), m.type === "done" && (p.close(), o.remove(), r.classList.remove("n8n-chat-bubble--streaming"), c.textContent = this._formatTimestamp(/* @__PURE__ */ new Date()), l.innerHTML = this._renderMarkdown(h), b = m.status || "active", this.saveToHistory(h, "ai"), b && b !== "active" && (this._startPolling(), this._showHandoffBanner(!0), this.addMessage(this.i18n.t("humanAgentJoined"), "ai")), this.showQuickReplies && this.quickReplies.length > 0 && this._renderQuickReplies(), a(h));
|
|
540
577
|
} catch (m) {
|
|
541
578
|
}
|
|
542
579
|
}, p.onerror = () => {
|
|
@@ -549,14 +586,14 @@ class w {
|
|
|
549
586
|
// POST with retry (agent-api mode + legacy mode)
|
|
550
587
|
// ─────────────────────────────────────────────────────────────────────────
|
|
551
588
|
_sendMessageWithRetry(s) {
|
|
552
|
-
return d(this, arguments, function* (e,
|
|
589
|
+
return d(this, arguments, function* (e, i = [], t = 0) {
|
|
553
590
|
try {
|
|
554
591
|
const a = new AbortController(), n = setTimeout(() => a.abort(), this.requestTimeout), r = new FormData();
|
|
555
592
|
if (r.append("sessionId", this.sessionId), r.append("message", e), this._agentMode) {
|
|
556
593
|
const c = yield this._generateEmbedToken();
|
|
557
594
|
r.append("embedToken", c), this.visitorName && r.append("visitor_name", this.visitorName), this.visitorEmail && r.append("visitor_email", this.visitorEmail);
|
|
558
595
|
} else this.embedToken && r.append("embedToken", this.embedToken);
|
|
559
|
-
|
|
596
|
+
i.forEach((c, h) => {
|
|
560
597
|
r.append(`file${h}`, c);
|
|
561
598
|
});
|
|
562
599
|
const l = yield fetch(this.webhookUrl, {
|
|
@@ -565,39 +602,39 @@ class w {
|
|
|
565
602
|
signal: a.signal
|
|
566
603
|
});
|
|
567
604
|
if (clearTimeout(n), !l.ok) {
|
|
568
|
-
if (l.status >= 500 &&
|
|
569
|
-
return yield this._delay(this.retryDelay * (
|
|
605
|
+
if (l.status >= 500 && t < this.maxRetries)
|
|
606
|
+
return yield this._delay(this.retryDelay * (t + 1)), this._sendMessageWithRetry(e, i, t + 1);
|
|
570
607
|
const c = yield l.json().catch(() => ({}));
|
|
571
608
|
throw new Error(c.message || `Network error: ${l.statusText}`);
|
|
572
609
|
}
|
|
573
610
|
const o = yield l.json();
|
|
574
611
|
if (!o.response)
|
|
575
612
|
throw new Error(this.i18n.t("errorInvalidResponse"));
|
|
576
|
-
return o.status && o.status !== "active" && this.pollingUrl ? (this._startPolling(), this.addMessage(this.i18n.t("humanAgentJoined"), "ai")) : this._stopPolling(), o.response;
|
|
613
|
+
return o.status && o.status !== "active" && this.pollingUrl ? (this._startPolling(), this._showHandoffBanner(!0), this.addMessage(this.i18n.t("humanAgentJoined"), "ai")) : this._stopPolling(), o.response;
|
|
577
614
|
} catch (a) {
|
|
578
615
|
if (a.name === "AbortError")
|
|
579
616
|
throw new Error(this.i18n.t("errorTimeout"));
|
|
580
|
-
if ((a.message.includes("NetworkError") || a.message.includes("Failed to fetch")) &&
|
|
581
|
-
return yield this._delay(this.retryDelay * (
|
|
617
|
+
if ((a.message.includes("NetworkError") || a.message.includes("Failed to fetch")) && t < this.maxRetries)
|
|
618
|
+
return yield this._delay(this.retryDelay * (t + 1)), this._sendMessageWithRetry(e, i, t + 1);
|
|
582
619
|
throw a;
|
|
583
620
|
}
|
|
584
621
|
});
|
|
585
622
|
}
|
|
586
623
|
_delay(e) {
|
|
587
|
-
return new Promise((
|
|
624
|
+
return new Promise((i) => setTimeout(i, e));
|
|
588
625
|
}
|
|
589
626
|
_getErrorMessage(e) {
|
|
590
|
-
const
|
|
591
|
-
return
|
|
627
|
+
const i = e.message || "";
|
|
628
|
+
return i.includes("timeout") || i.includes("Timeout") ? this.i18n.t("errorTimeout") : i.includes("NetworkError") || i.includes("Failed to fetch") ? this.i18n.t("errorNetwork") : i.includes("500") || i.includes("503") ? this.i18n.t("errorServer") : i === this.i18n.t("streamingError") ? i : this.i18n.t("errorGeneric");
|
|
592
629
|
}
|
|
593
630
|
// ─────────────────────────────────────────────────────────────────────────
|
|
594
631
|
// Rate limiting
|
|
595
632
|
// ─────────────────────────────────────────────────────────────────────────
|
|
596
633
|
_checkRateLimit() {
|
|
597
634
|
const e = Date.now();
|
|
598
|
-
if (this.messageTimes = this.messageTimes.filter((
|
|
599
|
-
const
|
|
600
|
-
throw new Error(this.i18n.t("errorRateLimit", { seconds:
|
|
635
|
+
if (this.messageTimes = this.messageTimes.filter((i) => e - i < this.rateLimitWindow), this.messageTimes.length >= this.rateLimit) {
|
|
636
|
+
const i = Math.ceil((this.rateLimitWindow - (e - this.messageTimes[0])) / 1e3);
|
|
637
|
+
throw new Error(this.i18n.t("errorRateLimit", { seconds: i }));
|
|
601
638
|
}
|
|
602
639
|
this.messageTimes.push(e);
|
|
603
640
|
}
|
|
@@ -606,15 +643,15 @@ class w {
|
|
|
606
643
|
// ─────────────────────────────────────────────────────────────────────────
|
|
607
644
|
_renderQuickReplies(e = this.quickReplies) {
|
|
608
645
|
if (!this.showQuickReplies || e.length === 0) return;
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
const
|
|
612
|
-
|
|
646
|
+
const i = this.messageList.querySelector(".n8n-chat-quick-replies");
|
|
647
|
+
i && i.remove();
|
|
648
|
+
const t = document.createElement("div");
|
|
649
|
+
t.className = "n8n-chat-quick-replies", e.forEach((s) => {
|
|
613
650
|
const a = document.createElement("button");
|
|
614
651
|
a.className = "n8n-chat-quick-reply-btn", a.textContent = s, a.addEventListener("click", () => {
|
|
615
|
-
this.input.value = s, this.sendMessage(),
|
|
616
|
-
}),
|
|
617
|
-
}), this.messageList.appendChild(
|
|
652
|
+
this.input.value = s, this.sendMessage(), t.remove();
|
|
653
|
+
}), t.appendChild(a);
|
|
654
|
+
}), this.messageList.appendChild(t), this.messageList.scrollTop = this.messageList.scrollHeight;
|
|
618
655
|
}
|
|
619
656
|
// ─────────────────────────────────────────────────────────────────────────
|
|
620
657
|
// Notification badge
|
|
@@ -638,19 +675,19 @@ class w {
|
|
|
638
675
|
try {
|
|
639
676
|
const e = localStorage.getItem(this.historyKey);
|
|
640
677
|
if (!e) return;
|
|
641
|
-
JSON.parse(e).forEach((
|
|
642
|
-
this.addMessageWithoutSaving(
|
|
678
|
+
JSON.parse(e).forEach((i) => {
|
|
679
|
+
this.addMessageWithoutSaving(i.text, i.sender, new Date(i.timestamp));
|
|
643
680
|
});
|
|
644
681
|
} catch (e) {
|
|
645
682
|
console.warn("Failed to load chat history:", e);
|
|
646
683
|
}
|
|
647
684
|
}
|
|
648
|
-
saveToHistory(e,
|
|
685
|
+
saveToHistory(e, i, t = /* @__PURE__ */ new Date()) {
|
|
649
686
|
if (this.enableHistory)
|
|
650
687
|
try {
|
|
651
688
|
const s = localStorage.getItem(this.historyKey);
|
|
652
689
|
let a = s ? JSON.parse(s) : [];
|
|
653
|
-
a.push({ text: e, sender:
|
|
690
|
+
a.push({ text: e, sender: i, timestamp: t.toISOString() }), a.length > this.maxHistoryItems && (a = a.slice(-this.maxHistoryItems)), localStorage.setItem(this.historyKey, JSON.stringify(a));
|
|
654
691
|
} catch (s) {
|
|
655
692
|
console.warn("Failed to save chat history:", s);
|
|
656
693
|
}
|
|
@@ -662,8 +699,8 @@ class w {
|
|
|
662
699
|
try {
|
|
663
700
|
const e = localStorage.getItem(this.stateKey);
|
|
664
701
|
if (e) {
|
|
665
|
-
const
|
|
666
|
-
this._bubbleDismissed = this.showBubbleOnce &&
|
|
702
|
+
const i = JSON.parse(e);
|
|
703
|
+
this._bubbleDismissed = this.showBubbleOnce && i.bubbleDismissed || !1;
|
|
667
704
|
}
|
|
668
705
|
} catch (e) {
|
|
669
706
|
console.warn("Failed to load chat state:", e);
|
|
@@ -694,8 +731,8 @@ class w {
|
|
|
694
731
|
if (!this.container) return;
|
|
695
732
|
const e = this.container.querySelector(".n8n-chat-offline-indicator");
|
|
696
733
|
if (!this.isOnline && !e) {
|
|
697
|
-
const
|
|
698
|
-
|
|
734
|
+
const i = document.createElement("div");
|
|
735
|
+
i.className = "n8n-chat-offline-indicator", i.innerHTML = `
|
|
699
736
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
700
737
|
<line x1="1" y1="1" x2="23" y2="23"></line>
|
|
701
738
|
<path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"></path>
|
|
@@ -706,7 +743,7 @@ class w {
|
|
|
706
743
|
<line x1="12" y1="20" x2="12.01" y2="20"></line>
|
|
707
744
|
</svg>
|
|
708
745
|
<span>${this.i18n.t("offline")}</span>
|
|
709
|
-
`, this.container.insertBefore(
|
|
746
|
+
`, this.container.insertBefore(i, this.messageList);
|
|
710
747
|
} else this.isOnline && e && e.remove();
|
|
711
748
|
this.button && (this.button.disabled = !this.isOnline || this.isLoading);
|
|
712
749
|
}
|
|
@@ -718,10 +755,10 @@ class w {
|
|
|
718
755
|
e.key === "Escape" && this.isOpen && this.mode === "widget" && this.toggleChatWindow();
|
|
719
756
|
}), this.container.addEventListener("keydown", (e) => {
|
|
720
757
|
if (e.key !== "Tab") return;
|
|
721
|
-
const
|
|
758
|
+
const i = this.container.querySelectorAll(
|
|
722
759
|
'button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
723
|
-
),
|
|
724
|
-
e.shiftKey && document.activeElement ===
|
|
760
|
+
), t = i[0], s = i[i.length - 1];
|
|
761
|
+
e.shiftKey && document.activeElement === t ? (e.preventDefault(), s.focus()) : !e.shiftKey && document.activeElement === s && (e.preventDefault(), t.focus());
|
|
725
762
|
});
|
|
726
763
|
}
|
|
727
764
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -736,24 +773,42 @@ class w {
|
|
|
736
773
|
const s = yield this._generateEmbedToken();
|
|
737
774
|
e.searchParams.set("embedToken", s);
|
|
738
775
|
} else this.embedToken && e.searchParams.set("embedToken", this.embedToken);
|
|
739
|
-
const
|
|
740
|
-
if (!
|
|
741
|
-
const
|
|
742
|
-
if (
|
|
743
|
-
|
|
776
|
+
const i = yield fetch(e.toString());
|
|
777
|
+
if (!i.ok) return;
|
|
778
|
+
const t = yield i.json();
|
|
779
|
+
if (t.messages && t.messages.length > 0) {
|
|
780
|
+
t.messages.forEach((a) => {
|
|
744
781
|
a.role === "human_agent" && this.addMessage(a.content, "ai");
|
|
745
782
|
});
|
|
746
|
-
const s =
|
|
747
|
-
this._lastMessageTimestamp = s.created_at, this.isOpen || (this.unreadCount +=
|
|
783
|
+
const s = t.messages[t.messages.length - 1];
|
|
784
|
+
this._lastMessageTimestamp = s.created_at, this.isOpen || (this.unreadCount += t.messages.filter((a) => a.role === "human_agent").length, this._updateUnreadBadge());
|
|
748
785
|
}
|
|
749
|
-
(
|
|
786
|
+
(t.status === "active" || t.status === "resolved") && this._stopPolling();
|
|
750
787
|
} catch (e) {
|
|
751
788
|
}
|
|
752
789
|
}), this.pollingInterval));
|
|
753
790
|
});
|
|
754
791
|
}
|
|
755
792
|
_stopPolling() {
|
|
756
|
-
this._pollingTimer && (clearInterval(this._pollingTimer), this._pollingTimer = null);
|
|
793
|
+
this._pollingTimer && (clearInterval(this._pollingTimer), this._pollingTimer = null, this._showHandoffBanner(!1));
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Show or hide the "Waiting for a human agent" banner above the message list.
|
|
797
|
+
* @param {boolean} show - true to show, false to remove.
|
|
798
|
+
*/
|
|
799
|
+
_showHandoffBanner(e) {
|
|
800
|
+
if (!this.messageList) return;
|
|
801
|
+
const i = this.messageList.parentElement;
|
|
802
|
+
if (!i) return;
|
|
803
|
+
const t = i.querySelector(".pindai-chat-handoff-banner");
|
|
804
|
+
if (e && !t) {
|
|
805
|
+
const s = document.createElement("div");
|
|
806
|
+
s.className = "pindai-chat-handoff-banner";
|
|
807
|
+
const a = document.createElement("span");
|
|
808
|
+
a.className = "pindai-chat-handoff-pulse";
|
|
809
|
+
const n = document.createElement("span");
|
|
810
|
+
n.textContent = this.i18n.t("waitingForAgent"), s.appendChild(a), s.appendChild(n), i.insertBefore(s, this.messageList);
|
|
811
|
+
} else !e && t && t.remove();
|
|
757
812
|
}
|
|
758
813
|
// ─────────────────────────────────────────────────────────────────────────
|
|
759
814
|
// Legacy aliases (keep public API stable)
|
|
@@ -773,8 +828,8 @@ class w {
|
|
|
773
828
|
renderFilePreview(e) {
|
|
774
829
|
return this._renderFilePreview(e);
|
|
775
830
|
}
|
|
776
|
-
sendMessageWithRetry(e,
|
|
777
|
-
return this._sendMessageWithRetry(e,
|
|
831
|
+
sendMessageWithRetry(e, i, t) {
|
|
832
|
+
return this._sendMessageWithRetry(e, i, t);
|
|
778
833
|
}
|
|
779
834
|
checkRateLimit() {
|
|
780
835
|
return this._checkRateLimit();
|