@wabbit-dashboard/embed 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,3604 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Wabbit = {}));
5
+ })(this, (function (exports) { 'use strict';
6
+
7
+ /**
8
+ * Configuration management utilities
9
+ */
10
+ /**
11
+ * Validate SDK configuration
12
+ *
13
+ * @param config - Configuration to validate
14
+ * @throws {Error} If configuration is invalid
15
+ */
16
+ function validateConfig(config) {
17
+ if (!config.apiKey) {
18
+ throw new Error('[Wabbit] API key is required');
19
+ }
20
+ if (typeof config.apiKey !== 'string') {
21
+ throw new Error('[Wabbit] API key must be a string');
22
+ }
23
+ // Validate chat config if enabled
24
+ if (config.chat?.enabled) {
25
+ if (!config.chat.collectionId) {
26
+ throw new Error('[Wabbit] collectionId is required when chat is enabled');
27
+ }
28
+ }
29
+ // Validate forms config if enabled
30
+ if (config.forms?.enabled) {
31
+ if (!config.forms.formId) {
32
+ throw new Error('[Wabbit] formId is required when forms is enabled');
33
+ }
34
+ }
35
+ }
36
+ /**
37
+ * Detect API URL based on environment
38
+ * Priority: user config > global variable > environment detection > default
39
+ */
40
+ function detectApiUrl(userConfig) {
41
+ // 1. User explicitly provided
42
+ if (userConfig)
43
+ return userConfig;
44
+ // 2. Global variable (for build-time injection)
45
+ if (typeof window !== 'undefined' && window.WABBIT_API_URL) {
46
+ return window.WABBIT_API_URL;
47
+ }
48
+ // 3. Environment detection (development vs production)
49
+ if (typeof window !== 'undefined') {
50
+ const hostname = window.location.hostname;
51
+ // Development environment detection
52
+ if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '0.0.0.0') {
53
+ return 'http://localhost:3000';
54
+ }
55
+ }
56
+ // 4. Production default
57
+ return 'https://api.wabbit.io';
58
+ }
59
+ /**
60
+ * Detect WebSocket URL based on environment
61
+ * Priority: user config > global variable > derive from apiUrl > default
62
+ */
63
+ function detectWebSocketUrl(userConfig, apiUrl) {
64
+ // 1. User explicitly provided
65
+ if (userConfig)
66
+ return userConfig;
67
+ // 2. Global variable (for build-time injection)
68
+ if (typeof window !== 'undefined' && window.WABBIT_WS_URL) {
69
+ return window.WABBIT_WS_URL;
70
+ }
71
+ // 3. Derive from apiUrl if it's a development URL
72
+ if (apiUrl && (apiUrl.includes('localhost') || apiUrl.includes('127.0.0.1'))) {
73
+ return 'ws://localhost:8003/ws/chat';
74
+ }
75
+ // 4. Production default
76
+ return 'wss://api.wabbit.io/ws/chat';
77
+ }
78
+ /**
79
+ * Merge configuration with defaults
80
+ *
81
+ * @param config - User configuration
82
+ * @returns Merged configuration with defaults
83
+ */
84
+ function mergeConfig(config) {
85
+ // Auto-detect URLs if not provided
86
+ const apiUrl = detectApiUrl(config.apiUrl);
87
+ const wsUrl = detectWebSocketUrl(config.wsUrl, apiUrl);
88
+ return {
89
+ ...config,
90
+ apiUrl,
91
+ wsUrl,
92
+ chat: config.chat
93
+ ? {
94
+ ...config.chat,
95
+ position: config.chat.position || 'bottom-right',
96
+ triggerType: config.chat.triggerType || 'button',
97
+ theme: config.chat.theme || 'auto',
98
+ primaryColor: config.chat.primaryColor || '#6366f1',
99
+ placeholder: config.chat.placeholder || 'Type your message...'
100
+ }
101
+ : undefined,
102
+ forms: config.forms
103
+ ? {
104
+ ...config.forms,
105
+ theme: config.forms.theme || 'auto'
106
+ }
107
+ : undefined,
108
+ emailCapture: config.emailCapture
109
+ ? {
110
+ ...config.emailCapture,
111
+ triggerAfterMessages: config.emailCapture.triggerAfterMessages || 3,
112
+ fields: config.emailCapture.fields || ['email']
113
+ }
114
+ : undefined
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Main Wabbit SDK class
120
+ *
121
+ * This is the main entry point for initializing and managing
122
+ * all Wabbit widgets (chat, forms, email capture).
123
+ */
124
+ /**
125
+ * Wabbit SDK main class
126
+ */
127
+ class Wabbit {
128
+ constructor(config) {
129
+ // Widget instances
130
+ this.chatWidget = null; // ChatWidget | null
131
+ this.formsWidget = null; // FormWidget | null
132
+ this.emailCaptureWidget = null; // EmailCapture | null
133
+ this.config = config;
134
+ }
135
+ /**
136
+ * Initialize the Wabbit SDK
137
+ *
138
+ * @param config - SDK configuration
139
+ * @returns Wabbit instance
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * const wabbit = Wabbit.init({
144
+ * apiKey: 'pk_live_xxx',
145
+ * chat: { enabled: true, collectionId: 'abc123' }
146
+ * });
147
+ * ```
148
+ */
149
+ static init(config) {
150
+ if (Wabbit.instance) {
151
+ console.warn('[Wabbit] SDK already initialized. Returning existing instance.');
152
+ return Wabbit.instance;
153
+ }
154
+ // Validate and merge config with defaults
155
+ validateConfig(config);
156
+ const mergedConfig = mergeConfig(config);
157
+ Wabbit.instance = new Wabbit(mergedConfig);
158
+ // Initialize widgets based on merged config
159
+ if (mergedConfig.chat?.enabled && mergedConfig.chat) {
160
+ // Import ChatWidget dynamically to avoid circular dependencies
161
+ Promise.resolve().then(function () { return ChatWidget$1; }).then(({ ChatWidget }) => {
162
+ const chat = mergedConfig.chat;
163
+ const chatConfig = {
164
+ enabled: chat.enabled,
165
+ collectionId: chat.collectionId,
166
+ // Use chat-specific apiKey/apiUrl if provided, otherwise inherit from WabbitConfig
167
+ apiKey: chat.apiKey || mergedConfig.apiKey,
168
+ apiUrl: chat.apiUrl || mergedConfig.apiUrl,
169
+ wsUrl: mergedConfig.wsUrl || chat.wsUrl, // Use global wsUrl or chat-specific wsUrl
170
+ position: chat.position,
171
+ triggerType: chat.triggerType,
172
+ triggerDelay: chat.triggerDelay,
173
+ theme: chat.theme,
174
+ primaryColor: chat.primaryColor,
175
+ welcomeMessage: chat.welcomeMessage,
176
+ placeholder: chat.placeholder
177
+ };
178
+ const chatWidget = new ChatWidget(chatConfig);
179
+ chatWidget.init();
180
+ Wabbit.instance.chatWidget = chatWidget;
181
+ });
182
+ }
183
+ if (mergedConfig.forms?.enabled && mergedConfig.forms) {
184
+ // Import FormWidget dynamically to avoid circular dependencies
185
+ Promise.resolve().then(function () { return FormWidget$1; }).then(({ FormWidget }) => {
186
+ const forms = mergedConfig.forms;
187
+ const formConfig = {
188
+ enabled: forms.enabled,
189
+ containerId: forms.containerId,
190
+ formId: forms.formId,
191
+ // Use form-specific apiKey/apiUrl if provided, otherwise inherit from WabbitConfig
192
+ apiKey: forms.apiKey || mergedConfig.apiKey,
193
+ apiUrl: forms.apiUrl || mergedConfig.apiUrl,
194
+ theme: forms.theme,
195
+ primaryColor: forms.primaryColor,
196
+ onSubmit: forms.onSubmit,
197
+ onError: forms.onError,
198
+ onLoad: forms.onLoad
199
+ };
200
+ const formWidget = new FormWidget(formConfig);
201
+ formWidget.init();
202
+ Wabbit.instance.formsWidget = formWidget;
203
+ });
204
+ }
205
+ // Initialize email capture widget if enabled
206
+ // Note: We need to wait for chat widget to be initialized first
207
+ if (mergedConfig.emailCapture?.enabled) {
208
+ const emailCapture = mergedConfig.emailCapture;
209
+ Promise.resolve().then(function () { return EmailCaptureWidget$1; }).then(({ EmailCaptureWidget }) => {
210
+ const emailCaptureConfig = {
211
+ enabled: emailCapture.enabled,
212
+ triggerAfterMessages: emailCapture.triggerAfterMessages,
213
+ title: emailCapture.title,
214
+ description: emailCapture.description,
215
+ fields: emailCapture.fields,
216
+ onCapture: emailCapture.onCapture
217
+ };
218
+ const emailCaptureWidget = new EmailCaptureWidget(emailCaptureConfig);
219
+ emailCaptureWidget.init();
220
+ Wabbit.instance.emailCaptureWidget = emailCaptureWidget;
221
+ // Connect to chat widget after it's initialized
222
+ // Use a small delay to ensure chat widget is ready
223
+ setTimeout(() => {
224
+ if (Wabbit.instance.chatWidget) {
225
+ const chatWidget = Wabbit.instance.chatWidget;
226
+ // Set WebSocket client
227
+ if (chatWidget.wsClient) {
228
+ emailCaptureWidget.setWebSocketClient(chatWidget.wsClient);
229
+ }
230
+ // Hook into message sending by intercepting ChatWidget's handleSendMessage
231
+ const originalHandleSendMessage = chatWidget.handleSendMessage;
232
+ if (originalHandleSendMessage) {
233
+ chatWidget.handleSendMessage = function (content) {
234
+ originalHandleSendMessage.call(this, content);
235
+ // Notify email capture widget when user sends a message
236
+ emailCaptureWidget.handleMessage();
237
+ };
238
+ }
239
+ }
240
+ }, 100);
241
+ });
242
+ }
243
+ // Auto-initialize forms with data-wabbit-form-id (backward compatibility)
244
+ Wabbit.instance.initLegacyForms(config);
245
+ // Call onReady callback if provided
246
+ if (config.onReady) {
247
+ // Wait for DOM to be ready
248
+ if (document.readyState === 'loading') {
249
+ document.addEventListener('DOMContentLoaded', () => {
250
+ config.onReady?.();
251
+ });
252
+ }
253
+ else {
254
+ config.onReady();
255
+ }
256
+ }
257
+ return Wabbit.instance;
258
+ }
259
+ /**
260
+ * Get the current Wabbit instance
261
+ *
262
+ * @returns Wabbit instance or null if not initialized
263
+ */
264
+ static getInstance() {
265
+ return Wabbit.instance;
266
+ }
267
+ /**
268
+ * Destroy the SDK instance and cleanup
269
+ */
270
+ static destroy() {
271
+ if (Wabbit.instance) {
272
+ // Cleanup widgets
273
+ if (Wabbit.instance.chatWidget) {
274
+ Wabbit.instance.chatWidget.destroy();
275
+ }
276
+ if (Wabbit.instance.formsWidget) {
277
+ Wabbit.instance.formsWidget.destroy();
278
+ }
279
+ // Cleanup legacy forms
280
+ const legacyContainers = document.querySelectorAll('[data-wabbit-form-id]');
281
+ legacyContainers.forEach((container) => {
282
+ const widget = container.__wabbitFormWidget;
283
+ if (widget && typeof widget.destroy === 'function') {
284
+ widget.destroy();
285
+ }
286
+ });
287
+ // Cleanup legacy form observer
288
+ if (Wabbit.instance.legacyFormObserver) {
289
+ Wabbit.instance.legacyFormObserver.disconnect();
290
+ Wabbit.instance.legacyFormObserver = null;
291
+ }
292
+ if (Wabbit.instance.emailCaptureWidget) {
293
+ Wabbit.instance.emailCaptureWidget.destroy();
294
+ }
295
+ Wabbit.instance = null;
296
+ }
297
+ }
298
+ /**
299
+ * Get current configuration
300
+ */
301
+ getConfig() {
302
+ return { ...this.config };
303
+ }
304
+ /**
305
+ * Get chat widget instance
306
+ */
307
+ get chat() {
308
+ return this.chatWidget;
309
+ }
310
+ /**
311
+ * Get forms widget instance
312
+ */
313
+ get forms() {
314
+ return this.formsWidget;
315
+ }
316
+ /**
317
+ * Get email capture widget instance
318
+ */
319
+ get emailCapture() {
320
+ return this.emailCaptureWidget;
321
+ }
322
+ /**
323
+ * Initialize legacy forms (backward compatibility)
324
+ *
325
+ * Automatically initializes forms with data-wabbit-form-id attribute
326
+ * This maintains compatibility with the old embed-form.js script
327
+ */
328
+ initLegacyForms(config) {
329
+ // Only initialize if SDK is initialized with API key
330
+ if (!config.apiKey) {
331
+ return;
332
+ }
333
+ // Wait for DOM to be ready
334
+ if (document.readyState === 'loading') {
335
+ document.addEventListener('DOMContentLoaded', () => {
336
+ this.initLegacyFormsOnReady(config);
337
+ });
338
+ }
339
+ else {
340
+ this.initLegacyFormsOnReady(config);
341
+ }
342
+ }
343
+ /**
344
+ * Initialize legacy forms when DOM is ready
345
+ */
346
+ initLegacyFormsOnReady(config) {
347
+ // Find all containers with data-wabbit-form-id
348
+ const containers = document.querySelectorAll('[data-wabbit-form-id]');
349
+ if (containers.length === 0) {
350
+ return;
351
+ }
352
+ console.log('[Wabbit] Found', containers.length, 'legacy form container(s)');
353
+ // Initialize each form
354
+ containers.forEach((container) => {
355
+ const formId = container.getAttribute('data-wabbit-form-id');
356
+ if (!formId) {
357
+ return;
358
+ }
359
+ // Skip if already initialized (check if container has form instance)
360
+ if (container.querySelector('[data-wabbit-form-instance="true"]')) {
361
+ console.warn(`[Wabbit] Form ${formId} already initialized`);
362
+ return;
363
+ }
364
+ // Import FormWidget dynamically
365
+ Promise.resolve().then(function () { return FormWidget$1; }).then(({ FormWidget }) => {
366
+ const formConfig = {
367
+ enabled: true,
368
+ // For backward compatibility, use the container itself as the container
369
+ // FormWidget.findContainer() will handle data-wabbit-form-id lookup
370
+ containerId: undefined, // Will use data-wabbit-form-id via findContainer()
371
+ formId: formId,
372
+ apiKey: config.apiKey,
373
+ apiUrl: config.apiUrl,
374
+ theme: config.forms?.theme || 'auto',
375
+ primaryColor: config.forms?.primaryColor,
376
+ onSubmit: config.forms?.onSubmit,
377
+ onError: config.forms?.onError,
378
+ onLoad: config.forms?.onLoad,
379
+ };
380
+ const formWidget = new FormWidget(formConfig);
381
+ // Override findContainer to use the specific container element
382
+ // This ensures the form renders in the correct container with data-wabbit-form-id
383
+ formWidget.findContainer = () => {
384
+ return container;
385
+ };
386
+ formWidget.init();
387
+ // Store widget reference on container for cleanup
388
+ container.__wabbitFormWidget = formWidget;
389
+ });
390
+ });
391
+ // Watch for dynamically added forms (for SPAs)
392
+ this.setupLegacyFormObserver(config);
393
+ }
394
+ /**
395
+ * Setup observer for dynamically added legacy forms
396
+ */
397
+ setupLegacyFormObserver(config) {
398
+ // Only setup once
399
+ if (this.legacyFormObserver) {
400
+ return;
401
+ }
402
+ const observer = new MutationObserver((mutations) => {
403
+ for (const mutation of mutations) {
404
+ if (mutation.addedNodes.length) {
405
+ const hasFormContainer = Array.from(mutation.addedNodes).some((node) => {
406
+ if (node.nodeType !== 1)
407
+ return false;
408
+ const element = node;
409
+ return (element.hasAttribute?.('data-wabbit-form-id') ||
410
+ element.querySelector?.('[data-wabbit-form-id]'));
411
+ });
412
+ if (hasFormContainer) {
413
+ console.log('[Wabbit] Detected dynamically added legacy form container');
414
+ this.initLegacyFormsOnReady(config);
415
+ break;
416
+ }
417
+ }
418
+ }
419
+ });
420
+ observer.observe(document.body, {
421
+ childList: true,
422
+ subtree: true,
423
+ });
424
+ this.legacyFormObserver = observer;
425
+ }
426
+ }
427
+ Wabbit.instance = null;
428
+
429
+ /**
430
+ * localStorage wrapper with type safety
431
+ */
432
+ /**
433
+ * Safe storage wrapper
434
+ */
435
+ class SafeStorage {
436
+ constructor(storage = localStorage, prefix = 'wabbit_') {
437
+ this.storage = storage;
438
+ this.prefix = prefix;
439
+ }
440
+ /**
441
+ * Get item from storage
442
+ */
443
+ get(key) {
444
+ try {
445
+ const item = this.storage.getItem(this.prefix + key);
446
+ if (item === null) {
447
+ return null;
448
+ }
449
+ return JSON.parse(item);
450
+ }
451
+ catch (error) {
452
+ console.error(`[Wabbit] Failed to get storage item "${key}":`, error);
453
+ return null;
454
+ }
455
+ }
456
+ /**
457
+ * Set item in storage
458
+ */
459
+ set(key, value) {
460
+ try {
461
+ this.storage.setItem(this.prefix + key, JSON.stringify(value));
462
+ return true;
463
+ }
464
+ catch (error) {
465
+ console.error(`[Wabbit] Failed to set storage item "${key}":`, error);
466
+ return false;
467
+ }
468
+ }
469
+ /**
470
+ * Remove item from storage
471
+ */
472
+ remove(key) {
473
+ this.storage.removeItem(this.prefix + key);
474
+ }
475
+ /**
476
+ * Clear all items with prefix
477
+ */
478
+ clear() {
479
+ if (this.storage.length !== undefined && this.storage.key) {
480
+ const keys = [];
481
+ for (let i = 0; i < this.storage.length; i++) {
482
+ const key = this.storage.key(i);
483
+ if (key && key.startsWith(this.prefix)) {
484
+ keys.push(key);
485
+ }
486
+ }
487
+ keys.forEach((key) => this.storage.removeItem(key));
488
+ }
489
+ else {
490
+ // Fallback: try to clear common keys
491
+ const commonKeys = ['session_id', 'email_capture_dismissed'];
492
+ commonKeys.forEach((key) => this.remove(key));
493
+ }
494
+ }
495
+ }
496
+ // Export singleton instance
497
+ const storage = new SafeStorage();
498
+ /**
499
+ * Get item from storage (simple string getter)
500
+ */
501
+ function getStorageItem(key) {
502
+ try {
503
+ return localStorage.getItem('wabbit_' + key);
504
+ }
505
+ catch {
506
+ return null;
507
+ }
508
+ }
509
+ /**
510
+ * Set item in storage (simple string setter)
511
+ */
512
+ function setStorageItem(key, value) {
513
+ try {
514
+ localStorage.setItem('wabbit_' + key, value);
515
+ }
516
+ catch (error) {
517
+ console.error(`[Wabbit] Failed to set storage item "${key}":`, error);
518
+ }
519
+ }
520
+
521
+ /**
522
+ * WebSocket client for chat functionality
523
+ *
524
+ * Based on demo-website/src/lib/websocket.ts but without React dependencies
525
+ */
526
+ class ChatWebSocketClient {
527
+ constructor(apiKey, wsUrl, sessionId = null) {
528
+ this.ws = null;
529
+ this.status = 'disconnected';
530
+ this.reconnectAttempts = 0;
531
+ this.maxReconnectAttempts = 3;
532
+ this.reconnectDelay = 1000;
533
+ // Event handlers
534
+ this.onStatusChange = null;
535
+ this.onWelcome = null;
536
+ this.onMessage = null;
537
+ this.onMessageHistory = null;
538
+ this.onError = null;
539
+ this.onDisconnect = null;
540
+ this.apiKey = apiKey;
541
+ this.wsUrl = wsUrl;
542
+ this.sessionId = sessionId || this.getStoredSessionId();
543
+ }
544
+ setStatus(status) {
545
+ this.status = status;
546
+ if (this.onStatusChange) {
547
+ this.onStatusChange(status);
548
+ }
549
+ }
550
+ getStatus() {
551
+ return this.status;
552
+ }
553
+ async connect() {
554
+ if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
555
+ return;
556
+ }
557
+ this.setStatus('connecting');
558
+ const params = new URLSearchParams({
559
+ api_key: this.apiKey,
560
+ });
561
+ if (this.sessionId) {
562
+ params.append('session_id', this.sessionId);
563
+ }
564
+ const url = `${this.wsUrl}?${params.toString()}`;
565
+ try {
566
+ this.ws = new WebSocket(url);
567
+ this.ws.onopen = () => {
568
+ this.setStatus('connected');
569
+ this.reconnectAttempts = 0;
570
+ console.log('[Wabbit] WebSocket connected');
571
+ };
572
+ this.ws.onmessage = (event) => {
573
+ try {
574
+ const data = JSON.parse(event.data);
575
+ this.handleMessage(data);
576
+ }
577
+ catch (error) {
578
+ console.error('[Wabbit] Failed to parse message:', error);
579
+ }
580
+ };
581
+ this.ws.onerror = (error) => {
582
+ console.error('[Wabbit] WebSocket error:', error);
583
+ if (this.onError) {
584
+ this.onError('Connection error occurred');
585
+ }
586
+ };
587
+ this.ws.onclose = () => {
588
+ console.log('[Wabbit] WebSocket closed');
589
+ this.setStatus('disconnected');
590
+ if (this.onDisconnect) {
591
+ this.onDisconnect();
592
+ }
593
+ this.attemptReconnect();
594
+ };
595
+ }
596
+ catch (error) {
597
+ console.error('[Wabbit] Failed to connect:', error);
598
+ if (this.onError) {
599
+ this.onError('Failed to establish connection');
600
+ }
601
+ this.setStatus('disconnected');
602
+ }
603
+ }
604
+ handleMessage(data) {
605
+ switch (data.type) {
606
+ case 'welcome':
607
+ this.sessionId = data.session_id;
608
+ // Store session ID in localStorage
609
+ if (this.sessionId) {
610
+ storage.set('session_id', this.sessionId);
611
+ }
612
+ if (this.onWelcome) {
613
+ this.onWelcome(data.session_id, data.collection_id, data.message || 'Connected');
614
+ }
615
+ break;
616
+ case 'message_history':
617
+ // Handle conversation history when reconnecting
618
+ if (this.onMessageHistory && data.messages && Array.isArray(data.messages)) {
619
+ const messages = data.messages.map((msg) => ({
620
+ id: msg.id || crypto.randomUUID(),
621
+ role: msg.role,
622
+ content: msg.content,
623
+ timestamp: new Date(msg.timestamp),
624
+ metadata: msg.metadata,
625
+ }));
626
+ this.onMessageHistory(messages);
627
+ }
628
+ break;
629
+ case 'assistant_message':
630
+ if (this.onMessage) {
631
+ const message = {
632
+ id: data.message_id || crypto.randomUUID(),
633
+ role: 'assistant',
634
+ content: data.content,
635
+ timestamp: new Date(),
636
+ metadata: data.metadata,
637
+ };
638
+ this.onMessage(message);
639
+ }
640
+ break;
641
+ case 'error':
642
+ if (this.onError) {
643
+ this.onError(data.message || 'An error occurred');
644
+ }
645
+ break;
646
+ default:
647
+ console.log('[Wabbit] Unknown message type:', data.type);
648
+ }
649
+ }
650
+ sendMessage(content, metadata) {
651
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
652
+ if (this.onError) {
653
+ this.onError('Not connected to chat service');
654
+ }
655
+ return;
656
+ }
657
+ const message = {
658
+ type: 'message',
659
+ content,
660
+ metadata: metadata || {},
661
+ };
662
+ this.ws.send(JSON.stringify(message));
663
+ }
664
+ disconnect() {
665
+ if (this.ws) {
666
+ this.ws.close();
667
+ this.ws = null;
668
+ }
669
+ this.setStatus('disconnected');
670
+ }
671
+ attemptReconnect() {
672
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
673
+ console.log('[Wabbit] Max reconnect attempts reached');
674
+ return;
675
+ }
676
+ this.reconnectAttempts++;
677
+ this.setStatus('reconnecting');
678
+ const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
679
+ console.log(`[Wabbit] Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
680
+ setTimeout(() => {
681
+ this.connect();
682
+ }, delay);
683
+ }
684
+ clearSession() {
685
+ this.sessionId = null;
686
+ storage.remove('session_id');
687
+ }
688
+ getStoredSessionId() {
689
+ return storage.get('session_id') || null;
690
+ }
691
+ }
692
+
693
+ /**
694
+ * DOM utility functions
695
+ */
696
+ /**
697
+ * Escape HTML to prevent XSS attacks
698
+ *
699
+ * @param text - Text to escape
700
+ * @returns Escaped HTML string
701
+ */
702
+ function escapeHtml(text) {
703
+ const div = document.createElement('div');
704
+ div.textContent = text;
705
+ return div.innerHTML;
706
+ }
707
+ /**
708
+ * Wait for DOM to be ready
709
+ *
710
+ * @param callback - Callback to execute when DOM is ready
711
+ */
712
+ function onDOMReady(callback) {
713
+ if (document.readyState === 'loading') {
714
+ document.addEventListener('DOMContentLoaded', callback);
715
+ }
716
+ else {
717
+ callback();
718
+ }
719
+ }
720
+ /**
721
+ * Create a DOM element with attributes
722
+ *
723
+ * @param tag - HTML tag name
724
+ * @param attributes - Element attributes
725
+ * @param children - Child elements or text
726
+ * @returns Created element
727
+ */
728
+ function createElement(tag, attributes, children) {
729
+ const element = document.createElement(tag);
730
+ if (attributes) {
731
+ Object.entries(attributes).forEach(([key, value]) => {
732
+ element.setAttribute(key, value);
733
+ });
734
+ }
735
+ if (children) {
736
+ children.forEach((child) => {
737
+ if (typeof child === 'string') {
738
+ element.appendChild(document.createTextNode(child));
739
+ }
740
+ else {
741
+ element.appendChild(child);
742
+ }
743
+ });
744
+ }
745
+ return element;
746
+ }
747
+
748
+ /**
749
+ * Chat Bubble - Floating button component
750
+ */
751
+ class ChatBubble {
752
+ constructor(options) {
753
+ this.element = null;
754
+ this.options = options;
755
+ }
756
+ /**
757
+ * Create and render the bubble
758
+ */
759
+ render() {
760
+ if (this.element) {
761
+ return this.element;
762
+ }
763
+ this.element = createElement('button', {
764
+ class: `wabbit-chat-bubble ${this.options.position}`,
765
+ 'aria-label': 'Open chat',
766
+ type: 'button'
767
+ });
768
+ // Add chat icon (simple SVG)
769
+ this.element.innerHTML = `
770
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
771
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
772
+ </svg>
773
+ `;
774
+ this.element.addEventListener('click', this.options.onClick);
775
+ document.body.appendChild(this.element);
776
+ return this.element;
777
+ }
778
+ /**
779
+ * Show the bubble
780
+ */
781
+ show() {
782
+ if (this.element) {
783
+ this.element.style.display = 'flex';
784
+ }
785
+ }
786
+ /**
787
+ * Hide the bubble
788
+ */
789
+ hide() {
790
+ if (this.element) {
791
+ this.element.style.display = 'none';
792
+ }
793
+ }
794
+ /**
795
+ * Remove the bubble from DOM
796
+ */
797
+ destroy() {
798
+ if (this.element) {
799
+ this.element.removeEventListener('click', this.options.onClick);
800
+ this.element.remove();
801
+ this.element = null;
802
+ }
803
+ }
804
+ }
805
+
806
+ /**
807
+ * Chat Panel - Expandable chat window component
808
+ */
809
+ class ChatPanel {
810
+ constructor(options) {
811
+ this.element = null;
812
+ this.messagesContainer = null;
813
+ this.inputElement = null;
814
+ this.sendButton = null;
815
+ this.messages = [];
816
+ this.isWaitingForResponse = false;
817
+ this.options = options;
818
+ }
819
+ /**
820
+ * Create and render the panel
821
+ */
822
+ render() {
823
+ if (this.element) {
824
+ return this.element;
825
+ }
826
+ // Main panel
827
+ this.element = createElement('div', {
828
+ class: `wabbit-chat-panel ${this.options.position}`
829
+ });
830
+ // Header
831
+ const header = createElement('div', { class: 'wabbit-chat-panel-header' });
832
+ const headerTitle = createElement('div', { class: 'wabbit-chat-panel-header-title' });
833
+ const headerIcon = createElement('div', { class: 'wabbit-chat-panel-header-icon' });
834
+ headerIcon.textContent = 'AI';
835
+ const headerText = createElement('div', { class: 'wabbit-chat-panel-header-text' });
836
+ const headerH3 = createElement('h3');
837
+ headerH3.textContent = 'AI Assistant';
838
+ const headerP = createElement('p');
839
+ headerP.textContent = 'Powered by Wabbit';
840
+ headerText.appendChild(headerH3);
841
+ headerText.appendChild(headerP);
842
+ headerTitle.appendChild(headerIcon);
843
+ headerTitle.appendChild(headerText);
844
+ const closeButton = createElement('button', {
845
+ class: 'wabbit-chat-panel-close',
846
+ 'aria-label': 'Close chat',
847
+ type: 'button'
848
+ });
849
+ closeButton.innerHTML = '×';
850
+ closeButton.addEventListener('click', this.options.onClose);
851
+ header.appendChild(headerTitle);
852
+ header.appendChild(closeButton);
853
+ // Messages area
854
+ this.messagesContainer = createElement('div', { class: 'wabbit-chat-messages' });
855
+ // Input area
856
+ const inputArea = createElement('div', { class: 'wabbit-chat-input-area' });
857
+ const inputWrapper = createElement('div', { class: 'wabbit-chat-input-wrapper' });
858
+ this.inputElement = document.createElement('textarea');
859
+ this.inputElement.className = 'wabbit-chat-input';
860
+ this.inputElement.placeholder = this.options.placeholder || 'Type your message...';
861
+ this.inputElement.rows = 1;
862
+ this.inputElement.disabled = this.options.disabled || false;
863
+ // Auto-resize textarea
864
+ this.inputElement.addEventListener('input', () => {
865
+ this.inputElement.style.height = 'auto';
866
+ this.inputElement.style.height = `${Math.min(this.inputElement.scrollHeight, 120)}px`;
867
+ });
868
+ // Send on Enter (Shift+Enter for new line)
869
+ this.inputElement.addEventListener('keydown', (e) => {
870
+ if (e.key === 'Enter' && !e.shiftKey) {
871
+ e.preventDefault();
872
+ this.handleSend();
873
+ }
874
+ });
875
+ this.sendButton = createElement('button', {
876
+ class: 'wabbit-chat-send-button',
877
+ type: 'button',
878
+ 'aria-label': 'Send message'
879
+ });
880
+ this.sendButton.innerHTML = `
881
+ <svg class="wabbit-chat-send-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
882
+ <line x1="22" y1="2" x2="11" y2="13"></line>
883
+ <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
884
+ </svg>
885
+ `;
886
+ this.sendButton.addEventListener('click', () => this.handleSend());
887
+ this.updateSendButtonState();
888
+ inputWrapper.appendChild(this.inputElement);
889
+ inputWrapper.appendChild(this.sendButton);
890
+ inputArea.appendChild(inputWrapper);
891
+ // Assemble panel
892
+ this.element.appendChild(header);
893
+ this.element.appendChild(this.messagesContainer);
894
+ this.element.appendChild(inputArea);
895
+ document.body.appendChild(this.element);
896
+ // Initially hide the panel (it will be shown when opened)
897
+ this.element.style.display = 'none';
898
+ // Show welcome message if provided
899
+ if (this.options.welcomeMessage) {
900
+ this.addSystemMessage(this.options.welcomeMessage);
901
+ }
902
+ return this.element;
903
+ }
904
+ /**
905
+ * Add a message to the panel
906
+ */
907
+ addMessage(message) {
908
+ this.messages.push(message);
909
+ this.renderMessage(message);
910
+ this.scrollToBottom();
911
+ }
912
+ /**
913
+ * Add multiple messages
914
+ */
915
+ setMessages(messages) {
916
+ this.messages = messages;
917
+ if (this.messagesContainer) {
918
+ this.messagesContainer.innerHTML = '';
919
+ messages.forEach((msg) => this.renderMessage(msg));
920
+ this.scrollToBottom();
921
+ }
922
+ }
923
+ /**
924
+ * Add system message
925
+ */
926
+ addSystemMessage(content) {
927
+ const message = {
928
+ id: `system-${Date.now()}`,
929
+ role: 'system',
930
+ content,
931
+ timestamp: new Date()
932
+ };
933
+ this.addMessage(message);
934
+ }
935
+ /**
936
+ * Set waiting for response state
937
+ */
938
+ setWaitingForResponse(waiting) {
939
+ this.isWaitingForResponse = waiting;
940
+ if (this.messagesContainer) {
941
+ // Remove existing typing indicator
942
+ const existing = this.messagesContainer.querySelector('.wabbit-chat-typing');
943
+ if (existing) {
944
+ existing.remove();
945
+ }
946
+ if (waiting) {
947
+ const typing = createElement('div', { class: 'wabbit-chat-typing' });
948
+ typing.innerHTML = `
949
+ <div class="wabbit-chat-typing-dot"></div>
950
+ <div class="wabbit-chat-typing-dot"></div>
951
+ <div class="wabbit-chat-typing-dot"></div>
952
+ `;
953
+ this.messagesContainer.appendChild(typing);
954
+ this.scrollToBottom();
955
+ }
956
+ }
957
+ this.updateSendButtonState();
958
+ }
959
+ /**
960
+ * Set disabled state
961
+ */
962
+ setDisabled(disabled) {
963
+ if (this.inputElement) {
964
+ this.inputElement.disabled = disabled;
965
+ this.updateSendButtonState();
966
+ }
967
+ }
968
+ /**
969
+ * Render a single message
970
+ */
971
+ renderMessage(message) {
972
+ if (!this.messagesContainer)
973
+ return;
974
+ const messageDiv = createElement('div', {
975
+ class: `wabbit-chat-message wabbit-chat-message-${message.role}`
976
+ });
977
+ const bubble = createElement('div', { class: 'wabbit-chat-message-bubble' });
978
+ const content = createElement('div', { class: 'wabbit-chat-message-content' });
979
+ // Escape HTML for user and system messages, but allow markdown-like formatting for assistant
980
+ if (message.role === 'assistant') {
981
+ // Simple markdown-like rendering (basic)
982
+ content.innerHTML = this.formatMessage(message.content);
983
+ }
984
+ else {
985
+ content.textContent = message.content;
986
+ }
987
+ bubble.appendChild(content);
988
+ messageDiv.appendChild(bubble);
989
+ this.messagesContainer.appendChild(messageDiv);
990
+ }
991
+ /**
992
+ * Format message content (simple markdown-like)
993
+ */
994
+ formatMessage(content) {
995
+ // Escape HTML first
996
+ let formatted = escapeHtml(content);
997
+ // Simple markdown-like formatting
998
+ formatted = formatted.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
999
+ formatted = formatted.replace(/\*(.+?)\*/g, '<em>$1</em>');
1000
+ formatted = formatted.replace(/`(.+?)`/g, '<code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;">$1</code>');
1001
+ formatted = formatted.replace(/\n/g, '<br>');
1002
+ return formatted;
1003
+ }
1004
+ /**
1005
+ * Handle send message
1006
+ */
1007
+ handleSend() {
1008
+ if (!this.inputElement || this.isWaitingForResponse)
1009
+ return;
1010
+ const content = this.inputElement.value.trim();
1011
+ if (!content)
1012
+ return;
1013
+ this.inputElement.value = '';
1014
+ this.inputElement.style.height = 'auto';
1015
+ this.options.onSendMessage(content);
1016
+ }
1017
+ /**
1018
+ * Update send button state
1019
+ */
1020
+ updateSendButtonState() {
1021
+ if (!this.sendButton || !this.inputElement)
1022
+ return;
1023
+ const hasText = this.inputElement.value.trim().length > 0;
1024
+ const disabled = this.options.disabled || this.isWaitingForResponse || !hasText;
1025
+ this.sendButton.setAttribute('disabled', disabled ? 'true' : 'false');
1026
+ if (this.sendButton instanceof HTMLButtonElement) {
1027
+ this.sendButton.disabled = disabled;
1028
+ }
1029
+ }
1030
+ /**
1031
+ * Scroll to bottom of messages
1032
+ */
1033
+ scrollToBottom() {
1034
+ if (this.messagesContainer) {
1035
+ this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
1036
+ }
1037
+ }
1038
+ /**
1039
+ * Show the panel
1040
+ */
1041
+ show() {
1042
+ if (this.element) {
1043
+ this.element.style.display = 'flex';
1044
+ // Focus input after a brief delay
1045
+ setTimeout(() => {
1046
+ if (this.inputElement) {
1047
+ this.inputElement.focus();
1048
+ }
1049
+ }, 100);
1050
+ }
1051
+ }
1052
+ /**
1053
+ * Hide the panel
1054
+ */
1055
+ hide() {
1056
+ if (this.element) {
1057
+ this.element.style.display = 'none';
1058
+ }
1059
+ }
1060
+ /**
1061
+ * Remove the panel from DOM
1062
+ */
1063
+ destroy() {
1064
+ if (this.element) {
1065
+ this.element.remove();
1066
+ this.element = null;
1067
+ this.messagesContainer = null;
1068
+ this.inputElement = null;
1069
+ this.sendButton = null;
1070
+ }
1071
+ }
1072
+ }
1073
+
1074
+ /**
1075
+ * Theme detection utilities
1076
+ */
1077
+ /**
1078
+ * Detect current theme from the page
1079
+ *
1080
+ * Simple detection:
1081
+ * 1. Check data-theme attribute (explicit setting)
1082
+ * 2. Check dark class (common pattern)
1083
+ * 3. Default to light (most pages are light)
1084
+ *
1085
+ * Note: We default to 'light' instead of checking system preference
1086
+ * because most web pages are light-themed, and it's better to have
1087
+ * readable dark text on light background than light text on light background.
1088
+ *
1089
+ * @returns Current theme ('light' or 'dark')
1090
+ */
1091
+ function detectTheme() {
1092
+ const htmlEl = document.documentElement;
1093
+ // Check data-theme attribute (explicit setting)
1094
+ const dataTheme = htmlEl.getAttribute('data-theme');
1095
+ if (dataTheme === 'light' || dataTheme === 'dark') {
1096
+ return dataTheme;
1097
+ }
1098
+ // Check dark class (common pattern like Tailwind)
1099
+ if (htmlEl.classList.contains('dark')) {
1100
+ return 'dark';
1101
+ }
1102
+ // Default to light (most pages are light-themed)
1103
+ // This ensures readable dark text on light background
1104
+ return 'light';
1105
+ }
1106
+ /**
1107
+ * Watch for theme changes
1108
+ *
1109
+ * @param callback - Callback to execute when theme changes
1110
+ * @returns Cleanup function
1111
+ */
1112
+ function watchTheme(callback) {
1113
+ const observer = new MutationObserver(() => {
1114
+ callback(detectTheme());
1115
+ });
1116
+ observer.observe(document.documentElement, {
1117
+ attributes: true,
1118
+ attributeFilter: ['data-theme', 'class']
1119
+ });
1120
+ // Also watch system preference changes
1121
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
1122
+ const handleChange = () => {
1123
+ callback(detectTheme());
1124
+ };
1125
+ mediaQuery.addEventListener('change', handleChange);
1126
+ // Initial call
1127
+ callback(detectTheme());
1128
+ // Return cleanup function
1129
+ return () => {
1130
+ observer.disconnect();
1131
+ mediaQuery.removeEventListener('change', handleChange);
1132
+ };
1133
+ }
1134
+
1135
+ /**
1136
+ * Chat widget styles
1137
+ *
1138
+ * Based on demo-website styles but adapted for embedded widget
1139
+ */
1140
+ /**
1141
+ * Inject chat widget styles into the page
1142
+ */
1143
+ function injectChatStyles(primaryColor = '#6366f1', theme) {
1144
+ const styleId = 'wabbit-chat-styles';
1145
+ // Remove existing styles if any
1146
+ const existing = document.getElementById(styleId);
1147
+ if (existing) {
1148
+ existing.remove();
1149
+ }
1150
+ // Determine actual theme: if 'auto', detect from page; otherwise use specified theme
1151
+ const actualTheme = theme === 'auto' || !theme ? detectTheme() : theme;
1152
+ const isDark = actualTheme === 'dark';
1153
+ // CSS variables based on theme
1154
+ const cssVars = isDark
1155
+ ? {
1156
+ '--wabbit-bg-base': '#1A1816',
1157
+ '--wabbit-bg-elevated': '#2D2826',
1158
+ '--wabbit-bg-hover': '#3D3935',
1159
+ '--wabbit-text-primary': '#F5F1ED',
1160
+ '--wabbit-text-secondary': '#A39C94',
1161
+ '--wabbit-text-muted': '#6B6560',
1162
+ '--wabbit-border-subtle': '#3D3935',
1163
+ '--wabbit-primary': primaryColor,
1164
+ }
1165
+ : {
1166
+ '--wabbit-bg-base': '#FAF8F6',
1167
+ '--wabbit-bg-elevated': '#FFFFFF',
1168
+ '--wabbit-bg-hover': '#F2F0EE',
1169
+ '--wabbit-text-primary': '#2D2826',
1170
+ '--wabbit-text-secondary': '#5A5450',
1171
+ '--wabbit-text-muted': '#8A847E',
1172
+ '--wabbit-border-subtle': '#E6E3E0',
1173
+ '--wabbit-primary': primaryColor,
1174
+ };
1175
+ const cssVarsString = Object.entries(cssVars)
1176
+ .map(([key, value]) => `${key}: ${value};`)
1177
+ .join('\n ');
1178
+ const styles = `
1179
+ :root {
1180
+ ${cssVarsString}
1181
+ }
1182
+
1183
+ /* Chat Bubble */
1184
+ .wabbit-chat-bubble {
1185
+ position: fixed;
1186
+ z-index: 9999;
1187
+ width: 60px;
1188
+ height: 60px;
1189
+ border-radius: 50%;
1190
+ background: linear-gradient(135deg, var(--wabbit-primary), ${adjustColor(primaryColor, 0.2)});
1191
+ border: none;
1192
+ cursor: pointer;
1193
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.1);
1194
+ display: flex;
1195
+ align-items: center;
1196
+ justify-content: center;
1197
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
1198
+ color: white;
1199
+ font-size: 24px;
1200
+ }
1201
+
1202
+ .wabbit-chat-bubble:hover {
1203
+ transform: scale(1.1);
1204
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2), 0 4px 8px rgba(0, 0, 0, 0.15);
1205
+ }
1206
+
1207
+ .wabbit-chat-bubble:active {
1208
+ transform: scale(0.95);
1209
+ }
1210
+
1211
+ .wabbit-chat-bubble.bottom-right {
1212
+ bottom: 20px;
1213
+ right: 20px;
1214
+ }
1215
+
1216
+ .wabbit-chat-bubble.bottom-left {
1217
+ bottom: 20px;
1218
+ left: 20px;
1219
+ }
1220
+
1221
+ /* Chat Panel */
1222
+ .wabbit-chat-panel {
1223
+ position: fixed;
1224
+ z-index: 9998;
1225
+ width: 380px;
1226
+ max-width: calc(100vw - 40px);
1227
+ height: 600px;
1228
+ max-height: calc(100vh - 100px);
1229
+ background: var(--wabbit-bg-elevated);
1230
+ border: 1px solid var(--wabbit-border-subtle);
1231
+ border-radius: 16px;
1232
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
1233
+ display: flex;
1234
+ flex-direction: column;
1235
+ overflow: hidden;
1236
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
1237
+ font-size: 14px;
1238
+ line-height: 1.5;
1239
+ color: var(--wabbit-text-primary);
1240
+ }
1241
+
1242
+ .wabbit-chat-panel.bottom-right {
1243
+ bottom: 90px;
1244
+ right: 20px;
1245
+ }
1246
+
1247
+ .wabbit-chat-panel.bottom-left {
1248
+ bottom: 90px;
1249
+ left: 20px;
1250
+ }
1251
+
1252
+ @media (max-width: 480px) {
1253
+ .wabbit-chat-panel {
1254
+ width: calc(100vw - 20px);
1255
+ height: calc(100vh - 20px);
1256
+ max-height: calc(100vh - 20px);
1257
+ bottom: 10px !important;
1258
+ left: 10px !important;
1259
+ right: 10px !important;
1260
+ border-radius: 12px;
1261
+ }
1262
+ }
1263
+
1264
+ /* Panel Header */
1265
+ .wabbit-chat-panel-header {
1266
+ background: rgba(var(--wabbit-primary-rgb, 99, 102, 241), 0.1);
1267
+ border-bottom: 1px solid var(--wabbit-border-subtle);
1268
+ padding: 16px;
1269
+ display: flex;
1270
+ align-items: center;
1271
+ justify-content: space-between;
1272
+ }
1273
+
1274
+ .wabbit-chat-panel-header-title {
1275
+ display: flex;
1276
+ align-items: center;
1277
+ gap: 12px;
1278
+ }
1279
+
1280
+ .wabbit-chat-panel-header-icon {
1281
+ width: 32px;
1282
+ height: 32px;
1283
+ border-radius: 50%;
1284
+ background: linear-gradient(135deg, var(--wabbit-primary), ${adjustColor(primaryColor, 0.2)});
1285
+ display: flex;
1286
+ align-items: center;
1287
+ justify-content: center;
1288
+ color: white;
1289
+ font-size: 16px;
1290
+ font-weight: 600;
1291
+ }
1292
+
1293
+ .wabbit-chat-panel-header-text h3 {
1294
+ margin: 0;
1295
+ font-size: 16px;
1296
+ font-weight: 600;
1297
+ color: var(--wabbit-text-primary);
1298
+ }
1299
+
1300
+ .wabbit-chat-panel-header-text p {
1301
+ margin: 0;
1302
+ font-size: 12px;
1303
+ color: var(--wabbit-text-muted);
1304
+ }
1305
+
1306
+ .wabbit-chat-panel-close {
1307
+ background: none;
1308
+ border: none;
1309
+ cursor: pointer;
1310
+ color: var(--wabbit-text-secondary);
1311
+ font-size: 24px;
1312
+ line-height: 1;
1313
+ padding: 4px;
1314
+ transition: color 0.2s;
1315
+ }
1316
+
1317
+ .wabbit-chat-panel-close:hover {
1318
+ color: var(--wabbit-text-primary);
1319
+ }
1320
+
1321
+ /* Messages Area */
1322
+ .wabbit-chat-messages {
1323
+ flex: 1;
1324
+ overflow-y: auto;
1325
+ padding: 16px;
1326
+ display: flex;
1327
+ flex-direction: column;
1328
+ gap: 16px;
1329
+ }
1330
+
1331
+ .wabbit-chat-message {
1332
+ display: flex;
1333
+ flex-direction: column;
1334
+ gap: 4px;
1335
+ }
1336
+
1337
+ .wabbit-chat-message-user {
1338
+ align-items: flex-end;
1339
+ }
1340
+
1341
+ .wabbit-chat-message-assistant {
1342
+ align-items: flex-start;
1343
+ }
1344
+
1345
+ .wabbit-chat-message-system {
1346
+ align-items: center;
1347
+ }
1348
+
1349
+ .wabbit-chat-message-bubble {
1350
+ max-width: 85%;
1351
+ padding: 12px 16px;
1352
+ border-radius: 16px;
1353
+ word-wrap: break-word;
1354
+ white-space: pre-wrap;
1355
+ }
1356
+
1357
+ .wabbit-chat-message-user .wabbit-chat-message-bubble {
1358
+ background: linear-gradient(135deg, var(--wabbit-primary), ${adjustColor(primaryColor, 0.2)});
1359
+ color: white;
1360
+ border-bottom-right-radius: 4px;
1361
+ }
1362
+
1363
+ .wabbit-chat-message-assistant .wabbit-chat-message-bubble {
1364
+ background: var(--wabbit-bg-hover);
1365
+ color: var(--wabbit-text-primary);
1366
+ border: 1px solid var(--wabbit-border-subtle);
1367
+ border-bottom-left-radius: 4px;
1368
+ }
1369
+
1370
+ .wabbit-chat-message-system .wabbit-chat-message-bubble {
1371
+ background: rgba(var(--wabbit-primary-rgb, 99, 102, 241), 0.1);
1372
+ border: 1px solid rgba(var(--wabbit-primary-rgb, 99, 102, 241), 0.3);
1373
+ color: var(--wabbit-primary);
1374
+ text-align: center;
1375
+ font-size: 12px;
1376
+ padding: 8px 12px;
1377
+ }
1378
+
1379
+ .wabbit-chat-message-content {
1380
+ font-size: 14px;
1381
+ line-height: 1.5;
1382
+ }
1383
+
1384
+ /* Typing Indicator */
1385
+ .wabbit-chat-typing {
1386
+ display: flex;
1387
+ gap: 4px;
1388
+ padding: 12px 16px;
1389
+ }
1390
+
1391
+ .wabbit-chat-typing-dot {
1392
+ width: 8px;
1393
+ height: 8px;
1394
+ border-radius: 50%;
1395
+ background: var(--wabbit-text-muted);
1396
+ animation: wabbit-typing 1.4s infinite;
1397
+ }
1398
+
1399
+ .wabbit-chat-typing-dot:nth-child(2) {
1400
+ animation-delay: 0.2s;
1401
+ }
1402
+
1403
+ .wabbit-chat-typing-dot:nth-child(3) {
1404
+ animation-delay: 0.4s;
1405
+ }
1406
+
1407
+ @keyframes wabbit-typing {
1408
+ 0%, 60%, 100% {
1409
+ transform: translateY(0);
1410
+ opacity: 0.7;
1411
+ }
1412
+ 30% {
1413
+ transform: translateY(-10px);
1414
+ opacity: 1;
1415
+ }
1416
+ }
1417
+
1418
+ /* Input Area */
1419
+ .wabbit-chat-input-area {
1420
+ border-top: 1px solid var(--wabbit-border-subtle);
1421
+ padding: 12px;
1422
+ background: var(--wabbit-bg-elevated);
1423
+ }
1424
+
1425
+ .wabbit-chat-input-wrapper {
1426
+ display: flex;
1427
+ gap: 8px;
1428
+ align-items: flex-end;
1429
+ }
1430
+
1431
+ .wabbit-chat-input {
1432
+ flex: 1;
1433
+ resize: none;
1434
+ background: var(--wabbit-bg-hover);
1435
+ border: 1px solid var(--wabbit-border-subtle);
1436
+ border-radius: 12px;
1437
+ padding: 10px 12px;
1438
+ color: var(--wabbit-text-primary);
1439
+ font-size: 14px;
1440
+ font-family: inherit;
1441
+ line-height: 1.5;
1442
+ min-height: 44px;
1443
+ max-height: 120px;
1444
+ transition: border-color 0.2s, box-shadow 0.2s;
1445
+ }
1446
+
1447
+ .wabbit-chat-input:focus {
1448
+ outline: none;
1449
+ border-color: var(--wabbit-primary);
1450
+ box-shadow: 0 0 0 3px rgba(var(--wabbit-primary-rgb, 99, 102, 241), 0.1);
1451
+ }
1452
+
1453
+ .wabbit-chat-input::placeholder {
1454
+ color: var(--wabbit-text-muted);
1455
+ }
1456
+
1457
+ .wabbit-chat-input:disabled {
1458
+ opacity: 0.5;
1459
+ cursor: not-allowed;
1460
+ }
1461
+
1462
+ .wabbit-chat-send-button {
1463
+ width: 44px;
1464
+ height: 44px;
1465
+ border-radius: 12px;
1466
+ background: linear-gradient(135deg, var(--wabbit-primary), ${adjustColor(primaryColor, 0.2)});
1467
+ border: none;
1468
+ color: white;
1469
+ cursor: pointer;
1470
+ display: flex;
1471
+ align-items: center;
1472
+ justify-content: center;
1473
+ transition: opacity 0.2s, transform 0.2s;
1474
+ flex-shrink: 0;
1475
+ }
1476
+
1477
+ .wabbit-chat-send-button:hover:not(:disabled) {
1478
+ opacity: 0.9;
1479
+ transform: scale(1.05);
1480
+ }
1481
+
1482
+ .wabbit-chat-send-button:active:not(:disabled) {
1483
+ transform: scale(0.95);
1484
+ }
1485
+
1486
+ .wabbit-chat-send-button:disabled {
1487
+ opacity: 0.5;
1488
+ cursor: not-allowed;
1489
+ }
1490
+
1491
+ .wabbit-chat-send-icon {
1492
+ width: 20px;
1493
+ height: 20px;
1494
+ }
1495
+
1496
+ /* Scrollbar */
1497
+ .wabbit-chat-messages::-webkit-scrollbar {
1498
+ width: 6px;
1499
+ }
1500
+
1501
+ .wabbit-chat-messages::-webkit-scrollbar-track {
1502
+ background: transparent;
1503
+ }
1504
+
1505
+ .wabbit-chat-messages::-webkit-scrollbar-thumb {
1506
+ background: var(--wabbit-border-subtle);
1507
+ border-radius: 3px;
1508
+ }
1509
+
1510
+ .wabbit-chat-messages::-webkit-scrollbar-thumb:hover {
1511
+ background: var(--wabbit-text-muted);
1512
+ }
1513
+
1514
+ /* Animations */
1515
+ @keyframes wabbit-fade-in {
1516
+ from {
1517
+ opacity: 0;
1518
+ transform: translateY(10px);
1519
+ }
1520
+ to {
1521
+ opacity: 1;
1522
+ transform: translateY(0);
1523
+ }
1524
+ }
1525
+
1526
+ .wabbit-chat-panel {
1527
+ animation: wabbit-fade-in 0.3s ease-out;
1528
+ }
1529
+
1530
+ .wabbit-chat-message {
1531
+ animation: wabbit-fade-in 0.2s ease-out;
1532
+ }
1533
+ `;
1534
+ const style = document.createElement('style');
1535
+ style.id = styleId;
1536
+ style.setAttribute('data-wabbit', 'true');
1537
+ style.textContent = styles;
1538
+ document.head.appendChild(style);
1539
+ }
1540
+ /**
1541
+ * Adjust color brightness
1542
+ */
1543
+ function adjustColor(color, amount) {
1544
+ // Simple color adjustment - convert hex to RGB and adjust
1545
+ const hex = color.replace('#', '');
1546
+ const r = parseInt(hex.substr(0, 2), 16);
1547
+ const g = parseInt(hex.substr(2, 2), 16);
1548
+ const b = parseInt(hex.substr(4, 2), 16);
1549
+ const newR = Math.max(0, Math.min(255, Math.round(r + (255 - r) * amount)));
1550
+ const newG = Math.max(0, Math.min(255, Math.round(g + (255 - g) * amount)));
1551
+ const newB = Math.max(0, Math.min(255, Math.round(b + (255 - b) * amount)));
1552
+ return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`;
1553
+ }
1554
+
1555
+ /**
1556
+ * Chat Widget - Main class that integrates all chat components
1557
+ */
1558
+ class ChatWidget {
1559
+ constructor(config) {
1560
+ this.wsClient = null;
1561
+ this.bubble = null;
1562
+ this.panel = null;
1563
+ this.isOpen = false;
1564
+ this.cleanup = [];
1565
+ this.themeCleanup = null;
1566
+ this.config = config;
1567
+ }
1568
+ /**
1569
+ * Initialize the chat widget
1570
+ */
1571
+ async init() {
1572
+ // Wait for DOM to be ready
1573
+ await new Promise((resolve) => {
1574
+ onDOMReady(() => {
1575
+ resolve();
1576
+ });
1577
+ });
1578
+ // Inject styles with theme configuration
1579
+ injectChatStyles(this.config.primaryColor || '#6366f1', this.config.theme);
1580
+ // Setup theme watcher if theme is 'auto'
1581
+ this.setupThemeWatcher();
1582
+ // Create WebSocket client
1583
+ const wsUrl = this.getWebSocketUrl();
1584
+ this.wsClient = new ChatWebSocketClient(this.config.apiKey || '', wsUrl, null // Will use stored session if available
1585
+ );
1586
+ // Set up event handlers
1587
+ this.setupWebSocketHandlers();
1588
+ // Create bubble
1589
+ this.bubble = new ChatBubble({
1590
+ position: this.config.position || 'bottom-right',
1591
+ onClick: () => this.toggle()
1592
+ });
1593
+ // Create panel
1594
+ this.panel = new ChatPanel({
1595
+ position: this.config.position || 'bottom-right',
1596
+ welcomeMessage: this.config.welcomeMessage,
1597
+ placeholder: this.config.placeholder,
1598
+ onSendMessage: (content) => this.handleSendMessage(content),
1599
+ onClose: () => this.close(),
1600
+ disabled: true
1601
+ });
1602
+ // Render components
1603
+ this.bubble.render();
1604
+ this.panel.render();
1605
+ // Ensure initial state: panel hidden, bubble visible
1606
+ // This prevents state mismatch where panel might be visible but isOpen is false
1607
+ if (this.panel) {
1608
+ this.panel.hide();
1609
+ }
1610
+ if (this.bubble) {
1611
+ this.bubble.show();
1612
+ }
1613
+ this.isOpen = false; // Ensure state is consistent
1614
+ // Handle trigger types
1615
+ this.handleTriggerType();
1616
+ // Connect WebSocket
1617
+ await this.wsClient.connect();
1618
+ }
1619
+ /**
1620
+ * Setup WebSocket event handlers
1621
+ */
1622
+ setupWebSocketHandlers() {
1623
+ if (!this.wsClient)
1624
+ return;
1625
+ this.wsClient.onWelcome = (sessionId, _collectionId, message) => {
1626
+ console.log('[Wabbit] Chat connected:', sessionId);
1627
+ if (this.panel) {
1628
+ this.panel.setDisabled(false);
1629
+ if (message) {
1630
+ this.panel.addSystemMessage(message);
1631
+ }
1632
+ }
1633
+ };
1634
+ this.wsClient.onMessageHistory = (messages) => {
1635
+ console.log('[Wabbit] Loaded message history:', messages.length);
1636
+ if (this.panel) {
1637
+ this.panel.setMessages(messages);
1638
+ }
1639
+ };
1640
+ this.wsClient.onMessage = (message) => {
1641
+ if (this.panel) {
1642
+ this.panel.addMessage(message);
1643
+ this.panel.setWaitingForResponse(false);
1644
+ }
1645
+ };
1646
+ this.wsClient.onError = (error) => {
1647
+ console.error('[Wabbit] Chat error:', error);
1648
+ if (this.panel) {
1649
+ this.panel.addSystemMessage(`Error: ${error}`);
1650
+ this.panel.setWaitingForResponse(false);
1651
+ }
1652
+ };
1653
+ this.wsClient.onStatusChange = (status) => {
1654
+ if (this.panel) {
1655
+ this.panel.setDisabled(status !== 'connected');
1656
+ }
1657
+ };
1658
+ this.wsClient.onDisconnect = () => {
1659
+ if (this.panel) {
1660
+ this.panel.addSystemMessage('Disconnected from chat service');
1661
+ this.panel.setDisabled(true);
1662
+ }
1663
+ };
1664
+ }
1665
+ /**
1666
+ * Handle trigger type configuration
1667
+ */
1668
+ handleTriggerType() {
1669
+ const triggerType = this.config.triggerType || 'button';
1670
+ switch (triggerType) {
1671
+ case 'auto':
1672
+ // Auto-open after delay
1673
+ const delay = this.config.triggerDelay || 5000;
1674
+ setTimeout(() => {
1675
+ this.open();
1676
+ }, delay);
1677
+ break;
1678
+ case 'scroll':
1679
+ // Open after scrolling
1680
+ let hasScrolled = false;
1681
+ const scrollHandler = () => {
1682
+ if (!hasScrolled) {
1683
+ hasScrolled = true;
1684
+ this.open();
1685
+ window.removeEventListener('scroll', scrollHandler);
1686
+ }
1687
+ };
1688
+ window.addEventListener('scroll', scrollHandler);
1689
+ this.cleanup.push(() => {
1690
+ window.removeEventListener('scroll', scrollHandler);
1691
+ });
1692
+ break;
1693
+ }
1694
+ }
1695
+ /**
1696
+ * Get WebSocket URL
1697
+ */
1698
+ getWebSocketUrl() {
1699
+ // Use wsUrl if explicitly provided
1700
+ if (this.config.wsUrl) {
1701
+ return this.config.wsUrl;
1702
+ }
1703
+ // Otherwise, derive from apiUrl
1704
+ const apiUrl = this.config.apiUrl || 'https://api.wabbit.io';
1705
+ const wsProtocol = apiUrl.startsWith('https') ? 'wss' : 'ws';
1706
+ const wsHost = apiUrl.replace(/^https?:\/\//, '').replace(/\/$/, '');
1707
+ return `${wsProtocol}://${wsHost}/ws/chat`;
1708
+ }
1709
+ /**
1710
+ * Handle send message
1711
+ * Made public so EmailCaptureWidget can hook into it
1712
+ */
1713
+ handleSendMessage(content) {
1714
+ if (!this.wsClient)
1715
+ return;
1716
+ // Add user message to panel immediately
1717
+ const userMessage = {
1718
+ id: `user-${Date.now()}`,
1719
+ role: 'user',
1720
+ content,
1721
+ timestamp: new Date()
1722
+ };
1723
+ if (this.panel) {
1724
+ this.panel.addMessage(userMessage);
1725
+ this.panel.setWaitingForResponse(true);
1726
+ }
1727
+ // Send via WebSocket
1728
+ this.wsClient.sendMessage(content);
1729
+ }
1730
+ /**
1731
+ * Open the chat panel
1732
+ */
1733
+ open() {
1734
+ if (this.isOpen)
1735
+ return;
1736
+ this.isOpen = true;
1737
+ if (this.panel) {
1738
+ this.panel.show();
1739
+ }
1740
+ if (this.bubble) {
1741
+ this.bubble.hide();
1742
+ }
1743
+ }
1744
+ /**
1745
+ * Close the chat panel
1746
+ */
1747
+ close() {
1748
+ if (!this.isOpen)
1749
+ return;
1750
+ this.isOpen = false;
1751
+ if (this.panel) {
1752
+ this.panel.hide();
1753
+ }
1754
+ if (this.bubble) {
1755
+ this.bubble.show();
1756
+ }
1757
+ }
1758
+ /**
1759
+ * Toggle chat panel
1760
+ */
1761
+ toggle() {
1762
+ if (this.isOpen) {
1763
+ this.close();
1764
+ }
1765
+ else {
1766
+ this.open();
1767
+ }
1768
+ }
1769
+ /**
1770
+ * Send a message programmatically
1771
+ */
1772
+ sendMessage(content) {
1773
+ this.handleSendMessage(content);
1774
+ }
1775
+ /**
1776
+ * Setup theme watcher for 'auto' theme mode
1777
+ */
1778
+ setupThemeWatcher() {
1779
+ const themeMode = this.config.theme || 'auto';
1780
+ // Only watch for theme changes if theme is 'auto'
1781
+ if (themeMode === 'auto') {
1782
+ this.themeCleanup = watchTheme(() => {
1783
+ // Re-inject styles when theme changes
1784
+ injectChatStyles(this.config.primaryColor || '#6366f1', 'auto');
1785
+ });
1786
+ }
1787
+ }
1788
+ /**
1789
+ * Destroy the widget and cleanup
1790
+ */
1791
+ destroy() {
1792
+ // Run cleanup functions
1793
+ this.cleanup.forEach((fn) => fn());
1794
+ this.cleanup = [];
1795
+ // Cleanup theme watcher
1796
+ if (this.themeCleanup) {
1797
+ this.themeCleanup();
1798
+ this.themeCleanup = null;
1799
+ }
1800
+ // Disconnect WebSocket
1801
+ if (this.wsClient) {
1802
+ this.wsClient.disconnect();
1803
+ this.wsClient = null;
1804
+ }
1805
+ // Remove components
1806
+ if (this.bubble) {
1807
+ this.bubble.destroy();
1808
+ this.bubble = null;
1809
+ }
1810
+ if (this.panel) {
1811
+ this.panel.destroy();
1812
+ this.panel = null;
1813
+ }
1814
+ }
1815
+ }
1816
+
1817
+ var ChatWidget$1 = /*#__PURE__*/Object.freeze({
1818
+ __proto__: null,
1819
+ ChatWidget: ChatWidget
1820
+ });
1821
+
1822
+ /**
1823
+ * Form renderer
1824
+ *
1825
+ * Handles rendering form HTML from form definition
1826
+ */
1827
+ /**
1828
+ * Form renderer class
1829
+ */
1830
+ class FormRenderer {
1831
+ constructor(styles) {
1832
+ this.styles = styles;
1833
+ }
1834
+ /**
1835
+ * Render form HTML
1836
+ */
1837
+ render(formData, theme) {
1838
+ const colors = this.styles.getColors(theme);
1839
+ const fields = formData.fields;
1840
+ let html = '<form class="wabbit-form" data-wabbit-form-instance="true">';
1841
+ fields.forEach((field) => {
1842
+ html += this.renderField(field, colors);
1843
+ });
1844
+ html += `<button
1845
+ type="submit"
1846
+ class="wabbit-submit"
1847
+ >Submit</button>`;
1848
+ html += `<div class="wabbit-message" style="display: none;"></div>`;
1849
+ html += '</form>';
1850
+ return html;
1851
+ }
1852
+ /**
1853
+ * Render a single field
1854
+ */
1855
+ renderField(field, colors) {
1856
+ let html = '<div class="wabbit-field">';
1857
+ // Generate input ID for accessibility (for text, email, number, textarea, select)
1858
+ const needsLabelFor = ['text', 'email', 'number', 'textarea', 'select'].includes(field.type);
1859
+ const inputId = needsLabelFor ? `wabbit-input-${field.id}` : undefined;
1860
+ const labelForAttr = inputId ? ` for="${escapeHtml(inputId)}"` : '';
1861
+ // Use inline style to ensure label color is applied correctly
1862
+ // This matches the legacy embed-form.js behavior
1863
+ // Use !important to ensure color is not overridden by external CSS
1864
+ html += `<label${labelForAttr} style="display: block; font-weight: 600; margin-bottom: 0.5rem; color: ${colors.labelColor} !important; font-size: 0.875rem;">${escapeHtml(field.label)}${field.required ? ' <span style="color: #E07A5F;">*</span>' : ''}</label>`;
1865
+ switch (field.type) {
1866
+ case 'text':
1867
+ case 'email':
1868
+ case 'number':
1869
+ if (inputId) {
1870
+ html += this.renderTextInput(field, colors, inputId);
1871
+ }
1872
+ break;
1873
+ case 'textarea':
1874
+ if (inputId) {
1875
+ html += this.renderTextarea(field, colors, inputId);
1876
+ }
1877
+ break;
1878
+ case 'select':
1879
+ if (inputId) {
1880
+ html += this.renderSelect(field, colors, inputId);
1881
+ }
1882
+ break;
1883
+ case 'radio':
1884
+ html += this.renderRadio(field, colors);
1885
+ break;
1886
+ case 'checkbox':
1887
+ html += this.renderCheckbox(field, colors);
1888
+ break;
1889
+ case 'rating':
1890
+ html += this.renderRating(field, colors);
1891
+ break;
1892
+ }
1893
+ html += '</div>';
1894
+ return html;
1895
+ }
1896
+ /**
1897
+ * Render text input field
1898
+ */
1899
+ renderTextInput(field, _colors, inputId) {
1900
+ return `<input
1901
+ type="${field.type}"
1902
+ id="${escapeHtml(inputId)}"
1903
+ name="${escapeHtml(field.id)}"
1904
+ placeholder="${field.placeholder ? escapeHtml(field.placeholder) : ''}"
1905
+ ${field.required ? 'required' : ''}
1906
+ />`;
1907
+ }
1908
+ /**
1909
+ * Render textarea field
1910
+ */
1911
+ renderTextarea(field, _colors, inputId) {
1912
+ return `<textarea
1913
+ id="${escapeHtml(inputId)}"
1914
+ name="${escapeHtml(field.id)}"
1915
+ placeholder="${field.placeholder ? escapeHtml(field.placeholder) : ''}"
1916
+ ${field.required ? 'required' : ''}
1917
+ rows="4"
1918
+ ></textarea>`;
1919
+ }
1920
+ /**
1921
+ * Render select field
1922
+ */
1923
+ renderSelect(field, _colors, inputId) {
1924
+ if (!field.options || field.options.length === 0) {
1925
+ return '';
1926
+ }
1927
+ let html = `<select id="${escapeHtml(inputId)}" name="${escapeHtml(field.id)}" ${field.required ? 'required' : ''}>`;
1928
+ html += '<option value="">Select...</option>';
1929
+ field.options.forEach((opt) => {
1930
+ html += `<option value="${escapeHtml(opt)}">${escapeHtml(opt)}</option>`;
1931
+ });
1932
+ html += '</select>';
1933
+ return html;
1934
+ }
1935
+ /**
1936
+ * Render radio group
1937
+ */
1938
+ renderRadio(field, _colors) {
1939
+ if (!field.options || field.options.length === 0) {
1940
+ return '';
1941
+ }
1942
+ let html = '<div class="wabbit-radio-group">';
1943
+ field.options.forEach((opt, idx) => {
1944
+ const radioId = `${field.id}_${idx}`;
1945
+ html += `<div>
1946
+ <label>
1947
+ <input
1948
+ type="radio"
1949
+ name="${escapeHtml(field.id)}"
1950
+ value="${escapeHtml(opt)}"
1951
+ id="${escapeHtml(radioId)}"
1952
+ ${field.required && idx === 0 ? 'required' : ''}
1953
+ />
1954
+ <span>${escapeHtml(opt)}</span>
1955
+ </label>
1956
+ </div>`;
1957
+ });
1958
+ html += '</div>';
1959
+ return html;
1960
+ }
1961
+ /**
1962
+ * Render checkbox group
1963
+ */
1964
+ renderCheckbox(field, _colors) {
1965
+ if (!field.options || field.options.length === 0) {
1966
+ return '';
1967
+ }
1968
+ let html = '<div class="wabbit-checkbox-group">';
1969
+ field.options.forEach((opt, idx) => {
1970
+ const checkboxId = `${field.id}_${idx}`;
1971
+ html += `<div>
1972
+ <label>
1973
+ <input
1974
+ type="checkbox"
1975
+ name="${escapeHtml(field.id)}[]"
1976
+ value="${escapeHtml(opt)}"
1977
+ id="${escapeHtml(checkboxId)}"
1978
+ />
1979
+ <span>${escapeHtml(opt)}</span>
1980
+ </label>
1981
+ </div>`;
1982
+ });
1983
+ html += '</div>';
1984
+ return html;
1985
+ }
1986
+ /**
1987
+ * Render rating field
1988
+ */
1989
+ renderRating(field, _colors) {
1990
+ const max = field.max || 5;
1991
+ let html = '<div class="wabbit-rating">';
1992
+ for (let i = 1; i <= max; i++) {
1993
+ const starId = `${field.id}_${i}`;
1994
+ html += `<input
1995
+ type="radio"
1996
+ name="${escapeHtml(field.id)}"
1997
+ value="${i}"
1998
+ id="${escapeHtml(starId)}"
1999
+ ${field.required && i === 1 ? 'required' : ''}
2000
+ />`;
2001
+ html += `<label for="${escapeHtml(starId)}">★</label>`;
2002
+ }
2003
+ html += '</div>';
2004
+ return html;
2005
+ }
2006
+ }
2007
+
2008
+ /**
2009
+ * Form styles management
2010
+ *
2011
+ * Handles theme-aware styling and dynamic style injection
2012
+ */
2013
+ /**
2014
+ * Form styles manager
2015
+ */
2016
+ class FormStyles {
2017
+ constructor(primaryColor, containerId) {
2018
+ this.styleElement = null;
2019
+ this.currentTheme = 'light';
2020
+ // Note: primaryColor is now handled via CSS variables set on container
2021
+ // We keep a reference for fallback/legacy support
2022
+ this._primaryColor = '#C15F3C';
2023
+ /**
2024
+ * Theme color schemes
2025
+ */
2026
+ this.themes = {
2027
+ dark: {
2028
+ formBg: '#1A1816', // Dark base background (matching chat widget)
2029
+ labelColor: '#F5F1ED',
2030
+ inputBg: '#2D2826', // Elevated surface (matching chat widget bg-elevated)
2031
+ inputText: '#F5F1ED',
2032
+ inputBorder: '#3D3935',
2033
+ inputBorderFocus: '#C15F3C',
2034
+ placeholderColor: '#6B6560',
2035
+ radioColor: '#A39C94',
2036
+ ratingInactive: '#3D3935',
2037
+ errorBg: 'rgba(224, 122, 95, 0.1)',
2038
+ errorColor: '#E07A5F',
2039
+ errorBorder: '#E07A5F',
2040
+ successBg: 'rgba(132, 169, 140, 0.1)',
2041
+ successColor: '#84A98C',
2042
+ successBorder: '#84A98C',
2043
+ buttonBg: '#C15F3C',
2044
+ buttonBgHover: '#AB4E2F',
2045
+ },
2046
+ light: {
2047
+ formBg: '#FAF8F6', // Light base background (matching chat widget)
2048
+ labelColor: '#2D2826', // Darker color for better readability (matching legacy embed-form.js)
2049
+ inputBg: '#FFFFFF',
2050
+ inputText: '#2D2826',
2051
+ inputBorder: '#D4CFCA',
2052
+ inputBorderFocus: '#C15F3C',
2053
+ placeholderColor: '#8A847E',
2054
+ radioColor: '#5A5450',
2055
+ ratingInactive: '#D4CFCA',
2056
+ errorBg: 'rgba(193, 84, 56, 0.1)',
2057
+ errorColor: '#C15438',
2058
+ errorBorder: '#C15438',
2059
+ successBg: 'rgba(92, 122, 99, 0.1)',
2060
+ successColor: '#5C7A63',
2061
+ successBorder: '#5C7A63',
2062
+ buttonBg: '#C15F3C',
2063
+ buttonBgHover: '#AB4E2F',
2064
+ },
2065
+ };
2066
+ // Generate unique style ID based on container ID or random
2067
+ this.styleId = containerId
2068
+ ? `wabbit-form-styles-${containerId.replace(/[^a-zA-Z0-9]/g, '-')}`
2069
+ : `wabbit-form-styles-${Math.random().toString(36).substr(2, 9)}`;
2070
+ if (primaryColor) {
2071
+ this._primaryColor = primaryColor;
2072
+ // Update focus colors to use primary color (for fallback)
2073
+ this.themes.dark.inputBorderFocus = primaryColor;
2074
+ this.themes.light.inputBorderFocus = primaryColor;
2075
+ this.themes.dark.buttonBg = primaryColor;
2076
+ this.themes.light.buttonBg = primaryColor;
2077
+ // Calculate hover color (darker by 10%)
2078
+ const hoverColor = this.darkenColor(primaryColor, 10);
2079
+ this.themes.dark.buttonBgHover = hoverColor;
2080
+ this.themes.light.buttonBgHover = hoverColor;
2081
+ }
2082
+ this.injectStyles();
2083
+ }
2084
+ /**
2085
+ * Get current theme colors
2086
+ */
2087
+ getColors(theme) {
2088
+ const effectiveTheme = theme || this.currentTheme;
2089
+ return this.themes[effectiveTheme];
2090
+ }
2091
+ /**
2092
+ * Update theme
2093
+ */
2094
+ updateTheme(theme) {
2095
+ this.currentTheme = theme;
2096
+ this.injectStyles();
2097
+ }
2098
+ /**
2099
+ * Update primary color
2100
+ */
2101
+ updatePrimaryColor(color) {
2102
+ this._primaryColor = color;
2103
+ this.themes.dark.inputBorderFocus = color;
2104
+ this.themes.light.inputBorderFocus = color;
2105
+ this.themes.dark.buttonBg = color;
2106
+ this.themes.light.buttonBg = color;
2107
+ const hoverColor = this.darkenColor(color, 10);
2108
+ this.themes.dark.buttonBgHover = hoverColor;
2109
+ this.themes.light.buttonBgHover = hoverColor;
2110
+ this.injectStyles();
2111
+ }
2112
+ /**
2113
+ * Get primary color (for reference)
2114
+ */
2115
+ getPrimaryColor() {
2116
+ return this._primaryColor;
2117
+ }
2118
+ /**
2119
+ * Darken a color by a percentage
2120
+ */
2121
+ darkenColor(color, percent) {
2122
+ // Simple darken for hex colors
2123
+ if (color.startsWith('#')) {
2124
+ const num = parseInt(color.slice(1), 16);
2125
+ const r = Math.max(0, Math.floor((num >> 16) * (1 - percent / 100)));
2126
+ const g = Math.max(0, Math.floor(((num >> 8) & 0x00FF) * (1 - percent / 100)));
2127
+ const b = Math.max(0, Math.floor((num & 0x0000FF) * (1 - percent / 100)));
2128
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
2129
+ }
2130
+ return color;
2131
+ }
2132
+ /**
2133
+ * Inject styles into document
2134
+ */
2135
+ injectStyles() {
2136
+ // Check if style element already exists (for this instance)
2137
+ this.styleElement = document.getElementById(this.styleId);
2138
+ if (!this.styleElement) {
2139
+ this.styleElement = document.createElement('style');
2140
+ this.styleElement.id = this.styleId;
2141
+ this.styleElement.setAttribute('data-wabbit', 'form-styles');
2142
+ document.head.appendChild(this.styleElement);
2143
+ }
2144
+ const colors = this.getColors();
2145
+ const css = this.generateCSS(colors);
2146
+ this.styleElement.textContent = css;
2147
+ }
2148
+ /**
2149
+ * Generate CSS for form styles
2150
+ */
2151
+ generateCSS(colors) {
2152
+ return `
2153
+ /* Wabbit Form Styles */
2154
+ .wabbit-form {
2155
+ max-width: 600px;
2156
+ margin: 0 auto;
2157
+ font-family: Inter, system-ui, sans-serif;
2158
+ background-color: ${colors.formBg};
2159
+ padding: 1.5rem;
2160
+ border-radius: 0.5rem;
2161
+ }
2162
+
2163
+ .wabbit-field {
2164
+ margin-bottom: 1.5rem;
2165
+ }
2166
+
2167
+ /* Label color is set via inline style in FormRenderer for theme-aware rendering */
2168
+ /* This ensures the correct theme color is applied at render time */
2169
+
2170
+ .wabbit-field input[type="text"],
2171
+ .wabbit-field input[type="email"],
2172
+ .wabbit-field input[type="number"],
2173
+ .wabbit-field textarea,
2174
+ .wabbit-field select {
2175
+ width: 100%;
2176
+ padding: 0.75rem 1rem;
2177
+ border: 1px solid ${colors.inputBorder};
2178
+ border-radius: 0.5rem;
2179
+ font-size: 1rem;
2180
+ background-color: ${colors.inputBg};
2181
+ color: ${colors.inputText};
2182
+ transition: border-color 0.2s, box-shadow 0.2s;
2183
+ box-sizing: border-box;
2184
+ }
2185
+
2186
+ .wabbit-field input[type="text"]:focus,
2187
+ .wabbit-field input[type="email"]:focus,
2188
+ .wabbit-field input[type="number"]:focus,
2189
+ .wabbit-field textarea:focus,
2190
+ .wabbit-field select:focus {
2191
+ outline: none;
2192
+ border-color: var(--wabbit-primary-color);
2193
+ box-shadow: 0 0 0 3px rgba(193, 95, 60, 0.1);
2194
+ }
2195
+
2196
+ .wabbit-field input::placeholder,
2197
+ .wabbit-field textarea::placeholder {
2198
+ color: ${colors.placeholderColor};
2199
+ opacity: 1;
2200
+ }
2201
+
2202
+ .wabbit-field textarea {
2203
+ resize: vertical;
2204
+ }
2205
+
2206
+ .wabbit-field .wabbit-radio-group,
2207
+ .wabbit-field .wabbit-checkbox-group {
2208
+ margin-bottom: 0.5rem;
2209
+ }
2210
+
2211
+ .wabbit-field .wabbit-radio-group label,
2212
+ .wabbit-field .wabbit-checkbox-group label {
2213
+ display: flex;
2214
+ align-items: center;
2215
+ cursor: pointer;
2216
+ color: ${colors.radioColor};
2217
+ margin-bottom: 0.5rem;
2218
+ }
2219
+
2220
+ .wabbit-field input[type="radio"],
2221
+ .wabbit-field input[type="checkbox"] {
2222
+ margin-right: 0.5rem;
2223
+ accent-color: var(--wabbit-primary-color);
2224
+ }
2225
+
2226
+ .wabbit-rating {
2227
+ display: flex;
2228
+ gap: 0.25rem;
2229
+ }
2230
+
2231
+ .wabbit-rating input[type="radio"] {
2232
+ display: none;
2233
+ }
2234
+
2235
+ .wabbit-rating label {
2236
+ cursor: pointer;
2237
+ font-size: 1.5rem;
2238
+ color: ${colors.ratingInactive};
2239
+ transition: color 0.2s;
2240
+ }
2241
+
2242
+ .wabbit-rating label:hover {
2243
+ color: var(--wabbit-primary-color);
2244
+ }
2245
+
2246
+ .wabbit-rating input[type="radio"]:checked ~ label,
2247
+ .wabbit-rating label:has(+ input[type="radio"]:checked) {
2248
+ color: var(--wabbit-primary-color);
2249
+ }
2250
+
2251
+ .wabbit-submit {
2252
+ width: 100%;
2253
+ padding: 0.75rem 1.5rem;
2254
+ background-color: var(--wabbit-primary-color);
2255
+ color: white;
2256
+ border: none;
2257
+ border-radius: 0.5rem;
2258
+ font-size: 1rem;
2259
+ font-weight: 600;
2260
+ cursor: pointer;
2261
+ transition: background-color 0.2s, transform 0.15s;
2262
+ }
2263
+
2264
+ .wabbit-submit:hover:not(:disabled) {
2265
+ background-color: var(--wabbit-primary-color-hover);
2266
+ transform: translateY(-1px);
2267
+ }
2268
+
2269
+ .wabbit-submit:disabled {
2270
+ opacity: 0.6;
2271
+ cursor: not-allowed;
2272
+ }
2273
+
2274
+ .wabbit-message {
2275
+ display: none;
2276
+ margin-top: 1rem;
2277
+ padding: 0.75rem;
2278
+ border-radius: 0.5rem;
2279
+ }
2280
+
2281
+ .wabbit-message.wabbit-message-success {
2282
+ background-color: ${colors.successBg};
2283
+ color: ${colors.successColor};
2284
+ border: 1px solid ${colors.successBorder};
2285
+ }
2286
+
2287
+ .wabbit-message.wabbit-message-error {
2288
+ background-color: ${colors.errorBg};
2289
+ color: ${colors.errorColor};
2290
+ border: 1px solid ${colors.errorBorder};
2291
+ }
2292
+ `;
2293
+ }
2294
+ /**
2295
+ * Cleanup styles
2296
+ */
2297
+ destroy() {
2298
+ if (this.styleElement) {
2299
+ this.styleElement.remove();
2300
+ this.styleElement = null;
2301
+ }
2302
+ }
2303
+ }
2304
+
2305
+ /**
2306
+ * API client for making HTTP requests
2307
+ */
2308
+ /**
2309
+ * API client class
2310
+ */
2311
+ class ApiClient {
2312
+ constructor(options) {
2313
+ this.baseUrl = options.baseUrl.replace(/\/$/, '');
2314
+ this.apiKey = options.apiKey;
2315
+ this.timeout = options.timeout || 30000;
2316
+ }
2317
+ /**
2318
+ * Make a GET request
2319
+ *
2320
+ * @param endpoint - API endpoint path
2321
+ * @param options - Request options (e.g., requireAuth)
2322
+ */
2323
+ async get(endpoint, options) {
2324
+ return this.request('GET', endpoint, undefined, options);
2325
+ }
2326
+ /**
2327
+ * Make a POST request
2328
+ *
2329
+ * @param endpoint - API endpoint path
2330
+ * @param data - Request body data
2331
+ * @param options - Request options (e.g., requireAuth)
2332
+ */
2333
+ async post(endpoint, data, options) {
2334
+ return this.request('POST', endpoint, data, options);
2335
+ }
2336
+ /**
2337
+ * Make a request
2338
+ */
2339
+ async request(method, endpoint, data, requestOptions) {
2340
+ const url = `${this.baseUrl}${endpoint}`;
2341
+ // Build headers
2342
+ const headers = {
2343
+ 'Content-Type': 'application/json',
2344
+ };
2345
+ // Add API key if authentication is required (default: true)
2346
+ // Public endpoints (like form definitions) should set requireAuth: false
2347
+ const requireAuth = requestOptions?.requireAuth !== false; // Default to true
2348
+ if (requireAuth && this.apiKey) {
2349
+ headers['X-API-Key'] = this.apiKey;
2350
+ }
2351
+ // Create abort controller for timeout (more compatible than AbortSignal.timeout)
2352
+ const controller = new AbortController();
2353
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
2354
+ const fetchOptions = {
2355
+ method,
2356
+ headers,
2357
+ mode: 'cors', // Explicitly set CORS mode like the old embed-form.js
2358
+ signal: controller.signal
2359
+ };
2360
+ if (data && method !== 'GET') {
2361
+ fetchOptions.body = JSON.stringify(data);
2362
+ }
2363
+ try {
2364
+ const response = await fetch(url, fetchOptions);
2365
+ // Clear timeout on successful response
2366
+ clearTimeout(timeoutId);
2367
+ if (!response.ok) {
2368
+ let errorText;
2369
+ try {
2370
+ const errorJson = await response.json();
2371
+ // Handle Next.js API route error format
2372
+ if (errorJson.detail) {
2373
+ errorText = errorJson.detail;
2374
+ }
2375
+ else if (errorJson.error) {
2376
+ errorText = errorJson.error;
2377
+ }
2378
+ else if (errorJson.message) {
2379
+ errorText = errorJson.message;
2380
+ }
2381
+ else {
2382
+ errorText = JSON.stringify(errorJson);
2383
+ }
2384
+ }
2385
+ catch {
2386
+ const text = await response.text();
2387
+ // Try to extract meaningful error message from text
2388
+ if (text) {
2389
+ errorText = text;
2390
+ }
2391
+ else {
2392
+ // Fallback to status text only (no HTTP status code)
2393
+ errorText = response.statusText || 'An error occurred';
2394
+ }
2395
+ }
2396
+ return {
2397
+ success: false,
2398
+ error: errorText // Return only the error message, not HTTP status code
2399
+ };
2400
+ }
2401
+ const result = await response.json();
2402
+ return {
2403
+ success: true,
2404
+ data: result
2405
+ };
2406
+ }
2407
+ catch (error) {
2408
+ clearTimeout(timeoutId);
2409
+ // Handle abort (timeout)
2410
+ if (error instanceof Error && error.name === 'AbortError') {
2411
+ return {
2412
+ success: false,
2413
+ error: `Request timeout after ${this.timeout}ms`
2414
+ };
2415
+ }
2416
+ if (error instanceof TypeError && error.message.includes('CORS')) {
2417
+ return {
2418
+ success: false,
2419
+ error: 'CORS error: Please check server configuration'
2420
+ };
2421
+ }
2422
+ return {
2423
+ success: false,
2424
+ error: error instanceof Error ? error.message : 'Unknown error'
2425
+ };
2426
+ }
2427
+ }
2428
+ }
2429
+
2430
+ /**
2431
+ * Form Widget
2432
+ *
2433
+ * Main class for managing form widgets
2434
+ */
2435
+ /**
2436
+ * Form Widget class
2437
+ */
2438
+ class FormWidget {
2439
+ constructor(config) {
2440
+ this.container = null;
2441
+ this.formElement = null;
2442
+ this.currentTheme = 'light';
2443
+ this.themeCleanup = null;
2444
+ this.formObserver = null;
2445
+ this.config = config;
2446
+ // Validate apiKey (required for API client)
2447
+ if (!config.apiKey) {
2448
+ throw new Error('FormWidget requires apiKey. Provide it in FormConfig or WabbitConfig.');
2449
+ }
2450
+ // Initialize API client
2451
+ const apiUrl = config.apiUrl || this.getApiUrlFromScript();
2452
+ this.apiClient = new ApiClient({
2453
+ baseUrl: apiUrl,
2454
+ apiKey: config.apiKey,
2455
+ });
2456
+ // Initialize styles and renderer
2457
+ // Pass containerId to FormStyles to generate unique style ID
2458
+ this.styles = new FormStyles(config.primaryColor, config.containerId);
2459
+ this.renderer = new FormRenderer(this.styles);
2460
+ }
2461
+ /**
2462
+ * Initialize the form widget
2463
+ */
2464
+ init() {
2465
+ onDOMReady(() => {
2466
+ // Determine initial theme based on config
2467
+ const themeMode = this.config.theme || 'auto';
2468
+ if (themeMode === 'auto') {
2469
+ this.currentTheme = detectTheme();
2470
+ }
2471
+ else {
2472
+ this.currentTheme = themeMode;
2473
+ }
2474
+ // Update FormStyles theme to match determined theme
2475
+ this.styles.updateTheme(this.currentTheme);
2476
+ // Setup theme watcher (will handle auto mode or explicit theme)
2477
+ this.setupThemeWatcher();
2478
+ // Load and render form
2479
+ this.loadForm();
2480
+ });
2481
+ }
2482
+ /**
2483
+ * Load and render form
2484
+ */
2485
+ async loadForm() {
2486
+ try {
2487
+ // Find container
2488
+ this.container = this.findContainer();
2489
+ if (!this.container) {
2490
+ throw new Error(`Container not found: ${this.config.containerId || 'data-wabbit-form-id'}`);
2491
+ }
2492
+ // Load form definition (public endpoint, no auth required)
2493
+ const response = await this.apiClient.get(`/api/forms/${this.config.formId}/definition`, { requireAuth: false });
2494
+ if (!response.success || !response.data) {
2495
+ throw new Error(response.error || 'Failed to load form definition');
2496
+ }
2497
+ const formData = response.data;
2498
+ // Render form
2499
+ const html = this.renderer.render(formData, this.currentTheme);
2500
+ this.container.innerHTML = html;
2501
+ // Set CSS variables on container for scoped styling
2502
+ // This allows each form container to have its own primary color
2503
+ // Always set the variables, even if primaryColor is not provided (use default from FormStyles)
2504
+ const primaryColor = this.config.primaryColor || '#C15F3C'; // Default primary color
2505
+ this.container.style.setProperty('--wabbit-primary-color', primaryColor);
2506
+ // Calculate hover color (darker by 10%)
2507
+ const hoverColor = this.calculateHoverColor(primaryColor);
2508
+ this.container.style.setProperty('--wabbit-primary-color-hover', hoverColor);
2509
+ // Debug: Log the color being set (remove in production)
2510
+ console.log(`[Wabbit] Form in container "${this.config.containerId}" using primary color: ${primaryColor}`);
2511
+ // Find form element
2512
+ this.formElement = this.container.querySelector('form');
2513
+ if (!this.formElement) {
2514
+ throw new Error('Form element not found after rendering');
2515
+ }
2516
+ // Attach submit handler
2517
+ this.attachSubmitHandler(formData.settings.successMessage);
2518
+ // Setup rating field interactions
2519
+ this.setupRatingInteractions();
2520
+ // Call onLoad callback
2521
+ if (this.config.onLoad) {
2522
+ this.config.onLoad();
2523
+ }
2524
+ }
2525
+ catch (error) {
2526
+ console.error('[Wabbit] Form load error:', error);
2527
+ this.showError(error instanceof Error ? error.message : 'Failed to load form');
2528
+ if (this.config.onError) {
2529
+ this.config.onError(error instanceof Error ? error : new Error(String(error)));
2530
+ }
2531
+ }
2532
+ }
2533
+ /**
2534
+ * Find container element
2535
+ */
2536
+ findContainer() {
2537
+ // Priority 1: containerId from config
2538
+ if (this.config.containerId) {
2539
+ const element = document.getElementById(this.config.containerId) || document.querySelector(this.config.containerId);
2540
+ if (element) {
2541
+ return element;
2542
+ }
2543
+ }
2544
+ // Priority 2: data-wabbit-form-id attribute (backward compatibility)
2545
+ const dataAttrContainer = document.querySelector(`[data-wabbit-form-id="${this.config.formId}"]`);
2546
+ if (dataAttrContainer) {
2547
+ return dataAttrContainer;
2548
+ }
2549
+ return null;
2550
+ }
2551
+ /**
2552
+ * Attach form submit handler
2553
+ */
2554
+ attachSubmitHandler(successMessage) {
2555
+ if (!this.formElement)
2556
+ return;
2557
+ this.formElement.addEventListener('submit', async (e) => {
2558
+ e.preventDefault();
2559
+ await this.handleSubmit(successMessage);
2560
+ });
2561
+ }
2562
+ /**
2563
+ * Setup rating field interactions
2564
+ */
2565
+ setupRatingInteractions() {
2566
+ if (!this.formElement)
2567
+ return;
2568
+ const ratingInputs = this.formElement.querySelectorAll('.wabbit-rating input[type="radio"]');
2569
+ ratingInputs.forEach((input) => {
2570
+ const label = this.formElement.querySelector(`label[for="${input.id}"]`);
2571
+ if (!label)
2572
+ return;
2573
+ const inputElement = input;
2574
+ // Update star colors when clicked
2575
+ inputElement.addEventListener('change', () => {
2576
+ this.updateRatingStars(inputElement);
2577
+ });
2578
+ // Hover effect
2579
+ label.addEventListener('mouseenter', () => {
2580
+ const colors = this.styles.getColors(this.currentTheme);
2581
+ label.style.color = colors.inputBorderFocus;
2582
+ });
2583
+ label.addEventListener('mouseleave', () => {
2584
+ this.updateRatingStars(inputElement);
2585
+ });
2586
+ });
2587
+ }
2588
+ /**
2589
+ * Update rating star colors
2590
+ */
2591
+ updateRatingStars(checkedInput) {
2592
+ if (!this.formElement)
2593
+ return;
2594
+ const fieldName = checkedInput.name;
2595
+ const colors = this.styles.getColors(this.currentTheme);
2596
+ const allInputs = this.formElement.querySelectorAll(`input[name="${fieldName}"]`);
2597
+ const allLabels = this.formElement.querySelectorAll(`label[for^="${fieldName}_"]`);
2598
+ allInputs.forEach((input) => {
2599
+ const label = Array.from(allLabels).find((l) => l.getAttribute('for') === input.id);
2600
+ if (label) {
2601
+ if (input.checked) {
2602
+ label.style.color = colors.inputBorderFocus;
2603
+ }
2604
+ else {
2605
+ label.style.color = colors.ratingInactive;
2606
+ }
2607
+ }
2608
+ });
2609
+ }
2610
+ /**
2611
+ * Handle form submission
2612
+ */
2613
+ async handleSubmit(successMessage) {
2614
+ if (!this.formElement)
2615
+ return;
2616
+ const submitButton = this.formElement.querySelector('.wabbit-submit');
2617
+ const messageDiv = this.formElement.querySelector('.wabbit-message');
2618
+ if (!submitButton || !messageDiv)
2619
+ return;
2620
+ // Disable submit button
2621
+ submitButton.disabled = true;
2622
+ submitButton.textContent = 'Submitting...';
2623
+ // Hide previous messages
2624
+ messageDiv.style.display = 'none';
2625
+ messageDiv.className = 'wabbit-message';
2626
+ try {
2627
+ // Collect form data
2628
+ const formData = new FormData(this.formElement);
2629
+ const responses = {};
2630
+ for (const [key, value] of formData.entries()) {
2631
+ if (key.endsWith('[]')) {
2632
+ // Checkbox group
2633
+ const fieldId = key.replace('[]', '');
2634
+ if (!responses[fieldId]) {
2635
+ responses[fieldId] = [];
2636
+ }
2637
+ responses[fieldId].push(value);
2638
+ }
2639
+ else {
2640
+ responses[key] = value;
2641
+ }
2642
+ }
2643
+ // Submit form (public endpoint, no auth required)
2644
+ const response = await this.apiClient.post(`/api/forms/${this.config.formId}/submit`, {
2645
+ responses,
2646
+ metadata: {
2647
+ referrer: document.referrer,
2648
+ userAgent: navigator.userAgent,
2649
+ },
2650
+ }, { requireAuth: false });
2651
+ if (response.success && response.data?.success) {
2652
+ // Success
2653
+ const message = response.data.message || successMessage || 'Thank you! Your submission has been received.';
2654
+ this.showSuccess(message, messageDiv);
2655
+ this.formElement.reset();
2656
+ // Call onSubmit callback
2657
+ if (this.config.onSubmit) {
2658
+ this.config.onSubmit(responses, response.data.submissionId);
2659
+ }
2660
+ // Re-enable button after 2 seconds
2661
+ setTimeout(() => {
2662
+ submitButton.disabled = false;
2663
+ submitButton.textContent = 'Submit';
2664
+ }, 2000);
2665
+ }
2666
+ else {
2667
+ // Error - format user-friendly message
2668
+ const rawError = response.data?.error || response.error || 'Failed to submit form';
2669
+ const errorMessage = this.formatErrorMessage(rawError);
2670
+ this.showError(errorMessage, messageDiv);
2671
+ submitButton.disabled = false;
2672
+ submitButton.textContent = 'Submit';
2673
+ if (this.config.onError) {
2674
+ this.config.onError(new Error(rawError));
2675
+ }
2676
+ }
2677
+ }
2678
+ catch (error) {
2679
+ console.error('[Wabbit] Submission error:', error);
2680
+ const rawError = error instanceof Error ? error.message : 'Network error. Please try again.';
2681
+ const errorMessage = this.formatErrorMessage(rawError);
2682
+ this.showError(errorMessage, messageDiv);
2683
+ submitButton.disabled = false;
2684
+ submitButton.textContent = 'Submit';
2685
+ if (this.config.onError) {
2686
+ this.config.onError(error instanceof Error ? error : new Error(String(error)));
2687
+ }
2688
+ }
2689
+ }
2690
+ /**
2691
+ * Format error message to be user-friendly
2692
+ * Removes HTTP status codes and technical details
2693
+ */
2694
+ formatErrorMessage(error) {
2695
+ if (!error)
2696
+ return 'An error occurred. Please try again.';
2697
+ // Remove HTTP status codes (e.g., "HTTP 400", "400 Bad Request")
2698
+ let message = error
2699
+ .replace(/^HTTP\s+\d+\s*/i, '')
2700
+ .replace(/^\d+\s+[A-Z\s]+:\s*/i, '')
2701
+ .replace(/\b\d{3}\s+[A-Z\s]+\b/gi, '')
2702
+ .trim();
2703
+ // If message is empty after cleaning, provide a friendly default
2704
+ if (!message || message.length === 0) {
2705
+ return 'An error occurred. Please try again.';
2706
+ }
2707
+ // Remove common technical prefixes
2708
+ message = message.replace(/^(Error|Failed|Error:)\s*/i, '');
2709
+ return message;
2710
+ }
2711
+ /**
2712
+ * Show success message
2713
+ */
2714
+ showSuccess(message, messageDiv) {
2715
+ const div = messageDiv || this.formElement?.querySelector('.wabbit-message');
2716
+ if (!div)
2717
+ return;
2718
+ div.textContent = message;
2719
+ div.className = 'wabbit-message wabbit-message-success';
2720
+ div.style.display = 'block';
2721
+ }
2722
+ /**
2723
+ * Show error message
2724
+ */
2725
+ showError(message, messageDiv) {
2726
+ if (this.container && !messageDiv) {
2727
+ const colors = this.styles.getColors(this.currentTheme);
2728
+ this.container.innerHTML = `<p style="color: ${colors.errorColor}; padding: 1rem; border: 1px solid ${colors.errorBorder}; border-radius: 0.5rem; background-color: ${colors.errorBg};">${escapeHtml(message)}</p>`;
2729
+ return;
2730
+ }
2731
+ const div = messageDiv || this.formElement?.querySelector('.wabbit-message');
2732
+ if (!div)
2733
+ return;
2734
+ div.textContent = message;
2735
+ div.className = 'wabbit-message wabbit-message-error';
2736
+ div.style.display = 'block';
2737
+ }
2738
+ /**
2739
+ * Setup theme watcher
2740
+ */
2741
+ setupThemeWatcher() {
2742
+ const themeMode = this.config.theme || 'auto';
2743
+ if (themeMode === 'auto') {
2744
+ this.themeCleanup = watchTheme((theme) => {
2745
+ this.currentTheme = theme;
2746
+ this.updateFormTheme();
2747
+ });
2748
+ }
2749
+ else {
2750
+ // Use explicit theme from config
2751
+ this.currentTheme = themeMode;
2752
+ // Update FormStyles to use the explicit theme
2753
+ this.styles.updateTheme(themeMode);
2754
+ }
2755
+ }
2756
+ /**
2757
+ * Update form theme
2758
+ */
2759
+ updateFormTheme() {
2760
+ // Update styles with new theme
2761
+ this.styles.updateTheme(this.currentTheme);
2762
+ // Update rating stars if form is rendered
2763
+ if (this.formElement) {
2764
+ const ratingInputs = this.formElement.querySelectorAll('.wabbit-rating input[type="radio"]');
2765
+ ratingInputs.forEach((input) => {
2766
+ if (input.checked) {
2767
+ this.updateRatingStars(input);
2768
+ }
2769
+ });
2770
+ }
2771
+ }
2772
+ /**
2773
+ * Calculate hover color (darker by 10%)
2774
+ */
2775
+ calculateHoverColor(color) {
2776
+ if (color.startsWith('#')) {
2777
+ const num = parseInt(color.slice(1), 16);
2778
+ const r = Math.max(0, Math.floor((num >> 16) * 0.9));
2779
+ const g = Math.max(0, Math.floor(((num >> 8) & 0x00FF) * 0.9));
2780
+ const b = Math.max(0, Math.floor((num & 0x0000FF) * 0.9));
2781
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
2782
+ }
2783
+ return color;
2784
+ }
2785
+ /**
2786
+ * Get API URL from script tag (for backward compatibility)
2787
+ */
2788
+ getApiUrlFromScript() {
2789
+ const scriptTag = document.currentScript ||
2790
+ document.querySelector('script[src*="embed"]');
2791
+ if (scriptTag && scriptTag instanceof HTMLScriptElement && scriptTag.src) {
2792
+ try {
2793
+ return new URL(scriptTag.src).origin;
2794
+ }
2795
+ catch {
2796
+ // Invalid URL
2797
+ }
2798
+ }
2799
+ return 'http://localhost:3000';
2800
+ }
2801
+ /**
2802
+ * Destroy the form widget
2803
+ */
2804
+ destroy() {
2805
+ if (this.themeCleanup) {
2806
+ this.themeCleanup();
2807
+ this.themeCleanup = null;
2808
+ }
2809
+ if (this.formObserver) {
2810
+ this.formObserver.disconnect();
2811
+ this.formObserver = null;
2812
+ }
2813
+ if (this.styles) {
2814
+ this.styles.destroy();
2815
+ }
2816
+ // Clear container
2817
+ if (this.container) {
2818
+ this.container.innerHTML = '';
2819
+ }
2820
+ this.container = null;
2821
+ this.formElement = null;
2822
+ }
2823
+ }
2824
+
2825
+ var FormWidget$1 = /*#__PURE__*/Object.freeze({
2826
+ __proto__: null,
2827
+ FormWidget: FormWidget
2828
+ });
2829
+
2830
+ /**
2831
+ * Simple event emitter for SDK
2832
+ */
2833
+ /**
2834
+ * Simple event emitter class
2835
+ */
2836
+ class EventEmitter {
2837
+ constructor() {
2838
+ this.events = new Map();
2839
+ }
2840
+ /**
2841
+ * Subscribe to an event
2842
+ */
2843
+ on(event, handler) {
2844
+ if (!this.events.has(event)) {
2845
+ this.events.set(event, []);
2846
+ }
2847
+ this.events.get(event).push(handler);
2848
+ }
2849
+ /**
2850
+ * Unsubscribe from an event
2851
+ */
2852
+ off(event, handler) {
2853
+ const handlers = this.events.get(event);
2854
+ if (handlers) {
2855
+ const index = handlers.indexOf(handler);
2856
+ if (index > -1) {
2857
+ handlers.splice(index, 1);
2858
+ }
2859
+ }
2860
+ }
2861
+ /**
2862
+ * Emit an event
2863
+ */
2864
+ emit(event, ...args) {
2865
+ const handlers = this.events.get(event);
2866
+ if (handlers) {
2867
+ handlers.forEach((handler) => {
2868
+ try {
2869
+ handler(...args);
2870
+ }
2871
+ catch (error) {
2872
+ console.error(`[Wabbit] Error in event handler for "${event}":`, error);
2873
+ }
2874
+ });
2875
+ }
2876
+ }
2877
+ /**
2878
+ * Remove all listeners for an event
2879
+ */
2880
+ removeAllListeners(event) {
2881
+ if (event) {
2882
+ this.events.delete(event);
2883
+ }
2884
+ else {
2885
+ this.events.clear();
2886
+ }
2887
+ }
2888
+ /**
2889
+ * Get listener count for an event
2890
+ */
2891
+ listenerCount(event) {
2892
+ return this.events.get(event)?.length || 0;
2893
+ }
2894
+ }
2895
+
2896
+ /**
2897
+ * Email Capture Modal Component
2898
+ *
2899
+ * Vanilla JavaScript implementation of the email capture modal
2900
+ */
2901
+ class EmailCaptureModal {
2902
+ constructor(config) {
2903
+ this.overlay = null;
2904
+ this.modal = null;
2905
+ this.isOpen = false;
2906
+ this.config = config;
2907
+ }
2908
+ /**
2909
+ * Show the email capture modal
2910
+ */
2911
+ show(onSubmit, onDismiss) {
2912
+ if (this.isOpen) {
2913
+ return;
2914
+ }
2915
+ this.onSubmitCallback = onSubmit;
2916
+ this.onDismissCallback = onDismiss;
2917
+ this.isOpen = true;
2918
+ this.render();
2919
+ }
2920
+ /**
2921
+ * Hide the email capture modal
2922
+ */
2923
+ hide() {
2924
+ if (!this.isOpen) {
2925
+ return;
2926
+ }
2927
+ this.isOpen = false;
2928
+ this.destroy();
2929
+ }
2930
+ /**
2931
+ * Render the modal
2932
+ */
2933
+ render() {
2934
+ const theme = detectTheme();
2935
+ const isDark = theme === 'dark';
2936
+ // Create overlay
2937
+ this.overlay = document.createElement('div');
2938
+ this.overlay.className = 'wabbit-email-capture-overlay';
2939
+ this.overlay.addEventListener('click', (e) => {
2940
+ if (e.target === this.overlay) {
2941
+ this.handleDismiss();
2942
+ }
2943
+ });
2944
+ // Create modal
2945
+ this.modal = document.createElement('div');
2946
+ this.modal.className = `wabbit-email-capture-modal ${isDark ? 'dark' : ''}`;
2947
+ // Build form fields based on config
2948
+ const fields = this.config.fields || ['email'];
2949
+ const formFields = fields.map((field) => this.renderField(field)).join('');
2950
+ // Render modal content
2951
+ this.modal.innerHTML = `
2952
+ <button class="wabbit-email-capture-close ${isDark ? 'dark' : ''}" aria-label="Close">
2953
+ <svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
2954
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
2955
+ </svg>
2956
+ </button>
2957
+ <div class="wabbit-email-capture-content">
2958
+ <h2 class="wabbit-email-capture-title">
2959
+ ${this.config.title || 'Save this conversation?'}
2960
+ </h2>
2961
+ <p class="wabbit-email-capture-description">
2962
+ ${this.config.description || 'Enter your email to continue this chat later'}
2963
+ </p>
2964
+ <form class="wabbit-email-capture-form">
2965
+ ${formFields}
2966
+ <div class="wabbit-email-capture-checkbox">
2967
+ <input type="checkbox" id="wabbit-dont-show-again" />
2968
+ <label for="wabbit-dont-show-again">Don't show this again</label>
2969
+ </div>
2970
+ <div class="wabbit-email-capture-buttons">
2971
+ <button type="submit" class="wabbit-email-capture-button wabbit-email-capture-button-primary">
2972
+ Save Conversation
2973
+ </button>
2974
+ <button type="button" class="wabbit-email-capture-button wabbit-email-capture-button-secondary">
2975
+ Maybe Later
2976
+ </button>
2977
+ </div>
2978
+ </form>
2979
+ </div>
2980
+ `;
2981
+ // Attach event listeners
2982
+ this.attachEventListeners();
2983
+ // Append to DOM
2984
+ this.overlay.appendChild(this.modal);
2985
+ document.body.appendChild(this.overlay);
2986
+ }
2987
+ /**
2988
+ * Render a form field
2989
+ */
2990
+ renderField(field) {
2991
+ const fieldConfig = {
2992
+ email: { label: 'Email', type: 'email', placeholder: 'your@email.com', required: true },
2993
+ name: { label: 'Name', type: 'text', placeholder: 'Your name', required: false },
2994
+ company: { label: 'Company', type: 'text', placeholder: 'Your company', required: false }
2995
+ };
2996
+ const config = fieldConfig[field];
2997
+ return `
2998
+ <div class="wabbit-email-capture-field">
2999
+ <label class="wabbit-email-capture-label" for="wabbit-field-${field}">
3000
+ ${config.label}
3001
+ </label>
3002
+ <input
3003
+ type="${config.type}"
3004
+ id="wabbit-field-${field}"
3005
+ name="${field}"
3006
+ placeholder="${config.placeholder}"
3007
+ class="wabbit-email-capture-input"
3008
+ ${config.required ? 'required' : ''}
3009
+ />
3010
+ <div class="wabbit-email-capture-error" id="wabbit-error-${field}" style="display: none;"></div>
3011
+ </div>
3012
+ `;
3013
+ }
3014
+ /**
3015
+ * Attach event listeners
3016
+ */
3017
+ attachEventListeners() {
3018
+ if (!this.modal)
3019
+ return;
3020
+ // Close button
3021
+ const closeButton = this.modal.querySelector('.wabbit-email-capture-close');
3022
+ if (closeButton) {
3023
+ closeButton.addEventListener('click', () => this.handleDismiss());
3024
+ }
3025
+ // Form submit
3026
+ const form = this.modal.querySelector('form');
3027
+ if (form) {
3028
+ form.addEventListener('submit', (e) => this.handleSubmit(e));
3029
+ }
3030
+ // Maybe Later button
3031
+ const maybeLaterButton = this.modal.querySelector('.wabbit-email-capture-button-secondary');
3032
+ if (maybeLaterButton) {
3033
+ maybeLaterButton.addEventListener('click', () => this.handleDismiss());
3034
+ }
3035
+ }
3036
+ /**
3037
+ * Handle form submission
3038
+ */
3039
+ async handleSubmit(e) {
3040
+ e.preventDefault();
3041
+ if (!this.modal || !this.onSubmitCallback)
3042
+ return;
3043
+ // Get form data
3044
+ const form = this.modal.querySelector('form');
3045
+ if (!form)
3046
+ return;
3047
+ const formData = new FormData(form);
3048
+ const data = {
3049
+ email: formData.get('email') || '',
3050
+ name: formData.get('name') || undefined,
3051
+ company: formData.get('company') || undefined
3052
+ };
3053
+ // Validate email
3054
+ if (!data.email || !this.isValidEmail(data.email)) {
3055
+ this.showError('email', 'Please enter a valid email address');
3056
+ return;
3057
+ }
3058
+ // Get "Don't show again" checkbox
3059
+ const dontShowAgain = form.querySelector('#wabbit-dont-show-again')?.checked || false;
3060
+ // Disable form
3061
+ this.setFormDisabled(true);
3062
+ try {
3063
+ await this.onSubmitCallback(data, dontShowAgain);
3064
+ this.hide();
3065
+ }
3066
+ catch (error) {
3067
+ this.showError('email', error instanceof Error ? error.message : 'Failed to save email');
3068
+ this.setFormDisabled(false);
3069
+ }
3070
+ }
3071
+ /**
3072
+ * Handle dismiss
3073
+ */
3074
+ handleDismiss() {
3075
+ if (this.onDismissCallback) {
3076
+ this.onDismissCallback();
3077
+ }
3078
+ this.hide();
3079
+ }
3080
+ /**
3081
+ * Set form disabled state
3082
+ */
3083
+ setFormDisabled(disabled) {
3084
+ if (!this.modal)
3085
+ return;
3086
+ const inputs = this.modal.querySelectorAll('input, button');
3087
+ inputs.forEach((input) => {
3088
+ input.style.opacity = disabled ? '0.5' : '1';
3089
+ input.style.pointerEvents = disabled ? 'none' : 'auto';
3090
+ });
3091
+ const submitButton = this.modal.querySelector('.wabbit-email-capture-button-primary');
3092
+ if (submitButton) {
3093
+ submitButton.disabled = disabled;
3094
+ submitButton.textContent = disabled ? 'Saving...' : 'Save Conversation';
3095
+ }
3096
+ }
3097
+ /**
3098
+ * Show error message
3099
+ */
3100
+ showError(field, message) {
3101
+ if (!this.modal)
3102
+ return;
3103
+ const errorElement = this.modal.querySelector(`#wabbit-error-${field}`);
3104
+ if (errorElement) {
3105
+ errorElement.textContent = message;
3106
+ errorElement.style.display = 'block';
3107
+ }
3108
+ }
3109
+ /**
3110
+ * Validate email format
3111
+ */
3112
+ isValidEmail(email) {
3113
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3114
+ return emailRegex.test(email);
3115
+ }
3116
+ /**
3117
+ * Destroy the modal
3118
+ */
3119
+ destroy() {
3120
+ if (this.overlay) {
3121
+ this.overlay.remove();
3122
+ this.overlay = null;
3123
+ }
3124
+ this.modal = null;
3125
+ this.isOpen = false;
3126
+ }
3127
+ }
3128
+
3129
+ /**
3130
+ * Email Capture Modal Styles
3131
+ */
3132
+ /**
3133
+ * Inject email capture modal styles
3134
+ */
3135
+ function injectEmailCaptureStyles(primaryColor = '#6366f1') {
3136
+ const styleId = 'wabbit-email-capture-styles';
3137
+ // Remove existing styles if any
3138
+ const existing = document.getElementById(styleId);
3139
+ if (existing) {
3140
+ existing.remove();
3141
+ }
3142
+ const style = document.createElement('style');
3143
+ style.id = styleId;
3144
+ style.setAttribute('data-wabbit', 'email-capture-styles');
3145
+ style.textContent = `
3146
+ /* Email Capture Modal Styles */
3147
+ .wabbit-email-capture-overlay {
3148
+ position: fixed;
3149
+ inset: 0;
3150
+ z-index: 9999;
3151
+ display: flex;
3152
+ align-items: center;
3153
+ justify-content: center;
3154
+ background-color: rgba(0, 0, 0, 0.5);
3155
+ backdrop-filter: blur(4px);
3156
+ animation: wabbit-fade-in 0.2s ease-out;
3157
+ padding: 1rem;
3158
+ box-sizing: border-box;
3159
+ overflow-y: auto;
3160
+ }
3161
+
3162
+ .wabbit-email-capture-modal {
3163
+ position: relative;
3164
+ background: white;
3165
+ border-radius: 0.5rem;
3166
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
3167
+ padding: 1.5rem;
3168
+ width: calc(100% - 2rem);
3169
+ max-width: 28rem;
3170
+ margin: 1rem;
3171
+ animation: wabbit-slide-up 0.3s ease-out;
3172
+ box-sizing: border-box;
3173
+ overflow: hidden;
3174
+ }
3175
+
3176
+ .wabbit-email-capture-modal.dark {
3177
+ background: #1f2937;
3178
+ color: #f9fafb;
3179
+ }
3180
+
3181
+ .wabbit-email-capture-close {
3182
+ position: absolute;
3183
+ top: 1rem;
3184
+ right: 1rem;
3185
+ background: none;
3186
+ border: none;
3187
+ color: #6b7280;
3188
+ cursor: pointer;
3189
+ padding: 0.25rem;
3190
+ display: flex;
3191
+ align-items: center;
3192
+ justify-content: center;
3193
+ border-radius: 0.25rem;
3194
+ transition: all 0.2s;
3195
+ }
3196
+
3197
+ .wabbit-email-capture-close:hover {
3198
+ color: #374151;
3199
+ background-color: #f3f4f6;
3200
+ }
3201
+
3202
+ .wabbit-email-capture-close.dark:hover {
3203
+ color: #d1d5db;
3204
+ background-color: #374151;
3205
+ }
3206
+
3207
+ .wabbit-email-capture-close:disabled {
3208
+ opacity: 0.5;
3209
+ cursor: not-allowed;
3210
+ }
3211
+
3212
+ .wabbit-email-capture-title {
3213
+ font-size: 1.25rem;
3214
+ font-weight: 600;
3215
+ color: #111827;
3216
+ margin-bottom: 0.5rem;
3217
+ }
3218
+
3219
+ .wabbit-email-capture-modal.dark .wabbit-email-capture-title {
3220
+ color: #f9fafb;
3221
+ }
3222
+
3223
+ .wabbit-email-capture-description {
3224
+ font-size: 0.875rem;
3225
+ color: #6b7280;
3226
+ margin-bottom: 1.5rem;
3227
+ }
3228
+
3229
+ .wabbit-email-capture-modal.dark .wabbit-email-capture-description {
3230
+ color: #d1d5db;
3231
+ }
3232
+
3233
+ .wabbit-email-capture-form {
3234
+ display: flex;
3235
+ flex-direction: column;
3236
+ gap: 1rem;
3237
+ width: 100%;
3238
+ box-sizing: border-box;
3239
+ }
3240
+
3241
+ .wabbit-email-capture-field {
3242
+ display: flex;
3243
+ flex-direction: column;
3244
+ gap: 0.5rem;
3245
+ width: 100%;
3246
+ box-sizing: border-box;
3247
+ }
3248
+
3249
+ .wabbit-email-capture-label {
3250
+ font-size: 0.875rem;
3251
+ font-weight: 500;
3252
+ color: #374151;
3253
+ }
3254
+
3255
+ .wabbit-email-capture-modal.dark .wabbit-email-capture-label {
3256
+ color: #d1d5db;
3257
+ }
3258
+
3259
+ .wabbit-email-capture-input {
3260
+ width: 100%;
3261
+ padding: 0.75rem;
3262
+ border: 1px solid #d1d5db;
3263
+ border-radius: 0.375rem;
3264
+ font-size: 0.875rem;
3265
+ transition: all 0.2s;
3266
+ background: white;
3267
+ color: #111827;
3268
+ box-sizing: border-box;
3269
+ }
3270
+
3271
+ .wabbit-email-capture-modal.dark .wabbit-email-capture-input {
3272
+ background: #374151;
3273
+ border-color: #4b5563;
3274
+ color: #f9fafb;
3275
+ }
3276
+
3277
+ .wabbit-email-capture-input:focus {
3278
+ outline: none;
3279
+ border-color: ${primaryColor};
3280
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
3281
+ }
3282
+
3283
+ .wabbit-email-capture-input:disabled {
3284
+ opacity: 0.5;
3285
+ cursor: not-allowed;
3286
+ }
3287
+
3288
+ .wabbit-email-capture-error {
3289
+ font-size: 0.875rem;
3290
+ color: #dc2626;
3291
+ margin-top: 0.25rem;
3292
+ }
3293
+
3294
+ .wabbit-email-capture-checkbox {
3295
+ display: flex;
3296
+ align-items: center;
3297
+ gap: 0.5rem;
3298
+ }
3299
+
3300
+ .wabbit-email-capture-checkbox input[type="checkbox"] {
3301
+ width: 1rem;
3302
+ height: 1rem;
3303
+ accent-color: ${primaryColor};
3304
+ cursor: pointer;
3305
+ }
3306
+
3307
+ .wabbit-email-capture-checkbox label {
3308
+ font-size: 0.875rem;
3309
+ color: #6b7280;
3310
+ cursor: pointer;
3311
+ }
3312
+
3313
+ .wabbit-email-capture-modal.dark .wabbit-email-capture-checkbox label {
3314
+ color: #d1d5db;
3315
+ }
3316
+
3317
+ .wabbit-email-capture-buttons {
3318
+ display: flex;
3319
+ gap: 0.75rem;
3320
+ margin-top: 0.5rem;
3321
+ width: 100%;
3322
+ box-sizing: border-box;
3323
+ }
3324
+
3325
+ .wabbit-email-capture-button {
3326
+ flex: 1;
3327
+ padding: 0.75rem 1.5rem;
3328
+ border: none;
3329
+ border-radius: 0.375rem;
3330
+ font-size: 0.875rem;
3331
+ font-weight: 500;
3332
+ cursor: pointer;
3333
+ transition: all 0.2s;
3334
+ box-sizing: border-box;
3335
+ min-width: 0;
3336
+ }
3337
+
3338
+ .wabbit-email-capture-button-primary {
3339
+ background-color: ${primaryColor};
3340
+ color: white;
3341
+ }
3342
+
3343
+ .wabbit-email-capture-button-primary:hover:not(:disabled) {
3344
+ opacity: 0.9;
3345
+ transform: translateY(-1px);
3346
+ }
3347
+
3348
+ .wabbit-email-capture-button-primary:disabled {
3349
+ opacity: 0.5;
3350
+ cursor: not-allowed;
3351
+ }
3352
+
3353
+ .wabbit-email-capture-button-secondary {
3354
+ background-color: white;
3355
+ color: #374151;
3356
+ border: 1px solid #d1d5db;
3357
+ }
3358
+
3359
+ .wabbit-email-capture-modal.dark .wabbit-email-capture-button-secondary {
3360
+ background-color: #374151;
3361
+ color: #d1d5db;
3362
+ border-color: #4b5563;
3363
+ }
3364
+
3365
+ .wabbit-email-capture-button-secondary:hover:not(:disabled) {
3366
+ background-color: #f9fafb;
3367
+ }
3368
+
3369
+ .wabbit-email-capture-modal.dark .wabbit-email-capture-button-secondary:hover:not(:disabled) {
3370
+ background-color: #4b5563;
3371
+ }
3372
+
3373
+ @keyframes wabbit-fade-in {
3374
+ from {
3375
+ opacity: 0;
3376
+ }
3377
+ to {
3378
+ opacity: 1;
3379
+ }
3380
+ }
3381
+
3382
+ @keyframes wabbit-slide-up {
3383
+ from {
3384
+ opacity: 0;
3385
+ transform: translateY(1rem);
3386
+ }
3387
+ to {
3388
+ opacity: 1;
3389
+ transform: translateY(0);
3390
+ }
3391
+ }
3392
+ `;
3393
+ document.head.appendChild(style);
3394
+ }
3395
+
3396
+ /**
3397
+ * Email Capture Widget
3398
+ *
3399
+ * Manages email capture modal and integrates with chat widget
3400
+ */
3401
+ class EmailCaptureWidget {
3402
+ constructor(config) {
3403
+ this.messageCount = 0;
3404
+ this.emailCaptured = false;
3405
+ this.wsClient = null; // WebSocket client from ChatWidget
3406
+ this.config = config;
3407
+ this.modal = new EmailCaptureModal(config);
3408
+ this.onCaptureCallback = config.onCapture;
3409
+ }
3410
+ /**
3411
+ * Initialize the email capture widget
3412
+ */
3413
+ init() {
3414
+ // Inject styles
3415
+ injectEmailCaptureStyles('#6366f1'); // Default primary color
3416
+ // Check if user has dismissed email capture
3417
+ const dismissed = getStorageItem('wabbit_email_capture_dismissed');
3418
+ if (dismissed === 'true') {
3419
+ console.log('[Wabbit] Email capture dismissed by user');
3420
+ return;
3421
+ }
3422
+ // Check if email already captured
3423
+ const captured = getStorageItem('wabbit_email_captured');
3424
+ if (captured === 'true') {
3425
+ this.emailCaptured = true;
3426
+ console.log('[Wabbit] Email already captured');
3427
+ return;
3428
+ }
3429
+ }
3430
+ /**
3431
+ * Set WebSocket client (from ChatWidget)
3432
+ */
3433
+ setWebSocketClient(wsClient) {
3434
+ this.wsClient = wsClient;
3435
+ }
3436
+ /**
3437
+ * Handle new message (called by ChatWidget)
3438
+ */
3439
+ handleMessage() {
3440
+ if (this.emailCaptured) {
3441
+ return;
3442
+ }
3443
+ // Only count user messages
3444
+ this.messageCount++;
3445
+ const triggerAfter = this.config.triggerAfterMessages || 3;
3446
+ if (this.messageCount >= triggerAfter) {
3447
+ this.showModal();
3448
+ }
3449
+ }
3450
+ /**
3451
+ * Show the email capture modal
3452
+ */
3453
+ showModal() {
3454
+ if (this.emailCaptured) {
3455
+ return;
3456
+ }
3457
+ this.modal.show((data, dontShowAgain) => this.handleEmailSubmit(data, dontShowAgain), () => this.handleDismiss());
3458
+ }
3459
+ /**
3460
+ * Handle email submission
3461
+ */
3462
+ async handleEmailSubmit(data, dontShowAgain) {
3463
+ if (dontShowAgain) {
3464
+ setStorageItem('wabbit_email_capture_dismissed', 'true');
3465
+ }
3466
+ // Send email via WebSocket if available
3467
+ if (this.wsClient && this.wsClient.ws && this.wsClient.ws.readyState === WebSocket.OPEN) {
3468
+ return new Promise((resolve, reject) => {
3469
+ const timeout = setTimeout(() => {
3470
+ reject(new Error('Email capture timeout'));
3471
+ }, 5000);
3472
+ const messageHandler = (event) => {
3473
+ try {
3474
+ const response = JSON.parse(event.data);
3475
+ if (response.type === 'email_confirmed') {
3476
+ clearTimeout(timeout);
3477
+ this.wsClient.ws.removeEventListener('message', messageHandler);
3478
+ if (response.success) {
3479
+ this.emailCaptured = true;
3480
+ setStorageItem('wabbit_email_captured', 'true');
3481
+ // Call callback
3482
+ if (this.onCaptureCallback) {
3483
+ this.onCaptureCallback({
3484
+ email: data.email,
3485
+ name: data.name || '',
3486
+ company: data.company || ''
3487
+ });
3488
+ }
3489
+ resolve();
3490
+ }
3491
+ else {
3492
+ reject(new Error(response.message || 'Failed to save email'));
3493
+ }
3494
+ }
3495
+ }
3496
+ catch (error) {
3497
+ // Not the message we're waiting for, ignore
3498
+ }
3499
+ };
3500
+ this.wsClient.ws.addEventListener('message', messageHandler);
3501
+ // Send email to server
3502
+ this.wsClient.ws.send(JSON.stringify({
3503
+ type: 'provide_email',
3504
+ email: data.email,
3505
+ name: data.name,
3506
+ company: data.company
3507
+ }));
3508
+ });
3509
+ }
3510
+ else {
3511
+ // WebSocket not available, just mark as captured locally
3512
+ this.emailCaptured = true;
3513
+ setStorageItem('wabbit_email_captured', 'true');
3514
+ if (this.onCaptureCallback) {
3515
+ this.onCaptureCallback({
3516
+ email: data.email,
3517
+ name: data.name || '',
3518
+ company: data.company || ''
3519
+ });
3520
+ }
3521
+ }
3522
+ }
3523
+ /**
3524
+ * Handle modal dismiss
3525
+ */
3526
+ handleDismiss() {
3527
+ // User dismissed, but don't mark as dismissed permanently
3528
+ // They can see it again next time
3529
+ }
3530
+ /**
3531
+ * Destroy the widget
3532
+ */
3533
+ destroy() {
3534
+ if (this.modal) {
3535
+ this.modal.destroy();
3536
+ }
3537
+ this.wsClient = null;
3538
+ }
3539
+ }
3540
+
3541
+ var EmailCaptureWidget$1 = /*#__PURE__*/Object.freeze({
3542
+ __proto__: null,
3543
+ EmailCaptureWidget: EmailCaptureWidget
3544
+ });
3545
+
3546
+ /**
3547
+ * Wabbit Embed SDK
3548
+ *
3549
+ * Unified widget library for embedding chat, forms, and email capture
3550
+ * functionality into customer websites.
3551
+ *
3552
+ * @packageDocumentation
3553
+ */
3554
+ // Import Wabbit class and types
3555
+ // Export main API functions as named exports
3556
+ // For UMD: These automatically become properties of the global Wabbit object (Wabbit.init, Wabbit.getInstance, Wabbit.destroy)
3557
+ // For ESM/CJS: Can import { init, getInstance, destroy } from '@wabbit/embed'
3558
+ function init(config) {
3559
+ return Wabbit.init(config);
3560
+ }
3561
+ function getInstance() {
3562
+ return Wabbit.getInstance();
3563
+ }
3564
+ function destroy() {
3565
+ return Wabbit.destroy();
3566
+ }
3567
+ // Create an object as default export (for ESM/CJS: import Wabbit from '@wabbit/embed')
3568
+ // Note: For UMD, we don't use default export, but directly use named exports
3569
+ const WabbitSDK = {
3570
+ init,
3571
+ getInstance,
3572
+ destroy
3573
+ };
3574
+
3575
+ exports.ApiClient = ApiClient;
3576
+ exports.ChatBubble = ChatBubble;
3577
+ exports.ChatPanel = ChatPanel;
3578
+ exports.ChatWebSocketClient = ChatWebSocketClient;
3579
+ exports.ChatWidget = ChatWidget;
3580
+ exports.EmailCaptureModal = EmailCaptureModal;
3581
+ exports.EmailCaptureWidget = EmailCaptureWidget;
3582
+ exports.EventEmitter = EventEmitter;
3583
+ exports.FormRenderer = FormRenderer;
3584
+ exports.FormStyles = FormStyles;
3585
+ exports.FormWidget = FormWidget;
3586
+ exports.SafeStorage = SafeStorage;
3587
+ exports.Wabbit = Wabbit;
3588
+ exports.createElement = createElement;
3589
+ exports.default = WabbitSDK;
3590
+ exports.destroy = destroy;
3591
+ exports.detectTheme = detectTheme;
3592
+ exports.escapeHtml = escapeHtml;
3593
+ exports.getInstance = getInstance;
3594
+ exports.init = init;
3595
+ exports.mergeConfig = mergeConfig;
3596
+ exports.onDOMReady = onDOMReady;
3597
+ exports.storage = storage;
3598
+ exports.validateConfig = validateConfig;
3599
+ exports.watchTheme = watchTheme;
3600
+
3601
+ Object.defineProperty(exports, '__esModule', { value: true });
3602
+
3603
+ }));
3604
+ //# sourceMappingURL=wabbit-embed.umd.min.js.map