wakz-chat-widget 2.0.0 → 3.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 +1 -44
  2. package/index.js +495 -448
  3. package/package.json +6 -24
package/index.js CHANGED
@@ -2,10 +2,9 @@
2
2
  * WAKZ Chat Widget v3.0.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
+ * Complete UI/UX revamp glass morphism, centered modal, modern design.
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();
200
226
 
201
227
  /* ── Runtime state ── */
202
228
  self.config = Object.assign({}, _DEFAULTS);
@@ -209,22 +235,26 @@
209
235
  self._configError = false;
210
236
  self._pollTimer = null;
211
237
  self._lastPollTime = null;
212
- self._POLL_INTERVAL = 4000; /* Poll every 4 seconds */
213
- self._knownMessageIds = {}; /* Deduplication set for message IDs */
238
+ self._POLL_INTERVAL = 4000;
239
+ self._knownMessageIds = {};
240
+ self._pendingReply = false;
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;
246
+ self._overlay = null;
220
247
  self._chatWindow = null;
221
248
  self._messagesContainer = null;
249
+ self._inputArea = null;
222
250
  self._inputEl = null;
223
251
  self._sendBtn = null;
224
252
  self._toggleBtn = null;
225
253
  self._statusDot = null;
226
254
  self._headerStatusDot = null;
227
255
  self._headerStatusText = null;
256
+ self._headerAvatarEl = null;
257
+ self._headerNameEl = null;
228
258
 
229
259
  /* ── Bootstrap ── */
230
260
  self._injectCSS();
@@ -235,7 +265,7 @@
235
265
  }
236
266
 
237
267
  /* ════════════════════════════════════════════════════════════════
238
- CSS — Complete premium styling injected into Shadow DOM
268
+ CSS — Complete styling injected into Shadow DOM (v3.0.0 revamp)
239
269
  ════════════════════════════════════════════════════════════════ */
240
270
 
