@aiassist-secure/vanilla 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1593 @@
1
+ /**
2
+ * AiAssist Vanilla Widget v1.0.0
3
+ * https://aiassist.net
4
+ *
5
+ * Drop-in AI chat for any website
6
+ * (c) 2026 AiAssist - MIT License
7
+ */
8
+ /**
9
+ * AiAssist Vanilla Widget
10
+ * Drop-in AI chat for any website
11
+ *
12
+ * Usage:
13
+ * <script src="https://cdn.aiassist.net/widget.js"></script>
14
+ * <script>
15
+ * AiAssist.init({ apiKey: 'your-api-key' });
16
+ * </script>
17
+ */
18
+
19
+ (function() {
20
+ 'use strict';
21
+
22
+ const DEFAULT_ENDPOINT = 'https://api.aiassist.net';
23
+
24
+ const STYLES = `
25
+ :host {
26
+ --aa-primary: #00D4FF;
27
+ --aa-bg: #0A0A0B;
28
+ --aa-surface: #1A1A1B;
29
+ --aa-text: #FFFFFF;
30
+ --aa-text-muted: rgba(255,255,255,0.5);
31
+ --aa-border: rgba(255,255,255,0.1);
32
+ --aa-radius: 16px;
33
+ --aa-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
34
+ }
35
+
36
+ * {
37
+ box-sizing: border-box;
38
+ margin: 0;
39
+ padding: 0;
40
+ }
41
+
42
+ .aa-container {
43
+ position: fixed;
44
+ z-index: 999999;
45
+ font-family: var(--aa-font);
46
+ }
47
+
48
+ .aa-container.bottom-right { bottom: 20px; right: 20px; }
49
+ .aa-container.bottom-left { bottom: 20px; left: 20px; }
50
+ .aa-container.top-right { top: 20px; right: 20px; }
51
+ .aa-container.top-left { top: 20px; left: 20px; }
52
+
53
+ .aa-bubble {
54
+ width: 60px;
55
+ height: 60px;
56
+ border-radius: 50%;
57
+ background: var(--aa-primary);
58
+ border: none;
59
+ cursor: pointer;
60
+ display: flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ box-shadow: 0 4px 20px rgba(0, 212, 255, 0.4);
64
+ transition: transform 0.2s, box-shadow 0.2s;
65
+ }
66
+
67
+ .aa-bubble:hover {
68
+ transform: scale(1.1);
69
+ box-shadow: 0 6px 30px rgba(0, 212, 255, 0.5);
70
+ }
71
+
72
+ .aa-bubble svg {
73
+ width: 28px;
74
+ height: 28px;
75
+ fill: #000;
76
+ }
77
+
78
+ .aa-bubble.has-unread::after {
79
+ content: attr(data-count);
80
+ position: absolute;
81
+ top: -5px;
82
+ right: -5px;
83
+ background: #FF4444;
84
+ color: white;
85
+ font-size: 12px;
86
+ font-weight: bold;
87
+ width: 22px;
88
+ height: 22px;
89
+ border-radius: 50%;
90
+ display: flex;
91
+ align-items: center;
92
+ justify-content: center;
93
+ }
94
+
95
+ .aa-widget {
96
+ display: none;
97
+ width: 380px;
98
+ height: 600px;
99
+ max-height: calc(100vh - 100px);
100
+ background: var(--aa-bg);
101
+ border-radius: var(--aa-radius);
102
+ border: 1px solid var(--aa-border);
103
+ overflow: hidden;
104
+ flex-direction: column;
105
+ box-shadow: 0 10px 50px rgba(0,0,0,0.5);
106
+ position: relative;
107
+ }
108
+
109
+ .aa-widget.open {
110
+ display: flex;
111
+ }
112
+
113
+ @media (max-width: 480px) {
114
+ .aa-widget {
115
+ position: fixed;
116
+ top: 0;
117
+ left: 0;
118
+ right: 0;
119
+ bottom: 0;
120
+ width: 100%;
121
+ height: 100%;
122
+ max-height: 100%;
123
+ border-radius: 0;
124
+ z-index: 999999;
125
+ }
126
+ .aa-container.bottom-right,
127
+ .aa-container.bottom-left,
128
+ .aa-container.top-right,
129
+ .aa-container.top-left {
130
+ position: static;
131
+ }
132
+ .aa-bubble {
133
+ position: fixed;
134
+ bottom: 16px;
135
+ right: 16px;
136
+ width: 56px;
137
+ height: 56px;
138
+ }
139
+ .aa-header {
140
+ padding: 14px 16px;
141
+ }
142
+ .aa-messages {
143
+ padding: 16px;
144
+ gap: 12px;
145
+ }
146
+ .aa-message {
147
+ max-width: 90%;
148
+ }
149
+ .aa-message-content {
150
+ padding: 10px 14px;
151
+ font-size: 14px;
152
+ }
153
+ .aa-input-area {
154
+ padding: 12px 16px;
155
+ padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
156
+ }
157
+ .aa-input {
158
+ padding: 12px 16px;
159
+ font-size: 16px;
160
+ }
161
+ .aa-send {
162
+ width: 44px;
163
+ height: 44px;
164
+ }
165
+ }
166
+
167
+ .aa-header {
168
+ padding: 16px 20px;
169
+ background: var(--aa-surface);
170
+ border-bottom: 1px solid var(--aa-border);
171
+ display: flex;
172
+ align-items: center;
173
+ justify-content: space-between;
174
+ }
175
+
176
+ .aa-header-title {
177
+ display: flex;
178
+ align-items: center;
179
+ gap: 10px;
180
+ }
181
+
182
+ .aa-header-logo {
183
+ width: 32px;
184
+ height: 32px;
185
+ border-radius: 50%;
186
+ background: linear-gradient(135deg, var(--aa-primary), #0088AA);
187
+ display: flex;
188
+ align-items: center;
189
+ justify-content: center;
190
+ }
191
+
192
+ .aa-header-logo svg {
193
+ width: 18px;
194
+ height: 18px;
195
+ fill: #000;
196
+ }
197
+
198
+ .aa-header h3 {
199
+ color: var(--aa-text);
200
+ font-size: 15px;
201
+ font-weight: 600;
202
+ }
203
+
204
+ .aa-header p {
205
+ color: var(--aa-text-muted);
206
+ font-size: 12px;
207
+ }
208
+
209
+ .aa-close {
210
+ background: none;
211
+ border: none;
212
+ color: var(--aa-text-muted);
213
+ cursor: pointer;
214
+ padding: 8px;
215
+ border-radius: 8px;
216
+ transition: all 0.2s;
217
+ display: flex;
218
+ align-items: center;
219
+ justify-content: center;
220
+ flex-shrink: 0;
221
+ }
222
+
223
+ .aa-close svg {
224
+ width: 20px;
225
+ height: 20px;
226
+ fill: currentColor;
227
+ }
228
+
229
+ .aa-close:hover {
230
+ background: var(--aa-border);
231
+ color: var(--aa-text);
232
+ }
233
+
234
+ .aa-messages {
235
+ flex: 1;
236
+ overflow-y: auto;
237
+ padding: 20px;
238
+ display: flex;
239
+ flex-direction: column;
240
+ gap: 16px;
241
+ }
242
+
243
+ .aa-message {
244
+ max-width: 85%;
245
+ animation: aa-fade-in 0.3s ease;
246
+ }
247
+
248
+ @keyframes aa-fade-in {
249
+ from { opacity: 0; transform: translateY(10px); }
250
+ to { opacity: 1; transform: translateY(0); }
251
+ }
252
+
253
+ .aa-message.user {
254
+ align-self: flex-end;
255
+ }
256
+
257
+ .aa-message.ai,
258
+ .aa-message.assistant {
259
+ align-self: flex-start;
260
+ }
261
+
262
+ .aa-message-content {
263
+ padding: 12px 16px;
264
+ border-radius: 16px;
265
+ font-size: 14px;
266
+ line-height: 1.5;
267
+ }
268
+
269
+ .aa-message.user .aa-message-content {
270
+ background: var(--aa-primary);
271
+ color: #000;
272
+ border-bottom-right-radius: 4px;
273
+ }
274
+
275
+ .aa-message.ai .aa-message-content,
276
+ .aa-message.assistant .aa-message-content {
277
+ background: var(--aa-surface);
278
+ color: var(--aa-text);
279
+ border: 1px solid var(--aa-border);
280
+ border-bottom-left-radius: 4px;
281
+ }
282
+
283
+ .aa-message-label {
284
+ font-size: 11px;
285
+ color: var(--aa-text-muted);
286
+ margin-bottom: 4px;
287
+ }
288
+
289
+ .aa-message.user .aa-message-label {
290
+ text-align: right;
291
+ }
292
+
293
+ .aa-typing {
294
+ display: flex;
295
+ align-items: center;
296
+ gap: 4px;
297
+ padding: 12px 16px;
298
+ background: var(--aa-surface);
299
+ border-radius: 16px;
300
+ border: 1px solid var(--aa-border);
301
+ align-self: flex-start;
302
+ }
303
+
304
+ .aa-typing span {
305
+ width: 8px;
306
+ height: 8px;
307
+ background: var(--aa-text-muted);
308
+ border-radius: 50%;
309
+ animation: aa-bounce 1s infinite;
310
+ }
311
+
312
+ .aa-typing span:nth-child(2) { animation-delay: 0.2s; }
313
+ .aa-typing span:nth-child(3) { animation-delay: 0.4s; }
314
+
315
+ @keyframes aa-bounce {
316
+ 0%, 60%, 100% { transform: translateY(0); }
317
+ 30% { transform: translateY(-8px); }
318
+ }
319
+
320
+ .aa-input-area {
321
+ padding: 16px;
322
+ background: var(--aa-surface);
323
+ border-top: 1px solid var(--aa-border);
324
+ }
325
+
326
+ .aa-input-wrapper {
327
+ display: flex;
328
+ gap: 8px;
329
+ background: var(--aa-bg);
330
+ border: 1px solid var(--aa-border);
331
+ border-radius: 12px;
332
+ padding: 8px 12px;
333
+ transition: border-color 0.2s;
334
+ }
335
+
336
+ .aa-input-wrapper:focus-within {
337
+ border-color: var(--aa-primary);
338
+ }
339
+
340
+ .aa-input {
341
+ flex: 1;
342
+ background: none;
343
+ border: none;
344
+ color: var(--aa-text);
345
+ font-size: 14px;
346
+ font-family: var(--aa-font);
347
+ outline: none;
348
+ resize: none;
349
+ min-height: 24px;
350
+ max-height: 120px;
351
+ }
352
+
353
+ .aa-input::placeholder {
354
+ color: var(--aa-text-muted);
355
+ }
356
+
357
+ .aa-send {
358
+ width: 36px;
359
+ height: 36px;
360
+ border-radius: 10px;
361
+ background: var(--aa-primary);
362
+ border: none;
363
+ cursor: pointer;
364
+ display: flex;
365
+ align-items: center;
366
+ justify-content: center;
367
+ transition: opacity 0.2s, transform 0.2s;
368
+ align-self: flex-end;
369
+ }
370
+
371
+ .aa-send:disabled {
372
+ opacity: 0.3;
373
+ cursor: not-allowed;
374
+ }
375
+
376
+ .aa-send:not(:disabled):hover {
377
+ transform: scale(1.05);
378
+ }
379
+
380
+ .aa-send svg {
381
+ width: 18px;
382
+ height: 18px;
383
+ fill: #000;
384
+ }
385
+
386
+ .aa-powered {
387
+ text-align: center;
388
+ padding: 8px;
389
+ font-size: 11px;
390
+ color: var(--aa-text-muted);
391
+ }
392
+
393
+ .aa-powered a {
394
+ color: var(--aa-text-muted);
395
+ text-decoration: none;
396
+ }
397
+
398
+ .aa-powered a:hover {
399
+ color: var(--aa-text);
400
+ }
401
+
402
+ .aa-email-gate {
403
+ position: absolute;
404
+ inset: 0;
405
+ background: rgba(0,0,0,0.8);
406
+ backdrop-filter: blur(4px);
407
+ display: flex;
408
+ align-items: center;
409
+ justify-content: center;
410
+ padding: 12px;
411
+ z-index: 10;
412
+ border-radius: 12px;
413
+ }
414
+
415
+ .aa-email-gate.hidden {
416
+ display: none;
417
+ }
418
+
419
+ .aa-email-form {
420
+ display: flex;
421
+ align-items: center;
422
+ gap: 8px;
423
+ width: 100%;
424
+ }
425
+
426
+ .aa-email-lock {
427
+ width: 32px;
428
+ height: 32px;
429
+ border-radius: 50%;
430
+ background: rgba(0, 212, 255, 0.2);
431
+ border: 1px solid rgba(0, 212, 255, 0.3);
432
+ display: flex;
433
+ align-items: center;
434
+ justify-content: center;
435
+ flex-shrink: 0;
436
+ }
437
+
438
+ .aa-email-lock svg {
439
+ width: 14px;
440
+ height: 14px;
441
+ fill: var(--aa-primary);
442
+ }
443
+
444
+ .aa-email-input-wrap {
445
+ flex: 1;
446
+ position: relative;
447
+ }
448
+
449
+ .aa-email-input-wrap svg {
450
+ position: absolute;
451
+ left: 10px;
452
+ top: 50%;
453
+ transform: translateY(-50%);
454
+ width: 14px;
455
+ height: 14px;
456
+ fill: var(--aa-text-muted);
457
+ }
458
+
459
+ .aa-email-input {
460
+ width: 100%;
461
+ padding: 10px 10px 10px 32px;
462
+ background: rgba(255,255,255,0.1);
463
+ border: 1px solid rgba(255,255,255,0.2);
464
+ border-radius: 10px;
465
+ color: var(--aa-text);
466
+ font-size: 13px;
467
+ font-family: var(--aa-font);
468
+ outline: none;
469
+ }
470
+
471
+ .aa-email-input:focus {
472
+ border-color: rgba(0, 212, 255, 0.5);
473
+ }
474
+
475
+ .aa-email-input::placeholder {
476
+ color: var(--aa-text-muted);
477
+ }
478
+
479
+ .aa-email-submit {
480
+ width: 36px;
481
+ height: 36px;
482
+ border-radius: 10px;
483
+ background: var(--aa-primary);
484
+ border: none;
485
+ cursor: pointer;
486
+ display: flex;
487
+ align-items: center;
488
+ justify-content: center;
489
+ flex-shrink: 0;
490
+ }
491
+
492
+ .aa-email-submit:disabled {
493
+ opacity: 0.5;
494
+ cursor: not-allowed;
495
+ }
496
+
497
+ .aa-email-submit svg {
498
+ width: 16px;
499
+ height: 16px;
500
+ fill: #000;
501
+ }
502
+
503
+ .aa-email-error {
504
+ position: absolute;
505
+ bottom: -18px;
506
+ left: 0;
507
+ right: 0;
508
+ text-align: center;
509
+ font-size: 11px;
510
+ color: #ff6b6b;
511
+ }
512
+
513
+ .aa-welcome {
514
+ text-align: center;
515
+ padding: 40px 20px;
516
+ }
517
+
518
+ .aa-welcome-icon {
519
+ width: 64px;
520
+ height: 64px;
521
+ margin: 0 auto 16px;
522
+ background: linear-gradient(135deg, var(--aa-primary), #0088AA);
523
+ border-radius: 50%;
524
+ display: flex;
525
+ align-items: center;
526
+ justify-content: center;
527
+ }
528
+
529
+ .aa-welcome-icon svg {
530
+ width: 32px;
531
+ height: 32px;
532
+ fill: #000;
533
+ }
534
+
535
+ .aa-welcome h2 {
536
+ color: var(--aa-text);
537
+ font-size: 20px;
538
+ margin-bottom: 8px;
539
+ }
540
+
541
+ .aa-welcome p {
542
+ color: var(--aa-text-muted);
543
+ font-size: 14px;
544
+ }
545
+
546
+ .aa-status-indicator {
547
+ display: flex;
548
+ align-items: center;
549
+ gap: 6px;
550
+ }
551
+ .aa-status-dot {
552
+ width: 6px;
553
+ height: 6px;
554
+ border-radius: 50%;
555
+ }
556
+ .aa-status-dot.online { background: var(--aa-primary); }
557
+ .aa-status-dot.away { background: #f59e0b; }
558
+ .aa-status-dot.offline { background: #6b7280; }
559
+ .aa-offline-message {
560
+ font-size: 11px;
561
+ color: var(--aa-text-muted);
562
+ margin-top: 4px;
563
+ }
564
+
565
+ `;
566
+
567
+ const ICONS = {
568
+ chat: '<svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H5.17L4 17.17V4h16v12z"/><path d="M7 9h10v2H7zm0-3h10v2H7z"/></svg>',
569
+ close: '<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>',
570
+ send: '<svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>',
571
+ sparkle: '<svg viewBox="0 0 24 24"><path d="M12 2L9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61z"/></svg>',
572
+ lock: '<svg viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>',
573
+ mail: '<svg viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg>',
574
+ arrowUp: '<svg viewBox="0 0 24 24"><path d="M12 4l-8 8h5v8h6v-8h5z"/></svg>'
575
+ };
576
+
577
+ const WAITING_MSG = {
578
+ id: 'waiting-human',
579
+ role: 'system',
580
+ content: "You've been connected to our support team. A specialist will be with you shortly."
581
+ };
582
+
583
+ const normalizeRole = (role) => {
584
+ if (role === 'manager') return 'human';
585
+ return role;
586
+ };
587
+
588
+ const CLIENT_ID_KEY = 'aiassist_client_id';
589
+ const LEAD_STORAGE_KEY = 'aai_lead_captured';
590
+
591
+ function getOrCreateClientId() {
592
+ try {
593
+ let clientId = localStorage.getItem(CLIENT_ID_KEY);
594
+ if (!clientId) {
595
+ clientId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
596
+ const r = Math.random() * 16 | 0;
597
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
598
+ return v.toString(16);
599
+ });
600
+ localStorage.setItem(CLIENT_ID_KEY, clientId);
601
+ }
602
+ return clientId;
603
+ } catch (e) {
604
+ return 'temp-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
605
+ }
606
+ }
607
+
608
+ class AiAssistWidget {
609
+ constructor(options = {}) {
610
+ this.options = {
611
+ apiKey: options.apiKey || '',
612
+ endpoint: options.endpoint || DEFAULT_ENDPOINT,
613
+ position: options.position || 'bottom-right',
614
+ theme: options.theme || 'dark',
615
+ title: options.title || 'AI Assistant',
616
+ subtitle: options.subtitle || 'Ask me anything',
617
+ placeholder: options.placeholder || 'Type your message...',
618
+ greeting: options.greeting || null,
619
+ systemPrompt: options.systemPrompt || null,
620
+ poweredBy: options.poweredBy !== false,
621
+ autoOpen: options.autoOpen || false,
622
+ autoOpenDelay: options.autoOpenDelay || 3000,
623
+ zIndex: options.zIndex || 999999,
624
+
625
+ onReady: options.onReady || (() => {}),
626
+ onOpen: options.onOpen || (() => {}),
627
+ onClose: options.onClose || (() => {}),
628
+ onMessage: options.onMessage || (() => {}),
629
+ onError: options.onError || (() => {}),
630
+ onConversationStart: options.onConversationStart || (() => {}),
631
+ onConversationEnd: options.onConversationEnd || (() => {}),
632
+ onModeChange: options.onModeChange || (() => {}),
633
+ onLeadCapture: options.onLeadCapture || (() => {}),
634
+ requireEmail: options.requireEmail !== false
635
+ };
636
+
637
+ this.isOpen = false;
638
+ this.isTyping = false;
639
+ this.messages = [];
640
+ this.messageIds = new Set();
641
+ this.workspaceId = null;
642
+ this.unreadCount = 0;
643
+ this.mode = 'ai';
644
+ this.showWaitingMessage = false;
645
+ this.awaitingApproval = false;
646
+ this.humanMsgCountAtTakeover = 0;
647
+ this.lastKnownHumanCount = 0;
648
+ this.clientId = getOrCreateClientId();
649
+ this.isLeadCaptured = false;
650
+ this.leadInfo = null;
651
+ this.staffAvailability = "online";
652
+ this.availabilityMessage = null;
653
+ this.autoOfflineMessage = null;
654
+
655
+ this._init();
656
+ }
657
+
658
+ _init() {
659
+ this._checkLeadStatus();
660
+ this._createWidget();
661
+ this._attachEvents();
662
+ this._loadExistingChat();
663
+ this._fetchAvailability();
664
+
665
+ if (this.options.autoOpen) {
666
+ setTimeout(() => this.open(), this.options.autoOpenDelay);
667
+ }
668
+
669
+ this.options.onReady();
670
+ this._dispatch('ready');
671
+ }
672
+
673
+ async _fetchAvailability() {
674
+ try {
675
+ const res = await fetch(`${this.options.endpoint}/v1/availability`, {
676
+ headers: { "Authorization": `Bearer ${this.options.apiKey}` }
677
+ });
678
+ if (res.ok) {
679
+ const data = await res.json();
680
+ this.staffAvailability = data.availability || "online";
681
+ this.availabilityMessage = data.message || null;
682
+ this.autoOfflineMessage = data.auto_offline_message || null;
683
+ this._updateHeader();
684
+ }
685
+ } catch (e) {}
686
+ }
687
+
688
+ _checkLeadStatus() {
689
+ if (!this.options.requireEmail) {
690
+ this.isLeadCaptured = true;
691
+ return;
692
+ }
693
+ try {
694
+ const stored = localStorage.getItem(LEAD_STORAGE_KEY);
695
+ if (stored) {
696
+ const data = JSON.parse(stored);
697
+ if (data.leadId && data.email) {
698
+ this.isLeadCaptured = true;
699
+ this.leadInfo = data;
700
+ }
701
+ }
702
+ } catch (e) {}
703
+
704
+ if (!this.isLeadCaptured && this.clientId) {
705
+ this._checkLeadOnServer();
706
+ }
707
+ }
708
+
709
+ async _checkLeadOnServer() {
710
+ try {
711
+ const res = await fetch(`${this.options.endpoint}/api/leads/check/${this.clientId}`, {
712
+ headers: { 'X-API-Key': this.options.apiKey }
713
+ });
714
+ if (res.ok) {
715
+ const data = await res.json();
716
+ if (data.captured && data.leadId && data.email) {
717
+ this.isLeadCaptured = true;
718
+ this.leadInfo = { leadId: data.leadId, email: data.email };
719
+ localStorage.setItem(LEAD_STORAGE_KEY, JSON.stringify(this.leadInfo));
720
+ this._hideEmailGate();
721
+ }
722
+ }
723
+ } catch (e) {}
724
+ }
725
+
726
+ async _captureEmail(email) {
727
+ try {
728
+ const res = await fetch(`${this.options.endpoint}/api/leads/capture`, {
729
+ method: 'POST',
730
+ headers: {
731
+ 'Content-Type': 'application/json',
732
+ 'X-API-Key': this.options.apiKey
733
+ },
734
+ body: JSON.stringify({
735
+ email: email,
736
+ client_id: this.clientId,
737
+ source: 'widget'
738
+ })
739
+ });
740
+ if (!res.ok) throw new Error('Failed to capture');
741
+ const data = await res.json();
742
+ this.isLeadCaptured = true;
743
+ this.leadInfo = { leadId: data.lead_id, email: email };
744
+ localStorage.setItem(LEAD_STORAGE_KEY, JSON.stringify(this.leadInfo));
745
+ this._hideEmailGate();
746
+ this.options.onLeadCapture(this.leadInfo);
747
+ this._dispatch('lead:capture', this.leadInfo);
748
+ return true;
749
+ } catch (e) {
750
+ return false;
751
+ }
752
+ }
753
+
754
+ _hideEmailGate() {
755
+ if (this.emailGate) {
756
+ this.emailGate.style.display = 'none';
757
+ }
758
+ if (this.input) {
759
+ this.input.disabled = false;
760
+ this.input.focus();
761
+ }
762
+ }
763
+
764
+ async _loadExistingChat() {
765
+ if (!this.clientId || !this.options.apiKey) return;
766
+
767
+ try {
768
+ const res = await fetch(`${this.options.endpoint}/api/workspaces/by-client/${this.clientId}`, {
769
+ headers: {
770
+ 'X-API-Key': this.options.apiKey
771
+ }
772
+ });
773
+
774
+ if (!res.ok) return;
775
+
776
+ const data = await res.json();
777
+
778
+ if (data.exists && data.workspace) {
779
+ this.workspaceId = data.workspace.id;
780
+
781
+ const welcome = this.messagesEl.querySelector('.aa-welcome');
782
+ if (welcome) welcome.remove();
783
+
784
+ if (data.messages && data.messages.length > 0) {
785
+ data.messages.forEach(msg => {
786
+ const role = normalizeRole(msg.role);
787
+ this._addMessage(role, msg.content, msg.id, false);
788
+ });
789
+ }
790
+
791
+ const isHumanMode = data.workspace.mode === 'human' || data.workspace.mode === 'takeover';
792
+ if (isHumanMode) {
793
+ this.mode = 'human';
794
+ this.options.onModeChange('human');
795
+ this._dispatch('mode:change', { mode: 'human' });
796
+ }
797
+
798
+ this._startPolling();
799
+ this.options.onConversationStart(this.workspaceId);
800
+ this._dispatch('conversation:start', { workspaceId: this.workspaceId });
801
+ }
802
+ } catch (error) {
803
+ console.warn('AiAssist: Failed to load existing chat', error);
804
+ }
805
+ }
806
+
807
+ _createWidget() {
808
+ this.container = document.createElement('div');
809
+ this.container.id = 'aiassist-widget';
810
+
811
+ const shadow = this.container.attachShadow({ mode: 'open' });
812
+
813
+ const style = document.createElement('style');
814
+ style.textContent = STYLES;
815
+ shadow.appendChild(style);
816
+
817
+ this.root = document.createElement('div');
818
+ this.root.className = `aa-container ${this.options.position}`;
819
+ this.root.style.zIndex = this.options.zIndex;
820
+
821
+ this.root.innerHTML = `
822
+ <button class="aa-bubble" data-testid="button-chat-bubble">
823
+ ${ICONS.chat}
824
+ </button>
825
+
826
+ <div class="aa-widget" data-testid="chat-widget">
827
+ <div class="aa-header">
828
+ <div class="aa-header-title">
829
+ <div class="aa-header-logo">${ICONS.sparkle}</div>
830
+ <div class="aa-header-info">
831
+ <h3>${this._escapeHtml(this.options.title)}</h3>
832
+ <div class="aa-status-indicator" data-testid="status-staff-availability">
833
+ <span class="aa-status-dot online"></span>
834
+ <p>${this._escapeHtml(this.options.subtitle)}</p>
835
+ </div>
836
+ <div class="aa-offline-message" data-testid="status-offline-message" style="display: none;"></div>
837
+ </div>
838
+ </div>
839
+ <button class="aa-close" data-testid="button-close-chat">${ICONS.close}</button>
840
+ </div>
841
+
842
+ <div class="aa-messages" data-testid="chat-messages">
843
+ <div class="aa-welcome">
844
+ <div class="aa-welcome-icon">${ICONS.sparkle}</div>
845
+ <h2>${this._escapeHtml(this.options.title)}</h2>
846
+ <p>${this._escapeHtml(this.options.subtitle)}</p>
847
+ </div>
848
+ </div>
849
+
850
+ <div class="aa-input-area" style="position: relative;">
851
+ <div class="aa-email-gate ${this.isLeadCaptured ? 'hidden' : ''}" data-testid="email-gate">
852
+ <form class="aa-email-form">
853
+ <div class="aa-email-lock">${ICONS.lock}</div>
854
+ <div class="aa-email-input-wrap">
855
+ ${ICONS.mail}
856
+ <input type="email" class="aa-email-input" placeholder="Enter email to unlock chat" data-testid="input-email-gate" />
857
+ </div>
858
+ <button type="submit" class="aa-email-submit" data-testid="button-unlock-chat">${ICONS.arrowUp}</button>
859
+ <div class="aa-email-error"></div>
860
+ </form>
861
+ </div>
862
+ <div class="aa-input-wrapper">
863
+ <textarea
864
+ class="aa-input"
865
+ placeholder="${this._escapeHtml(this.options.placeholder)}"
866
+ ${!this.isLeadCaptured ? 'disabled' : ''}
867
+ rows="1"
868
+ data-testid="input-chat-message"
869
+ ></textarea>
870
+ <button class="aa-send" disabled data-testid="button-send-message">
871
+ ${ICONS.send}
872
+ </button>
873
+ </div>
874
+ </div>
875
+
876
+ ${this.options.poweredBy ? `
877
+ <div class="aa-powered">
878
+ Powered by <a href="https://aiassist.net" target="_blank">AiAssist</a>
879
+ </div>
880
+ ` : ''}
881
+
882
+ </div>
883
+ `;
884
+
885
+ shadow.appendChild(this.root);
886
+ document.body.appendChild(this.container);
887
+
888
+ this.bubble = this.root.querySelector('.aa-bubble');
889
+ this.widget = this.root.querySelector('.aa-widget');
890
+ this.messagesEl = this.root.querySelector('.aa-messages');
891
+ this.input = this.root.querySelector('.aa-input');
892
+ this.sendBtn = this.root.querySelector('.aa-send');
893
+ this.closeBtn = this.root.querySelector('.aa-close');
894
+ this.emailGate = this.root.querySelector('.aa-email-gate');
895
+ this.emailInput = this.root.querySelector('.aa-email-input');
896
+ this.emailSubmit = this.root.querySelector('.aa-email-submit');
897
+ this.emailForm = this.root.querySelector('.aa-email-form');
898
+ this.emailError = this.root.querySelector('.aa-email-error');
899
+ this.statusIndicator = this.root.querySelector('.aa-status-indicator');
900
+ this.statusDot = this.root.querySelector('.aa-status-dot');
901
+ this.statusText = this.root.querySelector('.aa-status-indicator p');
902
+ this.offlineMessageEl = this.root.querySelector('.aa-offline-message');
903
+
904
+ this._shouldAutoScroll = true;
905
+ this._scrollToNewAiMessage = false;
906
+ this.messagesEl.addEventListener('scroll', () => this._handleScroll());
907
+ }
908
+
909
+ _attachEvents() {
910
+ this.bubble.addEventListener('click', () => this.toggle());
911
+ this.closeBtn.addEventListener('click', () => this.close());
912
+
913
+ this.input.addEventListener('input', () => {
914
+ this._autoResize();
915
+ this.sendBtn.disabled = !this.input.value.trim();
916
+ this._sendTypingPreview();
917
+ });
918
+
919
+ this.input.addEventListener('keydown', (e) => {
920
+ if (e.key === 'Enter' && !e.shiftKey) {
921
+ e.preventDefault();
922
+ this._handleSend();
923
+ }
924
+ });
925
+
926
+ this.sendBtn.addEventListener('click', () => this._handleSend());
927
+
928
+ if (this.emailForm) {
929
+ this.emailForm.addEventListener('submit', async (e) => {
930
+ e.preventDefault();
931
+ const email = this.emailInput.value.trim();
932
+ if (!email) {
933
+ this.emailError.textContent = 'Please enter your email';
934
+ return;
935
+ }
936
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
937
+ if (!emailRegex.test(email)) {
938
+ this.emailError.textContent = 'Please enter a valid email';
939
+ return;
940
+ }
941
+ this.emailError.textContent = '';
942
+ this.emailSubmit.disabled = true;
943
+ const success = await this._captureEmail(email);
944
+ if (!success) {
945
+ this.emailError.textContent = 'Something went wrong. Try again.';
946
+ this.emailSubmit.disabled = false;
947
+ }
948
+ });
949
+
950
+ this.emailInput.addEventListener('input', () => {
951
+ this.emailError.textContent = '';
952
+ this.emailSubmit.disabled = !this.emailInput.value.trim();
953
+ });
954
+ }
955
+ }
956
+
957
+ _autoResize() {
958
+ this.input.style.height = 'auto';
959
+ this.input.style.height = Math.min(this.input.scrollHeight, 120) + 'px';
960
+ }
961
+
962
+ _escapeHtml(text) {
963
+ const div = document.createElement('div');
964
+ div.textContent = text;
965
+ return div.innerHTML;
966
+ }
967
+
968
+ _updateHeader() {
969
+ if (!this.statusDot || !this.statusText) return;
970
+
971
+ this.statusDot.classList.remove('online', 'away', 'offline');
972
+ this.statusDot.classList.add(this.staffAvailability);
973
+
974
+ if ((this.staffAvailability === 'away' || this.staffAvailability === 'offline') && this.availabilityMessage) {
975
+ this.statusText.textContent = this.availabilityMessage;
976
+ } else {
977
+ this.statusText.textContent = this.options.subtitle;
978
+ }
979
+
980
+ if (this.offlineMessageEl) {
981
+ if (this.staffAvailability === 'offline' && this.autoOfflineMessage) {
982
+ this.offlineMessageEl.textContent = this.autoOfflineMessage;
983
+ this.offlineMessageEl.style.display = 'block';
984
+ } else {
985
+ this.offlineMessageEl.style.display = 'none';
986
+ }
987
+ }
988
+ }
989
+
990
+ _dispatch(event, detail = {}) {
991
+ document.dispatchEvent(new CustomEvent(`aiassist:${event}`, { detail }));
992
+ }
993
+
994
+ async _handleSend() {
995
+ const content = this.input.value.trim();
996
+ if (!content || this.isTyping || !this.isLeadCaptured) return;
997
+
998
+ this.input.value = '';
999
+ this.sendBtn.disabled = true;
1000
+ this._autoResize();
1001
+
1002
+ const tempId = `temp-${Date.now()}`;
1003
+ this._addMessage('user', content, tempId);
1004
+ this._showTyping();
1005
+ this._scrollToNewAiMessage = true;
1006
+
1007
+ try {
1008
+ if (!this.workspaceId) {
1009
+ await this._createWorkspace(content, tempId);
1010
+ } else {
1011
+ await this._sendMessage(content, tempId);
1012
+ }
1013
+ } catch (error) {
1014
+ this._hideTyping();
1015
+ // Show the actual error message from the API if available
1016
+ const funnyFallbacks = [
1017
+ "🤖 Oops! I tripped over a cable. Let me try that again...",
1018
+ "🌀 My circuits got a bit confused there. Mind trying again?",
1019
+ "🎭 Even AIs have off moments! Give it another shot?",
1020
+ "🔧 Technical hiccup! The gremlins are being dealt with.",
1021
+ "☕ I might need more coffee... or electricity. Try again?"
1022
+ ];
1023
+ const errorMessage = error.message && !error.message.includes('API error:')
1024
+ ? error.message
1025
+ : funnyFallbacks[Math.floor(Math.random() * funnyFallbacks.length)];
1026
+ this._addMessage('ai', errorMessage);
1027
+ this.options.onError(error);
1028
+ this._dispatch('error', { error });
1029
+ }
1030
+ }
1031
+
1032
+ async _createWorkspace(message, tempUserId) {
1033
+ const payload = {
1034
+ initial_message: message,
1035
+ client_id: this.clientId,
1036
+ metadata: {
1037
+ source: 'vanilla-widget',
1038
+ url: window.location.href
1039
+ }
1040
+ };
1041
+
1042
+ if (this.options.systemPrompt) {
1043
+ payload.system_prompt = this.options.systemPrompt;
1044
+ }
1045
+
1046
+ const res = await fetch(`${this.options.endpoint}/api/workspaces`, {
1047
+ method: 'POST',
1048
+ headers: {
1049
+ 'Content-Type': 'application/json',
1050
+ 'X-API-Key': this.options.apiKey
1051
+ },
1052
+ body: JSON.stringify(payload)
1053
+ });
1054
+
1055
+ if (!res.ok) {
1056
+ const errorData = await res.json().catch(() => ({}));
1057
+ const errorMsg = errorData.detail || `API error: ${res.status}`;
1058
+ throw new Error(errorMsg);
1059
+ }
1060
+
1061
+ const data = await res.json();
1062
+ this.workspaceId = data.workspace.id;
1063
+
1064
+ this.options.onConversationStart(this.workspaceId);
1065
+ this._dispatch('conversation:start', { workspaceId: this.workspaceId });
1066
+
1067
+ this._hideTyping();
1068
+
1069
+ if (data.messages) {
1070
+ data.messages.forEach(msg => {
1071
+ const role = normalizeRole(msg.role);
1072
+ if (role === 'user') {
1073
+ this._replaceTempMessage(tempUserId, msg.id);
1074
+ } else {
1075
+ this._addMessage(role, msg.content, msg.id, false);
1076
+ this.options.onMessage(msg);
1077
+ }
1078
+ });
1079
+ }
1080
+
1081
+ const isHumanMode = data.workspace.mode === 'human' || data.workspace.mode === 'takeover';
1082
+ if (isHumanMode) {
1083
+ this.mode = 'human';
1084
+ this.options.onModeChange('human');
1085
+ this._dispatch('mode:change', { mode: 'human' });
1086
+ const hasHumanResponse = data.messages && data.messages.some(m => normalizeRole(m.role) === 'human');
1087
+ if (!hasHumanResponse) {
1088
+ this._showWaitingMessage();
1089
+ }
1090
+ }
1091
+
1092
+ this._startPolling();
1093
+ }
1094
+
1095
+ async _sendMessage(content, tempUserId) {
1096
+ const res = await fetch(`${this.options.endpoint}/api/workspaces/${this.workspaceId}/messages`, {
1097
+ method: 'POST',
1098
+ headers: {
1099
+ 'Content-Type': 'application/json',
1100
+ 'X-API-Key': this.options.apiKey
1101
+ },
1102
+ body: JSON.stringify({ content })
1103
+ });
1104
+
1105
+ if (!res.ok) {
1106
+ const errorData = await res.json().catch(() => ({}));
1107
+ const errorMsg = errorData.detail || `API error: ${res.status}`;
1108
+ throw new Error(errorMsg);
1109
+ }
1110
+
1111
+ const data = await res.json();
1112
+
1113
+ this._hideTyping();
1114
+
1115
+ const wasAiMode = this.mode === 'ai';
1116
+ const isHumanMode = data.mode === 'human' || data.mode === 'takeover';
1117
+
1118
+ if (isHumanMode && wasAiMode) {
1119
+ this.mode = 'human';
1120
+ this.humanMsgCountAtTakeover = this.messages.filter(m => m.role === 'human').length;
1121
+ this.options.onModeChange('human');
1122
+ this._dispatch('mode:change', { mode: 'human' });
1123
+ }
1124
+
1125
+ if (data.user_message) {
1126
+ this._replaceTempMessage(tempUserId, data.user_message.id);
1127
+ }
1128
+
1129
+ if (data.pending_approval) {
1130
+ this.awaitingApproval = true;
1131
+ this._showAwaitingApproval();
1132
+ } else {
1133
+ this.awaitingApproval = false;
1134
+ this._hideAwaitingApproval();
1135
+ }
1136
+
1137
+ if (data.responses) {
1138
+ const hasHumanResponse = data.responses.some(r => normalizeRole(r.role) === 'human');
1139
+
1140
+ data.responses.forEach(msg => {
1141
+ const role = normalizeRole(msg.role);
1142
+ this._addMessage(role, msg.content, msg.id, false);
1143
+ this.options.onMessage(msg);
1144
+ this._dispatch('message', { message: msg });
1145
+ });
1146
+
1147
+ if (hasHumanResponse) {
1148
+ this._hideWaitingMessage();
1149
+ } else if (isHumanMode && wasAiMode) {
1150
+ this._showWaitingMessage();
1151
+ }
1152
+ }
1153
+
1154
+ this._startPolling();
1155
+ }
1156
+
1157
+ _sendTypingPreview() {
1158
+ if (!this.workspaceId) return;
1159
+
1160
+ clearTimeout(this._typingTimeout);
1161
+ this._typingTimeout = setTimeout(() => {
1162
+ fetch(`${this.options.endpoint}/api/workspaces/${this.workspaceId}/typing`, {
1163
+ method: 'POST',
1164
+ headers: {
1165
+ 'Content-Type': 'application/json',
1166
+ 'X-API-Key': this.options.apiKey
1167
+ },
1168
+ body: JSON.stringify({ text: this.input.value })
1169
+ }).catch(() => {});
1170
+ }, 300);
1171
+ }
1172
+
1173
+ _addMessage(role, content, id = null, animate = true) {
1174
+ const msgId = id || `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1175
+
1176
+ if (this.messageIds.has(msgId)) return;
1177
+ this.messageIds.add(msgId);
1178
+
1179
+ const welcome = this.messagesEl.querySelector('.aa-welcome');
1180
+ if (welcome) welcome.remove();
1181
+
1182
+ const displayRole = role === 'human' ? 'ai' : role;
1183
+
1184
+ const msgEl = document.createElement('div');
1185
+ msgEl.className = `aa-message ${displayRole}`;
1186
+ msgEl.setAttribute('data-message-id', msgId);
1187
+ if (!animate) msgEl.style.animation = 'none';
1188
+
1189
+ const label = role === 'user' ? 'You' :
1190
+ role === 'human' ? `${this.options.title} (Human)` :
1191
+ this.options.title;
1192
+
1193
+ msgEl.innerHTML = `
1194
+ <div class="aa-message-label">${this._escapeHtml(label)}</div>
1195
+ <div class="aa-message-content">${this._formatContent(content)}</div>
1196
+ `;
1197
+
1198
+ this.messagesEl.appendChild(msgEl);
1199
+
1200
+ if (role !== 'user' && this._scrollToNewAiMessage) {
1201
+ this._scrollToMessage(msgEl);
1202
+ this._scrollToNewAiMessage = false;
1203
+ } else {
1204
+ this._scrollToBottom();
1205
+ }
1206
+
1207
+ this.messages.push({ id: msgId, role, content, timestamp: new Date().toISOString() });
1208
+
1209
+ if (!this.isOpen && role !== 'user') {
1210
+ this.unreadCount++;
1211
+ this._updateBadge();
1212
+ }
1213
+ }
1214
+
1215
+ _replaceTempMessage(tempId, canonicalId) {
1216
+ if (!tempId || this.messageIds.has(canonicalId)) return;
1217
+
1218
+ this.messageIds.delete(tempId);
1219
+ this.messageIds.add(canonicalId);
1220
+
1221
+ const msgIndex = this.messages.findIndex(m => m.id === tempId);
1222
+ if (msgIndex !== -1) {
1223
+ this.messages[msgIndex].id = canonicalId;
1224
+ }
1225
+
1226
+ const msgEl = this.messagesEl.querySelector(`[data-message-id="${tempId}"]`);
1227
+ if (msgEl) {
1228
+ msgEl.setAttribute('data-message-id', canonicalId);
1229
+ }
1230
+ }
1231
+
1232
+ _showWaitingMessage() {
1233
+ if (this.showWaitingMessage) return;
1234
+ this.showWaitingMessage = true;
1235
+
1236
+ const waitingEl = document.createElement('div');
1237
+ waitingEl.className = 'aa-message system';
1238
+ waitingEl.id = 'aa-waiting-message';
1239
+ waitingEl.innerHTML = `
1240
+ <div class="aa-message-content" style="background: transparent; border: none; color: var(--aa-text-muted); text-align: center; font-size: 13px;">
1241
+ ${this._escapeHtml(WAITING_MSG.content)}
1242
+ </div>
1243
+ `;
1244
+
1245
+ this.messagesEl.appendChild(waitingEl);
1246
+ this._scrollToBottom();
1247
+ }
1248
+
1249
+ _hideWaitingMessage() {
1250
+ this.showWaitingMessage = false;
1251
+ const waitingEl = this.messagesEl.querySelector('#aa-waiting-message');
1252
+ if (waitingEl) waitingEl.remove();
1253
+ }
1254
+
1255
+ _showAwaitingApproval() {
1256
+ if (this.messagesEl.querySelector('#aa-awaiting-approval')) return;
1257
+
1258
+ const approvalEl = document.createElement('div');
1259
+ approvalEl.className = 'aa-message system';
1260
+ approvalEl.id = 'aa-awaiting-approval';
1261
+ approvalEl.innerHTML = `
1262
+ <div class="aa-message-content" style="background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.2); color: #fbbf24; text-align: center; font-size: 13px;">
1263
+ Hang tight! Someone will be with you in just a moment.
1264
+ </div>
1265
+ `;
1266
+
1267
+ this.messagesEl.appendChild(approvalEl);
1268
+ this._scrollToBottom();
1269
+ }
1270
+
1271
+ _hideAwaitingApproval() {
1272
+ this.awaitingApproval = false;
1273
+ const approvalEl = this.messagesEl.querySelector('#aa-awaiting-approval');
1274
+ if (approvalEl) approvalEl.remove();
1275
+ }
1276
+
1277
+ _formatContent(content) {
1278
+ return this._escapeHtml(content)
1279
+ .replace(/\n/g, '<br>')
1280
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
1281
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
1282
+ .replace(/`(.*?)`/g, '<code>$1</code>');
1283
+ }
1284
+
1285
+ _showTyping() {
1286
+ this.isTyping = true;
1287
+
1288
+ const typing = document.createElement('div');
1289
+ typing.className = 'aa-typing';
1290
+ typing.id = 'aa-typing-indicator';
1291
+ typing.innerHTML = '<span></span><span></span><span></span>';
1292
+
1293
+ this.messagesEl.appendChild(typing);
1294
+ this._scrollToBottom();
1295
+ }
1296
+
1297
+ _hideTyping() {
1298
+ this.isTyping = false;
1299
+ const typing = this.messagesEl.querySelector('#aa-typing-indicator');
1300
+ if (typing) typing.remove();
1301
+ }
1302
+
1303
+ _scrollToBottom(forceScroll = false) {
1304
+ if (forceScroll || this._shouldAutoScroll) {
1305
+ this.messagesEl.scrollTop = this.messagesEl.scrollHeight;
1306
+ }
1307
+ }
1308
+
1309
+ _scrollToMessage(msgEl) {
1310
+ if (msgEl && this.messagesEl) {
1311
+ const messageTop = msgEl.offsetTop - 20;
1312
+ this.messagesEl.scrollTop = messageTop;
1313
+ }
1314
+ }
1315
+
1316
+ _checkIfNearBottom() {
1317
+ const { scrollTop, scrollHeight, clientHeight } = this.messagesEl;
1318
+ return scrollHeight - scrollTop - clientHeight < 100;
1319
+ }
1320
+
1321
+ _handleScroll() {
1322
+ this._shouldAutoScroll = this._checkIfNearBottom();
1323
+ }
1324
+
1325
+ _updateBadge() {
1326
+ if (this.unreadCount > 0) {
1327
+ this.bubble.classList.add('has-unread');
1328
+ this.bubble.setAttribute('data-count', this.unreadCount > 9 ? '9+' : this.unreadCount);
1329
+ } else {
1330
+ this.bubble.classList.remove('has-unread');
1331
+ }
1332
+ }
1333
+
1334
+ _startPolling() {
1335
+ if (this._pollInterval) return;
1336
+
1337
+ this._pollInterval = setInterval(async () => {
1338
+ if (!this.workspaceId) return;
1339
+
1340
+ try {
1341
+ const [wsRes, msgRes] = await Promise.all([
1342
+ fetch(`${this.options.endpoint}/api/workspaces/${this.workspaceId}`, {
1343
+ headers: { 'X-API-Key': this.options.apiKey }
1344
+ }),
1345
+ fetch(`${this.options.endpoint}/api/workspaces/${this.workspaceId}/messages`, {
1346
+ headers: { 'X-API-Key': this.options.apiKey }
1347
+ })
1348
+ ]);
1349
+
1350
+ if (msgRes.ok) {
1351
+ const data = await msgRes.json();
1352
+ const serverMessages = (data.messages || []).map(m => ({
1353
+ id: m.id,
1354
+ role: normalizeRole(m.role),
1355
+ content: m.content,
1356
+ created_at: m.created_at
1357
+ }));
1358
+
1359
+ const currentHumanMsgCount = serverMessages.filter(m => m.role === 'human').length;
1360
+ const previousCount = this.lastKnownHumanCount;
1361
+ this.lastKnownHumanCount = currentHumanMsgCount;
1362
+
1363
+ if (wsRes.ok) {
1364
+ const wsData = await wsRes.json();
1365
+ const workspace = wsData.workspace || wsData;
1366
+ const isHumanMode = workspace.mode === 'human' || workspace.mode === 'takeover';
1367
+
1368
+ if (isHumanMode && this.mode === 'ai') {
1369
+ this.mode = 'human';
1370
+ this.humanMsgCountAtTakeover = previousCount;
1371
+ this.options.onModeChange('human');
1372
+ this._dispatch('mode:change', { mode: 'human' });
1373
+
1374
+ if (currentHumanMsgCount > previousCount) {
1375
+ this._hideWaitingMessage();
1376
+ } else {
1377
+ this._showWaitingMessage();
1378
+ }
1379
+ } else if (!isHumanMode && this.mode === 'human') {
1380
+ this.mode = 'ai';
1381
+ this._hideWaitingMessage();
1382
+ this.humanMsgCountAtTakeover = 0;
1383
+ this.options.onModeChange('ai');
1384
+ this._dispatch('mode:change', { mode: 'ai' });
1385
+ } else if (isHumanMode && this.mode === 'human' && this.showWaitingMessage) {
1386
+ if (currentHumanMsgCount > this.humanMsgCountAtTakeover) {
1387
+ this._hideWaitingMessage();
1388
+ }
1389
+ }
1390
+ }
1391
+
1392
+ let hasNewAiMessage = false;
1393
+ serverMessages.forEach(msg => {
1394
+ if (!this.messageIds.has(msg.id) && msg.role !== 'user') {
1395
+ this._addMessage(msg.role, msg.content, msg.id, true);
1396
+ this.options.onMessage(msg);
1397
+ this._dispatch('message', { message: msg });
1398
+ if (msg.role === 'ai' || msg.role === 'human') {
1399
+ hasNewAiMessage = true;
1400
+ }
1401
+ }
1402
+ });
1403
+
1404
+ if (hasNewAiMessage && this.awaitingApproval) {
1405
+ this._hideAwaitingApproval();
1406
+ }
1407
+ }
1408
+ } catch (e) {
1409
+ console.error('Polling error:', e);
1410
+ }
1411
+ }, 3000);
1412
+ }
1413
+
1414
+ _stopPolling() {
1415
+ if (this._pollInterval) {
1416
+ clearInterval(this._pollInterval);
1417
+ this._pollInterval = null;
1418
+ }
1419
+ }
1420
+
1421
+ // Public API
1422
+
1423
+ open() {
1424
+ this.isOpen = true;
1425
+ this.widget.classList.add('open');
1426
+ this.bubble.style.display = 'none';
1427
+ this.unreadCount = 0;
1428
+ this._updateBadge();
1429
+ this.input.focus();
1430
+
1431
+ if (this.options.greeting && this.messages.length === 0) {
1432
+ this._addMessage('ai', this.options.greeting, 'greeting-msg', true);
1433
+ }
1434
+
1435
+ if (this.workspaceId) {
1436
+ this._startPolling();
1437
+ }
1438
+
1439
+ this.options.onOpen();
1440
+ this._dispatch('open');
1441
+ }
1442
+
1443
+ close() {
1444
+ this.isOpen = false;
1445
+ this.widget.classList.remove('open');
1446
+ this.bubble.style.display = 'flex';
1447
+
1448
+ this._stopPolling();
1449
+
1450
+ this.options.onClose();
1451
+ this._dispatch('close');
1452
+ }
1453
+
1454
+ toggle() {
1455
+ if (this.isOpen) {
1456
+ this.close();
1457
+ } else {
1458
+ this.open();
1459
+ }
1460
+ }
1461
+
1462
+ sendMessage(content, metadata = {}) {
1463
+ if (typeof content !== 'string' || !content.trim()) return;
1464
+
1465
+ this.input.value = content;
1466
+ this._handleSend();
1467
+ }
1468
+
1469
+ getMessages() {
1470
+ return [...this.messages];
1471
+ }
1472
+
1473
+ getWorkspaceId() {
1474
+ return this.workspaceId;
1475
+ }
1476
+
1477
+ clearConversation() {
1478
+ this._stopPolling();
1479
+ this.messages = [];
1480
+ this.messageIds = new Set();
1481
+ this.workspaceId = null;
1482
+ this.mode = 'ai';
1483
+ this.showWaitingMessage = false;
1484
+ this.humanMsgCountAtTakeover = 0;
1485
+ this.lastKnownHumanCount = 0;
1486
+
1487
+ this.messagesEl.innerHTML = `
1488
+ <div class="aa-welcome">
1489
+ <div class="aa-welcome-icon">${ICONS.sparkle}</div>
1490
+ <h2>${this._escapeHtml(this.options.title)}</h2>
1491
+ <p>${this._escapeHtml(this.options.subtitle)}</p>
1492
+ </div>
1493
+ `;
1494
+ }
1495
+
1496
+ endConversation() {
1497
+ if (this.workspaceId) {
1498
+ this.options.onConversationEnd(this.workspaceId);
1499
+ this._dispatch('conversation:end', { workspaceId: this.workspaceId });
1500
+ }
1501
+ this.clearConversation();
1502
+ }
1503
+
1504
+ async updateContext(context) {
1505
+ if (!this.workspaceId) return;
1506
+
1507
+ try {
1508
+ await fetch(`${this.options.endpoint}/api/workspaces/${this.workspaceId}`, {
1509
+ method: 'PATCH',
1510
+ headers: {
1511
+ 'Content-Type': 'application/json',
1512
+ 'X-API-Key': this.options.apiKey
1513
+ },
1514
+ body: JSON.stringify({ metadata: context })
1515
+ });
1516
+ } catch (e) {
1517
+ console.error('Failed to update context:', e);
1518
+ }
1519
+ }
1520
+
1521
+ getMode() {
1522
+ return this.mode;
1523
+ }
1524
+
1525
+ identify(userData) {
1526
+ this.userData = userData;
1527
+ }
1528
+
1529
+ clearIdentity() {
1530
+ this.userData = null;
1531
+ }
1532
+
1533
+ setPosition(position) {
1534
+ this.root.className = `aa-container ${position}`;
1535
+ this.options.position = position;
1536
+ }
1537
+
1538
+ show() {
1539
+ this.container.style.display = 'block';
1540
+ }
1541
+
1542
+ hide() {
1543
+ this.container.style.display = 'none';
1544
+ }
1545
+
1546
+ destroy() {
1547
+ this._stopPolling();
1548
+ this.container.remove();
1549
+ }
1550
+ }
1551
+
1552
+ // Global API
1553
+ const AiAssist = {
1554
+ _instance: null,
1555
+
1556
+ init(options) {
1557
+ if (this._instance) {
1558
+ console.warn('AiAssist already initialized');
1559
+ return this._instance;
1560
+ }
1561
+ this._instance = new AiAssistWidget(options);
1562
+ return this._instance;
1563
+ },
1564
+
1565
+ open() { this._instance?.open(); },
1566
+ close() { this._instance?.close(); },
1567
+ toggle() { this._instance?.toggle(); },
1568
+ isOpen() { return this._instance?.isOpen || false; },
1569
+ sendMessage(content, metadata) { this._instance?.sendMessage(content, metadata); },
1570
+ getMessages() { return this._instance?.getMessages() || []; },
1571
+ getWorkspaceId() { return this._instance?.getWorkspaceId(); },
1572
+ getMode() { return this._instance?.getMode() || 'ai'; },
1573
+ clearConversation() { this._instance?.clearConversation(); },
1574
+ endConversation() { this._instance?.endConversation(); },
1575
+ updateContext(context) { return this._instance?.updateContext(context); },
1576
+ identify(userData) { this._instance?.identify(userData); },
1577
+ clearIdentity() { this._instance?.clearIdentity(); },
1578
+ setPosition(position) { this._instance?.setPosition(position); },
1579
+ show() { this._instance?.show(); },
1580
+ hide() { this._instance?.hide(); },
1581
+ destroy() {
1582
+ this._instance?.destroy();
1583
+ this._instance = null;
1584
+ }
1585
+ };
1586
+
1587
+
1588
+ return AiAssist;
1589
+ })();
1590
+
1591
+ export default AiAssist;
1592
+ export { AiAssist };
1593
+