wakz-chat-widget 2.0.0 → 2.2.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 +51 -24
  2. package/index.js +305 -413
  3. package/package.json +13 -6
package/index.js CHANGED
@@ -1,11 +1,10 @@
1
1
  /**
2
- * WAKZ Chat Widget v3.0.0
2
+ * WAKZ Chat Widget v2.2.0
3
3
  * ─────────────────────────────────────────────────────────────────
4
4
  * A production-grade, self-contained chat widget using Shadow DOM.
5
- * Premium glassmorphism design matching the WAKZ site design system.
5
+ * Competes with Intercom & Crisp in quality.
6
6
  *
7
7
  * Embed: <script src="/wakz-widget.js" data-api-key="xxx" data-server="https://..." async></script>
8
- * New attrs: data-icon-color="#171717" data-icon-shape="circle"
9
8
  *
10
9
  * ZERO external dependencies — pure vanilla JavaScript.
11
10
  */
@@ -51,9 +50,7 @@
51
50
  if (!target && scripts.length > 0) target = scripts[scripts.length - 1];
52
51
  return {
53
52
  apiKey: (target && target.getAttribute('data-api-key')) || '',
54
- server: (target && target.getAttribute('data-server')) || '',
55
- iconColor: (target && target.getAttribute('data-icon-color')) || '',
56
- iconShape: (target && target.getAttribute('data-icon-shape')) || ''
53
+ server: (target && target.getAttribute('data-server')) || ''
57
54
  };
58
55
  }
59
56
 
@@ -82,6 +79,46 @@
82
79
  return el;
83
80
  }
84
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
+
85
122
  /** Fetch with an AbortController timeout (default 30s) */
86
123
  function _fetchWithTimeout(url, options, timeoutMs) {
87
124
  var controller = new AbortController();
@@ -95,21 +132,12 @@
95
132
  ════════════════════════════════════════════════════════════════ */
96
133
 
97
134
  var _ICONS = {
98
- /* Modern chat icon: rounded square with 3 horizontal chat lines */
99
- chatIcon:
100
- '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">' +
101
- '<rect x="3" y="3" width="18" height="18" rx="5" ry="5" stroke="currentColor" stroke-width="1.8" fill="none"/>' +
102
- '<line x1="8" y1="9" x2="16" y2="9" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' +
103
- '<line x1="8" y1="12.5" x2="14" y2="12.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' +
104
- '<line x1="8" y1="16" x2="12" y2="16" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' +
105
- '</svg>',
106
- /* Send arrow */
135
+ chatBubble:
136
+ '<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>',
107
137
  send:
108
138
  '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>',
109
- /* Close X */
110
139
  close:
111
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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>',
112
- /* Error circle */
140
+ '<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>',
113
141
  error:
114
142
  '<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>'
115
143
  };
@@ -169,12 +197,11 @@
169
197
  botName: 'WAKZ',
170
198
  welcomeMessage: '',
171
199
  primaryColor: '#171717',
172
- iconColor: '#171717',
173
- iconShape: 'circle',
174
- chatBg: '#FAFAFA',
200
+ chatBg: '#f8f9fa',
201
+ btnColor: '#171717',
175
202
  widgetBg: '#ffffff',
176
203
  position: 'bottom-right',
177
- language: 'ar',
204
+ language: 'en',
178
205
  showStatus: true,
179
206
  online: true
180
207
  };
@@ -195,8 +222,7 @@
195
222
  self.apiKey = attrs.apiKey;
196
223
  self.server = attrs.server;
197
224
  self.visitorId = _getVisitorId();
198
- self._scriptIconColor = attrs.iconColor;
199
- self._scriptIconShape = attrs.iconShape;
225
+ self._deviceInfo = _getDeviceInfo(); // Device model, platform, type — collected once
200
226
 
201
227
  /* ── Runtime state ── */
202
228
  self.config = Object.assign({}, _DEFAULTS);
@@ -211,12 +237,12 @@
211
237
  self._lastPollTime = null;
212
238
  self._POLL_INTERVAL = 4000; /* Poll every 4 seconds */
213
239
  self._knownMessageIds = {}; /* Deduplication set for message IDs */
240
+ self._pendingReply = false; /* True while waiting for API response — pauses polling */
214
241
 
215
242
  /* ── DOM refs (populated after mount) ── */
216
243
  self._host = null;
217
244
  self._shadow = null;
218
245
  self._root = null;
219
- self._overlayEl = null;
220
246
  self._chatWindow = null;
221
247
  self._messagesContainer = null;
222
248
  self._inputEl = null;
@@ -235,7 +261,7 @@
235
261
  }
236
262
 
237
263
  /* ════════════════════════════════════════════════════════════════
238
- CSS — Complete premium styling injected into Shadow DOM
264
+ CSS — Complete styling injected into Shadow DOM
239
265
  ════════════════════════════════════════════════════════════════ */
240
266
 