241
271
  WAKZWidget.prototype._injectCSS = function () {
@@ -246,14 +276,15 @@
246
276
  /* ── CSS Custom Properties (theming) ── */
247
277
  ':host {',
248
278
  ' --wakz-primary: #171717;',
249
- ' --wakz-icon-color: #171717;',
250
- ' --wakz-chat-bg: #FAFAFA;',
279
+ ' --wakz-btn: #171717;',
280
+ ' --wakz-chat-bg: #f8f9fa;',
251
281
  ' --wakz-widget-bg: #ffffff;',
252
- ' --wakz-radius: 20px;',
253
- ' --wakz-radius-bubble: 18px;',
254
- ' --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);',
282
+ ' --wakz-icon-radius: 50%;',
283
+ ' --wakz-shadow-sm: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06);',
284
+ ' --wakz-shadow-md: 0 4px 12px rgba(0,0,0,.1), 0 2px 4px rgba(0,0,0,.06);',
285
+ ' --wakz-shadow-lg: 0 25px 60px rgba(0,0,0,.15), 0 10px 20px rgba(0,0,0,.1);',
286
+ ' --wakz-transition: 200ms ease;',
287
+ ' --wakz-font: \'Inter\', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;',
257
288
  ' all: initial;',
258
289
  ' font-family: var(--wakz-font);',
259
290
  '}',
@@ -266,55 +297,49 @@
266
297
  '}',
267
298
 
268
299
  /* ══════════════════════════════════════════════════
269
- FLOATING ACTION BUTTON (FAB)
300
+ FLOATING ACTION BUTTON (FAB) — 48px, independent color
270
301
  ══════════════════════════════════════════════════ */
271
302
  '.wakz-fab {',
272
303
  ' position: fixed;',
273
304
  ' z-index: 2147483647;',
274
- ' width: 52px;',
275
- ' height: 52px;',
276
- ' border-radius: 50%;',
305
+ ' width: 48px;',
306
+ ' height: 48px;',
307
+ ' border-radius: var(--wakz-icon-radius);',
277
308
  ' border: none;',
278
309
  ' cursor: pointer;',
279
310
  ' display: flex;',
280
311
  ' align-items: center;',
281
312
  ' 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;',
313
+ ' box-shadow: 0 4px 14px rgba(0,0,0,.15), 0 2px 6px rgba(0,0,0,.1);',
314
+ ' transition: transform var(--wakz-transition), box-shadow var(--wakz-transition);',
284
315
  ' outline: none;',
285
316
  ' -webkit-tap-highlight-color: transparent;',
286
- ' background: var(--wakz-icon-color, #171717);',
317
+ ' background: var(--wakz-btn);',
287
318
  '}',
288
319
  '.wakz-fab:hover {',
289
- ' 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
+ ' transform: scale(1.06);',
321
+ ' box-shadow: 0 8px 24px rgba(0,0,0,.2), 0 4px 10px rgba(0,0,0,.12);',
291
322
  '}',
292
323
  '.wakz-fab:active {',
293
- ' transform: scale(0.96);',
324
+ ' transform: scale(0.94);',
294
325
  '}',
295
326
  '.wakz-fab svg {',
296
- ' width: 24px;',
297
- ' height: 24px;',
327
+ ' width: 22px;',
328
+ ' height: 22px;',
298
329
  ' color: #ffffff;',
299
330
  ' pointer-events: none;',
300
331
  '}',
301
332
 
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
333
  /* ── FAB Positioning ── */
315
334
  '.wakz-fab-pos-br { bottom: 24px; right: 24px; }',
316
335
  '.wakz-fab-pos-bl { bottom: 24px; left: 24px; }',
317
336
 
337
+ /* ── Hide FAB when chat is open ── */
338
+ '.wakz-window.wakz-visible ~ .wakz-fab,',
339
+ '.wakz-fab.wakz-fab-hidden {',
340
+ ' display: none !important;',
341
+ '}',
342
+
318
343
  /* ── FAB Entrance Bounce ── */
319
344
  '@keyframes wakz-fab-enter {',
320
345
  ' 0% { opacity: 0; transform: scale(0.3); }',
@@ -327,14 +352,14 @@
327
352
  '}',
328
353
 
329
354
  /* ══════════════════════════════════════════════════
330
- STATUS DOT (on FAB)
355
+ STATUS DOT (on FAB) — 10px, top-right
331
356
  ══════════════════════════════════════════════════ */
332
357
  '.wakz-fab-dot {',
333
358
  ' position: absolute;',
334
359
  ' top: -1px;',
335
360
  ' right: -1px;',
336
- ' width: 12px;',
337
- ' height: 12px;',
361
+ ' width: 10px;',
362
+ ' height: 10px;',
338
363
  ' border-radius: 50%;',
339
364
  ' border: 2px solid #ffffff;',
340
365
  ' z-index: 2;',
@@ -351,7 +376,7 @@
351
376
  '}',
352
377
  '@keyframes wakz-dot-pulse {',
353
378
  ' 0%, 100% { box-shadow: 0 0 0 0 rgba(34,197,94,0.5); }',
354
- ' 50% { box-shadow: 0 0 0 6px rgba(34,197,94,0); }',
379
+ ' 50% { box-shadow: 0 0 0 5px rgba(34,197,94,0); }',
355
380
  '}',
356
381
  '.wakz-fab-dot.online {',
357
382
  ' animation: wakz-dot-pulse 2s ease-in-out infinite;',
@@ -364,21 +389,18 @@
364
389
  '}',
365
390
 
366
391
  /* ══════════════════════════════════════════════════
367
- OVERLAY BACKDROP
392
+ OVERLAY / BACKDROP — glass blur behind modal
368
393
  ══════════════════════════════════════════════════ */
369
394
  '.wakz-overlay {',
370
395
  ' position: fixed;',
371
- ' top: 0;',
372
- ' left: 0;',
373
- ' right: 0;',
374
- ' bottom: 0;',
396
+ ' inset: 0;',
375
397
  ' z-index: 2147483645;',
376
- ' background: rgba(0,0,0,0.4);',
377
- ' -webkit-backdrop-filter: blur(4px);',
398
+ ' background: rgba(0,0,0,0.25);',
378
399
  ' backdrop-filter: blur(4px);',
400
+ ' -webkit-backdrop-filter: blur(4px);',
379
401
  ' opacity: 0;',
380
402
  ' pointer-events: none;',
381
- ' transition: opacity 0.3s cubic-bezier(0.16,1,0.3,1);',
403
+ ' transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);',
382
404
  '}',
383
405
  '.wakz-overlay.wakz-visible {',
384
406
  ' opacity: 1;',
@@ -394,20 +416,18 @@
394
416
  ' top: 50%;',
395
417
  ' left: 50%;',
396
418
  ' transform: translate(-50%, -50%) scale(0.95);',
397
- ' width: 420px;',
398
- ' max-height: 75vh;',
399
- ' border-radius: 20px;',
419
+ ' width: 390px;',
420
+ ' height: min(580px, 80vh);',
421
+ ' border-radius: 16px;',
400
422
  ' overflow: hidden;',
401
423
  ' display: flex;',
402
424
  ' flex-direction: column;',
403
425
  ' 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);',
426
+ ' box-shadow: 0 25px 60px rgba(0,0,0,0.15), 0 10px 20px rgba(0,0,0,0.1);',
408
427
  ' opacity: 0;',
409
428
  ' 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);',
429
+ ' transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),',
430
+ ' transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);',
411
431
  '}',
412
432
  '.wakz-window.wakz-visible {',
413
433
  ' opacity: 1;',
@@ -416,11 +436,12 @@
416
436
  '}',
417
437
 
418
438
  /* ══════════════════════════════════════════════════
419
- HEADER — Glassmorphism (matching floatingGlassShell)
439
+ HEADER — Glass effect matching main site
420
440
  ══════════════════════════════════════════════════ */
421
441
  '.wakz-hdr {',
422
442
  ' flex-shrink: 0;',
423
- ' padding: 14px 18px;',
443
+ ' height: 52px;',
444
+ ' padding: 0 16px;',
424
445
  ' display: flex;',
425
446
  ' align-items: center;',
426
447
  ' justify-content: space-between;',
@@ -428,32 +449,39 @@
428
449
  ' cursor: default;',
429
450
  ' user-select: none;',
430
451
  ' 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;',
452
+ ' backdrop-filter: blur(24px);',
453
+ ' -webkit-backdrop-filter: blur(24px);',
454
+ ' border-bottom: 1px solid rgba(229,229,229,0.6);',
455
+ ' box-shadow: 0 4px 12px rgba(0,0,0,0.05);',
456
+ ' border-radius: 16px 16px 0 0;',
435
457
  '}',
436
- '.wakz-hdr-right {',
458
+ '.wakz-hdr-left {',
437
459
  ' display: flex;',
438
460
  ' align-items: center;',
439
- ' gap: 11px;',
461
+ ' gap: 10px;',
440
462
  ' min-width: 0;',
463
+ ' flex: 1;',
464
+ '}',
465
+ '.wakz-hdr-right {',
466
+ ' display: flex;',
467
+ ' align-items: center;',
468
+ ' gap: 8px;',
469
+ ' flex-shrink: 0;',
441
470
  '}',
442
471
 
443
- /* ── Bot Avatar (header) ── */
472
+ /* ── Bot Avatar (32px circle) ── */
444
473
  '.wakz-avatar {',
445
- ' width: 36px;',
446
- ' height: 36px;',
474
+ ' width: 32px;',
475
+ ' height: 32px;',
447
476
  ' border-radius: 50%;',
448
- ' background: var(--wakz-primary);',
477
+ ' background: rgba(23,23,23,0.1);',
449
478
  ' display: flex;',
450
479
  ' align-items: center;',
451
480
  ' justify-content: center;',
452
481
  ' font-weight: 700;',
453
- ' font-size: 15px;',
482
+ ' font-size: 14px;',
454
483
  ' flex-shrink: 0;',
455
- ' color: #ffffff;',
456
- ' letter-spacing: 0.5px;',
484
+ ' color: var(--wakz-primary);',
457
485
  '}',
458
486
 
459
487
  /* ── Header Info ── */
@@ -463,7 +491,7 @@
463
491
  ' min-width: 0;',
464
492
  '}',
465
493
  '.wakz-hdr-name {',
466
- ' font-size: 16px;',
494
+ ' font-size: 15px;',
467
495
  ' font-weight: 600;',
468
496
  ' line-height: 1.3;',
469
497
  ' color: #171717;',
@@ -471,23 +499,19 @@
471
499
  ' overflow: hidden;',
472
500
  ' text-overflow: ellipsis;',
473
501
  '}',
474
-
475
- /* ── Header Left: status + close ── */
476
- '.wakz-hdr-left {',
477
- ' display: flex;',
478
- ' align-items: center;',
479
- ' gap: 10px;',
480
- '}',
481
502
  '.wakz-hdr-status {',
482
- ' font-size: 12px;',
483
- ' color: #171717;',
503
+ ' font-size: 11px;',
504
+ ' color: #A3A3A3;',
484
505
  ' display: flex;',
485
506
  ' align-items: center;',
486
- ' gap: 5px;',
507
+ ' gap: 4px;',
508
+ ' margin-top: 1px;',
487
509
  '}',
510
+
511
+ /* ── Header Status Dot (6px) ── */
488
512
  '.wakz-hdr-status-dot {',
489
- ' width: 7px;',
490
- ' height: 7px;',
513
+ ' width: 6px;',
514
+ ' height: 6px;',
491
515
  ' border-radius: 50%;',
492
516
  ' display: inline-block;',
493
517
  ' flex-shrink: 0;',
@@ -496,161 +520,165 @@
496
520
  '.wakz-hdr-status-dot.online { background: #22c55e; }',
497
521
  '.wakz-hdr-status-dot.offline { background: #ef4444; }',
498
522
 
499
- /* ── Close Button ── */
523
+ /* ── Close Button (28px circle, dark) ── */
500
524
  '.wakz-close {',
501
- ' width: 32px;',
502
- ' height: 32px;',
525
+ ' width: 28px;',
526
+ ' height: 28px;',
503
527
  ' border-radius: 50%;',
504
528
  ' border: none;',
505
- ' background: transparent;',
506
- ' color: #171717;',
529
+ ' background: rgba(23,23,23,0.05);',
530
+ ' color: #525252;',
507
531
  ' cursor: pointer;',
508
532
  ' display: flex;',
509
533
  ' align-items: center;',
510
534
  ' justify-content: center;',
511
- ' transition: background 0.2s ease;',
535
+ ' transition: background var(--wakz-transition), color var(--wakz-transition);',
512
536
  ' outline: none;',
513
537
  ' flex-shrink: 0;',
514
538
  '}',
515
539
  '.wakz-close:hover {',
516
- ' background: rgba(0,0,0,0.06);',
540
+ ' background: rgba(23,23,23,0.1);',
541
+ ' color: #171717;',
517
542
  '}',
518
543
  '.wakz-close svg {',
519
- ' width: 18px;',
520
- ' height: 18px;',
544
+ ' width: 16px;',
545
+ ' height: 16px;',
521
546
  '}',
522
547
 
523
548
  /* ══════════════════════════════════════════════════
524
- MESSAGES AREA
549
+ MESSAGES AREA — scrollable, padding-bottom for input
525
550
  ══════════════════════════════════════════════════ */
526
551
  '.wakz-msgs {',
527
552
  ' flex: 1;',
528
553
  ' overflow-y: auto;',
529
554
  ' overflow-x: hidden;',
530
- ' padding: 16px;',
555
+ ' padding: 16px 14px 88px;',
531
556
  ' display: flex;',
532
557
  ' flex-direction: column;',
533
- ' gap: 8px;',
558
+ ' gap: 4px;',
534
559
  ' scroll-behavior: smooth;',
535
560
  ' background: var(--wakz-chat-bg);',
561
+ ' position: relative;',
536
562
  '}',
537
- '.wakz-msgs::-webkit-scrollbar { width: 5px; }',
563
+ '.wakz-msgs::-webkit-scrollbar { width: 4px; }',
538
564
  '.wakz-msgs::-webkit-scrollbar-track { background: transparent; }',
539
565
  '.wakz-msgs::-webkit-scrollbar-thumb {',
540
- ' background: rgba(0,0,0,0.08);',
566
+ ' background: rgba(0,0,0,0.1);',
541
567
  ' border-radius: 10px;',
542
568
  '}',
543
569
 
544
570
  /* ══════════════════════════════════════════════════
545
- MESSAGE STYLES
571
+ MESSAGE ROWS — new v3 styles
546
572
  ══════════════════════════════════════════════════ */
547
- '@keyframes wakz-msg-in {',
548
- ' 0% { opacity: 0; transform: translateY(8px); }',
549
- ' 100% { opacity: 1; transform: translateY(0); }',
550
- '}',
551
-
552
- /* ── User Message (bubble, right-aligned LTR / left-aligned RTL) ── */
553
573
  '.wakz-msg-row {',
554
- ' animation: wakz-msg-in 0.3s cubic-bezier(0.16,1,0.3,1) forwards;',
574
+ ' display: flex;',
575
+ ' animation: wakz-msg-in 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards;',
576
+ '}',
577
+ '.wakz-msg-row.bot {',
578
+ ' align-self: flex-start;',
579
+ ' align-items: flex-start;',
580
+ ' gap: 8px;',
581
+ ' max-width: 80%;',
555
582
  '}',
556
583
  '.wakz-msg-row.user {',
557
584
  ' align-self: flex-end;',
558
585
  ' max-width: 75%;',
559
586
  '}',
560
- '.wakz-msg-row.bot {',
561
- ' align-self: flex-start;',
562
- ' max-width: 85%;',
587
+
588
+ '@keyframes wakz-msg-in {',
589
+ ' 0% { opacity: 0; transform: translateY(8px); }',
590
+ ' 100% { opacity: 1; transform: translateY(0); }',
563
591
  '}',
564
592
 
565
- '.wakz-bubble-user {',
566
- ' padding: 10px 16px;',
593
+ /* ── User Bubbles ── */
594
+ '.wakz-bubble.user {',
595
+ ' padding: 10px 14px;',
567
596
  ' font-size: 14px;',
568
597
  ' line-height: 1.55;',
569
598
  ' word-wrap: break-word;',
570
599
  ' overflow-wrap: break-word;',
571
600
  ' white-space: pre-wrap;',
572
- ' border-radius: 18px 18px 6px 18px;',
601
+ ' border-radius: 14px;',
602
+ ' border-bottom-right-radius: 4px;',
573
603
  ' background: var(--wakz-primary);',
574
604
  ' color: #ffffff;',
605
+ ' box-shadow: 0 2px 8px rgba(0,0,0,0.08);',
575
606
  '}',
576
607
 
577
- /* ── Timestamp ── */
578
- '.wakz-ts {',
579
- ' display: block;',
580
- ' font-size: 10px;',
581
- ' opacity: 0.6;',
582
- ' margin-top: 4px;',
583
- '}',
584
- '.wakz-bubble-user .wakz-ts {',
585
- ' text-align: right;',
586
- ' color: rgba(255,255,255,0.7);',
587
- '}',
588
-
589
- /* ── Bot Message (no bubble, flat on bg with left accent border) ── */
590
- '.wakz-msg-bot-wrap {',
608
+ /* ── Bot Messages — NO bubble, text directly on background ── */
609
+ '.wakz-bot-content {',
591
610
  ' display: flex;',
592
611
  ' flex-direction: column;',
593
612
  ' gap: 2px;',
613
+ ' min-width: 0;',
594
614
  '}',
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;',
615
+ '.wakz-bot-text {',
612
616
  ' font-size: 14px;',
613
617
  ' line-height: 1.6;',
614
- ' color: #374151;',
618
+ ' color: #404040;',
615
619
  ' word-wrap: break-word;',
616
620
  ' overflow-wrap: break-word;',
617
621
  ' white-space: pre-wrap;',
618
622
  '}',
619
- '.wakz-msg-bot-content .wakz-ts {',
620
- ' color: #9CA3AF;',
623
+ '.wakz-bot-ts {',
624
+ ' font-size: 10px;',
625
+ ' color: #A3A3A3;',
621
626
  '}',
622
627
 
623
- /* ── Support / Human Agent Message ── */
624
- '.wakz-msg-bot-content.wakz-human {',
625
- ' border-left-color: #16A34A;',
626
- '}',
627
- '.wakz-human-badge {',
628
- ' display: inline-flex;',
628
+ /* ── Bot Avatar (22px, inline with message) ── */
629
+ '.wakz-bot-avatar {',
630
+ ' width: 22px;',
631
+ ' height: 22px;',
632
+ ' border-radius: 50%;',
633
+ ' background: rgba(23,23,23,0.08);',
634
+ ' display: flex;',
629
635
  ' align-items: center;',
630
- ' font-size: 11px;',
631
- ' background: rgba(22,163,74,0.08);',
632
- ' color: #16A34A;',
633
- ' border-radius: 20px;',
634
- ' padding: 2px 8px;',
635
- ' margin-top: 6px;',
636
- ' font-weight: 600;',
636
+ ' justify-content: center;',
637
+ ' font-weight: 700;',
638
+ ' font-size: 10px;',
639
+ ' flex-shrink: 0;',
640
+ ' color: var(--wakz-primary);',
641
+ ' margin-top: 2px;',
642
+ '}',
643
+
644
+ /* ── User Timestamp ── */
645
+ '.wakz-ts {',
646
+ ' display: block;',
647
+ ' font-size: 10px;',
648
+ ' opacity: 0.7;',
649
+ ' margin-top: 3px;',
650
+ ' text-align: right;',
637
651
  '}',
638
652
 
639
- /* ── Error bubble ── */
640
- '.wakz-bubble-error {',
653
+ /* ── Error Messages ── */
654
+ '.wakz-bubble.error-bubble {',
641
655
  ' padding: 10px 14px;',
642
656
  ' font-size: 14px;',
643
657
  ' line-height: 1.55;',
644
658
  ' word-wrap: break-word;',
645
659
  ' overflow-wrap: break-word;',
646
660
  ' white-space: pre-wrap;',
661
+ ' border-radius: 14px;',
662
+ ' border-bottom-left-radius: 4px;',
647
663
  ' background: #FEF2F2;',
648
664
  ' color: #DC2626;',
649
665
  ' border: 1px solid #FECACA;',
650
- ' border-radius: 18px 18px 6px 18px;',
651
666
  '}',
652
667
 
653
- /* ── Retry Button (inside error) ── */
668
+ /* ── Human Agent Badge ── */
669
+ '.wakz-human-badge {',
670
+ ' display: inline-block;',
671
+ ' font-size: 10px;',
672
+ ' background: rgba(22, 163, 74, 0.08);',
673
+ ' color: #16A34A;',
674
+ ' border-radius: 6px;',
675
+ ' padding: 2px 8px;',
676
+ ' margin-top: 4px;',
677
+ ' font-weight: 600;',
678
+ ' letter-spacing: 0.2px;',
679
+ '}',
680
+
681
+ /* ── Retry Button (inside error bubble) ── */
654
682
  '.wakz-retry {',
655
683
  ' display: inline-flex;',
656
684
  ' align-items: center;',
@@ -665,7 +693,7 @@
665
693
  ' color: #dc2626;',
666
694
  ' cursor: pointer;',
667
695
  ' font-family: var(--wakz-font);',
668
- ' transition: background 0.2s ease, border-color 0.2s ease;',
696
+ ' transition: background var(--wakz-transition), border-color var(--wakz-transition);',
669
697
  ' outline: none;',
670
698
  '}',
671
699
  '.wakz-retry:hover {',
@@ -674,32 +702,26 @@
674
702
  '}',
675
703
 
676
704
  /* ══════════════════════════════════════════════════
677
- WELCOME MESSAGE
705
+ TYPING INDICATOR — no bubble, just dots on background
678
706
  ══════════════════════════════════════════════════ */
679
- '.wakz-welcome-wrap {',
707
+ '.wakz-typing-row {',
680
708
  ' display: flex;',
681
- ' flex-direction: column;',
682
- ' gap: 2px;',
683
- ' max-width: 85%;',
709
+ ' align-items: flex-start;',
710
+ ' gap: 8px;',
684
711
  ' align-self: flex-start;',
685
- ' animation: wakz-msg-in 0.35s cubic-bezier(0.16,1,0.3,1) forwards;',
712
+ ' animation: wakz-msg-in 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards;',
686
713
  '}',
687
-
688
- /* ══════════════════════════════════════════════════
689
- TYPING INDICATOR (no bubble — on background)
690
- ══════════════════════════════════════════════════ */
691
714
  '.wakz-typing {',
692
715
  ' display: flex;',
693
716
  ' align-items: center;',
694
717
  ' gap: 4px;',
695
- ' padding: 10px 0 10px 14px;',
696
- ' border-left: 3px solid rgba(23,23,23,0.2);',
718
+ ' padding: 6px 0;',
697
719
  '}',
698
720
  '.wakz-typing-dot {',
699
- ' width: 7px;',
700
- ' height: 7px;',
721
+ ' width: 6px;',
722
+ ' height: 6px;',
701
723
  ' border-radius: 50%;',
702
- ' background: #9CA3AF;',
724
+ ' background: #A3A3A3;',
703
725
  ' animation: wakz-bounce-dot 1.4s ease-in-out infinite;',
704
726
  '}',
705
727
  '.wakz-typing-dot:nth-child(2) { animation-delay: 0.16s; }',
@@ -710,47 +732,53 @@
710
732
  '}',
711
733
 
712
734
  /* ══════════════════════════════════════════════════
713
- INPUT AREA — Glass Effect
735
+ INPUT AREA — Floating, glass effect, inside messages
714
736
  ══════════════════════════════════════════════════ */
715
- '.wakz-input-wrap {',
716
- ' 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);',
737
+ '.wakz-input-area {',
738
+ ' position: absolute;',
739
+ ' bottom: 12px;',
740
+ ' left: 12px;',
741
+ ' right: 12px;',
722
742
  ' display: flex;',
723
743
  ' align-items: flex-end;',
724
744
  ' gap: 8px;',
745
+ ' padding: 8px 10px 8px 14px;',
746
+ ' border-radius: 12px;',
747
+ ' background: rgba(255,255,255,0.85);',
748
+ ' backdrop-filter: blur(24px);',
749
+ ' -webkit-backdrop-filter: blur(24px);',
750
+ ' border: 1px solid rgba(229,229,229,0.6);',
751
+ ' box-shadow: 0 4px 12px rgba(0,0,0,0.05);',
752
+ ' z-index: 10;',
725
753
  '}',
726
754
  '.wakz-input {',
727
755
  ' flex: 1;',
728
- ' border: 1.5px solid #E5E5E5;',
729
- ' border-radius: 14px;',
730
- ' padding: 10px 16px;',
756
+ ' border: none;',
757
+ ' border-radius: 0;',
758
+ ' padding: 6px 4px;',
731
759
  ' font-size: 14px;',
732
760
  ' line-height: 1.4;',
733
761
  ' outline: none;',
734
762
  ' font-family: var(--wakz-font);',
735
- ' transition: border-color 0.2s ease, background 0.2s ease;',
736
763
  ' resize: none;',
737
- ' max-height: 100px;',
764
+ ' max-height: 80px;',
765
+ ' min-height: 22px;',
738
766
  ' overflow-y: auto;',
739
- ' background: #F9FAFB;',
740
- ' color: #111827;',
767
+ ' background: transparent;',
768
+ ' color: #171717;',
741
769
  '}',
742
770
  '.wakz-input:focus {',
743
- ' border-color: var(--wakz-primary);',
744
- ' background: #ffffff;',
771
+ ' outline: none;',
745
772
  '}',
746
773
  '.wakz-input::placeholder {',
747
- ' color: #9ca3af;',
774
+ ' color: #A3A3A3;',
748
775
  '}',
749
776
 
750
- /* ── Send Button (floating circle, separate from textarea) ── */
777
+ /* ── Send Button (36px floating circle, separate from input) ── */
751
778
  '.wakz-send {',
752
- ' width: 44px;',
753
- ' height: 44px;',
779
+ ' width: 36px;',
780
+ ' height: 36px;',
781
+ ' min-width: 36px;',
754
782
  ' border-radius: 50%;',
755
783
  ' border: none;',
756
784
  ' color: #ffffff;',
@@ -759,87 +787,88 @@
759
787
  ' align-items: center;',
760
788
  ' justify-content: center;',
761
789
  ' flex-shrink: 0;',
762
- ' transition: opacity 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;',
790
+ ' transition: transform var(--wakz-transition), opacity var(--wakz-transition), box-shadow var(--wakz-transition);',
763
791
  ' outline: none;',
764
792
  ' background: var(--wakz-primary);',
765
793
  ' box-shadow: 0 2px 8px rgba(0,0,0,0.12);',
766
794
  '}',
767
795
  '.wakz-send:hover:not(:disabled) {',
768
796
  ' transform: scale(1.05);',
769
- ' box-shadow: 0 4px 16px rgba(0,0,0,0.18);',
797
+ ' box-shadow: 0 4px 14px rgba(0,0,0,0.18);',
770
798
  '}',
771
799
  '.wakz-send:disabled {',
772
- ' opacity: 0.35;',
800
+ ' opacity: 0.3;',
773
801
  ' cursor: not-allowed;',
774
802
  '}',
775
803
  '.wakz-send:not(:disabled):active {',
776
804
  ' transform: scale(0.92);',
777
805
  '}',
778
806
  '.wakz-send svg {',
779
- ' width: 18px;',
780
- ' height: 18px;',
807
+ ' width: 16px;',
808
+ ' height: 16px;',
781
809
  ' pointer-events: none;',
782
810
  '}',
783
811
 
812
+ /* ══════════════════════════════════════════════════
813
+ WELCOME MESSAGE — same as bot (no bubble)
814
+ ══════════════════════════════════════════════════ */
815
+ '.wakz-welcome-wrap {',
816
+ ' display: flex;',
817
+ ' align-items: flex-start;',
818
+ ' gap: 8px;',
819
+ ' max-width: 80%;',
820
+ ' align-self: flex-start;',
821
+ ' animation: wakz-msg-in 0.35s cubic-bezier(0.4, 0, 0.2, 1) forwards;',
822
+ '}',
823
+
784
824
  /* ══════════════════════════════════════════════════
785
825
  RTL SUPPORT
786
826
  ══════════════════════════════════════════════════ */
787
827
  '.wakz-rtl { direction: rtl; }',
788
- '.wakz-rtl .wakz-bubble-user {',
789
- ' border-radius: 18px 18px 18px 6px;',
790
- '}',
791
- '.wakz-rtl .wakz-bubble-user .wakz-ts {',
792
- ' text-align: left;',
828
+ '.wakz-rtl .wakz-bubble.user {',
829
+ ' border-bottom-right-radius: 14px;',
830
+ ' border-bottom-left-radius: 4px;',
793
831
  '}',
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;',
832
+ '.wakz-rtl .wakz-bubble.error-bubble {',
833
+ ' border-bottom-left-radius: 14px;',
834
+ ' border-bottom-right-radius: 4px;',
798
835
  '}',
799
- '.wakz-rtl .wakz-msg-bot-content.wakz-human {',
800
- ' border-left: none;',
801
- ' border-right-color: #16A34A;',
802
- '}',
803
- '.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;',
807
- '}',
808
- '.wakz-rtl .wakz-bubble-error {',
809
- ' border-radius: 18px 18px 18px 6px;',
836
+ '.wakz-rtl .wakz-ts {',
837
+ ' text-align: left;',
810
838
  '}',
811
839
 
812
840
  /* ══════════════════════════════════════════════════
813
- MOBILE RESPONSIVE (< 480px)
841
+ MOBILE RESPONSIVE centered modal, NOT fullscreen
814
842
  ══════════════════════════════════════════════════ */
815
843
  '@media (max-width: 480px) {',
816
- ' .wakz-overlay {',
817
- ' display: none;',
818
- ' }',
819
844
  ' .wakz-window {',
820
- ' top: auto !important;',
821
- ' left: 0 !important;',
822
- ' right: 0 !important;',
823
- ' bottom: 0 !important;',
824
- ' width: 100vw !important;',
825
- ' height: 100vh !important;',
826
- ' 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;',
845
+ ' width: calc(100vw - 32px) !important;',
846
+ ' height: min(500px, 75vh) !important;',
831
847
  ' }',
832
848
  ' .wakz-window.wakz-visible {',
833
- ' transform: translateY(0) !important;',
849
+ ' transform: translate(-50%, -50%) scale(1);',
850
+ ' }',
851
+ ' .wakz-fab {',
852
+ ' width: 44px;',
853
+ ' height: 44px;',
834
854
  ' }',
835
855
  ' .wakz-fab-pos-br { bottom: 16px; right: 16px; }',
836
856
  ' .wakz-fab-pos-bl { bottom: 16px; left: 16px; }',
857
+ ' .wakz-input-area {',
858
+ ' left: 10px;',
859
+ ' right: 10px;',
860
+ ' bottom: 10px;',
861
+ ' padding: 6px 8px 6px 12px;',
862
+ ' }',
863
+ ' .wakz-msgs {',
864
+ ' padding: 14px 12px 84px;',
865
+ ' }',
837
866
  '}'
838
867
  ].join('\n');
