@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.
@@ -1,18 +1,18 @@
1
- var d = (u, e, t) => new Promise((i, s) => {
1
+ var d = (u, e, i) => new Promise((t, s) => {
2
2
  var a = (l) => {
3
3
  try {
4
- r(t.next(l));
4
+ r(i.next(l));
5
5
  } catch (o) {
6
6
  s(o);
7
7
  }
8
8
  }, n = (l) => {
9
9
  try {
10
- r(t.throw(l));
10
+ r(i.throw(l));
11
11
  } catch (o) {
12
12
  s(o);
13
13
  }
14
- }, r = (l) => l.done ? i(l.value) : Promise.resolve(l.value).then(a, n);
15
- r((t = t.apply(u, e)).next());
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, t = {}) {
141
+ t(e, i = {}) {
142
142
  var s;
143
- let i = ((s = g[this.locale]) == null ? void 0 : s[e]) || g.en[e] || e;
144
- return Object.keys(t).forEach((a) => {
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
- i = i.replace(n, t[a]);
147
- }), i;
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 t = e.webhookUrl || e.n8nUrl, i = !!(this.agentId && this.embedSecret && this.apiBaseUrl);
175
- if (!i && !!!t)
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 = i ? `${this.apiBaseUrl}/v1/chat/${this.agentId}/message` : t, this._agentMode = i, 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.launcherColor = e.launcherColor || "#2563eb", this.sendButtonColor = e.sendButtonColor || "#2563eb", this.accentColor = e.accentColor || "#2563eb", this.theme = e.theme || "light", this.buttonAlignment = e.buttonAlignment || "bottom-right";
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 t = new Uint8Array(e.length / 2);
221
- for (let i = 0; i < e.length; i += 2)
222
- t[i / 2] = parseInt(e.slice(i, i + 2), 16);
223
- return t;
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
- ), t = new TextEncoder().encode(`${this.agentId}:${this.sessionId}`), i = yield crypto.subtle.sign("HMAC", e, t);
239
- return this._embedToken = Array.from(new Uint8Array(i)).map((s) => s.toString(16).padStart(2, "0")).join(""), this._embedToken;
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)"), t = (i) => {
249
+ const e = window.matchMedia("(prefers-color-scheme: dark)"), i = (t) => {
250
250
  document.documentElement.setAttribute(
251
251
  "data-pindai-theme",
252
- i.matches ? "dark" : "light"
252
+ t.matches ? "dark" : "light"
253
253
  );
254
254
  };
255
- t(e), e.addEventListener("change", t);
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 t = document.createElement("img");
278
- t.src = this.launcherIconUrl, t.alt = "", t.onerror = () => {
279
- t.onerror = null, t.src = this._getDefaultLauncherIcon();
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 i = document.createElement("span");
282
- i.className = "n8n-chat-unread-badge", i.style.display = "none", i.textContent = "0", this.launcher.appendChild(t), this.launcher.appendChild(i), e.appendChild(this.launcher), document.body.appendChild(e), this.launcher.addEventListener("click", () => this.toggleChatWindow()), this.launcher.addEventListener("keydown", (s) => {
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", (t) => {
339
- t.preventDefault(), this.sendMessage();
340
- }), this.input.addEventListener("keypress", (t) => {
341
- t.key === "Enter" && (t.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", (i) => this._handleFileSelect(i)), this._setupKeyboardNavigation(), this.loadHistory(), this.messageList.children.length === 0 && this.addMessage(this.initialMessage, "ai");
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 t = e.querySelector(".n8n-chat-title");
353
+ const i = e.querySelector(".n8n-chat-title");
354
354
  if (this.avatarUrl) {
355
- const i = document.createElement("img");
356
- i.className = "n8n-chat-header-avatar", i.alt = "", i.src = this.avatarUrl, i.onerror = () => {
357
- i.onerror = null, i.remove();
358
- }, e.insertBefore(i, t);
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 i = document.createElement("img");
361
- i.className = "n8n-chat-logo", i.alt = "", i.src = this.logoUrl, i.onerror = () => {
362
- i.onerror = null, i.remove();
363
- }, e.insertBefore(i, t);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
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 i = /* @__PURE__ */ new Date() - e;
410
- return i < 6e4 ? this.i18n.t("justNow") : i < 36e5 ? this.i18n.t("minutesAgo", { minutes: Math.floor(i / 6e4) }) : e.toLocaleTimeString(this.locale === "id" ? "id-ID" : "en-US", {
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, t, i = /* @__PURE__ */ new Date()) {
440
+ addMessage(e, i, t = /* @__PURE__ */ new Date()) {
419
441
  const s = document.createElement("div");
420
- s.className = `n8n-chat-bubble n8n-chat-${t}-message`;
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(i), n.setAttribute("data-timestamp", i.toISOString()), s.appendChild(a), s.appendChild(n), this.messageList.appendChild(s), this.messageList.scrollTop = this.messageList.scrollHeight, this.saveToHistory(e, t, i), !this.isOpen && t === "ai" && this._incrementUnread();
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 t = document.createElement("div");
434
- t.className = "n8n-chat-message-text", t.textContent = "";
435
- const i = document.createElement("span");
436
- i.className = "n8n-chat-stream-cursor";
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(t), e.appendChild(i), e.appendChild(s), this.messageList.appendChild(e), this.messageList.scrollTop = this.messageList.scrollHeight, { bubble: e, textNode: t, cursor: i, timeNode: s };
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, t, i) {
462
+ addMessageWithoutSaving(e, i, t) {
441
463
  const s = document.createElement("div");
442
- s.className = `n8n-chat-bubble n8n-chat-${t}-message`;
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(i), n.setAttribute("data-timestamp", i.toISOString()), s.appendChild(a), s.appendChild(n), this.messageList.appendChild(s), this.messageList.scrollTop = this.messageList.scrollHeight;
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 t = this.messageList.querySelector(".n8n-chat-typing-indicator");
474
+ let i = this.messageList.querySelector(".n8n-chat-typing-indicator");
453
475
  if (e) {
454
- if (!t) {
455
- t = document.createElement("div"), t.className = "n8n-chat-bubble n8n-chat-ai-message n8n-chat-typing-indicator";
456
- const i = "<span></span><span></span><span></span>";
457
- this.showActionIndicators ? t.innerHTML = `
458
- <div class="n8n-chat-typing-dots">${i}</div>
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
- ` : t.innerHTML = i, t.setAttribute("aria-label", this.i18n.t("typingIndicator")), this.messageList.appendChild(t), this.messageList.scrollTop = this.messageList.scrollHeight;
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 t && t.remove();
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((i) => {
469
- if (!this.allowedFileTypes.includes(i.type)) {
470
- this.addMessage(this.i18n.t("fileTypeNotSupported", { filename: i.name }), "ai");
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 (i.size > this.maxFileSize) {
495
+ if (t.size > this.maxFileSize) {
474
496
  const s = this.maxFileSize / 1024 / 1024;
475
- this.addMessage(this.i18n.t("fileTooLarge", { filename: i.name, maxSize: s }), "ai");
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(i), this._renderFilePreview(i);
504
+ this.uploadedFiles.push(t), this._renderFilePreview(t);
483
505
  }), e.target.value = "";
484
506
  }
485
507
  _renderFilePreview(e) {
486
- const t = this.container.querySelector(".n8n-chat-file-preview");
487
- if (!t) return;
488
- t.style.display = "flex";
489
- const i = document.createElement("div");
490
- i.className = "n8n-chat-file-item", i.innerHTML = `
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")}">&times;</button>
493
- `, i.querySelector(".n8n-chat-file-remove").addEventListener("click", () => {
494
- this.uploadedFiles = this.uploadedFiles.filter((s) => s.name !== e.name), i.remove(), this.uploadedFiles.length === 0 && (t.style.display = "none");
495
- }), t.appendChild(i);
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 (t) {
507
- this.addMessage(t.message, "ai");
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 t = yield this._sendMessageWithRetry(e, this.uploadedFiles);
520
- this._showTypingIndicator(!1), this.addMessage(t, "ai"), this.showQuickReplies && this.quickReplies.length > 0 && this._renderQuickReplies();
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 (t) {
523
- this._showTypingIndicator(!1), this.addMessage(this._getErrorMessage(t), "ai");
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 t = this.container.querySelector(".n8n-chat-file-preview");
528
- t && (t.innerHTML = "", t.style.display = "none");
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 t = yield this._generateEmbedToken(), i = new URLSearchParams({
561
+ const i = yield this._generateEmbedToken(), t = new URLSearchParams({
540
562
  message: e,
541
563
  sessionId: this.sessionId,
542
- embedToken: t
564
+ embedToken: i
543
565
  });
544
- this.visitorName && i.set("visitor_name", this.visitorName), this.visitorEmail && i.set("visitor_email", this.visitorEmail);
545
- const s = `${this.apiBaseUrl}/v1/chat/${this.agentId}/stream?${i}`;
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, t = [], i = 0) {
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
- t.forEach((c, h) => {
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 && i < this.maxRetries)
584
- return yield this._delay(this.retryDelay * (i + 1)), this._sendMessageWithRetry(e, t, i + 1);
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")) && i < this.maxRetries)
596
- return yield this._delay(this.retryDelay * (i + 1)), this._sendMessageWithRetry(e, t, i + 1);
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((t) => setTimeout(t, e));
624
+ return new Promise((i) => setTimeout(i, e));
603
625
  }
604
626
  _getErrorMessage(e) {
605
- const t = e.message || "";
606
- return t.includes("timeout") || t.includes("Timeout") ? this.i18n.t("errorTimeout") : t.includes("NetworkError") || t.includes("Failed to fetch") ? this.i18n.t("errorNetwork") : t.includes("500") || t.includes("503") ? this.i18n.t("errorServer") : t === this.i18n.t("streamingError") ? t : this.i18n.t("errorGeneric");
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((t) => e - t < this.rateLimitWindow), this.messageTimes.length >= this.rateLimit) {
614
- const t = Math.ceil((this.rateLimitWindow - (e - this.messageTimes[0])) / 1e3);
615
- throw new Error(this.i18n.t("errorRateLimit", { seconds: t }));
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 t = this.messageList.querySelector(".n8n-chat-quick-replies");
625
- t && t.remove();
626
- const i = document.createElement("div");
627
- i.className = "n8n-chat-quick-replies", e.forEach((s) => {
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(), i.remove();
631
- }), i.appendChild(a);
632
- }), this.messageList.appendChild(i), this.messageList.scrollTop = this.messageList.scrollHeight;
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((t) => {
657
- this.addMessageWithoutSaving(t.text, t.sender, new Date(t.timestamp));
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, t, i = /* @__PURE__ */ new Date()) {
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: t, timestamp: i.toISOString() }), a.length > this.maxHistoryItems && (a = a.slice(-this.maxHistoryItems)), localStorage.setItem(this.historyKey, JSON.stringify(a));
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 t = JSON.parse(e);
681
- this._bubbleDismissed = this.showBubbleOnce && t.bubbleDismissed || !1;
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 t = document.createElement("div");
713
- t.className = "n8n-chat-offline-indicator", t.innerHTML = `
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(t, this.messageList);
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 t = this.container.querySelectorAll(
758
+ const i = this.container.querySelectorAll(
737
759
  'button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'
738
- ), i = t[0], s = t[t.length - 1];
739
- e.shiftKey && document.activeElement === i ? (e.preventDefault(), s.focus()) : !e.shiftKey && document.activeElement === s && (e.preventDefault(), i.focus());
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 t = yield fetch(e.toString());
755
- if (!t.ok) return;
756
- const i = yield t.json();
757
- if (i.messages && i.messages.length > 0) {
758
- i.messages.forEach((a) => {
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 = i.messages[i.messages.length - 1];
762
- this._lastMessageTimestamp = s.created_at, this.isOpen || (this.unreadCount += i.messages.filter((a) => a.role === "human_agent").length, this._updateUnreadBadge());
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
- (i.status === "active" || i.status === "resolved") && this._stopPolling();
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 t = this.messageList.parentElement;
780
- if (!t) return;
781
- const i = t.querySelector(".pindai-chat-handoff-banner");
782
- if (e && !i) {
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), t.insertBefore(s, this.messageList);
789
- } else !e && i && i.remove();
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, t, i) {
810
- return this._sendMessageWithRetry(e, t, i);
831
+ sendMessageWithRetry(e, i, t) {
832
+ return this._sendMessageWithRetry(e, i, t);
811
833
  }
812
834
  checkRateLimit() {
813
835
  return this._checkRateLimit();