241
267
  WAKZWidget.prototype._injectCSS = function () {
@@ -246,14 +272,18 @@
246
272
  /* ── CSS Custom Properties (theming) ── */
247
273
  ':host {',
248
274
  ' --wakz-primary: #171717;',
249
- ' --wakz-icon-color: #171717;',
250
- ' --wakz-chat-bg: #FAFAFA;',
275
+ ' --wakz-btn: #171717;',
276
+ ' --wakz-chat-bg: #f8f9fa;',
251
277
  ' --wakz-widget-bg: #ffffff;',
252
- ' --wakz-radius: 20px;',
253
- ' --wakz-radius-bubble: 18px;',
278
+ ' --wakz-radius-window: 16px;',
279
+ ' --wakz-radius-bubble: 16px;',
280
+ ' --wakz-radius-fab: 28px;',
281
+ ' --wakz-shadow-sm: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06);',
282
+ ' --wakz-shadow-md: 0 4px 12px rgba(0,0,0,.1), 0 2px 4px rgba(0,0,0,.06);',
283
+ ' --wakz-shadow-lg: 0 12px 40px rgba(0,0,0,.15), 0 4px 12px rgba(0,0,0,.1);',
284
+ ' --wakz-shadow-xl: 0 20px 60px rgba(0,0,0,.2), 0 8px 20px rgba(0,0,0,.12);',
285
+ ' --wakz-transition: 200ms ease;',
254
286
  ' --wakz-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;',
255
- ' --wakz-shadow: 0 25px 60px rgba(0,0,0,0.15), 0 8px 20px rgba(0,0,0,0.1);',
256
- ' --wakz-shadow-sm: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04);',
257
287
  ' all: initial;',
258
288
  ' font-family: var(--wakz-font);',
259
289
  '}',
@@ -271,46 +301,34 @@
271
301
  '.wakz-fab {',
272
302
  ' position: fixed;',
273
303
  ' z-index: 2147483647;',
274
- ' width: 52px;',
275
- ' height: 52px;',
276
- ' border-radius: 50%;',
304
+ ' width: 56px;',
305
+ ' height: 56px;',
306
+ ' border-radius: var(--wakz-radius-fab);',
277
307
  ' border: none;',
278
308
  ' cursor: pointer;',
279
309
  ' display: flex;',
280
310
  ' align-items: center;',
281
311
  ' justify-content: center;',
282
- ' box-shadow: 0 12px 40px rgba(0,0,0,0.15), 0 4px 12px rgba(0,0,0,0.1);',
283
- ' transition: opacity 0.3s cubic-bezier(0.16,1,0.3,1), transform 0.3s cubic-bezier(0.16,1,0.3,1), box-shadow 0.2s ease;',
312
+ ' box-shadow: var(--wakz-shadow-lg);',
313
+ ' transition: transform var(--wakz-transition), box-shadow var(--wakz-transition);',
284
314
  ' outline: none;',
285
315
  ' -webkit-tap-highlight-color: transparent;',
286
- ' background: var(--wakz-icon-color, #171717);',
316
+ ' background: var(--wakz-btn);',
287
317
  '}',
288
318
  '.wakz-fab:hover {',
289
319
  ' transform: scale(1.08);',
290
- ' box-shadow: 0 20px 60px rgba(0,0,0,0.2), 0 8px 20px rgba(0,0,0,0.12);',
320
+ ' box-shadow: var(--wakz-shadow-xl);',
291
321
  '}',
292
322
  '.wakz-fab:active {',
293
323
  ' transform: scale(0.96);',
294
324
  '}',
295
325
  '.wakz-fab svg {',
296
- ' width: 24px;',
297
- ' height: 24px;',
326
+ ' width: 26px;',
327
+ ' height: 26px;',
298
328
  ' color: #ffffff;',
299
329
  ' pointer-events: none;',
300
330
  '}',
301
331
 
302
- /* ── FAB Shape: rounded square ── */
303
- '.wakz-fab-shape-rounded {',
304
- ' border-radius: 16px;',
305
- '}',
306
-
307
- /* ── FAB hidden when chat is open ── */
308
- '.wakz-fab-hidden {',
309
- ' opacity: 0;',
310
- ' pointer-events: none;',
311
- ' transform: scale(0.8);',
312
- '}',
313
-
314
332
  /* ── FAB Positioning ── */
315
333
  '.wakz-fab-pos-br { bottom: 24px; right: 24px; }',
316
334
  '.wakz-fab-pos-bl { bottom: 24px; left: 24px; }',
@@ -331,18 +349,18 @@
331
349
  ══════════════════════════════════════════════════ */
332
350
  '.wakz-fab-dot {',
333
351
  ' position: absolute;',
334
- ' top: -1px;',
335
- ' right: -1px;',
336
- ' width: 12px;',
337
- ' height: 12px;',
352
+ ' top: 0px;',
353
+ ' right: 0px;',
354
+ ' width: 14px;',
355
+ ' height: 14px;',
338
356
  ' border-radius: 50%;',
339
- ' border: 2px solid #ffffff;',
357
+ ' border: 2.5px solid #ffffff;',
340
358
  ' z-index: 2;',
341
359
  ' transition: background 0.3s ease;',
342
360
  '}',
343
361
  '.wakz-fab-pos-bl .wakz-fab-dot {',
344
362
  ' right: auto;',
345
- ' left: -1px;',
363
+ ' left: 0px;',
346
364
  '}',
347
365
 
348
366
  /* ── Online (green pulse) ── */
@@ -363,60 +381,45 @@
363
381
  ' animation: none;',
364
382
  '}',
365
383
 
366
- /* ══════════════════════════════════════════════════
367
- OVERLAY BACKDROP
368
- ══════════════════════════════════════════════════ */
369
- '.wakz-overlay {',
370
- ' position: fixed;',
371
- ' top: 0;',
372
- ' left: 0;',
373
- ' right: 0;',
374
- ' bottom: 0;',
375
- ' z-index: 2147483645;',
376
- ' background: rgba(0,0,0,0.4);',
377
- ' -webkit-backdrop-filter: blur(4px);',
378
- ' backdrop-filter: blur(4px);',
379
- ' opacity: 0;',
380
- ' pointer-events: none;',
381
- ' transition: opacity 0.3s cubic-bezier(0.16,1,0.3,1);',
382
- '}',
383
- '.wakz-overlay.wakz-visible {',
384
- ' opacity: 1;',
385
- ' pointer-events: auto;',
384
+ /* ── Error (red, no pulse) ── */
385
+ '.wakz-fab-dot.error {',
386
+ ' background: #ef4444;',
387
+ ' animation: none;',
386
388
  '}',
387
389
 
388
390
  /* ══════════════════════════════════════════════════
389
- CHAT WINDOW — Centered Modal
391
+ CHAT WINDOW
390
392
  ══════════════════════════════════════════════════ */
391
393
  '.wakz-window {',
392
394
  ' position: fixed;',
393
395
  ' z-index: 2147483646;',
394
- ' top: 50%;',
395
- ' left: 50%;',
396
- ' transform: translate(-50%, -50%) scale(0.95);',
397
- ' width: 420px;',
398
- ' max-height: 75vh;',
399
- ' border-radius: 20px;',
396
+ ' width: 380px;',
397
+ ' min-height: 500px;',
398
+ ' max-height: 70vh;',
399
+ ' border-radius: var(--wakz-radius-window);',
400
400
  ' overflow: hidden;',
401
401
  ' display: flex;',
402
402
  ' flex-direction: column;',
403
403
  ' background: var(--wakz-widget-bg);',
404
- ' border: 1px solid #E5E5E5;',
405
- ' box-shadow: 0 25px 60px rgba(0,0,0,0.15), 0 8px 20px rgba(0,0,0,0.1);',
406
- ' -webkit-backdrop-filter: blur(20px);',
407
- ' backdrop-filter: blur(20px);',
404
+ ' box-shadow: var(--wakz-shadow-xl);',
408
405
  ' opacity: 0;',
406
+ ' transform: translateY(20px) scale(0.95);',
409
407
  ' pointer-events: none;',
410
- ' transition: opacity 0.3s cubic-bezier(0.16,1,0.3,1), transform 0.3s cubic-bezier(0.16,1,0.3,1);',
408
+ ' transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),',
409
+ ' transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);',
411
410
  '}',
412
411
  '.wakz-window.wakz-visible {',
413
412
  ' opacity: 1;',
414
- ' transform: translate(-50%, -50%) scale(1);',
413
+ ' transform: translateY(0) scale(1);',
415
414
  ' pointer-events: auto;',
416
415
  '}',
417
416
 
417
+ /* ── Window Positioning ── */
418
+ '.wakz-win-pos-br { bottom: 92px; right: 24px; }',
419
+ '.wakz-win-pos-bl { bottom: 92px; left: 24px; }',
420
+
418
421
  /* ══════════════════════════════════════════════════
419
- HEADER — Glassmorphism (matching floatingGlassShell)
422
+ HEADER
420
423
  ══════════════════════════════════════════════════ */
421
424
  '.wakz-hdr {',
422
425
  ' flex-shrink: 0;',
@@ -424,35 +427,30 @@
424
427
  ' display: flex;',
425
428
  ' align-items: center;',
426
429
  ' justify-content: space-between;',
427
- ' color: #171717;',
430
+ ' color: #ffffff;',
428
431
  ' cursor: default;',
429
432
  ' user-select: none;',
430
- ' background: rgba(255,255,255,0.85);',
431
- ' -webkit-backdrop-filter: blur(20px);',
432
- ' backdrop-filter: blur(20px);',
433
- ' border-bottom: 1px solid rgba(0,0,0,0.06);',
434
- ' height: 60px;',
433
+ ' background: var(--wakz-primary);',
435
434
  '}',
436
- '.wakz-hdr-right {',
435
+ '.wakz-hdr-left {',
437
436
  ' display: flex;',
438
437
  ' align-items: center;',
439
438
  ' gap: 11px;',
440
439
  ' min-width: 0;',
441
440
  '}',
442
441
 
443
- /* ── Bot Avatar (header) ── */
442
+ /* ── Bot Avatar ── */
444
443
  '.wakz-avatar {',
445
- ' width: 36px;',
446
- ' height: 36px;',
444
+ ' width: 38px;',
445
+ ' height: 38px;',
447
446
  ' border-radius: 50%;',
448
- ' background: var(--wakz-primary);',
447
+ ' background: rgba(255,255,255,0.18);',
449
448
  ' display: flex;',
450
449
  ' align-items: center;',
451
450
  ' justify-content: center;',
452
451
  ' font-weight: 700;',
453
- ' font-size: 15px;',
452
+ ' font-size: 16px;',
454
453
  ' flex-shrink: 0;',
455
- ' color: #ffffff;',
456
454
  ' letter-spacing: 0.5px;',
457
455
  '}',
458
456
 
@@ -463,27 +461,20 @@
463
461
  ' min-width: 0;',
464
462
  '}',
465
463
  '.wakz-hdr-name {',
466
- ' font-size: 16px;',
464
+ ' font-size: 15px;',
467
465
  ' font-weight: 600;',
468
466
  ' line-height: 1.3;',
469
- ' color: #171717;',
470
467
  ' white-space: nowrap;',
471
468
  ' overflow: hidden;',
472
469
  ' text-overflow: ellipsis;',
473
470
  '}',
474
-
475
- /* ── Header Left: status + close ── */
476
- '.wakz-hdr-left {',
477
- ' display: flex;',
478
- ' align-items: center;',
479
- ' gap: 10px;',
480
- '}',
481
471
  '.wakz-hdr-status {',
482
472
  ' font-size: 12px;',
483
- ' color: #171717;',
473
+ ' opacity: 0.9;',
484
474
  ' display: flex;',
485
475
  ' align-items: center;',
486
476
  ' gap: 5px;',
477
+ ' margin-top: 2px;',
487
478
  '}',
488
479
  '.wakz-hdr-status-dot {',
489
480
  ' width: 7px;',
@@ -493,8 +484,8 @@
493
484
  ' flex-shrink: 0;',
494
485
  ' transition: background 0.3s ease;',
495
486
  '}',
496
- '.wakz-hdr-status-dot.online { background: #22c55e; }',
497
- '.wakz-hdr-status-dot.offline { background: #ef4444; }',
487
+ '.wakz-hdr-status-dot.online { background: #4ade80; }',
488
+ '.wakz-hdr-status-dot.offline { background: #f87171; }',
498
489
 
499
490
  /* ── Close Button ── */
500
491
  '.wakz-close {',
@@ -502,18 +493,18 @@
502
493
  ' height: 32px;',
503
494
  ' border-radius: 50%;',
504
495
  ' border: none;',
505
- ' background: transparent;',
506
- ' color: #171717;',
496
+ ' background: rgba(255,255,255,0.12);',
497
+ ' color: #ffffff;',
507
498
  ' cursor: pointer;',
508
499
  ' display: flex;',
509
500
  ' align-items: center;',
510
501
  ' justify-content: center;',
511
- ' transition: background 0.2s ease;',
502
+ ' transition: background var(--wakz-transition);',
512
503
  ' outline: none;',
513
504
  ' flex-shrink: 0;',
514
505
  '}',
515
506
  '.wakz-close:hover {',
516
- ' background: rgba(0,0,0,0.06);',
507
+ ' background: rgba(255,255,255,0.28);',
517
508
  '}',
518
509
  '.wakz-close svg {',
519
510
  ' width: 18px;',
@@ -527,130 +518,90 @@
527
518
  ' flex: 1;',
528
519
  ' overflow-y: auto;',
529
520
  ' overflow-x: hidden;',
530
- ' padding: 16px;',
521
+ ' padding: 18px 16px 10px;',
531
522
  ' display: flex;',
532
523
  ' flex-direction: column;',
533
- ' gap: 8px;',
524
+ ' gap: 6px;',
534
525
  ' scroll-behavior: smooth;',
535
526
  ' background: var(--wakz-chat-bg);',
536
527
  '}',
537
528
  '.wakz-msgs::-webkit-scrollbar { width: 5px; }',
538
529
  '.wakz-msgs::-webkit-scrollbar-track { background: transparent; }',
539
530
  '.wakz-msgs::-webkit-scrollbar-thumb {',
540
- ' background: rgba(0,0,0,0.08);',
531
+ ' background: rgba(0,0,0,0.12);',
541
532
  ' border-radius: 10px;',
542
533
  '}',
543
534
 
544
535
  /* ══════════════════════════════════════════════════
545
- MESSAGE STYLES
536
+ MESSAGE BUBBLES
546
537
  ══════════════════════════════════════════════════ */
538
+ '.wakz-msg-row {',
539
+ ' display: flex;',
540
+ ' max-width: 82%;',
541
+ ' animation: wakz-msg-in 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards;',
542
+ '}',
543
+ '.wakz-msg-row.bot { align-self: flex-start; }',
544
+ '.wakz-msg-row.user { align-self: flex-end; }',
545
+
547
546
  '@keyframes wakz-msg-in {',
548
547
  ' 0% { opacity: 0; transform: translateY(8px); }',
549
548
  ' 100% { opacity: 1; transform: translateY(0); }',
550
549
  '}',
551
550
 
552
- /* ── User Message (bubble, right-aligned LTR / left-aligned RTL) ── */
553
- '.wakz-msg-row {',
554
- ' animation: wakz-msg-in 0.3s cubic-bezier(0.16,1,0.3,1) forwards;',
555
- '}',
556
- '.wakz-msg-row.user {',
557
- ' align-self: flex-end;',
558
- ' max-width: 75%;',
559
- '}',
560
- '.wakz-msg-row.bot {',
561
- ' align-self: flex-start;',
562
- ' max-width: 85%;',
563
- '}',
564
-
565
- '.wakz-bubble-user {',
566
- ' padding: 10px 16px;',
551
+ '.wakz-bubble {',
552
+ ' padding: 10px 14px;',
567
553
  ' font-size: 14px;',
568
554
  ' line-height: 1.55;',
569
555
  ' word-wrap: break-word;',
570
556
  ' overflow-wrap: break-word;',
571
557
  ' white-space: pre-wrap;',
572
- ' border-radius: 18px 18px 6px 18px;',
573
- ' background: var(--wakz-primary);',
558
+ ' border-radius: var(--wakz-radius-bubble);',
559
+ ' position: relative;',
560
+ '}',
561
+ '.wakz-bubble.bot {',
562
+ ' background: #ffffff;',
563
+ ' color: #1a1a1a;',
564
+ ' border-bottom-left-radius: 4px;',
565
+ ' box-shadow: var(--wakz-shadow-sm);',
566
+ '}',
567
+ '.wakz-bubble.user {',
574
568
  ' color: #ffffff;',
569
+ ' border-bottom-right-radius: 4px;',
570
+ ' background: var(--wakz-primary);',
571
+ '}',
572
+ '.wakz-bubble.error-bubble {',
573
+ ' background: #fef2f2;',
574
+ ' color: #dc2626;',
575
+ ' border: 1px solid #fecaca;',
576
+ ' border-bottom-left-radius: 4px;',
577
+ ' box-shadow: none;',
575
578
  '}',
576
579
 
577
- /* ── Timestamp ── */
580
+ /* ── Message Timestamp ── */
578
581
  '.wakz-ts {',
579
582
  ' display: block;',
580
- ' font-size: 10px;',
581
- ' opacity: 0.6;',
583
+ ' font-size: 11px;',
584
+ ' opacity: 0.45;',
582
585
  ' margin-top: 4px;',
583
586
  '}',
584
- '.wakz-bubble-user .wakz-ts {',
587
+ '.wakz-bubble.user .wakz-ts {',
585
588
  ' text-align: right;',
586
- ' color: rgba(255,255,255,0.7);',
587
589
  '}',
588
590
 
589
- /* ── Bot Message (no bubble, flat on bg with left accent border) ── */
590
- '.wakz-msg-bot-wrap {',
591
- ' display: flex;',
592
- ' flex-direction: column;',
593
- ' gap: 2px;',
594
- '}',
595
- '.wakz-msg-bot-avatar {',
596
- ' width: 20px;',
597
- ' height: 20px;',
598
- ' border-radius: 50%;',
599
- ' background: var(--wakz-primary);',
600
- ' display: flex;',
601
- ' align-items: center;',
602
- ' justify-content: center;',
603
- ' color: #ffffff;',
604
- ' font-weight: 700;',
605
- ' font-size: 9px;',
606
- ' flex-shrink: 0;',
607
- ' margin-bottom: 2px;',
608
- '}',
609
- '.wakz-msg-bot-content {',
610
- ' border-left: 3px solid rgba(23,23,23,0.2);',
611
- ' padding: 10px 0 10px 14px;',
612
- ' font-size: 14px;',
613
- ' line-height: 1.6;',
614
- ' color: #374151;',
615
- ' word-wrap: break-word;',
616
- ' overflow-wrap: break-word;',
617
- ' white-space: pre-wrap;',
618
- '}',
619
- '.wakz-msg-bot-content .wakz-ts {',
620
- ' color: #9CA3AF;',
621
- '}',
622
-
623
- /* ── Support / Human Agent Message ── */
624
- '.wakz-msg-bot-content.wakz-human {',
625
- ' border-left-color: #16A34A;',
626
- '}',
591
+ /* ── Human Agent Badge ── */
627
592
  '.wakz-human-badge {',
628
- ' display: inline-flex;',
629
- ' align-items: center;',
630
- ' font-size: 11px;',
631
- ' background: rgba(22,163,74,0.08);',
593
+ ' display: inline-block;',
594
+ ' font-size: 10px;',
595
+ ' background: rgba(22, 163, 74, 0.1);',
632
596
  ' color: #16A34A;',
633
- ' border-radius: 20px;',
634
- ' padding: 2px 8px;',
635
- ' margin-top: 6px;',
597
+ ' border-radius: 8px;',
598
+ ' padding: 1px 7px;',
599
+ ' margin-top: 5px;',
636
600
  ' font-weight: 600;',
601
+ ' letter-spacing: 0.2px;',
637
602
  '}',
638
603
 
639
- /* ── Error bubble ── */
640
- '.wakz-bubble-error {',
641
- ' padding: 10px 14px;',
642
- ' font-size: 14px;',
643
- ' line-height: 1.55;',
644
- ' word-wrap: break-word;',
645
- ' overflow-wrap: break-word;',
646
- ' white-space: pre-wrap;',
647
- ' background: #FEF2F2;',
648
- ' color: #DC2626;',
649
- ' border: 1px solid #FECACA;',
650
- ' border-radius: 18px 18px 6px 18px;',
651
- '}',
652
-
653
- /* ── Retry Button (inside error) ── */
604
+ /* ── Retry Button (inside error bubble) ── */
654
605
  '.wakz-retry {',
655
606
  ' display: inline-flex;',
656
607
  ' align-items: center;',
@@ -665,7 +616,7 @@
665
616
  ' color: #dc2626;',
666
617
  ' cursor: pointer;',
667
618
  ' font-family: var(--wakz-font);',
668
- ' transition: background 0.2s ease, border-color 0.2s ease;',
619
+ ' transition: background var(--wakz-transition), border-color var(--wakz-transition);',
669
620
  ' outline: none;',
670
621
  '}',
671
622
  '.wakz-retry:hover {',
@@ -674,32 +625,23 @@
674
625
  '}',
675
626
 
676
627
  /* ══════════════════════════════════════════════════
677
- WELCOME MESSAGE
678
- ══════════════════════════════════════════════════ */
679
- '.wakz-welcome-wrap {',
680
- ' display: flex;',
681
- ' flex-direction: column;',
682
- ' gap: 2px;',
683
- ' max-width: 85%;',
684
- ' align-self: flex-start;',
685
- ' animation: wakz-msg-in 0.35s cubic-bezier(0.16,1,0.3,1) forwards;',
686
- '}',
687
-
688
- /* ══════════════════════════════════════════════════
689
- TYPING INDICATOR (no bubble — on background)
628
+ TYPING INDICATOR
690
629
  ══════════════════════════════════════════════════ */
691
630
  '.wakz-typing {',
692
631
  ' display: flex;',
693
632
  ' align-items: center;',
694
633
  ' gap: 4px;',
695
- ' padding: 10px 0 10px 14px;',
696
- ' border-left: 3px solid rgba(23,23,23,0.2);',
634
+ ' padding: 14px 18px;',
635
+ ' background: #ffffff;',
636
+ ' border-radius: var(--wakz-radius-bubble);',
637
+ ' border-bottom-left-radius: 4px;',
638
+ ' box-shadow: var(--wakz-shadow-sm);',
697
639
  '}',
698
640
  '.wakz-typing-dot {',
699
641
  ' width: 7px;',
700
642
  ' height: 7px;',
701
643
  ' border-radius: 50%;',
702
- ' background: #9CA3AF;',
644
+ ' background: #9ca3af;',
703
645
  ' animation: wakz-bounce-dot 1.4s ease-in-out infinite;',
704
646
  '}',
705
647
  '.wakz-typing-dot:nth-child(2) { animation-delay: 0.16s; }',
@@ -710,33 +652,31 @@
710
652
  '}',
711
653
 
712
654
  /* ══════════════════════════════════════════════════
713
- INPUT AREA — Glass Effect
655
+ INPUT AREA
714
656
  ══════════════════════════════════════════════════ */
715
657
  '.wakz-input-wrap {',
716
658
  ' flex-shrink: 0;',
717
- ' padding: 12px 16px;',
718
- ' border-top: 1px solid rgba(0,0,0,0.06);',
719
- ' background: rgba(255,255,255,0.7);',
720
- ' -webkit-backdrop-filter: blur(12px);',
721
- ' backdrop-filter: blur(12px);',
659
+ ' padding: 12px 14px;',
660
+ ' border-top: 1px solid #e5e7eb;',
661
+ ' background: var(--wakz-widget-bg);',
722
662
  ' display: flex;',
723
663
  ' align-items: flex-end;',
724
664
  ' gap: 8px;',
725
665
  '}',
726
666
  '.wakz-input {',
727
667
  ' flex: 1;',
728
- ' border: 1.5px solid #E5E5E5;',
729
- ' border-radius: 14px;',
668
+ ' border: 1.5px solid #e5e7eb;',
669
+ ' border-radius: 22px;',
730
670
  ' padding: 10px 16px;',
731
671
  ' font-size: 14px;',
732
672
  ' line-height: 1.4;',
733
673
  ' outline: none;',
734
674
  ' font-family: var(--wakz-font);',
735
- ' transition: border-color 0.2s ease, background 0.2s ease;',
675
+ ' transition: border-color var(--wakz-transition);',
736
676
  ' resize: none;',
737
677
  ' max-height: 100px;',
738
678
  ' overflow-y: auto;',
739
- ' background: #F9FAFB;',
679
+ ' background: #f9fafb;',
740
680
  ' color: #111827;',
741
681
  '}',
742
682
  '.wakz-input:focus {',
@@ -747,10 +687,10 @@
747
687
  ' color: #9ca3af;',
748
688
  '}',
749
689
 
750
- /* ── Send Button (floating circle, separate from textarea) ── */
690
+ /* ── Send Button ── */
751
691
  '.wakz-send {',
752
- ' width: 44px;',
753
- ' height: 44px;',
692
+ ' width: 40px;',
693
+ ' height: 40px;',
754
694
  ' border-radius: 50%;',
755
695
  ' border: none;',
756
696
  ' color: #ffffff;',
@@ -759,21 +699,20 @@
759
699
  ' align-items: center;',
760
700
  ' justify-content: center;',
761
701
  ' flex-shrink: 0;',
762
- ' transition: opacity 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;',
702
+ ' transition: opacity var(--wakz-transition), transform 0.1s ease, box-shadow var(--wakz-transition);',
763
703
  ' outline: none;',
764
704
  ' background: var(--wakz-primary);',
765
- ' box-shadow: 0 2px 8px rgba(0,0,0,0.12);',
705
+ ' box-shadow: var(--wakz-shadow-sm);',
766
706
  '}',
767
707
  '.wakz-send:hover:not(:disabled) {',
768
- ' transform: scale(1.05);',
769
- ' box-shadow: 0 4px 16px rgba(0,0,0,0.18);',
708
+ ' box-shadow: var(--wakz-shadow-md);',
770
709
  '}',
771
710
  '.wakz-send:disabled {',
772
711
  ' opacity: 0.35;',
773
712
  ' cursor: not-allowed;',
774
713
  '}',
775
714
  '.wakz-send:not(:disabled):active {',
776
- ' transform: scale(0.92);',
715
+ ' transform: scale(0.88);',
777
716
  '}',
778
717
  '.wakz-send svg {',
779
718
  ' width: 18px;',
@@ -782,55 +721,68 @@
782
721
  '}',
783
722
 
784
723
  /* ══════════════════════════════════════════════════
785
- RTL SUPPORT
724
+ WELCOME MESSAGE
786
725
  ══════════════════════════════════════════════════ */
787
- '.wakz-rtl { direction: rtl; }',
788
- '.wakz-rtl .wakz-bubble-user {',
789
- ' border-radius: 18px 18px 18px 6px;',
726
+ '.wakz-welcome-wrap {',
727
+ ' display: flex;',
728
+ ' gap: 8px;',
729
+ ' max-width: 88%;',
730
+ ' align-self: flex-start;',
731
+ ' animation: wakz-msg-in 0.35s cubic-bezier(0.4, 0, 0.2, 1) forwards;',
790
732
  '}',
791
- '.wakz-rtl .wakz-bubble-user .wakz-ts {',
792
- ' text-align: left;',
733
+ '.wakz-welcome-avatar {',
734
+ ' width: 28px;',
735
+ ' height: 28px;',
736
+ ' border-radius: 50%;',
737
+ ' background: var(--wakz-primary);',
738
+ ' display: flex;',
739
+ ' align-items: center;',
740
+ ' justify-content: center;',
741
+ ' color: #ffffff;',
742
+ ' font-weight: 700;',
743
+ ' font-size: 12px;',
744
+ ' flex-shrink: 0;',
745
+ ' align-self: flex-end;',
746
+ ' margin-bottom: 2px;',
793
747
  '}',
794
- '.wakz-rtl .wakz-msg-bot-content {',
795
- ' border-left: none;',
796
- ' border-right: 3px solid rgba(23,23,23,0.2);',
797
- ' padding: 10px 14px 10px 0;',
748
+
749
+ /* ══════════════════════════════════════════════════
750
+ RTL SUPPORT
751
+ ══════════════════════════════════════════════════ */
752
+ '.wakz-rtl { direction: rtl; }',
753
+ '.wakz-rtl .wakz-bubble.bot {',
754
+ ' border-bottom-left-radius: var(--wakz-radius-bubble);',
755
+ ' border-bottom-right-radius: 4px;',
798
756
  '}',
799
- '.wakz-rtl .wakz-msg-bot-content.wakz-human {',
800
- ' border-left: none;',
801
- ' border-right-color: #16A34A;',
757
+ '.wakz-rtl .wakz-bubble.user {',
758
+ ' border-bottom-right-radius: var(--wakz-radius-bubble);',
759
+ ' border-bottom-left-radius: 4px;',
802
760
  '}',
803
761
  '.wakz-rtl .wakz-typing {',
804
- ' border-left: none;',
805
- ' border-right: 3px solid rgba(23,23,23,0.2);',
806
- ' padding: 10px 14px 10px 0;',
762
+ ' border-bottom-left-radius: var(--wakz-radius-bubble);',
763
+ ' border-bottom-right-radius: 4px;',
807
764
  '}',
808
- '.wakz-rtl .wakz-bubble-error {',
809
- ' border-radius: 18px 18px 18px 6px;',
765
+ '.wakz-rtl .wakz-bubble.user .wakz-ts {',
766
+ ' text-align: left;',
810
767
  '}',
811
768
 
812
769
  /* ══════════════════════════════════════════════════
813
- MOBILE RESPONSIVE (< 480px)
770
+ MOBILE RESPONSIVE
814
771
  ══════════════════════════════════════════════════ */
815
772
  '@media (max-width: 480px) {',
816
- ' .wakz-overlay {',
817
- ' display: none;',
818
- ' }',
819
773
  ' .wakz-window {',
820
- ' top: auto !important;',
821
- ' left: 0 !important;',
822
- ' right: 0 !important;',
823
- ' bottom: 0 !important;',
824
774
  ' width: 100vw !important;',
825
775
  ' height: 100vh !important;',
776
+ ' min-height: 100vh !important;',
826
777
  ' max-height: 100vh !important;',
827
- ' border-radius: 20px 20px 0 0 !important;',
828
- ' transform: translateY(100%) !important;',
829
- ' border: none !important;',
830
- ' border-top: 1px solid #E5E5E5 !important;',
778
+ ' bottom: 0 !important;',
779
+ ' top: 0 !important;',
780
+ ' right: 0 !important;',
781
+ ' left: 0 !important;',
782
+ ' border-radius: 0 !important;',
831
783
  ' }',
832
784
  ' .wakz-window.wakz-visible {',
833
- ' transform: translateY(0) !important;',
785
+ ' transform: translateY(0) scale(1);',
834
786
  ' }',
835
787
  ' .wakz-fab-pos-br { bottom: 16px; right: 16px; }',
836
788
  ' .wakz-fab-pos-bl { bottom: 16px; left: 16px; }',
@@ -847,8 +799,6 @@
847
799
  var isRtl = self.config.language === 'ar';
848
800
  var posClass = self.config.position === 'bottom-left' ? 'bl' : 'br';
849
801
  var str = _strings(self.config.language);
850
- var iconColor = self._scriptIconColor || self.config.iconColor;
851
- var iconShape = self._scriptIconShape || self.config.iconShape;
852
802
 
853
803
  /* ── Host + Shadow DOM ── */
854
804
  self._host = _el('div');
@@ -861,16 +811,11 @@
861
811
  self._shadow.appendChild(self._root);
862
812
 
863
813
  /* ══════════════ FLOATING ACTION BUTTON ══════════════ */
864
- var fabClasses = 'wakz-fab wakz-fab-pos-' + posClass;
865
- if (iconShape === 'rounded') fabClasses += ' wakz-fab-shape-rounded';
866
-
867
814
  self._toggleBtn = _el('button', {
868
- className: fabClasses,
869
- 'aria-label': str.openChat,
870
- style: { '--wakz-icon-color': iconColor }
815
+ className: 'wakz-fab wakz-fab-pos-' + posClass,
816
+ 'aria-label': str.openChat
871
817
  });
872
- self._toggleBtn.style.background = iconColor;
873
- self._toggleBtn.innerHTML = _ICONS.chatIcon;
818
+ self._toggleBtn.innerHTML = _ICONS.chatBubble;
874
819
 
875
820
  /* Status dot on FAB */
876
821
  self._statusDot = _el('span', { className: 'wakz-fab-dot ' + (self.config.online ? 'online' : 'offline') });
@@ -878,41 +823,32 @@
878
823
  self._toggleBtn.appendChild(self._statusDot);
879
824
  self._root.appendChild(self._toggleBtn);
880
825
 
881
- /* ══════════════ OVERLAY BACKDROP ══════════════ */
882
- self._overlayEl = _el('div', { className: 'wakz-overlay' });
883
- self._root.appendChild(self._overlayEl);
884
-
885
- /* ══════════════ CHAT WINDOW (Centered Modal) ══════════════ */
886
- self._chatWindow = _el('div', { className: 'wakz-window' });
826
+ /* ══════════════ CHAT WINDOW ══════════════ */
827
+ self._chatWindow = _el('div', { className: 'wakz-window wakz-win-pos-' + posClass });
887
828
 
888
829
  /* ── Header ── */
889
830
  var headerEl = _el('div', { className: 'wakz-hdr' });
890
831
 
891
- /* Right side: avatar + name (in RTL, right side = start) */
892
- var headerRight = _el('div', { className: 'wakz-hdr-right' });
832
+ var headerLeft = _el('div', { className: 'wakz-hdr-left' });
893
833
  var avatarLetter = (self.config.botName || 'W')[0].toUpperCase();
894
- headerRight.appendChild(_el('div', { className: 'wakz-avatar' }, [avatarLetter]));
834
+ headerLeft.appendChild(_el('div', { className: 'wakz-avatar' }, [avatarLetter]));
895
835
 
896
836
  var headerInfo = _el('div', { className: 'wakz-hdr-info' });
897
837
  headerInfo.appendChild(_el('span', { className: 'wakz-hdr-name' }, [self.config.botName]));
898
- headerRight.appendChild(headerInfo);
899
- headerEl.appendChild(headerRight);
900
-
901
- /* Left side: status + close (in RTL, left side = end) */
902
- var headerLeft = _el('div', { className: 'wakz-hdr-left' });
903
838
 
904
839
  var statusLine = _el('span', { className: 'wakz-hdr-status' });
905
840
  self._headerStatusDot = _el('span', { className: 'wakz-hdr-status-dot ' + (self.config.online ? 'online' : 'offline') });
906
841
  if (self.config.showStatus) statusLine.appendChild(self._headerStatusDot);
907
842
  self._headerStatusText = document.createTextNode(self.config.online ? str.online : str.offline);
908
843
  statusLine.appendChild(self._headerStatusText);
909
- headerLeft.appendChild(statusLine);
844
+ headerInfo.appendChild(statusLine);
910
845
 
911
- var closeBtn = _el('button', { className: 'wakz-close', 'aria-label': str.closeChat });
912
- closeBtn.innerHTML = _ICONS.close;
913
- headerLeft.appendChild(closeBtn);
846
+ headerLeft.appendChild(headerInfo);
914
847
  headerEl.appendChild(headerLeft);
915
848
 
849
+ var closeBtn = _el('button', { className: 'wakz-close', 'aria-label': str.closeChat });
850
+ closeBtn.innerHTML = _ICONS.close;
851
+ headerEl.appendChild(closeBtn);
916
852
  self._chatWindow.appendChild(headerEl);
917
853
 
918
854
  /* ── Messages Container ── */
@@ -958,11 +894,6 @@
958
894
  self.toggleChat(false);
959
895
  });
