@pindai-ai/chat-widget 2.0.3 → 2.0.4

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 CHANGED
@@ -17,7 +17,8 @@
17
17
  <script>
18
18
  document.addEventListener('DOMContentLoaded', () => {
19
19
  window.PindaiChatWidget.init({
20
- webhookUrl: 'https://your-backend.com/webhook/chat'
20
+ webhookUrl: 'https://your-backend.com/webhook/chat',
21
+ showLogo: false // Recommended: hide logo if you don't have a custom one
21
22
  });
22
23
  });
23
24
  </script>
@@ -73,7 +74,7 @@ npm install @pindai-ai/chat-widget
73
74
 
74
75
  ```bash
75
76
  # Clone repository
76
- git clone https://github.com/pindai-ai/pindai-chat-widget.git
77
+ git clone https://github.com/PindaiAI/pindai-chat-widget.git
77
78
  cd pindai-chat-widget
78
79
 
79
80
  # Install dependencies
@@ -110,8 +111,10 @@ npm run build
110
111
  document.addEventListener('DOMContentLoaded', function () {
111
112
  window.PindaiChatWidget.init({
112
113
  webhookUrl: 'https://your-backend.com/webhook/chat',
114
+ mode: 'widget',
115
+ locale: 'id', // 'id' for Indonesian, 'en' for English
113
116
  title: 'Customer Support',
114
- locale: 'id' // 'id' for Indonesian, 'en' for English
117
+ showLogo: false // Hide logo by default (recommended)
115
118
  });
116
119
  });
117
120
  </script>
@@ -144,8 +147,8 @@ Replace `YOUR-USERNAME` with your GitHub username
144
147
  | `title` | string | Localized | Chat header title |
145
148
  | `initialMessage` | string | Localized | First AI message |
146
149
  | **Branding** |
147
- | `logoUrl` | string | `'https://pindai.ai/logo.png'` | Header logo URL |
148
- | `showLogo` | boolean | `true` | Show/hide logo |
150
+ | `logoUrl` | string | `'https://pindai.ai/logo.png'` | Header logo URL (use data URI for custom logo) |
151
+ | `showLogo` | boolean | `true` | Show/hide logo. **Recommended: `false`** if you don't have a custom logo |
149
152
  | `launcherColor` | string | `'#0066FF'` | Launcher button background color |
150
153
  | `launcherIconUrl` | string | Default icon | Custom launcher icon URL |
151
154
  | `sendButtonColor` | string | `'#0066FF'` | Send button background color |
@@ -285,18 +288,42 @@ def chat():
285
288
 
286
289
  ### Custom Branding
287
290
 
291
+ **Option 1: No Logo (Recommended)**
292
+ ```javascript
293
+ window.PindaiChatWidget.init({
294
+ webhookUrl: 'https://your-backend.com/webhook/chat',
295
+ locale: 'id',
296
+ title: 'Bantuan Pelanggan',
297
+ showLogo: false, // No logo, cleaner header
298
+ launcherColor: '#FF5733',
299
+ sendButtonColor: '#4CAF50',
300
+ accentColor: '#FFC107',
301
+ });
302
+ ```
303
+
304
+ **Option 2: Custom Logo URL**
288
305
  ```javascript
289
306
  window.PindaiChatWidget.init({
290
307
  webhookUrl: 'https://your-backend.com/webhook/chat',
291
308
  locale: 'id',
292
309
  title: 'Bantuan Pelanggan',
293
310
  logoUrl: 'https://yourcompany.com/logo.png',
311
+ showLogo: true,
294
312
  launcherColor: '#FF5733',
295
313
  sendButtonColor: '#4CAF50',
296
314
  accentColor: '#FFC107',
297
315
  });
298
316
  ```
299
317
 
318
+ **Option 3: Data URI Logo (No External Request)**
319
+ ```javascript
320
+ window.PindaiChatWidget.init({
321
+ webhookUrl: 'https://your-backend.com/webhook/chat',
322
+ logoUrl: 'data:image/svg+xml;charset=UTF-8,%3csvg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"%3e%3ccircle cx="16" cy="16" r="14" fill="%230066FF"/%3e%3ctext x="16" y="21" font-size="16" font-weight="bold" text-anchor="middle" fill="white"%3eP%3c/text%3e%3c/svg%3e',
323
+ showLogo: true,
324
+ });
325
+ ```
326
+
300
327
  ### Custom Quick Replies
301
328
 
302
329
  ```javascript
@@ -357,6 +384,68 @@ This widget meets **WCAG 2.2 Level AA** compliance:
357
384
 
358
385
  ---
359
386
 
387
+ ## 🔧 Troubleshooting
388
+
389
+ ### Logo Not Loading / 404 Error
390
+
391
+ **Problem:** You see a broken image or 404 error for the logo.
392
+
393
+ **Solution:** Set `showLogo: false` in your configuration:
394
+
395
+ ```javascript
396
+ window.PindaiChatWidget.init({
397
+ webhookUrl: 'https://your-backend.com/webhook/chat',
398
+ showLogo: false // This hides the logo completely
399
+ });
400
+ ```
401
+
402
+ Or provide your own logo URL:
403
+
404
+ ```javascript
405
+ window.PindaiChatWidget.init({
406
+ webhookUrl: 'https://your-backend.com/webhook/chat',
407
+ logoUrl: 'https://your-domain.com/your-logo.png',
408
+ showLogo: true
409
+ });
410
+ ```
411
+
412
+ ### Widget Not Appearing
413
+
414
+ **Problem:** Widget doesn't show up on the page.
415
+
416
+ **Solutions:**
417
+ 1. Check browser console for errors (F12)
418
+ 2. Verify the `webhookUrl` is correct
419
+ 3. Make sure the script loads after the DOM is ready
420
+ 4. Check if there are CSP (Content Security Policy) restrictions
421
+
422
+ ### Webhook Returns Error
423
+
424
+ **Problem:** Messages fail to send or you get errors.
425
+
426
+ **Solutions:**
427
+ 1. Verify your n8n/backend workflow is active
428
+ 2. Check the webhook URL is correct and accessible
429
+ 3. Test the webhook with curl:
430
+ ```bash
431
+ curl -X POST https://your-webhook-url \
432
+ -H "Content-Type: application/json" \
433
+ -d '{"sessionId":"test","message":"Hello"}'
434
+ ```
435
+ 4. Check CORS settings on your backend
436
+
437
+ ### CDN Not Updating
438
+
439
+ **Problem:** Changes not reflecting after npm publish.
440
+
441
+ **Solutions:**
442
+ 1. Wait 5-10 minutes for CDN cache to clear
443
+ 2. Use version-specific URL: `@pindai-ai/chat-widget@2.0.3`
444
+ 3. Add cache buster: `...widget.js?v=2`
445
+ 4. Force CDN purge: Visit `https://purge.jsdelivr.net/npm/@pindai-ai/chat-widget@2/dist/pindai-chat-widget.js`
446
+
447
+ ---
448
+
360
449
  ## 🌐 Browser Support
361
450
 
362
451
  | Browser | Version |
@@ -427,6 +516,20 @@ npm run preview
427
516
 
428
517
  ## 📝 Changelog
429
518
 
519
+ ### Version 2.0.3 (2026-02-06)
520
+
521
+ **Documentation:**
522
+ - Added comprehensive troubleshooting section
523
+ - Clarified `showLogo: false` recommendation in Quick Start
524
+ - Added multiple logo customization options (no logo, URL, data URI)
525
+ - Improved configuration examples with best practices
526
+
527
+ ### Version 2.0.2 (2026-02-06)
528
+
529
+ **Updates:**
530
+ - Published with watermark feature to npm
531
+ - CDN distribution updated
532
+
430
533
  ### Version 2.0.1 (2026-02-06)
431
534
 
432
535
  **Updates:**
