@pindai-ai/chat-widget 2.0.3 → 3.0.0

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,28 +1,316 @@
1
- (function(r){typeof define=="function"&&define.amd?define(r):r()})(function(){"use strict";var m=(r,d,o)=>new Promise((l,e)=>{var t=s=>{try{a(o.next(s))}catch(n){e(n)}},i=s=>{try{a(o.throw(s))}catch(n){e(n)}},a=s=>s.done?l(s.value):Promise.resolve(s.value).then(t,i);a((o=o.apply(r,d)).next())});const r={en:{title:"Pindai Agent",placeholder:"Write a message...",initialMessage:"Hello! How can I help you today?",send:"Send",close:"Close",upload:"Upload file",removeFile:"Remove file",typingIndicator:"AI is typing...",sending:"Sending...",justNow:"Just now",minutesAgo:"{minutes}m ago",offline:"Offline - messages will be sent when online",connectionRestored:"Connection restored",connectionLost:"No internet connection",errorGeneric:"An error occurred. Please try again.",errorTimeout:"Request timeout. Please try again.",errorNetwork:"No internet connection. Check your network.",errorServer:"Server is busy. Please try again later.",errorRateLimit:"Too many messages. Please wait {seconds} seconds.",errorInvalidResponse:"Invalid server response. Please contact support.",fileTypeNotSupported:"File type not supported: {filename}",fileTooLarge:"File too large: {filename} (max {maxSize}MB)",maxFilesExceeded:"Maximum {maxFiles} files allowed",quickReply1:"How can I extract data from documents?",quickReply2:"What file types are supported?",quickReply3:"Tell me about pricing",quickReply4:"Contact support",ariaOpenChat:"Open chat widget",ariaCloseChat:"Close chat window",ariaSendMessage:"Send message",ariaMessageInput:"Type your message",ariaUploadFile:"Upload file",ariaRemoveFile:"Remove file",ariaChatWindow:"Chat window",ariaMessageLog:"Chat messages"},id:{title:"Pindai Agent",placeholder:"Tulis pesan...",initialMessage:"Halo! Bagaimana saya bisa membantu Anda hari ini?",send:"Kirim",close:"Tutup",upload:"Unggah file",removeFile:"Hapus file",typingIndicator:"AI sedang mengetik...",sending:"Mengirim...",justNow:"Baru saja",minutesAgo:"{minutes}m yang lalu",offline:"Offline - pesan akan dikirim saat online",connectionRestored:"Koneksi kembali",connectionLost:"Tidak ada koneksi internet",errorGeneric:"Terjadi kesalahan. Silakan coba lagi.",errorTimeout:"Waktu permintaan habis. Silakan coba lagi.",errorNetwork:"Tidak ada koneksi internet. Periksa jaringan Anda.",errorServer:"Server sedang sibuk. Silakan coba lagi dalam beberapa saat.",errorRateLimit:"Terlalu banyak pesan. Silakan tunggu {seconds} detik.",errorInvalidResponse:"Respons server tidak valid. Silakan hubungi dukungan.",fileTypeNotSupported:"Jenis file tidak didukung: {filename}",fileTooLarge:"File terlalu besar: {filename} (maks {maxSize}MB)",maxFilesExceeded:"Maksimal {maxFiles} file diperbolehkan",quickReply1:"Bagaimana cara ekstraksi dokumen?",quickReply2:"Jenis file apa yang didukung?",quickReply3:"Tentang harga",quickReply4:"Hubungi dukungan",ariaOpenChat:"Buka widget chat",ariaCloseChat:"Tutup jendela chat",ariaSendMessage:"Kirim pesan",ariaMessageInput:"Ketik pesan Anda",ariaUploadFile:"Unggah file",ariaRemoveFile:"Hapus file",ariaChatWindow:"Jendela chat",ariaMessageLog:"Pesan chat"}};class d{constructor(e="id"){this.locale=this.isValidLocale(e)?e:"id"}isValidLocale(e){return Object.keys(r).includes(e)}t(e,t={}){var a;let i=((a=r[this.locale])==null?void 0:a[e])||r.en[e]||e;return Object.keys(t).forEach(s=>{const n=new RegExp(`\\{${s}\\}`,"g");i=i.replace(n,t[s])}),i}setLocale(e){return this.isValidLocale(e)?(this.locale=e,!0):(console.warn(`Invalid locale: ${e}. Keeping current locale: ${this.locale}`),!1)}getLocale(){return this.locale}getAvailableLocales(){return Object.keys(r)}}class o{constructor(e){const t=e.webhookUrl||e.n8nUrl;if(!t)throw new Error('PindaiChatWidget: "webhookUrl" option is required.');this.webhookUrl=t,this.mode=e.mode||"widget",this.locale=e.locale||"id",this.i18n=new d(this.locale),this.title=e.title||this.i18n.t("title"),this.initialMessage=e.initialMessage||this.i18n.t("initialMessage"),this.launcherIconUrl=e.launcherIconUrl||this.getDefaultIcon(),this.logoUrl=e.logoUrl||"https://pindai.ai/logo.png",this.showLogo=e.showLogo!==!1,this.launcherColor=e.launcherColor||"#0066FF",this.sendButtonColor=e.sendButtonColor||"#0066FF",this.accentColor=e.accentColor||"#00C896",this.enableFileUpload=e.enableFileUpload!==!1,this.allowedFileTypes=e.allowedFileTypes||["image/jpeg","image/png","image/gif","image/webp","application/pdf","application/msword","application/vnd.openxmlformats-officedocument.wordprocessingml.document","application/vnd.ms-excel","application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],this.maxFileSize=e.maxFileSize||10*1024*1024,this.maxFiles=e.maxFiles||5,this.uploadedFiles=[],this.enableNotifications=e.enableNotifications!==!1,this.enableSound=e.enableSound===!0,this.unreadCount=0,this.showQuickReplies=e.showQuickReplies!==!1,this.quickReplies=e.quickReplies||[this.i18n.t("quickReply1"),this.i18n.t("quickReply2"),this.i18n.t("quickReply3"),this.i18n.t("quickReply4")],this.enableHistory=e.enableHistory!==!1,this.maxHistoryItems=e.maxHistoryItems||50,this.historyKey=`pindai-chat-history-${this.webhookUrl}`,this.stateKey=`pindai-chat-state-${this.webhookUrl}`,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.chatWindow=null,this.messageList=null,this.input=null,this.button=null,this.closeButton=null,this.sessionId=`web-session-${Date.now()}-${Math.random()}`,this.isLoading=!1,this.isOpen=!1,this.isOnline=navigator.onLine,this.loadState(),this.setupOfflineDetection(),this.mode==="fullscreen"?this.initChatWindow():this.initLauncher()}initLauncher(){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"),this.launcher.innerHTML=`
2
- <img src="${this.launcherIconUrl}" alt="">
3
- <span class="n8n-chat-unread-badge" style="display: none;">0</span>
4
- `,document.body.appendChild(this.launcher),this.launcher.addEventListener("click",()=>this.toggleChatWindow()),this.launcher.addEventListener("keydown",e=>{(e.key==="Enter"||e.key===" ")&&(e.preventDefault(),this.toggleChatWindow())})}initChatWindow(){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.innerHTML=`
1
+ var d = (u, e, t) => new Promise((i, s) => {
2
+ var a = (l) => {
3
+ try {
4
+ r(t.next(l));
5
+ } catch (o) {
6
+ s(o);
7
+ }
8
+ }, n = (l) => {
9
+ try {
10
+ r(t.throw(l));
11
+ } catch (o) {
12
+ s(o);
13
+ }
14
+ }, r = (l) => l.done ? i(l.value) : Promise.resolve(l.value).then(a, n);
15
+ r((t = t.apply(u, e)).next());
16
+ });
17
+ const g = {
18
+ en: {
19
+ // Widget UI
20
+ title: "Pindai Agent",
21
+ placeholder: "Write a message...",
22
+ initialMessage: "Hello! How can I help you today?",
23
+ send: "Send",
24
+ close: "Close",
25
+ upload: "Upload file",
26
+ removeFile: "Remove file",
27
+ // Loading and status
28
+ typingIndicator: "AI is typing...",
29
+ sending: "Sending...",
30
+ justNow: "Just now",
31
+ minutesAgo: "{minutes}m ago",
32
+ // Offline/Online status
33
+ offline: "Offline - messages will be sent when online",
34
+ connectionRestored: "Connection restored",
35
+ connectionLost: "No internet connection",
36
+ // Error messages
37
+ errorGeneric: "An error occurred. Please try again.",
38
+ errorTimeout: "Request timeout. Please try again.",
39
+ errorNetwork: "No internet connection. Check your network.",
40
+ errorServer: "Server is busy. Please try again later.",
41
+ errorRateLimit: "Too many messages. Please wait {seconds} seconds.",
42
+ errorInvalidResponse: "Invalid server response. Please contact support.",
43
+ // File upload errors
44
+ fileTypeNotSupported: "File type not supported: {filename}",
45
+ fileTooLarge: "File too large: {filename} (max {maxSize}MB)",
46
+ maxFilesExceeded: "Maximum {maxFiles} files allowed",
47
+ // Quick replies (default suggestions)
48
+ quickReply1: "How can I extract data from documents?",
49
+ quickReply2: "What file types are supported?",
50
+ quickReply3: "Tell me about pricing",
51
+ quickReply4: "Contact support",
52
+ // Accessibility labels
53
+ ariaOpenChat: "Open chat widget",
54
+ ariaCloseChat: "Close chat window",
55
+ ariaSendMessage: "Send message",
56
+ ariaMessageInput: "Type your message",
57
+ ariaUploadFile: "Upload file",
58
+ ariaRemoveFile: "Remove file",
59
+ ariaChatWindow: "Chat window",
60
+ ariaMessageLog: "Chat messages",
61
+ // New v3.0 keys
62
+ bubbleDismiss: "Dismiss",
63
+ streamingError: "Connection interrupted. Please try again.",
64
+ humanAgentJoined: "A team member has joined the conversation.",
65
+ humanAgentLabel: "Support Agent",
66
+ actionIndicator: "Performing action...",
67
+ thinkingIndicator: "Thinking...",
68
+ poweredBy: "Powered by"
69
+ },
70
+ id: {
71
+ // Widget UI
72
+ title: "Pindai Agent",
73
+ placeholder: "Tulis pesan...",
74
+ initialMessage: "Halo! Bagaimana saya bisa membantu Anda hari ini?",
75
+ send: "Kirim",
76
+ close: "Tutup",
77
+ upload: "Unggah file",
78
+ removeFile: "Hapus file",
79
+ // Loading and status
80
+ typingIndicator: "AI sedang mengetik...",
81
+ sending: "Mengirim...",
82
+ justNow: "Baru saja",
83
+ minutesAgo: "{minutes}m yang lalu",
84
+ // Offline/Online status
85
+ offline: "Offline - pesan akan dikirim saat online",
86
+ connectionRestored: "Koneksi kembali",
87
+ connectionLost: "Tidak ada koneksi internet",
88
+ // Error messages
89
+ errorGeneric: "Terjadi kesalahan. Silakan coba lagi.",
90
+ errorTimeout: "Waktu permintaan habis. Silakan coba lagi.",
91
+ errorNetwork: "Tidak ada koneksi internet. Periksa jaringan Anda.",
92
+ errorServer: "Server sedang sibuk. Silakan coba lagi dalam beberapa saat.",
93
+ errorRateLimit: "Terlalu banyak pesan. Silakan tunggu {seconds} detik.",
94
+ errorInvalidResponse: "Respons server tidak valid. Silakan hubungi dukungan.",
95
+ // File upload errors
96
+ fileTypeNotSupported: "Jenis file tidak didukung: {filename}",
97
+ fileTooLarge: "File terlalu besar: {filename} (maks {maxSize}MB)",
98
+ maxFilesExceeded: "Maksimal {maxFiles} file diperbolehkan",
99
+ // Quick replies (default suggestions)
100
+ quickReply1: "Bagaimana cara ekstraksi dokumen?",
101
+ quickReply2: "Jenis file apa yang didukung?",
102
+ quickReply3: "Tentang harga",
103
+ quickReply4: "Hubungi dukungan",
104
+ // Accessibility labels
105
+ ariaOpenChat: "Buka widget chat",
106
+ ariaCloseChat: "Tutup jendela chat",
107
+ ariaSendMessage: "Kirim pesan",
108
+ ariaMessageInput: "Ketik pesan Anda",
109
+ ariaUploadFile: "Unggah file",
110
+ ariaRemoveFile: "Hapus file",
111
+ ariaChatWindow: "Jendela chat",
112
+ ariaMessageLog: "Pesan chat",
113
+ // New v3.0 keys
114
+ bubbleDismiss: "Tutup",
115
+ streamingError: "Koneksi terputus. Silakan coba lagi.",
116
+ humanAgentJoined: "Anggota tim telah bergabung dalam percakapan.",
117
+ humanAgentLabel: "Agen Dukungan",
118
+ actionIndicator: "Sedang memproses...",
119
+ thinkingIndicator: "Sedang berpikir...",
120
+ poweredBy: "Dibuat dengan"
121
+ }
122
+ };
123
+ class y {
124
+ constructor(e = "id") {
125
+ this.locale = this.isValidLocale(e) ? e : "id";
126
+ }
127
+ /**
128
+ * Check if locale is valid
129
+ */
130
+ isValidLocale(e) {
131
+ return Object.keys(g).includes(e);
132
+ }
133
+ /**
134
+ * Translate a key with optional parameter substitution
135
+ * @param {string} key - Translation key
136
+ * @param {object} params - Parameters to substitute in the translation
137
+ * @returns {string} Translated string
138
+ */
139
+ t(e, t = {}) {
140
+ var s;
141
+ let i = ((s = g[this.locale]) == null ? void 0 : s[e]) || g.en[e] || e;
142
+ return Object.keys(t).forEach((a) => {
143
+ const n = new RegExp(`\\{${a}\\}`, "g");
144
+ i = i.replace(n, t[a]);
145
+ }), i;
146
+ }
147
+ /**
148
+ * Change the current locale
149
+ * @param {string} locale - New locale (id or en)
150
+ */
151
+ setLocale(e) {
152
+ return this.isValidLocale(e) ? (this.locale = e, !0) : (console.warn(`Invalid locale: ${e}. Keeping current locale: ${this.locale}`), !1);
153
+ }
154
+ /**
155
+ * Get current locale
156
+ * @returns {string} Current locale
157
+ */
158
+ getLocale() {
159
+ return this.locale;
160
+ }
161
+ /**
162
+ * Get all available locales
163
+ * @returns {string[]} Array of available locale codes
164
+ */
165
+ getAvailableLocales() {
166
+ return Object.keys(g);
167
+ }
168
+ }
169
+ class w {
170
+ constructor(e) {
171
+ this.agentId = e.agentId || null, this.embedSecret = e.embedSecret || null, this.apiBaseUrl = e.apiBaseUrl ? e.apiBaseUrl.replace(/\/$/, "") : null;
172
+ const t = e.webhookUrl || e.n8nUrl, i = !!(this.agentId && this.embedSecret && this.apiBaseUrl);
173
+ if (!i && !!!t)
174
+ throw new Error(
175
+ 'PindaiChatWidget: Provide either (agentId + embedSecret + apiBaseUrl) for Pindai Agent-API mode, or "webhookUrl" for generic webhook mode.'
176
+ );
177
+ 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";
178
+ const a = e.bubbleMessages || (e.bubbleText ? [e.bubbleText] : null);
179
+ 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
+ "image/jpeg",
181
+ "image/png",
182
+ "image/gif",
183
+ "image/webp",
184
+ "application/pdf",
185
+ "application/msword",
186
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
187
+ "application/vnd.ms-excel",
188
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
189
+ ], this.maxFileSize = e.maxFileSize || 10 * 1024 * 1024, this.maxFiles = e.maxFiles || 5, this.uploadedFiles = [], this.enableNotifications = e.enableNotifications !== !1, this.enableSound = e.enableSound === !0, this.unreadCount = 0, this.showQuickReplies = e.showQuickReplies !== !1, this.quickReplies = e.quickReplies || [
190
+ this.i18n.t("quickReply1"),
191
+ this.i18n.t("quickReply2"),
192
+ this.i18n.t("quickReply3"),
193
+ this.i18n.t("quickReply4")
194
+ ], this.enableHistory = e.enableHistory !== !1, this.maxHistoryItems = e.maxHistoryItems || 50;
195
+ 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, this.sessionId = `web-session-${Date.now()}-${Math.random()}`, this.isLoading = !1, this.isOpen = !1, this.isOnline = navigator.onLine, this.loadState(), this._setupOfflineDetection(), this._applyTheme(), this.mode === "fullscreen" ? this._initChatWindow() : this._initLauncher();
197
+ }
198
+ // ─────────────────────────────────────────────────────────────────────────
199
+ // Auth helpers (Agent-API mode)
200
+ // ─────────────────────────────────────────────────────────────────────────
201
+ /**
202
+ * Convert hex string to Uint8Array
203
+ */
204
+ _hexToBytes(e) {
205
+ const t = new Uint8Array(e.length / 2);
206
+ for (let i = 0; i < e.length; i += 2)
207
+ t[i / 2] = parseInt(e.slice(i, i + 2), 16);
208
+ return t;
209
+ }
210
+ /**
211
+ * Generate HMAC-SHA256 embed token using native crypto.subtle
212
+ * token = HMAC-SHA256(embedSecret, "{agentId}:{sessionId}")
213
+ */
214
+ _generateEmbedToken() {
215
+ return d(this, null, function* () {
216
+ if (this._embedToken) return this._embedToken;
217
+ const e = yield crypto.subtle.importKey(
218
+ "raw",
219
+ this._hexToBytes(this.embedSecret),
220
+ { name: "HMAC", hash: "SHA-256" },
221
+ !1,
222
+ ["sign"]
223
+ ), t = new TextEncoder().encode(`${this.agentId}:${this.sessionId}`), i = yield crypto.subtle.sign("HMAC", e, t);
224
+ return this._embedToken = Array.from(new Uint8Array(i)).map((s) => s.toString(16).padStart(2, "0")).join(""), this._embedToken;
225
+ });
226
+ }
227
+ // ─────────────────────────────────────────────────────────────────────────
228
+ // Theme
229
+ // ─────────────────────────────────────────────────────────────────────────
230
+ _applyTheme() {
231
+ if (this.theme === "dark")
232
+ document.documentElement.setAttribute("data-pindai-theme", "dark");
233
+ else if (this.theme === "auto") {
234
+ const e = window.matchMedia("(prefers-color-scheme: dark)"), t = (i) => {
235
+ document.documentElement.setAttribute(
236
+ "data-pindai-theme",
237
+ i.matches ? "dark" : "light"
238
+ );
239
+ };
240
+ t(e), e.addEventListener("change", t);
241
+ }
242
+ }
243
+ // ─────────────────────────────────────────────────────────────────────────
244
+ // Launcher
245
+ // ─────────────────────────────────────────────────────────────────────────
246
+ _initLauncher() {
247
+ const e = document.createElement("div");
248
+ if (e.className = "n8n-chat-launcher-wrapper", this.buttonAlignment === "bottom-left" && e.classList.add("n8n-chat-launcher-wrapper--left"), this.bubbleMessages && !(this.showBubbleOnce && this._bubbleDismissed)) {
249
+ this.bubblePopup = document.createElement("div"), this.bubblePopup.className = "n8n-chat-bubble-popup", this.bubblePopup.style.display = "none";
250
+ const s = document.createElement("span");
251
+ s.className = "n8n-chat-bubble-popup-text", s.textContent = this.bubbleMessages[0];
252
+ const a = document.createElement("button");
253
+ a.className = "n8n-chat-bubble-popup-dismiss", a.setAttribute("aria-label", this.i18n.t("bubbleDismiss")), a.textContent = "×", a.addEventListener("click", (r) => {
254
+ r.stopPropagation(), this._dismissBubble();
255
+ });
256
+ const n = document.createElement("div");
257
+ n.className = "n8n-chat-bubble-popup-arrow", this.bubblePopup.appendChild(s), this.bubblePopup.appendChild(a), this.bubblePopup.appendChild(n), e.appendChild(this.bubblePopup), this._bubbleTextSpan = s, this._bubbleDelayTimer = setTimeout(() => {
258
+ !this._bubbleDismissed && this.bubblePopup && (this.bubblePopup.style.display = "flex", this.bubbleMessages.length > 1 && this._startBubbleRotation());
259
+ }, this.bubbleDelay);
260
+ }
261
+ 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 t = document.createElement("img");
263
+ t.src = this.launcherIconUrl, t.alt = "", t.onerror = () => {
264
+ t.onerror = null, t.src = this._getDefaultLauncherIcon();
265
+ };
266
+ const i = document.createElement("span");
267
+ 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) => {
268
+ (s.key === "Enter" || s.key === " ") && (s.preventDefault(), this.toggleChatWindow());
269
+ });
270
+ }
271
+ _dismissBubble() {
272
+ this._bubbleDelayTimer && (clearTimeout(this._bubbleDelayTimer), this._bubbleDelayTimer = null), this._stopBubbleRotation(), this.bubblePopup && (this.bubblePopup.style.display = "none"), this._bubbleDismissed = !0, this.saveState();
273
+ }
274
+ _startBubbleRotation() {
275
+ this._stopBubbleRotation(), this._bubbleRotateTimer = setInterval(() => {
276
+ if (!this.bubblePopup || this._bubbleDismissed) {
277
+ this._stopBubbleRotation();
278
+ return;
279
+ }
280
+ this._bubbleMsgIndex = (this._bubbleMsgIndex + 1) % this.bubbleMessages.length, this._bubbleTextSpan && (this._bubbleTextSpan.classList.add("n8n-chat-bubble-popup-text--exit"), setTimeout(() => {
281
+ this._bubbleTextSpan && (this._bubbleTextSpan.textContent = this.bubbleMessages[this._bubbleMsgIndex], this._bubbleTextSpan.classList.remove("n8n-chat-bubble-popup-text--exit"), this._bubbleTextSpan.classList.add("n8n-chat-bubble-popup-text--enter"), setTimeout(() => {
282
+ this._bubbleTextSpan && this._bubbleTextSpan.classList.remove("n8n-chat-bubble-popup-text--enter");
283
+ }, 300));
284
+ }, 250));
285
+ }, this.bubbleInterval);
286
+ }
287
+ _stopBubbleRotation() {
288
+ this._bubbleRotateTimer && (clearInterval(this._bubbleRotateTimer), this._bubbleRotateTimer = null);
289
+ }
290
+ // ─────────────────────────────────────────────────────────────────────────
291
+ // Chat Window
292
+ // ─────────────────────────────────────────────────────────────────────────
293
+ _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);
295
+ const e = this._buildFooterHtml();
296
+ this.container.innerHTML = `
5
297
  <div class="n8n-chat-header">
6
298
  <div class="n8n-chat-header-content">
7
- ${this.showLogo?`<img src="${this.logoUrl}" alt="Pindai Logo" class="n8n-chat-logo">`:""}
8
- <span class="n8n-chat-title">${this.title}</span>
299
+ <span class="n8n-chat-title">${this._escapeHtml(this.title)}</span>
9
300
  </div>
10
301
  <button class="n8n-chat-close-btn" aria-label="${this.i18n.t("ariaCloseChat")}">&times;</button>
11
302
  </div>
12
303
  <div class="n8n-chat-messages" role="log" aria-live="polite" aria-atomic="false"></div>
13
- <div class="n8n-chat-watermark">
14
- <span>Powered by</span>
15
- <a href="https://pindai.ai" target="_blank" rel="noopener noreferrer">Pindai.ai</a>
16
- </div>
304
+ ${e}
17
305
  <div class="n8n-chat-input-area">
18
- ${this.enableFileUpload?`
306
+ ${this.enableFileUpload ? `
19
307
  <label class="n8n-chat-file-upload-btn" aria-label="${this.i18n.t("ariaUploadFile")}">
20
308
  <input type="file" multiple accept="${this.allowedFileTypes.join(",")}" hidden>
21
309
  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
22
310
  <path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/>
23
311
  </svg>
24
312
  </label>
25
- `:""}
313
+ ` : ""}
26
314
  <input type="text" placeholder="${this.i18n.t("placeholder")}" aria-label="${this.i18n.t("ariaMessageInput")}" />
27
315
  <button class="n8n-chat-send-btn" style="background-color: ${this.sendButtonColor}" aria-label="${this.i18n.t("ariaSendMessage")}">
28
316
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -31,15 +319,383 @@
31
319
  </svg>
32
320
  </button>
33
321
  </div>
34
- ${this.enableFileUpload?'<div class="n8n-chat-file-preview" style="display: none;"></div>':""}
35
- `,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",e=>{e.preventDefault(),this.sendMessage()}),this.input.addEventListener("keypress",e=>{e.key==="Enter"&&(e.preventDefault(),this.sendMessage())}),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")}toggleChatWindow(){this.isOpen?(this.container.classList.remove("n8n-chat-widget--open"),this.launcher&&this.launcher.classList.remove("n8n-chat-launcher--hidden")):(this.container||this.initChatWindow(),setTimeout(()=>{this.container.classList.add("n8n-chat-widget--open"),this.launcher&&this.launcher.classList.add("n8n-chat-launcher--hidden"),this.input.focus(),this.clearUnreadCount()},10)),this.isOpen=!this.isOpen,this.saveState()}getDefaultIcon(){return`data:image/svg+xml;charset=UTF-8,${encodeURIComponent(`
36
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
37
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
38
- </svg>
39
- `)}`}formatTimestamp(e){const i=new Date-e;if(i<6e4)return this.i18n.t("justNow");if(i<36e5){const a=Math.floor(i/6e4);return this.i18n.t("minutesAgo",{minutes:a})}return e.toLocaleTimeString(this.locale==="id"?"id-ID":"en-US",{hour:"2-digit",minute:"2-digit"})}addMessage(e,t,i=new Date){const a=document.createElement("div");a.className=`n8n-chat-bubble n8n-chat-${t}-message`;const s=document.createElement("div");s.className="n8n-chat-message-text",s.textContent=e;const n=document.createElement("div");n.className="n8n-chat-message-timestamp",n.textContent=this.formatTimestamp(i),n.setAttribute("data-timestamp",i.toISOString()),a.appendChild(s),a.appendChild(n),this.messageList.appendChild(a),this.messageList.scrollTop=this.messageList.scrollHeight,this.saveToHistory(e,t,i),!this.isOpen&&t==="ai"&&this.incrementUnread()}showTypingIndicator(e){let t=this.messageList.querySelector(".n8n-chat-typing-indicator");e?t||(t=document.createElement("div"),t.className="n8n-chat-bubble n8n-chat-ai-message n8n-chat-typing-indicator",t.innerHTML="<span></span><span></span><span></span>",t.setAttribute("aria-label",this.i18n.t("typingIndicator")),this.messageList.appendChild(t),this.messageList.scrollTop=this.messageList.scrollHeight):t&&t.remove()}handleFileSelect(e){Array.from(e.target.files).forEach(i=>{if(!this.allowedFileTypes.includes(i.type)){this.addMessage(this.i18n.t("fileTypeNotSupported",{filename:i.name}),"ai");return}if(i.size>this.maxFileSize){const a=this.maxFileSize/1024/1024;this.addMessage(this.i18n.t("fileTooLarge",{filename:i.name,maxSize:a}),"ai");return}if(this.uploadedFiles.length>=this.maxFiles){this.addMessage(this.i18n.t("maxFilesExceeded",{maxFiles:this.maxFiles}),"ai");return}this.uploadedFiles.push(i),this.renderFilePreview(i)}),e.target.value=""}renderFilePreview(e){const t=this.container.querySelector(".n8n-chat-file-preview");if(!t)return;t.style.display="flex";const i=document.createElement("div");i.className="n8n-chat-file-item",i.innerHTML=`
40
- <span class="n8n-chat-file-name">${e.name}</span>
41
- <button class="n8n-chat-file-remove" data-file="${e.name}" aria-label="${this.i18n.t("ariaRemoveFile")}">&times;</button>
42
- `,i.querySelector(".n8n-chat-file-remove").addEventListener("click",()=>{this.uploadedFiles=this.uploadedFiles.filter(s=>s.name!==e.name),i.remove(),this.uploadedFiles.length===0&&(t.style.display="none")}),t.appendChild(i)}sendMessage(){return m(this,null,function*(){const e=this.input.value.trim();if(!(!e&&this.uploadedFiles.length===0||this.isLoading)){try{this.checkRateLimit()}catch(t){this.addMessage(t.message,"ai");return}if(!this.isOnline){this.addMessage(this.i18n.t("connectionLost"),"ai");return}this.isLoading=!0,this.button.disabled=!0,this.input.disabled=!0,e&&this.addMessage(e,"user"),this.input.value="",this.showTypingIndicator(!0);try{const t=yield this.sendMessageWithRetry(e,this.uploadedFiles);this.addMessage(t,"ai"),this.showQuickReplies&&this.quickReplies.length>0&&this.renderQuickReplies()}catch(t){const i=this.getErrorMessage(t);this.addMessage(i,"ai")}finally{if(this.isLoading=!1,this.button.disabled=!1,this.input.disabled=!1,this.showTypingIndicator(!1),this.input.focus(),this.uploadedFiles.length>0){this.uploadedFiles=[];const t=this.container.querySelector(".n8n-chat-file-preview");t&&(t.innerHTML="",t.style.display="none")}}}})}sendMessageWithRetry(a){return m(this,arguments,function*(e,t=[],i=0){try{const s=new AbortController,n=setTimeout(()=>s.abort(),this.requestTimeout),c=new FormData;c.append("sessionId",this.sessionId),c.append("message",e),t.forEach((u,p)=>{c.append(`file${p}`,u)});const h=yield fetch(this.webhookUrl,{method:"POST",body:c,signal:s.signal});if(clearTimeout(n),!h.ok){if(h.status>=500&&i<this.maxRetries)return yield this.delay(this.retryDelay*(i+1)),this.sendMessageWithRetry(e,t,i+1);const u=yield h.json().catch(()=>({}));throw new Error(u.message||`Network error: ${h.statusText}`)}const g=yield h.json();if(!g.response)throw new Error(this.i18n.t("errorInvalidResponse"));return g.response}catch(s){if(s.name==="AbortError")throw new Error(this.i18n.t("errorTimeout"));if(s.message.includes("NetworkError")&&i<this.maxRetries)return yield this.delay(this.retryDelay*(i+1)),this.sendMessageWithRetry(e,t,i+1);throw s}})}delay(e){return new Promise(t=>setTimeout(t,e))}getErrorMessage(e){return e.message.includes("timeout")||e.message.includes("Timeout")?this.i18n.t("errorTimeout"):e.message.includes("NetworkError")||e.message.includes("Failed to fetch")?this.i18n.t("errorNetwork"):e.message.includes("500")||e.message.includes("503")?this.i18n.t("errorServer"):this.i18n.t("errorGeneric")}checkRateLimit(){const e=Date.now();if(this.messageTimes=this.messageTimes.filter(t=>e-t<this.rateLimitWindow),this.messageTimes.length>=this.rateLimit){const t=this.messageTimes[0],i=Math.ceil((this.rateLimitWindow-(e-t))/1e3);throw new Error(this.i18n.t("errorRateLimit",{seconds:i}))}this.messageTimes.push(e)}renderQuickReplies(e=this.quickReplies){if(!this.showQuickReplies||e.length===0)return;const t=this.messageList.querySelector(".n8n-chat-quick-replies");t&&t.remove();const i=document.createElement("div");i.className="n8n-chat-quick-replies",e.forEach(a=>{const s=document.createElement("button");s.className="n8n-chat-quick-reply-btn",s.textContent=a,s.addEventListener("click",()=>{this.input.value=a,this.sendMessage(),i.remove()}),i.appendChild(s)}),this.messageList.appendChild(i),this.messageList.scrollTop=this.messageList.scrollHeight}incrementUnread(){this.isOpen||(this.unreadCount++,this.updateUnreadBadge())}updateUnreadBadge(){if(!this.launcher)return;const e=this.launcher.querySelector(".n8n-chat-unread-badge");e&&(e.textContent=this.unreadCount,e.style.display=this.unreadCount>0?"flex":"none")}clearUnreadCount(){this.unreadCount=0,this.updateUnreadBadge()}loadHistory(){if(this.enableHistory)try{const e=localStorage.getItem(this.historyKey);if(!e)return;JSON.parse(e).forEach(i=>{this.addMessageWithoutSaving(i.text,i.sender,new Date(i.timestamp))})}catch(e){console.warn("Failed to load chat history:",e)}}addMessageWithoutSaving(e,t,i){const a=document.createElement("div");a.className=`n8n-chat-bubble n8n-chat-${t}-message`;const s=document.createElement("div");s.className="n8n-chat-message-text",s.textContent=e;const n=document.createElement("div");n.className="n8n-chat-message-timestamp",n.textContent=this.formatTimestamp(i),n.setAttribute("data-timestamp",i.toISOString()),a.appendChild(s),a.appendChild(n),this.messageList.appendChild(a),this.messageList.scrollTop=this.messageList.scrollHeight}saveToHistory(e,t,i=new Date){if(this.enableHistory)try{const a=localStorage.getItem(this.historyKey);let s=a?JSON.parse(a):[];s.push({text:e,sender:t,timestamp:i.toISOString()}),s.length>this.maxHistoryItems&&(s=s.slice(-this.maxHistoryItems)),localStorage.setItem(this.historyKey,JSON.stringify(s))}catch(a){console.warn("Failed to save chat history:",a)}}loadState(){try{const e=localStorage.getItem(this.stateKey)}catch(e){console.warn("Failed to load chat state:",e)}}saveState(){try{localStorage.setItem(this.stateKey,JSON.stringify({isOpen:this.isOpen,timestamp:new Date().toISOString()}))}catch(e){console.warn("Failed to save chat state:",e)}}setupOfflineDetection(){window.addEventListener("online",()=>{this.isOnline=!0,this.updateOnlineStatus()}),window.addEventListener("offline",()=>{this.isOnline=!1,this.updateOnlineStatus()})}updateOnlineStatus(){if(!this.container)return;const e=this.container.querySelector(".n8n-chat-offline-indicator");if(!this.isOnline&&!e){const t=document.createElement("div");t.className="n8n-chat-offline-indicator",t.innerHTML=`
322
+ ${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", (t) => {
324
+ t.preventDefault(), this.sendMessage();
325
+ }), this.input.addEventListener("keypress", (t) => {
326
+ t.key === "Enter" && (t.preventDefault(), this.sendMessage());
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", (i) => this._handleFileSelect(i)), this._setupKeyboardNavigation(), this.loadHistory(), this.messageList.children.length === 0 && this.addMessage(this.initialMessage, "ai");
328
+ }
329
+ /**
330
+ * Inject the header avatar/logo as a real DOM element so we can attach
331
+ * an onerror handler. Falls back gracefully when the image cannot be loaded:
332
+ * - avatarUrl fails → image is hidden (no broken icon in header)
333
+ * - logoUrl fails → image is hidden (clean header, title still shows)
334
+ */
335
+ _injectHeaderImage() {
336
+ const e = this.container.querySelector(".n8n-chat-header-content");
337
+ if (!e) return;
338
+ const t = e.querySelector(".n8n-chat-title");
339
+ if (this.avatarUrl) {
340
+ const i = document.createElement("img");
341
+ i.className = "n8n-chat-header-avatar", i.alt = "", i.src = this.avatarUrl, i.onerror = () => {
342
+ i.onerror = null, i.remove();
343
+ }, e.insertBefore(i, t);
344
+ } else if (this.showLogo) {
345
+ const i = document.createElement("img");
346
+ i.className = "n8n-chat-logo", i.alt = "", i.src = this.logoUrl, i.onerror = () => {
347
+ i.onerror = null, i.remove();
348
+ }, e.insertBefore(i, t);
349
+ }
350
+ }
351
+ /**
352
+ * Build footer HTML based on showBranding + customFooter options.
353
+ * Mirrors the dashboard "Branding" tab behaviour:
354
+ * - showBranding: true + no customFooter → "Powered by Pindai.ai"
355
+ * - showBranding: true + customFooter set → "Powered by Pindai.ai | <customFooter>"
356
+ * - showBranding: false + customFooter set → only "<customFooter>"
357
+ * - showBranding: false + no customFooter → no footer
358
+ */
359
+ _buildFooterHtml() {
360
+ const e = [];
361
+ return this.showBranding && e.push(`<span>${this.i18n.t("poweredBy")} <a href="https://pindai.ai" target="_blank" rel="noopener noreferrer">Pindai.ai</a></span>`), this.customFooter && e.push(`<span>${this._renderSimpleMarkdown(this.customFooter)}</span>`), e.length === 0 ? "" : `<div class="n8n-chat-watermark">${e.join(' <span class="n8n-chat-footer-sep">|</span> ')}</div>`;
362
+ }
363
+ /**
364
+ * Minimal safe markdown renderer — only handles links: [text](url)
365
+ * No eval, no innerHTML from untrusted HTML — URLs are validated.
366
+ */
367
+ _renderSimpleMarkdown(e) {
368
+ return this._escapeHtml(e).replace(
369
+ /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g,
370
+ '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>'
371
+ );
372
+ }
373
+ _escapeHtml(e) {
374
+ return String(e).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
375
+ }
376
+ // ─────────────────────────────────────────────────────────────────────────
377
+ // Toggle open/close
378
+ // ─────────────────────────────────────────────────────────────────────────
379
+ toggleChatWindow() {
380
+ this.isOpen ? (this.container.classList.remove("n8n-chat-widget--open"), this.launcher && this.launcher.classList.remove("n8n-chat-launcher--hidden")) : (this.container || this._initChatWindow(), setTimeout(() => {
381
+ this.container.classList.add("n8n-chat-widget--open"), this.launcher && this.launcher.classList.add("n8n-chat-launcher--hidden"), this._dismissBubble(), this.input.focus(), this._clearUnreadCount();
382
+ }, 10)), this.isOpen = !this.isOpen, this.saveState();
383
+ }
384
+ // ─────────────────────────────────────────────────────────────────────────
385
+ // Default launcher icon
386
+ // ─────────────────────────────────────────────────────────────────────────
387
+ _getDefaultLauncherIcon() {
388
+ return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>')}`;
389
+ }
390
+ // ─────────────────────────────────────────────────────────────────────────
391
+ // Timestamp formatting
392
+ // ─────────────────────────────────────────────────────────────────────────
393
+ _formatTimestamp(e) {
394
+ const i = /* @__PURE__ */ new Date() - e;
395
+ 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", {
396
+ hour: "2-digit",
397
+ minute: "2-digit"
398
+ });
399
+ }
400
+ // ─────────────────────────────────────────────────────────────────────────
401
+ // Messages
402
+ // ─────────────────────────────────────────────────────────────────────────
403
+ addMessage(e, t, i = /* @__PURE__ */ new Date()) {
404
+ const s = document.createElement("div");
405
+ s.className = `n8n-chat-bubble n8n-chat-${t}-message`;
406
+ const a = document.createElement("div");
407
+ a.className = "n8n-chat-message-text", a.textContent = e;
408
+ const n = document.createElement("div");
409
+ 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();
410
+ }
411
+ /**
412
+ * Create a streaming message bubble (text filled incrementally via SSE).
413
+ * Returns the text node for direct DOM updates.
414
+ */
415
+ _createStreamingBubble() {
416
+ const e = document.createElement("div");
417
+ e.className = "n8n-chat-bubble n8n-chat-ai-message n8n-chat-bubble--streaming";
418
+ const t = document.createElement("div");
419
+ t.className = "n8n-chat-message-text", t.textContent = "";
420
+ const i = document.createElement("span");
421
+ i.className = "n8n-chat-stream-cursor";
422
+ const s = document.createElement("div");
423
+ 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 };
424
+ }
425
+ addMessageWithoutSaving(e, t, i) {
426
+ const s = document.createElement("div");
427
+ s.className = `n8n-chat-bubble n8n-chat-${t}-message`;
428
+ const a = document.createElement("div");
429
+ a.className = "n8n-chat-message-text", a.textContent = e;
430
+ const n = document.createElement("div");
431
+ 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;
432
+ }
433
+ // ─────────────────────────────────────────────────────────────────────────
434
+ // Typing indicator
435
+ // ─────────────────────────────────────────────────────────────────────────
436
+ _showTypingIndicator(e) {
437
+ let t = this.messageList.querySelector(".n8n-chat-typing-indicator");
438
+ if (e) {
439
+ if (!t) {
440
+ t = document.createElement("div"), t.className = "n8n-chat-bubble n8n-chat-ai-message n8n-chat-typing-indicator";
441
+ const i = "<span></span><span></span><span></span>";
442
+ this.showActionIndicators ? t.innerHTML = `
443
+ <div class="n8n-chat-typing-dots">${i}</div>
444
+ <span class="n8n-chat-typing-label">${this.i18n.t("thinkingIndicator")}</span>
445
+ ` : t.innerHTML = i, t.setAttribute("aria-label", this.i18n.t("typingIndicator")), this.messageList.appendChild(t), this.messageList.scrollTop = this.messageList.scrollHeight;
446
+ }
447
+ } else t && t.remove();
448
+ }
449
+ // ─────────────────────────────────────────────────────────────────────────
450
+ // File upload
451
+ // ─────────────────────────────────────────────────────────────────────────
452
+ _handleFileSelect(e) {
453
+ Array.from(e.target.files).forEach((i) => {
454
+ if (!this.allowedFileTypes.includes(i.type)) {
455
+ this.addMessage(this.i18n.t("fileTypeNotSupported", { filename: i.name }), "ai");
456
+ return;
457
+ }
458
+ if (i.size > this.maxFileSize) {
459
+ const s = this.maxFileSize / 1024 / 1024;
460
+ this.addMessage(this.i18n.t("fileTooLarge", { filename: i.name, maxSize: s }), "ai");
461
+ return;
462
+ }
463
+ if (this.uploadedFiles.length >= this.maxFiles) {
464
+ this.addMessage(this.i18n.t("maxFilesExceeded", { maxFiles: this.maxFiles }), "ai");
465
+ return;
466
+ }
467
+ this.uploadedFiles.push(i), this._renderFilePreview(i);
468
+ }), e.target.value = "";
469
+ }
470
+ _renderFilePreview(e) {
471
+ const t = this.container.querySelector(".n8n-chat-file-preview");
472
+ if (!t) return;
473
+ t.style.display = "flex";
474
+ const i = document.createElement("div");
475
+ i.className = "n8n-chat-file-item", i.innerHTML = `
476
+ <span class="n8n-chat-file-name">${this._escapeHtml(e.name)}</span>
477
+ <button class="n8n-chat-file-remove" aria-label="${this.i18n.t("ariaRemoveFile")}">&times;</button>
478
+ `, i.querySelector(".n8n-chat-file-remove").addEventListener("click", () => {
479
+ this.uploadedFiles = this.uploadedFiles.filter((s) => s.name !== e.name), i.remove(), this.uploadedFiles.length === 0 && (t.style.display = "none");
480
+ }), t.appendChild(i);
481
+ }
482
+ // ─────────────────────────────────────────────────────────────────────────
483
+ // Send message — entry point
484
+ // ─────────────────────────────────────────────────────────────────────────
485
+ sendMessage() {
486
+ return d(this, null, function* () {
487
+ const e = this.input.value.trim();
488
+ if (!(!e && this.uploadedFiles.length === 0 || this.isLoading)) {
489
+ try {
490
+ this._checkRateLimit();
491
+ } catch (t) {
492
+ this.addMessage(t.message, "ai");
493
+ return;
494
+ }
495
+ if (!this.isOnline) {
496
+ this.addMessage(this.i18n.t("connectionLost"), "ai");
497
+ return;
498
+ }
499
+ this.isLoading = !0, this.button.disabled = !0, this.input.disabled = !0, e && this.addMessage(e, "user"), this.input.value = "", this._showTypingIndicator(!0);
500
+ try {
501
+ if (this._agentMode && this.enableStreaming && this.uploadedFiles.length === 0)
502
+ yield this._sendWithStreaming(e);
503
+ else {
504
+ const t = yield this._sendMessageWithRetry(e, this.uploadedFiles);
505
+ this._showTypingIndicator(!1), this.addMessage(t, "ai"), this.showQuickReplies && this.quickReplies.length > 0 && this._renderQuickReplies();
506
+ }
507
+ } catch (t) {
508
+ this._showTypingIndicator(!1), this.addMessage(this._getErrorMessage(t), "ai");
509
+ } finally {
510
+ if (this.isLoading = !1, this.button.disabled = !1, this.input.disabled = !1, this.input.focus(), this.uploadedFiles.length > 0) {
511
+ this.uploadedFiles = [];
512
+ const t = this.container.querySelector(".n8n-chat-file-preview");
513
+ t && (t.innerHTML = "", t.style.display = "none");
514
+ }
515
+ }
516
+ }
517
+ });
518
+ }
519
+ // ─────────────────────────────────────────────────────────────────────────
520
+ // SSE streaming (agent-api mode, no files)
521
+ // ─────────────────────────────────────────────────────────────────────────
522
+ _sendWithStreaming(e) {
523
+ return d(this, null, function* () {
524
+ const t = yield this._generateEmbedToken(), i = new URLSearchParams({
525
+ message: e,
526
+ sessionId: this.sessionId,
527
+ embedToken: t
528
+ });
529
+ this.visitorName && i.set("visitor_name", this.visitorName), this.visitorEmail && i.set("visitor_email", this.visitorEmail);
530
+ const s = `${this.apiBaseUrl}/v1/chat/${this.agentId}/stream?${i}`;
531
+ return new Promise((a, n) => {
532
+ this._showTypingIndicator(!1);
533
+ const { bubble: r, textNode: l, cursor: o, timeNode: c } = this._createStreamingBubble();
534
+ let h = "", b = "active";
535
+ const p = new EventSource(s);
536
+ p.onmessage = (f) => {
537
+ try {
538
+ 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));
540
+ } catch (m) {
541
+ }
542
+ }, p.onerror = () => {
543
+ p.close(), o.remove(), r.classList.remove("n8n-chat-bubble--streaming"), h ? (this.saveToHistory(h, "ai"), a(h)) : n(new Error(this.i18n.t("streamingError")));
544
+ };
545
+ });
546
+ });
547
+ }
548
+ // ─────────────────────────────────────────────────────────────────────────
549
+ // POST with retry (agent-api mode + legacy mode)
550
+ // ─────────────────────────────────────────────────────────────────────────
551
+ _sendMessageWithRetry(s) {
552
+ return d(this, arguments, function* (e, t = [], i = 0) {
553
+ try {
554
+ const a = new AbortController(), n = setTimeout(() => a.abort(), this.requestTimeout), r = new FormData();
555
+ if (r.append("sessionId", this.sessionId), r.append("message", e), this._agentMode) {
556
+ const c = yield this._generateEmbedToken();
557
+ r.append("embedToken", c), this.visitorName && r.append("visitor_name", this.visitorName), this.visitorEmail && r.append("visitor_email", this.visitorEmail);
558
+ } else this.embedToken && r.append("embedToken", this.embedToken);
559
+ t.forEach((c, h) => {
560
+ r.append(`file${h}`, c);
561
+ });
562
+ const l = yield fetch(this.webhookUrl, {
563
+ method: "POST",
564
+ body: r,
565
+ signal: a.signal
566
+ });
567
+ if (clearTimeout(n), !l.ok) {
568
+ if (l.status >= 500 && i < this.maxRetries)
569
+ return yield this._delay(this.retryDelay * (i + 1)), this._sendMessageWithRetry(e, t, i + 1);
570
+ const c = yield l.json().catch(() => ({}));
571
+ throw new Error(c.message || `Network error: ${l.statusText}`);
572
+ }
573
+ const o = yield l.json();
574
+ if (!o.response)
575
+ 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;
577
+ } catch (a) {
578
+ if (a.name === "AbortError")
579
+ throw new Error(this.i18n.t("errorTimeout"));
580
+ if ((a.message.includes("NetworkError") || a.message.includes("Failed to fetch")) && i < this.maxRetries)
581
+ return yield this._delay(this.retryDelay * (i + 1)), this._sendMessageWithRetry(e, t, i + 1);
582
+ throw a;
583
+ }
584
+ });
585
+ }
586
+ _delay(e) {
587
+ return new Promise((t) => setTimeout(t, e));
588
+ }
589
+ _getErrorMessage(e) {
590
+ const t = e.message || "";
591
+ 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");
592
+ }
593
+ // ─────────────────────────────────────────────────────────────────────────
594
+ // Rate limiting
595
+ // ─────────────────────────────────────────────────────────────────────────
596
+ _checkRateLimit() {
597
+ const e = Date.now();
598
+ if (this.messageTimes = this.messageTimes.filter((t) => e - t < this.rateLimitWindow), this.messageTimes.length >= this.rateLimit) {
599
+ const t = Math.ceil((this.rateLimitWindow - (e - this.messageTimes[0])) / 1e3);
600
+ throw new Error(this.i18n.t("errorRateLimit", { seconds: t }));
601
+ }
602
+ this.messageTimes.push(e);
603
+ }
604
+ // ─────────────────────────────────────────────────────────────────────────
605
+ // Quick replies
606
+ // ─────────────────────────────────────────────────────────────────────────
607
+ _renderQuickReplies(e = this.quickReplies) {
608
+ if (!this.showQuickReplies || e.length === 0) return;
609
+ const t = this.messageList.querySelector(".n8n-chat-quick-replies");
610
+ t && t.remove();
611
+ const i = document.createElement("div");
612
+ i.className = "n8n-chat-quick-replies", e.forEach((s) => {
613
+ const a = document.createElement("button");
614
+ a.className = "n8n-chat-quick-reply-btn", a.textContent = s, a.addEventListener("click", () => {
615
+ this.input.value = s, this.sendMessage(), i.remove();
616
+ }), i.appendChild(a);
617
+ }), this.messageList.appendChild(i), this.messageList.scrollTop = this.messageList.scrollHeight;
618
+ }
619
+ // ─────────────────────────────────────────────────────────────────────────
620
+ // Notification badge
621
+ // ─────────────────────────────────────────────────────────────────────────
622
+ _incrementUnread() {
623
+ this.isOpen || (this.unreadCount++, this._updateUnreadBadge());
624
+ }
625
+ _updateUnreadBadge() {
626
+ if (!this.launcher) return;
627
+ const e = this.launcher.querySelector(".n8n-chat-unread-badge");
628
+ e && (e.textContent = this.unreadCount, e.style.display = this.unreadCount > 0 ? "flex" : "none");
629
+ }
630
+ _clearUnreadCount() {
631
+ this.unreadCount = 0, this._updateUnreadBadge();
632
+ }
633
+ // ─────────────────────────────────────────────────────────────────────────
634
+ // History persistence
635
+ // ─────────────────────────────────────────────────────────────────────────
636
+ loadHistory() {
637
+ if (this.enableHistory)
638
+ try {
639
+ const e = localStorage.getItem(this.historyKey);
640
+ if (!e) return;
641
+ JSON.parse(e).forEach((t) => {
642
+ this.addMessageWithoutSaving(t.text, t.sender, new Date(t.timestamp));
643
+ });
644
+ } catch (e) {
645
+ console.warn("Failed to load chat history:", e);
646
+ }
647
+ }
648
+ saveToHistory(e, t, i = /* @__PURE__ */ new Date()) {
649
+ if (this.enableHistory)
650
+ try {
651
+ const s = localStorage.getItem(this.historyKey);
652
+ let a = s ? JSON.parse(s) : [];
653
+ 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));
654
+ } catch (s) {
655
+ console.warn("Failed to save chat history:", s);
656
+ }
657
+ }
658
+ // ─────────────────────────────────────────────────────────────────────────
659
+ // State persistence
660
+ // ─────────────────────────────────────────────────────────────────────────
661
+ loadState() {
662
+ try {
663
+ const e = localStorage.getItem(this.stateKey);
664
+ if (e) {
665
+ const t = JSON.parse(e);
666
+ this._bubbleDismissed = this.showBubbleOnce && t.bubbleDismissed || !1;
667
+ }
668
+ } catch (e) {
669
+ console.warn("Failed to load chat state:", e);
670
+ }
671
+ }
672
+ saveState() {
673
+ try {
674
+ localStorage.setItem(this.stateKey, JSON.stringify({
675
+ isOpen: this.isOpen,
676
+ bubbleDismissed: this._bubbleDismissed,
677
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
678
+ }));
679
+ } catch (e) {
680
+ console.warn("Failed to save chat state:", e);
681
+ }
682
+ }
683
+ // ─────────────────────────────────────────────────────────────────────────
684
+ // Offline detection
685
+ // ─────────────────────────────────────────────────────────────────────────
686
+ _setupOfflineDetection() {
687
+ window.addEventListener("online", () => {
688
+ this.isOnline = !0, this._updateOnlineStatus();
689
+ }), window.addEventListener("offline", () => {
690
+ this.isOnline = !1, this._updateOnlineStatus();
691
+ });
692
+ }
693
+ _updateOnlineStatus() {
694
+ if (!this.container) return;
695
+ const e = this.container.querySelector(".n8n-chat-offline-indicator");
696
+ if (!this.isOnline && !e) {
697
+ const t = document.createElement("div");
698
+ t.className = "n8n-chat-offline-indicator", t.innerHTML = `
43
699
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
44
700
  <line x1="1" y1="1" x2="23" y2="23"></line>
45
701
  <path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"></path>
@@ -50,5 +706,124 @@
50
706
  <line x1="12" y1="20" x2="12.01" y2="20"></line>
51
707
  </svg>
52
708
  <span>${this.i18n.t("offline")}</span>
53
- `,this.container.insertBefore(t,this.messageList)}else this.isOnline&&e&&e.remove();this.button&&(this.button.disabled=!this.isOnline||this.isLoading)}setupKeyboardNavigation(){document.addEventListener("keydown",e=>{e.key==="Escape"&&this.isOpen&&this.mode==="widget"&&this.toggleChatWindow()}),this.container.addEventListener("keydown",e=>{if(e.key==="Tab"){const t=this.container.querySelectorAll('button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'),i=t[0],a=t[t.length-1];e.shiftKey&&document.activeElement===i?(e.preventDefault(),a.focus()):!e.shiftKey&&document.activeElement===a&&(e.preventDefault(),i.focus())}})}}window.PindaiChatWidget={init:l=>{if(!document.querySelector(".n8n-chat-widget")&&!document.querySelector(".n8n-chat-launcher"))return new o(l)}},window.N8nChatWidget=window.PindaiChatWidget});
709
+ `, this.container.insertBefore(t, this.messageList);
710
+ } else this.isOnline && e && e.remove();
711
+ this.button && (this.button.disabled = !this.isOnline || this.isLoading);
712
+ }
713
+ // ─────────────────────────────────────────────────────────────────────────
714
+ // Keyboard navigation
715
+ // ─────────────────────────────────────────────────────────────────────────
716
+ _setupKeyboardNavigation() {
717
+ document.addEventListener("keydown", (e) => {
718
+ e.key === "Escape" && this.isOpen && this.mode === "widget" && this.toggleChatWindow();
719
+ }), this.container.addEventListener("keydown", (e) => {
720
+ if (e.key !== "Tab") return;
721
+ const t = this.container.querySelectorAll(
722
+ 'button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'
723
+ ), i = t[0], s = t[t.length - 1];
724
+ e.shiftKey && document.activeElement === i ? (e.preventDefault(), s.focus()) : !e.shiftKey && document.activeElement === s && (e.preventDefault(), i.focus());
725
+ });
726
+ }
727
+ // ─────────────────────────────────────────────────────────────────────────
728
+ // Human-agent polling
729
+ // ─────────────────────────────────────────────────────────────────────────
730
+ _startPolling() {
731
+ return d(this, null, function* () {
732
+ !this.pollingUrl || this._pollingTimer || (this._lastMessageTimestamp = this._lastMessageTimestamp || (/* @__PURE__ */ new Date()).toISOString(), this._pollingTimer = setInterval(() => d(this, null, function* () {
733
+ try {
734
+ const e = new URL(this.pollingUrl);
735
+ if (e.searchParams.set("sessionId", this.sessionId), e.searchParams.set("since", this._lastMessageTimestamp), this._agentMode) {
736
+ const s = yield this._generateEmbedToken();
737
+ e.searchParams.set("embedToken", s);
738
+ } else this.embedToken && e.searchParams.set("embedToken", this.embedToken);
739
+ const t = yield fetch(e.toString());
740
+ if (!t.ok) return;
741
+ const i = yield t.json();
742
+ if (i.messages && i.messages.length > 0) {
743
+ i.messages.forEach((a) => {
744
+ a.role === "human_agent" && this.addMessage(a.content, "ai");
745
+ });
746
+ const s = i.messages[i.messages.length - 1];
747
+ this._lastMessageTimestamp = s.created_at, this.isOpen || (this.unreadCount += i.messages.filter((a) => a.role === "human_agent").length, this._updateUnreadBadge());
748
+ }
749
+ (i.status === "active" || i.status === "resolved") && this._stopPolling();
750
+ } catch (e) {
751
+ }
752
+ }), this.pollingInterval));
753
+ });
754
+ }
755
+ _stopPolling() {
756
+ this._pollingTimer && (clearInterval(this._pollingTimer), this._pollingTimer = null);
757
+ }
758
+ // ─────────────────────────────────────────────────────────────────────────
759
+ // Legacy aliases (keep public API stable)
760
+ // ─────────────────────────────────────────────────────────────────────────
761
+ initLauncher() {
762
+ return this._initLauncher();
763
+ }
764
+ initChatWindow() {
765
+ return this._initChatWindow();
766
+ }
767
+ showTypingIndicator(e) {
768
+ return this._showTypingIndicator(e);
769
+ }
770
+ handleFileSelect(e) {
771
+ return this._handleFileSelect(e);
772
+ }
773
+ renderFilePreview(e) {
774
+ return this._renderFilePreview(e);
775
+ }
776
+ sendMessageWithRetry(e, t, i) {
777
+ return this._sendMessageWithRetry(e, t, i);
778
+ }
779
+ checkRateLimit() {
780
+ return this._checkRateLimit();
781
+ }
782
+ renderQuickReplies(e) {
783
+ return this._renderQuickReplies(e);
784
+ }
785
+ incrementUnread() {
786
+ return this._incrementUnread();
787
+ }
788
+ updateUnreadBadge() {
789
+ return this._updateUnreadBadge();
790
+ }
791
+ clearUnreadCount() {
792
+ return this._clearUnreadCount();
793
+ }
794
+ setupOfflineDetection() {
795
+ return this._setupOfflineDetection();
796
+ }
797
+ updateOnlineStatus() {
798
+ return this._updateOnlineStatus();
799
+ }
800
+ setupKeyboardNavigation() {
801
+ return this._setupKeyboardNavigation();
802
+ }
803
+ startPolling() {
804
+ return this._startPolling();
805
+ }
806
+ stopPolling() {
807
+ return this._stopPolling();
808
+ }
809
+ formatTimestamp(e) {
810
+ return this._formatTimestamp(e);
811
+ }
812
+ getDefaultIcon() {
813
+ return this._getDefaultLauncherIcon();
814
+ }
815
+ getErrorMessage(e) {
816
+ return this._getErrorMessage(e);
817
+ }
818
+ delay(e) {
819
+ return this._delay(e);
820
+ }
821
+ }
822
+ window.PindaiChatWidget = {
823
+ init: (u) => {
824
+ if (!document.querySelector(".n8n-chat-widget") && !document.querySelector(".n8n-chat-launcher"))
825
+ return new w(u);
826
+ }
827
+ };
828
+ window.N8nChatWidget = window.PindaiChatWidget;
54
829
  //# sourceMappingURL=pindai-chat-widget.js.map