wakz-chat-widget 1.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.
Files changed (3) hide show
  1. package/README.md +44 -0
  2. package/index.js +1285 -0
  3. package/package.json +31 -0
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # wakz-chat-widget
2
+
3
+ Production-grade AI chat widget by **WAKZ** — Shadow DOM isolated, zero dependencies, Intercom/Crisp quality.
4
+
5
+ ## Installation
6
+
7
+ No installation needed. Add this script tag to your website:
8
+
9
+ ```html
10
+ <!-- WAKZ Chat Widget -->
11
+ <script
12
+ src="https://unpkg.com/wakz-chat-widget@latest/index.js"
13
+ data-api-key="YOUR_API_KEY"
14
+ data-server="https://your-wakz-server.com"
15
+ async>
16
+ </script>
17
+ ```
18
+
19
+ Replace:
20
+ - `YOUR_API_KEY` — Your WAKZ embed API key
21
+ - `https://your-wakz-server.com` — Your WAKZ server URL
22
+
23
+ ## Features
24
+
25
+ - **Shadow DOM** — Fully isolated from your website's CSS
26
+ - **Zero Dependencies** — Pure vanilla JavaScript
27
+ - **RTL Support** — Full Arabic/Hebrew right-to-left support
28
+ - **Dynamic Theming** — Colors, position, bot name, welcome message configurable from dashboard
29
+ - **Responsive** — Full-screen on mobile, floating window on desktop
30
+ - **Real-time Chat** — AI-powered responses via WAKZ backend
31
+ - **Typing Indicator** — Smooth animated dots while bot is thinking
32
+ - **Session Persistence** — Chat history preserved across page reloads
33
+ - **Online/Offline Status** — Green/red dot with pulse animation
34
+
35
+ ## CDN
36
+
37
+ ```html
38
+ https://unpkg.com/wakz-chat-widget@latest/index.js
39
+ https://cdn.jsdelivr.net/npm/wakz-chat-widget@latest/index.js
40
+ ```
41
+
42
+ ## License
43
+
44
+ MIT
package/index.js ADDED
@@ -0,0 +1,1285 @@
1
+ /**
2
+ * WAKZ Chat Widget v2.0.0
3
+ * ─────────────────────────────────────────────────────────────────
4
+ * A production-grade, self-contained chat widget using Shadow DOM.
5
+ * Competes with Intercom & Crisp in quality.
6
+ *
7
+ * Embed: <script src="/wakz-widget.js" data-api-key="xxx" data-server="https://..." async></script>
8
+ *
9
+ * ZERO external dependencies — pure vanilla JavaScript.
10
+ */
11
+ (function () {
12
+ 'use strict';
13
+
14
+ /* ════════════════════════════════════════════════════════════════
15
+ UTILITY HELPERS
16
+ ════════════════════════════════════════════════════════════════ */
17
+
18
+ /** Generate a UUID v4 compliant string */
19
+ function _uuid() {
20
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
21
+ var r = (Math.random() * 16) | 0;
22
+ var v = c === 'x' ? r : (r & 0x3) | 0x8;
23
+ return v.toString(16);
24
+ });
25
+ }
26
+
27
+ /** Get or create a persistent visitor ID stored in localStorage */
28
+ function _getVisitorId() {
29
+ var KEY = 'wakz_visitor_id';
30
+ var id = null;
31
+ try { id = localStorage.getItem(KEY); } catch (e) { /* ignore */ }
32
+ if (!id) {
33
+ id = _uuid();
34
+ try { localStorage.setItem(KEY, id); } catch (e) { /* ignore */ }
35
+ }
36
+ return id;
37
+ }
38
+
39
+ /** Locate our own <script> tag and read its data attributes */
40
+ function _getScriptAttrs() {
41
+ var scripts = document.querySelectorAll('script[data-api-key]');
42
+ var target = null;
43
+ for (var i = 0; i < scripts.length; i++) {
44
+ var src = scripts[i].getAttribute('src') || '';
45
+ if (src.indexOf('wakz-widget') !== -1) {
46
+ target = scripts[i];
47
+ break;
48
+ }
49
+ }
50
+ if (!target && scripts.length > 0) target = scripts[scripts.length - 1];
51
+ return {
52
+ apiKey: (target && target.getAttribute('data-api-key')) || '',
53
+ server: (target && target.getAttribute('data-server')) || ''
54
+ };
55
+ }
56
+
57
+ /** Convenience DOM element creator */
58
+ function _el(tag, attrs, children) {
59
+ var el = document.createElement(tag);
60
+ if (attrs) {
61
+ Object.keys(attrs).forEach(function (k) {
62
+ var v = attrs[k];
63
+ if (k === 'className') { el.className = v; }
64
+ else if (k === 'style' && typeof v === 'object') {
65
+ Object.keys(v).forEach(function (s) { el.style.setProperty(s, v[s]); });
66
+ }
67
+ else if (k.indexOf('on') === 0 && typeof v === 'function') {
68
+ el.addEventListener(k.slice(2).toLowerCase(), v);
69
+ }
70
+ else { el.setAttribute(k, v); }
71
+ });
72
+ }
73
+ if (children) {
74
+ (Array.isArray(children) ? children : [children]).forEach(function (c) {
75
+ if (c === null || c === undefined) return;
76
+ el.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
77
+ });
78
+ }
79
+ return el;
80
+ }
81
+
82
+ /** Fetch with an AbortController timeout (default 30s) */
83
+ function _fetchWithTimeout(url, options, timeoutMs) {
84
+ var controller = new AbortController();
85
+ var merged = Object.assign({}, options, { signal: controller.signal });
86
+ var timer = setTimeout(function () { controller.abort(); }, timeoutMs || 30000);
87
+ return fetch(url, merged).finally(function () { clearTimeout(timer); });
88
+ }
89
+
90
+ /* ════════════════════════════════════════════════════════════════
91
+ SVG ICONS (inline for zero-dependency)
92
+ ════════════════════════════════════════════════════════════════ */
93
+
94
+ var _ICONS = {
95
+ chatBubble:
96
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>',
97
+ send:
98
+ '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>',
99
+ close:
100
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
101
+ error:
102
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>'
103
+ };
104
+
105
+ /* ════════════════════════════════════════════════════════════════
106
+ LOCALIZATION STRINGS
107
+ ════════════════════════════════════════════════════════════════ */
108
+
109
+ var _I18N = {
110
+ ar: {
111
+ placeholder: 'اكتب رسالتك...',
112
+ online: 'متصل',
113
+ offline: 'غير متصل',
114
+ errorMsg: 'حدث خطأ في الاتصال. يرجى المحاولة مرة أخرى.',
115
+ retry: 'إعادة المحاولة',
116
+ configError: 'تعذر تحميل إعدادات المحادثة.',
117
+ openChat: 'فتح المحادثة',
118
+ closeChat: 'إغلاق المحادثة',
119
+ sendMessage: 'إرسال'
120
+ },
121
+ en: {
122
+ placeholder: 'Type your message...',
123
+ online: 'Online',
124
+ offline: 'Offline',
125
+ errorMsg: 'Something went wrong. Please try again.',
126
+ retry: 'Retry',
127
+ configError: 'Could not load chat settings.',
128
+ openChat: 'Open chat',
129
+ closeChat: 'Close chat',
130
+ sendMessage: 'Send'
131
+ },
132
+ fr: {
133
+ placeholder: 'Tapez votre message...',
134
+ online: 'En ligne',
135
+ offline: 'Hors ligne',
136
+ errorMsg: 'Une erreur est survenue. Veuillez réessayer.',
137
+ retry: 'Réessayer',
138
+ configError: 'Impossible de charger les paramètres.',
139
+ openChat: 'Ouvrir le chat',
140
+ closeChat: 'Fermer le chat',
141
+ sendMessage: 'Envoyer'
142
+ }
143
+ };
144
+
145
+ function _strings(lang) {
146
+ return _I18N[lang] || _I18N['en'];
147
+ }
148
+
149
+ /* ════════════════════════════════════════════════════════════════
150
+ DEFAULT CONFIGURATION
151
+ ════════════════════════════════════════════════════════════════ */
152
+
153
+ var _DEFAULTS = {
154
+ botName: 'WAKZ',
155
+ welcomeMessage: '',
156
+ primaryColor: '#171717',
157
+ chatBg: '#f8f9fa',
158
+ btnColor: '#171717',
159
+ widgetBg: '#ffffff',
160
+ position: 'bottom-right',
161
+ language: 'en',
162
+ showStatus: true,
163
+ online: true
164
+ };
165
+
166
+ /* ════════════════════════════════════════════════════════════════
167
+ WAKZ WIDGET — MAIN CLASS
168
+ ════════════════════════════════════════════════════════════════ */
169
+
170
+ /** Singleton guard — only one widget per page */
171
+ if (window.__wakz_widget_initialized) return;
172
+ window.__wakz_widget_initialized = true;
173
+
174
+ function WAKZWidget() {
175
+ var self = this;
176
+
177
+ /* ── Script attributes ── */
178
+ var attrs = _getScriptAttrs();
179
+ self.apiKey = attrs.apiKey;
180
+ self.server = attrs.server;
181
+ self.visitorId = _getVisitorId();
182
+
183
+ /* ── Runtime state ── */
184
+ self.config = Object.assign({}, _DEFAULTS);
185
+ self.isOpen = false;
186
+ self.isLoading = false;
187
+ self.messages = [];
188
+ self._hasFetchedHistory = false;
189
+ self._typingEl = null;
190
+ self._configLoaded = false;
191
+ self._configError = false;
192
+
193
+ /* ── DOM refs (populated after mount) ── */
194
+ self._host = null;
195
+ self._shadow = null;
196
+ self._root = null;
197
+ self._chatWindow = null;
198
+ self._messagesContainer = null;
199
+ self._inputEl = null;
200
+ self._sendBtn = null;
201
+ self._toggleBtn = null;
202
+ self._statusDot = null;
203
+ self._headerStatusDot = null;
204
+ self._headerStatusText = null;
205
+
206
+ /* ── Bootstrap ── */
207
+ self._injectCSS();
208
+ self._createDOM();
209
+ self._attachEvents();
210
+ self._fetchConfig();
211
+ self._playFabEntrance();
212
+ }
213
+
214
+ /* ════════════════════════════════════════════════════════════════
215
+ CSS — Complete styling injected into Shadow DOM
216
+ ════════════════════════════════════════════════════════════════ */
217
+
218
+ WAKZWidget.prototype._injectCSS = function () {
219
+ var self = this;
220
+ self._styleEl = _el('style');
221
+
222
+ self._styleEl.textContent = [
223
+ /* ── CSS Custom Properties (theming) ── */
224
+ ':host {',
225
+ ' --wakz-primary: #171717;',
226
+ ' --wakz-btn: #171717;',
227
+ ' --wakz-chat-bg: #f8f9fa;',
228
+ ' --wakz-widget-bg: #ffffff;',
229
+ ' --wakz-radius-window: 16px;',
230
+ ' --wakz-radius-bubble: 16px;',
231
+ ' --wakz-radius-fab: 28px;',
232
+ ' --wakz-shadow-sm: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06);',
233
+ ' --wakz-shadow-md: 0 4px 12px rgba(0,0,0,.1), 0 2px 4px rgba(0,0,0,.06);',
234
+ ' --wakz-shadow-lg: 0 12px 40px rgba(0,0,0,.15), 0 4px 12px rgba(0,0,0,.1);',
235
+ ' --wakz-shadow-xl: 0 20px 60px rgba(0,0,0,.2), 0 8px 20px rgba(0,0,0,.12);',
236
+ ' --wakz-transition: 200ms ease;',
237
+ ' --wakz-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;',
238
+ ' all: initial;',
239
+ ' font-family: var(--wakz-font);',
240
+ '}',
241
+
242
+ /* ── Reset ── */
243
+ '.wakz *, .wakz *::before, .wakz *::after {',
244
+ ' box-sizing: border-box;',
245
+ ' margin: 0;',
246
+ ' padding: 0;',
247
+ '}',
248
+
249
+ /* ══════════════════════════════════════════════════
250
+ FLOATING ACTION BUTTON (FAB)
251
+ ══════════════════════════════════════════════════ */
252
+ '.wakz-fab {',
253
+ ' position: fixed;',
254
+ ' z-index: 2147483647;',
255
+ ' width: 56px;',
256
+ ' height: 56px;',
257
+ ' border-radius: var(--wakz-radius-fab);',
258
+ ' border: none;',
259
+ ' cursor: pointer;',
260
+ ' display: flex;',
261
+ ' align-items: center;',
262
+ ' justify-content: center;',
263
+ ' box-shadow: var(--wakz-shadow-lg);',
264
+ ' transition: transform var(--wakz-transition), box-shadow var(--wakz-transition);',
265
+ ' outline: none;',
266
+ ' -webkit-tap-highlight-color: transparent;',
267
+ ' background: var(--wakz-btn);',
268
+ '}',
269
+ '.wakz-fab:hover {',
270
+ ' transform: scale(1.08);',
271
+ ' box-shadow: var(--wakz-shadow-xl);',
272
+ '}',
273
+ '.wakz-fab:active {',
274
+ ' transform: scale(0.96);',
275
+ '}',
276
+ '.wakz-fab svg {',
277
+ ' width: 26px;',
278
+ ' height: 26px;',
279
+ ' color: #ffffff;',
280
+ ' pointer-events: none;',
281
+ '}',
282
+
283
+ /* ── FAB Positioning ── */
284
+ '.wakz-fab-pos-br { bottom: 24px; right: 24px; }',
285
+ '.wakz-fab-pos-bl { bottom: 24px; left: 24px; }',
286
+
287
+ /* ── FAB Entrance Bounce ── */
288
+ '@keyframes wakz-fab-enter {',
289
+ ' 0% { opacity: 0; transform: scale(0.3); }',
290
+ ' 50% { opacity: 1; transform: scale(1.12); }',
291
+ ' 70% { transform: scale(0.92); }',
292
+ ' 100% { transform: scale(1); }',
293
+ '}',
294
+ '.wakz-fab-enter {',
295
+ ' animation: wakz-fab-enter 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;',
296
+ '}',
297
+
298
+ /* ══════════════════════════════════════════════════
299
+ STATUS DOT (on FAB)
300
+ ══════════════════════════════════════════════════ */
301
+ '.wakz-fab-dot {',
302
+ ' position: absolute;',
303
+ ' top: 0px;',
304
+ ' right: 0px;',
305
+ ' width: 14px;',
306
+ ' height: 14px;',
307
+ ' border-radius: 50%;',
308
+ ' border: 2.5px solid #ffffff;',
309
+ ' z-index: 2;',
310
+ ' transition: background 0.3s ease;',
311
+ '}',
312
+ '.wakz-fab-pos-bl .wakz-fab-dot {',
313
+ ' right: auto;',
314
+ ' left: 0px;',
315
+ '}',
316
+
317
+ /* ── Online (green pulse) ── */
318
+ '.wakz-fab-dot.online {',
319
+ ' background: #22c55e;',
320
+ '}',
321
+ '@keyframes wakz-dot-pulse {',
322
+ ' 0%, 100% { box-shadow: 0 0 0 0 rgba(34,197,94,0.5); }',
323
+ ' 50% { box-shadow: 0 0 0 6px rgba(34,197,94,0); }',
324
+ '}',
325
+ '.wakz-fab-dot.online {',
326
+ ' animation: wakz-dot-pulse 2s ease-in-out infinite;',
327
+ '}',
328
+
329
+ /* ── Offline (red, no pulse) ── */
330
+ '.wakz-fab-dot.offline {',
331
+ ' background: #ef4444;',
332
+ ' animation: none;',
333
+ '}',
334
+
335
+ /* ── Error (red, no pulse) ── */
336
+ '.wakz-fab-dot.error {',
337
+ ' background: #ef4444;',
338
+ ' animation: none;',
339
+ '}',
340
+
341
+ /* ══════════════════════════════════════════════════
342
+ CHAT WINDOW
343
+ ══════════════════════════════════════════════════ */
344
+ '.wakz-window {',
345
+ ' position: fixed;',
346
+ ' z-index: 2147483646;',
347
+ ' width: 380px;',
348
+ ' min-height: 500px;',
349
+ ' max-height: 70vh;',
350
+ ' border-radius: var(--wakz-radius-window);',
351
+ ' overflow: hidden;',
352
+ ' display: flex;',
353
+ ' flex-direction: column;',
354
+ ' background: var(--wakz-widget-bg);',
355
+ ' box-shadow: var(--wakz-shadow-xl);',
356
+ ' opacity: 0;',
357
+ ' transform: translateY(20px) scale(0.95);',
358
+ ' pointer-events: none;',
359
+ ' transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),',
360
+ ' transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);',
361
+ '}',
362
+ '.wakz-window.wakz-visible {',
363
+ ' opacity: 1;',
364
+ ' transform: translateY(0) scale(1);',
365
+ ' pointer-events: auto;',
366
+ '}',
367
+
368
+ /* ── Window Positioning ── */
369
+ '.wakz-win-pos-br { bottom: 92px; right: 24px; }',
370
+ '.wakz-win-pos-bl { bottom: 92px; left: 24px; }',
371
+
372
+ /* ══════════════════════════════════════════════════
373
+ HEADER
374
+ ══════════════════════════════════════════════════ */
375
+ '.wakz-hdr {',
376
+ ' flex-shrink: 0;',
377
+ ' padding: 14px 18px;',
378
+ ' display: flex;',
379
+ ' align-items: center;',
380
+ ' justify-content: space-between;',
381
+ ' color: #ffffff;',
382
+ ' cursor: default;',
383
+ ' user-select: none;',
384
+ ' background: var(--wakz-primary);',
385
+ '}',
386
+ '.wakz-hdr-left {',
387
+ ' display: flex;',
388
+ ' align-items: center;',
389
+ ' gap: 11px;',
390
+ ' min-width: 0;',
391
+ '}',
392
+
393
+ /* ── Bot Avatar ── */
394
+ '.wakz-avatar {',
395
+ ' width: 38px;',
396
+ ' height: 38px;',
397
+ ' border-radius: 50%;',
398
+ ' background: rgba(255,255,255,0.18);',
399
+ ' display: flex;',
400
+ ' align-items: center;',
401
+ ' justify-content: center;',
402
+ ' font-weight: 700;',
403
+ ' font-size: 16px;',
404
+ ' flex-shrink: 0;',
405
+ ' letter-spacing: 0.5px;',
406
+ '}',
407
+
408
+ /* ── Header Info ── */
409
+ '.wakz-hdr-info {',
410
+ ' display: flex;',
411
+ ' flex-direction: column;',
412
+ ' min-width: 0;',
413
+ '}',
414
+ '.wakz-hdr-name {',
415
+ ' font-size: 15px;',
416
+ ' font-weight: 600;',
417
+ ' line-height: 1.3;',
418
+ ' white-space: nowrap;',
419
+ ' overflow: hidden;',
420
+ ' text-overflow: ellipsis;',
421
+ '}',
422
+ '.wakz-hdr-status {',
423
+ ' font-size: 12px;',
424
+ ' opacity: 0.9;',
425
+ ' display: flex;',
426
+ ' align-items: center;',
427
+ ' gap: 5px;',
428
+ ' margin-top: 2px;',
429
+ '}',
430
+ '.wakz-hdr-status-dot {',
431
+ ' width: 7px;',
432
+ ' height: 7px;',
433
+ ' border-radius: 50%;',
434
+ ' display: inline-block;',
435
+ ' flex-shrink: 0;',
436
+ ' transition: background 0.3s ease;',
437
+ '}',
438
+ '.wakz-hdr-status-dot.online { background: #4ade80; }',
439
+ '.wakz-hdr-status-dot.offline { background: #f87171; }',
440
+
441
+ /* ── Close Button ── */
442
+ '.wakz-close {',
443
+ ' width: 32px;',
444
+ ' height: 32px;',
445
+ ' border-radius: 50%;',
446
+ ' border: none;',
447
+ ' background: rgba(255,255,255,0.12);',
448
+ ' color: #ffffff;',
449
+ ' cursor: pointer;',
450
+ ' display: flex;',
451
+ ' align-items: center;',
452
+ ' justify-content: center;',
453
+ ' transition: background var(--wakz-transition);',
454
+ ' outline: none;',
455
+ ' flex-shrink: 0;',
456
+ '}',
457
+ '.wakz-close:hover {',
458
+ ' background: rgba(255,255,255,0.28);',
459
+ '}',
460
+ '.wakz-close svg {',
461
+ ' width: 18px;',
462
+ ' height: 18px;',
463
+ '}',
464
+
465
+ /* ══════════════════════════════════════════════════
466
+ MESSAGES AREA
467
+ ══════════════════════════════════════════════════ */
468
+ '.wakz-msgs {',
469
+ ' flex: 1;',
470
+ ' overflow-y: auto;',
471
+ ' overflow-x: hidden;',
472
+ ' padding: 18px 16px 10px;',
473
+ ' display: flex;',
474
+ ' flex-direction: column;',
475
+ ' gap: 6px;',
476
+ ' scroll-behavior: smooth;',
477
+ ' background: var(--wakz-chat-bg);',
478
+ '}',
479
+ '.wakz-msgs::-webkit-scrollbar { width: 5px; }',
480
+ '.wakz-msgs::-webkit-scrollbar-track { background: transparent; }',
481
+ '.wakz-msgs::-webkit-scrollbar-thumb {',
482
+ ' background: rgba(0,0,0,0.12);',
483
+ ' border-radius: 10px;',
484
+ '}',
485
+
486
+ /* ══════════════════════════════════════════════════
487
+ MESSAGE BUBBLES
488
+ ══════════════════════════════════════════════════ */
489
+ '.wakz-msg-row {',
490
+ ' display: flex;',
491
+ ' max-width: 82%;',
492
+ ' animation: wakz-msg-in 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards;',
493
+ '}',
494
+ '.wakz-msg-row.bot { align-self: flex-start; }',
495
+ '.wakz-msg-row.user { align-self: flex-end; }',
496
+
497
+ '@keyframes wakz-msg-in {',
498
+ ' 0% { opacity: 0; transform: translateY(8px); }',
499
+ ' 100% { opacity: 1; transform: translateY(0); }',
500
+ '}',
501
+
502
+ '.wakz-bubble {',
503
+ ' padding: 10px 14px;',
504
+ ' font-size: 14px;',
505
+ ' line-height: 1.55;',
506
+ ' word-wrap: break-word;',
507
+ ' overflow-wrap: break-word;',
508
+ ' white-space: pre-wrap;',
509
+ ' border-radius: var(--wakz-radius-bubble);',
510
+ ' position: relative;',
511
+ '}',
512
+ '.wakz-bubble.bot {',
513
+ ' background: #ffffff;',
514
+ ' color: #1a1a1a;',
515
+ ' border-bottom-left-radius: 4px;',
516
+ ' box-shadow: var(--wakz-shadow-sm);',
517
+ '}',
518
+ '.wakz-bubble.user {',
519
+ ' color: #ffffff;',
520
+ ' border-bottom-right-radius: 4px;',
521
+ ' background: var(--wakz-primary);',
522
+ '}',
523
+ '.wakz-bubble.error-bubble {',
524
+ ' background: #fef2f2;',
525
+ ' color: #dc2626;',
526
+ ' border: 1px solid #fecaca;',
527
+ ' border-bottom-left-radius: 4px;',
528
+ ' box-shadow: none;',
529
+ '}',
530
+
531
+ /* ── Message Timestamp ── */
532
+ '.wakz-ts {',
533
+ ' display: block;',
534
+ ' font-size: 11px;',
535
+ ' opacity: 0.45;',
536
+ ' margin-top: 4px;',
537
+ '}',
538
+ '.wakz-bubble.user .wakz-ts {',
539
+ ' text-align: right;',
540
+ '}',
541
+
542
+ /* ── Retry Button (inside error bubble) ── */
543
+ '.wakz-retry {',
544
+ ' display: inline-flex;',
545
+ ' align-items: center;',
546
+ ' gap: 4px;',
547
+ ' margin-top: 8px;',
548
+ ' padding: 5px 14px;',
549
+ ' font-size: 12px;',
550
+ ' font-weight: 500;',
551
+ ' border-radius: 20px;',
552
+ ' border: 1px solid #fca5a5;',
553
+ ' background: #ffffff;',
554
+ ' color: #dc2626;',
555
+ ' cursor: pointer;',
556
+ ' font-family: var(--wakz-font);',
557
+ ' transition: background var(--wakz-transition), border-color var(--wakz-transition);',
558
+ ' outline: none;',
559
+ '}',
560
+ '.wakz-retry:hover {',
561
+ ' background: #fef2f2;',
562
+ ' border-color: #f87171;',
563
+ '}',
564
+
565
+ /* ══════════════════════════════════════════════════
566
+ TYPING INDICATOR
567
+ ══════════════════════════════════════════════════ */
568
+ '.wakz-typing {',
569
+ ' display: flex;',
570
+ ' align-items: center;',
571
+ ' gap: 4px;',
572
+ ' padding: 14px 18px;',
573
+ ' background: #ffffff;',
574
+ ' border-radius: var(--wakz-radius-bubble);',
575
+ ' border-bottom-left-radius: 4px;',
576
+ ' box-shadow: var(--wakz-shadow-sm);',
577
+ '}',
578
+ '.wakz-typing-dot {',
579
+ ' width: 7px;',
580
+ ' height: 7px;',
581
+ ' border-radius: 50%;',
582
+ ' background: #9ca3af;',
583
+ ' animation: wakz-bounce-dot 1.4s ease-in-out infinite;',
584
+ '}',
585
+ '.wakz-typing-dot:nth-child(2) { animation-delay: 0.16s; }',
586
+ '.wakz-typing-dot:nth-child(3) { animation-delay: 0.32s; }',
587
+ '@keyframes wakz-bounce-dot {',
588
+ ' 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }',
589
+ ' 30% { transform: translateY(-7px); opacity: 1; }',
590
+ '}',
591
+
592
+ /* ══════════════════════════════════════════════════
593
+ INPUT AREA
594
+ ══════════════════════════════════════════════════ */
595
+ '.wakz-input-wrap {',
596
+ ' flex-shrink: 0;',
597
+ ' padding: 12px 14px;',
598
+ ' border-top: 1px solid #e5e7eb;',
599
+ ' background: var(--wakz-widget-bg);',
600
+ ' display: flex;',
601
+ ' align-items: flex-end;',
602
+ ' gap: 8px;',
603
+ '}',
604
+ '.wakz-input {',
605
+ ' flex: 1;',
606
+ ' border: 1.5px solid #e5e7eb;',
607
+ ' border-radius: 22px;',
608
+ ' padding: 10px 16px;',
609
+ ' font-size: 14px;',
610
+ ' line-height: 1.4;',
611
+ ' outline: none;',
612
+ ' font-family: var(--wakz-font);',
613
+ ' transition: border-color var(--wakz-transition);',
614
+ ' resize: none;',
615
+ ' max-height: 100px;',
616
+ ' overflow-y: auto;',
617
+ ' background: #f9fafb;',
618
+ ' color: #111827;',
619
+ '}',
620
+ '.wakz-input:focus {',
621
+ ' border-color: var(--wakz-primary);',
622
+ ' background: #ffffff;',
623
+ '}',
624
+ '.wakz-input::placeholder {',
625
+ ' color: #9ca3af;',
626
+ '}',
627
+
628
+ /* ── Send Button ── */
629
+ '.wakz-send {',
630
+ ' width: 40px;',
631
+ ' height: 40px;',
632
+ ' border-radius: 50%;',
633
+ ' border: none;',
634
+ ' color: #ffffff;',
635
+ ' cursor: pointer;',
636
+ ' display: flex;',
637
+ ' align-items: center;',
638
+ ' justify-content: center;',
639
+ ' flex-shrink: 0;',
640
+ ' transition: opacity var(--wakz-transition), transform 0.1s ease, box-shadow var(--wakz-transition);',
641
+ ' outline: none;',
642
+ ' background: var(--wakz-primary);',
643
+ ' box-shadow: var(--wakz-shadow-sm);',
644
+ '}',
645
+ '.wakz-send:hover:not(:disabled) {',
646
+ ' box-shadow: var(--wakz-shadow-md);',
647
+ '}',
648
+ '.wakz-send:disabled {',
649
+ ' opacity: 0.35;',
650
+ ' cursor: not-allowed;',
651
+ '}',
652
+ '.wakz-send:not(:disabled):active {',
653
+ ' transform: scale(0.88);',
654
+ '}',
655
+ '.wakz-send svg {',
656
+ ' width: 18px;',
657
+ ' height: 18px;',
658
+ ' pointer-events: none;',
659
+ '}',
660
+
661
+ /* ══════════════════════════════════════════════════
662
+ WELCOME MESSAGE
663
+ ══════════════════════════════════════════════════ */
664
+ '.wakz-welcome-wrap {',
665
+ ' display: flex;',
666
+ ' gap: 8px;',
667
+ ' max-width: 88%;',
668
+ ' align-self: flex-start;',
669
+ ' animation: wakz-msg-in 0.35s cubic-bezier(0.4, 0, 0.2, 1) forwards;',
670
+ '}',
671
+ '.wakz-welcome-avatar {',
672
+ ' width: 28px;',
673
+ ' height: 28px;',
674
+ ' border-radius: 50%;',
675
+ ' background: var(--wakz-primary);',
676
+ ' display: flex;',
677
+ ' align-items: center;',
678
+ ' justify-content: center;',
679
+ ' color: #ffffff;',
680
+ ' font-weight: 700;',
681
+ ' font-size: 12px;',
682
+ ' flex-shrink: 0;',
683
+ ' align-self: flex-end;',
684
+ ' margin-bottom: 2px;',
685
+ '}',
686
+
687
+ /* ══════════════════════════════════════════════════
688
+ RTL SUPPORT
689
+ ══════════════════════════════════════════════════ */
690
+ '.wakz-rtl { direction: rtl; }',
691
+ '.wakz-rtl .wakz-bubble.bot {',
692
+ ' border-bottom-left-radius: var(--wakz-radius-bubble);',
693
+ ' border-bottom-right-radius: 4px;',
694
+ '}',
695
+ '.wakz-rtl .wakz-bubble.user {',
696
+ ' border-bottom-right-radius: var(--wakz-radius-bubble);',
697
+ ' border-bottom-left-radius: 4px;',
698
+ '}',
699
+ '.wakz-rtl .wakz-typing {',
700
+ ' border-bottom-left-radius: var(--wakz-radius-bubble);',
701
+ ' border-bottom-right-radius: 4px;',
702
+ '}',
703
+ '.wakz-rtl .wakz-bubble.user .wakz-ts {',
704
+ ' text-align: left;',
705
+ '}',
706
+
707
+ /* ══════════════════════════════════════════════════
708
+ MOBILE RESPONSIVE
709
+ ══════════════════════════════════════════════════ */
710
+ '@media (max-width: 480px) {',
711
+ ' .wakz-window {',
712
+ ' width: 100vw !important;',
713
+ ' height: 100vh !important;',
714
+ ' min-height: 100vh !important;',
715
+ ' max-height: 100vh !important;',
716
+ ' bottom: 0 !important;',
717
+ ' top: 0 !important;',
718
+ ' right: 0 !important;',
719
+ ' left: 0 !important;',
720
+ ' border-radius: 0 !important;',
721
+ ' }',
722
+ ' .wakz-window.wakz-visible {',
723
+ ' transform: translateY(0) scale(1);',
724
+ ' }',
725
+ ' .wakz-fab-pos-br { bottom: 16px; right: 16px; }',
726
+ ' .wakz-fab-pos-bl { bottom: 16px; left: 16px; }',
727
+ '}'
728
+ ].join('\n');
729
+ };
730
+
731
+ /* ════════════════════════════════════════════════════════════════
732
+ DOM CREATION
733
+ ════════════════════════════════════════════════════════════════ */
734
+
735
+ WAKZWidget.prototype._createDOM = function () {
736
+ var self = this;
737
+ var isRtl = self.config.language === 'ar';
738
+ var posClass = self.config.position === 'bottom-left' ? 'bl' : 'br';
739
+ var str = _strings(self.config.language);
740
+
741
+ /* ── Host + Shadow DOM ── */
742
+ self._host = _el('div');
743
+ document.body.appendChild(self._host);
744
+ self._shadow = self._host.attachShadow({ mode: 'closed' });
745
+
746
+ /* ── Root wrapper ── */
747
+ self._root = _el('div', { className: 'wakz' + (isRtl ? ' wakz-rtl' : '') });
748
+ self._shadow.appendChild(self._styleEl);
749
+ self._shadow.appendChild(self._root);
750
+
751
+ /* ══════════════ FLOATING ACTION BUTTON ══════════════ */
752
+ self._toggleBtn = _el('button', {
753
+ className: 'wakz-fab wakz-fab-pos-' + posClass,
754
+ 'aria-label': str.openChat
755
+ });
756
+ self._toggleBtn.innerHTML = _ICONS.chatBubble;
757
+
758
+ /* Status dot on FAB */
759
+ self._statusDot = _el('span', { className: 'wakz-fab-dot ' + (self.config.online ? 'online' : 'offline') });
760
+ self._statusDot.style.display = self.config.showStatus ? '' : 'none';
761
+ self._toggleBtn.appendChild(self._statusDot);
762
+ self._root.appendChild(self._toggleBtn);
763
+
764
+ /* ══════════════ CHAT WINDOW ══════════════ */
765
+ self._chatWindow = _el('div', { className: 'wakz-window wakz-win-pos-' + posClass });
766
+
767
+ /* ── Header ── */
768
+ var headerEl = _el('div', { className: 'wakz-hdr' });
769
+
770
+ var headerLeft = _el('div', { className: 'wakz-hdr-left' });
771
+ var avatarLetter = (self.config.botName || 'W')[0].toUpperCase();
772
+ headerLeft.appendChild(_el('div', { className: 'wakz-avatar' }, [avatarLetter]));
773
+
774
+ var headerInfo = _el('div', { className: 'wakz-hdr-info' });
775
+ headerInfo.appendChild(_el('span', { className: 'wakz-hdr-name' }, [self.config.botName]));
776
+
777
+ var statusLine = _el('span', { className: 'wakz-hdr-status' });
778
+ self._headerStatusDot = _el('span', { className: 'wakz-hdr-status-dot ' + (self.config.online ? 'online' : 'offline') });
779
+ if (self.config.showStatus) statusLine.appendChild(self._headerStatusDot);
780
+ self._headerStatusText = document.createTextNode(self.config.online ? str.online : str.offline);
781
+ statusLine.appendChild(self._headerStatusText);
782
+ headerInfo.appendChild(statusLine);
783
+
784
+ headerLeft.appendChild(headerInfo);
785
+ headerEl.appendChild(headerLeft);
786
+
787
+ var closeBtn = _el('button', { className: 'wakz-close', 'aria-label': str.closeChat });
788
+ closeBtn.innerHTML = _ICONS.close;
789
+ headerEl.appendChild(closeBtn);
790
+ self._chatWindow.appendChild(headerEl);
791
+
792
+ /* ── Messages Container ── */
793
+ self._messagesContainer = _el('div', { className: 'wakz-msgs' });
794
+ self._chatWindow.appendChild(self._messagesContainer);
795
+
796
+ /* ── Input Area ── */
797
+ var inputWrap = _el('div', { className: 'wakz-input-wrap' });
798
+
799
+ self._inputEl = _el('textarea', {
800
+ className: 'wakz-input',
801
+ placeholder: str.placeholder,
802
+ rows: 1,
803
+ dir: isRtl ? 'rtl' : 'ltr'
804
+ });
805
+ inputWrap.appendChild(self._inputEl);
806
+
807
+ self._sendBtn = _el('button', {
808
+ className: 'wakz-send',
809
+ 'aria-label': str.sendMessage
810
+ });
811
+ self._sendBtn.innerHTML = _ICONS.send;
812
+ inputWrap.appendChild(self._sendBtn);
813
+ self._chatWindow.appendChild(inputWrap);
814
+
815
+ self._root.appendChild(self._chatWindow);
816
+ };
817
+
818
+ /* ════════════════════════════════════════════════════════════════
819
+ EVENT LISTENERS
820
+ ════════════════════════════════════════════════════════════════ */
821
+
822
+ WAKZWidget.prototype._attachEvents = function () {
823
+ var self = this;
824
+
825
+ /* Toggle button click */
826
+ self._toggleBtn.addEventListener('click', function () {
827
+ self.toggleChat(!self.isOpen);
828
+ });
829
+
830
+ /* Close button click */
831
+ self._chatWindow.querySelector('.wakz-close').addEventListener('click', function () {
832
+ self.toggleChat(false);
833
+ });
834
+
835
+ /* Send button click */
836
+ self._sendBtn.addEventListener('click', function () {
837
+ self._handleSend();
838
+ });
839
+
840
+ /* Enter key to send, Shift+Enter for newline */
841
+ self._inputEl.addEventListener('keydown', function (e) {
842
+ if (e.key === 'Enter' && !e.shiftKey) {
843
+ e.preventDefault();
844
+ if (!self._sendBtn.disabled) self._handleSend();
845
+ }
846
+ });
847
+
848
+ /* Auto-resize textarea */
849
+ self._inputEl.addEventListener('input', function () {
850
+ self._inputEl.style.height = 'auto';
851
+ self._inputEl.style.height = Math.min(self._inputEl.scrollHeight, 100) + 'px';
852
+ });
853
+
854
+ /* Escape to close */
855
+ document.addEventListener('keydown', function (e) {
856
+ if (e.key === 'Escape' && self.isOpen) self.toggleChat(false);
857
+ });
858
+ };
859
+
860
+ /* ════════════════════════════════════════════════════════════════
861
+ FAB ENTRANCE ANIMATION
862
+ ════════════════════════════════════════════════════════════════ */
863
+
864
+ WAKZWidget.prototype._playFabEntrance = function () {
865
+ var self = this;
866
+ /* Start invisible, then animate after a brief delay */
867
+ self._toggleBtn.style.opacity = '0';
868
+ setTimeout(function () {
869
+ self._toggleBtn.style.opacity = '';
870
+ self._toggleBtn.classList.add('wakz-fab-enter');
871
+ }, 800);
872
+ };
873
+
874
+ /* ════════════════════════════════════════════════════════════════
875
+ TOGGLE CHAT OPEN / CLOSE
876
+ ════════════════════════════════════════════════════════════════ */
877
+
878
+ WAKZWidget.prototype.toggleChat = function (open) {
879
+ var self = this;
880
+ self.isOpen = open;
881
+ if (open) {
882
+ self._chatWindow.classList.add('wakz-visible');
883
+ /* Fetch history on first open (after config is loaded) */
884
+ if (self._configLoaded && !self._hasFetchedHistory) {
885
+ self._fetchHistory();
886
+ }
887
+ /* Show welcome message if no messages yet */
888
+ if (self._configLoaded && self.messages.length === 0 && self.config.welcomeMessage) {
889
+ self._appendWelcomeMessage(self.config.welcomeMessage);
890
+ }
891
+ setTimeout(function () { self._inputEl.focus(); }, 320);
892
+ } else {
893
+ self._chatWindow.classList.remove('wakz-visible');
894
+ }
895
+ };
896
+
897
+ /* ════════════════════════════════════════════════════════════════
898
+ FETCH WIDGET CONFIG
899
+ ════════════════════════════════════════════════════════════════ */
900
+
901
+ WAKZWidget.prototype._fetchConfig = function () {
902
+ var self = this;
903
+ if (!self.server || !self.apiKey) {
904
+ self._handleConfigError();
905
+ return;
906
+ }
907
+
908
+ var url = self.server + '/api/v1/embed/config?key=' + encodeURIComponent(self.apiKey);
909
+
910
+ _fetchWithTimeout(url, {
911
+ method: 'GET',
912
+ headers: { 'Accept': 'application/json' }
913
+ }, 30000)
914
+ .then(function (res) { return res.json(); })
915
+ .then(function (data) {
916
+ if (data && data.success && data.config) {
917
+ self.config = Object.assign({}, _DEFAULTS, data.config);
918
+ self._configLoaded = true;
919
+ self._applyConfig();
920
+ } else {
921
+ self._handleConfigError();
922
+ }
923
+ })
924
+ .catch(function (err) {
925
+ /* AbortError means timeout */
926
+ self._handleConfigError();
927
+ });
928
+ };
929
+
930
+ /** Handle config fetch failure — show offline state */
931
+ WAKZWidget.prototype._handleConfigError = function () {
932
+ var self = this;
933
+ self._configError = true;
934
+ self.config.online = false;
935
+
936
+ /* Update FAB dot to red */
937
+ if (self._statusDot) {
938
+ self._statusDot.className = 'wakz-fab-dot offline';
939
+ self._statusDot.style.display = '';
940
+ }
941
+ /* Update header status */
942
+ if (self._headerStatusDot) {
943
+ self._headerStatusDot.className = 'wakz-hdr-status-dot offline';
944
+ }
945
+ if (self._headerStatusText) {
946
+ self._headerStatusText.textContent = _strings(self.config.language).offline;
947
+ }
948
+ };
949
+
950
+ /** Apply loaded config to the DOM */
951
+ WAKZWidget.prototype._applyConfig = function () {
952
+ var self = this;
953
+ var cfg = self.config;
954
+ var str = _strings(cfg.language);
955
+ var isRtl = cfg.language === 'ar';
956
+ var posClass = cfg.position === 'bottom-left' ? 'bl' : 'br';
957
+
958
+ /* ── Update CSS custom properties ── */
959
+ var hostStyle = self._shadow.host.style;
960
+ hostStyle.setProperty('--wakz-primary', cfg.primaryColor);
961
+ hostStyle.setProperty('--wakz-btn', cfg.btnColor);
962
+ hostStyle.setProperty('--wakz-chat-bg', cfg.chatBg);
963
+ hostStyle.setProperty('--wakz-widget-bg', cfg.widgetBg);
964
+
965
+ /* ── RTL ── */
966
+ if (isRtl) self._root.classList.add('wakz-rtl');
967
+ else self._root.classList.remove('wakz-rtl');
968
+
969
+ /* ── FAB ── */
970
+ self._toggleBtn.className = 'wakz-fab wakz-fab-pos-' + posClass + ' wakz-fab-enter';
971
+ self._toggleBtn.setAttribute('aria-label', str.openChat);
972
+
973
+ /* ── FAB Status Dot ── */
974
+ self._statusDot.className = 'wakz-fab-dot ' + (cfg.online ? 'online' : 'offline');
975
+ self._statusDot.style.display = cfg.showStatus ? '' : 'none';
976
+
977
+ /* ── Window Position ── */
978
+ self._chatWindow.className = 'wakz-window wakz-win-pos-' + posClass + (self.isOpen ? ' wakz-visible' : '');
979
+
980
+ /* ── Header ── */
981
+ var headerEl = self._chatWindow.querySelector('.wakz-hdr');
982
+ if (headerEl) headerEl.style.background = cfg.primaryColor;
983
+
984
+ /* ── Bot Name & Avatar ── */
985
+ var nameEl = self._chatWindow.querySelector('.wakz-hdr-name');
986
+ if (nameEl) nameEl.textContent = cfg.botName;
987
+ var avatarEl = self._chatWindow.querySelector('.wakz-avatar');
988
+ if (avatarEl) avatarEl.textContent = (cfg.botName || 'W')[0].toUpperCase();
989
+
990
+ /* ── Header Status ── */
991
+ if (self._headerStatusDot) {
992
+ self._headerStatusDot.className = 'wakz-hdr-status-dot ' + (cfg.online ? 'online' : 'offline');
993
+ self._headerStatusDot.style.display = cfg.showStatus ? '' : 'none';
994
+ }
995
+ if (self._headerStatusText) {
996
+ self._headerStatusText.textContent = cfg.online ? str.online : str.offline;
997
+ }
998
+
999
+ /* ── Input ── */
1000
+ self._inputEl.placeholder = str.placeholder;
1001
+ self._inputEl.dir = isRtl ? 'rtl' : 'ltr';
1002
+
1003
+ /* ── Close button label ── */
1004
+ var closeBtn = self._chatWindow.querySelector('.wakz-close');
1005
+ if (closeBtn) closeBtn.setAttribute('aria-label', str.closeChat);
1006
+
1007
+ /* ── Send button label ── */
1008
+ self._sendBtn.setAttribute('aria-label', str.sendMessage);
1009
+
1010
+ /* ── If chat is already open, show welcome message ── */
1011
+ if (self.isOpen && self.messages.length === 0 && cfg.welcomeMessage) {
1012
+ self._appendWelcomeMessage(cfg.welcomeMessage);
1013
+ }
1014
+ };
1015
+
1016
+ /* ════════════════════════════════════════════════════════════════
1017
+ FETCH CHAT HISTORY
1018
+ ════════════════════════════════════════════════════════════════ */
1019
+
1020
+ WAKZWidget.prototype._fetchHistory = function () {
1021
+ var self = this;
1022
+ if (!self.server || !self.apiKey) return;
1023
+
1024
+ self._hasFetchedHistory = true;
1025
+ var url = self.server + '/api/v1/embed/chat?api_key=' +
1026
+ encodeURIComponent(self.apiKey) + '&visitor_id=' + encodeURIComponent(self.visitorId);
1027
+
1028
+ _fetchWithTimeout(url, {
1029
+ method: 'GET',
1030
+ headers: { 'Accept': 'application/json' }
1031
+ }, 15000)
1032
+ .then(function (res) { return res.json(); })
1033
+ .then(function (data) {
1034
+ if (data && data.success && data.messages && data.messages.length > 0) {
1035
+ /* Clear any existing messages (e.g., welcome) and render history */
1036
+ self._clearMessages();
1037
+ var msgs = data.messages;
1038
+ for (var i = 0; i < msgs.length; i++) {
1039
+ var m = msgs[i];
1040
+ var role = m.role === 'user' ? 'user' : 'bot';
1041
+ self._appendMessage(role, m.content, false, m.createdAt);
1042
+ }
1043
+ }
1044
+ })
1045
+ .catch(function () {
1046
+ /* Silently ignore history fetch failures */
1047
+ });
1048
+ };
1049
+
1050
+ /* ════════════════════════════════════════════════════════════════
1051
+ CLEAR ALL MESSAGES
1052
+ ════════════════════════════════════════════════════════════════ */
1053
+
1054
+ WAKZWidget.prototype._clearMessages = function () {
1055
+ var self = this;
1056
+ self._messagesContainer.innerHTML = '';
1057
+ self.messages = [];
1058
+ };
1059
+
1060
+ /* ════════════════════════════════════════════════════════════════
1061
+ APPEND WELCOME MESSAGE (with bot avatar)
1062
+ ════════════════════════════════════════════════════════════════ */
1063
+
1064
+ WAKZWidget.prototype._appendWelcomeMessage = function (text) {
1065
+ var self = this;
1066
+ if (!text) return;
1067
+
1068
+ var wrap = _el('div', { className: 'wakz-welcome-wrap' });
1069
+
1070
+ /* Bot avatar */
1071
+ var avatar = _el('div', { className: 'wakz-welcome-avatar' },
1072
+ [(self.config.botName || 'W')[0].toUpperCase()]);
1073
+ wrap.appendChild(avatar);
1074
+
1075
+ /* Bubble */
1076
+ var bubble = _el('div', { className: 'wakz-bubble bot' }, [text]);
1077
+ wrap.appendChild(bubble);
1078
+
1079
+ self._messagesContainer.appendChild(wrap);
1080
+ self.messages.push({ sender: 'bot', text: text });
1081
+ self._scrollToBottom();
1082
+ };
1083
+
1084
+ /* ════════════════════════════════════════════════════════════════
1085
+ APPEND MESSAGE TO CHAT
1086
+ ════════════════════════════════════════════════════════════════ */
1087
+
1088
+ WAKZWidget.prototype._appendMessage = function (sender, text, isError, timestamp) {
1089
+ var self = this;
1090
+
1091
+ var row = _el('div', { className: 'wakz-msg-row ' + sender });
1092
+
1093
+ var bubbleClass = 'wakz-bubble ' + sender;
1094
+ if (isError) bubbleClass += ' error-bubble';
1095
+ var bubble = _el('div', { className: bubbleClass });
1096
+
1097
+ bubble.appendChild(document.createTextNode(text));
1098
+
1099
+ /* Timestamp */
1100
+ var tsText = '';
1101
+ if (timestamp) {
1102
+ try {
1103
+ var d = new Date(timestamp);
1104
+ if (!isNaN(d.getTime())) {
1105
+ tsText = d.getHours().toString().padStart(2, '0') + ':' +
1106
+ d.getMinutes().toString().padStart(2, '0');
1107
+ }
1108
+ } catch (e) { /* ignore */ }
1109
+ }
1110
+ if (!tsText) {
1111
+ var now = new Date();
1112
+ tsText = now.getHours().toString().padStart(2, '0') + ':' +
1113
+ now.getMinutes().toString().padStart(2, '0');
1114
+ }
1115
+ bubble.appendChild(_el('span', { className: 'wakz-ts' }, [tsText]));
1116
+
1117
+ /* Retry button for errors */
1118
+ if (isError) {
1119
+ var str = _strings(self.config.language);
1120
+ var retryBtn = _el('button', { className: 'wakz-retry' }, [
1121
+ _ICONS.error + ' ' + str.retry
1122
+ ]);
1123
+ (function (rowCapture) {
1124
+ retryBtn.addEventListener('click', function () {
1125
+ /* Remove error row */
1126
+ if (rowCapture.parentNode) rowCapture.parentNode.removeChild(rowCapture);
1127
+ /* Find and remove from messages array */
1128
+ for (var i = self.messages.length - 1; i >= 0; i--) {
1129
+ if (self.messages[i].text === text && self.messages[i].isError) {
1130
+ self.messages.splice(i, 1);
1131
+ break;
1132
+ }
1133
+ }
1134
+ /* Resend last user message */
1135
+ var lastUserMsg = null;
1136
+ for (var j = self.messages.length - 1; j >= 0; j--) {
1137
+ if (self.messages[j].sender === 'user') {
1138
+ lastUserMsg = self.messages[j].text;
1139
+ break;
1140
+ }
1141
+ }
1142
+ if (lastUserMsg) self._sendToAPI(lastUserMsg);
1143
+ });
1144
+ })(row);
1145
+ bubble.appendChild(retryBtn);
1146
+ }
1147
+
1148
+ row.appendChild(bubble);
1149
+ self._messagesContainer.appendChild(row);
1150
+
1151
+ self.messages.push({ sender: sender, text: text, isError: !!isError });
1152
+ self._scrollToBottom();
1153
+ };
1154
+
1155
+ /* ════════════════════════════════════════════════════════════════
1156
+ TYPING INDICATOR
1157
+ ════════════════════════════════════════════════════════════════ */
1158
+
1159
+ WAKZWidget.prototype._showTyping = function () {
1160
+ var self = this;
1161
+ if (self._typingEl) return;
1162
+
1163
+ self._typingEl = _el('div', { className: 'wakz-msg-row bot' });
1164
+ self._typingEl.innerHTML =
1165
+ '<div class="wakz-typing">' +
1166
+ '<span class="wakz-typing-dot"></span>' +
1167
+ '<span class="wakz-typing-dot"></span>' +
1168
+ '<span class="wakz-typing-dot"></span>' +
1169
+ '</div>';
1170
+ self._messagesContainer.appendChild(self._typingEl);
1171
+ self._scrollToBottom();
1172
+ };
1173
+
1174
+ WAKZWidget.prototype._hideTyping = function () {
1175
+ var self = this;
1176
+ if (self._typingEl && self._typingEl.parentNode) {
1177
+ self._typingEl.parentNode.removeChild(self._typingEl);
1178
+ }
1179
+ self._typingEl = null;
1180
+ };
1181
+
1182
+ /* ════════════════════════════════════════════════════════════════
1183
+ SCROLL TO BOTTOM
1184
+ ════════════════════════════════════════════════════════════════ */
1185
+
1186
+ WAKZWidget.prototype._scrollToBottom = function () {
1187
+ var self = this;
1188
+ requestAnimationFrame(function () {
1189
+ requestAnimationFrame(function () {
1190
+ self._messagesContainer.scrollTop = self._messagesContainer.scrollHeight;
1191
+ });
1192
+ });
1193
+ };
1194
+
1195
+ /* ════════════════════════════════════════════════════════════════
1196
+ HANDLE SEND ACTION
1197
+ ════════════════════════════════════════════════════════════════ */
1198
+
1199
+ WAKZWidget.prototype._handleSend = function () {
1200
+ var self = this;
1201
+ var text = (self._inputEl.value || '').trim();
1202
+ if (!text || self.isLoading) return;
1203
+
1204
+ /* Append user message immediately */
1205
+ self._appendMessage('user', text);
1206
+
1207
+ /* Clear input */
1208
+ self._inputEl.value = '';
1209
+ self._inputEl.style.height = 'auto';
1210
+
1211
+ /* Send to API */
1212
+ self._sendToAPI(text);
1213
+ };
1214
+
1215
+ /* ════════════════════════════════════════════════════════════════
1216
+ SEND MESSAGE TO API
1217
+ ════════════════════════════════════════════════════════════════ */
1218
+
1219
+ WAKZWidget.prototype._sendToAPI = function (message) {
1220
+ var self = this;
1221
+
1222
+ if (!self.server || !self.apiKey) {
1223
+ self._appendMessage('bot', 'Widget not configured. Provide data-api-key and data-server.', true);
1224
+ return;
1225
+ }
1226
+
1227
+ self.isLoading = true;
1228
+ self._sendBtn.disabled = true;
1229
+ self._showTyping();
1230
+
1231
+ var payload = {
1232
+ api_key: self.apiKey,
1233
+ visitor_id: self.visitorId,
1234
+ message: message,
1235
+ session_data: {
1236
+ domain: window.location.hostname || '',
1237
+ page_url: window.location.href || '',
1238
+ user_agent: navigator.userAgent || '',
1239
+ language: navigator.language || ''
1240
+ }
1241
+ };
1242
+
1243
+ _fetchWithTimeout(self.server + '/api/v1/embed/chat', {
1244
+ method: 'POST',
1245
+ headers: {
1246
+ 'Content-Type': 'application/json',
1247
+ 'Accept': 'application/json'
1248
+ },
1249
+ body: JSON.stringify(payload)
1250
+ }, 30000)
1251
+ .then(function (res) { return res.json(); })
1252
+ .then(function (data) {
1253
+ self._hideTyping();
1254
+ if (data && data.success && data.reply) {
1255
+ self._appendMessage('bot', data.reply);
1256
+ } else {
1257
+ self._appendMessage('bot', _strings(self.config.language).errorMsg, true);
1258
+ }
1259
+ })
1260
+ .catch(function () {
1261
+ self._hideTyping();
1262
+ self._appendMessage('bot', _strings(self.config.language).errorMsg, true);
1263
+ })
1264
+ .finally(function () {
1265
+ self.isLoading = false;
1266
+ self._sendBtn.disabled = false;
1267
+ if (self.isOpen) self._inputEl.focus();
1268
+ });
1269
+ };
1270
+
1271
+ /* ════════════════════════════════════════════════════════════════
1272
+ BOOTSTRAP
1273
+ ════════════════════════════════════════════════════════════════ */
1274
+
1275
+ function boot() {
1276
+ try { new WAKZWidget(); } catch (e) { console.error('[WAKZ Widget] Init error:', e); }
1277
+ }
1278
+
1279
+ if (document.readyState === 'loading') {
1280
+ document.addEventListener('DOMContentLoaded', boot);
1281
+ } else {
1282
+ boot();
1283
+ }
1284
+
1285
+ })();
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "wakz-chat-widget",
3
+ "version": "1.0.0",
4
+ "description": "Production-grade AI chat widget by WAKZ — Shadow DOM isolated, zero dependencies, Intercom/Crisp quality.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "prepublishOnly": "echo 'Ready to publish wakz-chat-widget'"
8
+ },
9
+ "keywords": [
10
+ "chat",
11
+ "widget",
12
+ "ai",
13
+ "chatbot",
14
+ "wakz",
15
+ "embed",
16
+ "shadow-dom",
17
+ "customer-support",
18
+ "live-chat",
19
+ "saas"
20
+ ],
21
+ "author": "WAKZ",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": ""
26
+ },
27
+ "files": [
28
+ "index.js",
29
+ "README.md"
30
+ ]
31
+ }