@pindai-ai/chat-widget 2.0.1 → 2.0.3

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
@@ -2,10 +2,26 @@
2
2
 
3
3
  > Modern, accessible chat widget for Pindai.ai - AI-powered document extraction for Indonesian enterprises
4
4
 
5
- ![Demo](./images/1.gif)
6
-
7
5
  [![npm version](https://img.shields.io/npm/v/@pindai-ai/chat-widget.svg)](https://www.npmjs.com/package/@pindai-ai/chat-widget)
6
+ [![npm downloads](https://img.shields.io/npm/dm/@pindai-ai/chat-widget.svg)](https://www.npmjs.com/package/@pindai-ai/chat-widget)
8
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
+ [![jsDelivr hits](https://img.shields.io/jsdelivr/npm/hm/@pindai-ai/chat-widget)](https://www.jsdelivr.com/package/npm/@pindai-ai/chat-widget)
9
+
10
+ ## 🚀 Quick Start
11
+
12
+ ```html
13
+ <!-- Add this to your HTML -->
14
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@pindai-ai/chat-widget@2/dist/pindai-chat-widget.css">
15
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@pindai-ai/chat-widget@2/dist/pindai-chat-widget.js"></script>
16
+
17
+ <script>
18
+ document.addEventListener('DOMContentLoaded', () => {
19
+ window.PindaiChatWidget.init({
20
+ webhookUrl: 'https://your-backend.com/webhook/chat'
21
+ });
22
+ });
23
+ </script>
24
+ ```
9
25
 
10
26
  ## ✨ Features
11
27
 
@@ -35,6 +51,7 @@ npm install @pindai-ai/chat-widget
35
51
 
36
52
  ### Via CDN (jsDelivr)
37
53
 
54
+ **Latest 2.x version (recommended):**
38
55
  ```html
39
56
  <link rel="stylesheet"
40
57
  href="https://cdn.jsdelivr.net/npm/@pindai-ai/chat-widget@2/dist/pindai-chat-widget.css">
@@ -42,6 +59,16 @@ npm install @pindai-ai/chat-widget
42
59
  src="https://cdn.jsdelivr.net/npm/@pindai-ai/chat-widget@2/dist/pindai-chat-widget.js"></script>
43
60
  ```
44
61
 
62
+ **Specific version (2.0.1):**
63
+ ```html
64
+ <link rel="stylesheet"
65
+ href="https://cdn.jsdelivr.net/npm/@pindai-ai/chat-widget@2.0.1/dist/pindai-chat-widget.css">
66
+ <script type="module"
67
+ src="https://cdn.jsdelivr.net/npm/@pindai-ai/chat-widget@2.0.1/dist/pindai-chat-widget.js"></script>
68
+ ```
69
+
70
+ > Using `@2` automatically gives you the latest 2.x.x version with bug fixes and improvements. Use `@2.0.1` if you need a specific version.
71
+
45
72
  ### Local Development
46
73
 
47
74
  ```bash
@@ -61,29 +88,9 @@ npm run build
61
88
 
62
89
  ---
63
90
 
64
- ## 🚀 Quick Start
65
-
66
- ### Simplest Way to Embed
67
-
68
- Add this to your website before the closing `</body>` tag:
69
-
70
- ```html
71
- <!-- Add near the end of your body tag -->
72
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@pindai-ai/chat-widget@2/dist/pindai-chat-widget.css">
73
- <script type="module" src="https://cdn.jsdelivr.net/npm/@pindai-ai/chat-widget@2/dist/pindai-chat-widget.js"></script>
74
-
75
- <script>
76
- document.addEventListener('DOMContentLoaded', function () {
77
- window.PindaiChatWidget.init({
78
- webhookUrl: 'https://your-backend.com/webhook/chat' // Your n8n webhook URL
79
- });
80
- });
81
- </script>
82
- ```
83
-
84
- That's it! Replace `your-backend.com/webhook/chat` with your actual endpoint URL.
91
+ ## 📖 Usage Examples
85
92
 
86
- ### Complete Example
93
+ ### Complete HTML Example
87
94
 
88
95
  ```html
89
96
  <!DOCTYPE html>
@@ -206,17 +213,28 @@ Your backend should respond with JSON:
206
213
  ### Example Backend Implementations
207
214
 
208
215
  <details>
209
- <summary><strong>n8n Workflow</strong></summary>
216
+ <summary><strong>n8n Workflow (Recommended)</strong></summary>
210
217
 
211
- 1. Add "Webhook" node (POST method)
212
- 2. Add "Chat Trigger" or custom AI logic
213
- 3. Return JSON with `response` field
218
+ We provide a ready-to-use n8n workflow with AI chat memory!
214
219
 
215
- ```json
216
- {
217
- "response": "{{ $json.aiResponse }}"
218
- }
219
- ```
220
+ **Quick Setup:**
221
+
222
+ 1. Import the workflow from [`pindai-chat-workflow.json`](./pindai-chat-workflow.json)
223
+ 2. Add your OpenAI API key to the "OpenAI Chat Model" node
224
+ 3. Activate the workflow
225
+ 4. Copy the webhook URL and use it in your widget
226
+
227
+ **What's included:**
228
+ - AI Agent with Indonesian system prompt
229
+ - Chat memory (remembers last 10 messages per session)
230
+ - Automatic response formatting
231
+ - Ready for production use
232
+
233
+ For detailed setup instructions, see [N8N_WORKFLOW_GUIDE.md](./N8N_WORKFLOW_GUIDE.md)
234
+
235
+ **Simple Test Workflow:**
236
+
237
+ If you want to test without AI first, use [`pindai-chat-workflow-simple.json`](./pindai-chat-workflow-simple.json) - no API keys needed!
220
238
 
221
239
  </details>
222
240
 
@@ -409,6 +427,15 @@ npm run preview
409
427
 
410
428
  ## 📝 Changelog
411
429
 
430
+ ### Version 2.0.1 (2026-02-06)
431
+
432
+ **Updates:**
433
+ - Vendor-agnostic API: Changed `n8nUrl` to `webhookUrl` parameter
434
+ - Added backward compatibility for `n8nUrl`
435
+ - Added "Powered by Pindai.ai" watermark in widget footer
436
+ - Updated documentation with n8n workflow examples
437
+ - Included ready-to-use n8n workflow JSON files
438
+
412
439
  ### Version 2.0.0 (2026-02-05)
413
440
 
414
441
  **Major Changes:**
@@ -464,19 +491,29 @@ MIT © [Pindai.ai](https://pindai.ai)
464
491
 
465
492
  ---
466
493
 
494
+ ## 🔗 Links
495
+
496
+ - **npm Package:** [npmjs.com/package/@pindai-ai/chat-widget](https://www.npmjs.com/package/@pindai-ai/chat-widget)
497
+ - **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)
500
+ - **Pindai.ai Website:** [pindai.ai](https://pindai.ai)
501
+
502
+ ---
503
+
467
504
  ## 🆘 Support
468
505
 
469
- - **Documentation:** [https://docs.pindai.ai](https://pindai.ai)
470
- - **Issues:** [https://github.com/pindai-ai/pindai-chat-widget/issues](https://github.com/pindai-ai/pindai-chat-widget/issues)
506
+ - **Documentation:** [N8N_WORKFLOW_GUIDE.md](./N8N_WORKFLOW_GUIDE.md)
507
+ - **Issues:** [GitHub Issues](https://github.com/pindai-ai/pindai-chat-widget/issues)
471
508
  - **Email:** support@pindai.ai
472
- - **Website:** [https://pindai.ai](https://pindai.ai)
509
+ - **Website:** [pindai.ai](https://pindai.ai)
473
510
 
474
511
  ---
475
512
 
476
513
  ## 🙏 Acknowledgments
477
514
 
478
515
  - Inspired by HubSpot chat widget design patterns
479
- - Built with modern web standards
516
+ - Built with modern web standards and accessibility best practices
480
517
  - Designed for Indonesian enterprises
481
518
  - Powered by Pindai.ai's AI document extraction technology
482
519
 
@@ -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:"Chat with AI",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:"Chat dengan AI",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(r){typeof define=="function"&&define.amd?define(r):r()})(function(){"use strict";var m=(r,d,o)=>new Promise((l,e)=>{var t=s=>{try{a(o.next(s))}catch(n){e(n)}},i=s=>{try{a(o.throw(s))}catch(n){e(n)}},a=s=>s.done?l(s.value):Promise.resolve(s.value).then(t,i);a((o=o.apply(r,d)).next())});const r={en:{title:"Pindai Agent",placeholder:"Write a message...",initialMessage:"Hello! How can I help you today?",send:"Send",close:"Close",upload:"Upload file",removeFile:"Remove file",typingIndicator:"AI is typing...",sending:"Sending...",justNow:"Just now",minutesAgo:"{minutes}m ago",offline:"Offline - messages will be sent when online",connectionRestored:"Connection restored",connectionLost:"No internet connection",errorGeneric:"An error occurred. Please try again.",errorTimeout:"Request timeout. Please try again.",errorNetwork:"No internet connection. Check your network.",errorServer:"Server is busy. Please try again later.",errorRateLimit:"Too many messages. Please wait {seconds} seconds.",errorInvalidResponse:"Invalid server response. Please contact support.",fileTypeNotSupported:"File type not supported: {filename}",fileTooLarge:"File too large: {filename} (max {maxSize}MB)",maxFilesExceeded:"Maximum {maxFiles} files allowed",quickReply1:"How can I extract data from documents?",quickReply2:"What file types are supported?",quickReply3:"Tell me about pricing",quickReply4:"Contact support",ariaOpenChat:"Open chat widget",ariaCloseChat:"Close chat window",ariaSendMessage:"Send message",ariaMessageInput:"Type your message",ariaUploadFile:"Upload file",ariaRemoveFile:"Remove file",ariaChatWindow:"Chat window",ariaMessageLog:"Chat messages"},id:{title:"Pindai Agent",placeholder:"Tulis pesan...",initialMessage:"Halo! Bagaimana saya bisa membantu Anda hari ini?",send:"Kirim",close:"Tutup",upload:"Unggah file",removeFile:"Hapus file",typingIndicator:"AI sedang mengetik...",sending:"Mengirim...",justNow:"Baru saja",minutesAgo:"{minutes}m yang lalu",offline:"Offline - pesan akan dikirim saat online",connectionRestored:"Koneksi kembali",connectionLost:"Tidak ada koneksi internet",errorGeneric:"Terjadi kesalahan. Silakan coba lagi.",errorTimeout:"Waktu permintaan habis. Silakan coba lagi.",errorNetwork:"Tidak ada koneksi internet. Periksa jaringan Anda.",errorServer:"Server sedang sibuk. Silakan coba lagi dalam beberapa saat.",errorRateLimit:"Terlalu banyak pesan. Silakan tunggu {seconds} detik.",errorInvalidResponse:"Respons server tidak valid. Silakan hubungi dukungan.",fileTypeNotSupported:"Jenis file tidak didukung: {filename}",fileTooLarge:"File terlalu besar: {filename} (maks {maxSize}MB)",maxFilesExceeded:"Maksimal {maxFiles} file diperbolehkan",quickReply1:"Bagaimana cara ekstraksi dokumen?",quickReply2:"Jenis file apa yang didukung?",quickReply3:"Tentang harga",quickReply4:"Hubungi dukungan",ariaOpenChat:"Buka widget chat",ariaCloseChat:"Tutup jendela chat",ariaSendMessage:"Kirim pesan",ariaMessageInput:"Ketik pesan Anda",ariaUploadFile:"Unggah file",ariaRemoveFile:"Hapus file",ariaChatWindow:"Jendela chat",ariaMessageLog:"Pesan chat"}};class d{constructor(e="id"){this.locale=this.isValidLocale(e)?e:"id"}isValidLocale(e){return Object.keys(r).includes(e)}t(e,t={}){var a;let i=((a=r[this.locale])==null?void 0:a[e])||r.en[e]||e;return Object.keys(t).forEach(s=>{const n=new RegExp(`\\{${s}\\}`,"g");i=i.replace(n,t[s])}),i}setLocale(e){return this.isValidLocale(e)?(this.locale=e,!0):(console.warn(`Invalid locale: ${e}. Keeping current locale: ${this.locale}`),!1)}getLocale(){return this.locale}getAvailableLocales(){return Object.keys(r)}}class o{constructor(e){const t=e.webhookUrl||e.n8nUrl;if(!t)throw new Error('PindaiChatWidget: "webhookUrl" option is required.');this.webhookUrl=t,this.mode=e.mode||"widget",this.locale=e.locale||"id",this.i18n=new d(this.locale),this.title=e.title||this.i18n.t("title"),this.initialMessage=e.initialMessage||this.i18n.t("initialMessage"),this.launcherIconUrl=e.launcherIconUrl||this.getDefaultIcon(),this.logoUrl=e.logoUrl||"https://pindai.ai/logo.png",this.showLogo=e.showLogo!==!1,this.launcherColor=e.launcherColor||"#0066FF",this.sendButtonColor=e.sendButtonColor||"#0066FF",this.accentColor=e.accentColor||"#00C896",this.enableFileUpload=e.enableFileUpload!==!1,this.allowedFileTypes=e.allowedFileTypes||["image/jpeg","image/png","image/gif","image/webp","application/pdf","application/msword","application/vnd.openxmlformats-officedocument.wordprocessingml.document","application/vnd.ms-excel","application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],this.maxFileSize=e.maxFileSize||10*1024*1024,this.maxFiles=e.maxFiles||5,this.uploadedFiles=[],this.enableNotifications=e.enableNotifications!==!1,this.enableSound=e.enableSound===!0,this.unreadCount=0,this.showQuickReplies=e.showQuickReplies!==!1,this.quickReplies=e.quickReplies||[this.i18n.t("quickReply1"),this.i18n.t("quickReply2"),this.i18n.t("quickReply3"),this.i18n.t("quickReply4")],this.enableHistory=e.enableHistory!==!1,this.maxHistoryItems=e.maxHistoryItems||50,this.historyKey=`pindai-chat-history-${this.webhookUrl}`,this.stateKey=`pindai-chat-state-${this.webhookUrl}`,this.maxRetries=e.maxRetries||3,this.retryDelay=e.retryDelay||1e3,this.requestTimeout=e.requestTimeout||3e4,this.rateLimit=e.rateLimit||5,this.rateLimitWindow=e.rateLimitWindow||6e4,this.messageTimes=[],this.container=null,this.launcher=null,this.chatWindow=null,this.messageList=null,this.input=null,this.button=null,this.closeButton=null,this.sessionId=`web-session-${Date.now()}-${Math.random()}`,this.isLoading=!1,this.isOpen=!1,this.isOnline=navigator.onLine,this.loadState(),this.setupOfflineDetection(),this.mode==="fullscreen"?this.initChatWindow():this.initLauncher()}initLauncher(){this.launcher=document.createElement("div"),this.launcher.className="n8n-chat-launcher",this.launcher.style.backgroundColor=this.launcherColor,this.launcher.setAttribute("role","button"),this.launcher.setAttribute("aria-label",this.i18n.t("ariaOpenChat")),this.launcher.setAttribute("tabindex","0"),this.launcher.innerHTML=`
2
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=`
@@ -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: 'Chat with AI',\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: 'Chat dengan AI',\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 // 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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pindai-ai/chat-widget",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
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",
package/src/i18n.js CHANGED
@@ -6,7 +6,7 @@
6
6
  export const translations = {
7
7
  en: {
8
8
  // Widget UI
9
- title: 'Chat with AI',
9
+ title: 'Pindai Agent',
10
10
  placeholder: 'Write a message...',
11
11
  initialMessage: 'Hello! How can I help you today?',
12
12
  send: 'Send',
@@ -57,7 +57,7 @@ export const translations = {
57
57
 
58
58
  id: {
59
59
  // Widget UI
60
- title: 'Chat dengan AI',
60
+ title: 'Pindai Agent',
61
61
  placeholder: 'Tulis pesan...',
62
62
  initialMessage: 'Halo! Bagaimana saya bisa membantu Anda hari ini?',
63
63
  send: 'Kirim',