839
868
  };
840
869
 
841
870
  /* ════════════════════════════════════════════════════════════════
842
- DOM CREATION
871
+ DOM CREATION — v3.0.0 new structure
843
872
  ════════════════════════════════════════════════════════════════ */
844
873
 
845
874
  WAKZWidget.prototype._createDOM = function () {
@@ -847,8 +876,6 @@
847
876
  var isRtl = self.config.language === 'ar';
848
877
  var posClass = self.config.position === 'bottom-left' ? 'bl' : 'br';
849
878
  var str = _strings(self.config.language);
850
- var iconColor = self._scriptIconColor || self.config.iconColor;
851
- var iconShape = self._scriptIconShape || self.config.iconShape;
852
879
 
853
880
  /* ── Host + Shadow DOM ── */
854
881
  self._host = _el('div');
@@ -860,67 +887,63 @@
860
887
  self._shadow.appendChild(self._styleEl);
861
888
  self._shadow.appendChild(self._root);
862
889
 
863
- /* ══════════════ FLOATING ACTION BUTTON ══════════════ */
864
- var fabClasses = 'wakz-fab wakz-fab-pos-' + posClass;
865
- if (iconShape === 'rounded') fabClasses += ' wakz-fab-shape-rounded';
866
-
890
+ /* ══════════════ FLOATING ACTION BUTTON (FAB) ══════════════ */
867
891
  self._toggleBtn = _el('button', {
868
- className: fabClasses,
869
- 'aria-label': str.openChat,
870
- style: { '--wakz-icon-color': iconColor }
892
+ className: 'wakz-fab wakz-fab-pos-' + posClass,
893
+ 'aria-label': str.openChat
871
894
  });
872
- self._toggleBtn.style.background = iconColor;
873
- self._toggleBtn.innerHTML = _ICONS.chatIcon;
895
+ self._toggleBtn.innerHTML = _ICONS.chatBubble;
874
896
 
875
- /* Status dot on FAB */
897
+ /* Status dot on FAB (10px) */
876
898
  self._statusDot = _el('span', { className: 'wakz-fab-dot ' + (self.config.online ? 'online' : 'offline') });
877
899
  self._statusDot.style.display = self.config.showStatus ? '' : 'none';
878
900
  self._toggleBtn.appendChild(self._statusDot);
879
901
  self._root.appendChild(self._toggleBtn);
880
902
 
881
- /* ══════════════ OVERLAY BACKDROP ══════════════ */
882
- self._overlayEl = _el('div', { className: 'wakz-overlay' });
883
- self._root.appendChild(self._overlayEl);
903
+ /* ══════════════ OVERLAY / BACKDROP ══════════════ */
904
+ self._overlay = _el('div', { className: 'wakz-overlay' });
905
+ self._root.appendChild(self._overlay);
884
906
 
885
907
  /* ══════════════ CHAT WINDOW (Centered Modal) ══════════════ */
886
908
  self._chatWindow = _el('div', { className: 'wakz-window' });
887
909
 
888
- /* ── Header ── */
910
+ /* ── Header (glass effect, dark text) ── */
889
911
  var headerEl = _el('div', { className: 'wakz-hdr' });
890
912
 
891
- /* Right side: avatar + name (in RTL, right side = start) */
892
- var headerRight = _el('div', { className: 'wakz-hdr-right' });
913
+ /* Left side: avatar + name + status */
914
+ var headerLeft = _el('div', { className: 'wakz-hdr-left' });
893
915
  var avatarLetter = (self.config.botName || 'W')[0].toUpperCase();
894
- headerRight.appendChild(_el('div', { className: 'wakz-avatar' }, [avatarLetter]));
916
+ self._headerAvatarEl = _el('div', { className: 'wakz-avatar' }, [avatarLetter]);
917
+ headerLeft.appendChild(self._headerAvatarEl);
895
918
 
896
919
  var headerInfo = _el('div', { className: 'wakz-hdr-info' });
897
- 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' });
920
+ self._headerNameEl = _el('span', { className: 'wakz-hdr-name' }, [self.config.botName]);
921
+ headerInfo.appendChild(self._headerNameEl);
903
922
 
904
923
  var statusLine = _el('span', { className: 'wakz-hdr-status' });
905
924
  self._headerStatusDot = _el('span', { className: 'wakz-hdr-status-dot ' + (self.config.online ? 'online' : 'offline') });
906
925
  if (self.config.showStatus) statusLine.appendChild(self._headerStatusDot);
907
926
  self._headerStatusText = document.createTextNode(self.config.online ? str.online : str.offline);
908
927
  statusLine.appendChild(self._headerStatusText);
909
- headerLeft.appendChild(statusLine);
928
+ headerInfo.appendChild(statusLine);
910
929
 
930
+ headerLeft.appendChild(headerInfo);
931
+ headerEl.appendChild(headerLeft);
932
+
933
+ /* Right side: status dot + text + close button */
934
+ var headerRight = _el('div', { className: 'wakz-hdr-right' });
911
935
  var closeBtn = _el('button', { className: 'wakz-close', 'aria-label': str.closeChat });
912
936
  closeBtn.innerHTML = _ICONS.close;
913
- headerLeft.appendChild(closeBtn);
914
- headerEl.appendChild(headerLeft);
937
+ headerRight.appendChild(closeBtn);
938
+ headerEl.appendChild(headerRight);
915
939
 
916
940
  self._chatWindow.appendChild(headerEl);
917
941
 
918
- /* ── Messages Container ── */
942
+ /* ── Messages Container (with floating input area inside) ── */
919
943
  self._messagesContainer = _el('div', { className: 'wakz-msgs' });
920
- self._chatWindow.appendChild(self._messagesContainer);
921
944
 
922
- /* ── Input Area ── */
923
- var inputWrap = _el('div', { className: 'wakz-input-wrap' });
945
+ /* ── Floating Input Area (inside messages container) ── */
946
+ self._inputArea = _el('div', { className: 'wakz-input-area' });
924
947
 
925
948
  self._inputEl = _el('textarea', {
926
949
  className: 'wakz-input',
@@ -928,15 +951,17 @@
928
951
  rows: 1,
929
952
  dir: isRtl ? 'rtl' : 'ltr'
930
953
  });
931
- inputWrap.appendChild(self._inputEl);
954
+ self._inputArea.appendChild(self._inputEl);
932
955
 
933
956
  self._sendBtn = _el('button', {
934
957
  className: 'wakz-send',
935
958
  'aria-label': str.sendMessage
936
959
  });
937
960
  self._sendBtn.innerHTML = _ICONS.send;
938
- inputWrap.appendChild(self._sendBtn);
939
- self._chatWindow.appendChild(inputWrap);
961
+ self._inputArea.appendChild(self._sendBtn);
962
+
963
+ self._messagesContainer.appendChild(self._inputArea);
964
+ self._chatWindow.appendChild(self._messagesContainer);
940
965
 
941
966
  self._root.appendChild(self._chatWindow);
942
967
  };
