wakz-chat-widget 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +419 -245
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  /**
2
- * WAKZ Chat Widget v2.0.0
2
+ * WAKZ Chat Widget v3.0.0
3
3
  * ─────────────────────────────────────────────────────────────────
4
4
  * A production-grade, self-contained chat widget using Shadow DOM.
5
- * Competes with Intercom & Crisp in quality.
5
+ * Premium glassmorphism design matching the WAKZ site design system.
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"
8
9
  *
9
10
  * ZERO external dependencies — pure vanilla JavaScript.
10
11
  */
@@ -50,7 +51,9 @@
50
51
  if (!target && scripts.length > 0) target = scripts[scripts.length - 1];
51
52
  return {
52
53
  apiKey: (target && target.getAttribute('data-api-key')) || '',
53
- server: (target && target.getAttribute('data-server')) || ''
54
+ server: (target && target.getAttribute('data-server')) || '',
55
+ iconColor: (target && target.getAttribute('data-icon-color')) || '',
56
+ iconShape: (target && target.getAttribute('data-icon-shape')) || ''
54
57
  };
55
58
  }
56
59
 
@@ -92,12 +95,21 @@
92
95
  ════════════════════════════════════════════════════════════════ */
93
96
 
94
97
  var _ICONS = {
95
- chatBubble:
96
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>',
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 */
97
107
  send:
98
108
  '<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 */
99
110
  close:
100
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
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 */
101
113
  error:
102
114
  '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>'
103
115
  };
@@ -157,11 +169,12 @@
157
169
  botName: 'WAKZ',
158
170
  welcomeMessage: '',
159
171
  primaryColor: '#171717',
160
- chatBg: '#f8f9fa',
161
- btnColor: '#171717',
172
+ iconColor: '#171717',
173
+ iconShape: 'circle',
174
+ chatBg: '#FAFAFA',
162
175
  widgetBg: '#ffffff',
163
176
  position: 'bottom-right',
164
- language: 'en',
177
+ language: 'ar',
165
178
  showStatus: true,
166
179
  online: true
167
180
  };
@@ -182,6 +195,8 @@
182
195
  self.apiKey = attrs.apiKey;
183
196
  self.server = attrs.server;
184
197
  self.visitorId = _getVisitorId();
198
+ self._scriptIconColor = attrs.iconColor;
199
+ self._scriptIconShape = attrs.iconShape;
185
200
 
186
201
  /* ── Runtime state ── */
187
202
  self.config = Object.assign({}, _DEFAULTS);
@@ -195,11 +210,13 @@
195
210
  self._pollTimer = null;
196
211
  self._lastPollTime = null;
197
212
  self._POLL_INTERVAL = 4000; /* Poll every 4 seconds */
213
+ self._knownMessageIds = {}; /* Deduplication set for message IDs */
198
214
 
199
215
  /* ── DOM refs (populated after mount) ── */
200
216
  self._host = null;
201
217
  self._shadow = null;
202
218
  self._root = null;
219
+ self._overlayEl = null;
203
220
  self._chatWindow = null;
204
221
  self._messagesContainer = null;
205
222
  self._inputEl = null;
@@ -218,7 +235,7 @@
218
235
  }
219
236
 
220
237
  /* ════════════════════════════════════════════════════════════════
221
- CSS — Complete styling injected into Shadow DOM
238
+ CSS — Complete premium styling injected into Shadow DOM
222
239
  ════════════════════════════════════════════════════════════════ */
223
240
 
224
241
  WAKZWidget.prototype._injectCSS = function () {
@@ -229,18 +246,14 @@
229
246
  /* ── CSS Custom Properties (theming) ── */
230
247
  ':host {',
231
248
  ' --wakz-primary: #171717;',
232
- ' --wakz-btn: #171717;',
233
- ' --wakz-chat-bg: #f8f9fa;',
249
+ ' --wakz-icon-color: #171717;',
250
+ ' --wakz-chat-bg: #FAFAFA;',
234
251
  ' --wakz-widget-bg: #ffffff;',
235
- ' --wakz-radius-window: 16px;',
236
- ' --wakz-radius-bubble: 16px;',
237
- ' --wakz-radius-fab: 28px;',
238
- ' --wakz-shadow-sm: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06);',
239
- ' --wakz-shadow-md: 0 4px 12px rgba(0,0,0,.1), 0 2px 4px rgba(0,0,0,.06);',
240
- ' --wakz-shadow-lg: 0 12px 40px rgba(0,0,0,.15), 0 4px 12px rgba(0,0,0,.1);',
241
- ' --wakz-shadow-xl: 0 20px 60px rgba(0,0,0,.2), 0 8px 20px rgba(0,0,0,.12);',
242
- ' --wakz-transition: 200ms ease;',
252
+ ' --wakz-radius: 20px;',
253
+ ' --wakz-radius-bubble: 18px;',
243
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);',
244
257
  ' all: initial;',
245
258
  ' font-family: var(--wakz-font);',
246
259
  '}',
@@ -258,34 +271,46 @@
258
271
  '.wakz-fab {',
259
272
  ' position: fixed;',
260
273
  ' z-index: 2147483647;',
261
- ' width: 56px;',
262
- ' height: 56px;',
263
- ' border-radius: var(--wakz-radius-fab);',
274
+ ' width: 52px;',
275
+ ' height: 52px;',
276
+ ' border-radius: 50%;',
264
277
  ' border: none;',
265
278
  ' cursor: pointer;',
266
279
  ' display: flex;',