960
896
 
961
- /* Overlay backdrop click to close */
962
- self._overlayEl.addEventListener('click', function () {
963
- self.toggleChat(false);
964
- });
965
-
966
897
  /* Send button click */
967
898
  self._sendBtn.addEventListener('click', function () {
968
899
  self._handleSend();
@@ -1011,9 +942,6 @@
1011
942
  self.isOpen = open;
1012
943
  if (open) {
1013
944
  self._chatWindow.classList.add('wakz-visible');
1014
- self._overlayEl.classList.add('wakz-visible');
1015
- /* Hide FAB when chat is open */
1016
- self._toggleBtn.classList.add('wakz-fab-hidden');
1017
945
  /* Fetch history on first open (after config is loaded) */
1018
946
  if (self._configLoaded && !self._hasFetchedHistory) {
1019
947
  self._fetchHistory();
@@ -1027,9 +955,6 @@
1027
955
  setTimeout(function () { self._inputEl.focus(); }, 320);
1028
956
  } else {
1029
957
  self._chatWindow.classList.remove('wakz-visible');
1030
- self._overlayEl.classList.remove('wakz-visible');
1031
- /* Show FAB when chat is closed */
1032
- self._toggleBtn.classList.remove('wakz-fab-hidden');
1033
958
  /* Stop polling when chat is closed */
1034
959
  self._stopPolling();
1035
960
  }
@@ -1056,9 +981,6 @@
1056
981
  .then(function (data) {
1057
982
  if (data && data.success && data.config) {
1058
983
  self.config = Object.assign({}, _DEFAULTS, data.config);
1059
- /* Apply script-level overrides */
1060
- if (self._scriptIconColor) self.config.iconColor = self._scriptIconColor;
1061
- if (self._scriptIconShape) self.config.iconShape = self._scriptIconShape;
1062
984
  self._configLoaded = true;
1063
985
  self._applyConfig();
1064
986
  } else {
@@ -1098,13 +1020,11 @@
1098
1020
  var str = _strings(cfg.language);
1099
1021
  var isRtl = cfg.language === 'ar';
1100
1022
  var posClass = cfg.position === 'bottom-left' ? 'bl' : 'br';
1101
- var iconColor = cfg.iconColor || '#171717';
1102
- var iconShape = cfg.iconShape || 'circle';
1103
1023
 
1104
1024
  /* ── Update CSS custom properties ── */
1105
1025
  var hostStyle = self._shadow.host.style;
1106
1026
  hostStyle.setProperty('--wakz-primary', cfg.primaryColor);
1107
- hostStyle.setProperty('--wakz-icon-color', iconColor);
1027
+ hostStyle.setProperty('--wakz-btn', cfg.btnColor);
1108
1028
  hostStyle.setProperty('--wakz-chat-bg', cfg.chatBg);
1109
1029
  hostStyle.setProperty('--wakz-widget-bg', cfg.widgetBg);
1110
1030
 
@@ -1113,19 +1033,19 @@
1113
1033
  else self._root.classList.remove('wakz-rtl');
1114
1034
 
1115
1035
  /* ── FAB ── */
1116
- var fabClasses = 'wakz-fab wakz-fab-pos-' + posClass + ' wakz-fab-enter';
1117
- if (iconShape === 'rounded') fabClasses += ' wakz-fab-shape-rounded';
1118
- if (self.isOpen) fabClasses += ' wakz-fab-hidden';
1119
- self._toggleBtn.className = fabClasses;
1120
- self._toggleBtn.style.background = iconColor;
1036
+ self._toggleBtn.className = 'wakz-fab wakz-fab-pos-' + posClass + ' wakz-fab-enter';
1121
1037
  self._toggleBtn.setAttribute('aria-label', str.openChat);
1122
1038
 
1123
1039
  /* ── FAB Status Dot ── */
1124
1040
  self._statusDot.className = 'wakz-fab-dot ' + (cfg.online ? 'online' : 'offline');
1125
1041
  self._statusDot.style.display = cfg.showStatus ? '' : 'none';
1126
1042
 
1127
- /* ── Window ── */
1128
- self._chatWindow.className = 'wakz-window' + (self.isOpen ? ' wakz-visible' : '');
1043
+ /* ── Window Position ── */
1044
+ self._chatWindow.className = 'wakz-window wakz-win-pos-' + posClass + (self.isOpen ? ' wakz-visible' : '');
1045
+
1046
+ /* ── Header ── */
1047
+ var headerEl = self._chatWindow.querySelector('.wakz-hdr');
1048
+ if (headerEl) headerEl.style.background = cfg.primaryColor;
1129
1049
 
1130
1050
  /* ── Bot Name & Avatar ── */
1131
1051
  var nameEl = self._chatWindow.querySelector('.wakz-hdr-name');
@@ -1227,6 +1147,8 @@
1227
1147
  WAKZWidget.prototype._pollForNewMessages = function () {
1228
1148
  var self = this;
1229
1149
  if (!self.server || !self.apiKey || !self._lastPollTime) return;
1150
+ /* Skip polling while waiting for our own reply — prevents race condition duplicates */
1151
+ if (self._pendingReply) return;
1230
1152
 
1231
1153
  var url = self.server + '/api/v1/embed/chat?api_key=' +
1232
1154
  encodeURIComponent(self.apiKey) +
@@ -1273,7 +1195,7 @@
1273
1195
  };
1274
1196
 
1275
1197
  /* ════════════════════════════════════════════════════════════════
1276
- APPEND WELCOME MESSAGE (bot style — no bubble, flat on bg)
1198
+ APPEND WELCOME MESSAGE (with bot avatar)
1277
1199
  ════════════════════════════════════════════════════════════════ */
1278
1200
 
1279
1201
  WAKZWidget.prototype._appendWelcomeMessage = function (text) {
@@ -1283,13 +1205,13 @@
1283
1205
  var wrap = _el('div', { className: 'wakz-welcome-wrap' });
1284
1206
 
1285
1207
  /* Bot avatar */
1286
- var avatar = _el('div', { className: 'wakz-msg-bot-avatar' },
1208
+ var avatar = _el('div', { className: 'wakz-welcome-avatar' },
1287
1209
  [(self.config.botName || 'W')[0].toUpperCase()]);
1288
1210
  wrap.appendChild(avatar);
1289
1211
 
1290
- /* Flat content with accent border */
1291
- var content = _el('div', { className: 'wakz-msg-bot-content' }, [text]);
1292
- wrap.appendChild(content);
1212
+ /* Bubble */
1213
+ var bubble = _el('div', { className: 'wakz-bubble bot' }, [text]);
1214
+ wrap.appendChild(bubble);
1293
1215
 
1294
1216
  self._messagesContainer.appendChild(wrap);
1295
1217
  self.messages.push({ sender: 'bot', text: text });
@@ -1303,30 +1225,41 @@
1303
1225
  WAKZWidget.prototype._appendMessage = function (sender, text, isError, timestamp, isHumanAgent) {
1304
1226
  var self = this;
1305
1227
 
1306
- if (sender === 'user') {
1307
- /* ── User message: rounded bubble ── */
1308
- var row = _el('div', { className: 'wakz-msg-row user' });
1309
- var bubble = _el('div', { className: 'wakz-bubble-user' });
1310
- bubble.appendChild(document.createTextNode(text));
1228
+ var row = _el('div', { className: 'wakz-msg-row ' + sender });
1311
1229
 
1312
- /* Timestamp */
1313
- var tsText = self._formatTime(timestamp);
1314
- bubble.appendChild(_el('span', { className: 'wakz-ts' }, [tsText]));
1230
+ var bubbleClass = 'wakz-bubble ' + sender;
1231
+ if (isError) bubbleClass += ' error-bubble';
1232
+ var bubble = _el('div', { className: bubbleClass });
1315
1233
 
1316
- row.appendChild(bubble);
1317
- self._messagesContainer.appendChild(row);
1318
- self.messages.push({ sender: sender, text: text, isError: !!isError });
1319
- self._scrollToBottom();
1320
- return;
1234
+ bubble.appendChild(document.createTextNode(text));
1235
+
1236
+ /* Timestamp */
1237
+ var tsText = '';
1238
+ if (timestamp) {
1239
+ try {
1240
+ var d = new Date(timestamp);
1241
+ if (!isNaN(d.getTime())) {
1242
+ tsText = d.getHours().toString().padStart(2, '0') + ':' +
1243
+ d.getMinutes().toString().padStart(2, '0');
1244
+ }
1245
+ } catch (e) { /* ignore */ }
1321
1246
  }
1247
+ if (!tsText) {
1248
+ var now = new Date();
1249
+ tsText = now.getHours().toString().padStart(2, '0') + ':' +
1250
+ now.getMinutes().toString().padStart(2, '0');
1251
+ }
1252
+ bubble.appendChild(_el('span', { className: 'wakz-ts' }, [tsText]));
1322
1253
 
1323
- if (isError) {
1324
- /* ── Error message: red-tinted bubble ── */
1325
- var errRow = _el('div', { className: 'wakz-msg-row bot' });
1326
- var errBubble = _el('div', { className: 'wakz-bubble-error' });
1327
- errBubble.appendChild(document.createTextNode(text));
1254
+ /* Human agent badge — visual distinction for admin replies */
1255
+ if (isHumanAgent) {
1256
+ var iStr = _strings(self.config.language);
1257
+ var badge = _el('span', { className: 'wakz-human-badge' }, [iStr.humanBadge || '👤 فريق الدعم']);
1258
+ bubble.appendChild(badge);
1259
+ }
1328
1260
 
1329
- /* Retry button for errors */
1261
+ /* Retry button for errors */
1262
+ if (isError) {
1330
1263
  var str = _strings(self.config.language);
1331
1264
  var retryBtn = _el('button', { className: 'wakz-retry' }, [
1332
1265
  _ICONS.error + ' ' + str.retry
@@ -1352,71 +1285,19 @@
1352
1285
  }
1353
1286
  if (lastUserMsg) self._sendToAPI(lastUserMsg);
1354
1287
  });
1355
- })(errRow);
1356
- errBubble.appendChild(retryBtn);
1357
-
1358
- errRow.appendChild(errBubble);
1359
- self._messagesContainer.appendChild(errRow);
1360
- self.messages.push({ sender: sender, text: text, isError: true });
1361
- self._scrollToBottom();
1362
- return;
1288
+ })(row);
1289
+ bubble.appendChild(retryBtn);
1363
1290
  }
1364
1291
 
1365
- /* ── Bot / Support message: flat on bg with accent border ── */
1366
- var botRow = _el('div', { className: 'wakz-msg-row bot' });
1367
- var wrap = _el('div', { className: 'wakz-msg-bot-wrap' });
1292
+ row.appendChild(bubble);
1293
+ self._messagesContainer.appendChild(row);
1368
1294
 
1369
- /* Small bot avatar */
1370
- var avatar = _el('div', { className: 'wakz-msg-bot-avatar' },
1371
- [(self.config.botName || 'W')[0].toUpperCase()]);
1372
- wrap.appendChild(avatar);
1373
-
1374
- /* Flat content */
1375
- var contentClass = 'wakz-msg-bot-content';
1376
- if (isHumanAgent) contentClass += ' wakz-human';
1377
- var content = _el('div', { className: contentClass });
1378
- content.appendChild(document.createTextNode(text));
1379
-
1380
- /* Timestamp */
1381
- var botTs = self._formatTime(timestamp);
1382
- content.appendChild(_el('span', { className: 'wakz-ts' }, [botTs]));
1383
-
1384
- /* Human agent badge */
1385
- if (isHumanAgent) {
1386
- var iStr = _strings(self.config.language);
1387
- var badge = _el('span', { className: 'wakz-human-badge' }, [iStr.humanBadge || '👤 فريق الدعم']);
1388
- content.appendChild(badge);
1389
- }
1390
-
1391
- wrap.appendChild(content);
1392
- botRow.appendChild(wrap);
1393
- self._messagesContainer.appendChild(botRow);
1394
-
1395
- self.messages.push({ sender: sender, text: text, isError: false, isHumanAgent: !!isHumanAgent });
1295
+ self.messages.push({ sender: sender, text: text, isError: !!isError });
1396
1296
  self._scrollToBottom();
1397
1297
  };
1398
1298
 
1399
1299
  /* ════════════════════════════════════════════════════════════════
1400
- FORMAT TIME
1401
- ════════════════════════════════════════════════════════════════ */
1402
-
1403
- WAKZWidget.prototype._formatTime = function (timestamp) {
1404
- if (timestamp) {
1405
- try {
1406
- var d = new Date(timestamp);
1407
- if (!isNaN(d.getTime())) {
1408
- return d.getHours().toString().padStart(2, '0') + ':' +
1409
- d.getMinutes().toString().padStart(2, '0');
1410
- }
1411
- } catch (e) { /* ignore */ }
1412
- }
1413
- var now = new Date();
1414
- return now.getHours().toString().padStart(2, '0') + ':' +
1415
- now.getMinutes().toString().padStart(2, '0');
1416
- };
1417
-
1418
- /* ════════════════════════════════════════════════════════════════
1419
- TYPING INDICATOR (no bubble — on background)
1300
+ TYPING INDICATOR
1420
1301
  ════════════════════════════════════════════════════════════════ */
1421
1302
 
1422
1303
  WAKZWidget.prototype._showTyping = function () {
@@ -1491,6 +1372,10 @@
1491
1372
  self._sendBtn.disabled = true;
1492
1373
  self._showTyping();
1493
1374
 
1375
+ /* Freeze poll time BEFORE sending — prevents polling from fetching our own messages */
1376
+ self._lastPollTime = new Date().toISOString();
1377
+ self._pendingReply = true;
1378
+
1494
1379
  var payload = {
1495
1380
  api_key: self.apiKey,
1496
1381
  visitor_id: self.visitorId,
@@ -1499,7 +1384,10 @@
1499
1384
  domain: window.location.hostname || '',
1500
1385
  page_url: window.location.href || '',
1501
1386
  user_agent: navigator.userAgent || '',
1502
- language: navigator.language || ''
1387
+ language: navigator.language || '',
1388
+ device_model: self._deviceInfo.device_model || '',
1389
+ device_platform: self._deviceInfo.device_platform || '',
1390
+ device_type: self._deviceInfo.device_type || 'desktop'
1503
1391
  }
1504
1392
  };
1505
1393
 
@@ -1514,19 +1402,23 @@
1514
1402
  .then(function (res) { return res.json(); })
1515
1403
  .then(function (data) {
1516
1404
  self._hideTyping();
1405
+ self._pendingReply = false;
1517
1406
  if (data && data.success && data.reply) {
1518
1407
  self._appendMessage('bot', data.reply);
1519
1408
  } else {
1520
1409
  self._appendMessage('bot', _strings(self.config.language).errorMsg, true);
1521
1410
  }
1522
- /* Update poll time to prevent re-fetching messages we already displayed */
1523
- self._lastPollTime = new Date().toISOString();
1411
+ /* Advance poll time past any messages created during this request */
1412
+ self._lastPollTime = (data && data.timestamp)
1413
+ ? new Date(new Date(data.timestamp).getTime() + 1000).toISOString()
1414
+ : new Date(Date.now() + 1000).toISOString();
1524
1415
  })
1525
1416
  .catch(function () {
1526
1417
  self._hideTyping();
1418
+ self._pendingReply = false;
1527
1419
  self._appendMessage('bot', _strings(self.config.language).errorMsg, true);
1528
- /* Update poll time even on error to prevent re-fetching */
1529
- self._lastPollTime = new Date().toISOString();
1420
+ /* Advance poll time even on error */
1421
+ self._lastPollTime = new Date(Date.now() + 1000).toISOString();
1530
1422
  })
1531
1423
  .finally(function () {
1532
1424
  self.isLoading = false;