@@ -953,13 +978,13 @@
953
978
  self.toggleChat(!self.isOpen);
954
979
  });
955
980
 
956
- /* Close button click */
957
- self._chatWindow.querySelector('.wakz-close').addEventListener('click', function () {
981
+ /* Overlay click to close */
982
+ self._overlay.addEventListener('click', function () {
958
983
  self.toggleChat(false);
959
984
  });
960
985
 
961
- /* Overlay backdrop click to close */
962
- self._overlayEl.addEventListener('click', function () {
986
+ /* Close button click */
987
+ self._chatWindow.querySelector('.wakz-close').addEventListener('click', function () {
963
988
  self.toggleChat(false);
964
989
  });
965
990
 
@@ -976,10 +1001,10 @@
976
1001
  }
977
1002
  });
978
1003
 
979
- /* Auto-resize textarea */
1004
+ /* Auto-resize textarea (max 80px) */
980
1005
  self._inputEl.addEventListener('input', function () {
981
1006
  self._inputEl.style.height = 'auto';
982
- self._inputEl.style.height = Math.min(self._inputEl.scrollHeight, 100) + 'px';
1007
+ self._inputEl.style.height = Math.min(self._inputEl.scrollHeight, 80) + 'px';
983
1008
  });
984
1009
 
985
1010
  /* Escape to close */
@@ -994,7 +1019,6 @@
994
1019
 
995
1020
  WAKZWidget.prototype._playFabEntrance = function () {
996
1021
  var self = this;
997
- /* Start invisible, then animate after a brief delay */
998
1022
  self._toggleBtn.style.opacity = '0';
999
1023
  setTimeout(function () {
1000
1024
  self._toggleBtn.style.opacity = '';
@@ -1011,8 +1035,7 @@
1011
1035
  self.isOpen = open;
1012
1036
  if (open) {
1013
1037
  self._chatWindow.classList.add('wakz-visible');
1014
- self._overlayEl.classList.add('wakz-visible');
1015
- /* Hide FAB when chat is open */
1038
+ self._overlay.classList.add('wakz-visible');
1016
1039
  self._toggleBtn.classList.add('wakz-fab-hidden');
1017
1040
  /* Fetch history on first open (after config is loaded) */
1018
1041
  if (self._configLoaded && !self._hasFetchedHistory) {
@@ -1027,8 +1050,7 @@
1027
1050
  setTimeout(function () { self._inputEl.focus(); }, 320);
1028
1051
  } else {
1029
1052
  self._chatWindow.classList.remove('wakz-visible');
1030
- self._overlayEl.classList.remove('wakz-visible');
1031
- /* Show FAB when chat is closed */
1053
+ self._overlay.classList.remove('wakz-visible');
1032
1054
  self._toggleBtn.classList.remove('wakz-fab-hidden');
1033
1055
  /* Stop polling when chat is closed */
1034
1056
  self._stopPolling();
@@ -1056,9 +1078,6 @@
1056
1078
  .then(function (data) {
1057
1079
  if (data && data.success && data.config) {
1058
1080
  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
1081
  self._configLoaded = true;
1063
1082
  self._applyConfig();
1064
1083
  } else {
@@ -1066,7 +1085,6 @@
1066
1085
  }
1067
1086
  })
1068
1087
  .catch(function (err) {
1069
- /* AbortError means timeout */
1070
1088
  self._handleConfigError();
1071
1089
  });
1072
1090
  };
@@ -1098,26 +1116,21 @@
1098
1116
  var str = _strings(cfg.language);
1099
1117
  var isRtl = cfg.language === 'ar';
1100
1118
  var posClass = cfg.position === 'bottom-left' ? 'bl' : 'br';
1101
- var iconColor = cfg.iconColor || '#171717';
1102
- var iconShape = cfg.iconShape || 'circle';
1103
1119
 
1104
1120
  /* ── Update CSS custom properties ── */
1105
1121
  var hostStyle = self._shadow.host.style;
1106
1122
  hostStyle.setProperty('--wakz-primary', cfg.primaryColor);
1107
- hostStyle.setProperty('--wakz-icon-color', iconColor);
1123
+ hostStyle.setProperty('--wakz-btn', cfg.btnColor);
1108
1124
  hostStyle.setProperty('--wakz-chat-bg', cfg.chatBg);
1109
1125
  hostStyle.setProperty('--wakz-widget-bg', cfg.widgetBg);
1126
+ hostStyle.setProperty('--wakz-icon-radius', cfg.iconRadius || '50%');
1110
1127
 
1111
1128
  /* ── RTL ── */
1112
1129
  if (isRtl) self._root.classList.add('wakz-rtl');
1113
1130
  else self._root.classList.remove('wakz-rtl');
1114
1131
 
1115
1132
  /* ── 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;
1133
+ self._toggleBtn.className = 'wakz-fab wakz-fab-pos-' + posClass + ' wakz-fab-enter' + (self.isOpen ? ' wakz-fab-hidden' : '');
1121
1134
  self._toggleBtn.setAttribute('aria-label', str.openChat);
1122
1135
 
1123
1136
  /* ── FAB Status Dot ── */
@@ -1127,11 +1140,12 @@
1127
1140
  /* ── Window ── */
1128
1141
  self._chatWindow.className = 'wakz-window' + (self.isOpen ? ' wakz-visible' : '');
1129
1142
 
1143
+ /* ── Overlay ── */
1144
+ self._overlay.className = 'wakz-overlay' + (self.isOpen ? ' wakz-visible' : '');
1145
+
1130
1146
  /* ── Bot Name & Avatar ── */
1131
- var nameEl = self._chatWindow.querySelector('.wakz-hdr-name');
1132
- if (nameEl) nameEl.textContent = cfg.botName;
1133
- var avatarEl = self._chatWindow.querySelector('.wakz-avatar');
1134
- if (avatarEl) avatarEl.textContent = (cfg.botName || 'W')[0].toUpperCase();
1147
+ if (self._headerNameEl) self._headerNameEl.textContent = cfg.botName;
1148
+ if (self._headerAvatarEl) self._headerAvatarEl.textContent = (cfg.botName || 'W')[0].toUpperCase();
1135
1149
 
1136
1150
  /* ── Header Status ── */
1137
1151
  if (self._headerStatusDot) {
@@ -1178,7 +1192,6 @@
1178
1192
  .then(function (res) { return res.json(); })
1179
1193
  .then(function (data) {
1180
1194
  if (data && data.success && data.messages && data.messages.length > 0) {
1181
- /* Clear any existing messages (e.g., welcome) and render history */
1182
1195
  self._clearMessages();
1183
1196
  self._knownMessageIds = {};
1184
1197
  var msgs = data.messages;
@@ -1189,7 +1202,6 @@
1189
1202
  var role = m.role === 'user' ? 'user' : 'bot';
1190
1203
  self._appendMessage(role, m.content, false, m.createdAt, m.role === 'human_agent');
1191
1204
  }
1192
- /* Set last poll time to the most recent message */
1193
1205
  var lastMsg = msgs[msgs.length - 1];
1194
1206
  if (lastMsg && lastMsg.createdAt) {
1195
1207
  self._lastPollTime = lastMsg.createdAt;
@@ -1207,7 +1219,7 @@
1207
1219
 
1208
1220
  WAKZWidget.prototype._startPolling = function () {
1209
1221
  var self = this;
1210
- self._stopPolling(); /* Clear any existing timer */
1222
+ self._stopPolling();
1211
1223
  if (!self._lastPollTime) {
1212
1224
  self._lastPollTime = new Date().toISOString();
1213
1225
  }
@@ -1227,6 +1239,8 @@
1227
1239
  WAKZWidget.prototype._pollForNewMessages = function () {
1228
1240
  var self = this;
1229
1241
  if (!self.server || !self.apiKey || !self._lastPollTime) return;
1242
+ /* Skip polling while waiting for our own reply */
1243
+ if (self._pendingReply) return;
1230
1244
 
1231
1245
  var url = self.server + '/api/v1/embed/chat?api_key=' +
1232
1246
  encodeURIComponent(self.apiKey) +
@@ -1249,7 +1263,6 @@
1249
1263
  var isHumanAgent = m.role === 'human_agent';
1250
1264
  self._appendMessage(role, m.content, false, m.createdAt, isHumanAgent);
1251
1265
  }
1252
- /* Update last poll time */
1253
1266
  var lastMsg = msgs[msgs.length - 1];
1254
1267
  if (lastMsg && lastMsg.createdAt) {
1255
1268
  self._lastPollTime = lastMsg.createdAt;
@@ -1267,13 +1280,19 @@
1267
1280
 
1268
1281
  WAKZWidget.prototype._clearMessages = function () {
1269
1282
  var self = this;
1270
- self._messagesContainer.innerHTML = '';
1283
+ /* Remove all children except the input area */
1284
+ var children = Array.prototype.slice.call(self._messagesContainer.children);
1285
+ for (var i = 0; i < children.length; i++) {
1286
+ if (children[i] !== self._inputArea) {
1287
+ self._messagesContainer.removeChild(children[i]);
1288
+ }
1289
+ }
1271
1290
  self.messages = [];
1272
1291
  self._knownMessageIds = {};
1273
1292
  };
1274
1293
 
1275
1294
  /* ════════════════════════════════════════════════════════════════
1276
- APPEND WELCOME MESSAGE (bot style — no bubble, flat on bg)
1295
+ APPEND WELCOME MESSAGE — no bubble style, matching bot messages
1277
1296
  ════════════════════════════════════════════════════════════════ */
1278
1297
 
1279
1298
  WAKZWidget.prototype._appendWelcomeMessage = function (text) {
@@ -1282,155 +1301,174 @@
1282
1301
 
1283
1302
  var wrap = _el('div', { className: 'wakz-welcome-wrap' });
1284
1303
 
1285
- /* Bot avatar */
1286
- var avatar = _el('div', { className: 'wakz-msg-bot-avatar' },
1304
+ /* Bot avatar (22px) */
1305
+ var avatar = _el('div', { className: 'wakz-bot-avatar' },
1287
1306
  [(self.config.botName || 'W')[0].toUpperCase()]);
1288
1307
  wrap.appendChild(avatar);
1289
1308
 
1290
- /* Flat content with accent border */
1291
- var content = _el('div', { className: 'wakz-msg-bot-content' }, [text]);
1309
+ /* Bot text content no bubble */
1310
+ var content = _el('div', { className: 'wakz-bot-content' });
1311
+ content.appendChild(_el('div', { className: 'wakz-bot-text' }, [text]));
1292
1312
  wrap.appendChild(content);
1293
1313
 
1294
- self._messagesContainer.appendChild(wrap);
1314
+ /* Insert before input area */
1315
+ self._messagesContainer.insertBefore(wrap, self._inputArea);
1295
1316
  self.messages.push({ sender: 'bot', text: text });
1296
1317
  self._scrollToBottom();
1297
1318
  };
1298
1319
 
1299
1320
  /* ════════════════════════════════════════════════════════════════
1300
- APPEND MESSAGE TO CHAT
1321
+ APPEND MESSAGE TO CHAT — v3 rendering
1301
1322
  ════════════════════════════════════════════════════════════════ */
1302
1323
 
1303
1324
  WAKZWidget.prototype._appendMessage = function (sender, text, isError, timestamp, isHumanAgent) {
1304
1325
  var self = this;
1305
1326
 
1327
+ /* ── Format timestamp ── */
1328
+ var tsText = '';
1329
+ if (timestamp) {
1330
+ try {
1331
+ var d = new Date(timestamp);
1332
+ if (!isNaN(d.getTime())) {
1333
+ tsText = d.getHours().toString().padStart(2, '0') + ':' +
1334
+ d.getMinutes().toString().padStart(2, '0');
1335
+ }
1336
+ } catch (e) { /* ignore */ }
1337
+ }
1338
+ if (!tsText) {
1339
+ var now = new Date();
1340
+ tsText = now.getHours().toString().padStart(2, '0') + ':' +
1341
+ now.getMinutes().toString().padStart(2, '0');
1342
+ }
1343
+
1306
1344
  if (sender === 'user') {
1307
- /* ── User message: rounded bubble ── */
1345
+ /* ── USER MESSAGE: colored bubble with tail ── */
1308
1346
  var row = _el('div', { className: 'wakz-msg-row user' });
1309
- var bubble = _el('div', { className: 'wakz-bubble-user' });
1310
- bubble.appendChild(document.createTextNode(text));
1311
1347
 
1312
- /* Timestamp */
1313
- var tsText = self._formatTime(timestamp);
1348
+ var bubbleClass = 'wakz-bubble user';
1349
+ if (isError) bubbleClass += ' error-bubble';
1350
+ var bubble = _el('div', { className: bubbleClass });
1351
+ bubble.appendChild(document.createTextNode(text));
1314
1352
  bubble.appendChild(_el('span', { className: 'wakz-ts' }, [tsText]));
1315
1353
 
1354
+ /* Retry button for error user messages */
1355
+ if (isError) {
1356
+ var str = _strings(self.config.language);
1357
+ var retryBtn = _el('button', { className: 'wakz-retry' }, [
1358
+ _ICONS.error + ' ' + str.retry
1359
+ ]);
1360
+ (function (rowCapture) {
1361
+ retryBtn.addEventListener('click', function () {
1362
+ if (rowCapture.parentNode) rowCapture.parentNode.removeChild(rowCapture);
1363
+ for (var i = self.messages.length - 1; i >= 0; i--) {
1364
+ if (self.messages[i].text === text && self.messages[i].isError) {
1365
+ self.messages.splice(i, 1);
1366
+ break;
1367
+ }
1368
+ }
1369
+ var lastUserMsg = null;
1370
+ for (var j = self.messages.length - 1; j >= 0; j--) {
1371
+ if (self.messages[j].sender === 'user') {
1372
+ lastUserMsg = self.messages[j].text;
1373
+ break;
1374
+ }
1375
+ }
1376
+ if (lastUserMsg) self._sendToAPI(lastUserMsg);
1377
+ });
1378
+ })(row);
1379
+ bubble.appendChild(retryBtn);
1380
+ }
1381
+
1316
1382
  row.appendChild(bubble);
1317
- self._messagesContainer.appendChild(row);
1383
+ self._messagesContainer.insertBefore(row, self._inputArea);
1318
1384
  self.messages.push({ sender: sender, text: text, isError: !!isError });
1319
- self._scrollToBottom();
1320
- return;
1321
- }
1322
-
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));
1328
-
1329
- /* Retry button for errors */
1330
- var str = _strings(self.config.language);
1331
- var retryBtn = _el('button', { className: 'wakz-retry' }, [
1332
- _ICONS.error + ' ' + str.retry
1333
- ]);
1334
- (function (rowCapture) {
1335
- retryBtn.addEventListener('click', function () {
1336
- /* Remove error row */
1337
- if (rowCapture.parentNode) rowCapture.parentNode.removeChild(rowCapture);
1338
- /* Find and remove from messages array */
1339
- for (var i = self.messages.length - 1; i >= 0; i--) {
1340
- if (self.messages[i].text === text && self.messages[i].isError) {
1341
- self.messages.splice(i, 1);
1342
- break;
1385
+ } else {
1386
+ /* ── BOT / HUMAN AGENT MESSAGE: no bubble, text on background ── */
1387
+ var botRow = _el('div', { className: 'wakz-msg-row bot' });
1388
+
1389
+ /* Small bot avatar (22px) */
1390
+ var botAvatar = _el('div', { className: 'wakz-bot-avatar' },
1391
+ [(self.config.botName || 'W')[0].toUpperCase()]);
1392
+ botRow.appendChild(botAvatar);
1393
+
1394
+ /* Content wrapper */
1395
+ var botContent = _el('div', { className: 'wakz-bot-content' });
1396
+
1397
+ if (isError) {
1398
+ /* Error: subtle red area */
1399
+ var errBubble = _el('div', { className: 'wakz-bubble error-bubble' });
1400
+ errBubble.appendChild(document.createTextNode(text));
1401
+
1402
+ var errStr = _strings(self.config.language);
1403
+ var errRetryBtn = _el('button', { className: 'wakz-retry' }, [
1404
+ _ICONS.error + ' ' + errStr.retry
1405
+ ]);
1406
+ (function (rowCapture, errText) {
1407
+ errRetryBtn.addEventListener('click', function () {
1408
+ if (rowCapture.parentNode) rowCapture.parentNode.removeChild(rowCapture);
1409
+ for (var i = self.messages.length - 1; i >= 0; i--) {
1410
+ if (self.messages[i].text === errText && self.messages[i].isError) {
1411
+ self.messages.splice(i, 1);
1412
+ break;
1413
+ }
1343
1414
  }
1344
- }
1345
- /* Resend last user message */
1346
- var lastUserMsg = null;
1347
- for (var j = self.messages.length - 1; j >= 0; j--) {
1348
- if (self.messages[j].sender === 'user') {
1349
- lastUserMsg = self.messages[j].text;
1350
- break;
1415
+ var lastUserMsg = null;
1416
+ for (var j = self.messages.length - 1; j >= 0; j--) {
1417
+ if (self.messages[j].sender === 'user') {
1418
+ lastUserMsg = self.messages[j].text;
1419
+ break;
1420
+ }
1351
1421
  }
1352
- }
1353
- if (lastUserMsg) self._sendToAPI(lastUserMsg);
1354
- });
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;
1363
- }
1422
+ if (lastUserMsg) self._sendToAPI(lastUserMsg);
1423
+ });
1424
+ })(botRow, text);
1425
+ errBubble.appendChild(errRetryBtn);
1426
+ botContent.appendChild(errBubble);
1427
+ } else {
1428
+ /* Normal bot/human message: no bubble */
1429
+ botContent.appendChild(_el('div', { className: 'wakz-bot-text' }, [text]));
1364
1430
 
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' });
1431
+ /* Timestamp */
1432
+ botContent.appendChild(_el('span', { className: 'wakz-bot-ts' }, [tsText]));
1368
1433
 
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);
1434
+ /* Human agent badge */
1435
+ if (isHumanAgent) {
1436
+ var iStr = _strings(self.config.language);
1437
+ var badge = _el('span', { className: 'wakz-human-badge' }, [iStr.humanBadge || '👤 فريق الدعم']);
1438
+ botContent.appendChild(badge);
1439
+ }
1440
+ }
1373
1441
 
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);
1442
+ botRow.appendChild(botContent);
1443
+ self._messagesContainer.insertBefore(botRow, self._inputArea);
1444
+ self.messages.push({ sender: sender, text: text, isError: !!isError });
1389
1445
  }
1390
1446
 
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 });
1396
1447
  self._scrollToBottom();
1397
1448
  };
1398
1449
 
1399
1450
  /* ════════════════════════════════════════════════════════════════
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)
1451
+ TYPING INDICATOR — no bubble, dots on background with avatar
1420
1452
  ════════════════════════════════════════════════════════════════ */
1421
1453
 
1422
1454
  WAKZWidget.prototype._showTyping = function () {
1423
1455
  var self = this;
1424
1456
  if (self._typingEl) return;
1425
1457
 
1426
- self._typingEl = _el('div', { className: 'wakz-msg-row bot' });
1427
- self._typingEl.innerHTML =
1428
- '<div class="wakz-typing">' +
1429
- '<span class="wakz-typing-dot"></span>' +
1430
- '<span class="wakz-typing-dot"></span>' +
1431
- '<span class="wakz-typing-dot"></span>' +
1432
- '</div>';
1433
- self._messagesContainer.appendChild(self._typingEl);
1458
+ self._typingEl = _el('div', { className: 'wakz-typing-row' });
1459
+
1460
+ /* Small avatar for consistency */
1461
+ self._typingEl.appendChild(_el('div', { className: 'wakz-bot-avatar' },
1462
+ [(self.config.botName || 'W')[0].toUpperCase()]));
1463
+
1464
+ /* Bouncing dots — no bubble wrapper */
1465
+ var typingDots = _el('div', { className: 'wakz-typing' });
1466
+ typingDots.appendChild(_el('span', { className: 'wakz-typing-dot' }));
1467
+ typingDots.appendChild(_el('span', { className: 'wakz-typing-dot' }));
1468
+ typingDots.appendChild(_el('span', { className: 'wakz-typing-dot' }));
1469
+ self._typingEl.appendChild(typingDots);
1470
+
1471
+ self._messagesContainer.insertBefore(self._typingEl, self._inputArea);
1434
1472
  self._scrollToBottom();
1435
1473
  };
1436
1474
 
@@ -1491,6 +1529,10 @@
1491
1529
  self._sendBtn.disabled = true;
1492
1530
  self._showTyping();
1493
1531
 
1532
+ /* Freeze poll time BEFORE sending */
1533
+ self._lastPollTime = new Date().toISOString();
1534
+ self._pendingReply = true;
1535
+
1494
1536
  var payload = {
1495
1537
  api_key: self.apiKey,
1496
1538
  visitor_id: self.visitorId,
@@ -1499,7 +1541,10 @@
1499
1541
  domain: window.location.hostname || '',
1500
1542
  page_url: window.location.href || '',
1501
1543
  user_agent: navigator.userAgent || '',
1502
- language: navigator.language || ''
1544
+ language: navigator.language || '',
1545
+ device_model: self._deviceInfo.device_model || '',
1546
+ device_platform: self._deviceInfo.device_platform || '',
1547
+ device_type: self._deviceInfo.device_type || 'desktop'
1503
1548
  }
1504
1549
  };
1505
1550
 
@@ -1514,19 +1559,21 @@
1514
1559
  .then(function (res) { return res.json(); })
1515
1560
  .then(function (data) {
1516
1561
  self._hideTyping();
1562
+ self._pendingReply = false;
1517
1563
  if (data && data.success && data.reply) {
1518
1564
  self._appendMessage('bot', data.reply);
1519
1565
  } else {
1520
1566
  self._appendMessage('bot', _strings(self.config.language).errorMsg, true);
1521
1567
  }
1522
- /* Update poll time to prevent re-fetching messages we already displayed */
1523
- self._lastPollTime = new Date().toISOString();
1568
+ self._lastPollTime = (data && data.timestamp)
1569
+ ? new Date(new Date(data.timestamp).getTime() + 1000).toISOString()
1570
+ : new Date(Date.now() + 1000).toISOString();
1524
1571
  })
1525
1572
  .catch(function () {
1526
1573
  self._hideTyping();
1574
+ self._pendingReply = false;
1527
1575
  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();
1576
+ self._lastPollTime = new Date(Date.now() + 1000).toISOString();
1530
1577
  })
1531
1578
  .finally(function () {
1532
1579
  self.isLoading = false;
@@ -1540,7 +1587,7 @@
1540
1587
  ════════════════════════════════════════════════════════════════ */
1541
1588
 
1542
1589
  function boot() {
1543
- try { new WAKZWidget(); } catch (e) { console.error('[WAKZ Widget] Init error:', e); }
1590
+ try { new WAKZWidget(); } catch (e) { console.error('[WAKZ Widget v3.0.0] Init error:', e); }
1544
1591
  }
1545
1592
 
1546
1593
  if (document.readyState === 'loading') {