267
280
  ' align-items: center;',
268
281
  ' justify-content: center;',
269
- ' box-shadow: var(--wakz-shadow-lg);',
270
- ' transition: transform var(--wakz-transition), box-shadow var(--wakz-transition);',
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;',
271
284
  ' outline: none;',
272
285
  ' -webkit-tap-highlight-color: transparent;',
273
- ' background: var(--wakz-btn);',
286
+ ' background: var(--wakz-icon-color, #171717);',
274
287
  '}',
275
288
  '.wakz-fab:hover {',
276
289
  ' transform: scale(1.08);',
277
- ' box-shadow: var(--wakz-shadow-xl);',
290
+ ' box-shadow: 0 20px 60px rgba(0,0,0,0.2), 0 8px 20px rgba(0,0,0,0.12);',
278
291
  '}',
279
292
  '.wakz-fab:active {',
280
293
  ' transform: scale(0.96);',
281
294
  '}',
282
295
  '.wakz-fab svg {',
283
- ' width: 26px;',
284
- ' height: 26px;',
296
+ ' width: 24px;',
297
+ ' height: 24px;',
285
298
  ' color: #ffffff;',
286
299
  ' pointer-events: none;',
287
300
  '}',
288
301
 
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
+
289
314
  /* ── FAB Positioning ── */
290
315
  '.wakz-fab-pos-br { bottom: 24px; right: 24px; }',
291
316
  '.wakz-fab-pos-bl { bottom: 24px; left: 24px; }',
@@ -306,18 +331,18 @@
306
331
  ══════════════════════════════════════════════════ */
307
332
  '.wakz-fab-dot {',
308
333
  ' position: absolute;',
309
- ' top: 0px;',
310
- ' right: 0px;',
311
- ' width: 14px;',
312
- ' height: 14px;',
334
+ ' top: -1px;',
335
+ ' right: -1px;',
336
+ ' width: 12px;',
337
+ ' height: 12px;',
313
338
  ' border-radius: 50%;',
314
- ' border: 2.5px solid #ffffff;',
339
+ ' border: 2px solid #ffffff;',
315
340
  ' z-index: 2;',
316
341
  ' transition: background 0.3s ease;',
317
342
  '}',
318
343
  '.wakz-fab-pos-bl .wakz-fab-dot {',
319
344
  ' right: auto;',
320
- ' left: 0px;',
345
+ ' left: -1px;',
321
346
  '}',
322
347
 
323
348
  /* ── Online (green pulse) ── */
@@ -338,45 +363,60 @@
338
363
  ' animation: none;',
339
364
  '}',
340
365
 
341
- /* ── Error (red, no pulse) ── */
342
- '.wakz-fab-dot.error {',
343
- ' background: #ef4444;',
344
- ' animation: none;',
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;',
345
386
  '}',
346
387
 
347
388
  /* ══════════════════════════════════════════════════
348
- CHAT WINDOW
389
+ CHAT WINDOW — Centered Modal
349
390
  ══════════════════════════════════════════════════ */
350
391
  '.wakz-window {',
351
392
  ' position: fixed;',
352
393
  ' z-index: 2147483646;',
353
- ' width: 380px;',
354
- ' min-height: 500px;',
355
- ' max-height: 70vh;',
356
- ' border-radius: var(--wakz-radius-window);',
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;',
357
400
  ' overflow: hidden;',
358
401
  ' display: flex;',
359
402
  ' flex-direction: column;',
360
403
  ' background: var(--wakz-widget-bg);',
361
- ' box-shadow: var(--wakz-shadow-xl);',
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);',
362
408
  ' opacity: 0;',
363
- ' transform: translateY(20px) scale(0.95);',
364
409
  ' pointer-events: none;',
365
- ' transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),',
366
- ' transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);',
410
+ ' transition: opacity 0.3s cubic-bezier(0.16,1,0.3,1), transform 0.3s cubic-bezier(0.16,1,0.3,1);',
367
411
  '}',
368
412
  '.wakz-window.wakz-visible {',
369
413
  ' opacity: 1;',
370
- ' transform: translateY(0) scale(1);',
414
+ ' transform: translate(-50%, -50%) scale(1);',
371
415
  ' pointer-events: auto;',
372
416
  '}',
373
417
 
374
- /* ── Window Positioning ── */
375
- '.wakz-win-pos-br { bottom: 92px; right: 24px; }',
376
- '.wakz-win-pos-bl { bottom: 92px; left: 24px; }',
377
-
378
418
  /* ══════════════════════════════════════════════════
379
- HEADER
419
+ HEADER — Glassmorphism (matching floatingGlassShell)
380
420
  ══════════════════════════════════════════════════ */
381
421
  '.wakz-hdr {',
382
422
  ' flex-shrink: 0;',
@@ -384,30 +424,35 @@
384
424
  ' display: flex;',
385
425
  ' align-items: center;',
386
426
  ' justify-content: space-between;',
387
- ' color: #ffffff;',
427
+ ' color: #171717;',
388
428
  ' cursor: default;',
389
429
  ' user-select: none;',
390
- ' background: var(--wakz-primary);',
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;',
391
435
  '}',
392
- '.wakz-hdr-left {',
436
+ '.wakz-hdr-right {',
393
437
  ' display: flex;',
394
438
  ' align-items: center;',
395
439
  ' gap: 11px;',
396
440
  ' min-width: 0;',
397
441
  '}',
398
442
 