@@ -495,8 +598,8 @@ MIT © [Pindai.ai](https://pindai.ai)
495
598
 
496
599
  - **npm Package:** [npmjs.com/package/@pindai-ai/chat-widget](https://www.npmjs.com/package/@pindai-ai/chat-widget)
497
600
  - **jsDelivr CDN:** [jsdelivr.com/package/npm/@pindai-ai/chat-widget](https://www.jsdelivr.com/package/npm/@pindai-ai/chat-widget)
498
- - **GitHub Repository:** [github.com/pindai-ai/pindai-chat-widget](https://github.com/pindai-ai/pindai-chat-widget)
499
- - **Issues & Bugs:** [github.com/pindai-ai/pindai-chat-widget/issues](https://github.com/pindai-ai/pindai-chat-widget/issues)
601
+ - **GitHub Repository:** [github.com/pindai-ai/pindai-chat-widget](https://github.com/PindaiAI/pindai-chat-widget)
602
+ - **Issues & Bugs:** [github.com/pindai-ai/pindai-chat-widget/issues](https://github.com/PindaiAI/pindai-chat-widget/issues)
500
603
  - **Pindai.ai Website:** [pindai.ai](https://pindai.ai)
501
604
 
502
605
  ---
@@ -504,7 +607,7 @@ MIT © [Pindai.ai](https://pindai.ai)
504
607
  ## 🆘 Support
505
608
 
506
609
  - **Documentation:** [N8N_WORKFLOW_GUIDE.md](./N8N_WORKFLOW_GUIDE.md)
507
- - **Issues:** [GitHub Issues](https://github.com/pindai-ai/pindai-chat-widget/issues)
610
+ - **Issues:** [GitHub Issues](https://github.com/PindaiAI/pindai-chat-widget/issues)
508
611
  - **Email:** support@pindai.ai
509
612
  - **Website:** [pindai.ai](https://pindai.ai)
510
613
 
@@ -1,4 +1,4 @@
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=`
1
+ (function(l){typeof define=="function"&&define.amd?define(l):l()})(function(){"use strict";var u=(l,m,r)=>new Promise((o,e)=>{var t=s=>{try{a(r.next(s))}catch(n){e(n)}},i=s=>{try{a(r.throw(s))}catch(n){e(n)}},a=s=>s.done?o(s.value):Promise.resolve(s.value).then(t,i);a((r=r.apply(l,m)).next())});const l={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 m{constructor(e="id"){this.locale=this.isValidLocale(e)?e:"id"}isValidLocale(e){return Object.keys(l).includes(e)}t(e,t={}){var a;let i=((a=l[this.locale])==null?void 0:a[e])||l.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(l)}}class r{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 m(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.embedToken=e.embedToken||null,this.pollingUrl=e.pollingUrl||null,this.pollingEnabled=!!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.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
2
  <img src="${this.launcherIconUrl}" alt="">
3
3
  <span class="n8n-chat-unread-badge" style="display: none;">0</span>
4
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=`
@@ -39,7 +39,7 @@
39
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
40
  <span class="n8n-chat-file-name">${e.name}</span>
41
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=`
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 u(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 u(this,arguments,function*(e,t=[],i=0){try{const s=new AbortController,n=setTimeout(()=>s.abort(),this.requestTimeout),h=new FormData;h.append("sessionId",this.sessionId),h.append("message",e),this.embedToken&&h.append("embedToken",this.embedToken),t.forEach((g,p)=>{h.append(`file${p}`,g)});const d=yield fetch(this.webhookUrl,{method:"POST",body:h,signal:s.signal});if(clearTimeout(n),!d.ok){if(d.status>=500&&i<this.maxRetries)return yield this.delay(this.retryDelay*(i+1)),this.sendMessageWithRetry(e,t,i+1);const g=yield d.json().catch(()=>({}));throw new Error(g.message||`Network error: ${d.statusText}`)}const c=yield d.json();if(!c.response)throw new Error(this.i18n.t("errorInvalidResponse"));return c.status&&c.status!=="active"&&this.pollingEnabled?this.startPolling():this.stopPolling(),c.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=`
43
43
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
44
44
  <line x1="1" y1="1" x2="23" y2="23"></line>
45
45
  <path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"></path>
@@ -50,5 +50,5 @@
50
50
  <line x1="12" y1="20" x2="12.01" y2="20"></line>
51
51
  </svg>
52
52
  <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});
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())}})}startPolling(){!this.pollingUrl||this._pollingTimer||(this._lastMessageTimestamp=this._lastMessageTimestamp||new Date().toISOString(),this._pollingTimer=setInterval(()=>u(this,null,function*(){try{const e=new URL(this.pollingUrl);e.searchParams.set("sessionId",this.sessionId),e.searchParams.set("since",this._lastMessageTimestamp),this.embedToken&&e.searchParams.set("embedToken",this.embedToken);const t=yield fetch(e.toString());if(!t.ok)return;const i=yield t.json();if(i.messages&&i.messages.length>0){i.messages.forEach(s=>{s.role==="human_agent"&&this.addMessage(s.content,"ai")});const a=i.messages[i.messages.length-1];this._lastMessageTimestamp=a.created_at,this.isOpen||(this.unreadCount+=i.messages.filter(s=>s.role==="human_agent").length,this.updateUnreadBadge())}(i.status==="active"||i.status==="resolved")&&this.stopPolling()}catch(e){}}),this.pollingInterval))}stopPolling(){this._pollingTimer&&(clearInterval(this._pollingTimer),this._pollingTimer=null)}}window.PindaiChatWidget={init:o=>{if(!document.querySelector(".n8n-chat-widget")&&!document.querySelector(".n8n-chat-launcher"))return new r(o)}},window.N8nChatWidget=window.PindaiChatWidget});
54
54
  //# sourceMappingURL=pindai-chat-widget.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pindai-chat-widget.js","sources":["../src/i18n.js","../src/main.js"],"sourcesContent":["/**\n * Internationalization (i18n) System for Pindai Chat Widget\n * Supports Indonesian (id) and English (en) locales\n */\n\nexport const translations = {\n en: {\n // Widget UI\n title: 'Pindai Agent',\n placeholder: 'Write a message...',\n initialMessage: 'Hello! How can I help you today?',\n send: 'Send',\n close: 'Close',\n upload: 'Upload file',\n removeFile: 'Remove file',\n\n // Loading and status\n typingIndicator: 'AI is typing...',\n sending: 'Sending...',\n justNow: 'Just now',\n minutesAgo: '{minutes}m ago',\n\n // Offline/Online status\n offline: 'Offline - messages will be sent when online',\n connectionRestored: 'Connection restored',\n connectionLost: 'No internet connection',\n\n // Error messages\n errorGeneric: 'An error occurred. Please try again.',\n errorTimeout: 'Request timeout. Please try again.',\n errorNetwork: 'No internet connection. Check your network.',\n errorServer: 'Server is busy. Please try again later.',\n errorRateLimit: 'Too many messages. Please wait {seconds} seconds.',\n errorInvalidResponse: 'Invalid server response. Please contact support.',\n\n // File upload errors\n fileTypeNotSupported: 'File type not supported: {filename}',\n fileTooLarge: 'File too large: {filename} (max {maxSize}MB)',\n maxFilesExceeded: 'Maximum {maxFiles} files allowed',\n\n // Quick replies (default suggestions)\n quickReply1: 'How can I extract data from documents?',\n quickReply2: 'What file types are supported?',\n quickReply3: 'Tell me about pricing',\n quickReply4: 'Contact support',\n\n // Accessibility labels\n ariaOpenChat: 'Open chat widget',\n ariaCloseChat: 'Close chat window',\n ariaSendMessage: 'Send message',\n ariaMessageInput: 'Type your message',\n ariaUploadFile: 'Upload file',\n ariaRemoveFile: 'Remove file',\n ariaChatWindow: 'Chat window',\n ariaMessageLog: 'Chat messages',\n },\n\n id: {\n // Widget UI\n title: 'Pindai Agent',\n placeholder: 'Tulis pesan...',\n initialMessage: 'Halo! Bagaimana saya bisa membantu Anda hari ini?',\n send: 'Kirim',\n close: 'Tutup',\n upload: 'Unggah file',\n removeFile: 'Hapus file',\n\n // Loading and status\n typingIndicator: 'AI sedang mengetik...',\n sending: 'Mengirim...',\n justNow: 'Baru saja',\n minutesAgo: '{minutes}m yang lalu',\n\n // Offline/Online status\n offline: 'Offline - pesan akan dikirim saat online',\n connectionRestored: 'Koneksi kembali',\n connectionLost: 'Tidak ada koneksi internet',\n\n // Error messages\n errorGeneric: 'Terjadi kesalahan. Silakan coba lagi.',\n errorTimeout: 'Waktu permintaan habis. Silakan coba lagi.',\n errorNetwork: 'Tidak ada koneksi internet. Periksa jaringan Anda.',\n errorServer: 'Server sedang sibuk. Silakan coba lagi dalam beberapa saat.',\n errorRateLimit: 'Terlalu banyak pesan. Silakan tunggu {seconds} detik.',\n errorInvalidResponse: 'Respons server tidak valid. Silakan hubungi dukungan.',\n\n // File upload errors\n fileTypeNotSupported: 'Jenis file tidak didukung: {filename}',\n fileTooLarge: 'File terlalu besar: {filename} (maks {maxSize}MB)',\n maxFilesExceeded: 'Maksimal {maxFiles} file diperbolehkan',\n\n // Quick replies (default suggestions)\n quickReply1: 'Bagaimana cara ekstraksi dokumen?',\n quickReply2: 'Jenis file apa yang didukung?',\n quickReply3: 'Tentang harga',\n quickReply4: 'Hubungi dukungan',\n\n // Accessibility labels\n ariaOpenChat: 'Buka widget chat',\n ariaCloseChat: 'Tutup jendela chat',\n ariaSendMessage: 'Kirim pesan',\n ariaMessageInput: 'Ketik pesan Anda',\n ariaUploadFile: 'Unggah file',\n ariaRemoveFile: 'Hapus file',\n ariaChatWindow: 'Jendela chat',\n ariaMessageLog: 'Pesan chat',\n }\n};\n\n/**\n * I18n class for managing translations\n */\nexport class I18n {\n constructor(locale = 'id') {\n this.locale = this.isValidLocale(locale) ? locale : 'id';\n }\n\n /**\n * Check if locale is valid\n */\n isValidLocale(locale) {\n return Object.keys(translations).includes(locale);\n }\n\n /**\n * Translate a key with optional parameter substitution\n * @param {string} key - Translation key\n * @param {object} params - Parameters to substitute in the translation\n * @returns {string} Translated string\n */\n t(key, params = {}) {\n let text = translations[this.locale]?.[key] || translations.en[key] || key;\n\n // Replace parameters like {param} with actual values\n Object.keys(params).forEach(param => {\n const regex = new RegExp(`\\\\{${param}\\\\}`, 'g');\n text = text.replace(regex, params[param]);\n });\n\n return text;\n }\n\n /**\n * Change the current locale\n * @param {string} locale - New locale (id or en)\n */\n setLocale(locale) {\n if (this.isValidLocale(locale)) {\n this.locale = locale;\n return true;\n }\n console.warn(`Invalid locale: ${locale}. Keeping current locale: ${this.locale}`);\n return false;\n }\n\n /**\n * Get current locale\n * @returns {string} Current locale\n */\n getLocale() {\n return this.locale;\n }\n\n /**\n * Get all available locales\n * @returns {string[]} Array of available locale codes\n */\n getAvailableLocales() {\n return Object.keys(translations);\n }\n}\n\n// Export default instance for convenience\nexport default I18n;\n","import './style.css';\nimport { I18n } from './i18n.js';\n\n/**\n * Pindai Chat Widget - Modern, Accessible, Indonesian-focused\n * Version 2.0.0\n */\nclass PindaiChatWidget {\n constructor(options) {\n // Backward compatibility: support both webhookUrl and n8nUrl\n const apiEndpoint = options.webhookUrl || options.n8nUrl;\n\n if (!apiEndpoint) {\n throw new Error('PindaiChatWidget: \"webhookUrl\" option is required.');\n }\n\n // Core configuration\n this.webhookUrl = apiEndpoint;\n this.mode = options.mode || 'widget';\n\n // Internationalization\n this.locale = options.locale || 'id'; // Default to Indonesian\n this.i18n = new I18n(this.locale);\n\n // UI customization\n this.title = options.title || this.i18n.t('title');\n this.initialMessage = options.initialMessage || this.i18n.t('initialMessage');\n this.launcherIconUrl = options.launcherIconUrl || this.getDefaultIcon();\n\n // Branding\n this.logoUrl = options.logoUrl || 'https://pindai.ai/logo.png';\n this.showLogo = options.showLogo !== false;\n this.launcherColor = options.launcherColor || '#0066FF';\n this.sendButtonColor = options.sendButtonColor || '#0066FF';\n this.accentColor = options.accentColor || '#00C896';\n\n // File upload configuration\n this.enableFileUpload = options.enableFileUpload !== false;\n this.allowedFileTypes = options.allowedFileTypes || [\n 'image/jpeg', 'image/png', 'image/gif', 'image/webp',\n 'application/pdf',\n 'application/msword',\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n 'application/vnd.ms-excel',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'\n ];\n this.maxFileSize = options.maxFileSize || 10 * 1024 * 1024; // 10MB\n this.maxFiles = options.maxFiles || 5;\n this.uploadedFiles = [];\n\n // Notifications\n this.enableNotifications = options.enableNotifications !== false;\n this.enableSound = options.enableSound === true;\n this.unreadCount = 0;\n\n // Quick replies\n this.showQuickReplies = options.showQuickReplies !== false;\n this.quickReplies = options.quickReplies || [\n this.i18n.t('quickReply1'),\n this.i18n.t('quickReply2'),\n this.i18n.t('quickReply3'),\n this.i18n.t('quickReply4')\n ];\n\n // Message history\n this.enableHistory = options.enableHistory !== false;\n this.maxHistoryItems = options.maxHistoryItems || 50;\n this.historyKey = `pindai-chat-history-${this.webhookUrl}`;\n this.stateKey = `pindai-chat-state-${this.webhookUrl}`;\n\n // Error handling & retry logic\n this.maxRetries = options.maxRetries || 3;\n this.retryDelay = options.retryDelay || 1000;\n this.requestTimeout = options.requestTimeout || 30000;\n\n // Rate limiting\n this.rateLimit = options.rateLimit || 5;\n this.rateLimitWindow = options.rateLimitWindow || 60000;\n this.messageTimes = [];\n\n // DOM references\n this.container = null;\n this.launcher = null;\n this.chatWindow = null;\n this.messageList = null;\n this.input = null;\n this.button = null;\n this.closeButton = null;\n\n // State\n this.sessionId = `web-session-${Date.now()}-${Math.random()}`;\n this.isLoading = false;\n this.isOpen = false;\n this.isOnline = navigator.onLine;\n\n // Initialize\n this.loadState();\n this.setupOfflineDetection();\n\n if (this.mode === 'fullscreen') {\n this.initChatWindow();\n } else {\n this.initLauncher();\n }\n }\n\n /**\n * Initialize launcher button\n */\n initLauncher() {\n this.launcher = document.createElement('div');\n this.launcher.className = 'n8n-chat-launcher';\n this.launcher.style.backgroundColor = this.launcherColor;\n this.launcher.setAttribute('role', 'button');\n this.launcher.setAttribute('aria-label', this.i18n.t('ariaOpenChat'));\n this.launcher.setAttribute('tabindex', '0');\n this.launcher.innerHTML = `\n <img src=\"${this.launcherIconUrl}\" alt=\"\">\n <span class=\"n8n-chat-unread-badge\" style=\"display: none;\">0</span>\n `;\n document.body.appendChild(this.launcher);\n\n this.launcher.addEventListener('click', () => this.toggleChatWindow());\n this.launcher.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n this.toggleChatWindow();\n }\n });\n }\n\n /**\n * Initialize chat window\n */\n initChatWindow() {\n this.container = document.createElement('div');\n this.container.className = `n8n-chat-widget ${this.mode === 'fullscreen' ? 'n8n-chat-widget--fullscreen' : ''}`;\n this.container.setAttribute('role', 'dialog');\n this.container.setAttribute('aria-modal', 'true');\n this.container.setAttribute('aria-label', this.title);\n\n this.container.innerHTML = `\n <div class=\"n8n-chat-header\">\n <div class=\"n8n-chat-header-content\">\n ${this.showLogo ? `<img src=\"${this.logoUrl}\" alt=\"Pindai Logo\" class=\"n8n-chat-logo\">` : ''}\n <span class=\"n8n-chat-title\">${this.title}</span>\n </div>\n <button class=\"n8n-chat-close-btn\" aria-label=\"${this.i18n.t('ariaCloseChat')}\">&times;</button>\n </div>\n <div class=\"n8n-chat-messages\" role=\"log\" aria-live=\"polite\" aria-atomic=\"false\"></div>\n <div class=\"n8n-chat-watermark\">\n <span>Powered by</span>\n <a href=\"https://pindai.ai\" target=\"_blank\" rel=\"noopener noreferrer\">Pindai.ai</a>\n </div>\n <div class=\"n8n-chat-input-area\">\n ${this.enableFileUpload ? `\n <label class=\"n8n-chat-file-upload-btn\" aria-label=\"${this.i18n.t('ariaUploadFile')}\">\n <input type=\"file\" multiple accept=\"${this.allowedFileTypes.join(',')}\" hidden>\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <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\"/>\n </svg>\n </label>\n ` : ''}\n <input type=\"text\" placeholder=\"${this.i18n.t('placeholder')}\" aria-label=\"${this.i18n.t('ariaMessageInput')}\" />\n <button class=\"n8n-chat-send-btn\" style=\"background-color: ${this.sendButtonColor}\" aria-label=\"${this.i18n.t('ariaSendMessage')}\">\n <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\">\n <line x1=\"22\" y1=\"2\" x2=\"11\" y2=\"13\"></line>\n <polygon points=\"22 2 15 22 11 13 2 9 22 2\"></polygon>\n </svg>\n </button>\n </div>\n ${this.enableFileUpload ? '<div class=\"n8n-chat-file-preview\" style=\"display: none;\"></div>' : ''}\n `;\n\n if (this.mode === 'widget') {\n document.body.appendChild(this.container);\n } else {\n document.body.innerHTML = '';\n document.body.appendChild(this.container);\n document.body.style.margin = '0';\n }\n\n // Get DOM references\n this.messageList = this.container.querySelector('.n8n-chat-messages');\n this.input = this.container.querySelector('input[type=\"text\"]');\n this.button = this.container.querySelector('.n8n-chat-send-btn');\n this.closeButton = this.container.querySelector('.n8n-chat-close-btn');\n\n // Event listeners\n this.button.addEventListener('click', (e) => {\n e.preventDefault();\n this.sendMessage();\n });\n\n this.input.addEventListener('keypress', (e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n this.sendMessage();\n }\n });\n\n if (this.mode === 'fullscreen') {\n this.closeButton.style.display = 'none';\n } else {\n this.closeButton.addEventListener('click', () => this.toggleChatWindow());\n }\n\n // File upload handler\n if (this.enableFileUpload) {\n const fileInput = this.container.querySelector('input[type=\"file\"]');\n fileInput.addEventListener('change', (e) => this.handleFileSelect(e));\n }\n\n // Setup keyboard navigation\n this.setupKeyboardNavigation();\n\n // Load history and show initial message\n this.loadHistory();\n if (this.messageList.children.length === 0) {\n this.addMessage(this.initialMessage, 'ai');\n }\n }\n\n /**\n * Toggle chat window open/close\n */\n toggleChatWindow() {\n if (!this.isOpen) {\n if (!this.container) {\n this.initChatWindow();\n }\n\n setTimeout(() => {\n this.container.classList.add('n8n-chat-widget--open');\n if (this.launcher) {\n this.launcher.classList.add('n8n-chat-launcher--hidden');\n }\n this.input.focus();\n this.clearUnreadCount();\n }, 10);\n } else {\n this.container.classList.remove('n8n-chat-widget--open');\n if (this.launcher) {\n this.launcher.classList.remove('n8n-chat-launcher--hidden');\n }\n }\n this.isOpen = !this.isOpen;\n this.saveState();\n }\n\n /**\n * Get default chat icon\n */\n getDefaultIcon() {\n const svgIcon = `\n <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\">\n <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>\n </svg>\n `;\n return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svgIcon)}`;\n }\n\n /**\n * Format timestamp for display\n */\n formatTimestamp(date) {\n const now = new Date();\n const diff = now - date;\n\n if (diff < 60000) {\n return this.i18n.t('justNow');\n }\n if (diff < 3600000) {\n const minutes = Math.floor(diff / 60000);\n return this.i18n.t('minutesAgo', { minutes });\n }\n\n return date.toLocaleTimeString(this.locale === 'id' ? 'id-ID' : 'en-US', {\n hour: '2-digit',\n minute: '2-digit'\n });\n }\n\n /**\n * Add message to chat\n */\n addMessage(text, sender, timestamp = new Date()) {\n const messageBubble = document.createElement('div');\n messageBubble.className = `n8n-chat-bubble n8n-chat-${sender}-message`;\n\n const textNode = document.createElement('div');\n textNode.className = 'n8n-chat-message-text';\n textNode.textContent = text;\n\n const timeNode = document.createElement('div');\n timeNode.className = 'n8n-chat-message-timestamp';\n timeNode.textContent = this.formatTimestamp(timestamp);\n timeNode.setAttribute('data-timestamp', timestamp.toISOString());\n\n messageBubble.appendChild(textNode);\n messageBubble.appendChild(timeNode);\n this.messageList.appendChild(messageBubble);\n\n this.messageList.scrollTop = this.messageList.scrollHeight;\n\n // Save to history\n this.saveToHistory(text, sender, timestamp);\n\n // Increment unread if chat closed and AI message\n if (!this.isOpen && sender === 'ai') {\n this.incrementUnread();\n }\n }\n\n /**\n * Show/hide typing indicator\n */\n showTypingIndicator(show) {\n let indicator = this.messageList.querySelector('.n8n-chat-typing-indicator');\n if (show) {\n if (!indicator) {\n indicator = document.createElement('div');\n indicator.className = 'n8n-chat-bubble n8n-chat-ai-message n8n-chat-typing-indicator';\n indicator.innerHTML = '<span></span><span></span><span></span>';\n indicator.setAttribute('aria-label', this.i18n.t('typingIndicator'));\n this.messageList.appendChild(indicator);\n this.messageList.scrollTop = this.messageList.scrollHeight;\n }\n } else {\n if (indicator) {\n indicator.remove();\n }\n }\n }\n\n /**\n * Handle file selection\n */\n handleFileSelect(event) {\n const files = Array.from(event.target.files);\n\n files.forEach(file => {\n // Check file type\n if (!this.allowedFileTypes.includes(file.type)) {\n this.addMessage(\n this.i18n.t('fileTypeNotSupported', { filename: file.name }),\n 'ai'\n );\n return;\n }\n\n // Check file size\n if (file.size > this.maxFileSize) {\n const maxSizeMB = this.maxFileSize / 1024 / 1024;\n this.addMessage(\n this.i18n.t('fileTooLarge', { filename: file.name, maxSize: maxSizeMB }),\n 'ai'\n );\n return;\n }\n\n // Check max files\n if (this.uploadedFiles.length >= this.maxFiles) {\n this.addMessage(\n this.i18n.t('maxFilesExceeded', { maxFiles: this.maxFiles }),\n 'ai'\n );\n return;\n }\n\n this.uploadedFiles.push(file);\n this.renderFilePreview(file);\n });\n\n event.target.value = ''; // Reset input\n }\n\n /**\n * Render file preview\n */\n renderFilePreview(file) {\n const preview = this.container.querySelector('.n8n-chat-file-preview');\n if (!preview) return;\n\n preview.style.display = 'flex';\n\n const fileItem = document.createElement('div');\n fileItem.className = 'n8n-chat-file-item';\n fileItem.innerHTML = `\n <span class=\"n8n-chat-file-name\">${file.name}</span>\n <button class=\"n8n-chat-file-remove\" data-file=\"${file.name}\" aria-label=\"${this.i18n.t('ariaRemoveFile')}\">&times;</button>\n `;\n\n const removeBtn = fileItem.querySelector('.n8n-chat-file-remove');\n removeBtn.addEventListener('click', () => {\n this.uploadedFiles = this.uploadedFiles.filter(f => f.name !== file.name);\n fileItem.remove();\n if (this.uploadedFiles.length === 0) {\n preview.style.display = 'none';\n }\n });\n\n preview.appendChild(fileItem);\n }\n\n /**\n * Send message with files\n */\n async sendMessage() {\n const messageText = this.input.value.trim();\n if ((!messageText && this.uploadedFiles.length === 0) || this.isLoading) return;\n\n // Check rate limit\n try {\n this.checkRateLimit();\n } catch (error) {\n this.addMessage(error.message, 'ai');\n return;\n }\n\n // Check online status\n if (!this.isOnline) {\n this.addMessage(this.i18n.t('connectionLost'), 'ai');\n return;\n }\n\n this.isLoading = true;\n this.button.disabled = true;\n this.input.disabled = true;\n\n if (messageText) {\n this.addMessage(messageText, 'user');\n }\n\n this.input.value = '';\n this.showTypingIndicator(true);\n\n try {\n const response = await this.sendMessageWithRetry(messageText, this.uploadedFiles);\n this.addMessage(response, 'ai');\n\n // Show quick replies after AI response\n if (this.showQuickReplies && this.quickReplies.length > 0) {\n this.renderQuickReplies();\n }\n } catch (error) {\n const errorMessage = this.getErrorMessage(error);\n this.addMessage(errorMessage, 'ai');\n } finally {\n this.isLoading = false;\n this.button.disabled = false;\n this.input.disabled = false;\n this.showTypingIndicator(false);\n this.input.focus();\n\n // Clear uploaded files\n if (this.uploadedFiles.length > 0) {\n this.uploadedFiles = [];\n const preview = this.container.querySelector('.n8n-chat-file-preview');\n if (preview) {\n preview.innerHTML = '';\n preview.style.display = 'none';\n }\n }\n }\n }\n\n /**\n * Send message with retry logic\n */\n async sendMessageWithRetry(messageText, files = [], retryCount = 0) {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.requestTimeout);\n\n const formData = new FormData();\n formData.append('sessionId', this.sessionId);\n formData.append('message', messageText);\n\n // Append files\n files.forEach((file, index) => {\n formData.append(`file${index}`, file);\n });\n\n const response = await fetch(this.webhookUrl, {\n method: 'POST',\n body: formData,\n signal: controller.signal\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n // Retry on 5xx errors\n if (response.status >= 500 && retryCount < this.maxRetries) {\n await this.delay(this.retryDelay * (retryCount + 1));\n return this.sendMessageWithRetry(messageText, files, retryCount + 1);\n }\n\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.message || `Network error: ${response.statusText}`);\n }\n\n const data = await response.json();\n if (!data.response) {\n throw new Error(this.i18n.t('errorInvalidResponse'));\n }\n\n return data.response;\n\n } catch (error) {\n if (error.name === 'AbortError') {\n throw new Error(this.i18n.t('errorTimeout'));\n }\n\n // Retry on network errors\n if (error.message.includes('NetworkError') && retryCount < this.maxRetries) {\n await this.delay(this.retryDelay * (retryCount + 1));\n return this.sendMessageWithRetry(messageText, files, retryCount + 1);\n }\n\n throw error;\n }\n }\n\n /**\n * Delay helper\n */\n delay(ms) {\n return new Promise(resolve => setTimeout(resolve, ms));\n }\n\n /**\n * Get user-friendly error message\n */\n getErrorMessage(error) {\n if (error.message.includes('timeout') || error.message.includes('Timeout')) {\n return this.i18n.t('errorTimeout');\n }\n if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) {\n return this.i18n.t('errorNetwork');\n }\n if (error.message.includes('500') || error.message.includes('503')) {\n return this.i18n.t('errorServer');\n }\n return this.i18n.t('errorGeneric');\n }\n\n /**\n * Check rate limit\n */\n checkRateLimit() {\n const now = Date.now();\n this.messageTimes = this.messageTimes.filter(\n time => now - time < this.rateLimitWindow\n );\n\n if (this.messageTimes.length >= this.rateLimit) {\n const oldestTime = this.messageTimes[0];\n const waitTime = Math.ceil((this.rateLimitWindow - (now - oldestTime)) / 1000);\n throw new Error(this.i18n.t('errorRateLimit', { seconds: waitTime }));\n }\n\n this.messageTimes.push(now);\n }\n\n /**\n * Render quick reply buttons\n */\n renderQuickReplies(replies = this.quickReplies) {\n if (!this.showQuickReplies || replies.length === 0) return;\n\n // Remove existing quick replies\n const existingReplies = this.messageList.querySelector('.n8n-chat-quick-replies');\n if (existingReplies) existingReplies.remove();\n\n const repliesContainer = document.createElement('div');\n repliesContainer.className = 'n8n-chat-quick-replies';\n\n replies.forEach(reply => {\n const button = document.createElement('button');\n button.className = 'n8n-chat-quick-reply-btn';\n button.textContent = reply;\n button.addEventListener('click', () => {\n this.input.value = reply;\n this.sendMessage();\n repliesContainer.remove();\n });\n repliesContainer.appendChild(button);\n });\n\n this.messageList.appendChild(repliesContainer);\n this.messageList.scrollTop = this.messageList.scrollHeight;\n }\n\n /**\n * Notification badge management\n */\n incrementUnread() {\n if (!this.isOpen) {\n this.unreadCount++;\n this.updateUnreadBadge();\n }\n }\n\n updateUnreadBadge() {\n if (!this.launcher) return;\n const badge = this.launcher.querySelector('.n8n-chat-unread-badge');\n if (badge) {\n badge.textContent = this.unreadCount;\n badge.style.display = this.unreadCount > 0 ? 'flex' : 'none';\n }\n }\n\n clearUnreadCount() {\n this.unreadCount = 0;\n this.updateUnreadBadge();\n }\n\n /**\n * Message history persistence\n */\n loadHistory() {\n if (!this.enableHistory) return;\n\n try {\n const stored = localStorage.getItem(this.historyKey);\n if (!stored) return;\n\n const history = JSON.parse(stored);\n history.forEach(item => {\n this.addMessageWithoutSaving(item.text, item.sender, new Date(item.timestamp));\n });\n } catch (error) {\n console.warn('Failed to load chat history:', error);\n }\n }\n\n addMessageWithoutSaving(text, sender, timestamp) {\n const messageBubble = document.createElement('div');\n messageBubble.className = `n8n-chat-bubble n8n-chat-${sender}-message`;\n\n const textNode = document.createElement('div');\n textNode.className = 'n8n-chat-message-text';\n textNode.textContent = text;\n\n const timeNode = document.createElement('div');\n timeNode.className = 'n8n-chat-message-timestamp';\n timeNode.textContent = this.formatTimestamp(timestamp);\n timeNode.setAttribute('data-timestamp', timestamp.toISOString());\n\n messageBubble.appendChild(textNode);\n messageBubble.appendChild(timeNode);\n this.messageList.appendChild(messageBubble);\n\n this.messageList.scrollTop = this.messageList.scrollHeight;\n }\n\n saveToHistory(text, sender, timestamp = new Date()) {\n if (!this.enableHistory) return;\n\n try {\n const stored = localStorage.getItem(this.historyKey);\n let history = stored ? JSON.parse(stored) : [];\n\n history.push({\n text,\n sender,\n timestamp: timestamp.toISOString()\n });\n\n // Keep only last N messages\n if (history.length > this.maxHistoryItems) {\n history = history.slice(-this.maxHistoryItems);\n }\n\n localStorage.setItem(this.historyKey, JSON.stringify(history));\n } catch (error) {\n console.warn('Failed to save chat history:', error);\n }\n }\n\n /**\n * State persistence\n */\n loadState() {\n try {\n const stored = localStorage.getItem(this.stateKey);\n if (stored) {\n // State loaded but not used for auto-open\n // User needs to explicitly open the chat\n }\n } catch (error) {\n console.warn('Failed to load chat state:', error);\n }\n }\n\n saveState() {\n try {\n localStorage.setItem(this.stateKey, JSON.stringify({\n isOpen: this.isOpen,\n timestamp: new Date().toISOString()\n }));\n } catch (error) {\n console.warn('Failed to save chat state:', error);\n }\n }\n\n /**\n * Offline detection\n */\n setupOfflineDetection() {\n window.addEventListener('online', () => {\n this.isOnline = true;\n this.updateOnlineStatus();\n });\n\n window.addEventListener('offline', () => {\n this.isOnline = false;\n this.updateOnlineStatus();\n });\n }\n\n updateOnlineStatus() {\n if (!this.container) return;\n\n const statusBar = this.container.querySelector('.n8n-chat-offline-indicator');\n if (!this.isOnline && !statusBar) {\n const indicator = document.createElement('div');\n indicator.className = 'n8n-chat-offline-indicator';\n indicator.innerHTML = `\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <line x1=\"1\" y1=\"1\" x2=\"23\" y2=\"23\"></line>\n <path d=\"M16.72 11.06A10.94 10.94 0 0 1 19 12.55\"></path>\n <path d=\"M5 12.55a10.94 10.94 0 0 1 5.17-2.39\"></path>\n <path d=\"M10.71 5.05A16 16 0 0 1 22.58 9\"></path>\n <path d=\"M1.42 9a15.91 15.91 0 0 1 4.7-2.88\"></path>\n <path d=\"M8.53 16.11a6 6 0 0 1 6.95 0\"></path>\n <line x1=\"12\" y1=\"20\" x2=\"12.01\" y2=\"20\"></line>\n </svg>\n <span>${this.i18n.t('offline')}</span>\n `;\n this.container.insertBefore(indicator, this.messageList);\n } else if (this.isOnline && statusBar) {\n statusBar.remove();\n }\n\n // Update button state\n if (this.button) {\n this.button.disabled = !this.isOnline || this.isLoading;\n }\n }\n\n /**\n * Keyboard navigation setup\n */\n setupKeyboardNavigation() {\n // ESC to close widget\n document.addEventListener('keydown', (e) => {\n if (e.key === 'Escape' && this.isOpen && this.mode === 'widget') {\n this.toggleChatWindow();\n }\n });\n\n // Tab trap within modal\n this.container.addEventListener('keydown', (e) => {\n if (e.key === 'Tab') {\n const focusableElements = this.container.querySelectorAll(\n 'button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex=\"-1\"])'\n );\n const firstElement = focusableElements[0];\n const lastElement = focusableElements[focusableElements.length - 1];\n\n if (e.shiftKey && document.activeElement === firstElement) {\n e.preventDefault();\n lastElement.focus();\n } else if (!e.shiftKey && document.activeElement === lastElement) {\n e.preventDefault();\n firstElement.focus();\n }\n }\n });\n }\n}\n\n// Export both class names for backward compatibility\nwindow.PindaiChatWidget = {\n init: (options) => {\n if (!document.querySelector('.n8n-chat-widget') && !document.querySelector('.n8n-chat-launcher')) {\n return new PindaiChatWidget(options);\n }\n }\n};\n\n// Backward compatibility with old name\nwindow.N8nChatWidget = window.PindaiChatWidget;\n"],"names":["translations","I18n","locale","key","params","text","_a","param","regex","PindaiChatWidget","options","apiEndpoint","e","date","diff","minutes","sender","timestamp","messageBubble","textNode","timeNode","show","indicator","event","file","maxSizeMB","preview","fileItem","f","__async","messageText","error","response","errorMessage","_0","files","retryCount","controller","timeoutId","formData","index","errorData","data","ms","resolve","now","time","oldestTime","waitTime","replies","existingReplies","repliesContainer","reply","button","badge","stored","item","history","statusBar","focusableElements","firstElement","lastElement"],"mappings":"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"}
1
+ {"version":3,"file":"pindai-chat-widget.js","sources":["../src/i18n.js","../src/main.js"],"sourcesContent":["/**\n * Internationalization (i18n) System for Pindai Chat Widget\n * Supports Indonesian (id) and English (en) locales\n */\n\nexport const translations = {\n en: {\n // Widget UI\n title: 'Pindai Agent',\n placeholder: 'Write a message...',\n initialMessage: 'Hello! How can I help you today?',\n send: 'Send',\n close: 'Close',\n upload: 'Upload file',\n removeFile: 'Remove file',\n\n // Loading and status\n typingIndicator: 'AI is typing...',\n sending: 'Sending...',\n justNow: 'Just now',\n minutesAgo: '{minutes}m ago',\n\n // Offline/Online status\n offline: 'Offline - messages will be sent when online',\n connectionRestored: 'Connection restored',\n connectionLost: 'No internet connection',\n\n // Error messages\n errorGeneric: 'An error occurred. Please try again.',\n errorTimeout: 'Request timeout. Please try again.',\n errorNetwork: 'No internet connection. Check your network.',\n errorServer: 'Server is busy. Please try again later.',\n errorRateLimit: 'Too many messages. Please wait {seconds} seconds.',\n errorInvalidResponse: 'Invalid server response. Please contact support.',\n\n // File upload errors\n fileTypeNotSupported: 'File type not supported: {filename}',\n fileTooLarge: 'File too large: {filename} (max {maxSize}MB)',\n maxFilesExceeded: 'Maximum {maxFiles} files allowed',\n\n // Quick replies (default suggestions)\n quickReply1: 'How can I extract data from documents?',\n quickReply2: 'What file types are supported?',\n quickReply3: 'Tell me about pricing',\n quickReply4: 'Contact support',\n\n // Accessibility labels\n ariaOpenChat: 'Open chat widget',\n ariaCloseChat: 'Close chat window',\n ariaSendMessage: 'Send message',\n ariaMessageInput: 'Type your message',\n ariaUploadFile: 'Upload file',\n ariaRemoveFile: 'Remove file',\n ariaChatWindow: 'Chat window',\n ariaMessageLog: 'Chat messages',\n },\n\n id: {\n // Widget UI\n title: 'Pindai Agent',\n placeholder: 'Tulis pesan...',\n initialMessage: 'Halo! Bagaimana saya bisa membantu Anda hari ini?',\n send: 'Kirim',\n close: 'Tutup',\n upload: 'Unggah file',\n removeFile: 'Hapus file',\n\n // Loading and status\n typingIndicator: 'AI sedang mengetik...',\n sending: 'Mengirim...',\n justNow: 'Baru saja',\n minutesAgo: '{minutes}m yang lalu',\n\n // Offline/Online status\n offline: 'Offline - pesan akan dikirim saat online',\n connectionRestored: 'Koneksi kembali',\n connectionLost: 'Tidak ada koneksi internet',\n\n // Error messages\n errorGeneric: 'Terjadi kesalahan. Silakan coba lagi.',\n errorTimeout: 'Waktu permintaan habis. Silakan coba lagi.',\n errorNetwork: 'Tidak ada koneksi internet. Periksa jaringan Anda.',\n errorServer: 'Server sedang sibuk. Silakan coba lagi dalam beberapa saat.',\n errorRateLimit: 'Terlalu banyak pesan. Silakan tunggu {seconds} detik.',\n errorInvalidResponse: 'Respons server tidak valid. Silakan hubungi dukungan.',\n\n // File upload errors\n fileTypeNotSupported: 'Jenis file tidak didukung: {filename}',\n fileTooLarge: 'File terlalu besar: {filename} (maks {maxSize}MB)',\n maxFilesExceeded: 'Maksimal {maxFiles} file diperbolehkan',\n\n // Quick replies (default suggestions)\n quickReply1: 'Bagaimana cara ekstraksi dokumen?',\n quickReply2: 'Jenis file apa yang didukung?',\n quickReply3: 'Tentang harga',\n quickReply4: 'Hubungi dukungan',\n\n // Accessibility labels\n ariaOpenChat: 'Buka widget chat',\n ariaCloseChat: 'Tutup jendela chat',\n ariaSendMessage: 'Kirim pesan',\n ariaMessageInput: 'Ketik pesan Anda',\n ariaUploadFile: 'Unggah file',\n ariaRemoveFile: 'Hapus file',\n ariaChatWindow: 'Jendela chat',\n ariaMessageLog: 'Pesan chat',\n }\n};\n\n/**\n * I18n class for managing translations\n */\nexport class I18n {\n constructor(locale = 'id') {\n this.locale = this.isValidLocale(locale) ? locale : 'id';\n }\n\n /**\n * Check if locale is valid\n */\n isValidLocale(locale) {\n return Object.keys(translations).includes(locale);\n }\n\n /**\n * Translate a key with optional parameter substitution\n * @param {string} key - Translation key\n * @param {object} params - Parameters to substitute in the translation\n * @returns {string} Translated string\n */\n t(key, params = {}) {\n let text = translations[this.locale]?.[key] || translations.en[key] || key;\n\n // Replace parameters like {param} with actual values\n Object.keys(params).forEach(param => {\n const regex = new RegExp(`\\\\{${param}\\\\}`, 'g');\n text = text.replace(regex, params[param]);\n });\n\n return text;\n }\n\n /**\n * Change the current locale\n * @param {string} locale - New locale (id or en)\n */\n setLocale(locale) {\n if (this.isValidLocale(locale)) {\n this.locale = locale;\n return true;\n }\n console.warn(`Invalid locale: ${locale}. Keeping current locale: ${this.locale}`);\n return false;\n }\n\n /**\n * Get current locale\n * @returns {string} Current locale\n */\n getLocale() {\n return this.locale;\n }\n\n /**\n * Get all available locales\n * @returns {string[]} Array of available locale codes\n */\n getAvailableLocales() {\n return Object.keys(translations);\n }\n}\n\n// Export default instance for convenience\nexport default I18n;\n","import './style.css';\nimport { I18n } from './i18n.js';\n\n/**\n * Pindai Chat Widget - Modern, Accessible, Indonesian-focused\n * Version 2.0.0\n */\nclass PindaiChatWidget {\n constructor(options) {\n // Backward compatibility: support both webhookUrl and n8nUrl\n const apiEndpoint = options.webhookUrl || options.n8nUrl;\n\n if (!apiEndpoint) {\n throw new Error('PindaiChatWidget: \"webhookUrl\" option is required.');\n }\n\n // Core configuration\n this.webhookUrl = apiEndpoint;\n this.mode = options.mode || 'widget';\n\n // Internationalization\n this.locale = options.locale || 'id'; // Default to Indonesian\n this.i18n = new I18n(this.locale);\n\n // UI customization\n this.title = options.title || this.i18n.t('title');\n this.initialMessage = options.initialMessage || this.i18n.t('initialMessage');\n this.launcherIconUrl = options.launcherIconUrl || this.getDefaultIcon();\n\n // Branding\n this.logoUrl = options.logoUrl || 'https://pindai.ai/logo.png';\n this.showLogo = options.showLogo !== false;\n this.launcherColor = options.launcherColor || '#0066FF';\n this.sendButtonColor = options.sendButtonColor || '#0066FF';\n this.accentColor = options.accentColor || '#00C896';\n\n // File upload configuration\n this.enableFileUpload = options.enableFileUpload !== false;\n this.allowedFileTypes = options.allowedFileTypes || [\n 'image/jpeg', 'image/png', 'image/gif', 'image/webp',\n 'application/pdf',\n 'application/msword',\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n 'application/vnd.ms-excel',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'\n ];\n this.maxFileSize = options.maxFileSize || 10 * 1024 * 1024; // 10MB\n this.maxFiles = options.maxFiles || 5;\n this.uploadedFiles = [];\n\n // Notifications\n this.enableNotifications = options.enableNotifications !== false;\n this.enableSound = options.enableSound === true;\n this.unreadCount = 0;\n\n // Quick replies\n this.showQuickReplies = options.showQuickReplies !== false;\n this.quickReplies = options.quickReplies || [\n this.i18n.t('quickReply1'),\n this.i18n.t('quickReply2'),\n this.i18n.t('quickReply3'),\n this.i18n.t('quickReply4')\n ];\n\n // Message history\n this.enableHistory = options.enableHistory !== false;\n this.maxHistoryItems = options.maxHistoryItems || 50;\n this.historyKey = `pindai-chat-history-${this.webhookUrl}`;\n this.stateKey = `pindai-chat-state-${this.webhookUrl}`;\n\n // Error handling & retry logic\n this.maxRetries = options.maxRetries || 3;\n this.retryDelay = options.retryDelay || 1000;\n this.requestTimeout = options.requestTimeout || 30000;\n\n // Rate limiting\n this.rateLimit = options.rateLimit || 5;\n this.rateLimitWindow = options.rateLimitWindow || 60000;\n this.messageTimes = [];\n\n // DOM references\n this.container = null;\n this.launcher = null;\n this.chatWindow = null;\n this.messageList = null;\n this.input = null;\n this.button = null;\n this.closeButton = null;\n\n // Pindai Agent auth + polling (backward-compatible: all fields optional)\n this.embedToken = options.embedToken || null;\n this.pollingUrl = options.pollingUrl || null;\n this.pollingEnabled = !!(options.pollingUrl);\n this.pollingInterval = options.pollingInterval || 3000;\n this._pollingTimer = null;\n this._lastMessageTimestamp = null;\n\n // State\n this.sessionId = `web-session-${Date.now()}-${Math.random()}`;\n this.isLoading = false;\n this.isOpen = false;\n this.isOnline = navigator.onLine;\n\n // Initialize\n this.loadState();\n this.setupOfflineDetection();\n\n if (this.mode === 'fullscreen') {\n this.initChatWindow();\n } else {\n this.initLauncher();\n }\n }\n\n /**\n * Initialize launcher button\n */\n initLauncher() {\n this.launcher = document.createElement('div');\n this.launcher.className = 'n8n-chat-launcher';\n this.launcher.style.backgroundColor = this.launcherColor;\n this.launcher.setAttribute('role', 'button');\n this.launcher.setAttribute('aria-label', this.i18n.t('ariaOpenChat'));\n this.launcher.setAttribute('tabindex', '0');\n this.launcher.innerHTML = `\n <img src=\"${this.launcherIconUrl}\" alt=\"\">\n <span class=\"n8n-chat-unread-badge\" style=\"display: none;\">0</span>\n `;\n document.body.appendChild(this.launcher);\n\n this.launcher.addEventListener('click', () => this.toggleChatWindow());\n this.launcher.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n this.toggleChatWindow();\n }\n });\n }\n\n /**\n * Initialize chat window\n */\n initChatWindow() {\n this.container = document.createElement('div');\n this.container.className = `n8n-chat-widget ${this.mode === 'fullscreen' ? 'n8n-chat-widget--fullscreen' : ''}`;\n this.container.setAttribute('role', 'dialog');\n this.container.setAttribute('aria-modal', 'true');\n this.container.setAttribute('aria-label', this.title);\n\n this.container.innerHTML = `\n <div class=\"n8n-chat-header\">\n <div class=\"n8n-chat-header-content\">\n ${this.showLogo ? `<img src=\"${this.logoUrl}\" alt=\"Pindai Logo\" class=\"n8n-chat-logo\">` : ''}\n <span class=\"n8n-chat-title\">${this.title}</span>\n </div>\n <button class=\"n8n-chat-close-btn\" aria-label=\"${this.i18n.t('ariaCloseChat')}\">&times;</button>\n </div>\n <div class=\"n8n-chat-messages\" role=\"log\" aria-live=\"polite\" aria-atomic=\"false\"></div>\n <div class=\"n8n-chat-watermark\">\n <span>Powered by</span>\n <a href=\"https://pindai.ai\" target=\"_blank\" rel=\"noopener noreferrer\">Pindai.ai</a>\n </div>\n <div class=\"n8n-chat-input-area\">\n ${this.enableFileUpload ? `\n <label class=\"n8n-chat-file-upload-btn\" aria-label=\"${this.i18n.t('ariaUploadFile')}\">\n <input type=\"file\" multiple accept=\"${this.allowedFileTypes.join(',')}\" hidden>\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <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\"/>\n </svg>\n </label>\n ` : ''}\n <input type=\"text\" placeholder=\"${this.i18n.t('placeholder')}\" aria-label=\"${this.i18n.t('ariaMessageInput')}\" />\n <button class=\"n8n-chat-send-btn\" style=\"background-color: ${this.sendButtonColor}\" aria-label=\"${this.i18n.t('ariaSendMessage')}\">\n <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\">\n <line x1=\"22\" y1=\"2\" x2=\"11\" y2=\"13\"></line>\n <polygon points=\"22 2 15 22 11 13 2 9 22 2\"></polygon>\n </svg>\n </button>\n </div>\n ${this.enableFileUpload ? '<div class=\"n8n-chat-file-preview\" style=\"display: none;\"></div>' : ''}\n `;\n\n if (this.mode === 'widget') {\n document.body.appendChild(this.container);\n } else {\n document.body.innerHTML = '';\n document.body.appendChild(this.container);\n document.body.style.margin = '0';\n }\n\n // Get DOM references\n this.messageList = this.container.querySelector('.n8n-chat-messages');\n this.input = this.container.querySelector('input[type=\"text\"]');\n this.button = this.container.querySelector('.n8n-chat-send-btn');\n this.closeButton = this.container.querySelector('.n8n-chat-close-btn');\n\n // Event listeners\n this.button.addEventListener('click', (e) => {\n e.preventDefault();\n this.sendMessage();\n });\n\n this.input.addEventListener('keypress', (e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n this.sendMessage();\n }\n });\n\n if (this.mode === 'fullscreen') {\n this.closeButton.style.display = 'none';\n } else {\n this.closeButton.addEventListener('click', () => this.toggleChatWindow());\n }\n\n // File upload handler\n if (this.enableFileUpload) {\n const fileInput = this.container.querySelector('input[type=\"file\"]');\n fileInput.addEventListener('change', (e) => this.handleFileSelect(e));\n }\n\n // Setup keyboard navigation\n this.setupKeyboardNavigation();\n\n // Load history and show initial message\n this.loadHistory();\n if (this.messageList.children.length === 0) {\n this.addMessage(this.initialMessage, 'ai');\n }\n }\n\n /**\n * Toggle chat window open/close\n */\n toggleChatWindow() {\n if (!this.isOpen) {\n if (!this.container) {\n this.initChatWindow();\n }\n\n setTimeout(() => {\n this.container.classList.add('n8n-chat-widget--open');\n if (this.launcher) {\n this.launcher.classList.add('n8n-chat-launcher--hidden');\n }\n this.input.focus();\n this.clearUnreadCount();\n }, 10);\n } else {\n this.container.classList.remove('n8n-chat-widget--open');\n if (this.launcher) {\n this.launcher.classList.remove('n8n-chat-launcher--hidden');\n }\n }\n this.isOpen = !this.isOpen;\n this.saveState();\n }\n\n /**\n * Get default chat icon\n */\n getDefaultIcon() {\n const svgIcon = `\n <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\">\n <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>\n </svg>\n `;\n return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svgIcon)}`;\n }\n\n /**\n * Format timestamp for display\n */\n formatTimestamp(date) {\n const now = new Date();\n const diff = now - date;\n\n if (diff < 60000) {\n return this.i18n.t('justNow');\n }\n if (diff < 3600000) {\n const minutes = Math.floor(diff / 60000);\n return this.i18n.t('minutesAgo', { minutes });\n }\n\n return date.toLocaleTimeString(this.locale === 'id' ? 'id-ID' : 'en-US', {\n hour: '2-digit',\n minute: '2-digit'\n });\n }\n\n /**\n * Add message to chat\n */\n addMessage(text, sender, timestamp = new Date()) {\n const messageBubble = document.createElement('div');\n messageBubble.className = `n8n-chat-bubble n8n-chat-${sender}-message`;\n\n const textNode = document.createElement('div');\n textNode.className = 'n8n-chat-message-text';\n textNode.textContent = text;\n\n const timeNode = document.createElement('div');\n timeNode.className = 'n8n-chat-message-timestamp';\n timeNode.textContent = this.formatTimestamp(timestamp);\n timeNode.setAttribute('data-timestamp', timestamp.toISOString());\n\n messageBubble.appendChild(textNode);\n messageBubble.appendChild(timeNode);\n this.messageList.appendChild(messageBubble);\n\n this.messageList.scrollTop = this.messageList.scrollHeight;\n\n // Save to history\n this.saveToHistory(text, sender, timestamp);\n\n // Increment unread if chat closed and AI message\n if (!this.isOpen && sender === 'ai') {\n this.incrementUnread();\n }\n }\n\n /**\n * Show/hide typing indicator\n */\n showTypingIndicator(show) {\n let indicator = this.messageList.querySelector('.n8n-chat-typing-indicator');\n if (show) {\n if (!indicator) {\n indicator = document.createElement('div');\n indicator.className = 'n8n-chat-bubble n8n-chat-ai-message n8n-chat-typing-indicator';\n indicator.innerHTML = '<span></span><span></span><span></span>';\n indicator.setAttribute('aria-label', this.i18n.t('typingIndicator'));\n this.messageList.appendChild(indicator);\n this.messageList.scrollTop = this.messageList.scrollHeight;\n }\n } else {\n if (indicator) {\n indicator.remove();\n }\n }\n }\n\n /**\n * Handle file selection\n */\n handleFileSelect(event) {\n const files = Array.from(event.target.files);\n\n files.forEach(file => {\n // Check file type\n if (!this.allowedFileTypes.includes(file.type)) {\n this.addMessage(\n this.i18n.t('fileTypeNotSupported', { filename: file.name }),\n 'ai'\n );\n return;\n }\n\n // Check file size\n if (file.size > this.maxFileSize) {\n const maxSizeMB = this.maxFileSize / 1024 / 1024;\n this.addMessage(\n this.i18n.t('fileTooLarge', { filename: file.name, maxSize: maxSizeMB }),\n 'ai'\n );\n return;\n }\n\n // Check max files\n if (this.uploadedFiles.length >= this.maxFiles) {\n this.addMessage(\n this.i18n.t('maxFilesExceeded', { maxFiles: this.maxFiles }),\n 'ai'\n );\n return;\n }\n\n this.uploadedFiles.push(file);\n this.renderFilePreview(file);\n });\n\n event.target.value = ''; // Reset input\n }\n\n /**\n * Render file preview\n */\n renderFilePreview(file) {\n const preview = this.container.querySelector('.n8n-chat-file-preview');\n if (!preview) return;\n\n preview.style.display = 'flex';\n\n const fileItem = document.createElement('div');\n fileItem.className = 'n8n-chat-file-item';\n fileItem.innerHTML = `\n <span class=\"n8n-chat-file-name\">${file.name}</span>\n <button class=\"n8n-chat-file-remove\" data-file=\"${file.name}\" aria-label=\"${this.i18n.t('ariaRemoveFile')}\">&times;</button>\n `;\n\n const removeBtn = fileItem.querySelector('.n8n-chat-file-remove');\n removeBtn.addEventListener('click', () => {\n this.uploadedFiles = this.uploadedFiles.filter(f => f.name !== file.name);\n fileItem.remove();\n if (this.uploadedFiles.length === 0) {\n preview.style.display = 'none';\n }\n });\n\n preview.appendChild(fileItem);\n }\n\n /**\n * Send message with files\n */\n async sendMessage() {\n const messageText = this.input.value.trim();\n if ((!messageText && this.uploadedFiles.length === 0) || this.isLoading) return;\n\n // Check rate limit\n try {\n this.checkRateLimit();\n } catch (error) {\n this.addMessage(error.message, 'ai');\n return;\n }\n\n // Check online status\n if (!this.isOnline) {\n this.addMessage(this.i18n.t('connectionLost'), 'ai');\n return;\n }\n\n this.isLoading = true;\n this.button.disabled = true;\n this.input.disabled = true;\n\n if (messageText) {\n this.addMessage(messageText, 'user');\n }\n\n this.input.value = '';\n this.showTypingIndicator(true);\n\n try {\n const response = await this.sendMessageWithRetry(messageText, this.uploadedFiles);\n this.addMessage(response, 'ai');\n\n // Show quick replies after AI response\n if (this.showQuickReplies && this.quickReplies.length > 0) {\n this.renderQuickReplies();\n }\n } catch (error) {\n const errorMessage = this.getErrorMessage(error);\n this.addMessage(errorMessage, 'ai');\n } finally {\n this.isLoading = false;\n this.button.disabled = false;\n this.input.disabled = false;\n this.showTypingIndicator(false);\n this.input.focus();\n\n // Clear uploaded files\n if (this.uploadedFiles.length > 0) {\n this.uploadedFiles = [];\n const preview = this.container.querySelector('.n8n-chat-file-preview');\n if (preview) {\n preview.innerHTML = '';\n preview.style.display = 'none';\n }\n }\n }\n }\n\n /**\n * Send message with retry logic\n */\n async sendMessageWithRetry(messageText, files = [], retryCount = 0) {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.requestTimeout);\n\n const formData = new FormData();\n formData.append('sessionId', this.sessionId);\n formData.append('message', messageText);\n\n // Pindai Agent: include embed token if configured\n if (this.embedToken) {\n formData.append('embedToken', this.embedToken);\n }\n\n // Append files\n files.forEach((file, index) => {\n formData.append(`file${index}`, file);\n });\n\n const response = await fetch(this.webhookUrl, {\n method: 'POST',\n body: formData,\n signal: controller.signal\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n // Retry on 5xx errors\n if (response.status >= 500 && retryCount < this.maxRetries) {\n await this.delay(this.retryDelay * (retryCount + 1));\n return this.sendMessageWithRetry(messageText, files, retryCount + 1);\n }\n\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.message || `Network error: ${response.statusText}`);\n }\n\n const data = await response.json();\n if (!data.response) {\n throw new Error(this.i18n.t('errorInvalidResponse'));\n }\n\n // Pindai Agent: start polling if handoff was triggered\n if (data.status && data.status !== 'active' && this.pollingEnabled) {\n this.startPolling();\n } else {\n this.stopPolling();\n }\n\n return data.response;\n\n } catch (error) {\n if (error.name === 'AbortError') {\n throw new Error(this.i18n.t('errorTimeout'));\n }\n\n // Retry on network errors\n if (error.message.includes('NetworkError') && retryCount < this.maxRetries) {\n await this.delay(this.retryDelay * (retryCount + 1));\n return this.sendMessageWithRetry(messageText, files, retryCount + 1);\n }\n\n throw error;\n }\n }\n\n /**\n * Delay helper\n */\n delay(ms) {\n return new Promise(resolve => setTimeout(resolve, ms));\n }\n\n /**\n * Get user-friendly error message\n */\n getErrorMessage(error) {\n if (error.message.includes('timeout') || error.message.includes('Timeout')) {\n return this.i18n.t('errorTimeout');\n }\n if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) {\n return this.i18n.t('errorNetwork');\n }\n if (error.message.includes('500') || error.message.includes('503')) {\n return this.i18n.t('errorServer');\n }\n return this.i18n.t('errorGeneric');\n }\n\n /**\n * Check rate limit\n */\n checkRateLimit() {\n const now = Date.now();\n this.messageTimes = this.messageTimes.filter(\n time => now - time < this.rateLimitWindow\n );\n\n if (this.messageTimes.length >= this.rateLimit) {\n const oldestTime = this.messageTimes[0];\n const waitTime = Math.ceil((this.rateLimitWindow - (now - oldestTime)) / 1000);\n throw new Error(this.i18n.t('errorRateLimit', { seconds: waitTime }));\n }\n\n this.messageTimes.push(now);\n }\n\n /**\n * Render quick reply buttons\n */\n renderQuickReplies(replies = this.quickReplies) {\n if (!this.showQuickReplies || replies.length === 0) return;\n\n // Remove existing quick replies\n const existingReplies = this.messageList.querySelector('.n8n-chat-quick-replies');\n if (existingReplies) existingReplies.remove();\n\n const repliesContainer = document.createElement('div');\n repliesContainer.className = 'n8n-chat-quick-replies';\n\n replies.forEach(reply => {\n const button = document.createElement('button');\n button.className = 'n8n-chat-quick-reply-btn';\n button.textContent = reply;\n button.addEventListener('click', () => {\n this.input.value = reply;\n this.sendMessage();\n repliesContainer.remove();\n });\n repliesContainer.appendChild(button);\n });\n\n this.messageList.appendChild(repliesContainer);\n this.messageList.scrollTop = this.messageList.scrollHeight;\n }\n\n /**\n * Notification badge management\n */\n incrementUnread() {\n if (!this.isOpen) {\n this.unreadCount++;\n this.updateUnreadBadge();\n }\n }\n\n updateUnreadBadge() {\n if (!this.launcher) return;\n const badge = this.launcher.querySelector('.n8n-chat-unread-badge');\n if (badge) {\n badge.textContent = this.unreadCount;\n badge.style.display = this.unreadCount > 0 ? 'flex' : 'none';\n }\n }\n\n clearUnreadCount() {\n this.unreadCount = 0;\n this.updateUnreadBadge();\n }\n\n /**\n * Message history persistence\n */\n loadHistory() {\n if (!this.enableHistory) return;\n\n try {\n const stored = localStorage.getItem(this.historyKey);\n if (!stored) return;\n\n const history = JSON.parse(stored);\n history.forEach(item => {\n this.addMessageWithoutSaving(item.text, item.sender, new Date(item.timestamp));\n });\n } catch (error) {\n console.warn('Failed to load chat history:', error);\n }\n }\n\n addMessageWithoutSaving(text, sender, timestamp) {\n const messageBubble = document.createElement('div');\n messageBubble.className = `n8n-chat-bubble n8n-chat-${sender}-message`;\n\n const textNode = document.createElement('div');\n textNode.className = 'n8n-chat-message-text';\n textNode.textContent = text;\n\n const timeNode = document.createElement('div');\n timeNode.className = 'n8n-chat-message-timestamp';\n timeNode.textContent = this.formatTimestamp(timestamp);\n timeNode.setAttribute('data-timestamp', timestamp.toISOString());\n\n messageBubble.appendChild(textNode);\n messageBubble.appendChild(timeNode);\n this.messageList.appendChild(messageBubble);\n\n this.messageList.scrollTop = this.messageList.scrollHeight;\n }\n\n saveToHistory(text, sender, timestamp = new Date()) {\n if (!this.enableHistory) return;\n\n try {\n const stored = localStorage.getItem(this.historyKey);\n let history = stored ? JSON.parse(stored) : [];\n\n history.push({\n text,\n sender,\n timestamp: timestamp.toISOString()\n });\n\n // Keep only last N messages\n if (history.length > this.maxHistoryItems) {\n history = history.slice(-this.maxHistoryItems);\n }\n\n localStorage.setItem(this.historyKey, JSON.stringify(history));\n } catch (error) {\n console.warn('Failed to save chat history:', error);\n }\n }\n\n /**\n * State persistence\n */\n loadState() {\n try {\n const stored = localStorage.getItem(this.stateKey);\n if (stored) {\n // State loaded but not used for auto-open\n // User needs to explicitly open the chat\n }\n } catch (error) {\n console.warn('Failed to load chat state:', error);\n }\n }\n\n saveState() {\n try {\n localStorage.setItem(this.stateKey, JSON.stringify({\n isOpen: this.isOpen,\n timestamp: new Date().toISOString()\n }));\n } catch (error) {\n console.warn('Failed to save chat state:', error);\n }\n }\n\n /**\n * Offline detection\n */\n setupOfflineDetection() {\n window.addEventListener('online', () => {\n this.isOnline = true;\n this.updateOnlineStatus();\n });\n\n window.addEventListener('offline', () => {\n this.isOnline = false;\n this.updateOnlineStatus();\n });\n }\n\n updateOnlineStatus() {\n if (!this.container) return;\n\n const statusBar = this.container.querySelector('.n8n-chat-offline-indicator');\n if (!this.isOnline && !statusBar) {\n const indicator = document.createElement('div');\n indicator.className = 'n8n-chat-offline-indicator';\n indicator.innerHTML = `\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <line x1=\"1\" y1=\"1\" x2=\"23\" y2=\"23\"></line>\n <path d=\"M16.72 11.06A10.94 10.94 0 0 1 19 12.55\"></path>\n <path d=\"M5 12.55a10.94 10.94 0 0 1 5.17-2.39\"></path>\n <path d=\"M10.71 5.05A16 16 0 0 1 22.58 9\"></path>\n <path d=\"M1.42 9a15.91 15.91 0 0 1 4.7-2.88\"></path>\n <path d=\"M8.53 16.11a6 6 0 0 1 6.95 0\"></path>\n <line x1=\"12\" y1=\"20\" x2=\"12.01\" y2=\"20\"></line>\n </svg>\n <span>${this.i18n.t('offline')}</span>\n `;\n this.container.insertBefore(indicator, this.messageList);\n } else if (this.isOnline && statusBar) {\n statusBar.remove();\n }\n\n // Update button state\n if (this.button) {\n this.button.disabled = !this.isOnline || this.isLoading;\n }\n }\n\n /**\n * Keyboard navigation setup\n */\n setupKeyboardNavigation() {\n // ESC to close widget\n document.addEventListener('keydown', (e) => {\n if (e.key === 'Escape' && this.isOpen && this.mode === 'widget') {\n this.toggleChatWindow();\n }\n });\n\n // Tab trap within modal\n this.container.addEventListener('keydown', (e) => {\n if (e.key === 'Tab') {\n const focusableElements = this.container.querySelectorAll(\n 'button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex=\"-1\"])'\n );\n const firstElement = focusableElements[0];\n const lastElement = focusableElements[focusableElements.length - 1];\n\n if (e.shiftKey && document.activeElement === firstElement) {\n e.preventDefault();\n lastElement.focus();\n } else if (!e.shiftKey && document.activeElement === lastElement) {\n e.preventDefault();\n firstElement.focus();\n }\n }\n });\n }\n\n /**\n * Pindai Agent: Start polling for human-agent replies.\n * Called automatically when conversation status is \"pending\" or \"assigned\".\n */\n startPolling() {\n if (!this.pollingUrl || this._pollingTimer) return;\n this._lastMessageTimestamp = this._lastMessageTimestamp || new Date().toISOString();\n\n this._pollingTimer = setInterval(async () => {\n try {\n const url = new URL(this.pollingUrl);\n url.searchParams.set('sessionId', this.sessionId);\n url.searchParams.set('since', this._lastMessageTimestamp);\n if (this.embedToken) {\n url.searchParams.set('embedToken', this.embedToken);\n }\n\n const resp = await fetch(url.toString());\n if (!resp.ok) return;\n\n const data = await resp.json();\n\n if (data.messages && data.messages.length > 0) {\n data.messages.forEach(msg => {\n if (msg.role === 'human_agent') {\n this.addMessage(msg.content, 'ai');\n }\n });\n // Update timestamp to last received message\n const last = data.messages[data.messages.length - 1];\n this._lastMessageTimestamp = last.created_at;\n\n // Notify if widget is closed\n if (!this.isOpen) {\n this.unreadCount += data.messages.filter(m => m.role === 'human_agent').length;\n this.updateUnreadBadge();\n }\n }\n\n // Stop polling when conversation returns to active or resolved\n if (data.status === 'active' || data.status === 'resolved') {\n this.stopPolling();\n }\n } catch (_e) {\n // Polling errors are non-fatal — silently skip\n }\n }, this.pollingInterval);\n }\n\n /**\n * Pindai Agent: Stop polling for human-agent replies.\n */\n stopPolling() {\n if (this._pollingTimer) {\n clearInterval(this._pollingTimer);\n this._pollingTimer = null;\n }\n }\n\n}\n\n// Export both class names for backward compatibility\nwindow.PindaiChatWidget = {\n init: (options) => {\n if (!document.querySelector('.n8n-chat-widget') && !document.querySelector('.n8n-chat-launcher')) {\n return new PindaiChatWidget(options);\n }\n }\n};\n\n// Backward compatibility with old name\nwindow.N8nChatWidget = window.PindaiChatWidget;\n"],"names":["translations","I18n","locale","key","params","text","_a","param","regex","PindaiChatWidget","options","apiEndpoint","e","date","diff","minutes","sender","timestamp","messageBubble","textNode","timeNode","show","indicator","event","file","maxSizeMB","preview","fileItem","f","__async","messageText","error","response","errorMessage","_0","files","retryCount","controller","timeoutId","formData","index","errorData","data","ms","resolve","now","time","oldestTime","waitTime","replies","existingReplies","repliesContainer","reply","button","badge","stored","item","history","statusBar","focusableElements","firstElement","lastElement","url","resp","msg","last","m","_e"],"mappings":"wSAKO,MAAMA,EAAe,CAC1B,GAAI,CAEF,MAAO,eACP,YAAa,qBACb,eAAgB,mCAChB,KAAM,OACN,MAAO,QACP,OAAQ,cACR,WAAY,cAGZ,gBAAiB,kBACjB,QAAS,aACT,QAAS,WACT,WAAY,iBAGZ,QAAS,8CACT,mBAAoB,sBACpB,eAAgB,yBAGhB,aAAc,uCACd,aAAc,qCACd,aAAc,8CACd,YAAa,0CACb,eAAgB,oDAChB,qBAAsB,mDAGtB,qBAAsB,sCACtB,aAAc,+CACd,iBAAkB,mCAGlB,YAAa,yCACb,YAAa,iCACb,YAAa,wBACb,YAAa,kBAGb,aAAc,mBACd,cAAe,oBACf,gBAAiB,eACjB,iBAAkB,oBAClB,eAAgB,cAChB,eAAgB,cAChB,eAAgB,cAChB,eAAgB,eACpB,EAEE,GAAI,CAEF,MAAO,eACP,YAAa,iBACb,eAAgB,oDAChB,KAAM,QACN,MAAO,QACP,OAAQ,cACR,WAAY,aAGZ,gBAAiB,wBACjB,QAAS,cACT,QAAS,YACT,WAAY,uBAGZ,QAAS,2CACT,mBAAoB,kBACpB,eAAgB,6BAGhB,aAAc,wCACd,aAAc,6CACd,aAAc,qDACd,YAAa,8DACb,eAAgB,wDAChB,qBAAsB,wDAGtB,qBAAsB,wCACtB,aAAc,oDACd,iBAAkB,yCAGlB,YAAa,oCACb,YAAa,gCACb,YAAa,gBACb,YAAa,mBAGb,aAAc,mBACd,cAAe,qBACf,gBAAiB,cACjB,iBAAkB,mBAClB,eAAgB,cAChB,eAAgB,aAChB,eAAgB,eAChB,eAAgB,YACpB,CACA,EAKO,MAAMC,CAAK,CAChB,YAAYC,EAAS,KAAM,CACzB,KAAK,OAAS,KAAK,cAAcA,CAAM,EAAIA,EAAS,IACtD,CAKA,cAAcA,EAAQ,CACpB,OAAO,OAAO,KAAKF,CAAY,EAAE,SAASE,CAAM,CAClD,CAQA,EAAEC,EAAKC,EAAS,GAAI,OAClB,IAAIC,IAAOC,EAAAN,EAAa,KAAK,MAAM,IAAxB,YAAAM,EAA4BH,KAAQH,EAAa,GAAGG,CAAG,GAAKA,EAGvE,cAAO,KAAKC,CAAM,EAAE,QAAQG,GAAS,CACnC,MAAMC,EAAQ,IAAI,OAAO,MAAMD,CAAK,MAAO,GAAG,EAC9CF,EAAOA,EAAK,QAAQG,EAAOJ,EAAOG,CAAK,CAAC,CAC1C,CAAC,EAEMF,CACT,CAMA,UAAUH,EAAQ,CAChB,OAAI,KAAK,cAAcA,CAAM,GAC3B,KAAK,OAASA,EACP,KAET,QAAQ,KAAK,mBAAmBA,CAAM,6BAA6B,KAAK,MAAM,EAAE,EACzE,GACT,CAMA,WAAY,CACV,OAAO,KAAK,MACd,CAMA,qBAAsB,CACpB,OAAO,OAAO,KAAKF,CAAY,CACjC,CACF,CCnKA,MAAMS,CAAiB,CACrB,YAAYC,EAAS,CAEnB,MAAMC,EAAcD,EAAQ,YAAcA,EAAQ,OAElD,GAAI,CAACC,EACH,MAAM,IAAI,MAAM,oDAAoD,EAItE,KAAK,WAAaA,EAClB,KAAK,KAAOD,EAAQ,MAAQ,SAG5B,KAAK,OAASA,EAAQ,QAAU,KAChC,KAAK,KAAO,IAAIT,EAAK,KAAK,MAAM,EAGhC,KAAK,MAAQS,EAAQ,OAAS,KAAK,KAAK,EAAE,OAAO,EACjD,KAAK,eAAiBA,EAAQ,gBAAkB,KAAK,KAAK,EAAE,gBAAgB,EAC5E,KAAK,gBAAkBA,EAAQ,iBAAmB,KAAK,eAAc,EAGrE,KAAK,QAAUA,EAAQ,SAAW,6BAClC,KAAK,SAAWA,EAAQ,WAAa,GACrC,KAAK,cAAgBA,EAAQ,eAAiB,UAC9C,KAAK,gBAAkBA,EAAQ,iBAAmB,UAClD,KAAK,YAAcA,EAAQ,aAAe,UAG1C,KAAK,iBAAmBA,EAAQ,mBAAqB,GACrD,KAAK,iBAAmBA,EAAQ,kBAAoB,CAClD,aAAc,YAAa,YAAa,aACxC,kBACA,qBACA,0EACA,2BACA,mEACN,EACI,KAAK,YAAcA,EAAQ,aAAe,GAAK,KAAO,KACtD,KAAK,SAAWA,EAAQ,UAAY,EACpC,KAAK,cAAgB,CAAA,EAGrB,KAAK,oBAAsBA,EAAQ,sBAAwB,GAC3D,KAAK,YAAcA,EAAQ,cAAgB,GAC3C,KAAK,YAAc,EAGnB,KAAK,iBAAmBA,EAAQ,mBAAqB,GACrD,KAAK,aAAeA,EAAQ,cAAgB,CAC1C,KAAK,KAAK,EAAE,aAAa,EACzB,KAAK,KAAK,EAAE,aAAa,EACzB,KAAK,KAAK,EAAE,aAAa,EACzB,KAAK,KAAK,EAAE,aAAa,CAC/B,EAGI,KAAK,cAAgBA,EAAQ,gBAAkB,GAC/C,KAAK,gBAAkBA,EAAQ,iBAAmB,GAClD,KAAK,WAAa,uBAAuB,KAAK,UAAU,GACxD,KAAK,SAAW,qBAAqB,KAAK,UAAU,GAGpD,KAAK,WAAaA,EAAQ,YAAc,EACxC,KAAK,WAAaA,EAAQ,YAAc,IACxC,KAAK,eAAiBA,EAAQ,gBAAkB,IAGhD,KAAK,UAAYA,EAAQ,WAAa,EACtC,KAAK,gBAAkBA,EAAQ,iBAAmB,IAClD,KAAK,aAAe,CAAA,EAGpB,KAAK,UAAY,KACjB,KAAK,SAAW,KAChB,KAAK,WAAa,KAClB,KAAK,YAAc,KACnB,KAAK,MAAQ,KACb,KAAK,OAAS,KACd,KAAK,YAAc,KAGnB,KAAK,WAAaA,EAAQ,YAAc,KACxC,KAAK,WAAaA,EAAQ,YAAc,KACxC,KAAK,eAAiB,CAAC,CAAEA,EAAQ,WACjC,KAAK,gBAAkBA,EAAQ,iBAAmB,IAClD,KAAK,cAAgB,KACrB,KAAK,sBAAwB,KAG7B,KAAK,UAAY,eAAe,KAAK,IAAG,CAAE,IAAI,KAAK,OAAM,CAAE,GAC3D,KAAK,UAAY,GACjB,KAAK,OAAS,GACd,KAAK,SAAW,UAAU,OAG1B,KAAK,UAAS,EACd,KAAK,sBAAqB,EAEtB,KAAK,OAAS,aAChB,KAAK,eAAc,EAEnB,KAAK,aAAY,CAErB,CAKA,cAAe,CACb,KAAK,SAAW,SAAS,cAAc,KAAK,EAC5C,KAAK,SAAS,UAAY,oBAC1B,KAAK,SAAS,MAAM,gBAAkB,KAAK,cAC3C,KAAK,SAAS,aAAa,OAAQ,QAAQ,EAC3C,KAAK,SAAS,aAAa,aAAc,KAAK,KAAK,EAAE,cAAc,CAAC,EACpE,KAAK,SAAS,aAAa,WAAY,GAAG,EAC1C,KAAK,SAAS,UAAY;AAAA,kBACZ,KAAK,eAAe;AAAA;AAAA,MAGlC,SAAS,KAAK,YAAY,KAAK,QAAQ,EAEvC,KAAK,SAAS,iBAAiB,QAAS,IAAM,KAAK,kBAAkB,EACrE,KAAK,SAAS,iBAAiB,UAAY,GAAM,EAC3C,EAAE,MAAQ,SAAW,EAAE,MAAQ,OACjC,EAAE,eAAc,EAChB,KAAK,iBAAgB,EAEzB,CAAC,CACH,CAKA,gBAAiB,CACf,KAAK,UAAY,SAAS,cAAc,KAAK,EAC7C,KAAK,UAAU,UAAY,mBAAmB,KAAK,OAAS,aAAe,8BAAgC,EAAE,GAC7G,KAAK,UAAU,aAAa,OAAQ,QAAQ,EAC5C,KAAK,UAAU,aAAa,aAAc,MAAM,EAChD,KAAK,UAAU,aAAa,aAAc,KAAK,KAAK,EAEpD,KAAK,UAAU,UAAY;AAAA;AAAA;AAAA,YAGnB,KAAK,SAAW,aAAa,KAAK,OAAO,6CAA+C,EAAE;AAAA,yCAC7D,KAAK,KAAK;AAAA;AAAA,yDAEM,KAAK,KAAK,EAAE,eAAe,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAQ3E,KAAK,iBAAmB;AAAA,gEAC8B,KAAK,KAAK,EAAE,gBAAgB,CAAC;AAAA,kDAC3C,KAAK,iBAAiB,KAAK,GAAG,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,UAKrE,EAAE;AAAA,0CAC4B,KAAK,KAAK,EAAE,aAAa,CAAC,iBAAiB,KAAK,KAAK,EAAE,kBAAkB,CAAC;AAAA,qEAC/C,KAAK,eAAe,iBAAiB,KAAK,KAAK,EAAE,iBAAiB,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAOhI,KAAK,iBAAmB,mEAAqE,EAAE;AAAA,MAG/F,KAAK,OAAS,SAChB,SAAS,KAAK,YAAY,KAAK,SAAS,GAExC,SAAS,KAAK,UAAY,GAC1B,SAAS,KAAK,YAAY,KAAK,SAAS,EACxC,SAAS,KAAK,MAAM,OAAS,KAI/B,KAAK,YAAc,KAAK,UAAU,cAAc,oBAAoB,EACpE,KAAK,MAAQ,KAAK,UAAU,cAAc,oBAAoB,EAC9D,KAAK,OAAS,KAAK,UAAU,cAAc,oBAAoB,EAC/D,KAAK,YAAc,KAAK,UAAU,cAAc,qBAAqB,EAGrE,KAAK,OAAO,iBAAiB,QAAU,GAAM,CAC3C,EAAE,eAAc,EAChB,KAAK,YAAW,CAClB,CAAC,EAED,KAAK,MAAM,iBAAiB,WAAa,GAAM,CACzC,EAAE,MAAQ,UACZ,EAAE,eAAc,EAChB,KAAK,YAAW,EAEpB,CAAC,EAEG,KAAK,OAAS,aAChB,KAAK,YAAY,MAAM,QAAU,OAEjC,KAAK,YAAY,iBAAiB,QAAS,IAAM,KAAK,kBAAkB,EAItE,KAAK,kBACW,KAAK,UAAU,cAAc,oBAAoB,EACzD,iBAAiB,SAAWE,GAAM,KAAK,iBAAiBA,CAAC,CAAC,EAItE,KAAK,wBAAuB,EAG5B,KAAK,YAAW,EACZ,KAAK,YAAY,SAAS,SAAW,GACvC,KAAK,WAAW,KAAK,eAAgB,IAAI,CAE7C,CAKA,kBAAmB,CACZ,KAAK,QAcR,KAAK,UAAU,UAAU,OAAO,uBAAuB,EACnD,KAAK,UACP,KAAK,SAAS,UAAU,OAAO,2BAA2B,IAfvD,KAAK,WACR,KAAK,eAAc,EAGrB,WAAW,IAAM,CACf,KAAK,UAAU,UAAU,IAAI,uBAAuB,EAChD,KAAK,UACP,KAAK,SAAS,UAAU,IAAI,2BAA2B,EAEzD,KAAK,MAAM,MAAK,EAChB,KAAK,iBAAgB,CACvB,EAAG,EAAE,GAOP,KAAK,OAAS,CAAC,KAAK,OACpB,KAAK,UAAS,CAChB,CAKA,gBAAiB,CAMf,MAAO,oCAAoC,mBAL3B;AAAA;AAAA;AAAA;AAAA,KAKqD,CAAC,EACxE,CAKA,gBAAgBC,EAAM,CAEpB,MAAMC,EADM,IAAI,KACGD,EAEnB,GAAIC,EAAO,IACT,OAAO,KAAK,KAAK,EAAE,SAAS,EAE9B,GAAIA,EAAO,KAAS,CAClB,MAAMC,EAAU,KAAK,MAAMD,EAAO,GAAK,EACvC,OAAO,KAAK,KAAK,EAAE,aAAc,CAAE,QAAAC,CAAO,CAAE,CAC9C,CAEA,OAAOF,EAAK,mBAAmB,KAAK,SAAW,KAAO,QAAU,QAAS,CACvE,KAAM,UACN,OAAQ,SACd,CAAK,CACH,CAKA,WAAWR,EAAMW,EAAQC,EAAY,IAAI,KAAQ,CAC/C,MAAMC,EAAgB,SAAS,cAAc,KAAK,EAClDA,EAAc,UAAY,4BAA4BF,CAAM,WAE5D,MAAMG,EAAW,SAAS,cAAc,KAAK,EAC7CA,EAAS,UAAY,wBACrBA,EAAS,YAAcd,EAEvB,MAAMe,EAAW,SAAS,cAAc,KAAK,EAC7CA,EAAS,UAAY,6BACrBA,EAAS,YAAc,KAAK,gBAAgBH,CAAS,EACrDG,EAAS,aAAa,iBAAkBH,EAAU,YAAW,CAAE,EAE/DC,EAAc,YAAYC,CAAQ,EAClCD,EAAc,YAAYE,CAAQ,EAClC,KAAK,YAAY,YAAYF,CAAa,EAE1C,KAAK,YAAY,UAAY,KAAK,YAAY,aAG9C,KAAK,cAAcb,EAAMW,EAAQC,CAAS,EAGtC,CAAC,KAAK,QAAUD,IAAW,MAC7B,KAAK,gBAAe,CAExB,CAKA,oBAAoBK,EAAM,CACxB,IAAIC,EAAY,KAAK,YAAY,cAAc,4BAA4B,EACvED,EACGC,IACHA,EAAY,SAAS,cAAc,KAAK,EACxCA,EAAU,UAAY,gEACtBA,EAAU,UAAY,0CACtBA,EAAU,aAAa,aAAc,KAAK,KAAK,EAAE,iBAAiB,CAAC,EACnE,KAAK,YAAY,YAAYA,CAAS,EACtC,KAAK,YAAY,UAAY,KAAK,YAAY,cAG5CA,GACFA,EAAU,OAAM,CAGtB,CAKA,iBAAiBC,EAAO,CACR,MAAM,KAAKA,EAAM,OAAO,KAAK,EAErC,QAAQC,GAAQ,CAEpB,GAAI,CAAC,KAAK,iBAAiB,SAASA,EAAK,IAAI,EAAG,CAC9C,KAAK,WACH,KAAK,KAAK,EAAE,uBAAwB,CAAE,SAAUA,EAAK,KAAM,EAC3D,IACV,EACQ,MACF,CAGA,GAAIA,EAAK,KAAO,KAAK,YAAa,CAChC,MAAMC,EAAY,KAAK,YAAc,KAAO,KAC5C,KAAK,WACH,KAAK,KAAK,EAAE,eAAgB,CAAE,SAAUD,EAAK,KAAM,QAASC,EAAW,EACvE,IACV,EACQ,MACF,CAGA,GAAI,KAAK,cAAc,QAAU,KAAK,SAAU,CAC9C,KAAK,WACH,KAAK,KAAK,EAAE,mBAAoB,CAAE,SAAU,KAAK,SAAU,EAC3D,IACV,EACQ,MACF,CAEA,KAAK,cAAc,KAAKD,CAAI,EAC5B,KAAK,kBAAkBA,CAAI,CAC7B,CAAC,EAEDD,EAAM,OAAO,MAAQ,EACvB,CAKA,kBAAkBC,EAAM,CACtB,MAAME,EAAU,KAAK,UAAU,cAAc,wBAAwB,EACrE,GAAI,CAACA,EAAS,OAEdA,EAAQ,MAAM,QAAU,OAExB,MAAMC,EAAW,SAAS,cAAc,KAAK,EAC7CA,EAAS,UAAY,qBACrBA,EAAS,UAAY;AAAA,yCACgBH,EAAK,IAAI;AAAA,wDACMA,EAAK,IAAI,iBAAiB,KAAK,KAAK,EAAE,gBAAgB,CAAC;AAAA,MAGzFG,EAAS,cAAc,uBAAuB,EACtD,iBAAiB,QAAS,IAAM,CACxC,KAAK,cAAgB,KAAK,cAAc,OAAOC,GAAKA,EAAE,OAASJ,EAAK,IAAI,EACxEG,EAAS,OAAM,EACX,KAAK,cAAc,SAAW,IAChCD,EAAQ,MAAM,QAAU,OAE5B,CAAC,EAEDA,EAAQ,YAAYC,CAAQ,CAC9B,CAKM,aAAc,QAAAE,EAAA,sBAClB,MAAMC,EAAc,KAAK,MAAM,MAAM,KAAI,EACzC,GAAK,GAACA,GAAe,KAAK,cAAc,SAAW,GAAM,KAAK,WAG9D,IAAI,CACF,KAAK,eAAc,CACrB,OAASC,EAAO,CACd,KAAK,WAAWA,EAAM,QAAS,IAAI,EACnC,MACF,CAGA,GAAI,CAAC,KAAK,SAAU,CAClB,KAAK,WAAW,KAAK,KAAK,EAAE,gBAAgB,EAAG,IAAI,EACnD,MACF,CAEA,KAAK,UAAY,GACjB,KAAK,OAAO,SAAW,GACvB,KAAK,MAAM,SAAW,GAElBD,GACF,KAAK,WAAWA,EAAa,MAAM,EAGrC,KAAK,MAAM,MAAQ,GACnB,KAAK,oBAAoB,EAAI,EAE7B,GAAI,CACF,MAAME,EAAW,MAAM,KAAK,qBAAqBF,EAAa,KAAK,aAAa,EAChF,KAAK,WAAWE,EAAU,IAAI,EAG1B,KAAK,kBAAoB,KAAK,aAAa,OAAS,GACtD,KAAK,mBAAkB,CAE3B,OAASD,EAAO,CACd,MAAME,EAAe,KAAK,gBAAgBF,CAAK,EAC/C,KAAK,WAAWE,EAAc,IAAI,CACpC,QAAC,CAQC,GAPA,KAAK,UAAY,GACjB,KAAK,OAAO,SAAW,GACvB,KAAK,MAAM,SAAW,GACtB,KAAK,oBAAoB,EAAK,EAC9B,KAAK,MAAM,MAAK,EAGZ,KAAK,cAAc,OAAS,EAAG,CACjC,KAAK,cAAgB,CAAA,EACrB,MAAMP,EAAU,KAAK,UAAU,cAAc,wBAAwB,EACjEA,IACFA,EAAQ,UAAY,GACpBA,EAAQ,MAAM,QAAU,OAE5B,CACF,EACF,GAKM,qBAAqBQ,EAAyC,QAAAL,EAAA,yBAAzCC,EAAaK,EAAQ,CAAA,EAAIC,EAAa,EAAG,CAClE,GAAI,CACF,MAAMC,EAAa,IAAI,gBACjBC,EAAY,WAAW,IAAMD,EAAW,MAAK,EAAI,KAAK,cAAc,EAEpEE,EAAW,IAAI,SACrBA,EAAS,OAAO,YAAa,KAAK,SAAS,EAC3CA,EAAS,OAAO,UAAWT,CAAW,EAGlC,KAAK,YACPS,EAAS,OAAO,aAAc,KAAK,UAAU,EAI/CJ,EAAM,QAAQ,CAACX,EAAMgB,IAAU,CAC7BD,EAAS,OAAO,OAAOC,CAAK,GAAIhB,CAAI,CACtC,CAAC,EAED,MAAMQ,EAAW,MAAM,MAAM,KAAK,WAAY,CAC5C,OAAQ,OACR,KAAMO,EACN,OAAQF,EAAW,MAC3B,CAAO,EAID,GAFA,aAAaC,CAAS,EAElB,CAACN,EAAS,GAAI,CAEhB,GAAIA,EAAS,QAAU,KAAOI,EAAa,KAAK,WAC9C,aAAM,KAAK,MAAM,KAAK,YAAcA,EAAa,EAAE,EAC5C,KAAK,qBAAqBN,EAAaK,EAAOC,EAAa,CAAC,EAGrE,MAAMK,EAAY,MAAMT,EAAS,KAAI,EAAG,MAAM,KAAO,CAAA,EAAG,EACxD,MAAM,IAAI,MAAMS,EAAU,SAAW,kBAAkBT,EAAS,UAAU,EAAE,CAC9E,CAEA,MAAMU,EAAO,MAAMV,EAAS,KAAI,EAChC,GAAI,CAACU,EAAK,SACR,MAAM,IAAI,MAAM,KAAK,KAAK,EAAE,sBAAsB,CAAC,EAIrD,OAAIA,EAAK,QAAUA,EAAK,SAAW,UAAY,KAAK,eAClD,KAAK,aAAY,EAEjB,KAAK,YAAW,EAGXA,EAAK,QAEd,OAASX,EAAO,CACd,GAAIA,EAAM,OAAS,aACjB,MAAM,IAAI,MAAM,KAAK,KAAK,EAAE,cAAc,CAAC,EAI7C,GAAIA,EAAM,QAAQ,SAAS,cAAc,GAAKK,EAAa,KAAK,WAC9D,aAAM,KAAK,MAAM,KAAK,YAAcA,EAAa,EAAE,EAC5C,KAAK,qBAAqBN,EAAaK,EAAOC,EAAa,CAAC,EAGrE,MAAML,CACR,CACF,GAKA,MAAMY,EAAI,CACR,OAAO,IAAI,QAAQC,GAAW,WAAWA,EAASD,CAAE,CAAC,CACvD,CAKA,gBAAgBZ,EAAO,CACrB,OAAIA,EAAM,QAAQ,SAAS,SAAS,GAAKA,EAAM,QAAQ,SAAS,SAAS,EAChE,KAAK,KAAK,EAAE,cAAc,EAE/BA,EAAM,QAAQ,SAAS,cAAc,GAAKA,EAAM,QAAQ,SAAS,iBAAiB,EAC7E,KAAK,KAAK,EAAE,cAAc,EAE/BA,EAAM,QAAQ,SAAS,KAAK,GAAKA,EAAM,QAAQ,SAAS,KAAK,EACxD,KAAK,KAAK,EAAE,aAAa,EAE3B,KAAK,KAAK,EAAE,cAAc,CACnC,CAKA,gBAAiB,CACf,MAAMc,EAAM,KAAK,IAAG,EAKpB,GAJA,KAAK,aAAe,KAAK,aAAa,OACpCC,GAAQD,EAAMC,EAAO,KAAK,eAChC,EAEQ,KAAK,aAAa,QAAU,KAAK,UAAW,CAC9C,MAAMC,EAAa,KAAK,aAAa,CAAC,EAChCC,EAAW,KAAK,MAAM,KAAK,iBAAmBH,EAAME,IAAe,GAAI,EAC7E,MAAM,IAAI,MAAM,KAAK,KAAK,EAAE,iBAAkB,CAAE,QAASC,CAAQ,CAAE,CAAC,CACtE,CAEA,KAAK,aAAa,KAAKH,CAAG,CAC5B,CAKA,mBAAmBI,EAAU,KAAK,aAAc,CAC9C,GAAI,CAAC,KAAK,kBAAoBA,EAAQ,SAAW,EAAG,OAGpD,MAAMC,EAAkB,KAAK,YAAY,cAAc,yBAAyB,EAC5EA,GAAiBA,EAAgB,OAAM,EAE3C,MAAMC,EAAmB,SAAS,cAAc,KAAK,EACrDA,EAAiB,UAAY,yBAE7BF,EAAQ,QAAQG,GAAS,CACvB,MAAMC,EAAS,SAAS,cAAc,QAAQ,EAC9CA,EAAO,UAAY,2BACnBA,EAAO,YAAcD,EACrBC,EAAO,iBAAiB,QAAS,IAAM,CACrC,KAAK,MAAM,MAAQD,EACnB,KAAK,YAAW,EAChBD,EAAiB,OAAM,CACzB,CAAC,EACDA,EAAiB,YAAYE,CAAM,CACrC,CAAC,EAED,KAAK,YAAY,YAAYF,CAAgB,EAC7C,KAAK,YAAY,UAAY,KAAK,YAAY,YAChD,CAKA,iBAAkB,CACX,KAAK,SACR,KAAK,cACL,KAAK,kBAAiB,EAE1B,CAEA,mBAAoB,CAClB,GAAI,CAAC,KAAK,SAAU,OACpB,MAAMG,EAAQ,KAAK,SAAS,cAAc,wBAAwB,EAC9DA,IACFA,EAAM,YAAc,KAAK,YACzBA,EAAM,MAAM,QAAU,KAAK,YAAc,EAAI,OAAS,OAE1D,CAEA,kBAAmB,CACjB,KAAK,YAAc,EACnB,KAAK,kBAAiB,CACxB,CAKA,aAAc,CACZ,GAAK,KAAK,cAEV,GAAI,CACF,MAAMC,EAAS,aAAa,QAAQ,KAAK,UAAU,EACnD,GAAI,CAACA,EAAQ,OAEG,KAAK,MAAMA,CAAM,EACzB,QAAQC,GAAQ,CACtB,KAAK,wBAAwBA,EAAK,KAAMA,EAAK,OAAQ,IAAI,KAAKA,EAAK,SAAS,CAAC,CAC/E,CAAC,CACH,OAASzB,EAAO,CACd,QAAQ,KAAK,+BAAgCA,CAAK,CACpD,CACF,CAEA,wBAAwB1B,EAAMW,EAAQC,EAAW,CAC/C,MAAMC,EAAgB,SAAS,cAAc,KAAK,EAClDA,EAAc,UAAY,4BAA4BF,CAAM,WAE5D,MAAMG,EAAW,SAAS,cAAc,KAAK,EAC7CA,EAAS,UAAY,wBACrBA,EAAS,YAAcd,EAEvB,MAAMe,EAAW,SAAS,cAAc,KAAK,EAC7CA,EAAS,UAAY,6BACrBA,EAAS,YAAc,KAAK,gBAAgBH,CAAS,EACrDG,EAAS,aAAa,iBAAkBH,EAAU,YAAW,CAAE,EAE/DC,EAAc,YAAYC,CAAQ,EAClCD,EAAc,YAAYE,CAAQ,EAClC,KAAK,YAAY,YAAYF,CAAa,EAE1C,KAAK,YAAY,UAAY,KAAK,YAAY,YAChD,CAEA,cAAcb,EAAMW,EAAQC,EAAY,IAAI,KAAQ,CAClD,GAAK,KAAK,cAEV,GAAI,CACF,MAAMsC,EAAS,aAAa,QAAQ,KAAK,UAAU,EACnD,IAAIE,EAAUF,EAAS,KAAK,MAAMA,CAAM,EAAI,CAAA,EAE5CE,EAAQ,KAAK,CACX,KAAApD,EACA,OAAAW,EACA,UAAWC,EAAU,YAAW,CACxC,CAAO,EAGGwC,EAAQ,OAAS,KAAK,kBACxBA,EAAUA,EAAQ,MAAM,CAAC,KAAK,eAAe,GAG/C,aAAa,QAAQ,KAAK,WAAY,KAAK,UAAUA,CAAO,CAAC,CAC/D,OAAS1B,EAAO,CACd,QAAQ,KAAK,+BAAgCA,CAAK,CACpD,CACF,CAKA,WAAY,CACV,GAAI,CACF,MAAMwB,EAAS,aAAa,QAAQ,KAAK,QAAQ,CAKnD,OAASxB,EAAO,CACd,QAAQ,KAAK,6BAA8BA,CAAK,CAClD,CACF,CAEA,WAAY,CACV,GAAI,CACF,aAAa,QAAQ,KAAK,SAAU,KAAK,UAAU,CACjD,OAAQ,KAAK,OACb,UAAW,IAAI,KAAI,EAAG,YAAW,CACzC,CAAO,CAAC,CACJ,OAASA,EAAO,CACd,QAAQ,KAAK,6BAA8BA,CAAK,CAClD,CACF,CAKA,uBAAwB,CACtB,OAAO,iBAAiB,SAAU,IAAM,CACtC,KAAK,SAAW,GAChB,KAAK,mBAAkB,CACzB,CAAC,EAED,OAAO,iBAAiB,UAAW,IAAM,CACvC,KAAK,SAAW,GAChB,KAAK,mBAAkB,CACzB,CAAC,CACH,CAEA,oBAAqB,CACnB,GAAI,CAAC,KAAK,UAAW,OAErB,MAAM2B,EAAY,KAAK,UAAU,cAAc,6BAA6B,EAC5E,GAAI,CAAC,KAAK,UAAY,CAACA,EAAW,CAChC,MAAMpC,EAAY,SAAS,cAAc,KAAK,EAC9CA,EAAU,UAAY,6BACtBA,EAAU,UAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAUZ,KAAK,KAAK,EAAE,SAAS,CAAC;AAAA,QAEhC,KAAK,UAAU,aAAaA,EAAW,KAAK,WAAW,CACzD,MAAW,KAAK,UAAYoC,GAC1BA,EAAU,OAAM,EAId,KAAK,SACP,KAAK,OAAO,SAAW,CAAC,KAAK,UAAY,KAAK,UAElD,CAKA,yBAA0B,CAExB,SAAS,iBAAiB,UAAY,GAAM,CACtC,EAAE,MAAQ,UAAY,KAAK,QAAU,KAAK,OAAS,UACrD,KAAK,iBAAgB,CAEzB,CAAC,EAGD,KAAK,UAAU,iBAAiB,UAAY,GAAM,CAChD,GAAI,EAAE,MAAQ,MAAO,CACnB,MAAMC,EAAoB,KAAK,UAAU,iBACvC,gFACV,EACcC,EAAeD,EAAkB,CAAC,EAClCE,EAAcF,EAAkBA,EAAkB,OAAS,CAAC,EAE9D,EAAE,UAAY,SAAS,gBAAkBC,GAC3C,EAAE,eAAc,EAChBC,EAAY,MAAK,GACR,CAAC,EAAE,UAAY,SAAS,gBAAkBA,IACnD,EAAE,eAAc,EAChBD,EAAa,MAAK,EAEtB,CACF,CAAC,CACH,CAMA,cAAe,CACT,CAAC,KAAK,YAAc,KAAK,gBAC7B,KAAK,sBAAwB,KAAK,uBAAyB,IAAI,KAAI,EAAG,YAAW,EAEjF,KAAK,cAAgB,YAAY,IAAY/B,EAAA,sBAC3C,GAAI,CACF,MAAMiC,EAAM,IAAI,IAAI,KAAK,UAAU,EACnCA,EAAI,aAAa,IAAI,YAAa,KAAK,SAAS,EAChDA,EAAI,aAAa,IAAI,QAAS,KAAK,qBAAqB,EACpD,KAAK,YACPA,EAAI,aAAa,IAAI,aAAc,KAAK,UAAU,EAGpD,MAAMC,EAAO,MAAM,MAAMD,EAAI,SAAQ,CAAE,EACvC,GAAI,CAACC,EAAK,GAAI,OAEd,MAAMrB,EAAO,MAAMqB,EAAK,KAAI,EAE5B,GAAIrB,EAAK,UAAYA,EAAK,SAAS,OAAS,EAAG,CAC7CA,EAAK,SAAS,QAAQsB,GAAO,CACvBA,EAAI,OAAS,eACf,KAAK,WAAWA,EAAI,QAAS,IAAI,CAErC,CAAC,EAED,MAAMC,EAAOvB,EAAK,SAASA,EAAK,SAAS,OAAS,CAAC,EACnD,KAAK,sBAAwBuB,EAAK,WAG7B,KAAK,SACR,KAAK,aAAevB,EAAK,SAAS,OAAOwB,GAAKA,EAAE,OAAS,aAAa,EAAE,OACxE,KAAK,kBAAiB,EAE1B,EAGIxB,EAAK,SAAW,UAAYA,EAAK,SAAW,aAC9C,KAAK,YAAW,CAEpB,OAASyB,EAAI,CAEb,CACF,GAAG,KAAK,eAAe,EACzB,CAKA,aAAc,CACR,KAAK,gBACP,cAAc,KAAK,aAAa,EAChC,KAAK,cAAgB,KAEzB,CAEF,CAGA,OAAO,iBAAmB,CACxB,KAAOzD,GAAY,CACjB,GAAI,CAAC,SAAS,cAAc,kBAAkB,GAAK,CAAC,SAAS,cAAc,oBAAoB,EAC7F,OAAO,IAAID,EAAiBC,CAAO,CAEvC,CACF,EAGA,OAAO,cAAgB,OAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pindai-ai/chat-widget",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "description": "Modern, accessible chat widget for Pindai.ai",
5
5
  "main": "dist/pindai-chat-widget.js",
6
6
  "style": "dist/pindai-chat-widget.css",
@@ -34,12 +34,12 @@
34
34
  "license": "MIT",
35
35
  "repository": {
36
36
  "type": "git",
37
- "url": "https://github.com/pindai-ai/pindai-chat-widget.git"
37
+ "url": "https://github.com/PindaiAI/pindai-chat-widget.git"
38
38
  },
39
39
  "bugs": {
40
- "url": "https://github.com/pindai-ai/pindai-chat-widget/issues"
40
+ "url": "https://github.com/PindaiAI/pindai-chat-widget/issues"
41
41
  },
42
- "homepage": "https://pindai.ai",
42
+ "homepage": "https://github.com/PindaiAI/pindai-chat-widget#readme",
43
43
  "files": [
44
44
  "dist",
45
45
  "src",
package/src/main.js CHANGED
@@ -87,6 +87,14 @@ class PindaiChatWidget {
87
87
  this.button = null;
88
88
  this.closeButton = null;
89
89
 
90
+ // Pindai Agent auth + polling (backward-compatible: all fields optional)
91
+ this.embedToken = options.embedToken || null;
92
+ this.pollingUrl = options.pollingUrl || null;
93
+ this.pollingEnabled = !!(options.pollingUrl);
94
+ this.pollingInterval = options.pollingInterval || 3000;
95
+ this._pollingTimer = null;
96
+ this._lastMessageTimestamp = null;
97
+
90
98
  // State
91
99
  this.sessionId = `web-session-${Date.now()}-${Math.random()}`;
92
100
  this.isLoading = false;
@@ -477,6 +485,11 @@ class PindaiChatWidget {
477
485
  formData.append('sessionId', this.sessionId);
478
486
  formData.append('message', messageText);
479
487
 
488
+ // Pindai Agent: include embed token if configured
489
+ if (this.embedToken) {
490
+ formData.append('embedToken', this.embedToken);
491
+ }
492
+
480
493
  // Append files
481
494
  files.forEach((file, index) => {
482
495
  formData.append(`file${index}`, file);
@@ -506,6 +519,13 @@ class PindaiChatWidget {
506
519
  throw new Error(this.i18n.t('errorInvalidResponse'));
507
520
  }
508
521
 
522
+ // Pindai Agent: start polling if handoff was triggered
523
+ if (data.status && data.status !== 'active' && this.pollingEnabled) {
524
+ this.startPolling();
525
+ } else {
526
+ this.stopPolling();
527
+ }
528
+
509
529
  return data.response;
510
530
 
511
531
  } catch (error) {
@@ -781,6 +801,66 @@ class PindaiChatWidget {
781
801
  }
782
802
  });
783
803
  }
804
+
805
+ /**
806
+ * Pindai Agent: Start polling for human-agent replies.
807
+ * Called automatically when conversation status is "pending" or "assigned".
808
+ */
809
+ startPolling() {
810
+ if (!this.pollingUrl || this._pollingTimer) return;
811
+ this._lastMessageTimestamp = this._lastMessageTimestamp || new Date().toISOString();
812
+
813
+ this._pollingTimer = setInterval(async () => {
814
+ try {
815
+ const url = new URL(this.pollingUrl);
816
+ url.searchParams.set('sessionId', this.sessionId);
817
+ url.searchParams.set('since', this._lastMessageTimestamp);
818
+ if (this.embedToken) {
819
+ url.searchParams.set('embedToken', this.embedToken);
820
+ }
821
+
822
+ const resp = await fetch(url.toString());
823
+ if (!resp.ok) return;
824
+
825
+ const data = await resp.json();
826
+
827
+ if (data.messages && data.messages.length > 0) {
828
+ data.messages.forEach(msg => {
829
+ if (msg.role === 'human_agent') {
830
+ this.addMessage(msg.content, 'ai');
831
+ }
832
+ });
833
+ // Update timestamp to last received message
834
+ const last = data.messages[data.messages.length - 1];
835
+ this._lastMessageTimestamp = last.created_at;
836
+
837
+ // Notify if widget is closed
838
+ if (!this.isOpen) {
839
+ this.unreadCount += data.messages.filter(m => m.role === 'human_agent').length;
840
+ this.updateUnreadBadge();
841
+ }
842
+ }
843
+
844
+ // Stop polling when conversation returns to active or resolved
845
+ if (data.status === 'active' || data.status === 'resolved') {
846
+ this.stopPolling();
847
+ }
848
+ } catch (_e) {
849
+ // Polling errors are non-fatal — silently skip
850
+ }
851
+ }, this.pollingInterval);
852
+ }
853
+
854
+ /**
855
+ * Pindai Agent: Stop polling for human-agent replies.
856
+ */
857
+ stopPolling() {
858
+ if (this._pollingTimer) {
859
+ clearInterval(this._pollingTimer);
860
+ this._pollingTimer = null;
861
+ }
862
+ }
863
+
784
864
  }
785
865
 
786
866
  // Export both class names for backward compatibility