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