399
- /* ── Bot Avatar ── */
443
+ /* ── Bot Avatar (header) ── */
400
444
  '.wakz-avatar {',
401
- ' width: 38px;',
402
- ' height: 38px;',
445
+ ' width: 36px;',
446
+ ' height: 36px;',
403
447
  ' border-radius: 50%;',
404
- ' background: rgba(255,255,255,0.18);',
448
+ ' background: var(--wakz-primary);',
405
449
  ' display: flex;',
406
450
  ' align-items: center;',
407
451
  ' justify-content: center;',
408
452
  ' font-weight: 700;',
409
- ' font-size: 16px;',
453
+ ' font-size: 15px;',
410
454
  ' flex-shrink: 0;',
455
+ ' color: #ffffff;',
411
456
  ' letter-spacing: 0.5px;',
412
457
  '}',
413
458
 
@@ -418,20 +463,27 @@
418
463
  ' min-width: 0;',
419
464
  '}',
420
465
  '.wakz-hdr-name {',
421
- ' font-size: 15px;',
466
+ ' font-size: 16px;',
422
467
  ' font-weight: 600;',
423
468
  ' line-height: 1.3;',
469
+ ' color: #171717;',
424
470
  ' white-space: nowrap;',
425
471
  ' overflow: hidden;',
426
472
  ' text-overflow: ellipsis;',
427
473
  '}',
474
+
475
+ /* ── Header Left: status + close ── */
476
+ '.wakz-hdr-left {',
477
+ ' display: flex;',
478
+ ' align-items: center;',
479
+ ' gap: 10px;',
480
+ '}',
428
481
  '.wakz-hdr-status {',
429
482
  ' font-size: 12px;',
430
- ' opacity: 0.9;',
483
+ ' color: #171717;',
431
484
  ' display: flex;',
432
485
  ' align-items: center;',
433
486
  ' gap: 5px;',
434
- ' margin-top: 2px;',
435
487
  '}',
436
488
  '.wakz-hdr-status-dot {',
437
489
  ' width: 7px;',
@@ -441,8 +493,8 @@
441
493
  ' flex-shrink: 0;',
442
494
  ' transition: background 0.3s ease;',
443
495
  '}',
444
- '.wakz-hdr-status-dot.online { background: #4ade80; }',
445
- '.wakz-hdr-status-dot.offline { background: #f87171; }',
496
+ '.wakz-hdr-status-dot.online { background: #22c55e; }',
497
+ '.wakz-hdr-status-dot.offline { background: #ef4444; }',
446
498
 
447
499
  /* ── Close Button ── */
448
500
  '.wakz-close {',
@@ -450,18 +502,18 @@
450
502
  ' height: 32px;',
451
503
  ' border-radius: 50%;',
452
504
  ' border: none;',
453
- ' background: rgba(255,255,255,0.12);',
454
- ' color: #ffffff;',
505
+ ' background: transparent;',
506
+ ' color: #171717;',
455
507
  ' cursor: pointer;',
456
508
  ' display: flex;',
457
509
  ' align-items: center;',
458
510
  ' justify-content: center;',
459
- ' transition: background var(--wakz-transition);',
511
+ ' transition: background 0.2s ease;',
460
512
  ' outline: none;',
461
513
  ' flex-shrink: 0;',
462
514
  '}',
463
515
  '.wakz-close:hover {',
464
- ' background: rgba(255,255,255,0.28);',
516
+ ' background: rgba(0,0,0,0.06);',
465
517
  '}',
466
518
  '.wakz-close svg {',
467
519
  ' width: 18px;',
@@ -475,90 +527,130 @@
475
527
  ' flex: 1;',
476
528
  ' overflow-y: auto;',
477
529
  ' overflow-x: hidden;',
478
- ' padding: 18px 16px 10px;',
530
+ ' padding: 16px;',
479
531
  ' display: flex;',
480
532
  ' flex-direction: column;',
481
- ' gap: 6px;',
533
+ ' gap: 8px;',
482
534
  ' scroll-behavior: smooth;',
483
535
  ' background: var(--wakz-chat-bg);',
484
536
  '}',
485
537
  '.wakz-msgs::-webkit-scrollbar { width: 5px; }',
486
538
  '.wakz-msgs::-webkit-scrollbar-track { background: transparent; }',
487
539
  '.wakz-msgs::-webkit-scrollbar-thumb {',
488
- ' background: rgba(0,0,0,0.12);',
540
+ ' background: rgba(0,0,0,0.08);',
489
541
  ' border-radius: 10px;',
490
542
  '}',
491
543
 
492
544
  /* ══════════════════════════════════════════════════
493
- MESSAGE BUBBLES
545
+ MESSAGE STYLES
494
546
  ══════════════════════════════════════════════════ */
495
- '.wakz-msg-row {',
496
- ' display: flex;',
497
- ' max-width: 82%;',
498
- ' animation: wakz-msg-in 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards;',
499
- '}',
500
- '.wakz-msg-row.bot { align-self: flex-start; }',
501
- '.wakz-msg-row.user { align-self: flex-end; }',
502
-
503
547
  '@keyframes wakz-msg-in {',
504
548
  ' 0% { opacity: 0; transform: translateY(8px); }',
505
549
  ' 100% { opacity: 1; transform: translateY(0); }',
506
550
  '}',
507
551
 
508
- '.wakz-bubble {',
509
- ' padding: 10px 14px;',
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;',
510
567
  ' font-size: 14px;',
511
568
  ' line-height: 1.55;',
512
569
  ' word-wrap: break-word;',
