@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 +71 -0
  2. package/index.js +1936 -0
  3. package/package.json +41 -0
package/index.js ADDED
@@ -0,0 +1,1936 @@
1
+ /**
2
+ * WAKZ Chat Widget v4.1.0
3
+ * ─────────────────────────────────────────────────────────────────
4
+ * A production-grade, self-contained chat widget using Shadow DOM.
5
+ * Liquid Glass design — pill header, floating input, dark user bubbles.
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
+ /** Detect device model, platform, and type using userAgentData API (Chrome 90+) with UA fallback */
83
+ function _getDeviceInfo() {
84
+ var info = {
85
+ device_model: '',
86
+ device_platform: '',
87
+ device_type: 'desktop'
88
+ };
89
+
90
+ // Try userAgentData API (Chrome 90+, Edge, Samsung Browser — NOT Safari/Firefox)
91
+ if (navigator.userAgentData) {
92
+ info.device_model = navigator.userAgentData.model || '';
93
+ info.device_platform = navigator.userAgentData.platform || '';
94
+ if (navigator.userAgentData.mobile === true) {
95
+ info.device_type = 'mobile';
96
+ } else if (navigator.userAgentData.mobile === false && info.device_platform === 'Android') {
97
+ info.device_type = 'tablet';
98
+ } else {
99
+ info.device_type = 'desktop';
100
+ }
101
+ }
102
+
103
+ // Fallback: parse from user agent string
104
+ var ua = navigator.userAgent || '';
105
+ if (!info.device_platform) {
106
+ if (/iPhone|iPad|iPod/i.test(ua)) info.device_platform = 'iOS';
107
+ else if (/Android/i.test(ua)) info.device_platform = 'Android';
108
+ else if (/Windows/i.test(ua)) info.device_platform = 'Windows';
109
+ else if (/Mac OS X/i.test(ua)) info.device_platform = 'macOS';
110
+ else if (/CrOS/i.test(ua)) info.device_platform = 'Chrome OS';
111
+ else if (/Linux/i.test(ua)) info.device_platform = 'Linux';
112
+ }
113
+ if (info.device_type === 'desktop') {
114
+ if (/iPad/i.test(ua)) info.device_type = 'tablet';
115
+ else if (/Mobile|iPhone|iPod/i.test(ua)) info.device_type = 'mobile';
116
+ else if (/Android(?!.*Mobile)/i.test(ua)) info.device_type = 'tablet';
117
+ else if (/Android/i.test(ua)) info.device_type = 'mobile';
118
+ }
119
+ return info;
120
+ }
121
+
122
+ /** Fetch with an AbortController timeout (default 30s) */
123
+ function _fetchWithTimeout(url, options, timeoutMs) {
124
+ var controller = new AbortController();
125
+ var merged = Object.assign({}, options, { signal: controller.signal });
126
+ var timer = setTimeout(function () { controller.abort(); }, timeoutMs || 30000);
127
+ return fetch(url, merged).finally(function () { clearTimeout(timer); });
128
+ }
129
+
130
+ /** XSS-safe: escape HTML entities */
131
+ function _escapeHTML(str) {
132
+ var div = document.createElement('div');
133
+ div.appendChild(document.createTextNode(str));
134
+ return div.innerHTML;
135
+ }
136
+
137
+ /* ════════════════════════════════════════════════════════════════
138
+ SVG ICONS (inline for zero-dependency)
139
+ ════════════════════════════════════════════════════════════════ */
140
+
141
+ var _ICONS = {
142
+ chatBubble:
143
+ '<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
144
+ send:
145
+ '<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M19 12H5M5 12L11 6M5 12L11 18" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
146
+ close:
147
+ '<svg width="14" height="14" viewBox="0 0 24 24" fill="none"><line x1="18" y1="6" x2="6" y2="18" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/><line x1="6" y1="6" x2="18" y2="18" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/></svg>',
148
+ error:
149
+ '<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>',
150
+ message:
151
+ '<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22z"/></svg>',
152
+ whatsapp:
153
+ '<svg width="26" height="26" viewBox="0 0 24 24" fill="currentColor"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/></svg>',
154
+ headset:
155
+ '<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3ZM3 19a2 2 0 0 0 2 2h3a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H5"/></svg>'
156
+ };
157
+
158
+ /* ════════════════════════════════════════════════════════════════
159
+ LOCALIZATION STRINGS
160
+ ════════════════════════════════════════════════════════════════ */
161
+
162
+ var _I18N = {
163
+ ar: {
164
+ placeholder: 'اكتب رسالتك...',
165
+ online: 'متصل الآن',
166
+ offline: 'غير متصل',
167
+ errorMsg: 'حدث خطأ في الاتصال. يرجى المحاولة مرة أخرى.',
168
+ retry: 'إعادة المحاولة',
169
+ configError: 'تعذر تحميل إعدادات المحادثة.',
170
+ openChat: 'فتح المحادثة',
171
+ closeChat: 'إغلاق المحادثة',
172
+ sendMessage: 'إرسال',
173
+ humanBadge: 'فريق الدعم',
174
+ defaultSubtitle: 'المساعد الذكي'
175
+ },
176
+ en: {
177
+ placeholder: 'Type your message...',
178
+ online: 'Online',
179
+ offline: 'Offline',
180
+ errorMsg: 'Something went wrong. Please try again.',
181
+ retry: 'Retry',
182
+ configError: 'Could not load chat settings.',
183
+ openChat: 'Open chat',
184
+ closeChat: 'Close chat',
185
+ sendMessage: 'Send',
186
+ humanBadge: 'Support Team',
187
+ defaultSubtitle: 'Smart Assistant'
188
+ },
189
+ fr: {
190
+ placeholder: 'Tapez votre message...',
191
+ online: 'En ligne',
192
+ offline: 'Hors ligne',
193
+ errorMsg: 'Une erreur est survenue. Veuillez réessayer.',
194
+ retry: 'Réessayer',
195
+ configError: 'Impossible de charger les paramètres.',
196
+ openChat: 'Ouvrir le chat',
197
+ closeChat: 'Fermer le chat',
198
+ sendMessage: 'Envoyer',
199
+ humanBadge: 'Équipe de support',
200
+ defaultSubtitle: 'Assistant intelligent'
201
+ }
202
+ };
203
+
204
+ function _strings(lang) {
205
+ return _I18N[lang] || _I18N['en'];
206
+ }
207
+
208
+ /* ════════════════════════════════════════════════════════════════
209
+ DEFAULT CONFIGURATION
210
+ ════════════════════════════════════════════════════════════════ */
211
+
212
+ var _DEFAULTS = {
213
+ botName: 'WAKZ',
214
+ botSubtitle: '',
215
+ welcomeMessage: '',
216
+ primaryColor: '#111111',
217
+ chatBg: '#ffffff',
218
+ btnColor: '#111111',
219
+ widgetBg: '#f5f5f5',
220
+ position: 'bottom-right',
221
+ language: 'en',
222
+ showStatus: true,
223
+ online: true,
224
+ /* v4.0.1 — Advanced customization */
225
+ fabShape: 'circle',
226
+ fabIcon: 'chat',
227
+ triggerMode: 'manual',
228
+ triggerDelay: 5,
229
+ displayMode: 'modal',
230
+ botBubbleColor: '#ffffff',
231
+ botTextColor: '#333333',
232
+ userBubbleColor: '#111111',
233
+ userTextColor: '#ffffff',
234
+ headerColor: '',
235
+ headerTextColor: ''
236
+ };
237
+
238
+ /* ════════════════════════════════════════════════════════════════
239
+ WAKZ WIDGET — MAIN CLASS
240
+ ════════════════════════════════════════════════════════════════ */
241
+
242
+ /** Singleton guard — only one widget per page */
243
+ if (window.__wakz_widget_initialized) return;
244
+ window.__wakz_widget_initialized = true;
245
+
246
+ function WAKZWidget() {
247
+ var self = this;
248
+
249
+ /* ── Script attributes ── */
250
+ var attrs = _getScriptAttrs();
251
+ self.apiKey = attrs.apiKey;
252
+ self.server = attrs.server;
253
+ self.visitorId = _getVisitorId();
254
+ self._deviceInfo = _getDeviceInfo();
255
+
256
+ /* ── Runtime state ── */
257
+ self.config = Object.assign({}, _DEFAULTS);
258
+ self.isOpen = false;
259
+ self.isLoading = false;
260
+ self.messages = [];
261
+ self._hasFetchedHistory = false;
262
+ self._typingEl = null;
263
+ self._configLoaded = false;
264
+ self._configError = false;
265
+ self._pollTimer = null;
266
+ self._lastPollTime = null;
267
+ self._POLL_INTERVAL = 4000;
268
+ self._knownMessageIds = {};
269
+ self._pendingReply = false;
270
+
271
+ /* ── DOM refs (populated after mount) ── */
272
+ self._host = null;
273
+ self._shadow = null;
274
+ self._root = null;
275
+ self._overlay = null;
276
+ self._chatWindow = null;
277
+ self._messagesInner = null;
278
+ self._inputWrap = null;
279
+ self._inputEl = null;
280
+ self._sendBtn = null;
281
+ self._toggleBtn = null;
282
+ self._statusDot = null;
283
+ self._headerTitleEl = null;
284
+ self._headerSubtitleEl = null;
285
+ self._headerStatusPill = null;
286
+ self._headerStatusDot = null;
287
+ self._headerStatusText = null;
288
+ self._closeBtn = null;
289
+
290
+ /* ── Bootstrap ── */
291
+ self._injectCSS();
292
+ self._createDOM();
293
+ self._attachEvents();
294
+ self._fetchConfig();
295
+ self._playFabEntrance();
296
+ }
297
+
298
+ /* ════════════════════════════════════════════════════════════════
299
+ CSS — Complete Liquid Glass design (v4.0.0)
300
+ ════════════════════════════════════════════════════════════════ */
301
+
302
+ WAKZWidget.prototype._injectCSS = function () {
303
+ var self = this;
304
+ self._styleEl = _el('style');
305
+
306
+ self._styleEl.textContent = [
307
+ /* ── CSS Custom Properties (theming) ── */
308
+ ':host {',
309
+ ' --wakz-primary: #111111;',
310
+ ' --wakz-btn: #111111;',
311
+ ' --wakz-chat-bg: #ffffff;',
312
+ ' --wakz-widget-bg: #ffffff;',
313
+ ' all: initial;',
314
+ ' font-family: "Cairo", "Tajawal", "IBM Plex Sans Arabic", system-ui, sans-serif;',
315
+ '}',
316
+
317
+ /* ── Reset ── */
318
+ '.wakz *, .wakz *::before, .wakz *::after {',
319
+ ' box-sizing: border-box;',
320
+ ' margin: 0;',
321
+ ' padding: 0;',
322
+ '}',
323
+
324
+ /* ══════════════════════════════════════════════════
325
+ ANIMATIONS
326
+ ══════════════════════════════════════════════════ */
327
+ '@keyframes wakz-fade-slide-up {',
328
+ ' from { opacity: 0; transform: translateY(6px); }',
329
+ ' to { opacity: 1; transform: translateY(0); }',
330
+ '}',
331
+
332
+ '@keyframes wakz-typing-bounce {',
333
+ ' 0%, 60%, 100% { transform: translateY(0); opacity: 0.35; }',
334
+ ' 30% { transform: translateY(-4px); opacity: 1; }',
335
+ '}',
336
+
337
+ '@keyframes wakz-pulse-green {',
338
+ ' 0%, 100% { box-shadow: 0 0 0 2px rgba(34,197,94,0.2); }',
339
+ ' 50% { box-shadow: 0 0 0 4px rgba(34,197,94,0.08); }',
340
+ '}',
341
+
342
+ '@keyframes wakz-fab-enter {',
343
+ ' 0% { opacity: 0; transform: scale(0.3); }',
344
+ ' 50% { opacity: 1; transform: scale(1.12); }',
345
+ ' 70% { transform: scale(0.92); }',
346
+ ' 100% { transform: scale(1); }',
347
+ '}',
348
+
349
+ /* ══════════════════════════════════════════════════
350
+ FLOATING ACTION BUTTON (FAB) — 56px black circle
351
+ ══════════════════════════════════════════════════ */
352
+ '.wakz-fab {',
353
+ ' position: fixed;',
354
+ ' z-index: 2147483647;',
355
+ ' width: 56px;',
356
+ ' height: 56px;',
357
+ ' border-radius: 50%;',
358
+ ' border: none;',
359
+ ' cursor: pointer;',
360
+ ' display: flex;',
361
+ ' align-items: center;',
362
+ ' justify-content: center;',
363
+ ' color: #ffffff;',
364
+ ' background: var(--wakz-btn);',
365
+ ' box-shadow: 0 4px 20px rgba(0,0,0,0.2), 0 1px 6px rgba(0,0,0,0.12);',
366
+ ' transition: transform 0.3s cubic-bezier(0.22,1,0.36,1), box-shadow 0.3s cubic-bezier(0.22,1,0.36,1);',
367
+ ' outline: none;',
368
+ ' -webkit-tap-highlight-color: transparent;',
369
+ '}',
370
+ '.wakz-fab:hover {',
371
+ ' transform: scale(1.1);',
372
+ ' box-shadow: 0 6px 28px rgba(0,0,0,0.25), 0 2px 8px rgba(0,0,0,0.15);',
373
+ '}',
374
+ '.wakz-fab:active { transform: scale(0.93); }',
375
+ '.wakz-fab svg { width: 26px; height: 26px; color: #fff; pointer-events: none; }',
376
+
377
+ /* ── FAB Positioning ── */
378
+ '.wakz-fab-pos-br { bottom: 24px; right: 24px; }',
379
+ '.wakz-fab-pos-bl { bottom: 24px; left: 24px; }',
380
+
381
+ /* ── Hide FAB when chat is open ── */
382
+ '.wakz-window.wakz-visible ~ .wakz-fab,',
383
+ '.wakz-fab.wakz-fab-hidden {',
384
+ ' display: none !important;',
385
+ '}',
386
+
387
+ /* ── FAB Entrance Bounce ── */
388
+ '.wakz-fab-enter {',
389
+ ' animation: wakz-fab-enter 0.6s cubic-bezier(0.34,1.56,0.64,1) forwards;',
390
+ '}',
391
+
392
+ /* ══════════════════════════════════════════════════
393
+ STATUS DOT (on FAB) — 10px, top-right
394
+ ══════════════════════════════════════════════════ */
395
+ '.wakz-fab-dot {',
396
+ ' position: absolute;',
397
+ ' top: -1px;',
398
+ ' right: -1px;',
399
+ ' width: 10px;',
400
+ ' height: 10px;',
401
+ ' border-radius: 50%;',
402
+ ' border: 2px solid #ffffff;',
403
+ ' z-index: 2;',
404
+ ' transition: background 0.3s ease;',
405
+ '}',
406
+ '.wakz-fab-pos-bl .wakz-fab-dot { right: auto; left: -1px; }',
407
+ '.wakz-fab-dot.online {',
408
+ ' background: #22c55e;',
409
+ ' animation: wakz-pulse-green 2.5s ease-in-out infinite;',
410
+ '}',
411
+ '.wakz-fab-dot.offline {',
412
+ ' background: #ef4444;',
413
+ ' animation: none;',
414
+ '}',
415
+
416
+ /* ══════════════════════════════════════════════════
417
+ OVERLAY / BACKDROP — blur behind modal
418
+ ══════════════════════════════════════════════════ */
419
+ '.wakz-overlay {',
420
+ ' position: fixed;',
421
+ ' inset: 0;',
422
+ ' z-index: 2147483645;',
423
+ ' background: rgba(0,0,0,0.25);',
424
+ ' backdrop-filter: blur(4px);',
425
+ ' -webkit-backdrop-filter: blur(4px);',
426
+ ' opacity: 0;',
427
+ ' pointer-events: none;',
428
+ ' transition: opacity 0.3s cubic-bezier(0.4,0,0.2,1);',
429
+ '}',
430
+ '.wakz-overlay.wakz-visible {',
431
+ ' opacity: 1;',
432
+ ' pointer-events: auto;',
433
+ '}',
434
+
435
+ /* ══════════════════════════════════════════════════
436
+ CHAT WINDOW — Centered, 420x560, 24px radius
437
+ ══════════════════════════════════════════════════ */
438
+ '.wakz-window {',
439
+ ' position: fixed;',
440
+ ' z-index: 2147483646;',
441
+ ' top: 50%;',
442
+ ' left: 50%;',
443
+ ' transform: translate(-50%, -50%) scale(0.95);',
444
+ ' width: 420px;',
445
+ ' height: 560px;',
446
+ ' max-width: calc(100vw - 24px);',
447
+ ' max-height: calc(100vh - 24px);',
448
+ ' border-radius: 24px;',
449
+ ' overflow: hidden;',
450
+ ' display: flex;',
451
+ ' flex-direction: column;',
452
+ ' background: var(--wakz-widget-bg);',
453
+ ' box-shadow:',
454
+ ' 0 0 0 0.5px rgba(0,0,0,0.06),',
455
+ ' 0 8px 40px rgba(0,0,0,0.08),',
456
+ ' 0 2px 12px rgba(0,0,0,0.04);',
457
+ ' opacity: 0;',
458
+ ' pointer-events: none;',
459
+ ' transition: opacity 0.3s cubic-bezier(0.4,0,0.2,1),',
460
+ ' transform 0.3s cubic-bezier(0.4,0,0.2,1);',
461
+ '}',
462
+ '.wakz-window.wakz-visible {',
463
+ ' opacity: 1;',
464
+ ' transform: translate(-50%, -50%) scale(1);',
465
+ ' pointer-events: auto;',
466
+ '}',
467
+
468
+ /* ══════════════════════════════════════════════════
469
+ FLOATING HEADER WRAP — absolute over messages
470
+ ══════════════════════════════════════════════════ */
471
+ '.wakz-header-wrap {',
472
+ ' position: absolute;',
473
+ ' top: 12px;',
474
+ ' right: 12px;',
475
+ ' left: 12px;',
476
+ ' z-index: 100;',
477
+ ' pointer-events: none;',
478
+ '}',
479
+
480
+ /* ── HEADER — Liquid Glass Pill ── */
481
+ '.wakz-header {',
482
+ ' pointer-events: all;',
483
+ ' display: flex;',
484
+ ' align-items: center;',
485
+ ' justify-content: space-between;',
486
+ ' position: relative;',
487
+ ' overflow: hidden;',
488
+ ' backdrop-filter: blur(40px) saturate(180%) brightness(102%);',
489
+ ' -webkit-backdrop-filter: blur(40px) saturate(180%) brightness(102%);',
490
+ ' background: rgba(255,255,255,0.82);',
491
+ ' border: 0.5px solid rgba(255,255,255,0.7);',
492
+ ' border-radius: 9999px;',
493
+ ' box-shadow:',
494
+ ' inset 0 1px 0 0 rgba(255,255,255,0.9),',
495
+ ' inset 0 -1px 0 0 rgba(0,0,0,0.04),',
496
+ ' 0 0 0 0.5px rgba(0,0,0,0.06),',
497
+ ' 0 8px 32px rgba(0,0,0,0.08),',
498
+ ' 0 2px 8px rgba(0,0,0,0.04);',
499
+ ' padding: 8px 8px 8px 12px;',
500
+ ' gap: 12px;',
501
+ '}',
502
+
503
+ /* ── Specular Highlight (real DOM element, not ::before) ── */
504
+ '.wakz-specular {',
505
+ ' position: absolute;',
506
+ ' top: 0;',
507
+ ' left: 15%;',
508
+ ' right: 15%;',
509
+ ' height: 1px;',
510
+ ' background: linear-gradient(',
511
+ ' 90deg,',
512
+ ' transparent,',
513
+ ' rgba(255,255,255,0.6) 20%,',
514
+ ' rgba(255,255,255,0.95) 50%,',
515
+ ' rgba(255,255,255,0.6) 80%,',
516
+ ' transparent',
517
+ ' );',
518
+ ' border-radius: inherit;',
519
+ ' pointer-events: none;',
520
+ ' z-index: 1;',
521
+ '}',
522
+
523
+ /* ── Brand (right side in RTL) ── */
524
+ '.wakz-hdr-brand {',
525
+ ' display: flex;',
526
+ ' align-items: center;',
527
+ ' flex-shrink: 0;',
528
+ ' padding: 0 6px;',
529
+ '}',
530
+ '.wakz-hdr-info {',
531
+ ' display: flex;',
532
+ ' flex-direction: column;',
533
+ ' gap: 1px;',
534
+ '}',
535
+ '.wakz-hdr-title {',
536
+ ' font-size: 15px;',
537
+ ' font-weight: 900;',
538
+ ' color: #0a0a0a;',
539
+ ' letter-spacing: -0.2px;',
540
+ ' line-height: 1.2;',
541
+ ' font-family: "Cairo", sans-serif;',
542
+ '}',
543
+ '.wakz-hdr-subtitle {',
544
+ ' font-size: 11px;',
545
+ ' font-weight: 500;',
546
+ ' color: #999;',
547
+ ' line-height: 1.2;',
548
+ ' font-family: "Tajawal", "Cairo", sans-serif;',
549
+ '}',
550
+
551
+ /* ── Actions (left side in RTL) ── */
552
+ '.wakz-hdr-actions {',
553
+ ' display: flex;',
554
+ ' align-items: center;',
555
+ ' gap: 8px;',
556
+ ' flex-shrink: 0;',
557
+ '}',
558
+
559
+ /* ── Status Pill ── */
560
+ '.wakz-status-pill {',
561
+ ' display: inline-flex;',
562
+ ' align-items: center;',
563
+ ' gap: 5px;',
564
+ ' padding: 4px 10px;',
565
+ ' border-radius: 50px;',
566
+ ' font-size: 11px;',
567
+ ' font-weight: 600;',
568
+ ' font-family: "Cairo", sans-serif;',
569
+ ' white-space: nowrap;',
570
+ ' transition: all 0.3s ease;',
571
+ '}',
572
+ '.wakz-status-pill--online {',
573
+ ' background: rgba(34,197,94,0.08);',
574
+ ' color: #16a34a;',
575
+ ' border: 1px solid rgba(34,197,94,0.15);',
576
+ '}',
577
+ '.wakz-status-pill--offline {',
578
+ ' background: rgba(156,163,175,0.1);',
579
+ ' color: #6b7280;',
580
+ ' border: 1px solid rgba(156,163,175,0.15);',
581
+ '}',
582
+
583
+ /* ── Status Dot inside pill (6px) ── */
584
+ '.wakz-status-dot {',
585
+ ' width: 6px;',
586
+ ' height: 6px;',
587
+ ' border-radius: 50%;',
588
+ ' flex-shrink: 0;',
589
+ '}',
590
+ '.wakz-status-pill--online .wakz-status-dot {',
591
+ ' background: #22c55e;',
592
+ ' animation: wakz-pulse-green 2.5s ease-in-out infinite;',
593
+ '}',
594
+ '.wakz-status-pill--offline .wakz-status-dot {',
595
+ ' background: #9ca3af;',
596
+ '}',
597
+
598
+ /* ── Close Button (glass circle) ── */
599
+ '.wakz-close {',
600
+ ' width: 30px;',
601
+ ' height: 30px;',
602
+ ' min-width: 30px;',
603
+ ' min-height: 30px;',
604
+ ' border-radius: 50%;',
605
+ ' border: none;',
606
+ ' cursor: pointer;',
607
+ ' display: flex;',
608
+ ' align-items: center;',
609
+ ' justify-content: center;',
610
+ ' color: #888;',
611
+ ' position: relative;',
612
+ ' overflow: hidden;',
613
+ ' backdrop-filter: blur(20px) saturate(180%);',
614
+ ' -webkit-backdrop-filter: blur(20px) saturate(180%);',
615
+ ' background: rgba(255,255,255,0.6);',
616
+ ' border: 0.5px solid rgba(255,255,255,0.6);',
617
+ ' box-shadow:',
618
+ ' inset 0 1px 0 0 rgba(255,255,255,0.8),',
619
+ ' 0 1px 4px rgba(0,0,0,0.04);',
620
+ ' transition: all 0.2s cubic-bezier(0.22,1,0.36,1);',
621
+ ' outline: none;',
622
+ '}',
623
+ '.wakz-close:hover {',
624
+ ' background: rgba(255,80,80,0.08);',
625
+ ' color: #ef4444;',
626
+ ' transform: scale(1.1);',
627
+ ' box-shadow:',
628
+ ' inset 0 1px 0 0 rgba(255,255,255,0.8),',
629
+ ' 0 2px 8px rgba(239,68,68,0.1);',
630
+ '}',
631
+ '.wakz-close:active { transform: scale(0.92); }',
632
+ '.wakz-close svg { width: 14px; height: 14px; }',
633
+
634
+ /* ══════════════════════════════════════════════════
635
+ MESSAGES AREA — scrollable
636
+ ══════════════════════════════════════════════════ */
637
+ '.wakz-msgs {',
638
+ ' flex: 1;',
639
+ ' overflow-y: auto;',
640
+ ' overflow-x: hidden;',
641
+ ' padding: 0;',
642
+ ' background: var(--wakz-chat-bg);',
643
+ ' padding-top: 76px;',
644
+ ' padding-bottom: 100px;',
645
+ ' padding-right: 14px;',
646
+ ' padding-left: 14px;',
647
+ ' scroll-behavior: smooth;',
648
+ ' position: relative;',
649
+ '}',
650
+ '.wakz-msgs::-webkit-scrollbar { width: 3px; }',
651
+ '.wakz-msgs::-webkit-scrollbar-track { background: transparent; }',
652
+ '.wakz-msgs::-webkit-scrollbar-thumb {',
653
+ ' background: rgba(0,0,0,0.08);',
654
+ ' border-radius: 99px;',
655
+ '}',
656
+
657
+ '.wakz-msgs-inner {',
658
+ ' display: flex;',
659
+ ' flex-direction: column;',
660
+ ' gap: 6px;',
661
+ '}',
662
+
663
+ /* ══════════════════════════════════════════════════
664
+ MESSAGE ROWS
665
+ ══════════════════════════════════════════════════ */
666
+ '.wakz-msg-row {',
667
+ ' display: flex;',
668
+ ' align-items: flex-end;',
669
+ ' width: 100%;',
670
+ ' animation: wakz-fade-slide-up 0.3s cubic-bezier(0.22,1,0.36,1) both;',
671
+ '}',
672
+
673
+ /* ── USER MESSAGE — dark bubble ── */
674
+ '.wakz-msg-row--user { justify-content: flex-end; }',
675
+ '.wakz-user-card {',
676
+ ' max-width: 78%;',
677
+ ' padding: 10px 14px 8px;',
678
+ ' background: var(--wakz-user-bubble);',
679
+ ' color: var(--wakz-user-text);',
680
+ ' border-radius: 18px 18px 5px 18px;',
681
+ ' box-shadow: 0 2px 10px rgba(0,0,0,0.06), 0 1px 3px rgba(0,0,0,0.03);',
682
+ '}',
683
+ '.wakz-user-text {',
684
+ ' font-size: 13.5px;',
685
+ ' font-weight: 400;',
686
+ ' line-height: 1.7;',
687
+ ' white-space: pre-wrap;',
688
+ ' word-break: break-word;',
689
+ ' text-align: right;',
690
+ ' font-family: "Tajawal", "Cairo", sans-serif;',
691
+ '}',
692
+ '.wakz-user-time {',
693
+ ' font-size: 9.5px;',
694
+ ' color: var(--wakz-user-text);',
695
+ ' opacity: 0.4;',
696
+ ' display: block;',
697
+ ' margin-top: 4px;',
698
+ ' text-align: left;',
699
+ ' font-family: "Tajawal", sans-serif;',
700
+ '}',
701
+
702
+ /* ── BOT MESSAGE — no bubble, on background ── */
703
+ '.wakz-msg-row--bot { justify-content: flex-start; }',
704
+ '.wakz-bot-content {',
705
+ ' max-width: 78%;',
706
+ ' display: flex;',
707
+ ' flex-direction: column;',
708
+ ' align-items: flex-end;',
709
+ ' gap: 1px;',
710
+ ' padding: 6px 8px;',
711
+ ' border-radius: 16px;',
712
+ ' background: var(--wakz-bot-bubble);',
713
+ '}',
714
+ '.wakz-bot-label {',
715
+ ' font-size: 10px;',
716
+ ' font-weight: 700;',
717
+ ' color: #aaa;',
718
+ ' letter-spacing: 0.1px;',
719
+ ' padding: 0 4px;',
720
+ ' font-family: "Cairo", sans-serif;',
721
+ '}',
722
+ '.wakz-bot-text {',
723
+ ' font-size: 13.5px;',
724
+ ' font-weight: 400;',
725
+ ' line-height: 1.75;',
726
+ ' color: var(--wakz-bot-text);',
727
+ ' white-space: pre-wrap;',
728
+ ' word-break: break-word;',
729
+ ' text-align: right;',
730
+ ' font-family: "Tajawal", "Cairo", sans-serif;',
731
+ ' padding: 0 4px;',
732
+ '}',
733
+ '.wakz-bot-time {',
734
+ ' font-size: 9.5px;',
735
+ ' color: #cccccc;',
736
+ ' display: block;',
737
+ ' margin-top: 1px;',
738
+ ' text-align: left;',
739
+ ' padding-left: 4px;',
740
+ ' font-family: "Tajawal", sans-serif;',
741
+ '}',
742
+
743
+ /* ── SUPPORT MESSAGE — green accent ── */
744
+ '.wakz-msg-row--support { justify-content: flex-start; }',
745
+ '.wakz-support-content {',
746
+ ' max-width: 78%;',
747
+ ' display: flex;',
748
+ ' flex-direction: column;',
749
+ ' align-items: flex-end;',
750
+ ' gap: 1px;',
751
+ ' border-right: 2.5px solid #22c55e;',
752
+ ' padding-right: 10px;',
753
+ '}',
754
+ '.wakz-support-label {',
755
+ ' font-size: 10px;',
756
+ ' font-weight: 700;',
757
+ ' color: #16a34a;',
758
+ ' letter-spacing: 0.1px;',
759
+ ' padding: 0 4px;',
760
+ ' font-family: "Cairo", sans-serif;',
761
+ ' display: flex;',
762
+ ' align-items: center;',
763
+ ' gap: 4px;',
764
+ '}',
765
+ '.wakz-support-label-dot {',
766
+ ' width: 4px;',
767
+ ' height: 4px;',
768
+ ' border-radius: 50%;',
769
+ ' background: #22c55e;',
770
+ ' flex-shrink: 0;',
771
+ '}',
772
+ '.wakz-support-text {',
773
+ ' font-size: 13.5px;',
774
+ ' font-weight: 400;',
775
+ ' line-height: 1.75;',
776
+ ' color: #333333;',
777
+ ' white-space: pre-wrap;',
778
+ ' word-break: break-word;',
779
+ ' text-align: right;',
780
+ ' font-family: "Tajawal", "Cairo", sans-serif;',
781
+ ' padding: 0 4px;',
782
+ '}',
783
+ '.wakz-support-time {',
784
+ ' font-size: 9.5px;',
785
+ ' color: #cccccc;',
786
+ ' display: block;',
787
+ ' margin-top: 1px;',
788
+ ' text-align: left;',
789
+ ' padding-left: 4px;',
790
+ ' font-family: "Tajawal", sans-serif;',
791
+ '}',
792
+
793
+ /* ── Error Messages ── */
794
+ '.wakz-error-bubble {',
795
+ ' max-width: 78%;',
796
+ ' padding: 10px 14px;',
797
+ ' font-size: 13.5px;',
798
+ ' line-height: 1.6;',
799
+ ' white-space: pre-wrap;',
800
+ ' word-break: break-word;',
801
+ ' border-radius: 14px;',
802
+ ' border-bottom-left-radius: 4px;',
803
+ ' background: #FEF2F2;',
804
+ ' color: #DC2626;',
805
+ ' border: 1px solid #FECACA;',
806
+ ' font-family: "Tajawal", "Cairo", sans-serif;',
807
+ '}',
808
+
809
+ /* ── Retry Button ── */
810
+ '.wakz-retry {',
811
+ ' display: inline-flex;',
812
+ ' align-items: center;',
813
+ ' gap: 4px;',
814
+ ' margin-top: 8px;',
815
+ ' padding: 5px 14px;',
816
+ ' font-size: 12px;',
817
+ ' font-weight: 500;',
818
+ ' border-radius: 20px;',
819
+ ' border: 1px solid #fca5a5;',
820
+ ' background: #ffffff;',
821
+ ' color: #dc2626;',
822
+ ' cursor: pointer;',
823
+ ' font-family: "Cairo", "Tajawal", sans-serif;',
824
+ ' transition: background 0.2s ease, border-color 0.2s ease;',
825
+ ' outline: none;',
826
+ '}',
827
+ '.wakz-retry:hover { background: #fef2f2; border-color: #f87171; }',
828
+
829
+ /* ══════════════════════════════════════════════════
830
+ TYPING INDICATOR
831
+ ══════════════════════════════════════════════════ */
832
+ '.wakz-typing-row {',
833
+ ' display: flex;',
834
+ ' align-items: flex-end;',
835
+ ' justify-content: flex-start;',
836
+ ' width: 100%;',
837
+ ' animation: wakz-fade-slide-up 0.25s ease both;',
838
+ '}',
839
+ '.wakz-typing-content {',
840
+ ' display: flex;',
841
+ ' flex-direction: column;',
842
+ ' align-items: flex-end;',
843
+ ' gap: 3px;',
844
+ '}',
845
+ '.wakz-typing-label {',
846
+ ' font-size: 10px;',
847
+ ' font-weight: 700;',
848
+ ' color: #aaa;',
849
+ ' padding: 0 4px;',
850
+ ' font-family: "Cairo", sans-serif;',
851
+ '}',
852
+ '.wakz-typing-dots {',
853
+ ' display: flex;',
854
+ ' align-items: center;',
855
+ ' gap: 4px;',
856
+ ' padding: 8px 12px;',
857
+ '}',
858
+ '.wakz-typing-dots span {',
859
+ ' width: 6px;',
860
+ ' height: 6px;',
861
+ ' border-radius: 50%;',
862
+ ' background: #bbb;',
863
+ ' animation: wakz-typing-bounce 1.4s ease-in-out infinite;',
864
+ '}',
865
+ '.wakz-typing-dots span:nth-child(1) { animation-delay: 0s; }',
866
+ '.wakz-typing-dots span:nth-child(2) { animation-delay: 0.18s; }',
867
+ '.wakz-typing-dots span:nth-child(3) { animation-delay: 0.36s; }',
868
+
869
+ /* ══════════════════════════════════════════════════
870
+ FLOATING INPUT WRAP — absolute bottom
871
+ ══════════════════════════════════════════════════ */
872
+ '.wakz-input-wrap {',
873
+ ' position: absolute;',
874
+ ' bottom: 12px;',
875
+ ' right: 12px;',
876
+ ' left: 12px;',
877
+ ' z-index: 100;',
878
+ ' pointer-events: none;',
879
+ '}',
880
+ '.wakz-input-row {',
881
+ ' pointer-events: all;',
882
+ ' width: 100%;',
883
+ ' display: flex;',
884
+ ' align-items: center;',
885
+ ' gap: 8px;',
886
+ '}',
887
+
888
+ /* ── Input Glass Pill ── */
889
+ '.wakz-input-glass {',
890
+ ' flex: 1;',
891
+ ' display: flex;',
892
+ ' align-items: center;',
893
+ ' position: relative;',
894
+ ' overflow: hidden;',
895
+ ' backdrop-filter: blur(40px) saturate(180%) brightness(102%);',
896
+ ' -webkit-backdrop-filter: blur(40px) saturate(180%) brightness(102%);',
897
+ ' background: rgba(255,255,255,0.85);',
898
+ ' border: 0.5px solid rgba(255,255,255,0.7);',
899
+ ' border-radius: 9999px;',
900
+ ' box-shadow:',
901
+ ' inset 0 1px 0 0 rgba(255,255,255,0.9),',
902
+ ' inset 0 -1px 0 0 rgba(0,0,0,0.04),',
903
+ ' 0 0 0 0.5px rgba(0,0,0,0.06),',
904
+ ' 0 8px 32px rgba(0,0,0,0.08),',
905
+ ' 0 2px 8px rgba(0,0,0,0.04);',
906
+ ' padding: 8px 16px;',
907
+ '}',
908
+
909
+ /* ── Input specular highlight ── */
910
+ '.wakz-input-specular {',
911
+ ' position: absolute;',
912
+ ' top: 0;',
913
+ ' left: 10%;',
914
+ ' right: 10%;',
915
+ ' height: 1px;',
916
+ ' background: linear-gradient(',
917
+ ' 90deg,',
918
+ ' transparent,',
919
+ ' rgba(255,255,255,0.5) 20%,',
920
+ ' rgba(255,255,255,0.8) 50%,',
921
+ ' rgba(255,255,255,0.5) 80%,',
922
+ ' transparent',
923
+ ' );',
924
+ ' border-radius: inherit;',
925
+ ' pointer-events: none;',
926
+ ' z-index: 1;',
927
+ '}',
928
+
929
+ /* ── Textarea ── */
930
+ '.wakz-input {',
931
+ ' flex: 1;',
932
+ ' resize: none;',
933
+ ' border: none;',
934
+ ' outline: none;',
935
+ ' background: transparent;',
936
+ ' font-family: "Tajawal", "Cairo", sans-serif;',
937
+ ' font-size: 13.5px;',
938
+ ' font-weight: 400;',
939
+ ' color: #111;',
940
+ ' line-height: 1.5;',
941
+ ' padding: 1px 4px;',
942
+ ' max-height: 48px;',
943
+ ' overflow-y: auto;',
944
+ ' align-self: center;',
945
+ '}',
946
+ '.wakz-input::placeholder {',
947
+ ' color: #bbb;',
948
+ ' font-weight: 400;',
949
+ ' font-family: "Cairo", "Tajawal", sans-serif;',
950
+ '}',
951
+ '.wakz-input:focus { outline: none; }',
952
+ '.wakz-input::-webkit-scrollbar { width: 3px; }',
953
+ '.wakz-input::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.1); border-radius: 99px; }',
954
+
955
+ /* ── Send Button — 40px circle, separate ── */
956
+ '.wakz-send {',
957
+ ' flex-shrink: 0;',
958
+ ' width: 40px;',
959
+ ' height: 40px;',
960
+ ' min-width: 40px;',
961
+ ' min-height: 40px;',
962
+ ' border-radius: 50%;',
963
+ ' border: none;',
964
+ ' cursor: pointer;',
965
+ ' display: flex;',
966
+ ' align-items: center;',
967
+ ' justify-content: center;',
968
+ ' position: relative;',
969
+ ' overflow: hidden;',
970
+ ' transition: all 0.25s cubic-bezier(0.22,1,0.36,1);',
971
+ ' outline: none;',
972
+ ' backdrop-filter: blur(40px) saturate(180%) brightness(102%);',
973
+ ' -webkit-backdrop-filter: blur(40px) saturate(180%) brightness(102%);',
974
+ ' background: rgba(255,255,255,0.82);',
975
+ ' border: 0.5px solid rgba(255,255,255,0.7);',
976
+ ' box-shadow:',
977
+ ' inset 0 1px 0 0 rgba(255,255,255,0.9),',
978
+ ' inset 0 -1px 0 0 rgba(0,0,0,0.04),',
979
+ ' 0 0 0 0.5px rgba(0,0,0,0.06),',
980
+ ' 0 4px 16px rgba(0,0,0,0.06);',
981
+ ' color: #bbb;',
982
+ '}',
983
+ '.wakz-send:disabled { cursor: default; }',
984
+ '.wakz-send svg {',
985
+ ' width: 18px;',
986
+ ' height: 18px;',
987
+ ' pointer-events: none;',
988
+ '}',
989
+ /* ── Send Active State ── */
990
+ '.wakz-send--active {',
991
+ ' background: var(--wakz-primary);',
992
+ ' border-color: var(--wakz-primary);',
993
+ ' color: #ffffff;',
994
+ ' filter: none;',
995
+ ' backdrop-filter: none;',
996
+ ' -webkit-backdrop-filter: none;',
997
+ ' box-shadow: 0 4px 20px rgba(0,0,0,0.22), 0 1px 6px rgba(0,0,0,0.12);',
998
+ '}',
999
+ '.wakz-send--active:hover:not(:disabled) {',
1000
+ ' background: var(--wakz-user-bubble);',
1001
+ ' transform: scale(1.08);',
1002
+ ' box-shadow: 0 6px 24px rgba(0,0,0,0.28), 0 2px 8px rgba(0,0,0,0.14);',
1003
+ '}',
1004
+ '.wakz-send--active:active:not(:disabled) { transform: scale(0.93); }',
1005
+
1006
+ /* ══════════════════════════════════════════════════
1007
+ RTL SUPPORT
1008
+ ══════════════════════════════════════════════════ */
1009
+ '.wakz-rtl { direction: rtl; }',
1010
+ '.wakz-rtl .wakz-user-card {',
1011
+ ' border-radius: 18px 18px 18px 5px;',
1012
+ '}',
1013
+ '.wakz-rtl .wakz-error-bubble {',
1014
+ ' border-bottom-left-radius: 14px;',
1015
+ ' border-bottom-right-radius: 4px;',
1016
+ '}',
1017
+
1018
+ /* ══════════════════════════════════════════════════
1019
+ FULLSCREEN MODE — overrides window to fill viewport
1020
+ ══════════════════════════════════════════════════ */
1021
+ '.wakz-mode-fullscreen {',
1022
+ ' width: 100vw !important;',
1023
+ ' height: 100vh !important;',
1024
+ ' max-width: 100vw !important;',
1025
+ ' max-height: 100vh !important;',
1026
+ ' top: 0 !important;',
1027
+ ' left: 0 !important;',
1028
+ ' transform: none !important;',
1029
+ ' border-radius: 0 !important;',
1030
+ ' box-shadow: none !important;',
1031
+ '}',
1032
+ '.wakz-mode-fullscreen.wakz-visible {',
1033
+ ' transform: none !important;',
1034
+ '}',
1035
+ /* Hide overlay in fullscreen mode — not needed */
1036
+ '.wakz-mode-fullscreen ~ .wakz-overlay {',
1037
+ ' display: none !important;',
1038
+ '}',
1039
+
1040
+ /* ══════════════════════════════════════════════════
1041
+ RESPONSIVE
1042
+ ══════════════════════════════════════════════════ */
1043
+ '@media (max-width: 440px) {',
1044
+ ' .wakz-window {',
1045
+ ' width: 100% !important;',
1046
+ ' height: 100% !important;',
1047
+ ' max-width: 100% !important;',
1048
+ ' max-height: 100% !important;',
1049
+ ' border-radius: 0 !important;',
1050
+ ' box-shadow: none !important;',
1051
+ ' }',
1052
+ '}',
1053
+ '@media (max-width: 380px) {',
1054
+ ' .wakz-hdr-title { font-size: 13px; }',
1055
+ ' .wakz-status-pill span:last-child { display: none; }',
1056
+ ' .wakz-status-pill { padding: 4px 8px; }',
1057
+ ' .wakz-send { width: 36px; height: 36px; min-width: 36px; min-height: 36px; }',
1058
+ ' .wakz-close { width: 26px; height: 26px; min-width: 26px; min-height: 26px; }',
1059
+ '}'
1060
+ ].join('\n');
1061
+ };
1062
+
1063
+ /* ════════════════════════════════════════════════════════════════
1064
+ DOM CREATION — v4.0.0 Liquid Glass structure
1065
+ ════════════════════════════════════════════════════════════════ */
1066
+
1067
+ WAKZWidget.prototype._createDOM = function () {
1068
+ var self = this;
1069
+ var isRtl = self.config.language === 'ar';
1070
+ var posClass = self.config.position === 'bottom-left' ? 'bl' : 'br';
1071
+ var str = _strings(self.config.language);
1072
+
1073
+ /* ── Host + Shadow DOM ── */
1074
+ self._host = _el('div');
1075
+ document.body.appendChild(self._host);
1076
+ self._shadow = self._host.attachShadow({ mode: 'closed' });
1077
+
1078
+ /* ── Google Fonts link (injected into shadow root) ── */
1079
+ var fontsLink = _el('link', {
1080
+ rel: 'stylesheet',
1081
+ href: 'https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700;900&family=Tajawal:wght@300;400;500;700&display=swap'
1082
+ });
1083
+ self._shadow.appendChild(fontsLink);
1084
+
1085
+ /* ── Root wrapper ── */
1086
+ self._root = _el('div', { className: 'wakz' + (isRtl ? ' wakz-rtl' : '') });
1087
+ self._shadow.appendChild(self._styleEl);
1088
+ self._shadow.appendChild(self._root);
1089
+
1090
+ /* ══════════════ FLOATING ACTION BUTTON (FAB) ══════════════ */
1091
+ self._toggleBtn = _el('button', {
1092
+ className: 'wakz-fab wakz-fab-pos-' + posClass,
1093
+ 'aria-label': str.openChat
1094
+ });
1095
+ self._toggleBtn.innerHTML = _ICONS.chatBubble;
1096
+
1097
+ /* Status dot on FAB (10px) */
1098
+ self._statusDot = _el('span', { className: 'wakz-fab-dot ' + (self.config.online ? 'online' : 'offline') });
1099
+ self._statusDot.style.display = self.config.showStatus ? '' : 'none';
1100
+ self._toggleBtn.appendChild(self._statusDot);
1101
+ self._root.appendChild(self._toggleBtn);
1102
+
1103
+ /* ══════════════ OVERLAY / BACKDROP ══════════════ */
1104
+ self._overlay = _el('div', { className: 'wakz-overlay' });
1105
+ self._root.appendChild(self._overlay);
1106
+
1107
+ /* ══════════════ CHAT WINDOW ══════════════ */
1108
+ self._chatWindow = _el('div', { className: 'wakz-window' });
1109
+
1110
+ /* ── Messages Container (with floating header + input inside) ── */
1111
+ self._messagesContainer = _el('div', { className: 'wakz-msgs' });
1112
+
1113
+ /* ══════════════ FLOATING HEADER (pill, absolute over messages) ══════════════ */
1114
+ var headerWrap = _el('div', { className: 'wakz-header-wrap' });
1115
+ self._headerEl = _el('div', { className: 'wakz-header' });
1116
+ var header = self._headerEl;
1117
+
1118
+ /* Specular highlight (real DOM element, not ::before) */
1119
+ header.appendChild(_el('div', { className: 'wakz-specular' }));
1120
+
1121
+ /* Brand side (right in RTL) */
1122
+ var brand = _el('div', { className: 'wakz-hdr-brand' });
1123
+ var info = _el('div', { className: 'wakz-hdr-info' });
1124
+ self._headerTitleEl = _el('span', { className: 'wakz-hdr-title' }, [self.config.botName]);
1125
+ info.appendChild(self._headerTitleEl);
1126
+
1127
+ var subtitleText = self.config.botSubtitle || str.defaultSubtitle || '';
1128
+ self._headerSubtitleEl = _el('span', { className: 'wakz-hdr-subtitle' }, [subtitleText]);
1129
+ info.appendChild(self._headerSubtitleEl);
1130
+
1131
+ brand.appendChild(info);
1132
+ header.appendChild(brand);
1133
+
1134
+ /* Actions side (left in RTL) */
1135
+ var actions = _el('div', { className: 'wakz-hdr-actions' });
1136
+
1137
+ /* Status pill */
1138
+ self._headerStatusPill = _el('div', {
1139
+ className: 'wakz-status-pill ' + (self.config.online ? 'wakz-status-pill--online' : 'wakz-status-pill--offline')
1140
+ });
1141
+ if (self.config.showStatus) {
1142
+ self._headerStatusDot = _el('span', { className: 'wakz-status-dot' });
1143
+ self._headerStatusPill.appendChild(self._headerStatusDot);
1144
+ self._headerStatusText = document.createTextNode(self.config.online ? str.online : str.offline);
1145
+ self._headerStatusPill.appendChild(self._headerStatusText);
1146
+ }
1147
+ actions.appendChild(self._headerStatusPill);
1148
+
1149
+ /* Close button (glass circle) */
1150
+ self._closeBtn = _el('button', { className: 'wakz-close', 'aria-label': str.closeChat });
1151
+ self._closeBtn.innerHTML = _ICONS.close;
1152
+ actions.appendChild(self._closeBtn);
1153
+
1154
+ header.appendChild(actions);
1155
+ headerWrap.appendChild(header);
1156
+ self._messagesContainer.appendChild(headerWrap);
1157
+
1158
+ /* ── Messages inner (scrollable content) ── */
1159
+ self._messagesInner = _el('div', { className: 'wakz-msgs-inner' });
1160
+ self._messagesContainer.appendChild(self._messagesInner);
1161
+
1162
+ /* ══════════════ FLOATING INPUT (pill, absolute bottom) ══════════════ */
1163
+ self._inputWrap = _el('div', { className: 'wakz-input-wrap' });
1164
+ var inputRow = _el('div', { className: 'wakz-input-row' });
1165
+
1166
+ /* Input glass pill */
1167
+ var inputGlass = _el('div', { className: 'wakz-input-glass' });
1168
+
1169
+ /* Specular highlight for input glass */
1170
+ inputGlass.appendChild(_el('div', { className: 'wakz-input-specular' }));
1171
+
1172
+ self._inputEl = _el('textarea', {
1173
+ className: 'wakz-input',
1174
+ placeholder: str.placeholder,
1175
+ rows: 1,
1176
+ dir: isRtl ? 'rtl' : 'ltr'
1177
+ });
1178
+ inputGlass.appendChild(self._inputEl);
1179
+ inputRow.appendChild(inputGlass);
1180
+
1181
+ /* Send button (40px circle, separate from input) */
1182
+ self._sendBtn = _el('button', {
1183
+ className: 'wakz-send',
1184
+ 'aria-label': str.sendMessage
1185
+ });
1186
+ self._sendBtn.innerHTML = _ICONS.send;
1187
+ inputRow.appendChild(self._sendBtn);
1188
+
1189
+ self._inputWrap.appendChild(inputRow);
1190
+ self._messagesContainer.appendChild(self._inputWrap);
1191
+
1192
+ self._chatWindow.appendChild(self._messagesContainer);
1193
+ self._root.appendChild(self._chatWindow);
1194
+ };
1195
+
1196
+ /* ════════════════════════════════════════════════════════════════
1197
+ EVENT LISTENERS
1198
+ ════════════════════════════════════════════════════════════════ */
1199
+
1200
+ WAKZWidget.prototype._attachEvents = function () {
1201
+ var self = this;
1202
+
1203
+ /* Toggle button click */
1204
+ self._toggleBtn.addEventListener('click', function () {
1205
+ self.toggleChat(!self.isOpen);
1206
+ });
1207
+
1208
+ /* Overlay click to close */
1209
+ self._overlay.addEventListener('click', function () {
1210
+ self.toggleChat(false);
1211
+ });
1212
+
1213
+ /* Close button click */
1214
+ self._closeBtn.addEventListener('click', function () {
1215
+ self.toggleChat(false);
1216
+ });
1217
+
1218
+ /* Send button click */
1219
+ self._sendBtn.addEventListener('click', function () {
1220
+ self._handleSend();
1221
+ });
1222
+
1223
+ /* Enter key to send, Shift+Enter for newline */
1224
+ self._inputEl.addEventListener('keydown', function (e) {
1225
+ if (e.key === 'Enter' && !e.shiftKey) {
1226
+ e.preventDefault();
1227
+ if (!self._sendBtn.disabled) self._handleSend();
1228
+ }
1229
+ });
1230
+
1231
+ /* Auto-resize textarea (max 48px) + toggle send active state */
1232
+ self._inputEl.addEventListener('input', function () {
1233
+ self._inputEl.style.height = 'auto';
1234
+ self._inputEl.style.height = Math.min(self._inputEl.scrollHeight, 48) + 'px';
1235
+ var hasText = (self._inputEl.value || '').trim().length > 0;
1236
+ if (hasText) {
1237
+ self._sendBtn.classList.add('wakz-send--active');
1238
+ } else {
1239
+ self._sendBtn.classList.remove('wakz-send--active');
1240
+ }
1241
+ });
1242
+
1243
+ /* Escape to close */
1244
+ document.addEventListener('keydown', function (e) {
1245
+ if (e.key === 'Escape' && self.isOpen) self.toggleChat(false);
1246
+ });
1247
+ };
1248
+
1249
+ /* ════════════════════════════════════════════════════════════════
1250
+ FAB ENTRANCE ANIMATION
1251
+ ════════════════════════════════════════════════════════════════ */
1252
+
1253
+ WAKZWidget.prototype._playFabEntrance = function () {
1254
+ var self = this;
1255
+ self._toggleBtn.style.opacity = '0';
1256
+ setTimeout(function () {
1257
+ self._toggleBtn.style.opacity = '';
1258
+ self._toggleBtn.classList.add('wakz-fab-enter');
1259
+ }, 800);
1260
+ };
1261
+
1262
+ /* ════════════════════════════════════════════════════════════════
1263
+ TOGGLE CHAT OPEN / CLOSE
1264
+ ════════════════════════════════════════════════════════════════ */
1265
+
1266
+ WAKZWidget.prototype.toggleChat = function (open) {
1267
+ var self = this;
1268
+ self.isOpen = open;
1269
+ if (open) {
1270
+ self._chatWindow.classList.add('wakz-visible');
1271
+ self._overlay.classList.add('wakz-visible');
1272
+ self._toggleBtn.classList.add('wakz-fab-hidden');
1273
+ /* Re-fetch config on every open to get latest customizations */
1274
+ self._fetchConfig();
1275
+ /* Fetch history on first open (after config is loaded) */
1276
+ if (self._configLoaded && !self._hasFetchedHistory) {
1277
+ self._fetchHistory();
1278
+ }
1279
+ /* Show welcome message if no messages yet */
1280
+ if (self._configLoaded && self.messages.length === 0 && self.config.welcomeMessage) {
1281
+ self._appendWelcomeMessage(self.config.welcomeMessage);
1282
+ }
1283
+ /* Start polling for new messages */
1284
+ self._startPolling();
1285
+ setTimeout(function () { self._inputEl.focus(); }, 320);
1286
+ } else {
1287
+ self._chatWindow.classList.remove('wakz-visible');
1288
+ self._overlay.classList.remove('wakz-visible');
1289
+ self._toggleBtn.classList.remove('wakz-fab-hidden');
1290
+ /* Stop polling when chat is closed */
1291
+ self._stopPolling();
1292
+ }
1293
+ };
1294
+
1295
+ /* ════════════════════════════════════════════════════════════════
1296
+ FETCH WIDGET CONFIG
1297
+ ════════════════════════════════════════════════════════════════ */
1298
+
1299
+ WAKZWidget.prototype._fetchConfig = function () {
1300
+ var self = this;
1301
+ if (!self.server || !self.apiKey) {
1302
+ self._handleConfigError();
1303
+ return;
1304
+ }
1305
+
1306
+ var url = self.server + '/api/v1/embed/config?key=' + encodeURIComponent(self.apiKey);
1307
+
1308
+ _fetchWithTimeout(url, {
1309
+ method: 'GET',
1310
+ headers: { 'Accept': 'application/json' }
1311
+ }, 30000)
1312
+ .then(function (res) {
1313
+ if (!res.ok) {
1314
+ self._handleConfigError(true);
1315
+ return null;
1316
+ }
1317
+ return res.json();
1318
+ })
1319
+ .then(function (data) {
1320
+ if (data && data.success && data.config) {
1321
+ self.config = Object.assign({}, _DEFAULTS, data.config);
1322
+ self._configLoaded = true;
1323
+ self._applyConfig();
1324
+ } else if (data !== null) {
1325
+ self._handleConfigError(true);
1326
+ }
1327
+ })
1328
+ .catch(function (err) {
1329
+ self._handleConfigError(false);
1330
+ });
1331
+ };
1332
+
1333
+ /** Handle config fetch failure — show offline state */
1334
+ WAKZWidget.prototype._handleConfigError = function (isPermanent) {
1335
+ var self = this;
1336
+ self._configError = true;
1337
+ self.config.online = false;
1338
+
1339
+ /* Hide FAB entirely if key is permanently invalid */
1340
+ if (isPermanent && self._toggleBtn) {
1341
+ self._toggleBtn.style.display = 'none';
1342
+ }
1343
+
1344
+ /* Update FAB dot to red */
1345
+ if (self._statusDot) {
1346
+ self._statusDot.className = 'wakz-fab-dot offline';
1347
+ self._statusDot.style.display = '';
1348
+ }
1349
+ /* Update header status pill to offline */
1350
+ if (self._headerStatusPill) {
1351
+ self._headerStatusPill.className = 'wakz-status-pill wakz-status-pill--offline';
1352
+ self._headerStatusPill.style.display = '';
1353
+ }
1354
+ if (self._headerStatusDot) {
1355
+ self._headerStatusDot.className = 'wakz-status-dot';
1356
+ }
1357
+ if (self._headerStatusText) {
1358
+ self._headerStatusText.textContent = _strings(self.config.language).offline;
1359
+ }
1360
+ };
1361
+
1362
+ /** Apply loaded config to the DOM */
1363
+ WAKZWidget.prototype._applyConfig = function () {
1364
+ var self = this;
1365
+ var cfg = self.config;
1366
+ var str = _strings(cfg.language);
1367
+ var isRtl = cfg.language === 'ar';
1368
+ var posClass = cfg.position === 'bottom-left' ? 'bl' : 'br';
1369
+
1370
+ /* ── Update CSS custom properties ── */
1371
+ var hostStyle = self._shadow.host.style;
1372
+ hostStyle.setProperty('--wakz-primary', cfg.primaryColor);
1373
+ hostStyle.setProperty('--wakz-btn', cfg.btnColor);
1374
+ hostStyle.setProperty('--wakz-chat-bg', cfg.chatBg);
1375
+ hostStyle.setProperty('--wakz-widget-bg', cfg.widgetBg);
1376
+ hostStyle.setProperty('--wakz-bot-bubble', cfg.botBubbleColor || '#ffffff');
1377
+ hostStyle.setProperty('--wakz-bot-text', cfg.botTextColor || '#333333');
1378
+ hostStyle.setProperty('--wakz-user-bubble', cfg.userBubbleColor || '#111111');
1379
+ hostStyle.setProperty('--wakz-user-text', cfg.userTextColor || '#ffffff');
1380
+
1381
+ /* ── RTL ── */
1382
+ if (isRtl) self._root.classList.add('wakz-rtl');
1383
+ else self._root.classList.remove('wakz-rtl');
1384
+
1385
+ /* ── FAB ── */
1386
+ self._toggleBtn.className = 'wakz-fab wakz-fab-pos-' + posClass + ' wakz-fab-enter' + (self.isOpen ? ' wakz-fab-hidden' : '');
1387
+ self._toggleBtn.setAttribute('aria-label', str.openChat);
1388
+
1389
+ /* ── FAB Status Dot ── */
1390
+ self._statusDot.className = 'wakz-fab-dot ' + (cfg.online ? 'online' : 'offline');
1391
+ self._statusDot.style.display = cfg.showStatus ? '' : 'none';
1392
+
1393
+ /* ── Window ── */
1394
+ self._chatWindow.className = 'wakz-window' + (self.isOpen ? ' wakz-visible' : '');
1395
+
1396
+ /* ── Overlay ── */
1397
+ self._overlay.className = 'wakz-overlay' + (self.isOpen ? ' wakz-visible' : '');
1398
+
1399
+ /* ── Header Title & Subtitle (DYNAMIC from config) ── */
1400
+ if (self._headerTitleEl) self._headerTitleEl.textContent = cfg.botName;
1401
+ if (self._headerSubtitleEl) {
1402
+ var sub = cfg.botSubtitle || str.defaultSubtitle || '';
1403
+ self._headerSubtitleEl.textContent = sub;
1404
+ }
1405
+
1406
+ /* ── Header Status Pill ── */
1407
+ if (self._headerStatusPill) {
1408
+ self._headerStatusPill.className = 'wakz-status-pill ' + (cfg.online ? 'wakz-status-pill--online' : 'wakz-status-pill--offline');
1409
+ self._headerStatusPill.style.display = cfg.showStatus ? '' : 'none';
1410
+ /* Rebuild pill content if needed */
1411
+ if (self._headerStatusDot) {
1412
+ self._headerStatusDot.className = 'wakz-status-dot';
1413
+ }
1414
+ if (self._headerStatusText) {
1415
+ self._headerStatusText.textContent = cfg.online ? str.online : str.offline;
1416
+ }
1417
+ }
1418
+
1419
+ /* ── Input ── */
1420
+ self._inputEl.placeholder = str.placeholder;
1421
+ self._inputEl.dir = isRtl ? 'rtl' : 'ltr';
1422
+
1423
+ /* ── Close button label ── */
1424
+ if (self._closeBtn) self._closeBtn.setAttribute('aria-label', str.closeChat);
1425
+
1426
+ /* ── Send button label ── */
1427
+ self._sendBtn.setAttribute('aria-label', str.sendMessage);
1428
+
1429
+ /* ── FAB Icon ── */
1430
+ var iconKey = cfg.fabIcon || 'chat';
1431
+ if (iconKey === 'chatBubble') iconKey = 'chat';
1432
+ if (_ICONS[iconKey]) {
1433
+ self._toggleBtn.innerHTML = _ICONS[iconKey];
1434
+ } else {
1435
+ self._toggleBtn.innerHTML = _ICONS.chatBubble;
1436
+ }
1437
+ /* Re-append status dot after innerHTML change */
1438
+ if (self._statusDot && self._statusDot.parentNode !== self._toggleBtn) {
1439
+ self._toggleBtn.appendChild(self._statusDot);
1440
+ }
1441
+
1442
+ /* ── FAB Shape ── */
1443
+ if (cfg.fabShape === 'pill') {
1444
+ self._toggleBtn.style.borderRadius = '9999px';
1445
+ self._toggleBtn.style.width = 'auto';
1446
+ self._toggleBtn.style.padding = '0 20px';
1447
+ } else {
1448
+ self._toggleBtn.style.borderRadius = '50%';
1449
+ self._toggleBtn.style.width = '56px';
1450
+ self._toggleBtn.style.padding = '0';
1451
+ }
1452
+
1453
+ /* ── Display Mode ── */
1454
+ if (cfg.displayMode === 'fullscreen') {
1455
+ self._chatWindow.classList.add('wakz-mode-fullscreen');
1456
+ self._chatWindow.classList.remove('wakz-mode-modal');
1457
+ } else {
1458
+ self._chatWindow.classList.add('wakz-mode-modal');
1459
+ self._chatWindow.classList.remove('wakz-mode-fullscreen');
1460
+ }
1461
+
1462
+ /* ── Auto Trigger ── */
1463
+ if (cfg.triggerMode === 'auto' && !self.isOpen) {
1464
+ var autoDelay = Math.max(1, Math.min(60, parseInt(cfg.triggerDelay) || 5)) * 1000;
1465
+ setTimeout(function () {
1466
+ if (!self.isOpen) self.toggleChat(true);
1467
+ }, autoDelay);
1468
+ }
1469
+
1470
+ /* ── Header Colors (only apply if explicitly customized) ── */
1471
+ if (cfg.headerColor) {
1472
+ self._headerEl.style.background = cfg.headerColor;
1473
+ }
1474
+ if (cfg.headerTextColor) {
1475
+ self._headerTitleEl.style.color = cfg.headerTextColor;
1476
+ self._headerSubtitleEl.style.color = cfg.headerTextColor + 'aa';
1477
+ self._closeBtn.style.color = cfg.headerTextColor;
1478
+ }
1479
+
1480
+ /* ── If chat is already open, show welcome message ── */
1481
+ if (self.isOpen && self.messages.length === 0 && cfg.welcomeMessage) {
1482
+ self._appendWelcomeMessage(cfg.welcomeMessage);
1483
+ }
1484
+ };
1485
+
1486
+ /* ════════════════════════════════════════════════════════════════
1487
+ FETCH CHAT HISTORY
1488
+ ════════════════════════════════════════════════════════════════ */
1489
+
1490
+ WAKZWidget.prototype._fetchHistory = function () {
1491
+ var self = this;
1492
+ if (!self.server || !self.apiKey) return;
1493
+
1494
+ self._hasFetchedHistory = true;
1495
+ var url = self.server + '/api/v1/embed/chat?api_key=' +
1496
+ encodeURIComponent(self.apiKey) + '&visitor_id=' + encodeURIComponent(self.visitorId);
1497
+
1498
+ _fetchWithTimeout(url, {
1499
+ method: 'GET',
1500
+ headers: { 'Accept': 'application/json' }
1501
+ }, 15000)
1502
+ .then(function (res) { return res.json(); })
1503
+ .then(function (data) {
1504
+ if (data && data.success && data.messages && data.messages.length > 0) {
1505
+ self._clearMessages();
1506
+ self._knownMessageIds = {};
1507
+ var msgs = data.messages;
1508
+ for (var i = 0; i < msgs.length; i++) {
1509
+ var m = msgs[i];
1510
+ if (m.id && self._knownMessageIds[m.id]) continue;
1511
+ if (m.id) self._knownMessageIds[m.id] = true;
1512
+ var role = m.role === 'user' ? 'user' : 'bot';
1513
+ self._appendMessage(role, m.content, false, m.createdAt, m.role === 'human_agent');
1514
+ }
1515
+ var lastMsg = msgs[msgs.length - 1];
1516
+ if (lastMsg && lastMsg.createdAt) {
1517
+ self._lastPollTime = lastMsg.createdAt;
1518
+ }
1519
+ }
1520
+ })
1521
+ .catch(function () {
1522
+ /* Silently ignore history fetch failures */
1523
+ });
1524
+ };
1525
+
1526
+ /* ════════════════════════════════════════════════════════════════
1527
+ POLLING — Fetch new messages periodically
1528
+ ════════════════════════════════════════════════════════════════ */
1529
+
1530
+ WAKZWidget.prototype._startPolling = function () {
1531
+ var self = this;
1532
+ self._stopPolling();
1533
+ if (!self._lastPollTime) {
1534
+ self._lastPollTime = new Date().toISOString();
1535
+ }
1536
+ self._pollTimer = setInterval(function () {
1537
+ self._pollForNewMessages();
1538
+ }, self._POLL_INTERVAL);
1539
+ };
1540
+
1541
+ WAKZWidget.prototype._stopPolling = function () {
1542
+ var self = this;
1543
+ if (self._pollTimer) {
1544
+ clearInterval(self._pollTimer);
1545
+ self._pollTimer = null;
1546
+ }
1547
+ };
1548
+
1549
+ WAKZWidget.prototype._pollForNewMessages = function () {
1550
+ var self = this;
1551
+ if (!self.server || !self.apiKey || !self._lastPollTime) return;
1552
+ /* Skip polling while waiting for our own reply */
1553
+ if (self._pendingReply) return;
1554
+
1555
+ var url = self.server + '/api/v1/embed/chat?api_key=' +
1556
+ encodeURIComponent(self.apiKey) +
1557
+ '&visitor_id=' + encodeURIComponent(self.visitorId) +
1558
+ '&since=' + encodeURIComponent(self._lastPollTime);
1559
+
1560
+ _fetchWithTimeout(url, {
1561
+ method: 'GET',
1562
+ headers: { 'Accept': 'application/json' }
1563
+ }, 10000)
1564
+ .then(function (res) { return res.json(); })
1565
+ .then(function (data) {
1566
+ if (data && data.success && data.messages && data.messages.length > 0) {
1567
+ var msgs = data.messages;
1568
+ for (var i = 0; i < msgs.length; i++) {
1569
+ var m = msgs[i];
1570
+ if (m.id && self._knownMessageIds[m.id]) continue;
1571
+ if (m.id) self._knownMessageIds[m.id] = true;
1572
+ var role = m.role === 'user' ? 'user' : 'bot';
1573
+ var isHumanAgent = m.role === 'human_agent';
1574
+ self._appendMessage(role, m.content, false, m.createdAt, isHumanAgent);
1575
+ }
1576
+ var lastMsg = msgs[msgs.length - 1];
1577
+ if (lastMsg && lastMsg.createdAt) {
1578
+ self._lastPollTime = lastMsg.createdAt;
1579
+ }
1580
+ }
1581
+ })
1582
+ .catch(function () {
1583
+ /* Silently ignore polling failures */
1584
+ });
1585
+ };
1586
+
1587
+ /* ════════════════════════════════════════════════════════════════
1588
+ CLEAR ALL MESSAGES
1589
+ ════════════════════════════════════════════════════════════════ */
1590
+
1591
+ WAKZWidget.prototype._clearMessages = function () {
1592
+ var self = this;
1593
+ /* Remove all children from messages-inner */
1594
+ if (self._messagesInner) {
1595
+ while (self._messagesInner.firstChild) {
1596
+ self._messagesInner.removeChild(self._messagesInner.firstChild);
1597
+ }
1598
+ }
1599
+ self.messages = [];
1600
+ self._knownMessageIds = {};
1601
+ };
1602
+
1603
+ /* ════════════════════════════════════════════════════════════════
1604
+ APPEND WELCOME MESSAGE — no bubble, matching bot messages
1605
+ ════════════════════════════════════════════════════════════════ */
1606
+
1607
+ WAKZWidget.prototype._appendWelcomeMessage = function (text) {
1608
+ var self = this;
1609
+ if (!text) return;
1610
+
1611
+ var row = _el('div', { className: 'wakz-msg-row wakz-msg-row--bot' });
1612
+
1613
+ var content = _el('div', { className: 'wakz-bot-content' });
1614
+
1615
+ /* Bot label */
1616
+ var label = _el('span', { className: 'wakz-bot-label' }, [self.config.botName]);
1617
+ content.appendChild(label);
1618
+
1619
+ /* Bot text */
1620
+ var textEl = _el('p', { className: 'wakz-bot-text' });
1621
+ textEl.textContent = text;
1622
+ content.appendChild(textEl);
1623
+
1624
+ row.appendChild(content);
1625
+ self._messagesInner.appendChild(row);
1626
+ self.messages.push({ sender: 'bot', text: text });
1627
+ self._scrollToBottom();
1628
+ };
1629
+
1630
+ /* ════════════════════════════════════════════════════════════════
1631
+ APPEND MESSAGE TO CHAT — v4 rendering
1632
+ ════════════════════════════════════════════════════════════════ */
1633
+
1634
+ WAKZWidget.prototype._appendMessage = function (sender, text, isError, timestamp, isHumanAgent) {
1635
+ var self = this;
1636
+
1637
+ /* ── Format timestamp ── */
1638
+ var tsText = '';
1639
+ if (timestamp) {
1640
+ try {
1641
+ var d = new Date(timestamp);
1642
+ if (!isNaN(d.getTime())) {
1643
+ tsText = d.getHours().toString().padStart(2, '0') + ':' +
1644
+ d.getMinutes().toString().padStart(2, '0');
1645
+ }
1646
+ } catch (e) { /* ignore */ }
1647
+ }
1648
+ if (!tsText) {
1649
+ var now = new Date();
1650
+ tsText = now.getHours().toString().padStart(2, '0') + ':' +
1651
+ now.getMinutes().toString().padStart(2, '0');
1652
+ }
1653
+
1654
+ if (sender === 'user') {
1655
+ /* ── USER MESSAGE: dark bubble (#111) ── */
1656
+ if (isError) {
1657
+ /* Error: red error bubble */
1658
+ var errRow = _el('div', { className: 'wakz-msg-row wakz-msg-row--user' });
1659
+ var errBubble = _el('div', { className: 'wakz-error-bubble' });
1660
+ errBubble.appendChild(document.createTextNode(text));
1661
+
1662
+ var errStr = _strings(self.config.language);
1663
+ var errRetryBtn = _el('button', { className: 'wakz-retry' }, [
1664
+ _ICONS.error + ' ' + errStr.retry
1665
+ ]);
1666
+ (function (rowCapture, errText) {
1667
+ errRetryBtn.addEventListener('click', function () {
1668
+ if (rowCapture.parentNode) rowCapture.parentNode.removeChild(rowCapture);
1669
+ for (var i = self.messages.length - 1; i >= 0; i--) {
1670
+ if (self.messages[i].text === errText && self.messages[i].isError) {
1671
+ self.messages.splice(i, 1);
1672
+ break;
1673
+ }
1674
+ }
1675
+ var lastUserMsg = null;
1676
+ for (var j = self.messages.length - 1; j >= 0; j--) {
1677
+ if (self.messages[j].sender === 'user') {
1678
+ lastUserMsg = self.messages[j].text;
1679
+ break;
1680
+ }
1681
+ }
1682
+ if (lastUserMsg) self._sendToAPI(lastUserMsg);
1683
+ });
1684
+ })(errRow, text);
1685
+ errBubble.appendChild(errRetryBtn);
1686
+ errRow.appendChild(errBubble);
1687
+ self._messagesInner.appendChild(errRow);
1688
+ self.messages.push({ sender: sender, text: text, isError: !!isError });
1689
+ } else {
1690
+ /* Normal user message: dark bubble */
1691
+ var userRow = _el('div', { className: 'wakz-msg-row wakz-msg-row--user' });
1692
+ var userCard = _el('div', { className: 'wakz-user-card' });
1693
+
1694
+ var userText = _el('p', { className: 'wakz-user-text' });
1695
+ userText.textContent = text; /* XSS safe: textContent */
1696
+ userCard.appendChild(userText);
1697
+
1698
+ userCard.appendChild(_el('span', { className: 'wakz-user-time' }, [tsText]));
1699
+ userRow.appendChild(userCard);
1700
+ self._messagesInner.appendChild(userRow);
1701
+ self.messages.push({ sender: sender, text: text, isError: false });
1702
+ }
1703
+ } else if (isHumanAgent) {
1704
+ /* ── SUPPORT MESSAGE: green accent ── */
1705
+ var supRow = _el('div', { className: 'wakz-msg-row wakz-msg-row--support' });
1706
+ var supContent = _el('div', { className: 'wakz-support-content' });
1707
+
1708
+ /* Support label with dot */
1709
+ var supLabel = _el('span', { className: 'wakz-support-label' });
1710
+ supLabel.appendChild(_el('span', { className: 'wakz-support-label-dot' }));
1711
+ var supStr = _strings(self.config.language);
1712
+ supLabel.appendChild(document.createTextNode(supStr.humanBadge || 'Support Team'));
1713
+ supContent.appendChild(supLabel);
1714
+
1715
+ /* Support text */
1716
+ var supText = _el('p', { className: 'wakz-support-text' });
1717
+ supText.textContent = text; /* XSS safe: textContent */
1718
+ supContent.appendChild(supText);
1719
+
1720
+ supContent.appendChild(_el('span', { className: 'wakz-support-time' }, [tsText]));
1721
+ supRow.appendChild(supContent);
1722
+ self._messagesInner.appendChild(supRow);
1723
+ self.messages.push({ sender: 'support', text: text, isError: false });
1724
+ } else {
1725
+ /* ── BOT MESSAGE: no bubble, on background ── */
1726
+ if (isError) {
1727
+ /* Error: red error bubble */
1728
+ var botErrRow = _el('div', { className: 'wakz-msg-row wakz-msg-row--bot' });
1729
+ var botErrBubble = _el('div', { className: 'wakz-error-bubble' });
1730
+ botErrBubble.appendChild(document.createTextNode(text));
1731
+
1732
+ var botErrStr = _strings(self.config.language);
1733
+ var botErrRetry = _el('button', { className: 'wakz-retry' }, [
1734
+ _ICONS.error + ' ' + botErrStr.retry
1735
+ ]);
1736
+ (function (rowCapture, errText) {
1737
+ botErrRetry.addEventListener('click', function () {
1738
+ if (rowCapture.parentNode) rowCapture.parentNode.removeChild(rowCapture);
1739
+ for (var i = self.messages.length - 1; i >= 0; i--) {
1740
+ if (self.messages[i].text === errText && self.messages[i].isError) {
1741
+ self.messages.splice(i, 1);
1742
+ break;
1743
+ }
1744
+ }
1745
+ var lastUserMsg = null;
1746
+ for (var j = self.messages.length - 1; j >= 0; j--) {
1747
+ if (self.messages[j].sender === 'user') {
1748
+ lastUserMsg = self.messages[j].text;
1749
+ break;
1750
+ }
1751
+ }
1752
+ if (lastUserMsg) self._sendToAPI(lastUserMsg);
1753
+ });
1754
+ })(botErrRow, text);
1755
+ botErrBubble.appendChild(botErrRetry);
1756
+ botErrRow.appendChild(botErrBubble);
1757
+ self._messagesInner.appendChild(botErrRow);
1758
+ self.messages.push({ sender: sender, text: text, isError: true });
1759
+ } else {
1760
+ /* Normal bot message: no bubble */
1761
+ var botRow = _el('div', { className: 'wakz-msg-row wakz-msg-row--bot' });
1762
+ var botContent = _el('div', { className: 'wakz-bot-content' });
1763
+
1764
+ /* Bot label */
1765
+ botContent.appendChild(_el('span', { className: 'wakz-bot-label' }, [self.config.botName]));
1766
+
1767
+ /* Bot text */
1768
+ var botTextEl = _el('p', { className: 'wakz-bot-text' });
1769
+ botTextEl.textContent = text; /* XSS safe: textContent */
1770
+ botContent.appendChild(botTextEl);
1771
+
1772
+ /* Timestamp */
1773
+ botContent.appendChild(_el('span', { className: 'wakz-bot-time' }, [tsText]));
1774
+
1775
+ botRow.appendChild(botContent);
1776
+ self._messagesInner.appendChild(botRow);
1777
+ self.messages.push({ sender: sender, text: text, isError: false });
1778
+ }
1779
+ }
1780
+
1781
+ self._scrollToBottom();
1782
+ };
1783
+
1784
+ /* ════════════════════════════════════════════════════════════════
1785
+ TYPING INDICATOR — dots with label, no bubble
1786
+ ════════════════════════════════════════════════════════════════ */
1787
+
1788
+ WAKZWidget.prototype._showTyping = function () {
1789
+ var self = this;
1790
+ if (self._typingEl) return;
1791
+
1792
+ self._typingEl = _el('div', { className: 'wakz-typing-row' });
1793
+
1794
+ var typingContent = _el('div', { className: 'wakz-typing-content' });
1795
+
1796
+ /* Typing label */
1797
+ typingContent.appendChild(_el('span', { className: 'wakz-typing-label' }, [self.config.botName]));
1798
+
1799
+ /* Bouncing dots */
1800
+ var typingDots = _el('div', { className: 'wakz-typing-dots' });
1801
+ typingDots.appendChild(_el('span'));
1802
+ typingDots.appendChild(_el('span'));
1803
+ typingDots.appendChild(_el('span'));
1804
+ typingContent.appendChild(typingDots);
1805
+
1806
+ self._typingEl.appendChild(typingContent);
1807
+ self._messagesInner.appendChild(self._typingEl);
1808
+ self._scrollToBottom();
1809
+ };
1810
+
1811
+ WAKZWidget.prototype._hideTyping = function () {
1812
+ var self = this;
1813
+ if (self._typingEl && self._typingEl.parentNode) {
1814
+ self._typingEl.parentNode.removeChild(self._typingEl);
1815
+ }
1816
+ self._typingEl = null;
1817
+ };
1818
+
1819
+ /* ════════════════════════════════════════════════════════════════
1820
+ SCROLL TO BOTTOM
1821
+ ════════════════════════════════════════════════════════════════ */
1822
+
1823
+ WAKZWidget.prototype._scrollToBottom = function () {
1824
+ var self = this;
1825
+ requestAnimationFrame(function () {
1826
+ requestAnimationFrame(function () {
1827
+ self._messagesContainer.scrollTop = self._messagesContainer.scrollHeight;
1828
+ });
1829
+ });
1830
+ };
1831
+
1832
+ /* ════════════════════════════════════════════════════════════════
1833
+ HANDLE SEND ACTION
1834
+ ════════════════════════════════════════════════════════════════ */
1835
+
1836
+ WAKZWidget.prototype._handleSend = function () {
1837
+ var self = this;
1838
+ var text = (self._inputEl.value || '').trim();
1839
+ if (!text || self.isLoading) return;
1840
+
1841
+ /* Append user message immediately */
1842
+ self._appendMessage('user', text);
1843
+
1844
+ /* Clear input */
1845
+ self._inputEl.value = '';
1846
+ self._inputEl.style.height = 'auto';
1847
+ self._sendBtn.classList.remove('wakz-send--active');
1848
+
1849
+ /* Send to API */
1850
+ self._sendToAPI(text);
1851
+ };
1852
+
1853
+ /* ════════════════════════════════════════════════════════════════
1854
+ SEND MESSAGE TO API
1855
+ ════════════════════════════════════════════════════════════════ */
1856
+
1857
+ WAKZWidget.prototype._sendToAPI = function (message) {
1858
+ var self = this;
1859
+
1860
+ if (!self.server || !self.apiKey) {
1861
+ self._appendMessage('bot', 'Widget not configured. Provide data-api-key and data-server.', true);
1862
+ return;
1863
+ }
1864
+
1865
+ self.isLoading = true;
1866
+ self._sendBtn.disabled = true;
1867
+ self._showTyping();
1868
+
1869
+ /* Freeze poll time BEFORE sending */
1870
+ self._lastPollTime = new Date().toISOString();
1871
+ self._pendingReply = true;
1872
+
1873
+ var payload = {
1874
+ api_key: self.apiKey,
1875
+ visitor_id: self.visitorId,
1876
+ message: message,
1877
+ session_data: {
1878
+ domain: window.location.hostname || '',
1879
+ page_url: window.location.href || '',
1880
+ user_agent: navigator.userAgent || '',
1881
+ language: navigator.language || '',
1882
+ device_model: self._deviceInfo.device_model || '',
1883
+ device_platform: self._deviceInfo.device_platform || '',
1884
+ device_type: self._deviceInfo.device_type || 'desktop'
1885
+ }
1886
+ };
1887
+
1888
+ _fetchWithTimeout(self.server + '/api/v1/embed/chat', {
1889
+ method: 'POST',
1890
+ headers: {
1891
+ 'Content-Type': 'application/json',
1892
+ 'Accept': 'application/json'
1893
+ },
1894
+ body: JSON.stringify(payload)
1895
+ }, 30000)
1896
+ .then(function (res) { return res.json(); })
1897
+ .then(function (data) {
1898
+ self._hideTyping();
1899
+ self._pendingReply = false;
1900
+ if (data && data.success && data.reply) {
1901
+ self._appendMessage('bot', data.reply);
1902
+ } else {
1903
+ self._appendMessage('bot', _strings(self.config.language).errorMsg, true);
1904
+ }
1905
+ self._lastPollTime = (data && data.timestamp)
1906
+ ? new Date(new Date(data.timestamp).getTime() + 1000).toISOString()
1907
+ : new Date(Date.now() + 1000).toISOString();
1908
+ })
1909
+ .catch(function () {
1910
+ self._hideTyping();
1911
+ self._pendingReply = false;
1912
+ self._appendMessage('bot', _strings(self.config.language).errorMsg, true);
1913
+ self._lastPollTime = new Date(Date.now() + 1000).toISOString();
1914
+ })
1915
+ .finally(function () {
1916
+ self.isLoading = false;
1917
+ self._sendBtn.disabled = false;
1918
+ if (self.isOpen) self._inputEl.focus();
1919
+ });
1920
+ };
1921
+
1922
+ /* ════════════════════════════════════════════════════════════════
1923
+ BOOTSTRAP
1924
+ ════════════════════════════════════════════════════════════════ */
1925
+
1926
+ function boot() {
1927
+ try { new WAKZWidget(); } catch (e) { console.error('[WAKZ Widget v4.0.0] Init error:', e); }
1928
+ }
1929
+
1930
+ if (document.readyState === 'loading') {
1931
+ document.addEventListener('DOMContentLoaded', boot);
1932
+ } else {
1933
+ boot();
1934
+ }
1935
+
1936
+ })();