@pindai-ai/chat-widget 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Gabriele Randazzo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,485 @@
1
+ # Pindai Chat Widget
2
+
3
+ > Modern, accessible chat widget for Pindai.ai - AI-powered document extraction for Indonesian enterprises
4
+
5
+ ![Demo](./images/1.gif)
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@pindai-ai/chat-widget.svg)](https://www.npmjs.com/package/@pindai-ai/chat-widget)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
9
+
10
+ ## ✨ Features
11
+
12
+ - 🎨 **Modern UI** - HubSpot-quality design with smooth animations and Pindai.ai branding
13
+ - ♿ **Accessible** - WCAG 2.2 AA compliant with full keyboard navigation and screen reader support
14
+ - 📱 **Mobile-First** - Optimized responsive design for all devices
15
+ - 🌐 **Bilingual** - Indonesian (default) and English localization
16
+ - 📎 **File Upload** - Support for PDFs, images, and documents
17
+ - 💾 **Persistent** - Message history and state saved to localStorage
18
+ - 🔔 **Notifications** - Unread message badges and optional sound alerts
19
+ - ⚡ **Quick Replies** - Suggested responses for common questions
20
+ - 🔄 **Retry Logic** - Automatic retry with exponential backoff on network errors
21
+ - 📡 **Offline Support** - Graceful offline detection and user messaging
22
+ - ⌨️ **Keyboard Nav** - Full keyboard support (Tab, Enter, ESC)
23
+ - 🎯 **Lightweight** - Only ~12KB gzipped (JS + CSS)
24
+ - 🔧 **Backend Agnostic** - Works with n8n, Dify, custom APIs, or any HTTP endpoint
25
+
26
+ ---
27
+
28
+ ## 📦 Installation
29
+
30
+ ### Via npm
31
+
32
+ ```bash
33
+ npm install @pindai-ai/chat-widget
34
+ ```
35
+
36
+ ### Via CDN (jsDelivr)
37
+
38
+ ```html
39
+ <link rel="stylesheet"
40
+ href="https://cdn.jsdelivr.net/npm/@pindai-ai/chat-widget@2/dist/pindai-chat-widget.css">
41
+ <script type="module"
42
+ src="https://cdn.jsdelivr.net/npm/@pindai-ai/chat-widget@2/dist/pindai-chat-widget.js"></script>
43
+ ```
44
+
45
+ ### Local Development
46
+
47
+ ```bash
48
+ # Clone repository
49
+ git clone https://github.com/pindai-ai/pindai-chat-widget.git
50
+ cd pindai-chat-widget
51
+
52
+ # Install dependencies
53
+ npm install
54
+
55
+ # Run development server
56
+ npm run dev
57
+
58
+ # Build for production
59
+ npm run build
60
+ ```
61
+
62
+ ---
63
+
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' // Change this to your backend URL
79
+ });
80
+ });
81
+ </script>
82
+ ```
83
+
84
+ That's it! Replace `your-backend.com/webhook/chat` with your actual endpoint URL.
85
+
86
+ ### Complete Example
87
+
88
+ ```html
89
+ <!DOCTYPE html>
90
+ <html lang="id">
91
+ <head>
92
+ <meta charset="UTF-8">
93
+ <title>Your Website</title>
94
+ </head>
95
+ <body>
96
+ <h1>Welcome to My Website</h1>
97
+ <p>Your content here...</p>
98
+
99
+ <!-- Pindai Chat Widget -->
100
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@pindai-ai/chat-widget@2/dist/pindai-chat-widget.css">
101
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@pindai-ai/chat-widget@2/dist/pindai-chat-widget.js"></script>
102
+ <script>
103
+ document.addEventListener('DOMContentLoaded', function () {
104
+ window.PindaiChatWidget.init({
105
+ webhookUrl: 'https://your-backend.com/webhook/chat',
106
+ title: 'Customer Support',
107
+ locale: 'id' // 'id' for Indonesian, 'en' for English
108
+ });
109
+ });
110
+ </script>
111
+ </body>
112
+ </html>
113
+ ```
114
+
115
+ ### Using During Development
116
+
117
+ If the widget isn't on npm yet, use the GitHub CDN:
118
+
119
+ ```html
120
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/YOUR-USERNAME/pindai-chat-widget@main/dist/pindai-chat-widget.css">
121
+ <script type="module" src="https://cdn.jsdelivr.net/gh/YOUR-USERNAME/pindai-chat-widget@main/dist/pindai-chat-widget.js"></script>
122
+ ```
123
+
124
+ Replace `YOUR-USERNAME` with your GitHub username
125
+
126
+ ---
127
+
128
+ ## ⚙️ Configuration Options
129
+
130
+ | Option | Type | Default | Description |
131
+ |--------|------|---------|-------------|
132
+ | **Required** |
133
+ | `webhookUrl` | string | - | **Required**. Your backend API endpoint (works with any service: n8n, Dify, custom API, etc.) |
134
+ | **Display** |
135
+ | `mode` | string | `'widget'` | Display mode: `'widget'` or `'fullscreen'` |
136
+ | `locale` | string | `'id'` | Language: `'id'` (Indonesian) or `'en'` (English) |
137
+ | `title` | string | Localized | Chat header title |
138
+ | `initialMessage` | string | Localized | First AI message |
139
+ | **Branding** |
140
+ | `logoUrl` | string | `'https://pindai.ai/logo.png'` | Header logo URL |
141
+ | `showLogo` | boolean | `true` | Show/hide logo |
142
+ | `launcherColor` | string | `'#0066FF'` | Launcher button background color |
143
+ | `launcherIconUrl` | string | Default icon | Custom launcher icon URL |
144
+ | `sendButtonColor` | string | `'#0066FF'` | Send button background color |
145
+ | `accentColor` | string | `'#00C896'` | Accent color for UI elements |
146
+ | **File Upload** |
147
+ | `enableFileUpload` | boolean | `true` | Enable file attachments |
148
+ | `allowedFileTypes` | array | See below | Accepted MIME types |
149
+ | `maxFileSize` | number | `10485760` | Max file size in bytes (10MB default) |
150
+ | `maxFiles` | number | `5` | Max files per message |
151
+ | **Features** |
152
+ | `enableNotifications` | boolean | `true` | Show unread badges |
153
+ | `enableSound` | boolean | `false` | Play notification sound |
154
+ | `enableHistory` | boolean | `true` | Persist message history |
155
+ | `maxHistoryItems` | number | `50` | Max messages to store |
156
+ | `showQuickReplies` | boolean | `true` | Show quick reply buttons |
157
+ | `quickReplies` | array | Localized | Custom suggested responses |
158
+ | **Technical** |
159
+ | `maxRetries` | number | `3` | API retry attempts |
160
+ | `retryDelay` | number | `1000` | Retry delay in ms |
161
+ | `requestTimeout` | number | `30000` | Request timeout in ms (30s) |
162
+ | `rateLimit` | number | `5` | Messages per minute |
163
+ | `rateLimitWindow` | number | `60000` | Rate limit window in ms |
164
+
165
+ ### Default Allowed File Types
166
+
167
+ ```javascript
168
+ [
169
+ 'image/jpeg', 'image/png', 'image/gif', 'image/webp',
170
+ 'application/pdf',
171
+ 'application/msword',
172
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
173
+ 'application/vnd.ms-excel',
174
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
175
+ ]
176
+ ```
177
+
178
+ ---
179
+
180
+ ## 🔌 Backend API Format
181
+
182
+ ### Request (from Widget)
183
+
184
+ The widget sends a POST request with FormData containing:
185
+
186
+ ```javascript
187
+ {
188
+ "sessionId": "web-session-1234567890-0.123",
189
+ "message": "User message text",
190
+ "file0": File, // Optional: uploaded files
191
+ "file1": File, // Optional: multiple files supported
192
+ // ...
193
+ }
194
+ ```
195
+
196
+ ### Response (from Backend)
197
+
198
+ Your backend should respond with JSON:
199
+
200
+ ```json
201
+ {
202
+ "response": "AI response text"
203
+ }
204
+ ```
205
+
206
+ ### Example Backend Implementations
207
+
208
+ <details>
209
+ <summary><strong>n8n Workflow</strong></summary>
210
+
211
+ 1. Add "Webhook" node (POST method)
212
+ 2. Add "Chat Trigger" or custom AI logic
213
+ 3. Return JSON with `response` field
214
+
215
+ ```json
216
+ {
217
+ "response": "{{ $json.aiResponse }}"
218
+ }
219
+ ```
220
+
221
+ </details>
222
+
223
+ <details>
224
+ <summary><strong>Express.js Server</strong></summary>
225
+
226
+ ```javascript
227
+ const express = require('express');
228
+ const app = express();
229
+
230
+ app.post('/webhook/chat', express.json(), async (req, res) => {
231
+ const { sessionId, message } = req.body;
232
+
233
+ // Your AI logic here
234
+ const aiResponse = await processWithAI(message);
235
+
236
+ res.json({ response: aiResponse });
237
+ });
238
+ ```
239
+
240
+ </details>
241
+
242
+ <details>
243
+ <summary><strong>Python Flask</strong></summary>
244
+
245
+ ```python
246
+ from flask import Flask, request, jsonify
247
+
248
+ app = Flask(__name__)
249
+
250
+ @app.route('/webhook/chat', methods=['POST'])
251
+ def chat():
252
+ data = request.get_json()
253
+ session_id = data.get('sessionId')
254
+ message = data.get('message')
255
+
256
+ # Your AI logic here
257
+ ai_response = process_with_ai(message)
258
+
259
+ return jsonify({'response': ai_response})
260
+ ```
261
+
262
+ </details>
263
+
264
+ ---
265
+
266
+ ## 🎨 Customization Examples
267
+
268
+ ### Custom Branding
269
+
270
+ ```javascript
271
+ window.PindaiChatWidget.init({
272
+ webhookUrl: 'https://your-backend.com/webhook/chat',
273
+ locale: 'id',
274
+ title: 'Bantuan Pelanggan',
275
+ logoUrl: 'https://yourcompany.com/logo.png',
276
+ launcherColor: '#FF5733',
277
+ sendButtonColor: '#4CAF50',
278
+ accentColor: '#FFC107',
279
+ });
280
+ ```
281
+
282
+ ### Custom Quick Replies
283
+
284
+ ```javascript
285
+ window.PindaiChatWidget.init({
286
+ webhookUrl: 'https://your-backend.com/webhook/chat',
287
+ quickReplies: [
288
+ 'Cara membuat akun?',
289
+ 'Lupa password',
290
+ 'Hubungi customer service',
291
+ 'Lihat demo produk'
292
+ ],
293
+ });
294
+ ```
295
+
296
+ ### Fullscreen Mode (for dedicated chat pages)
297
+
298
+ ```javascript
299
+ window.PindaiChatWidget.init({
300
+ webhookUrl: 'https://your-backend.com/webhook/chat',
301
+ mode: 'fullscreen', // Takes over entire page
302
+ title: 'Customer Support',
303
+ });
304
+ ```
305
+
306
+ ### Disable Features
307
+
308
+ ```javascript
309
+ window.PindaiChatWidget.init({
310
+ webhookUrl: 'https://your-backend.com/webhook/chat',
311
+ enableFileUpload: false, // Disable file uploads
312
+ enableHistory: false, // Don't save chat history
313
+ showQuickReplies: false, // Hide quick reply buttons
314
+ });
315
+ ```
316
+
317
+ ---
318
+
319
+ ## ♿ Accessibility
320
+
321
+ This widget meets **WCAG 2.2 Level AA** compliance:
322
+
323
+ ✅ Full keyboard navigation (Tab, Enter, ESC)
324
+ ✅ ARIA labels and landmarks
325
+ ✅ Screen reader compatible
326
+ ✅ 4.5:1 color contrast ratios
327
+ ✅ Focus indicators
328
+ ✅ Text resizable to 200%
329
+ ✅ Touch targets ≥ 44×44px
330
+
331
+ ### Keyboard Shortcuts
332
+
333
+ | Key | Action |
334
+ |-----|--------|
335
+ | `Tab` | Navigate between elements |
336
+ | `Enter` | Send message / Activate button |
337
+ | `ESC` | Close widget |
338
+ | `Space` | Activate launcher |
339
+
340
+ ---
341
+
342
+ ## 🌐 Browser Support
343
+
344
+ | Browser | Version |
345
+ |---------|---------|
346
+ | Chrome/Edge | 90+ |
347
+ | Firefox | 88+ |
348
+ | Safari | 14+ |
349
+ | iOS Safari | 14+ |
350
+ | Android Chrome | 90+ |
351
+
352
+ ---
353
+
354
+ ## 🔧 Development
355
+
356
+ ### Project Structure
357
+
358
+ ```
359
+ pindai-chat/
360
+ ├── src/
361
+ │ ├── main.js # Core widget logic
362
+ │ ├── style.css # All styles
363
+ │ └── i18n.js # Internationalization
364
+ ├── dist/ # Built assets
365
+ ├── images/ # Demo assets
366
+ ├── index.html # Demo page
367
+ ├── vite.config.js # Build configuration
368
+ ├── package.json # Package metadata
369
+ └── README.md # Documentation
370
+ ```
371
+
372
+ ### Build Commands
373
+
374
+ ```bash
375
+ # Development server with hot reload
376
+ npm run dev
377
+
378
+ # Production build
379
+ npm run build
380
+
381
+ # Preview production build
382
+ npm run preview
383
+ ```
384
+
385
+ ### Testing
386
+
387
+ 1. **Start dev server:**
388
+ ```bash
389
+ npm run dev
390
+ ```
391
+
392
+ 2. **Open browser:** `http://localhost:5173`
393
+
394
+ 3. **Test features:**
395
+ - Click launcher to open chat
396
+ - Send messages
397
+ - Upload files (PDF, images)
398
+ - Test quick replies
399
+ - Try keyboard navigation (Tab, ESC)
400
+ - Test offline mode (DevTools > Network > Offline)
401
+ - Check mobile responsiveness (DevTools > Device Toolbar)
402
+
403
+ 4. **Accessibility audit:**
404
+ - Open DevTools > Lighthouse
405
+ - Run Accessibility audit
406
+ - Should score 100
407
+
408
+ ---
409
+
410
+ ## 📝 Changelog
411
+
412
+ ### Version 2.0.0 (2026-02-05)
413
+
414
+ **Major Changes:**
415
+ - Complete UI/UX redesign with Pindai.ai branding
416
+ - Indonesian localization (default) with English support
417
+ - File upload capability (PDF, images, documents)
418
+ - WCAG 2.2 AA accessibility compliance
419
+ - Mobile-first responsive design
420
+ - Message history persistence
421
+ - Quick reply buttons
422
+ - Notification badges
423
+ - Enhanced error handling with retry logic
424
+ - Offline detection and user messaging
425
+ - Rate limiting
426
+ - Keyboard navigation (Tab, ESC)
427
+
428
+ **Technical:**
429
+ - Renamed from `N8nChatWidget` to `PindaiChatWidget`
430
+ - Added i18n system
431
+ - CSS variables for theming
432
+ - FormData for file uploads
433
+ - localStorage for history/state
434
+ - Backward compatibility maintained
435
+
436
+ **Breaking Changes:**
437
+ - Default locale changed to Indonesian (`'id'`)
438
+ - File uploads now use FormData instead of JSON
439
+ - Some CSS class names updated
440
+
441
+ ### Version 1.0.0
442
+
443
+ - Initial release
444
+ - Basic chat functionality
445
+ - n8n integration
446
+
447
+ ---
448
+
449
+ ## 🤝 Contributing
450
+
451
+ Contributions are welcome! Please:
452
+
453
+ 1. Fork the repository
454
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
455
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
456
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
457
+ 5. Open a Pull Request
458
+
459
+ ---
460
+
461
+ ## 📄 License
462
+
463
+ MIT © [Pindai.ai](https://pindai.ai)
464
+
465
+ ---
466
+
467
+ ## 🆘 Support
468
+
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)
471
+ - **Email:** support@pindai.ai
472
+ - **Website:** [https://pindai.ai](https://pindai.ai)
473
+
474
+ ---
475
+
476
+ ## 🙏 Acknowledgments
477
+
478
+ - Inspired by HubSpot chat widget design patterns
479
+ - Built with modern web standards
480
+ - Designed for Indonesian enterprises
481
+ - Powered by Pindai.ai's AI document extraction technology
482
+
483
+ ---
484
+
485
+ **Made with ❤️ by [Pindai.ai](https://pindai.ai) for Indonesian businesses**
@@ -0,0 +1 @@
1
+ :root{--pindai-primary: #0066FF;--pindai-primary-dark: #0052CC;--pindai-primary-light: #E6F0FF;--pindai-accent: #00C896;--pindai-accent-light: #E6FAF5;--pindai-gray-900: #1A1A1A;--pindai-gray-700: #4A4A4A;--pindai-gray-500: #9E9E9E;--pindai-gray-300: #E0E0E0;--pindai-gray-100: #F5F5F5;--pindai-gray-50: #FAFAFA;--pindai-error: #F44336;--pindai-success: #00C896;--pindai-warning: #FF9800;--pindai-info: #0066FF;--text-on-primary: #FFFFFF;--text-on-light: #1A1A1A;--text-on-dark: #FFFFFF;--space-xs: 4px;--space-sm: 8px;--space-md: 16px;--space-lg: 24px;--space-xl: 32px;--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Inter", Roboto, sans-serif;--font-size-xs: .6875rem;--font-size-sm: .875rem;--font-size-base: .9375rem;--font-size-lg: 1.125rem;--font-size-xl: 1.5rem;--transition-fast: .15s ease-in-out;--transition-base: .3s ease-in-out;--transition-slow: .5s ease-in-out;--shadow-sm: 0 2px 8px rgba(0, 0, 0, .1);--shadow-md: 0 4px 12px rgba(0, 0, 0, .15);--shadow-lg: 0 8px 24px rgba(0, 0, 0, .2);--radius-sm: 8px;--radius-md: 12px;--radius-lg: 16px;--radius-xl: 20px;--radius-full: 50%}*{box-sizing:border-box}.n8n-chat-launcher{position:fixed;bottom:16px;right:16px;width:56px;height:56px;background-color:var(--pindai-primary);border-radius:var(--radius-full);display:flex;justify-content:center;align-items:center;box-shadow:var(--shadow-lg);cursor:pointer;transition:transform var(--transition-base),opacity var(--transition-base),box-shadow var(--transition-base);z-index:9998;border:none;outline:none}.n8n-chat-launcher:hover{transform:scale(1.05);box-shadow:0 8px 28px #06f6}.n8n-chat-launcher:focus-visible{outline:3px solid var(--pindai-primary);outline-offset:4px}.n8n-chat-launcher img{width:28px;height:28px;filter:invert(1)}.n8n-chat-launcher--hidden{transform:scale(0);opacity:0;pointer-events:none}.n8n-chat-unread-badge{position:absolute;top:-4px;right:-4px;min-width:20px;height:20px;background-color:var(--pindai-error);color:var(--text-on-dark);font-size:12px;font-weight:700;border-radius:10px;display:flex;align-items:center;justify-content:center;padding:0 6px;box-shadow:0 2px 6px #0000004d;z-index:1}.n8n-chat-widget{position:fixed;bottom:0;right:0;width:100vw;height:100vh;max-height:100vh;border-radius:0;box-shadow:var(--shadow-lg);background-color:#fff;display:flex;flex-direction:column;overflow:hidden;font-family:var(--font-family);font-size:var(--font-size-base);transform:translateY(20px) scale(.95);opacity:0;visibility:hidden;transition:all var(--transition-base);z-index:9999}.n8n-chat-widget--open{transform:translateY(0) scale(1);opacity:1;visibility:visible}.n8n-chat-widget--fullscreen{width:100%;height:100%;bottom:0;right:0;border-radius:0;max-height:100vh;transform:none;opacity:1;visibility:visible}.n8n-chat-header{padding:var(--space-md);background-color:var(--pindai-gray-900);color:var(--text-on-dark);font-weight:700;font-size:var(--font-size-xl);letter-spacing:-.02em;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;border-bottom:1px solid rgba(255,255,255,.1)}.n8n-chat-header-content{display:flex;align-items:center;gap:12px;flex:1}.n8n-chat-logo{width:32px;height:32px;object-fit:contain;flex-shrink:0}.n8n-chat-title{font-size:var(--font-size-lg);flex:1}.n8n-chat-close-btn{background:none;border:none;color:var(--text-on-dark);font-size:32px;cursor:pointer;line-height:1;padding:0 8px;transition:opacity var(--transition-fast);flex-shrink:0}.n8n-chat-close-btn:hover{opacity:.8}.n8n-chat-close-btn:focus-visible{outline:2px solid var(--pindai-primary);outline-offset:2px;border-radius:4px}.n8n-chat-messages{flex-grow:1;padding:var(--space-lg);overflow-y:auto;display:flex;flex-direction:column;gap:var(--space-md);background-color:var(--pindai-gray-50);-webkit-overflow-scrolling:touch;overscroll-behavior:contain}.n8n-chat-messages::-webkit-scrollbar{width:6px}.n8n-chat-messages::-webkit-scrollbar-track{background:transparent}.n8n-chat-messages::-webkit-scrollbar-thumb{background:var(--pindai-gray-300);border-radius:3px}.n8n-chat-messages::-webkit-scrollbar-thumb:hover{background:var(--pindai-gray-500)}.n8n-chat-bubble{padding:12px 16px;border-radius:var(--radius-xl);max-width:75%;line-height:1.5;font-size:var(--font-size-base);word-wrap:break-word;animation:fadeInSlide .3s ease-out}@keyframes fadeInSlide{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.n8n-chat-user-message{background-color:var(--pindai-primary);color:var(--text-on-primary);align-self:flex-end;border-bottom-right-radius:4px}.n8n-chat-ai-message{background-color:var(--pindai-gray-100);color:var(--text-on-light);align-self:flex-start;border-bottom-left-radius:4px}.n8n-chat-message-text{margin-bottom:4px}.n8n-chat-message-timestamp{font-size:var(--font-size-xs);opacity:.6;margin-top:4px;display:block}.n8n-chat-user-message .n8n-chat-message-timestamp{text-align:right;color:#fffc}.n8n-chat-ai-message .n8n-chat-message-timestamp{color:#00000080}.n8n-chat-quick-replies{display:flex;flex-wrap:wrap;gap:var(--space-sm);padding:var(--space-md) 0;align-self:flex-start;max-width:90%;animation:fadeInSlide .4s ease-out}.n8n-chat-quick-reply-btn{padding:10px 16px;background:#fff;border:1.5px solid var(--pindai-primary);border-radius:var(--radius-xl);color:var(--pindai-primary);font-size:var(--font-size-sm);font-weight:500;cursor:pointer;transition:all var(--transition-fast);white-space:nowrap;min-height:44px}.n8n-chat-quick-reply-btn:hover{background:var(--pindai-primary);color:#fff;transform:translateY(-1px);box-shadow:0 2px 8px #0066ff4d}.n8n-chat-quick-reply-btn:focus-visible{outline:2px solid var(--pindai-primary);outline-offset:2px}.n8n-chat-typing-indicator{display:flex;align-items:center;gap:4px;padding:12px 16px}.n8n-chat-typing-indicator span{height:8px;width:8px;background-color:var(--pindai-gray-500);border-radius:50%;display:inline-block;animation:bounce 1.4s infinite ease-in-out both}.n8n-chat-typing-indicator span:nth-child(1){animation-delay:-.32s}.n8n-chat-typing-indicator span:nth-child(2){animation-delay:-.16s}@keyframes bounce{0%,80%,to{transform:scale(0)}40%{transform:scale(1)}}.n8n-chat-file-preview{padding:var(--space-sm) var(--space-md);display:flex;flex-wrap:wrap;gap:var(--space-sm);border-top:1px solid var(--pindai-gray-300);background:var(--pindai-gray-50)}.n8n-chat-file-item{display:flex;align-items:center;gap:var(--space-sm);padding:6px 12px;background:#fff;border:1px solid var(--pindai-gray-300);border-radius:var(--radius-lg);font-size:var(--font-size-sm);max-width:200px}.n8n-chat-file-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--pindai-gray-700)}.n8n-chat-file-remove{background:none;border:none;cursor:pointer;font-size:18px;line-height:1;color:var(--pindai-gray-500);padding:0;width:20px;height:20px;display:flex;align-items:center;justify-content:center;transition:color var(--transition-fast)}.n8n-chat-file-remove:hover{color:var(--pindai-error)}.n8n-chat-watermark{padding:8px 16px;text-align:center;font-size:12px;color:var(--pindai-gray-500);background-color:var(--pindai-gray-100);border-top:1px solid var(--pindai-gray-300);flex-shrink:0}.n8n-chat-watermark span{margin-right:4px}.n8n-chat-watermark a{color:var(--pindai-primary);text-decoration:none;font-weight:600;transition:color .2s ease}.n8n-chat-watermark a:hover{color:var(--pindai-primary-dark);text-decoration:underline}.n8n-chat-watermark a:focus-visible{outline:2px solid var(--pindai-primary);outline-offset:2px;border-radius:2px}.n8n-chat-input-area{display:flex;align-items:center;padding:var(--space-md);border-top:1px solid var(--pindai-gray-300);background-color:#fff;flex-shrink:0;gap:var(--space-sm)}.n8n-chat-file-upload-btn{width:44px;height:44px;display:flex;align-items:center;justify-content:center;cursor:pointer;border-radius:var(--radius-full);transition:background-color var(--transition-fast);flex-shrink:0;border:none;background:none;color:var(--pindai-gray-700)}.n8n-chat-file-upload-btn:hover{background-color:var(--pindai-gray-100)}.n8n-chat-file-upload-btn:focus-visible{outline:2px solid var(--pindai-primary);outline-offset:2px}.n8n-chat-input-area input{flex-grow:1;padding:12px 16px;border:1px solid var(--pindai-gray-300);border-radius:var(--radius-xl);font-size:16px;font-family:var(--font-family);outline:none;transition:border-color var(--transition-fast);min-height:44px}.n8n-chat-input-area input:focus{border-color:var(--pindai-primary);box-shadow:0 0 0 3px #0066ff1a}.n8n-chat-input-area input::placeholder{color:#757575}.n8n-chat-input-area button.n8n-chat-send-btn{width:44px;height:44px;padding:0;border:none;border-radius:var(--radius-full);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all var(--transition-fast);background-color:var(--send-button-color, var(--pindai-primary));color:#fff;flex-shrink:0}.n8n-chat-input-area button.n8n-chat-send-btn:hover:not(:disabled){transform:scale(1.05);filter:brightness(1.1);box-shadow:0 2px 8px #06f6}.n8n-chat-input-area button.n8n-chat-send-btn:focus-visible{outline:2px solid var(--pindai-primary);outline-offset:2px}.n8n-chat-input-area button svg{width:18px;height:18px;color:#fff}.n8n-chat-input-area button:disabled{background-color:var(--pindai-gray-300);cursor:not-allowed;transform:none;opacity:.6}.n8n-chat-offline-indicator{display:flex;align-items:center;gap:var(--space-sm);padding:var(--space-md);background-color:#fff3cd;color:#856404;font-size:var(--font-size-sm);border-bottom:1px solid #FFE69C;flex-shrink:0}.n8n-chat-offline-indicator svg{flex-shrink:0}.n8n-chat-loading-message{display:flex;align-items:center;gap:var(--space-sm);padding:var(--space-sm) 0;font-size:var(--font-size-sm);color:var(--pindai-gray-700)}.n8n-chat-spinner{width:16px;height:16px;border:2px solid var(--pindai-gray-300);border-top-color:var(--pindai-primary);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}button:focus-visible,input:focus-visible,a:focus-visible{outline:3px solid var(--pindai-primary);outline-offset:2px}@media (prefers-reduced-motion: reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}@media (min-width: 640px){.n8n-chat-widget{width:400px;height:85vh;max-height:700px;bottom:20px;right:20px;border-radius:var(--radius-lg)}.n8n-chat-launcher{bottom:20px;right:20px;width:60px;height:60px}.n8n-chat-launcher img{width:32px;height:32px}}@media (min-width: 1024px){.n8n-chat-widget{width:420px;height:80vh}.n8n-chat-messages{padding:var(--space-xl)}}@supports (padding: env(safe-area-inset-bottom)){.n8n-chat-widget{padding-bottom:env(safe-area-inset-bottom)}.n8n-chat-launcher{bottom:calc(16px + env(safe-area-inset-bottom))}@media (min-width: 640px){.n8n-chat-launcher{bottom:calc(20px + env(safe-area-inset-bottom))}}}
@@ -0,0 +1,54 @@
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=`
2
+ <img src="${this.launcherIconUrl}" alt="">
3
+ <span class="n8n-chat-unread-badge" style="display: none;">0</span>
4
+ `,document.body.appendChild(this.launcher),this.launcher.addEventListener("click",()=>this.toggleChatWindow()),this.launcher.addEventListener("keydown",e=>{(e.key==="Enter"||e.key===" ")&&(e.preventDefault(),this.toggleChatWindow())})}initChatWindow(){this.container=document.createElement("div"),this.container.className=`n8n-chat-widget ${this.mode==="fullscreen"?"n8n-chat-widget--fullscreen":""}`,this.container.setAttribute("role","dialog"),this.container.setAttribute("aria-modal","true"),this.container.setAttribute("aria-label",this.title),this.container.innerHTML=`
5
+ <div class="n8n-chat-header">
6
+ <div class="n8n-chat-header-content">
7
+ ${this.showLogo?`<img src="${this.logoUrl}" alt="Pindai Logo" class="n8n-chat-logo">`:""}
8
+ <span class="n8n-chat-title">${this.title}</span>
9
+ </div>
10
+ <button class="n8n-chat-close-btn" aria-label="${this.i18n.t("ariaCloseChat")}">&times;</button>
11
+ </div>
12
+ <div class="n8n-chat-messages" role="log" aria-live="polite" aria-atomic="false"></div>
13
+ <div class="n8n-chat-watermark">
14
+ <span>Powered by</span>
15
+ <a href="https://pindai.ai" target="_blank" rel="noopener noreferrer">Pindai.ai</a>
16
+ </div>
17
+ <div class="n8n-chat-input-area">
18
+ ${this.enableFileUpload?`
19
+ <label class="n8n-chat-file-upload-btn" aria-label="${this.i18n.t("ariaUploadFile")}">
20
+ <input type="file" multiple accept="${this.allowedFileTypes.join(",")}" hidden>
21
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
22
+ <path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/>
23
+ </svg>
24
+ </label>
25
+ `:""}
26
+ <input type="text" placeholder="${this.i18n.t("placeholder")}" aria-label="${this.i18n.t("ariaMessageInput")}" />
27
+ <button class="n8n-chat-send-btn" style="background-color: ${this.sendButtonColor}" aria-label="${this.i18n.t("ariaSendMessage")}">
28
+ <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">
29
+ <line x1="22" y1="2" x2="11" y2="13"></line>
30
+ <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
31
+ </svg>
32
+ </button>
33
+ </div>
34
+ ${this.enableFileUpload?'<div class="n8n-chat-file-preview" style="display: none;"></div>':""}
35
+ `,this.mode==="widget"?document.body.appendChild(this.container):(document.body.innerHTML="",document.body.appendChild(this.container),document.body.style.margin="0"),this.messageList=this.container.querySelector(".n8n-chat-messages"),this.input=this.container.querySelector('input[type="text"]'),this.button=this.container.querySelector(".n8n-chat-send-btn"),this.closeButton=this.container.querySelector(".n8n-chat-close-btn"),this.button.addEventListener("click",e=>{e.preventDefault(),this.sendMessage()}),this.input.addEventListener("keypress",e=>{e.key==="Enter"&&(e.preventDefault(),this.sendMessage())}),this.mode==="fullscreen"?this.closeButton.style.display="none":this.closeButton.addEventListener("click",()=>this.toggleChatWindow()),this.enableFileUpload&&this.container.querySelector('input[type="file"]').addEventListener("change",t=>this.handleFileSelect(t)),this.setupKeyboardNavigation(),this.loadHistory(),this.messageList.children.length===0&&this.addMessage(this.initialMessage,"ai")}toggleChatWindow(){this.isOpen?(this.container.classList.remove("n8n-chat-widget--open"),this.launcher&&this.launcher.classList.remove("n8n-chat-launcher--hidden")):(this.container||this.initChatWindow(),setTimeout(()=>{this.container.classList.add("n8n-chat-widget--open"),this.launcher&&this.launcher.classList.add("n8n-chat-launcher--hidden"),this.input.focus(),this.clearUnreadCount()},10)),this.isOpen=!this.isOpen,this.saveState()}getDefaultIcon(){return`data:image/svg+xml;charset=UTF-8,${encodeURIComponent(`
36
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
37
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
38
+ </svg>
39
+ `)}`}formatTimestamp(e){const i=new Date-e;if(i<6e4)return this.i18n.t("justNow");if(i<36e5){const a=Math.floor(i/6e4);return this.i18n.t("minutesAgo",{minutes:a})}return e.toLocaleTimeString(this.locale==="id"?"id-ID":"en-US",{hour:"2-digit",minute:"2-digit"})}addMessage(e,t,i=new Date){const a=document.createElement("div");a.className=`n8n-chat-bubble n8n-chat-${t}-message`;const s=document.createElement("div");s.className="n8n-chat-message-text",s.textContent=e;const n=document.createElement("div");n.className="n8n-chat-message-timestamp",n.textContent=this.formatTimestamp(i),n.setAttribute("data-timestamp",i.toISOString()),a.appendChild(s),a.appendChild(n),this.messageList.appendChild(a),this.messageList.scrollTop=this.messageList.scrollHeight,this.saveToHistory(e,t,i),!this.isOpen&&t==="ai"&&this.incrementUnread()}showTypingIndicator(e){let t=this.messageList.querySelector(".n8n-chat-typing-indicator");e?t||(t=document.createElement("div"),t.className="n8n-chat-bubble n8n-chat-ai-message n8n-chat-typing-indicator",t.innerHTML="<span></span><span></span><span></span>",t.setAttribute("aria-label",this.i18n.t("typingIndicator")),this.messageList.appendChild(t),this.messageList.scrollTop=this.messageList.scrollHeight):t&&t.remove()}handleFileSelect(e){Array.from(e.target.files).forEach(i=>{if(!this.allowedFileTypes.includes(i.type)){this.addMessage(this.i18n.t("fileTypeNotSupported",{filename:i.name}),"ai");return}if(i.size>this.maxFileSize){const a=this.maxFileSize/1024/1024;this.addMessage(this.i18n.t("fileTooLarge",{filename:i.name,maxSize:a}),"ai");return}if(this.uploadedFiles.length>=this.maxFiles){this.addMessage(this.i18n.t("maxFilesExceeded",{maxFiles:this.maxFiles}),"ai");return}this.uploadedFiles.push(i),this.renderFilePreview(i)}),e.target.value=""}renderFilePreview(e){const t=this.container.querySelector(".n8n-chat-file-preview");if(!t)return;t.style.display="flex";const i=document.createElement("div");i.className="n8n-chat-file-item",i.innerHTML=`
40
+ <span class="n8n-chat-file-name">${e.name}</span>
41
+ <button class="n8n-chat-file-remove" data-file="${e.name}" aria-label="${this.i18n.t("ariaRemoveFile")}">&times;</button>
42
+ `,i.querySelector(".n8n-chat-file-remove").addEventListener("click",()=>{this.uploadedFiles=this.uploadedFiles.filter(s=>s.name!==e.name),i.remove(),this.uploadedFiles.length===0&&(t.style.display="none")}),t.appendChild(i)}sendMessage(){return m(this,null,function*(){const e=this.input.value.trim();if(!(!e&&this.uploadedFiles.length===0||this.isLoading)){try{this.checkRateLimit()}catch(t){this.addMessage(t.message,"ai");return}if(!this.isOnline){this.addMessage(this.i18n.t("connectionLost"),"ai");return}this.isLoading=!0,this.button.disabled=!0,this.input.disabled=!0,e&&this.addMessage(e,"user"),this.input.value="",this.showTypingIndicator(!0);try{const t=yield this.sendMessageWithRetry(e,this.uploadedFiles);this.addMessage(t,"ai"),this.showQuickReplies&&this.quickReplies.length>0&&this.renderQuickReplies()}catch(t){const i=this.getErrorMessage(t);this.addMessage(i,"ai")}finally{if(this.isLoading=!1,this.button.disabled=!1,this.input.disabled=!1,this.showTypingIndicator(!1),this.input.focus(),this.uploadedFiles.length>0){this.uploadedFiles=[];const t=this.container.querySelector(".n8n-chat-file-preview");t&&(t.innerHTML="",t.style.display="none")}}}})}sendMessageWithRetry(a){return m(this,arguments,function*(e,t=[],i=0){try{const s=new AbortController,n=setTimeout(()=>s.abort(),this.requestTimeout),c=new FormData;c.append("sessionId",this.sessionId),c.append("message",e),t.forEach((u,p)=>{c.append(`file${p}`,u)});const h=yield fetch(this.webhookUrl,{method:"POST",body:c,signal:s.signal});if(clearTimeout(n),!h.ok){if(h.status>=500&&i<this.maxRetries)return yield this.delay(this.retryDelay*(i+1)),this.sendMessageWithRetry(e,t,i+1);const u=yield h.json().catch(()=>({}));throw new Error(u.message||`Network error: ${h.statusText}`)}const g=yield h.json();if(!g.response)throw new Error(this.i18n.t("errorInvalidResponse"));return g.response}catch(s){if(s.name==="AbortError")throw new Error(this.i18n.t("errorTimeout"));if(s.message.includes("NetworkError")&&i<this.maxRetries)return yield this.delay(this.retryDelay*(i+1)),this.sendMessageWithRetry(e,t,i+1);throw s}})}delay(e){return new Promise(t=>setTimeout(t,e))}getErrorMessage(e){return e.message.includes("timeout")||e.message.includes("Timeout")?this.i18n.t("errorTimeout"):e.message.includes("NetworkError")||e.message.includes("Failed to fetch")?this.i18n.t("errorNetwork"):e.message.includes("500")||e.message.includes("503")?this.i18n.t("errorServer"):this.i18n.t("errorGeneric")}checkRateLimit(){const e=Date.now();if(this.messageTimes=this.messageTimes.filter(t=>e-t<this.rateLimitWindow),this.messageTimes.length>=this.rateLimit){const t=this.messageTimes[0],i=Math.ceil((this.rateLimitWindow-(e-t))/1e3);throw new Error(this.i18n.t("errorRateLimit",{seconds:i}))}this.messageTimes.push(e)}renderQuickReplies(e=this.quickReplies){if(!this.showQuickReplies||e.length===0)return;const t=this.messageList.querySelector(".n8n-chat-quick-replies");t&&t.remove();const i=document.createElement("div");i.className="n8n-chat-quick-replies",e.forEach(a=>{const s=document.createElement("button");s.className="n8n-chat-quick-reply-btn",s.textContent=a,s.addEventListener("click",()=>{this.input.value=a,this.sendMessage(),i.remove()}),i.appendChild(s)}),this.messageList.appendChild(i),this.messageList.scrollTop=this.messageList.scrollHeight}incrementUnread(){this.isOpen||(this.unreadCount++,this.updateUnreadBadge())}updateUnreadBadge(){if(!this.launcher)return;const e=this.launcher.querySelector(".n8n-chat-unread-badge");e&&(e.textContent=this.unreadCount,e.style.display=this.unreadCount>0?"flex":"none")}clearUnreadCount(){this.unreadCount=0,this.updateUnreadBadge()}loadHistory(){if(this.enableHistory)try{const e=localStorage.getItem(this.historyKey);if(!e)return;JSON.parse(e).forEach(i=>{this.addMessageWithoutSaving(i.text,i.sender,new Date(i.timestamp))})}catch(e){console.warn("Failed to load chat history:",e)}}addMessageWithoutSaving(e,t,i){const a=document.createElement("div");a.className=`n8n-chat-bubble n8n-chat-${t}-message`;const s=document.createElement("div");s.className="n8n-chat-message-text",s.textContent=e;const n=document.createElement("div");n.className="n8n-chat-message-timestamp",n.textContent=this.formatTimestamp(i),n.setAttribute("data-timestamp",i.toISOString()),a.appendChild(s),a.appendChild(n),this.messageList.appendChild(a),this.messageList.scrollTop=this.messageList.scrollHeight}saveToHistory(e,t,i=new Date){if(this.enableHistory)try{const a=localStorage.getItem(this.historyKey);let s=a?JSON.parse(a):[];s.push({text:e,sender:t,timestamp:i.toISOString()}),s.length>this.maxHistoryItems&&(s=s.slice(-this.maxHistoryItems)),localStorage.setItem(this.historyKey,JSON.stringify(s))}catch(a){console.warn("Failed to save chat history:",a)}}loadState(){try{const e=localStorage.getItem(this.stateKey)}catch(e){console.warn("Failed to load chat state:",e)}}saveState(){try{localStorage.setItem(this.stateKey,JSON.stringify({isOpen:this.isOpen,timestamp:new Date().toISOString()}))}catch(e){console.warn("Failed to save chat state:",e)}}setupOfflineDetection(){window.addEventListener("online",()=>{this.isOnline=!0,this.updateOnlineStatus()}),window.addEventListener("offline",()=>{this.isOnline=!1,this.updateOnlineStatus()})}updateOnlineStatus(){if(!this.container)return;const e=this.container.querySelector(".n8n-chat-offline-indicator");if(!this.isOnline&&!e){const t=document.createElement("div");t.className="n8n-chat-offline-indicator",t.innerHTML=`
43
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
44
+ <line x1="1" y1="1" x2="23" y2="23"></line>
45
+ <path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"></path>
46
+ <path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"></path>
47
+ <path d="M10.71 5.05A16 16 0 0 1 22.58 9"></path>
48
+ <path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"></path>
49
+ <path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path>
50
+ <line x1="12" y1="20" x2="12.01" y2="20"></line>
51
+ </svg>
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});
54
+ //# sourceMappingURL=pindai-chat-widget.js.map