513
570
  ' overflow-wrap: break-word;',
514
571
  ' white-space: pre-wrap;',
515
- ' border-radius: var(--wakz-radius-bubble);',
516
- ' position: relative;',
517
- '}',
518
- '.wakz-bubble.bot {',
519
- ' background: #ffffff;',
520
- ' color: #1a1a1a;',
521
- ' border-bottom-left-radius: 4px;',
522
- ' box-shadow: var(--wakz-shadow-sm);',
523
- '}',
524
- '.wakz-bubble.user {',
525
- ' color: #ffffff;',
526
- ' border-bottom-right-radius: 4px;',
572
+ ' border-radius: 18px 18px 6px 18px;',
527
573
  ' background: var(--wakz-primary);',
528
- '}',
529
- '.wakz-bubble.error-bubble {',
530
- ' background: #fef2f2;',
531
- ' color: #dc2626;',
532
- ' border: 1px solid #fecaca;',
533
- ' border-bottom-left-radius: 4px;',
534
- ' box-shadow: none;',
574
+ ' color: #ffffff;',
535
575
  '}',
536
576
 
537
- /* ── Message Timestamp ── */
577
+ /* ── Timestamp ── */
538
578
  '.wakz-ts {',
539
579
  ' display: block;',
540
- ' font-size: 11px;',
541
- ' opacity: 0.45;',
580
+ ' font-size: 10px;',
581
+ ' opacity: 0.6;',
542
582
  ' margin-top: 4px;',
543
583
  '}',
544
- '.wakz-bubble.user .wakz-ts {',
584
+ '.wakz-bubble-user .wakz-ts {',
545
585
  ' text-align: right;',
586
+ ' color: rgba(255,255,255,0.7);',
546
587
  '}',
547
588
 
548
- /* ── Human Agent Badge ── */
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
+ '}',
549
627
  '.wakz-human-badge {',
550
- ' display: inline-block;',
551
- ' font-size: 10px;',
552
- ' background: rgba(22, 163, 74, 0.1);',
628
+ ' display: inline-flex;',
629
+ ' align-items: center;',
630
+ ' font-size: 11px;',
631
+ ' background: rgba(22,163,74,0.08);',
553
632
  ' color: #16A34A;',
554
- ' border-radius: 8px;',
555
- ' padding: 1px 7px;',
556
- ' margin-top: 5px;',
633
+ ' border-radius: 20px;',
634
+ ' padding: 2px 8px;',
635
+ ' margin-top: 6px;',
557
636
  ' font-weight: 600;',
558
- ' letter-spacing: 0.2px;',
559
637
  '}',
560
638
 
561
- /* ── Retry Button (inside error bubble) ── */
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) ── */
562
654
  '.wakz-retry {',
563
655
  ' display: inline-flex;',
564
656
  ' align-items: center;',
@@ -573,7 +665,7 @@
573
665
  ' color: #dc2626;',
574
666
  ' cursor: pointer;',
575
667
  ' font-family: var(--wakz-font);',
576
- ' transition: background var(--wakz-transition), border-color var(--wakz-transition);',
668
+ ' transition: background 0.2s ease, border-color 0.2s ease;',
577
669
  ' outline: none;',
578
670
  '}',
579
671
  '.wakz-retry:hover {',
@@ -582,23 +674,32 @@
582
674
  '}',
583
675
 
584
676
  /* ══════════════════════════════════════════════════
585
- TYPING INDICATOR
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)
586
690
  ══════════════════════════════════════════════════ */
587
691
  '.wakz-typing {',
588
692
  ' display: flex;',
589
693
  ' align-items: center;',
590
694
  ' gap: 4px;',
591
- ' padding: 14px 18px;',
592
- ' background: #ffffff;',
593
- ' border-radius: var(--wakz-radius-bubble);',
594
- ' border-bottom-left-radius: 4px;',
595
- ' box-shadow: var(--wakz-shadow-sm);',
695
+ ' padding: 10px 0 10px 14px;',
696
+ ' border-left: 3px solid rgba(23,23,23,0.2);',
596
697
  '}',
597
698
  '.wakz-typing-dot {',
598
699
  ' width: 7px;',
599
700
  ' height: 7px;',
600
701
  ' border-radius: 50%;',
601
- ' background: #9ca3af;',
702
+ ' background: #9CA3AF;',
602
703
  ' animation: wakz-bounce-dot 1.4s ease-in-out infinite;',
603
704
  '}',
604
705
  '.wakz-typing-dot:nth-child(2) { animation-delay: 0.16s; }',
@@ -609,31 +710,33 @@
609
710
  '}',
610
711
 
611
712
  /* ══════════════════════════════════════════════════
612
- INPUT AREA
713
+ INPUT AREA — Glass Effect
613
714
  ══════════════════════════════════════════════════ */
614
715
  '.wakz-input-wrap {',
615
716
  ' flex-shrink: 0;',
616
- ' padding: 12px 14px;',
617
- ' border-top: 1px solid #e5e7eb;',
618
- ' background: var(--wakz-widget-bg);',
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);',
619
722
  ' display: flex;',
620
723
  ' align-items: flex-end;',
621
724
  ' gap: 8px;',
622
725
  '}',
623
726
  '.wakz-input {',
624
727
  ' flex: 1;',
625
- ' border: 1.5px solid #e5e7eb;',
626
- ' border-radius: 22px;',
728
+ ' border: 1.5px solid #E5E5E5;',
729
+ ' border-radius: 14px;',
627
730
  ' padding: 10px 16px;',
