@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.
- package/README.md +387 -346
- package/dist/pindai-chat-widget.css +1 -1
- package/dist/pindai-chat-widget.js +797 -22
- package/dist/pindai-chat-widget.js.map +1 -1
- package/dist/pindai-chat-widget.umd.js +46 -0
- package/dist/pindai-chat-widget.umd.js.map +1 -0
- package/package.json +13 -5
- package/src/i18n.js +18 -0
- package/src/main.js +785 -299
- package/src/style.css +272 -5
|
@@ -1,28 +1,316 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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")}">×</button>
|
|
11
302
|
</div>
|
|
12
303
|
<div class="n8n-chat-messages" role="log" aria-live="polite" aria-atomic="false"></div>
|
|
13
|
-
|
|
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",
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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")}">×</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)
|
|
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
|