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