628
731
  ' font-size: 14px;',
629
732
  ' line-height: 1.4;',
630
733
  ' outline: none;',
631
734
  ' font-family: var(--wakz-font);',
632
- ' transition: border-color var(--wakz-transition);',
735
+ ' transition: border-color 0.2s ease, background 0.2s ease;',
633
736
  ' resize: none;',
634
737
  ' max-height: 100px;',
635
738
  ' overflow-y: auto;',
636
- ' background: #f9fafb;',
739
+ ' background: #F9FAFB;',
637
740
  ' color: #111827;',
638
741
  '}',
639
742
  '.wakz-input:focus {',
@@ -644,10 +747,10 @@
644
747
  ' color: #9ca3af;',
645
748
  '}',
646
749
 
647
- /* ── Send Button ── */
750
+ /* ── Send Button (floating circle, separate from textarea) ── */
648
751
  '.wakz-send {',
649
- ' width: 40px;',
650
- ' height: 40px;',
752
+ ' width: 44px;',
753
+ ' height: 44px;',
651
754
  ' border-radius: 50%;',
652
755
  ' border: none;',
653
756
  ' color: #ffffff;',
@@ -656,20 +759,21 @@
656
759
  ' align-items: center;',
657
760
  ' justify-content: center;',
658
761
  ' flex-shrink: 0;',
659
- ' transition: opacity var(--wakz-transition), transform 0.1s ease, box-shadow var(--wakz-transition);',
762
+ ' transition: opacity 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;',
660
763
  ' outline: none;',
661
764
  ' background: var(--wakz-primary);',
662
- ' box-shadow: var(--wakz-shadow-sm);',
765
+ ' box-shadow: 0 2px 8px rgba(0,0,0,0.12);',
663
766
  '}',
664
767
  '.wakz-send:hover:not(:disabled) {',
665
- ' box-shadow: var(--wakz-shadow-md);',
768
+ ' transform: scale(1.05);',
769
+ ' box-shadow: 0 4px 16px rgba(0,0,0,0.18);',
666
770
  '}',
667
771
  '.wakz-send:disabled {',
668
772
  ' opacity: 0.35;',
669
773
  ' cursor: not-allowed;',
670
774
  '}',
671
775
  '.wakz-send:not(:disabled):active {',
672
- ' transform: scale(0.88);',
776
+ ' transform: scale(0.92);',
673
777
  '}',
674
778
  '.wakz-send svg {',
675
779
  ' width: 18px;',
@@ -677,69 +781,56 @@
677
781
  ' pointer-events: none;',
678
782
  '}',
679
783
 
680
- /* ══════════════════════════════════════════════════
681
- WELCOME MESSAGE
682
- ══════════════════════════════════════════════════ */
683
- '.wakz-welcome-wrap {',
684
- ' display: flex;',
685
- ' gap: 8px;',
686
- ' max-width: 88%;',
687
- ' align-self: flex-start;',
688
- ' animation: wakz-msg-in 0.35s cubic-bezier(0.4, 0, 0.2, 1) forwards;',
689
- '}',
690
- '.wakz-welcome-avatar {',
691
- ' width: 28px;',
692
- ' height: 28px;',
693
- ' border-radius: 50%;',
694
- ' background: var(--wakz-primary);',
695
- ' display: flex;',
696
- ' align-items: center;',
697
- ' justify-content: center;',
698
- ' color: #ffffff;',
699
- ' font-weight: 700;',
700
- ' font-size: 12px;',
701
- ' flex-shrink: 0;',
702
- ' align-self: flex-end;',
703
- ' margin-bottom: 2px;',
704
- '}',
705
-
706
784
  /* ══════════════════════════════════════════════════
707
785
  RTL SUPPORT
708
786
  ══════════════════════════════════════════════════ */
709
787
  '.wakz-rtl { direction: rtl; }',
710
- '.wakz-rtl .wakz-bubble.bot {',
711
- ' border-bottom-left-radius: var(--wakz-radius-bubble);',
712
- ' border-bottom-right-radius: 4px;',
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;',
793
+ '}',
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;',
713
798
  '}',
714
- '.wakz-rtl .wakz-bubble.user {',
715
- ' border-bottom-right-radius: var(--wakz-radius-bubble);',
716
- ' border-bottom-left-radius: 4px;',
799
+ '.wakz-rtl .wakz-msg-bot-content.wakz-human {',
800
+ ' border-left: none;',
801
+ ' border-right-color: #16A34A;',
717
802
  '}',
718
803
  '.wakz-rtl .wakz-typing {',
719
- ' border-bottom-left-radius: var(--wakz-radius-bubble);',
720
- ' border-bottom-right-radius: 4px;',
804
+ ' border-left: none;',
805
+ ' border-right: 3px solid rgba(23,23,23,0.2);',
806
+ ' padding: 10px 14px 10px 0;',
721
807
  '}',
722
- '.wakz-rtl .wakz-bubble.user .wakz-ts {',
723
- ' text-align: left;',
808
+ '.wakz-rtl .wakz-bubble-error {',
809
+ ' border-radius: 18px 18px 18px 6px;',
724
810
  '}',
725
811
 
726
812
  /* ══════════════════════════════════════════════════
727
- MOBILE RESPONSIVE
813
+ MOBILE RESPONSIVE (< 480px)
728
814
  ══════════════════════════════════════════════════ */
729
815
  '@media (max-width: 480px) {',
