@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 +21 -0
- package/README.md +485 -0
- package/dist/pindai-chat-widget.css +1 -0
- package/dist/pindai-chat-widget.js +54 -0
- package/dist/pindai-chat-widget.js.map +1 -0
- package/dist/vite.svg +1 -0
- package/package.json +61 -0
- package/src/counter.js +9 -0
- package/src/i18n.js +174 -0
- package/src/javascript.svg +1 -0
- package/src/main.js +796 -0
- package/src/style.css +747 -0
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
|
+

|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@pindai-ai/chat-widget)
|
|
8
|
+
[](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")}">×</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")}">×</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
|