@pindai-ai/chat-widget 3.0.1 → 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 +161 -139
- 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/main.js +84 -4
- package/src/style.css +61 -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: {
|
|
@@ -138,13 +138,13 @@ class y {
|
|
|
138
138
|
* @param {object} params - Parameters to substitute in the translation
|
|
139
139
|
* @returns {string} Translated string
|
|
140
140
|
*/
|
|
141
|
-
t(e,
|
|
141
|
+
t(e, i = {}) {
|
|
142
142
|
var s;
|
|
143
|
-
let
|
|
144
|
-
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) => {
|
|
145
145
|
const n = new RegExp(`\\{${a}\\}`, "g");
|
|
146
|
-
|
|
147
|
-
}),
|
|
146
|
+
t = t.replace(n, i[a]);
|
|
147
|
+
}), t;
|
|
148
148
|
}
|
|
149
149
|
/**
|
|
150
150
|
* Change the current locale
|
|
@@ -171,12 +171,12 @@ class y {
|
|
|
171
171
|
class w {
|
|
172
172
|
constructor(e) {
|
|
173
173
|
this.agentId = e.agentId || null, this.embedSecret = e.embedSecret || null, this.apiBaseUrl = e.apiBaseUrl ? e.apiBaseUrl.replace(/\/$/, "") : null;
|
|
174
|
-
const
|
|
175
|
-
if (!
|
|
174
|
+
const i = e.webhookUrl || e.n8nUrl, t = !!(this.agentId && this.embedSecret && this.apiBaseUrl);
|
|
175
|
+
if (!t && !!!i)
|
|
176
176
|
throw new Error(
|
|
177
177
|
'PindaiChatWidget: Provide either (agentId + embedSecret + apiBaseUrl) for Pindai Agent-API mode, or "webhookUrl" for generic webhook mode.'
|
|
178
178
|
);
|
|
179
|
-
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";
|
|
180
180
|
const a = e.bubbleMessages || (e.bubbleText ? [e.bubbleText] : null);
|
|
181
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 || [
|
|
182
182
|
"image/jpeg",
|
|
@@ -217,10 +217,10 @@ class w {
|
|
|
217
217
|
* Convert hex string to Uint8Array
|
|
218
218
|
*/
|
|
219
219
|
_hexToBytes(e) {
|
|
220
|
-
const
|
|
221
|
-
for (let
|
|
222
|
-
t
|
|
223
|
-
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;
|
|
224
224
|
}
|
|
225
225
|
/**
|
|
226
226
|
* Generate HMAC-SHA256 embed token using native crypto.subtle
|
|
@@ -235,8 +235,8 @@ class w {
|
|
|
235
235
|
{ name: "HMAC", hash: "SHA-256" },
|
|
236
236
|
!1,
|
|
237
237
|
["sign"]
|
|
238
|
-
),
|
|
239
|
-
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;
|
|
240
240
|
});
|
|
241
241
|
}
|
|
242
242
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -246,13 +246,13 @@ class w {
|
|
|
246
246
|
if (this.theme === "dark")
|
|
247
247
|
document.documentElement.setAttribute("data-pindai-theme", "dark");
|
|
248
248
|
else if (this.theme === "auto") {
|
|
249
|
-
const e = window.matchMedia("(prefers-color-scheme: dark)"),
|
|
249
|
+
const e = window.matchMedia("(prefers-color-scheme: dark)"), i = (t) => {
|
|
250
250
|
document.documentElement.setAttribute(
|
|
251
251
|
"data-pindai-theme",
|
|
252
|
-
|
|
252
|
+
t.matches ? "dark" : "light"
|
|
253
253
|
);
|
|
254
254
|
};
|
|
255
|
-
|
|
255
|
+
i(e), e.addEventListener("change", i);
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
258
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -274,12 +274,12 @@ class w {
|
|
|
274
274
|
}, this.bubbleDelay);
|
|
275
275
|
}
|
|
276
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");
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
277
|
+
const i = document.createElement("img");
|
|
278
|
+
i.src = this.launcherIconUrl, i.alt = "", i.onerror = () => {
|
|
279
|
+
i.onerror = null, i.src = this._getDefaultLauncherIcon();
|
|
280
280
|
};
|
|
281
|
-
const
|
|
282
|
-
|
|
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) => {
|
|
283
283
|
(s.key === "Enter" || s.key === " ") && (s.preventDefault(), this.toggleChatWindow());
|
|
284
284
|
});
|
|
285
285
|
}
|
|
@@ -306,7 +306,7 @@ class w {
|
|
|
306
306
|
// Chat Window
|
|
307
307
|
// ─────────────────────────────────────────────────────────────────────────
|
|
308
308
|
_initChatWindow() {
|
|
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);
|
|
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);
|
|
310
310
|
const e = this._buildFooterHtml();
|
|
311
311
|
this.container.innerHTML = `
|
|
312
312
|
<div class="n8n-chat-header">
|
|
@@ -335,11 +335,11 @@ class w {
|
|
|
335
335
|
</button>
|
|
336
336
|
</div>
|
|
337
337
|
${this.enableFileUpload ? '<div class="n8n-chat-file-preview" style="display: none;"></div>' : ""}
|
|
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", (
|
|
339
|
-
|
|
340
|
-
}), this.input.addEventListener("keypress", (
|
|
341
|
-
|
|
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", (
|
|
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");
|
|
343
343
|
}
|
|
344
344
|
/**
|
|
345
345
|
* Inject the header avatar/logo as a real DOM element so we can attach
|
|
@@ -350,17 +350,17 @@ class w {
|
|
|
350
350
|
_injectHeaderImage() {
|
|
351
351
|
const e = this.container.querySelector(".n8n-chat-header-content");
|
|
352
352
|
if (!e) return;
|
|
353
|
-
const
|
|
353
|
+
const i = e.querySelector(".n8n-chat-title");
|
|
354
354
|
if (this.avatarUrl) {
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}, 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);
|
|
359
359
|
} else if (this.showLogo) {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}, 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);
|
|
364
364
|
}
|
|
365
365
|
}
|
|
366
366
|
/**
|
|
@@ -388,6 +388,28 @@ class w {
|
|
|
388
388
|
_escapeHtml(e) {
|
|
389
389
|
return String(e).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
390
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
|
+
}
|
|
391
413
|
// ─────────────────────────────────────────────────────────────────────────
|
|
392
414
|
// Toggle open/close
|
|
393
415
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -406,8 +428,8 @@ class w {
|
|
|
406
428
|
// Timestamp formatting
|
|
407
429
|
// ─────────────────────────────────────────────────────────────────────────
|
|
408
430
|
_formatTimestamp(e) {
|
|
409
|
-
const
|
|
410
|
-
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", {
|
|
411
433
|
hour: "2-digit",
|
|
412
434
|
minute: "2-digit"
|
|
413
435
|
});
|
|
@@ -415,13 +437,13 @@ class w {
|
|
|
415
437
|
// ─────────────────────────────────────────────────────────────────────────
|
|
416
438
|
// Messages
|
|
417
439
|
// ─────────────────────────────────────────────────────────────────────────
|
|
418
|
-
addMessage(e,
|
|
440
|
+
addMessage(e, i, t = /* @__PURE__ */ new Date()) {
|
|
419
441
|
const s = document.createElement("div");
|
|
420
|
-
s.className = `n8n-chat-bubble n8n-chat-${
|
|
442
|
+
s.className = `n8n-chat-bubble n8n-chat-${i}-message`;
|
|
421
443
|
const a = document.createElement("div");
|
|
422
|
-
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;
|
|
423
445
|
const n = document.createElement("div");
|
|
424
|
-
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();
|
|
425
447
|
}
|
|
426
448
|
/**
|
|
427
449
|
* Create a streaming message bubble (text filled incrementally via SSE).
|
|
@@ -430,69 +452,69 @@ class w {
|
|
|
430
452
|
_createStreamingBubble() {
|
|
431
453
|
const e = document.createElement("div");
|
|
432
454
|
e.className = "n8n-chat-bubble n8n-chat-ai-message n8n-chat-bubble--streaming";
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
|
|
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";
|
|
437
459
|
const s = document.createElement("div");
|
|
438
|
-
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 };
|
|
439
461
|
}
|
|
440
|
-
addMessageWithoutSaving(e,
|
|
462
|
+
addMessageWithoutSaving(e, i, t) {
|
|
441
463
|
const s = document.createElement("div");
|
|
442
|
-
s.className = `n8n-chat-bubble n8n-chat-${
|
|
464
|
+
s.className = `n8n-chat-bubble n8n-chat-${i}-message`;
|
|
443
465
|
const a = document.createElement("div");
|
|
444
|
-
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;
|
|
445
467
|
const n = document.createElement("div");
|
|
446
|
-
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;
|
|
447
469
|
}
|
|
448
470
|
// ─────────────────────────────────────────────────────────────────────────
|
|
449
471
|
// Typing indicator
|
|
450
472
|
// ─────────────────────────────────────────────────────────────────────────
|
|
451
473
|
_showTypingIndicator(e) {
|
|
452
|
-
let
|
|
474
|
+
let i = this.messageList.querySelector(".n8n-chat-typing-indicator");
|
|
453
475
|
if (e) {
|
|
454
|
-
if (!
|
|
455
|
-
|
|
456
|
-
const
|
|
457
|
-
this.showActionIndicators ?
|
|
458
|
-
<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>
|
|
459
481
|
<span class="n8n-chat-typing-label">${this.i18n.t("thinkingIndicator")}</span>
|
|
460
|
-
` :
|
|
482
|
+
` : i.innerHTML = t, i.setAttribute("aria-label", this.i18n.t("typingIndicator")), this.messageList.appendChild(i), this.messageList.scrollTop = this.messageList.scrollHeight;
|
|
461
483
|
}
|
|
462
|
-
} else
|
|
484
|
+
} else i && i.remove();
|
|
463
485
|
}
|
|
464
486
|
// ─────────────────────────────────────────────────────────────────────────
|
|
465
487
|
// File upload
|
|
466
488
|
// ─────────────────────────────────────────────────────────────────────────
|
|
467
489
|
_handleFileSelect(e) {
|
|
468
|
-
Array.from(e.target.files).forEach((
|
|
469
|
-
if (!this.allowedFileTypes.includes(
|
|
470
|
-
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");
|
|
471
493
|
return;
|
|
472
494
|
}
|
|
473
|
-
if (
|
|
495
|
+
if (t.size > this.maxFileSize) {
|
|
474
496
|
const s = this.maxFileSize / 1024 / 1024;
|
|
475
|
-
this.addMessage(this.i18n.t("fileTooLarge", { filename:
|
|
497
|
+
this.addMessage(this.i18n.t("fileTooLarge", { filename: t.name, maxSize: s }), "ai");
|
|
476
498
|
return;
|
|
477
499
|
}
|
|
478
500
|
if (this.uploadedFiles.length >= this.maxFiles) {
|
|
479
501
|
this.addMessage(this.i18n.t("maxFilesExceeded", { maxFiles: this.maxFiles }), "ai");
|
|
480
502
|
return;
|
|
481
503
|
}
|
|
482
|
-
this.uploadedFiles.push(
|
|
504
|
+
this.uploadedFiles.push(t), this._renderFilePreview(t);
|
|
483
505
|
}), e.target.value = "";
|
|
484
506
|
}
|
|
485
507
|
_renderFilePreview(e) {
|
|
486
|
-
const
|
|
487
|
-
if (!
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
|
|
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 = `
|
|
491
513
|
<span class="n8n-chat-file-name">${this._escapeHtml(e.name)}</span>
|
|
492
514
|
<button class="n8n-chat-file-remove" aria-label="${this.i18n.t("ariaRemoveFile")}">×</button>
|
|
493
|
-
`,
|
|
494
|
-
this.uploadedFiles = this.uploadedFiles.filter((s) => s.name !== e.name),
|
|
495
|
-
}),
|
|
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);
|
|
496
518
|
}
|
|
497
519
|
// ─────────────────────────────────────────────────────────────────────────
|
|
498
520
|
// Send message — entry point
|
|
@@ -503,8 +525,8 @@ class w {
|
|
|
503
525
|
if (!(!e && this.uploadedFiles.length === 0 || this.isLoading)) {
|
|
504
526
|
try {
|
|
505
527
|
this._checkRateLimit();
|
|
506
|
-
} catch (
|
|
507
|
-
this.addMessage(
|
|
528
|
+
} catch (i) {
|
|
529
|
+
this.addMessage(i.message, "ai");
|
|
508
530
|
return;
|
|
509
531
|
}
|
|
510
532
|
if (!this.isOnline) {
|
|
@@ -516,16 +538,16 @@ class w {
|
|
|
516
538
|
if (this._agentMode && this.enableStreaming && this.uploadedFiles.length === 0)
|
|
517
539
|
yield this._sendWithStreaming(e);
|
|
518
540
|
else {
|
|
519
|
-
const
|
|
520
|
-
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();
|
|
521
543
|
}
|
|
522
|
-
} catch (
|
|
523
|
-
this._showTypingIndicator(!1), this.addMessage(this._getErrorMessage(
|
|
544
|
+
} catch (i) {
|
|
545
|
+
this._showTypingIndicator(!1), this.addMessage(this._getErrorMessage(i), "ai");
|
|
524
546
|
} finally {
|
|
525
547
|
if (this.isLoading = !1, this.button.disabled = !1, this.input.disabled = !1, this.input.focus(), this.uploadedFiles.length > 0) {
|
|
526
548
|
this.uploadedFiles = [];
|
|
527
|
-
const
|
|
528
|
-
|
|
549
|
+
const i = this.container.querySelector(".n8n-chat-file-preview");
|
|
550
|
+
i && (i.innerHTML = "", i.style.display = "none");
|
|
529
551
|
}
|
|
530
552
|
}
|
|
531
553
|
}
|
|
@@ -536,13 +558,13 @@ class w {
|
|
|
536
558
|
// ─────────────────────────────────────────────────────────────────────────
|
|
537
559
|
_sendWithStreaming(e) {
|
|
538
560
|
return d(this, null, function* () {
|
|
539
|
-
const
|
|
561
|
+
const i = yield this._generateEmbedToken(), t = new URLSearchParams({
|
|
540
562
|
message: e,
|
|
541
563
|
sessionId: this.sessionId,
|
|
542
|
-
embedToken:
|
|
564
|
+
embedToken: i
|
|
543
565
|
});
|
|
544
|
-
this.visitorName &&
|
|
545
|
-
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}`;
|
|
546
568
|
return new Promise((a, n) => {
|
|
547
569
|
this._showTypingIndicator(!1);
|
|
548
570
|
const { bubble: r, textNode: l, cursor: o, timeNode: c } = this._createStreamingBubble();
|
|
@@ -551,7 +573,7 @@ class w {
|
|
|
551
573
|
p.onmessage = (f) => {
|
|
552
574
|
try {
|
|
553
575
|
const m = JSON.parse(f.data);
|
|
554
|
-
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._showHandoffBanner(!0), 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));
|
|
555
577
|
} catch (m) {
|
|
556
578
|
}
|
|
557
579
|
}, p.onerror = () => {
|
|
@@ -564,14 +586,14 @@ class w {
|
|
|
564
586
|
// POST with retry (agent-api mode + legacy mode)
|
|
565
587
|
// ─────────────────────────────────────────────────────────────────────────
|
|
566
588
|
_sendMessageWithRetry(s) {
|
|
567
|
-
return d(this, arguments, function* (e,
|
|
589
|
+
return d(this, arguments, function* (e, i = [], t = 0) {
|
|
568
590
|
try {
|
|
569
591
|
const a = new AbortController(), n = setTimeout(() => a.abort(), this.requestTimeout), r = new FormData();
|
|
570
592
|
if (r.append("sessionId", this.sessionId), r.append("message", e), this._agentMode) {
|
|
571
593
|
const c = yield this._generateEmbedToken();
|
|
572
594
|
r.append("embedToken", c), this.visitorName && r.append("visitor_name", this.visitorName), this.visitorEmail && r.append("visitor_email", this.visitorEmail);
|
|
573
595
|
} else this.embedToken && r.append("embedToken", this.embedToken);
|
|
574
|
-
|
|
596
|
+
i.forEach((c, h) => {
|
|
575
597
|
r.append(`file${h}`, c);
|
|
576
598
|
});
|
|
577
599
|
const l = yield fetch(this.webhookUrl, {
|
|
@@ -580,8 +602,8 @@ class w {
|
|
|
580
602
|
signal: a.signal
|
|
581
603
|
});
|
|
582
604
|
if (clearTimeout(n), !l.ok) {
|
|
583
|
-
if (l.status >= 500 &&
|
|
584
|
-
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);
|
|
585
607
|
const c = yield l.json().catch(() => ({}));
|
|
586
608
|
throw new Error(c.message || `Network error: ${l.statusText}`);
|
|
587
609
|
}
|
|
@@ -592,27 +614,27 @@ class w {
|
|
|
592
614
|
} catch (a) {
|
|
593
615
|
if (a.name === "AbortError")
|
|
594
616
|
throw new Error(this.i18n.t("errorTimeout"));
|
|
595
|
-
if ((a.message.includes("NetworkError") || a.message.includes("Failed to fetch")) &&
|
|
596
|
-
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);
|
|
597
619
|
throw a;
|
|
598
620
|
}
|
|
599
621
|
});
|
|
600
622
|
}
|
|
601
623
|
_delay(e) {
|
|
602
|
-
return new Promise((
|
|
624
|
+
return new Promise((i) => setTimeout(i, e));
|
|
603
625
|
}
|
|
604
626
|
_getErrorMessage(e) {
|
|
605
|
-
const
|
|
606
|
-
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");
|
|
607
629
|
}
|
|
608
630
|
// ─────────────────────────────────────────────────────────────────────────
|
|
609
631
|
// Rate limiting
|
|
610
632
|
// ─────────────────────────────────────────────────────────────────────────
|
|
611
633
|
_checkRateLimit() {
|
|
612
634
|
const e = Date.now();
|
|
613
|
-
if (this.messageTimes = this.messageTimes.filter((
|
|
614
|
-
const
|
|
615
|
-
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 }));
|
|
616
638
|
}
|
|
617
639
|
this.messageTimes.push(e);
|
|
618
640
|
}
|
|
@@ -621,15 +643,15 @@ class w {
|
|
|
621
643
|
// ─────────────────────────────────────────────────────────────────────────
|
|
622
644
|
_renderQuickReplies(e = this.quickReplies) {
|
|
623
645
|
if (!this.showQuickReplies || e.length === 0) return;
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
const
|
|
627
|
-
|
|
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) => {
|
|
628
650
|
const a = document.createElement("button");
|
|
629
651
|
a.className = "n8n-chat-quick-reply-btn", a.textContent = s, a.addEventListener("click", () => {
|
|
630
|
-
this.input.value = s, this.sendMessage(),
|
|
631
|
-
}),
|
|
632
|
-
}), 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;
|
|
633
655
|
}
|
|
634
656
|
// ─────────────────────────────────────────────────────────────────────────
|
|
635
657
|
// Notification badge
|
|
@@ -653,19 +675,19 @@ class w {
|
|
|
653
675
|
try {
|
|
654
676
|
const e = localStorage.getItem(this.historyKey);
|
|
655
677
|
if (!e) return;
|
|
656
|
-
JSON.parse(e).forEach((
|
|
657
|
-
this.addMessageWithoutSaving(
|
|
678
|
+
JSON.parse(e).forEach((i) => {
|
|
679
|
+
this.addMessageWithoutSaving(i.text, i.sender, new Date(i.timestamp));
|
|
658
680
|
});
|
|
659
681
|
} catch (e) {
|
|
660
682
|
console.warn("Failed to load chat history:", e);
|
|
661
683
|
}
|
|
662
684
|
}
|
|
663
|
-
saveToHistory(e,
|
|
685
|
+
saveToHistory(e, i, t = /* @__PURE__ */ new Date()) {
|
|
664
686
|
if (this.enableHistory)
|
|
665
687
|
try {
|
|
666
688
|
const s = localStorage.getItem(this.historyKey);
|
|
667
689
|
let a = s ? JSON.parse(s) : [];
|
|
668
|
-
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));
|
|
669
691
|
} catch (s) {
|
|
670
692
|
console.warn("Failed to save chat history:", s);
|
|
671
693
|
}
|
|
@@ -677,8 +699,8 @@ class w {
|
|
|
677
699
|
try {
|
|
678
700
|
const e = localStorage.getItem(this.stateKey);
|
|
679
701
|
if (e) {
|
|
680
|
-
const
|
|
681
|
-
this._bubbleDismissed = this.showBubbleOnce &&
|
|
702
|
+
const i = JSON.parse(e);
|
|
703
|
+
this._bubbleDismissed = this.showBubbleOnce && i.bubbleDismissed || !1;
|
|
682
704
|
}
|
|
683
705
|
} catch (e) {
|
|
684
706
|
console.warn("Failed to load chat state:", e);
|
|
@@ -709,8 +731,8 @@ class w {
|
|
|
709
731
|
if (!this.container) return;
|
|
710
732
|
const e = this.container.querySelector(".n8n-chat-offline-indicator");
|
|
711
733
|
if (!this.isOnline && !e) {
|
|
712
|
-
const
|
|
713
|
-
|
|
734
|
+
const i = document.createElement("div");
|
|
735
|
+
i.className = "n8n-chat-offline-indicator", i.innerHTML = `
|
|
714
736
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
715
737
|
<line x1="1" y1="1" x2="23" y2="23"></line>
|
|
716
738
|
<path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"></path>
|
|
@@ -721,7 +743,7 @@ class w {
|
|
|
721
743
|
<line x1="12" y1="20" x2="12.01" y2="20"></line>
|
|
722
744
|
</svg>
|
|
723
745
|
<span>${this.i18n.t("offline")}</span>
|
|
724
|
-
`, this.container.insertBefore(
|
|
746
|
+
`, this.container.insertBefore(i, this.messageList);
|
|
725
747
|
} else this.isOnline && e && e.remove();
|
|
726
748
|
this.button && (this.button.disabled = !this.isOnline || this.isLoading);
|
|
727
749
|
}
|
|
@@ -733,10 +755,10 @@ class w {
|
|
|
733
755
|
e.key === "Escape" && this.isOpen && this.mode === "widget" && this.toggleChatWindow();
|
|
734
756
|
}), this.container.addEventListener("keydown", (e) => {
|
|
735
757
|
if (e.key !== "Tab") return;
|
|
736
|
-
const
|
|
758
|
+
const i = this.container.querySelectorAll(
|
|
737
759
|
'button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
738
|
-
),
|
|
739
|
-
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());
|
|
740
762
|
});
|
|
741
763
|
}
|
|
742
764
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -751,17 +773,17 @@ class w {
|
|
|
751
773
|
const s = yield this._generateEmbedToken();
|
|
752
774
|
e.searchParams.set("embedToken", s);
|
|
753
775
|
} else this.embedToken && e.searchParams.set("embedToken", this.embedToken);
|
|
754
|
-
const
|
|
755
|
-
if (!
|
|
756
|
-
const
|
|
757
|
-
if (
|
|
758
|
-
|
|
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) => {
|
|
759
781
|
a.role === "human_agent" && this.addMessage(a.content, "ai");
|
|
760
782
|
});
|
|
761
|
-
const s =
|
|
762
|
-
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());
|
|
763
785
|
}
|
|
764
|
-
(
|
|
786
|
+
(t.status === "active" || t.status === "resolved") && this._stopPolling();
|
|
765
787
|
} catch (e) {
|
|
766
788
|
}
|
|
767
789
|
}), this.pollingInterval));
|
|
@@ -776,17 +798,17 @@ class w {
|
|
|
776
798
|
*/
|
|
777
799
|
_showHandoffBanner(e) {
|
|
778
800
|
if (!this.messageList) return;
|
|
779
|
-
const
|
|
780
|
-
if (!
|
|
781
|
-
const
|
|
782
|
-
if (e && !
|
|
801
|
+
const i = this.messageList.parentElement;
|
|
802
|
+
if (!i) return;
|
|
803
|
+
const t = i.querySelector(".pindai-chat-handoff-banner");
|
|
804
|
+
if (e && !t) {
|
|
783
805
|
const s = document.createElement("div");
|
|
784
806
|
s.className = "pindai-chat-handoff-banner";
|
|
785
807
|
const a = document.createElement("span");
|
|
786
808
|
a.className = "pindai-chat-handoff-pulse";
|
|
787
809
|
const n = document.createElement("span");
|
|
788
|
-
n.textContent = this.i18n.t("waitingForAgent"), s.appendChild(a), s.appendChild(n),
|
|
789
|
-
} else !e &&
|
|
810
|
+
n.textContent = this.i18n.t("waitingForAgent"), s.appendChild(a), s.appendChild(n), i.insertBefore(s, this.messageList);
|
|
811
|
+
} else !e && t && t.remove();
|
|
790
812
|
}
|
|
791
813
|
// ─────────────────────────────────────────────────────────────────────────
|
|
792
814
|
// Legacy aliases (keep public API stable)
|
|
@@ -806,8 +828,8 @@ class w {
|
|
|
806
828
|
renderFilePreview(e) {
|
|
807
829
|
return this._renderFilePreview(e);
|
|
808
830
|
}
|
|
809
|
-
sendMessageWithRetry(e,
|
|
810
|
-
return this._sendMessageWithRetry(e,
|
|
831
|
+
sendMessageWithRetry(e, i, t) {
|
|
832
|
+
return this._sendMessageWithRetry(e, i, t);
|
|
811
833
|
}
|
|
812
834
|
checkRateLimit() {
|
|
813
835
|
return this._checkRateLimit();
|