816
+ ' .wakz-overlay {',
817
+ ' display: none;',
818
+ ' }',
730
819
  ' .wakz-window {',
820
+ ' top: auto !important;',
821
+ ' left: 0 !important;',
822
+ ' right: 0 !important;',
823
+ ' bottom: 0 !important;',
731
824
  ' width: 100vw !important;',
732
825
  ' height: 100vh !important;',
733
- ' min-height: 100vh !important;',
734
826
  ' max-height: 100vh !important;',
735
- ' bottom: 0 !important;',
736
- ' top: 0 !important;',
737
- ' right: 0 !important;',
738
- ' left: 0 !important;',
739
- ' border-radius: 0 !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;',
740
831
  ' }',
741
832
  ' .wakz-window.wakz-visible {',
742
- ' transform: translateY(0) scale(1);',
833
+ ' transform: translateY(0) !important;',
743
834
  ' }',
744
835
  ' .wakz-fab-pos-br { bottom: 16px; right: 16px; }',
745
836
  ' .wakz-fab-pos-bl { bottom: 16px; left: 16px; }',
@@ -756,6 +847,8 @@
756
847
  var isRtl = self.config.language === 'ar';
757
848
  var posClass = self.config.position === 'bottom-left' ? 'bl' : 'br';
758
849
  var str = _strings(self.config.language);
850
+ var iconColor = self._scriptIconColor || self.config.iconColor;
851
+ var iconShape = self._scriptIconShape || self.config.iconShape;
759
852
 
760
853
  /* ── Host + Shadow DOM ── */
761
854
  self._host = _el('div');
@@ -768,11 +861,16 @@
768
861
  self._shadow.appendChild(self._root);
769
862
 
770
863
  /* ══════════════ FLOATING ACTION BUTTON ══════════════ */
864
+ var fabClasses = 'wakz-fab wakz-fab-pos-' + posClass;
865
+ if (iconShape === 'rounded') fabClasses += ' wakz-fab-shape-rounded';
866
+
771
867
  self._toggleBtn = _el('button', {
772
- className: 'wakz-fab wakz-fab-pos-' + posClass,
773
- 'aria-label': str.openChat
868
+ className: fabClasses,
869
+ 'aria-label': str.openChat,
870
+ style: { '--wakz-icon-color': iconColor }
774
871
  });
775
- self._toggleBtn.innerHTML = _ICONS.chatBubble;
872
+ self._toggleBtn.style.background = iconColor;
873
+ self._toggleBtn.innerHTML = _ICONS.chatIcon;
776
874
 
777
875
  /* Status dot on FAB */
778
876
  self._statusDot = _el('span', { className: 'wakz-fab-dot ' + (self.config.online ? 'online' : 'offline') });
@@ -780,32 +878,41 @@
780
878
  self._toggleBtn.appendChild(self._statusDot);
781
879
  self._root.appendChild(self._toggleBtn);
782
880
 
783
- /* ══════════════ CHAT WINDOW ══════════════ */
784
- self._chatWindow = _el('div', { className: 'wakz-window wakz-win-pos-' + posClass });
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' });
785
887
 
786
888
  /* ── Header ── */
787
889
  var headerEl = _el('div', { className: 'wakz-hdr' });
788
890
 
789
- var headerLeft = _el('div', { className: 'wakz-hdr-left' });
891
+ /* Right side: avatar + name (in RTL, right side = start) */
892
+ var headerRight = _el('div', { className: 'wakz-hdr-right' });
790
893
  var avatarLetter = (self.config.botName || 'W')[0].toUpperCase();
791
- headerLeft.appendChild(_el('div', { className: 'wakz-avatar' }, [avatarLetter]));
894
+ headerRight.appendChild(_el('div', { className: 'wakz-avatar' }, [avatarLetter]));
792
895
 
793
896
  var headerInfo = _el('div', { className: 'wakz-hdr-info' });
794
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' });
795
903
 
796
904
  var statusLine = _el('span', { className: 'wakz-hdr-status' });
797
905
  self._headerStatusDot = _el('span', { className: 'wakz-hdr-status-dot ' + (self.config.online ? 'online' : 'offline') });
798
906
  if (self.config.showStatus) statusLine.appendChild(self._headerStatusDot);
799
907
  self._headerStatusText = document.createTextNode(self.config.online ? str.online : str.offline);
800
908
  statusLine.appendChild(self._headerStatusText);
801
- headerInfo.appendChild(statusLine);
802
-
803
- headerLeft.appendChild(headerInfo);
804
- headerEl.appendChild(headerLeft);
909
+ headerLeft.appendChild(statusLine);
805
910
 
806
911
  var closeBtn = _el('button', { className: 'wakz-close', 'aria-label': str.closeChat });
807
912
  closeBtn.innerHTML = _ICONS.close;
808
- headerEl.appendChild(closeBtn);
913
+ headerLeft.appendChild(closeBtn);
914
+ headerEl.appendChild(headerLeft);
915
+
809
916
  self._chatWindow.appendChild(headerEl);
810
917
 
811
918
  /* ── Messages Container ── */
@@ -851,6 +958,11 @@
851
958
  self.toggleChat(false);
852
959
  });
853
960
 
961
+ /* Overlay backdrop click to close */
962
+ self._overlayEl.addEventListener('click', function () {
963
+ self.toggleChat(false);
964
+ });
965
+
854
966
  /* Send button click */
855
967
  self._sendBtn.addEventListener('click', function () {
856
968
  self._handleSend();
@@ -899,6 +1011,9 @@
899
1011
  self.isOpen = open;
900
1012
  if (open) {
901
1013
  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');
902
1017
  /* Fetch history on first open (after config is loaded) */
903
1018
  if (self._configLoaded && !self._hasFetchedHistory) {
904
1019
  self._fetchHistory();
@@ -912,6 +1027,9 @@
912
1027
  setTimeout(function () { self._inputEl.focus(); }, 320);
913
1028
  } else {
914
1029
  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');
915
1033
  /* Stop polling when chat is closed */
916
1034
  self._stopPolling();
917
1035
  }
@@ -938,6 +1056,9 @@
938
1056
  .then(function (data) {
939
1057
  if (data && data.success && data.config) {
940
1058
  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;
941
1062
  self._configLoaded = true;
942
1063
  self._applyConfig();
943
1064
  } else {
@@ -977,11 +1098,13 @@
977
1098
  var str = _strings(cfg.language);
978
1099
  var isRtl = cfg.language === 'ar';
979
1100
  var posClass = cfg.position === 'bottom-left' ? 'bl' : 'br';
1101
+ var iconColor = cfg.iconColor || '#171717';
1102
+ var iconShape = cfg.iconShape || 'circle';
980
1103
 
981
1104
  /* ── Update CSS custom properties ── */
982
1105
  var hostStyle = self._shadow.host.style;
983
1106
  hostStyle.setProperty('--wakz-primary', cfg.primaryColor);
984
- hostStyle.setProperty('--wakz-btn', cfg.btnColor);
1107
+ hostStyle.setProperty('--wakz-icon-color', iconColor);
985
1108
  hostStyle.setProperty('--wakz-chat-bg', cfg.chatBg);
986
1109
  hostStyle.setProperty('--wakz-widget-bg', cfg.widgetBg);
987
1110
 
@@ -990,19 +1113,19 @@
990
1113
  else self._root.classList.remove('wakz-rtl');
991
1114
 
992
1115
  /* ── FAB ── */
993
- self._toggleBtn.className = 'wakz-fab wakz-fab-pos-' + posClass + ' wakz-fab-enter';
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;
994
1121
  self._toggleBtn.setAttribute('aria-label', str.openChat);
995
1122
 
996
1123
  /* ── FAB Status Dot ── */
997
1124
  self._statusDot.className = 'wakz-fab-dot ' + (cfg.online ? 'online' : 'offline');
998
1125
  self._statusDot.style.display = cfg.showStatus ? '' : 'none';
999
1126
 
1000
- /* ── Window Position ── */
1001
- self._chatWindow.className = 'wakz-window wakz-win-pos-' + posClass + (self.isOpen ? ' wakz-visible' : '');
1002
-
1003
- /* ── Header ── */
1004
- var headerEl = self._chatWindow.querySelector('.wakz-hdr');
1005
- if (headerEl) headerEl.style.background = cfg.primaryColor;
1127
+ /* ── Window ── */
1128
+ self._chatWindow.className = 'wakz-window' + (self.isOpen ? ' wakz-visible' : '');
1006
1129
 
1007
1130
  /* ── Bot Name & Avatar ── */
1008
1131
  var nameEl = self._chatWindow.querySelector('.wakz-hdr-name');
@@ -1057,9 +1180,12 @@
1057
1180
  if (data && data.success && data.messages && data.messages.length > 0) {
1058
1181
  /* Clear any existing messages (e.g., welcome) and render history */
1059
1182
  self._clearMessages();
1183
+ self._knownMessageIds = {};
1060
1184
  var msgs = data.messages;
1061
1185
  for (var i = 0; i < msgs.length; i++) {
1062
1186
  var m = msgs[i];
1187
+ if (m.id && self._knownMessageIds[m.id]) continue;
1188
+ if (m.id) self._knownMessageIds[m.id] = true;
1063
1189
  var role = m.role === 'user' ? 'user' : 'bot';
1064
1190
  self._appendMessage(role, m.content, false, m.createdAt, m.role === 'human_agent');
1065
1191
  }
@@ -1117,6 +1243,8 @@
1117
1243
  var msgs = data.messages;
1118
1244
  for (var i = 0; i < msgs.length; i++) {
1119
1245
  var m = msgs[i];
1246
+ if (m.id && self._knownMessageIds[m.id]) continue;
1247
+ if (m.id) self._knownMessageIds[m.id] = true;
1120
1248
  var role = m.role === 'user' ? 'user' : 'bot';
1121
1249
  var isHumanAgent = m.role === 'human_agent';
1122
1250
  self._appendMessage(role, m.content, false, m.createdAt, isHumanAgent);
@@ -1141,10 +1269,11 @@
1141
1269
  var self = this;
1142
1270
  self._messagesContainer.innerHTML = '';
1143
1271
  self.messages = [];
1272
+ self._knownMessageIds = {};
1144
1273
  };
1145
1274
 
1146
1275
  /* ════════════════════════════════════════════════════════════════
1147
- APPEND WELCOME MESSAGE (with bot avatar)
1276
+ APPEND WELCOME MESSAGE (bot style — no bubble, flat on bg)
1148
1277
  ════════════════════════════════════════════════════════════════ */
1149
1278
 
1150
1279
  WAKZWidget.prototype._appendWelcomeMessage = function (text) {
@@ -1154,13 +1283,13 @@
1154
1283
  var wrap = _el('div', { className: 'wakz-welcome-wrap' });
1155
1284
 
1156
1285
  /* Bot avatar */
1157
- var avatar = _el('div', { className: 'wakz-welcome-avatar' },
1286
+ var avatar = _el('div', { className: 'wakz-msg-bot-avatar' },
1158
1287
  [(self.config.botName || 'W')[0].toUpperCase()]);
1159
1288
  wrap.appendChild(avatar);
1160
1289
 
1161
- /* Bubble */
1162
- var bubble = _el('div', { className: 'wakz-bubble bot' }, [text]);
1163
- wrap.appendChild(bubble);
1290
+ /* Flat content with accent border */
1291
+ var content = _el('div', { className: 'wakz-msg-bot-content' }, [text]);
1292
+ wrap.appendChild(content);
1164
1293
 
1165
1294
  self._messagesContainer.appendChild(wrap);
1166
1295
  self.messages.push({ sender: 'bot', text: text });
@@ -1174,41 +1303,30 @@
1174
1303
  WAKZWidget.prototype._appendMessage = function (sender, text, isError, timestamp, isHumanAgent) {
1175
1304
  var self = this;
1176
1305
 
1177
- var row = _el('div', { className: 'wakz-msg-row ' + sender });
1178
-
1179
- var bubbleClass = 'wakz-bubble ' + sender;
1180
- if (isError) bubbleClass += ' error-bubble';
1181
- var bubble = _el('div', { className: bubbleClass });
1182
-
1183
- bubble.appendChild(document.createTextNode(text));
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));
1184
1311
 
1185
- /* Timestamp */
1186
- var tsText = '';
1187
- if (timestamp) {
1188
- try {
1189
- var d = new Date(timestamp);
1190
- if (!isNaN(d.getTime())) {
1191
- tsText = d.getHours().toString().padStart(2, '0') + ':' +
1192
- d.getMinutes().toString().padStart(2, '0');
1193
- }
1194
- } catch (e) { /* ignore */ }
1195
- }
1196
- if (!tsText) {
1197
- var now = new Date();
1198
- tsText = now.getHours().toString().padStart(2, '0') + ':' +
1199
- now.getMinutes().toString().padStart(2, '0');
1200
- }
1201
- bubble.appendChild(_el('span', { className: 'wakz-ts' }, [tsText]));
1312
+ /* Timestamp */
1313
+ var tsText = self._formatTime(timestamp);
1314
+ bubble.appendChild(_el('span', { className: 'wakz-ts' }, [tsText]));
1202
1315
 
1203
- /* Human agent badge — visual distinction for admin replies */
1204
- if (isHumanAgent) {
1205
- var iStr = _strings(self.config.language);
1206
- var badge = _el('span', { className: 'wakz-human-badge' }, [iStr.humanBadge || '👤 فريق الدعم']);
1207
- bubble.appendChild(badge);
1316
+ row.appendChild(bubble);
1317
+ self._messagesContainer.appendChild(row);
1318
+ self.messages.push({ sender: sender, text: text, isError: !!isError });
1319
+ self._scrollToBottom();
1320
+ return;
1208
1321
  }
1209
1322
 
1210
- /* Retry button for errors */
1211
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 */
1212
1330
  var str = _strings(self.config.language);
1213
1331
  var retryBtn = _el('button', { className: 'wakz-retry' }, [
1214
1332
  _ICONS.error + ' ' + str.retry
@@ -1234,19 +1352,71 @@
1234
1352
  }
1235
1353
  if (lastUserMsg) self._sendToAPI(lastUserMsg);
1236
1354
  });
1237
- })(row);
1238
- bubble.appendChild(retryBtn);
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
+ }
1364
+
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' });
1368
+
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);
1239
1389
  }
1240
1390
 
1241
- row.appendChild(bubble);
1242
- self._messagesContainer.appendChild(row);
1391
+ wrap.appendChild(content);
1392
+ botRow.appendChild(wrap);
1393
+ self._messagesContainer.appendChild(botRow);
1243
1394
 
1244
- self.messages.push({ sender: sender, text: text, isError: !!isError });
1395
+ self.messages.push({ sender: sender, text: text, isError: false, isHumanAgent: !!isHumanAgent });
1245
1396
  self._scrollToBottom();
1246
1397
  };
1247
1398
 
1248
1399
  /* ════════════════════════════════════════════════════════════════
1249
- TYPING INDICATOR
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)
1250
1420
  ════════════════════════════════════════════════════════════════ */
1251
1421
 
1252
1422
  WAKZWidget.prototype._showTyping = function () {
@@ -1349,10 +1519,14 @@
1349
1519
  } else {
1350
1520
  self._appendMessage('bot', _strings(self.config.language).errorMsg, true);
1351
1521
  }
1522
+ /* Update poll time to prevent re-fetching messages we already displayed */
1523
+ self._lastPollTime = new Date().toISOString();
1352
1524
  })
1353
1525
  .catch(function () {
1354
1526
  self._hideTyping();
1355
1527
  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();
1356
1530
  })
1357
1531
  .finally(function () {
1358
1532
  self.isLoading = false;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "wakz-chat-widget",
3
- "version": "1.0.1",
4
- "description": "Production-grade AI chat widget by WAKZ — Shadow DOM isolated, zero dependencies, Intercom/Crisp quality.",
3
+ "version": "2.0.0",
4
+ "description": "Premium AI chat widget by WAKZ — Glassmorphism UI, Shadow DOM, zero deps, centered modal design.",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "prepublishOnly": "echo 'Ready to publish wakz-chat-widget'"