andy-widget-react 2.0.5

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,4658 @@
1
+ 'use client';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import { useState, useRef, useEffect, useCallback } from 'react';
4
+ import ReactDOM from 'react-dom';
5
+
6
+ /**
7
+ * Design tokens for AndyChat
8
+ * Centralized styling constants and design values
9
+ */
10
+
11
+ // Color tokens
12
+ const colors = {
13
+ // Primary brand colors
14
+ primary: '#3B82F6',
15
+ primaryHover: '#2563EB',
16
+ primaryLight: '#EBF4FF',
17
+
18
+ // Basic colors
19
+ white: '#FFFFFF',
20
+ black: '#000000',
21
+
22
+ // Neutral grays
23
+ gray100: '#F7FAFC',
24
+ gray200: '#EDF2F7',
25
+ gray300: '#E2E8F0',
26
+ gray400: '#CBD5E0',
27
+ gray500: '#A0ADB8',
28
+ gray600: '#718096',
29
+ gray700: '#4A5568',
30
+ gray800: '#2D3748',
31
+ gray900: '#1A202C',
32
+
33
+ // Semantic colors
34
+ success: '#48BB78',
35
+ warning: '#ED8936',
36
+ error: '#F56565',
37
+ info: '#4299E1',
38
+
39
+ // UI semantic colors (mapped to grays for consistency)
40
+ background: '#F9FAFC',
41
+ text: '#2D3748',
42
+ textSecondary: '#4A5568',
43
+ textMuted: '#718096',
44
+ border: '#E2E8F0',
45
+ hover: '#F7FAFC',
46
+
47
+ // Input specific colors
48
+ inputBackground: '#F5F5F5',
49
+ inputButton: '#F0F0F0',
50
+ inputButtonHover: '#E0E0E0',
51
+
52
+ // Chat specific colors
53
+ userMessage: '#3B82F6',
54
+ botMessage: '#FFFFFF',
55
+ userMessageText: '#FFFFFF',
56
+ botMessageText: '#2D3748',
57
+
58
+ // Dark theme colors
59
+ backgroundDark: '#1A1A1A',
60
+ backgroundSecondaryDark: '#2C2C2C',
61
+ backgroundTertiaryDark: '#383838',
62
+ textDark: '#E0E0E0',
63
+ textMutedDark: '#A0A0A0',
64
+ borderDark: '#444444',
65
+ hoverDark: '#4A4A4A'
66
+ };
67
+
68
+ // Typography tokens
69
+ const typography = {
70
+ fontFamily: {
71
+ primary: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
72
+ mono: 'SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
73
+ },
74
+ fontSize: {
75
+ xs: '0.75rem', // 12px
76
+ sm: '0.875rem', // 14px
77
+ base: '1rem', // 16px
78
+ lg: '1.125rem', // 18px
79
+ xl: '1.25rem', // 20px
80
+ '2xl': '1.5rem', // 24px
81
+ '3xl': '1.875rem' // 30px
82
+ },
83
+ fontWeight: {
84
+ normal: '400',
85
+ medium: '500',
86
+ semibold: '600',
87
+ bold: '700'
88
+ },
89
+ lineHeight: {
90
+ tight: '1.25',
91
+ normal: '1.5',
92
+ relaxed: '1.625'
93
+ }
94
+ };
95
+
96
+ // Spacing tokens
97
+ const spacing = {
98
+ 0: '0',
99
+ 1: '0.25rem', // 4px
100
+ 2: '0.5rem', // 8px
101
+ 3: '0.75rem', // 12px
102
+ 4: '1rem', // 16px
103
+ 5: '1.25rem', // 20px
104
+ 6: '1.5rem', // 24px
105
+ 8: '2rem', // 32px
106
+ 10: '2.5rem', // 40px
107
+ 12: '3rem', // 48px
108
+ 16: '4rem', // 64px
109
+ 20: '5rem', // 80px
110
+ 24: '6rem', // 96px
111
+ 32: '8rem' // 128px
112
+ };
113
+
114
+ // Border radius tokens
115
+ const borderRadius = {
116
+ none: '0',
117
+ sm: '0.125rem', // 2px
118
+ base: '0.25rem', // 4px
119
+ md: '0.375rem', // 6px
120
+ lg: '0.5rem', // 8px
121
+ xl: '0.75rem', // 12px
122
+ '2xl': '1rem', // 16px
123
+ full: '9999px'
124
+ };
125
+
126
+ // Shadow tokens
127
+ const shadows = {
128
+ sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
129
+ base: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
130
+ md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
131
+ lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
132
+ xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
133
+ inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)'
134
+ };
135
+
136
+ // Z-index tokens
137
+ const zIndex = {
138
+ dropdown: 1000,
139
+ sticky: 1020,
140
+ fixed: 1030,
141
+ modalBackdrop: 1040,
142
+ modal: 1050,
143
+ popover: 1060,
144
+ tooltip: 1070,
145
+ toast: 1080
146
+ };
147
+
148
+ // Breakpoints for responsive design
149
+ const breakpoints = {
150
+ sm: '640px',
151
+ md: '768px',
152
+ lg: '1024px',
153
+ xl: '1280px',
154
+ '2xl': '1536px'
155
+ };
156
+
157
+ // Animation tokens
158
+ const animation = {
159
+ duration: {
160
+ fast: '150ms',
161
+ normal: '300ms',
162
+ slow: '500ms'
163
+ },
164
+ easing: {
165
+ linear: 'linear',
166
+ easeIn: 'cubic-bezier(0.4, 0, 1, 1)',
167
+ easeOut: 'cubic-bezier(0, 0, 0.2, 1)',
168
+ easeInOut: 'cubic-bezier(0.4, 0, 0.2, 1)'
169
+ }
170
+ };
171
+
172
+ // Chat specific design tokens
173
+ const chat = {
174
+ // Chat bubble dimensions
175
+ bubbleSize: '60px',
176
+
177
+ // Chat window dimensions
178
+ windowWidth: '480px',
179
+ windowHeight: '680px',
180
+ windowMinWidth: '320px',
181
+ windowMinHeight: '400px',
182
+
183
+ // Message styling
184
+ messageMaxWidth: '80%',
185
+ messagePadding: spacing[3],
186
+ messageGap: spacing[2],
187
+
188
+ // Avatar sizing
189
+ avatarSize: '32px',
190
+ avatarSizeSmall: '24px'
191
+ };
192
+
193
+ /**
194
+ * Comprehensive theme object for React packages
195
+ * Provides all design tokens in a structured format ideal for CSS-in-JS solutions
196
+ */
197
+ const theme = {
198
+ colors,
199
+ typography,
200
+ spacing,
201
+ borderRadius,
202
+ shadows,
203
+ zIndex,
204
+ breakpoints,
205
+ animation,
206
+
207
+ // Convenience getters for common patterns
208
+ get primaryColor() {
209
+ return this.colors.primary
210
+ },
211
+
212
+ get fontFamily() {
213
+ return this.typography.fontFamily.primary
214
+ },
215
+
216
+ get baseFontSize() {
217
+ return this.typography.fontSize.base
218
+ },
219
+
220
+ get borderRadiusBase() {
221
+ return this.borderRadius.base
222
+ },
223
+
224
+ get shadowBase() {
225
+ return this.shadows.base
226
+ },
227
+
228
+ // Helper methods for React styling
229
+ spacingValue: value => spacing[value] || value,
230
+ fontSize: size => typography.fontSize[size] || size,
231
+ fontWeight: weight => typography.fontWeight[weight] || weight,
232
+ color: colorName => colors[colorName] || colorName,
233
+ shadow: shadowName => shadows[shadowName] || shadowName,
234
+ radius: radiusName => borderRadius[radiusName] || radiusName,
235
+
236
+ // Responsive breakpoint utilities for CSS-in-JS
237
+ breakpoint: {
238
+ up: size => `@media (min-width: ${breakpoints[size]})`,
239
+ down: size => {
240
+ const breakpointValues = Object.values(breakpoints);
241
+ const breakpointKeys = Object.keys(breakpoints);
242
+ const index = breakpointKeys.indexOf(size);
243
+ if (index > 0) {
244
+ const prevValue = parseInt(breakpointValues[index - 1]) - 1;
245
+ return `@media (max-width: ${prevValue}px)`
246
+ }
247
+ return '@media (max-width: 639px)'
248
+ },
249
+ only: size => {
250
+ const breakpointValues = Object.values(breakpoints);
251
+ const breakpointKeys = Object.keys(breakpoints);
252
+ const index = breakpointKeys.indexOf(size);
253
+ const minWidth = breakpoints[size];
254
+ if (index < breakpointKeys.length - 1) {
255
+ const maxValue = parseInt(breakpointValues[index + 1]) - 1;
256
+ return `@media (min-width: ${minWidth}) and (max-width: ${maxValue}px)`
257
+ }
258
+ return `@media (min-width: ${minWidth})`
259
+ }
260
+ }
261
+ };
262
+
263
+ /**
264
+ * AndyChat Configuration Management
265
+ * Centralized configuration logic, validation, and embed ID detection
266
+ */
267
+
268
+ /**
269
+ * Configuration constants and defaults
270
+ */
271
+ const CONFIG = {
272
+ // Delay in milliseconds before showing the user info form
273
+ FORM_DISPLAY_DELAY_MS: 1000,
274
+
275
+ // Whether to persist session in localStorage
276
+ PERSIST_SESSION: false,
277
+
278
+ // Session storage key
279
+ SESSION_STORAGE_KEY: 'andy-chat-session-id',
280
+
281
+ // API configuration
282
+ DEFAULT_API_BASE_URL: 'http://localhost:3000/api',
283
+ PRODUCTION_API_BASE_URL: 'https://app.andypartner.com/api',
284
+
285
+ // Embed ID detection methods (in order of priority)
286
+ EMBED_ID_SOURCES: [
287
+ 'ANDY_CHATBOT_ID', // Global variable
288
+ 'data-andy-embed-id', // Script tag attribute
289
+ 'data-embed-id', // Alternative script tag attribute
290
+ 'andy-embed-id' // URL parameter or meta tag
291
+ ],
292
+
293
+ // Default timeouts and limits
294
+ API_TIMEOUT_MS: 10000,
295
+ MAX_MESSAGE_LENGTH: 2000,
296
+ MAX_RETRIES: 3,
297
+ RETRY_DELAY_MS: 1000,
298
+
299
+ // UI behavior defaults
300
+ WELCOME_POPUP_DEFAULT_DELAY: 3000,
301
+ TYPING_INDICATOR_DELAY: 500,
302
+ MESSAGE_ANIMATION_DURATION: 300,
303
+
304
+ // Environment detection
305
+ ENVIRONMENTS: {
306
+ DEVELOPMENT: 'development',
307
+ PRODUCTION: 'production'
308
+ }
309
+ };
310
+
311
+ let apiBaseUrlOverride = null;
312
+ let warnedNonLocalHostUsingLocalApi = false;
313
+
314
+ const isLocalhostLikeHostname = hostname => {
315
+ if (!hostname) return false
316
+ return (
317
+ hostname === 'localhost' ||
318
+ hostname === '127.0.0.1' ||
319
+ hostname === '::1' ||
320
+ hostname === '0.0.0.0' ||
321
+ hostname.startsWith('192.168.')
322
+ )
323
+ };
324
+
325
+ const isLocalhostLikeUrl = url => {
326
+ return /:\/\/(localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0|192\.168\.)/i.test(url)
327
+ };
328
+
329
+ const normalizeApiBaseUrlOrNull = url => {
330
+ if (url == null) return null
331
+ if (typeof url !== 'string') return null
332
+ const raw = url.trim();
333
+ if (!raw) return null
334
+
335
+ // Allow same-origin overrides like "/api".
336
+ if (raw.startsWith('/')) return raw.replace(/\/+$/, '')
337
+
338
+ let parsed;
339
+ try {
340
+ parsed = new URL(raw);
341
+ } catch {
342
+ return null
343
+ }
344
+
345
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null
346
+
347
+ // Normalize trailing slash.
348
+ return raw.replace(/\/+$/, '')
349
+ };
350
+
351
+ const getApiUrlFromScriptTag = () => {
352
+ if (typeof document === 'undefined') return null
353
+
354
+ const scripts = document.querySelectorAll('script');
355
+ for (const script of scripts) {
356
+ const src = script.src || '';
357
+ if (!src.includes('andychat') && !src.includes('andy-chat') && !src.includes('bundle')) continue
358
+
359
+ const v = script.getAttribute('data-andy-api-url') || script.getAttribute('data-api-url');
360
+ if (v && v.trim()) return v.trim()
361
+ }
362
+
363
+ // Fallback: check any script tag that has the attribute (covers renamed bundles).
364
+ for (const script of scripts) {
365
+ const v = script.getAttribute('data-andy-api-url') || script.getAttribute('data-api-url');
366
+ if (v && v.trim()) return v.trim()
367
+ }
368
+
369
+ return null
370
+ };
371
+
372
+ const setApiBaseUrl = url => {
373
+ apiBaseUrlOverride = normalizeApiBaseUrlOrNull(url);
374
+ };
375
+
376
+ /**
377
+ * Detects the current environment based on various indicators
378
+ * @returns {string} The detected environment
379
+ */
380
+ const detectEnvironment = () => {
381
+ // Explicit env wins. Only accept our known values.
382
+ if (typeof window !== 'undefined' && window.ANDY_CHAT_ENV) {
383
+ const env = String(window.ANDY_CHAT_ENV).trim().toLowerCase();
384
+ if (env === CONFIG.ENVIRONMENTS.DEVELOPMENT) return CONFIG.ENVIRONMENTS.DEVELOPMENT
385
+ if (env === CONFIG.ENVIRONMENTS.PRODUCTION) return CONFIG.ENVIRONMENTS.PRODUCTION
386
+ }
387
+
388
+ // Hostname-based default.
389
+ if (typeof window !== 'undefined' && window.location) {
390
+ const hostname = window.location.hostname || '';
391
+ if (isLocalhostLikeHostname(hostname)) {
392
+ return CONFIG.ENVIRONMENTS.DEVELOPMENT
393
+ }
394
+ }
395
+
396
+ // Default to production
397
+ return CONFIG.ENVIRONMENTS.PRODUCTION
398
+ };
399
+
400
+ /**
401
+ * Gets the appropriate API base URL based on environment and configuration
402
+ * @returns {string} The API base URL to use
403
+ */
404
+ const getApiBaseUrl = () => {
405
+ if (apiBaseUrlOverride) return apiBaseUrlOverride
406
+
407
+ // Check for explicit API URL override
408
+ if (typeof window !== 'undefined' && window.ANDY_CHAT_API_URL) {
409
+ const normalized = normalizeApiBaseUrlOrNull(window.ANDY_CHAT_API_URL);
410
+ if (normalized) return normalized
411
+ }
412
+
413
+ // Script tag override (for CDN embeds w/o global vars)
414
+ const scriptOverride = getApiUrlFromScriptTag();
415
+ if (scriptOverride) {
416
+ const normalized = normalizeApiBaseUrlOrNull(scriptOverride);
417
+ if (normalized) return normalized
418
+ }
419
+
420
+ // Use environment-specific defaults
421
+ const environment = detectEnvironment();
422
+ const baseUrl = environment === CONFIG.ENVIRONMENTS.DEVELOPMENT ? CONFIG.DEFAULT_API_BASE_URL : CONFIG.PRODUCTION_API_BASE_URL;
423
+
424
+ // Catch misconfig: non-local site trying to call local API.
425
+ if (
426
+ !warnedNonLocalHostUsingLocalApi &&
427
+ typeof window !== 'undefined' &&
428
+ window.location &&
429
+ !isLocalhostLikeHostname(window.location.hostname || '') &&
430
+ isLocalhostLikeUrl(baseUrl)
431
+ ) {
432
+ warnedNonLocalHostUsingLocalApi = true;
433
+ console.warn('AndyChat: resolved a localhost API URL on a non-local hostname. Set window.ANDY_CHAT_API_URL or data-andy-api-url to fix.');
434
+ }
435
+
436
+ return baseUrl
437
+ };
438
+
439
+ /**
440
+ * Extracts embed ID from script tag attributes
441
+ * @param {HTMLScriptElement} scriptTag - The script tag to check
442
+ * @returns {string|null} The embed ID if found
443
+ */
444
+ const getEmbedIdFromScriptTag = scriptTag => {
445
+ if (!scriptTag) return null
446
+
447
+ // Check various attribute names
448
+ const attributes = ['data-andy-embed-id', 'data-embed-id', 'data-chatbot-id', 'andy-embed-id', 'embed-id'];
449
+
450
+ for (const attr of attributes) {
451
+ const value = scriptTag.getAttribute(attr);
452
+ if (value && value.trim()) {
453
+ return value.trim()
454
+ }
455
+ }
456
+
457
+ return null
458
+ };
459
+
460
+ /**
461
+ * Extracts embed ID from URL parameters
462
+ * @returns {string|null} The embed ID if found in URL
463
+ */
464
+ const getEmbedIdFromUrl = () => {
465
+ if (typeof window === 'undefined' || !window.location) return null
466
+
467
+ const urlParams = new URLSearchParams(window.location.search);
468
+ const paramNames = ['andy-embed-id', 'embed-id', 'chatbot-id', 'andy_embed_id'];
469
+
470
+ for (const param of paramNames) {
471
+ const value = urlParams.get(param);
472
+ if (value && value.trim()) {
473
+ return value.trim()
474
+ }
475
+ }
476
+
477
+ return null
478
+ };
479
+
480
+ /**
481
+ * Extracts embed ID from meta tags
482
+ * @returns {string|null} The embed ID if found in meta tags
483
+ */
484
+ const getEmbedIdFromMetaTags = () => {
485
+ if (typeof document === 'undefined') return null
486
+
487
+ const metaNames = ['andy:embed-id', 'andy-embed-id', 'chatbot-embed-id', 'andy:chatbot-id'];
488
+
489
+ for (const name of metaNames) {
490
+ const meta = document.querySelector(`meta[name="${name}"], meta[property="${name}"]`);
491
+ if (meta) {
492
+ const value = meta.getAttribute('content');
493
+ if (value && value.trim()) {
494
+ return value.trim()
495
+ }
496
+ }
497
+ }
498
+
499
+ return null
500
+ };
501
+
502
+ /**
503
+ * Comprehensive embed ID detection with multiple fallback methods
504
+ * @returns {string|null} The detected embed ID or null if not found
505
+ */
506
+ const getEmbedId = () => {
507
+ // Method 1: Check global variable (highest priority)
508
+ if (typeof window !== 'undefined' && window.ANDY_CHATBOT_ID) {
509
+ const id = window.ANDY_CHATBOT_ID;
510
+ if (typeof id === 'string' && id.trim()) {
511
+ return id.trim()
512
+ }
513
+ }
514
+
515
+ // Method 2: Check script tag attributes
516
+ if (typeof document !== 'undefined') {
517
+ // Find the AndyChat script tag
518
+ const scripts = document.querySelectorAll('script');
519
+ for (const script of scripts) {
520
+ // Check if this is likely the AndyChat script
521
+ const src = script.src || '';
522
+ if (src.includes('andychat') || src.includes('andy-chat')) {
523
+ const embedId = getEmbedIdFromScriptTag(script);
524
+ if (embedId) return embedId
525
+ }
526
+ }
527
+
528
+ // Check all script tags as fallback
529
+ for (const script of scripts) {
530
+ const embedId = getEmbedIdFromScriptTag(script);
531
+ if (embedId) return embedId
532
+ }
533
+ }
534
+
535
+ // Method 3: Check URL parameters
536
+ const urlEmbedId = getEmbedIdFromUrl();
537
+ if (urlEmbedId) return urlEmbedId
538
+
539
+ // Method 4: Check meta tags
540
+ const metaEmbedId = getEmbedIdFromMetaTags();
541
+ if (metaEmbedId) return metaEmbedId
542
+
543
+ // Method 5: Check for data attributes on body or html
544
+ if (typeof document !== 'undefined') {
545
+ const body = document.body;
546
+ const html = document.documentElement;
547
+
548
+ for (const element of [body, html]) {
549
+ if (element) {
550
+ const embedId = getEmbedIdFromScriptTag(element);
551
+ if (embedId) return embedId
552
+ }
553
+ }
554
+ }
555
+
556
+ console.warn('AndyChat: No embed ID found. Please ensure ANDY_CHATBOT_ID is set or use data-andy-embed-id attribute.');
557
+ return null
558
+ };
559
+
560
+ /**
561
+ * Validates an embed ID format
562
+ * @param {string} embedId - The embed ID to validate
563
+ * @returns {boolean} Whether the embed ID is valid
564
+ */
565
+ const isValidEmbedId = embedId => {
566
+ if (!embedId || typeof embedId !== 'string') {
567
+ return false
568
+ }
569
+
570
+ const trimmed = embedId.trim();
571
+
572
+ // Check minimum length
573
+ if (trimmed.length < 3) {
574
+ return false
575
+ }
576
+
577
+ // Check maximum length (reasonable limit)
578
+ if (trimmed.length > 100) {
579
+ return false
580
+ }
581
+
582
+ // Check for valid characters (alphanumeric, hyphens, underscores)
583
+ const validPattern = /^[a-zA-Z0-9\-_]+$/;
584
+ if (!validPattern.test(trimmed)) {
585
+ return false
586
+ }
587
+
588
+ return true
589
+ };
590
+
591
+ /**
592
+ * Validates a chatbot configuration object
593
+ * @param {Object} config - The configuration object to validate
594
+ * @returns {Object} Validation result with isValid boolean and errors array
595
+ */
596
+ const validateChatbotConfig = config => {
597
+ const errors = [];
598
+
599
+ if (!config || typeof config !== 'object') {
600
+ return {
601
+ isValid: false,
602
+ errors: ['Configuration must be an object']
603
+ }
604
+ }
605
+
606
+ // Validate required fields
607
+ if (!config.name || typeof config.name !== 'string' || config.name.trim().length === 0) {
608
+ errors.push('Configuration must have a valid name');
609
+ }
610
+
611
+ // Validate optional but important fields
612
+ if (config.brandColor && typeof config.brandColor === 'string') {
613
+ const hexPattern = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
614
+ if (!hexPattern.test(config.brandColor)) {
615
+ errors.push('Brand color must be a valid hex color (e.g., #3B82F6)');
616
+ }
617
+ }
618
+
619
+ if (config.logoUrl && typeof config.logoUrl === 'string') {
620
+ try {
621
+ new URL(config.logoUrl);
622
+ } catch {
623
+ errors.push('Logo URL must be a valid URL');
624
+ }
625
+ }
626
+
627
+ if (config.orientation && !['left', 'right'].includes(config.orientation)) {
628
+ errors.push('Orientation must be either "left" or "right"');
629
+ }
630
+
631
+ if (config.faqs && !Array.isArray(config.faqs)) {
632
+ errors.push('FAQs must be an array');
633
+ } else if (config.faqs) {
634
+ config.faqs.forEach((faq, index) => {
635
+ if (!faq.question || !faq.answer) {
636
+ errors.push(`FAQ item ${index + 1} must have both question and answer`);
637
+ }
638
+ });
639
+ }
640
+
641
+ // Validate welcome popup configuration
642
+ if (config.showWelcomePopup && typeof config.showWelcomePopup === 'object') {
643
+ if (typeof config.showWelcomePopup.enabled !== 'boolean') {
644
+ errors.push('Welcome popup "enabled" must be a boolean');
645
+ }
646
+ if (config.showWelcomePopup.seconds && typeof config.showWelcomePopup.seconds !== 'number') {
647
+ errors.push('Welcome popup "seconds" must be a number');
648
+ }
649
+ }
650
+
651
+ // Validate user info collection configuration
652
+ if (config.collectUserInfo && typeof config.collectUserInfo === 'object') {
653
+ if (typeof config.collectUserInfo.enabled !== 'boolean') {
654
+ errors.push('User info collection "enabled" must be a boolean');
655
+ }
656
+ }
657
+
658
+ return {
659
+ isValid: errors.length === 0,
660
+ errors
661
+ }
662
+ };
663
+
664
+ /**
665
+ * Normalizes and sanitizes a chatbot configuration
666
+ * @param {Object} config - The raw configuration object
667
+ * @returns {Object} The normalized configuration
668
+ */
669
+ const normalizeConfig = config => {
670
+ if (!config || typeof config !== 'object') {
671
+ return {}
672
+ }
673
+
674
+ const normalized = { ...config };
675
+
676
+ // Normalize strings
677
+ if (normalized.name && typeof normalized.name === 'string') {
678
+ normalized.name = normalized.name.trim();
679
+ }
680
+
681
+ if (normalized.headline && typeof normalized.headline === 'string') {
682
+ normalized.headline = normalized.headline.trim();
683
+ }
684
+
685
+ if (normalized.description && typeof normalized.description === 'string') {
686
+ normalized.description = normalized.description.trim();
687
+ }
688
+
689
+ if (normalized.welcomeMessage && typeof normalized.welcomeMessage === 'string') {
690
+ normalized.welcomeMessage = normalized.welcomeMessage.trim();
691
+ }
692
+
693
+ // Normalize brand color
694
+ if (normalized.brandColor && typeof normalized.brandColor === 'string') {
695
+ normalized.brandColor = normalized.brandColor.trim().toUpperCase();
696
+ // Ensure it starts with #
697
+ if (normalized.brandColor && !normalized.brandColor.startsWith('#')) {
698
+ normalized.brandColor = '#' + normalized.brandColor;
699
+ }
700
+ }
701
+
702
+ // Normalize orientation
703
+ if (normalized.orientation && typeof normalized.orientation === 'string') {
704
+ normalized.orientation = normalized.orientation.toLowerCase();
705
+ if (!['left', 'right'].includes(normalized.orientation)) {
706
+ normalized.orientation = 'right'; // Default fallback
707
+ }
708
+ }
709
+
710
+ // Normalize URLs
711
+ if (normalized.logoUrl && typeof normalized.logoUrl === 'string') {
712
+ normalized.logoUrl = normalized.logoUrl.trim();
713
+ }
714
+
715
+ return normalized
716
+ };
717
+
718
+ /**
719
+ * Creates a configuration initialization function for packages
720
+ * @param {string} packageName - Name of the package initializing
721
+ * @returns {Object} Configuration initialization result
722
+ */
723
+ const initializeConfig = (packageName = 'unknown') => {
724
+ // console.log(`AndyChat ${packageName}: Initializing configuration...`)
725
+
726
+ const embedId = getEmbedId();
727
+
728
+ if (!embedId) {
729
+ console.error(`AndyChat ${packageName}: No embed ID found`);
730
+ return {
731
+ success: false,
732
+ error: 'No embed ID found',
733
+ embedId: null,
734
+ environment: detectEnvironment(),
735
+ apiBaseUrl: getApiBaseUrl()
736
+ }
737
+ }
738
+
739
+ if (!isValidEmbedId(embedId)) {
740
+ console.error(`AndyChat ${packageName}: Invalid embed ID format: ${embedId}`);
741
+ return {
742
+ success: false,
743
+ error: 'Invalid embed ID format',
744
+ embedId,
745
+ environment: detectEnvironment(),
746
+ apiBaseUrl: getApiBaseUrl()
747
+ }
748
+ }
749
+
750
+ // console.log(`AndyChat ${packageName}: Found valid embed ID: ${embedId}`)
751
+
752
+ return {
753
+ success: true,
754
+ embedId,
755
+ environment: detectEnvironment(),
756
+ apiBaseUrl: getApiBaseUrl(),
757
+ config: CONFIG
758
+ }
759
+ };
760
+
761
+ /**
762
+ * AndyChat API client
763
+ * Handles all communication with the AndyChat backend API
764
+ */
765
+
766
+
767
+ // Default configuration for the chatbot.
768
+ // This serves as the base structure and default values.
769
+ const DEFAULT_CHATBOT_CONFIG = {
770
+ id: null,
771
+ name: 'AndyChat',
772
+ theme: 'light',
773
+ logoUrl: '',
774
+ headline: 'Chat with our AI',
775
+ brandColor: '#3B82F6',
776
+ description: 'Ask any question and our AI will answer!',
777
+ orientation: 'right',
778
+ bubbleStyle: 'icon', // 'icon' = circle only, 'pill' = with label
779
+ bubbleLabel: null, // Custom label text (null = use default based on defaultLang)
780
+ useBrandColor: true, // Whether to apply brand color to bubble/messages (default: true)
781
+ defaultLang: 'en', // Default language for the chatbot
782
+ showBranding: true, // Whether to show "Powered by Andy" footer (default: true)
783
+ showWelcomePopup: {
784
+ enabled: false,
785
+ seconds: 3,
786
+ message: 'Welcome! How can I help you today?' // Default welcome popup message
787
+ },
788
+ collectUserInfo: {
789
+ enabled: false,
790
+ message: "Let's get your name and email so we can follow up with you later."
791
+ },
792
+ welcomeMessage: 'Hi there! I am the AI Assistant. How can I help you today?',
793
+ predefinedQuestions: [],
794
+ faqs: []
795
+ };
796
+
797
+ /**
798
+ * Get default bubble label based on language
799
+ * @param {string} lang - Language code ('en' or 'es')
800
+ * @returns {string} Default bubble label
801
+ */
802
+ const getDefaultBubbleLabel = lang => {
803
+ return lang === 'es' ? 'Pregunta IA' : 'Ask AI'
804
+ };
805
+
806
+ /**
807
+ * @typedef {Object} FAQItem
808
+ * @property {string} question - The FAQ question
809
+ * @property {string} answer - The answer to the FAQ question
810
+ */
811
+
812
+ /**
813
+ * @typedef {Object} ChatbotConfigWelcomePopup
814
+ * @property {boolean} enabled - Whether the welcome popup is enabled.
815
+ * @property {number} seconds - Delay in seconds before showing the popup.
816
+ * @property {string} [message] - Custom message for the welcome popup.
817
+ */
818
+
819
+ /**
820
+ * @typedef {Object} ChatbotConfigCollectUserInfo
821
+ * @property {boolean} enabled - Whether user info collection is enabled.
822
+ * @property {string} [message] - Custom message for the user info collection prompt.
823
+ */
824
+
825
+ /**
826
+ * @typedef {Object} ChatbotConfig
827
+ * @property {string | null} id - The chatbot ID from the server.
828
+ * @property {string} name - The name of the chatbot or organization.
829
+ * @property {string} theme - UI theme (e.g., 'light', 'dark').
830
+ * @property {string} logoUrl - URL for the chatbot logo.
831
+ * @property {string} headline - Headline text for the chatbot.
832
+ * @property {string} brandColor - Theme color in hex format.
833
+ * @property {string} description - Description or subtitle for the chatbot.
834
+ * @property {string} orientation - Chat widget orientation (e.g., 'right', 'left').
835
+ * @property {boolean} showBranding - Whether to show "Powered by Andy" footer.
836
+ * @property {ChatbotConfigWelcomePopup} showWelcomePopup - Settings for the welcome popup.
837
+ * @property {ChatbotConfigCollectUserInfo} collectUserInfo - Settings for user info collection.
838
+ * @property {string} welcomeMessage - The main welcome message text for the chat window.
839
+ * @property {Array<{order: number, question: string}>} predefinedQuestions - Array of predefined questions.
840
+ * @property {FAQItem[]} faqs - Array of mapped FAQ items.
841
+ */
842
+
843
+ /**
844
+ * @typedef {Object} MessageResponse
845
+ * @property {string} id - Unique message ID
846
+ * @property {string} text - The bot's response text
847
+ * @property {string} type - Message type (usually "text")
848
+ * @property {string} createdAt - Timestamp of when the message was created
849
+ */
850
+
851
+ /**
852
+ * Maps raw API data to the internal ChatbotConfig structure.
853
+ * This function centralizes the logic for interpreting API responses and applying defaults.
854
+ * @param {object} apiData - The raw data object from the API response.
855
+ * @param {ChatbotConfig} defaultConfig - The default configuration object.
856
+ * @returns {ChatbotConfig} The fully processed chatbot configuration.
857
+ * @private
858
+ */
859
+ const _mapApiDataToChatbotConfig = (apiData, defaultConfig) => {
860
+ if (
861
+ !apiData ||
862
+ typeof apiData !== 'object' ||
863
+ Array.isArray(apiData) ||
864
+ !apiData.chatbot ||
865
+ typeof apiData.chatbot !== 'object' ||
866
+ Array.isArray(apiData.chatbot) ||
867
+ !isValidEmbedId(apiData.chatbot.id)
868
+ ) {
869
+ throw new Error('Invalid chatbot configuration response.')
870
+ }
871
+
872
+ const { chatbot, faqs: rawFaqs } = apiData;
873
+ const appearance = chatbot.appearance || {};
874
+ const behavior = chatbot.behavior || {};
875
+
876
+ // Start with a clone of the default config
877
+ const config = { ...defaultConfig };
878
+
879
+ // Override with server values, using nullish coalescing for safety
880
+ config.id = chatbot.id ?? defaultConfig.id;
881
+ config.name = chatbot.name ?? defaultConfig.name;
882
+ config.defaultLang = chatbot.defaultLang ?? defaultConfig.defaultLang;
883
+ config.theme = appearance.theme ?? defaultConfig.theme;
884
+ config.logoUrl = appearance.logoUrl ?? defaultConfig.logoUrl;
885
+ config.headline = appearance.headline ?? defaultConfig.headline;
886
+ config.brandColor = appearance.brandColor ?? defaultConfig.brandColor;
887
+ config.description = appearance.description ?? defaultConfig.description;
888
+ config.orientation = appearance.orientation ?? defaultConfig.orientation;
889
+ config.welcomeMessage = appearance.welcomeMessage ?? defaultConfig.welcomeMessage;
890
+ config.bubbleStyle = appearance.bubbleStyle ?? defaultConfig.bubbleStyle;
891
+ config.bubbleLabel = appearance.bubbleLabel ?? defaultConfig.bubbleLabel;
892
+ config.useBrandColor = appearance.useBrandColor ?? defaultConfig.useBrandColor;
893
+ config.showBranding = chatbot.showBranding ?? defaultConfig.showBranding;
894
+
895
+ // Handle showWelcomePopup (expected to be an object from API)
896
+ if (typeof behavior.showWelcomePopup === 'object' && behavior.showWelcomePopup !== null) {
897
+ config.showWelcomePopup = {
898
+ ...defaultConfig.showWelcomePopup, // Start with defaults
899
+ ...behavior.showWelcomePopup // Spread API object, overriding defaults
900
+ };
901
+ } else {
902
+ // If not a valid object, use the default object entirely
903
+ config.showWelcomePopup = { ...defaultConfig.showWelcomePopup };
904
+ }
905
+ // Ensure 'message' field for popup has a default if not provided by API and popup is enabled
906
+ if (config.showWelcomePopup.enabled && typeof config.showWelcomePopup.message === 'undefined') {
907
+ config.showWelcomePopup.message = defaultConfig.showWelcomePopup.message;
908
+ }
909
+
910
+ // The public Widget has no durable visitor-information submission contract yet.
911
+ // Keep collection disabled instead of presenting a form whose data cannot be persisted.
912
+ config.collectUserInfo = {
913
+ enabled: false,
914
+ message: behavior.collectUserInfo?.message ?? defaultConfig.collectUserInfo.message
915
+ };
916
+
917
+ // Process predefinedQuestions
918
+ config.predefinedQuestions = []; // Initialize/reset array
919
+ if (behavior.predefinedQuestions && Array.isArray(behavior.predefinedQuestions)) {
920
+ config.predefinedQuestions = behavior.predefinedQuestions
921
+ .filter(q => q && typeof q.question === 'string') // Ensure valid questions
922
+ .sort((a, b) => (a.order || 0) - (b.order || 0)); // Sort by order
923
+ }
924
+
925
+ // Process and map FAQs
926
+ config.faqs = []; // Initialize/reset faqs array from default
927
+ if (rawFaqs && Array.isArray(rawFaqs)) {
928
+ config.faqs = rawFaqs
929
+ .map(faq => ({
930
+ question: faq.question ?? faq.title,
931
+ answer: faq.answer ?? faq.content
932
+ }))
933
+ .filter(
934
+ faq =>
935
+ typeof faq.question === 'string' &&
936
+ faq.question.trim().length > 0 &&
937
+ typeof faq.answer === 'string' &&
938
+ faq.answer.trim().length > 0
939
+ )
940
+ .map(faq => ({ question: faq.question.trim(), answer: faq.answer.trim() }));
941
+ }
942
+
943
+ return config
944
+ };
945
+
946
+ /**
947
+ * Fetches chatbot configuration from the API
948
+ * @param {string} embedId - The chatbot embed ID
949
+ * @returns {Promise<ChatbotConfig>} The chatbot configuration
950
+ */
951
+ const fetchChatbotConfig = async embedId => {
952
+ try {
953
+ const apiBaseUrl = getApiBaseUrl();
954
+ const response = await fetch(`${apiBaseUrl}/chatbot/${embedId}`);
955
+ if (!response.ok) {
956
+ // If response is not OK, but we get JSON, try to parse it for an error message
957
+ let apiErrorMsg = `Failed to load chatbot config: ${response.status}`;
958
+ try {
959
+ const errorData = await response.json();
960
+ if (errorData && errorData.error) {
961
+ apiErrorMsg = errorData.error;
962
+ } else if (errorData && errorData.message) {
963
+ apiErrorMsg = errorData.message;
964
+ }
965
+ } catch (e) {
966
+ // Ignore if parsing errorData fails, use original status-based message
967
+ }
968
+ throw new Error(apiErrorMsg)
969
+ }
970
+ const apiData = await response.json();
971
+ // console.log('API data22:', apiData)
972
+
973
+ // Use the mapper to process the API data and apply defaults
974
+ const config = _mapApiDataToChatbotConfig(apiData, DEFAULT_CHATBOT_CONFIG);
975
+
976
+ // console.log('Chatbot config:', config)
977
+ return config
978
+ } catch (error) {
979
+ console.error('AndyChat: Failed to load configuration.');
980
+ throw error
981
+ }
982
+ };
983
+
984
+ /**
985
+ * Sends a user message to the API
986
+ * @param {string} embedId - The chatbot embed ID
987
+ * @param {string} sessionId - The current session ID
988
+ * @param {string} message - The user's message
989
+ * @returns {Promise<string>} The bot's response text
990
+ */
991
+ const sendMessage = async (embedId, sessionId, message) => {
992
+ try {
993
+ const apiBaseUrl = getApiBaseUrl();
994
+ const response = await fetch(`${apiBaseUrl}/messages`, {
995
+ method: 'POST',
996
+ headers: { 'Content-Type': 'application/json' },
997
+ body: JSON.stringify({
998
+ chatbotId: embedId,
999
+ sessionId: sessionId,
1000
+ currentUserMessage: {
1001
+ role: 'user',
1002
+ content: message
1003
+ }
1004
+ })
1005
+ });
1006
+
1007
+ if (response.ok) {
1008
+ const data = await response.json();
1009
+ if (data.error || data.limitStatus) {
1010
+ throw new Error(data.error || 'Chat is temporarily unavailable.')
1011
+ }
1012
+ if (typeof data.botResponse !== 'string' || !data.botResponse.trim()) {
1013
+ throw new Error('Unexpected response format from server.')
1014
+ }
1015
+ return data.botResponse
1016
+ } else {
1017
+ // Handle HTTP errors (e.g., 500, 404)
1018
+ let errorData;
1019
+ try {
1020
+ errorData = await response.json();
1021
+ } catch (e) {
1022
+ // If parsing error JSON fails, use status text
1023
+ // Also, if response.text() is available and gives more info, consider using it.
1024
+ const responseText = response.text ? await response.text() : '';
1025
+ throw new Error(response.statusText || `Server returned an error. ${responseText}`.trim())
1026
+ }
1027
+ throw new Error(errorData.error || errorData.message || response.statusText || 'Server returned an error.')
1028
+ }
1029
+ } catch (error) {
1030
+ console.error('AndyChat: Message request failed.');
1031
+ throw error
1032
+ }
1033
+ };
1034
+
1035
+ /**
1036
+ * Visitor information collection is disabled until Andy exposes a durable public submission
1037
+ * contract. This explicit failure prevents a Widget from reporting a mocked success.
1038
+ * @param {string} embedId - The chatbot embed ID.
1039
+ * @param {string} sessionId - The current session ID.
1040
+ * @param {object} userInfo - User information.
1041
+ * @param {string} userInfo.name - User's name.
1042
+ * @param {string} userInfo.email - User's email.
1043
+ * @returns {Promise<never>}
1044
+ */
1045
+ const submitUserInfo = async (embedId, sessionId, userInfo) => {
1046
+ throw new Error('Visitor information collection is not supported by this Widget release.')
1047
+ };
1048
+
1049
+ /**
1050
+ * AndyChat utility functions
1051
+ * Common utility functions used across the AndyChat system
1052
+ */
1053
+
1054
+ /**
1055
+ * Generate a unique session ID
1056
+ * @returns {string} A unique session ID
1057
+ */
1058
+ const generateSessionId = () => {
1059
+ if (crypto && crypto.randomUUID) {
1060
+ return crypto.randomUUID()
1061
+ } else {
1062
+ // Fallback for environments without crypto.randomUUID
1063
+ return `andy-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`
1064
+ }
1065
+ };
1066
+
1067
+ /**
1068
+ * Sanitize URL to prevent XSS attacks
1069
+ * @param {string} url - The URL to sanitize
1070
+ * @returns {string} The sanitized URL
1071
+ */
1072
+ const sanitizeUrl = url => {
1073
+ if (!url) return '#'
1074
+
1075
+ // Remove any potential javascript: or data: schemes
1076
+ const trimmedUrl = url.trim();
1077
+ const lowerUrl = trimmedUrl.toLowerCase();
1078
+
1079
+ // Allow http, https, mailto, and tel protocols
1080
+ if (
1081
+ lowerUrl.startsWith('http://') ||
1082
+ lowerUrl.startsWith('https://') ||
1083
+ lowerUrl.startsWith('mailto:') ||
1084
+ lowerUrl.startsWith('tel:')
1085
+ ) {
1086
+ return trimmedUrl
1087
+ }
1088
+
1089
+ // If no protocol specified, assume https for regular URLs
1090
+ if (!lowerUrl.includes(':') && !lowerUrl.startsWith('//')) {
1091
+ return 'https://' + trimmedUrl
1092
+ }
1093
+
1094
+ // For any other protocol, return a safe fallback
1095
+ return '#'
1096
+ };
1097
+
1098
+ /**
1099
+ * Parse markdown text to HTML
1100
+ * @param {string} text - The markdown text to parse
1101
+ * @returns {string} The parsed HTML
1102
+ */
1103
+ const parseMarkdown = text => {
1104
+ if (!text) return ''
1105
+
1106
+ let html = text;
1107
+
1108
+ // Handle links first (before bold/italic to avoid conflicts)
1109
+ // Match [text](url) format
1110
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, linkText, url) => {
1111
+ const sanitizedUrl = sanitizeUrl(url);
1112
+ const isExternal = sanitizedUrl.startsWith('http://') || sanitizedUrl.startsWith('https://');
1113
+ const targetAttr = isExternal ? ' target="_blank" rel="noopener noreferrer"' : '';
1114
+ return `<a href="${sanitizedUrl}" class="andy-message-link"${targetAttr}>${linkText}</a>`
1115
+ });
1116
+
1117
+ // Handle bold text (**text** or __text__)
1118
+ html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
1119
+ html = html.replace(/__(.*?)__/g, '<strong>$1</strong>');
1120
+
1121
+ // Handle italic text (*text* or _text_) - be careful not to match list asterisks
1122
+ html = html.replace(/\*([^*\n]+)\*/g, '<em>$1</em>');
1123
+ html = html.replace(/_([^_\n]+)_/g, '<em>$1</em>');
1124
+
1125
+ // Split into sections by double newlines for paragraphs
1126
+ const sections = html.split('\n\n');
1127
+ const processedSections = [];
1128
+
1129
+ for (let section of sections) {
1130
+ section = section.trim();
1131
+ if (!section) continue
1132
+
1133
+ const lines = section.split('\n');
1134
+ let inList = false;
1135
+ let listType = null;
1136
+ let listItems = [];
1137
+ let paragraphLines = [];
1138
+
1139
+ for (let line of lines) {
1140
+ line = line.trim();
1141
+
1142
+ // Check if it's a heading (# ## ### #### ##### ######)
1143
+ if (/^#{1,6}\s+/.test(line)) {
1144
+ // If we were in a list or paragraph, close it
1145
+ if (paragraphLines.length > 0) {
1146
+ processedSections.push('<p>' + paragraphLines.join('<br>') + '</p>');
1147
+ paragraphLines = [];
1148
+ }
1149
+ if (inList) {
1150
+ if (listType === 'ol') {
1151
+ processedSections.push(`<ol>${listItems.join('')}</ol>`);
1152
+ } else {
1153
+ processedSections.push(`<ul>${listItems.join('')}</ul>`);
1154
+ }
1155
+ inList = false;
1156
+ listType = null;
1157
+ listItems = [];
1158
+ }
1159
+
1160
+ // Determine heading level and extract content
1161
+ const match = line.match(/^(#{1,6})\s+(.+)$/);
1162
+ if (match) {
1163
+ const level = match[1].length; // Number of # characters
1164
+ const content = match[2].trim();
1165
+ processedSections.push(`<h${level}>${content}</h${level}>`);
1166
+ }
1167
+ }
1168
+ // Check if it's a numbered list item
1169
+ else if (/^\d+\.\s+/.test(line)) {
1170
+ // If we were in a different list type or paragraph, close it
1171
+ if (paragraphLines.length > 0) {
1172
+ processedSections.push('<p>' + paragraphLines.join('<br>') + '</p>');
1173
+ paragraphLines = [];
1174
+ }
1175
+ if (inList && listType !== 'ol') {
1176
+ processedSections.push(`<ul>${listItems.join('')}</ul>`);
1177
+ listItems = [];
1178
+ }
1179
+
1180
+ inList = true;
1181
+ listType = 'ol';
1182
+ const content = line.replace(/^\d+\.\s+/, '');
1183
+ listItems.push(`<li>${content}</li>`);
1184
+ }
1185
+ // Check if it's a bullet point
1186
+ else if (/^\*\s+/.test(line)) {
1187
+ // If we were in a different list type or paragraph, close it
1188
+ if (paragraphLines.length > 0) {
1189
+ processedSections.push('<p>' + paragraphLines.join('<br>') + '</p>');
1190
+ paragraphLines = [];
1191
+ }
1192
+ if (inList && listType !== 'ul') {
1193
+ processedSections.push(`<ol>${listItems.join('')}</ol>`);
1194
+ listItems = [];
1195
+ }
1196
+
1197
+ inList = true;
1198
+ listType = 'ul';
1199
+ const content = line.replace(/^\*\s+/, '');
1200
+ listItems.push(`<li>${content}</li>`);
1201
+ }
1202
+ // Regular paragraph line
1203
+ else {
1204
+ // If we were in a list, close it
1205
+ if (inList) {
1206
+ if (listType === 'ol') {
1207
+ processedSections.push(`<ol>${listItems.join('')}</ol>`);
1208
+ } else {
1209
+ processedSections.push(`<ul>${listItems.join('')}</ul>`);
1210
+ }
1211
+ inList = false;
1212
+ listType = null;
1213
+ listItems = [];
1214
+ }
1215
+
1216
+ if (line) {
1217
+ paragraphLines.push(line);
1218
+ }
1219
+ }
1220
+ }
1221
+
1222
+ // Close any remaining open elements
1223
+ if (inList) {
1224
+ if (listType === 'ol') {
1225
+ processedSections.push(`<ol>${listItems.join('')}</ol>`);
1226
+ } else {
1227
+ processedSections.push(`<ul>${listItems.join('')}</ul>`);
1228
+ }
1229
+ } else if (paragraphLines.length > 0) {
1230
+ processedSections.push('<p>' + paragraphLines.join('<br>') + '</p>');
1231
+ }
1232
+ }
1233
+
1234
+ return processedSections.join('')
1235
+ };
1236
+
1237
+ /**
1238
+ * Create greeting message from config
1239
+ * @param {Object} config - The chatbot configuration
1240
+ * @returns {string|null} The greeting message or null if no config
1241
+ */
1242
+ const createGreetingMessage = config => {
1243
+ if (!config) return null
1244
+
1245
+ // Prioritize using config.welcomeMessage if it is a non-empty string
1246
+ if (typeof config.welcomeMessage === 'string' && config.welcomeMessage.trim() !== '') {
1247
+ return config.welcomeMessage
1248
+ }
1249
+
1250
+ // Fallback to name-based message if config.name exists and welcomeMessage wasn't a usable string
1251
+ // This also covers the case where config.welcomeMessage might be boolean `true`
1252
+ // and we want to show a default greeting based on name.
1253
+ if (config.name) {
1254
+ return `Hi there 👋\nI'm the AI Assistant created by ${config.name}!\n\nIs there anything I can help you with?`
1255
+ }
1256
+
1257
+ return null // Return null if no suitable message can be generated
1258
+ };
1259
+
1260
+ /**
1261
+ * AndyChat styles helper functions
1262
+ * Utilities for generating complete stylesheets from the new CSS architecture
1263
+ * Implements the new CSS file structure from packages/core/src/styles/
1264
+ */
1265
+
1266
+ /**
1267
+ * Calculates the relative luminance of a hex color
1268
+ * @param {string} hexColor - Hex color string (e.g., '#FFFFFF' or '#FFF')
1269
+ * @returns {number} Relative luminance value between 0 and 1
1270
+ */
1271
+ const getLuminance = (hexColor) => {
1272
+ // Remove # if present
1273
+ const hex = hexColor.replace('#', '');
1274
+
1275
+ // Handle both 3-digit and 6-digit hex
1276
+ const fullHex = hex.length === 3
1277
+ ? hex.split('').map(c => c + c).join('')
1278
+ : hex;
1279
+
1280
+ const r = parseInt(fullHex.slice(0, 2), 16) / 255;
1281
+ const g = parseInt(fullHex.slice(2, 4), 16) / 255;
1282
+ const b = parseInt(fullHex.slice(4, 6), 16) / 255;
1283
+
1284
+ // Apply gamma correction
1285
+ const [rLin, gLin, bLin] = [r, g, b].map(c =>
1286
+ c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
1287
+ );
1288
+
1289
+ // Calculate luminance using WCAG formula
1290
+ return 0.2126 * rLin + 0.7152 * gLin + 0.0722 * bLin
1291
+ };
1292
+
1293
+ /**
1294
+ * Determines if a color is light (needs dark text) or dark (needs light text)
1295
+ * @param {string} hexColor - Hex color string
1296
+ * @returns {boolean} True if the color is light
1297
+ */
1298
+ const isLightColor = (hexColor) => {
1299
+ return getLuminance(hexColor) > 0.5
1300
+ };
1301
+
1302
+ /**
1303
+ * Gets the appropriate contrast text color for a given background
1304
+ * @param {string} bgColor - Background hex color
1305
+ * @returns {string} Either '#000000' for light backgrounds or '#FFFFFF' for dark backgrounds
1306
+ */
1307
+ const getContrastTextColor = (bgColor) => {
1308
+ return isLightColor(bgColor) ? '#000000' : '#FFFFFF'
1309
+ };
1310
+
1311
+ /**
1312
+ * Generates CSS variables string from _variables.css
1313
+ * @returns {string} CSS variables content
1314
+ */
1315
+ const generateDesignTokenVariablesString = () => {
1316
+ // For now, using the content we created in _variables.css
1317
+ // In a production build process, this would read the file directly
1318
+ return `
1319
+ /* AndyChat Design Tokens as CSS Custom Properties */
1320
+ :host {
1321
+ /* === COLOR TOKENS === */
1322
+
1323
+ /* Primary brand colors */
1324
+ --andy-color-primary: #3b82f6;
1325
+ --andy-color-primary-hover: #2563eb;
1326
+ --andy-color-primary-light: #ebf4ff;
1327
+
1328
+ /* Basic colors */
1329
+ --andy-color-white: #ffffff;
1330
+ --andy-color-black: #000000;
1331
+
1332
+ /* Neutral grays */
1333
+ --andy-color-gray-100: #f7fafc;
1334
+ --andy-color-gray-200: #edf2f7;
1335
+ --andy-color-gray-300: #e2e8f0;
1336
+ --andy-color-gray-400: #cbd5e0;
1337
+ --andy-color-gray-500: #a0adb8;
1338
+ --andy-color-gray-600: #718096;
1339
+ --andy-color-gray-700: #4a5568;
1340
+ --andy-color-gray-800: #2d3748;
1341
+ --andy-color-gray-900: #1a202c;
1342
+
1343
+ /* Semantic colors */
1344
+ --andy-color-success: #48bb78;
1345
+ --andy-color-warning: #ed8936;
1346
+ --andy-color-error: #f56565;
1347
+ --andy-color-error-light: #fed7d7;
1348
+ --andy-color-info: #4299e1;
1349
+
1350
+ /* UI semantic colors */
1351
+ --andy-color-background: #f9fafc;
1352
+ --andy-color-text: #2d3748;
1353
+ --andy-color-text-secondary: #4a5568;
1354
+ --andy-color-text-muted: #718096;
1355
+ --andy-color-border: #e2e8f0;
1356
+ --andy-color-hover: #f7fafc;
1357
+
1358
+ /* Input specific colors */
1359
+ --andy-color-input-background: #f5f5f5;
1360
+ --andy-color-input-button: #f0f0f0;
1361
+ --andy-color-input-button-hover: #e0e0e0;
1362
+
1363
+ /* Chat specific colors */
1364
+ --andy-color-user-message: #3b82f6;
1365
+ --andy-color-bot-message: #f5f5f5;
1366
+ --andy-color-user-message-text: #ffffff;
1367
+ --andy-color-bot-message-text: #2d3748;
1368
+
1369
+ /* Dark theme colors */
1370
+ --andy-color-background-dark: #1a1a1a;
1371
+ --andy-color-background-secondary-dark: #2c2c2c;
1372
+ --andy-color-background-tertiary-dark: #383838;
1373
+ --andy-color-text-dark: #e0e0e0;
1374
+ --andy-color-text-muted-dark: #a0a0a0;
1375
+ --andy-color-border-dark: #444444;
1376
+ --andy-color-hover-dark: #4a4a4a;
1377
+
1378
+ /* === TYPOGRAPHY TOKENS === */
1379
+ --andy-font-family-primary: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
1380
+ --andy-font-family-mono: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
1381
+ --andy-font-size-xs: 0.75rem;
1382
+ --andy-font-size-sm: 0.875rem;
1383
+ --andy-font-size-base: 1rem;
1384
+ --andy-font-size-lg: 1.125rem;
1385
+ --andy-font-size-xl: 1.25rem;
1386
+ --andy-font-size-2xl: 1.5rem;
1387
+ --andy-font-size-3xl: 1.875rem;
1388
+ --andy-font-weight-normal: 400;
1389
+ --andy-font-weight-medium: 500;
1390
+ --andy-font-weight-semibold: 600;
1391
+ --andy-font-weight-bold: 700;
1392
+ --andy-line-height-tight: 1.25;
1393
+ --andy-line-height-normal: 1.4;
1394
+ --andy-line-height-relaxed: 1.625;
1395
+
1396
+ /* === SPACING TOKENS === */
1397
+ --andy-spacing-0: 0;
1398
+ --andy-spacing-1: 0.25rem;
1399
+ --andy-spacing-2: 0.5rem;
1400
+ --andy-spacing-3: 0.75rem;
1401
+ --andy-spacing-4: 1rem;
1402
+ --andy-spacing-5: 1.25rem;
1403
+ --andy-spacing-6: 1.5rem;
1404
+ --andy-spacing-8: 2rem;
1405
+ --andy-spacing-10: 2.5rem;
1406
+ --andy-spacing-12: 3rem;
1407
+ --andy-spacing-16: 4rem;
1408
+ --andy-spacing-20: 5rem;
1409
+ --andy-spacing-24: 6rem;
1410
+ --andy-spacing-32: 8rem;
1411
+
1412
+ /* === BORDER RADIUS TOKENS === */
1413
+ --andy-radius-none: 0;
1414
+ --andy-radius-sm: 0.125rem;
1415
+ --andy-radius-base: 0.25rem;
1416
+ --andy-radius-md: 0.375rem;
1417
+ --andy-radius-lg: 0.5rem;
1418
+ --andy-radius-xl: 1rem;
1419
+ --andy-radius-2xl: 1rem;
1420
+ --andy-radius-full: 9999px;
1421
+
1422
+ /* === SHADOW TOKENS === */
1423
+ --andy-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
1424
+ --andy-shadow-base: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
1425
+ --andy-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
1426
+ --andy-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
1427
+ --andy-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
1428
+ --andy-shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
1429
+
1430
+ /* === Z-INDEX TOKENS === */
1431
+ --andy-z-dropdown: 1000;
1432
+ --andy-z-sticky: 1020;
1433
+ --andy-z-fixed: 1030;
1434
+ --andy-z-modal-backdrop: 1040;
1435
+ --andy-z-modal: 1050;
1436
+ --andy-z-popover: 1060;
1437
+ --andy-z-tooltip: 1070;
1438
+ --andy-z-toast: 1080;
1439
+
1440
+ /* === BREAKPOINT TOKENS === */
1441
+ --andy-breakpoint-sm: 640px;
1442
+ --andy-breakpoint-md: 768px;
1443
+ --andy-breakpoint-lg: 1024px;
1444
+ --andy-breakpoint-xl: 1280px;
1445
+ --andy-breakpoint-2xl: 1536px;
1446
+
1447
+ /* === ANIMATION TOKENS === */
1448
+ --andy-duration-fast: 150ms;
1449
+ --andy-duration-normal: 300ms;
1450
+ --andy-duration-slow: 500ms;
1451
+ --andy-easing-linear: linear;
1452
+ --andy-easing-ease-in: cubic-bezier(0.4, 0, 1, 1);
1453
+ --andy-easing-ease-out: cubic-bezier(0, 0, 0.2, 1);
1454
+ --andy-easing-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
1455
+
1456
+ /* === CHAT SPECIFIC TOKENS === */
1457
+ --andy-chat-bubble-size: 50px;
1458
+ --andy-chat-window-width: 400px;
1459
+ --andy-chat-window-height: min(664px, 100% - 104px);
1460
+ --andy-chat-window-min-width: 320px;
1461
+ --andy-chat-window-min-height: 400px;
1462
+ --andy-chat-message-max-width: 80%;
1463
+ --andy-chat-message-padding: var(--andy-spacing-2);
1464
+ --andy-chat-message-gap: var(--andy-spacing-3);
1465
+ --andy-chat-avatar-size: 32px;
1466
+ --andy-chat-avatar-size-small: 24px;
1467
+
1468
+ /* Keyboard handling */
1469
+ --andy-keyboard-height: 0px;
1470
+
1471
+ /* === THEMABLE VARIABLES (Default Light Theme) === */
1472
+ --andy-themable-background: var(--andy-color-background);
1473
+ --andy-themable-background-secondary: var(--andy-color-white);
1474
+ --andy-themable-background-tertiary: var(--andy-color-gray-100);
1475
+ --andy-themable-text: var(--andy-color-text);
1476
+ --andy-themable-text-secondary: var(--andy-color-text-secondary);
1477
+ --andy-themable-text-muted: var(--andy-color-text-muted);
1478
+ --andy-themable-border: var(--andy-color-border);
1479
+ --andy-themable-hover: var(--andy-color-hover);
1480
+ --andy-themable-shadow: rgba(0, 0, 0, 0.1);
1481
+ --andy-themable-shadow-light: rgba(0, 0, 0, 0.08);
1482
+ --andy-themable-button-primary-bg: var(--andy-color-primary);
1483
+ --andy-themable-button-primary-text: var(--andy-color-white);
1484
+ --andy-themable-button-primary-hover: var(--andy-color-primary-hover);
1485
+ --andy-themable-button-secondary-bg: var(--andy-color-gray-200);
1486
+ --andy-themable-button-secondary-text: var(--andy-color-text);
1487
+ --andy-themable-button-secondary-hover: var(--andy-color-gray-300);
1488
+ --andy-themable-input-bg: var(--andy-color-input-background);
1489
+ --andy-themable-input-border: var(--andy-color-border);
1490
+ --andy-themable-input-text: var(--andy-color-text);
1491
+ --andy-themable-input-placeholder: var(--andy-color-text-muted);
1492
+ --andy-themable-message-user-bg: var(--andy-color-user-message);
1493
+ --andy-themable-message-user-text: var(--andy-color-user-message-text);
1494
+ --andy-themable-message-bot-bg: var(--andy-color-bot-message);
1495
+ --andy-themable-message-bot-text: var(--andy-color-bot-message-text);
1496
+ --andy-themable-message-bot-border: transparent;
1497
+ --andy-themable-avatar-bg: var(--andy-color-primary);
1498
+ --andy-themable-avatar-text: var(--andy-color-white);
1499
+ --andy-themable-tooltip-bg: var(--andy-color-gray-800);
1500
+ --andy-themable-tooltip-text: var(--andy-color-white);
1501
+ }
1502
+ `
1503
+ };
1504
+
1505
+ /**
1506
+ * Gets the core structural CSS from base, layout, animations, and component files
1507
+ * TODO: In production, this should read from actual CSS files
1508
+ * @returns {string} Combined structural CSS
1509
+ */
1510
+ const getCoreStructuralCssString = () => {
1511
+ // For now, returning a condensed version of the key styles
1512
+ // In production, this would read and concatenate all CSS files
1513
+ return `
1514
+ /* Base Styles */
1515
+ *,
1516
+ *::before,
1517
+ *::after {
1518
+ box-sizing: border-box;
1519
+ }
1520
+
1521
+ :host {
1522
+ font-family: var(--andy-font-family-primary);
1523
+ font-size: var(--andy-font-size-base);
1524
+ line-height: var(--andy-line-height-normal);
1525
+ color: var(--andy-themable-text);
1526
+ -webkit-font-smoothing: antialiased;
1527
+ -moz-osx-font-smoothing: grayscale;
1528
+ }
1529
+
1530
+ /* Keyframes */
1531
+ @keyframes andy-loader {
1532
+ 0% { transform: rotate(0deg); }
1533
+ 100% { transform: rotate(360deg); }
1534
+ }
1535
+
1536
+ @keyframes andy-dot-pulse {
1537
+ 0%, 100% { transform: scale(1); opacity: 0.7; }
1538
+ 50% { transform: scale(1.1); opacity: 1; }
1539
+ }
1540
+
1541
+ @keyframes andy-fade-in-slide-up {
1542
+ from { opacity: 0; transform: translateY(20px); }
1543
+ to { opacity: 1; transform: translateY(0); }
1544
+ }
1545
+
1546
+ @keyframes andy-message-slide-in {
1547
+ from { opacity: 0; transform: translateY(10px); }
1548
+ to { opacity: 1; transform: translateY(0); }
1549
+ }
1550
+
1551
+ /* Chat Bubble */
1552
+ .andy-chat-bubble {
1553
+ position: fixed;
1554
+ bottom: 30px;
1555
+ right: 30px;
1556
+ width: var(--andy-chat-bubble-size);
1557
+ height: var(--andy-chat-bubble-size);
1558
+ z-index: var(--andy-z-fixed);
1559
+ background: var(--andy-themable-button-primary-bg);
1560
+ border: none;
1561
+ border-radius: var(--andy-radius-full);
1562
+ box-shadow: var(--andy-shadow-lg);
1563
+ display: flex;
1564
+ align-items: center;
1565
+ justify-content: center;
1566
+ cursor: pointer;
1567
+ color: var(--andy-themable-button-primary-text);
1568
+ outline: none;
1569
+ transition: all var(--andy-duration-normal) var(--andy-easing-ease-out);
1570
+ /* Hidden until config loads */
1571
+ opacity: 0;
1572
+ pointer-events: none;
1573
+ }
1574
+
1575
+ /* Show bubble once config is loaded */
1576
+ .andy-chat-bubble--loaded {
1577
+ opacity: 1;
1578
+ pointer-events: auto;
1579
+ }
1580
+
1581
+ .andy-chat-bubble:hover {
1582
+ background: var(--andy-themable-button-primary-hover);
1583
+ transform: scale(1.05);
1584
+ }
1585
+
1586
+ .andy-chat-bubble__icon {
1587
+ width: 28px;
1588
+ height: 28px;
1589
+ flex-shrink: 0;
1590
+ color: currentColor;
1591
+ fill: currentColor;
1592
+ /* Filter to convert black image to white */
1593
+ filter: brightness(0) invert(1);
1594
+ }
1595
+
1596
+ .andy-chat-bubble__logo {
1597
+ width: 50px;
1598
+ height: 50px;
1599
+ border-radius: var(--andy-radius-full);
1600
+ object-fit: cover;
1601
+ flex-shrink: 0;
1602
+ }
1603
+
1604
+ /* === PILL STYLE VARIANT === */
1605
+
1606
+ /* Pill-shaped bubble with label */
1607
+ .andy-chat-bubble--pill {
1608
+ width: auto;
1609
+ height: auto;
1610
+ padding: 12px 16px 12px 20px;
1611
+ border-radius: 9999px;
1612
+ gap: 8px;
1613
+ background: var(--andy-themable-button-primary-bg);
1614
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1615
+ transition: all 0.2s ease;
1616
+ }
1617
+
1618
+ .andy-chat-bubble--pill:hover {
1619
+ background: var(--andy-themable-button-primary-hover);
1620
+ transform: scale(1.05);
1621
+ }
1622
+
1623
+ .andy-chat-bubble--pill:active {
1624
+ transform: translateY(0) scale(0.98);
1625
+ }
1626
+
1627
+ /* Label text styling */
1628
+ .andy-chat-bubble__label {
1629
+ color: var(--andy-themable-button-primary-text);
1630
+ font-size: 14px;
1631
+ font-weight: 500;
1632
+ white-space: nowrap;
1633
+ line-height: 1;
1634
+ }
1635
+
1636
+ /* Icon styling for pill variant */
1637
+ .andy-chat-bubble__icon--pill {
1638
+ width: 26px !important;
1639
+ height: 26px !important;
1640
+ flex-shrink: 0;
1641
+ border-radius: 50%;
1642
+ object-fit: cover;
1643
+ }
1644
+
1645
+ /* Ensure pill icon sizing is enforced */
1646
+ .andy-chat-bubble--pill .andy-chat-bubble__icon--pill {
1647
+ width: 26px !important;
1648
+ height: 26px !important;
1649
+ max-width: 26px;
1650
+ max-height: 26px;
1651
+ filter: none;
1652
+ }
1653
+
1654
+ /* When open, pill collapses to icon only */
1655
+ .andy-chat-bubble--pill.andy-chat-bubble--open {
1656
+ width: var(--andy-chat-bubble-size);
1657
+ height: var(--andy-chat-bubble-size);
1658
+ padding: 0;
1659
+ justify-content: center;
1660
+ }
1661
+
1662
+ .andy-chat-bubble--pill.andy-chat-bubble--open .andy-chat-bubble__label {
1663
+ display: none;
1664
+ }
1665
+
1666
+ /* Chat Bubble Tooltip */
1667
+ .andy-chat-bubble__tooltip {
1668
+ position: absolute;
1669
+ bottom: calc(100% + 10px);
1670
+ right: 50%;
1671
+ transform: translateX(50%);
1672
+ background: var(--andy-themable-tooltip-bg);
1673
+ color: var(--andy-themable-tooltip-text);
1674
+ padding: var(--andy-spacing-2) var(--andy-spacing-3);
1675
+ border-radius: var(--andy-radius-md);
1676
+ font-size: var(--andy-font-size-sm);
1677
+ white-space: nowrap;
1678
+ pointer-events: none;
1679
+ opacity: 0;
1680
+ visibility: hidden;
1681
+ transition: opacity var(--andy-duration-fast) var(--andy-easing-ease-out),
1682
+ visibility var(--andy-duration-fast) var(--andy-easing-ease-out);
1683
+ box-shadow: var(--andy-shadow-md);
1684
+ z-index: calc(var(--andy-z-fixed) + 10);
1685
+ }
1686
+
1687
+ /* Tooltip arrow */
1688
+ .andy-chat-bubble__tooltip::after {
1689
+ content: '';
1690
+ position: absolute;
1691
+ top: 100%;
1692
+ right: 50%;
1693
+ transform: translateX(50%);
1694
+ border: 6px solid transparent;
1695
+ border-top-color: var(--andy-themable-tooltip-bg);
1696
+ }
1697
+
1698
+ /* Show tooltip on hover */
1699
+ .andy-chat-bubble:hover .andy-chat-bubble__tooltip {
1700
+ opacity: 1;
1701
+ visibility: visible;
1702
+ }
1703
+
1704
+ /* Adjust tooltip position for left orientation */
1705
+ :host(.andy-orientation-left) .andy-chat-bubble__tooltip {
1706
+ right: auto;
1707
+ left: 50%;
1708
+ transform: translateX(-50%);
1709
+ }
1710
+
1711
+ :host(.andy-orientation-left) .andy-chat-bubble__tooltip::after {
1712
+ right: auto;
1713
+ left: 50%;
1714
+ transform: translateX(-50%);
1715
+ }
1716
+
1717
+ /* Chat Window */
1718
+ .andy-chat-window {
1719
+ position: fixed;
1720
+ bottom: calc(var(--andy-chat-bubble-size) + 40px + var(--andy-spacing-2));
1721
+ right: 25px;
1722
+ width: var(--andy-chat-window-width);
1723
+ min-width: var(--andy-chat-window-min-width);
1724
+ height: var(--andy-chat-window-height);
1725
+ min-height: var(--andy-chat-window-min-height);
1726
+ background: var(--andy-themable-background-secondary);
1727
+ border-radius: var(--andy-radius-xl);
1728
+ box-shadow: var(--andy-shadow-xl);
1729
+ z-index: 9999;
1730
+ display: flex;
1731
+ flex-direction: column;
1732
+ overflow: hidden;
1733
+ opacity: 0;
1734
+ transform: translateY(20px);
1735
+ pointer-events: none;
1736
+ transition: all var(--andy-duration-normal) var(--andy-easing-ease-out);
1737
+ }
1738
+
1739
+ .andy-chat-window--open {
1740
+ opacity: 1;
1741
+ transform: translateY(0);
1742
+ pointer-events: all;
1743
+ }
1744
+
1745
+ /* Header */
1746
+ .andy-header {
1747
+ display: flex;
1748
+ align-items: center;
1749
+ justify-content: space-between;
1750
+ padding: 16px 20px;
1751
+ background: var(--andy-themable-background-secondary);
1752
+ color: var(--andy-themable-text);
1753
+ border-bottom: 1px solid var(--andy-themable-border);
1754
+ height: 60px;
1755
+ position: relative;
1756
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.04);
1757
+ }
1758
+
1759
+ .andy-header__back {
1760
+ display: none; /* Hide back arrow as requested */
1761
+ }
1762
+
1763
+ .andy-header__back:hover {
1764
+ background: var(--andy-themable-hover);
1765
+ }
1766
+
1767
+ .andy-header__bot-info {
1768
+ display: flex;
1769
+ align-items: center;
1770
+ gap: var(--andy-spacing-3);
1771
+ flex: 1;
1772
+ min-width: 0; /* Allow it to shrink */
1773
+ margin-right: var(--andy-spacing-2); /* Add some space before close button */
1774
+ }
1775
+
1776
+ .andy-header__avatar {
1777
+ width: 36px;
1778
+ height: 36px;
1779
+ border-radius: 10px;
1780
+ background: var(--andy-themable-button-primary-bg);
1781
+ color: var(--andy-themable-button-primary-text);
1782
+ display: flex;
1783
+ align-items: center;
1784
+ justify-content: center;
1785
+ flex-shrink: 0;
1786
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1787
+ }
1788
+
1789
+ .andy-header__avatar-icon {
1790
+ width: 20px;
1791
+ height: 20px;
1792
+ display: block;
1793
+ object-fit: contain;
1794
+ }
1795
+
1796
+ .andy-header__avatar-icon--default {
1797
+ filter: brightness(0) invert(1);
1798
+ }
1799
+
1800
+ .andy-header__avatar-image {
1801
+ width: 36px;
1802
+ height: 36px;
1803
+ object-fit: cover;
1804
+ border-radius: 10px;
1805
+ }
1806
+
1807
+ /* Remove background when custom logo is used in header */
1808
+ .andy-header__avatar:has(.andy-header__avatar-image) {
1809
+ background: transparent;
1810
+ box-shadow: none;
1811
+ }
1812
+
1813
+ .andy-header__text {
1814
+ display: flex;
1815
+ flex-direction: column;
1816
+ gap: 2px;
1817
+ flex: 1;
1818
+ min-width: 0;
1819
+ overflow: hidden;
1820
+ text-align: left;
1821
+ align-items: flex-start;
1822
+ }
1823
+
1824
+ .andy-header__name {
1825
+ font-size: 16px;
1826
+ font-weight: 600;
1827
+ color: var(--andy-themable-text);
1828
+ margin: 0;
1829
+ line-height: 1.2;
1830
+ letter-spacing: -0.01em;
1831
+ text-align: left;
1832
+ }
1833
+
1834
+ .andy-header__subtitle {
1835
+ display: block;
1836
+ font-size: 13px;
1837
+ color: var(--andy-themable-text-muted);
1838
+ font-weight: 400;
1839
+ margin: 0;
1840
+ margin-top: 2px;
1841
+ line-height: 1.2;
1842
+ white-space: nowrap;
1843
+ overflow: hidden;
1844
+ text-overflow: ellipsis;
1845
+ max-width: 280px; /* Increased from 200px to allow more text */
1846
+ text-align: left;
1847
+ }
1848
+
1849
+ .andy-header__faq {
1850
+ display: flex;
1851
+ align-items: center;
1852
+ justify-content: center;
1853
+ width: 32px;
1854
+ height: 32px;
1855
+ background: transparent;
1856
+ border: none;
1857
+ border-radius: var(--andy-radius-full);
1858
+ color: var(--andy-themable-text-muted);
1859
+ cursor: pointer;
1860
+ transition: all var(--andy-duration-fast) var(--andy-easing-ease-out);
1861
+ position: relative;
1862
+ }
1863
+
1864
+ .andy-header__faq:hover {
1865
+ background: rgba(0, 0, 0, 0.04);
1866
+ color: var(--andy-themable-button-primary-bg);
1867
+ }
1868
+
1869
+ .andy-header__faq-icon {
1870
+ width: 20px;
1871
+ height: 20px;
1872
+ color: currentColor;
1873
+ }
1874
+
1875
+ .andy-header__close {
1876
+ display: flex; /* Always visible now that FAQ is disabled */
1877
+ align-items: center;
1878
+ justify-content: center;
1879
+ width: 32px;
1880
+ height: 32px;
1881
+ background: transparent;
1882
+ border: none;
1883
+ border-radius: var(--andy-radius-full);
1884
+ color: var(--andy-themable-text-muted);
1885
+ cursor: pointer;
1886
+ transition: all var(--andy-duration-fast) var(--andy-easing-ease-out);
1887
+ position: relative;
1888
+ z-index: 1000; /* Ensure it's on top */
1889
+ flex-shrink: 0; /* Prevent it from shrinking */
1890
+ }
1891
+
1892
+ .andy-header__close:hover {
1893
+ background: rgba(0, 0, 0, 0.04);
1894
+ color: var(--andy-themable-text);
1895
+ }
1896
+
1897
+ .andy-header__close-icon {
1898
+ width: 20px;
1899
+ height: 20px;
1900
+ color: currentColor;
1901
+ }
1902
+
1903
+ .andy-header__dropdown {
1904
+ position: absolute;
1905
+ top: 100%;
1906
+ right: 0;
1907
+ margin-top: var(--andy-spacing-2);
1908
+ background: var(--andy-themable-background-secondary);
1909
+ border: 1px solid var(--andy-themable-border);
1910
+ border-radius: var(--andy-radius-base);
1911
+ box-shadow: var(--andy-shadow-lg);
1912
+ min-width: 160px;
1913
+ padding: var(--andy-spacing-2);
1914
+ z-index: 10;
1915
+ display: none;
1916
+ }
1917
+
1918
+ .andy-header__dropdown--open {
1919
+ display: block;
1920
+ }
1921
+
1922
+ .andy-dropdown__item {
1923
+ display: flex;
1924
+ align-items: center;
1925
+ gap: var(--andy-spacing-2);
1926
+ width: 100%;
1927
+ padding: var(--andy-spacing-2) var(--andy-spacing-3);
1928
+ background: transparent;
1929
+ border: none;
1930
+ border-radius: var(--andy-radius-sm);
1931
+ color: var(--andy-themable-text);
1932
+ font-size: var(--andy-font-size-sm);
1933
+ cursor: pointer;
1934
+ transition: all var(--andy-duration-fast) var(--andy-easing-ease-out);
1935
+ text-align: left;
1936
+ }
1937
+
1938
+ .andy-dropdown__item:hover {
1939
+ background: var(--andy-themable-hover);
1940
+ }
1941
+
1942
+ /* Base Button Styles */
1943
+ .andy-button {
1944
+ display: inline-flex;
1945
+ align-items: center;
1946
+ justify-content: center;
1947
+ gap: var(--andy-spacing-2);
1948
+ padding: var(--andy-spacing-2) var(--andy-spacing-4);
1949
+ border: none;
1950
+ border-radius: var(--andy-radius-base);
1951
+ font-family: var(--andy-font-family-primary);
1952
+ font-size: var(--andy-font-size-sm);
1953
+ font-weight: var(--andy-font-weight-medium);
1954
+ cursor: pointer;
1955
+ transition: all var(--andy-duration-fast) var(--andy-easing-ease-out);
1956
+ text-decoration: none;
1957
+ outline: none;
1958
+ }
1959
+
1960
+ .andy-button:focus-visible {
1961
+ outline: 2px solid var(--andy-themable-button-primary-bg);
1962
+ outline-offset: 2px;
1963
+ }
1964
+
1965
+ .andy-button--primary {
1966
+ background: var(--andy-themable-button-primary-bg);
1967
+ color: var(--andy-themable-button-primary-text);
1968
+ }
1969
+
1970
+ .andy-button--primary:hover {
1971
+ background: var(--andy-themable-button-primary-hover);
1972
+ }
1973
+
1974
+ .andy-button--secondary {
1975
+ background: var(--andy-themable-button-secondary-bg);
1976
+ color: var(--andy-themable-button-secondary-text);
1977
+ }
1978
+
1979
+ .andy-button--secondary:hover {
1980
+ background: var(--andy-themable-button-secondary-hover);
1981
+ }
1982
+
1983
+ /* Messages */
1984
+ .andy-chat-window__messages {
1985
+ flex: 1;
1986
+ display: flex;
1987
+ flex-direction: column;
1988
+ gap: 0;
1989
+ padding: var(--andy-spacing-5) var(--andy-spacing-6);
1990
+ overflow-y: auto;
1991
+ background: var(--andy-themable-background-secondary);
1992
+ min-height: 0;
1993
+ padding: 24px 20px 0px;
1994
+ }
1995
+
1996
+ /* Subtitle section at top of messages */
1997
+ .andy-chat-subtitle {
1998
+ font-size: 14px;
1999
+ color: var(--andy-themable-text-secondary);
2000
+ text-align: center;
2001
+ line-height: 21px;
2002
+ margin-bottom: var(--andy-spacing-5);
2003
+ }
2004
+
2005
+ .andy-chat-subtitle__text {
2006
+ padding: 0;
2007
+ margin: 0;
2008
+ text-wrap: balance;
2009
+ }
2010
+
2011
+
2012
+
2013
+ .andy-message-container {
2014
+ display: flex;
2015
+ margin-bottom: 8px;
2016
+ max-width: 75%;
2017
+ align-items: flex-start;
2018
+ gap: var(--andy-spacing-3);
2019
+ animation: andy-message-slide-in var(--andy-duration-normal) var(--andy-easing-ease-out);
2020
+ }
2021
+
2022
+ .andy-message-container--user {
2023
+ margin-left: auto;
2024
+ flex-direction: row-reverse;
2025
+ }
2026
+
2027
+ .andy-message-container--bot {
2028
+ margin-right: auto;
2029
+ flex-direction: row;
2030
+ align-items: flex-start;
2031
+ }
2032
+
2033
+ /* Bot message header with icon + name */
2034
+ .andy-bot-header {
2035
+ display: flex;
2036
+ align-items: center;
2037
+ gap: 8px;
2038
+ margin-bottom: 8px;
2039
+ }
2040
+
2041
+ .andy-bot-header__icon {
2042
+ width: 24px;
2043
+ height: 24px;
2044
+ background: var(--andy-themable-button-primary-bg);
2045
+ color: var(--andy-themable-button-primary-text);
2046
+ border-radius: 6px;
2047
+ display: flex;
2048
+ align-items: center;
2049
+ justify-content: center;
2050
+ flex-shrink: 0;
2051
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
2052
+ }
2053
+
2054
+ .andy-bot-header__icon svg {
2055
+ width: 14px;
2056
+ height: 14px;
2057
+ display: block;
2058
+ }
2059
+
2060
+ .andy-bot-header__icon-image {
2061
+ width: 24px;
2062
+ height: 24px;
2063
+ object-fit: cover;
2064
+ border-radius: 6px;
2065
+ display: block;
2066
+ }
2067
+
2068
+ .andy-bot-header__icon-image--default {
2069
+ width: 14px;
2070
+ height: 14px;
2071
+ object-fit: contain;
2072
+ filter: brightness(0) invert(1);
2073
+ }
2074
+
2075
+ /* Remove background when custom logo is used (not default icon) */
2076
+ .andy-bot-header__icon:has(.andy-bot-header__icon-image:not(.andy-bot-header__icon-image--default)) {
2077
+ background: transparent;
2078
+ box-shadow: none;
2079
+ }
2080
+
2081
+ .andy-bot-header__name {
2082
+ font-size: 14px;
2083
+ font-weight: 600;
2084
+ color: var(--andy-themable-text);
2085
+ line-height: 1.2;
2086
+ letter-spacing: -0.01em;
2087
+ }
2088
+
2089
+ .andy-avatar {
2090
+ display: none; /* Hide regular avatar, using new bot header */
2091
+ }
2092
+
2093
+ .andy-message {
2094
+ border-radius: var(--andy-radius-lg);
2095
+ font-size: var(--andy-font-size-sm);
2096
+ line-height: var(--andy-line-height-normal);
2097
+ word-wrap: break-word;
2098
+ position: relative;
2099
+ }
2100
+
2101
+ .andy-message--user {
2102
+ background: var(--andy-themable-button-primary-bg);
2103
+ color: var(--andy-themable-message-user-text);
2104
+ border-radius: 20px;
2105
+ padding: 8px 12px;
2106
+ display: flex;
2107
+ align-items: center;
2108
+ justify-content: center;
2109
+ text-align: center;
2110
+ min-height: 36px;
2111
+ font-weight: 500;
2112
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.1);
2113
+ line-height: 1.4;
2114
+ }
2115
+
2116
+ .andy-message--bot {
2117
+ background: var(--andy-themable-message-bot-bg);
2118
+ color: var(--andy-themable-message-bot-text);
2119
+ border-radius: 20px;
2120
+ border: 1px solid var(--andy-themable-message-bot-border);
2121
+ padding: 12px 16px;
2122
+ box-shadow: none;
2123
+ line-height: 1.5;
2124
+ text-align: left;
2125
+ }
2126
+
2127
+ /* Message Sub-Components */
2128
+ .andy-message__content {
2129
+ line-height: 1.4;
2130
+ margin: 0;
2131
+ overflow-wrap: break-word;
2132
+ word-break: break-word;
2133
+ text-align: left;
2134
+ }
2135
+
2136
+ /* User message content centering */
2137
+ .andy-message--user .andy-message__content {
2138
+ width: 100%;
2139
+ display: flex;
2140
+ align-items: center;
2141
+ justify-content: center;
2142
+ }
2143
+
2144
+ .andy-message--user .andy-message__content p {
2145
+ margin: 0;
2146
+ }
2147
+
2148
+ /* Bot message content left alignment */
2149
+ .andy-message--bot .andy-message__content {
2150
+ text-align: left;
2151
+ }
2152
+
2153
+ .andy-message__content p {
2154
+ margin: 0;
2155
+ margin-bottom: var(--andy-spacing-1);
2156
+ }
2157
+
2158
+ .andy-message__content p:last-child {
2159
+ margin-bottom: 0;
2160
+ }
2161
+
2162
+ .andy-message__content h1,
2163
+ .andy-message__content h2,
2164
+ .andy-message__content h3,
2165
+ .andy-message__content h4,
2166
+ .andy-message__content h5,
2167
+ .andy-message__content h6 {
2168
+ margin: 0 0 var(--andy-spacing-1) 0;
2169
+ font-weight: var(--andy-font-weight-bold);
2170
+ }
2171
+
2172
+ .andy-message__content ul,
2173
+ .andy-message__content ol {
2174
+ margin: var(--andy-spacing-2) 0;
2175
+ padding-left: var(--andy-spacing-4);
2176
+ }
2177
+
2178
+ .andy-message__content li {
2179
+ margin-bottom: var(--andy-spacing-1);
2180
+ }
2181
+
2182
+ .andy-message__content code {
2183
+ background: rgba(0, 0, 0, 0.1);
2184
+ padding: var(--andy-spacing-1) var(--andy-spacing-2);
2185
+ border-radius: var(--andy-radius-sm);
2186
+ font-family: monospace;
2187
+ font-size: 0.875em;
2188
+ }
2189
+
2190
+ .andy-message__content strong {
2191
+ font-weight: var(--andy-font-weight-bold);
2192
+ }
2193
+
2194
+ .andy-message__content em {
2195
+ font-style: italic;
2196
+ }
2197
+
2198
+ .andy-message__link {
2199
+ color: var(--andy-themable-button-primary-bg);
2200
+ text-decoration: none;
2201
+ }
2202
+
2203
+ .andy-message__link:hover {
2204
+ text-decoration: underline;
2205
+ }
2206
+
2207
+ /* Avatar Sub-Components */
2208
+ .andy-avatar__icon {
2209
+ width: 18px;
2210
+ height: 18px;
2211
+ flex-shrink: 0;
2212
+ display: block;
2213
+ object-fit: contain;
2214
+ }
2215
+
2216
+ .andy-avatar__icon--default {
2217
+ filter: brightness(0) invert(1);
2218
+ }
2219
+
2220
+ .andy-avatar__image {
2221
+ width: 100%;
2222
+ height: 100%;
2223
+ border-radius: var(--andy-radius-full);
2224
+ object-fit: cover;
2225
+ }
2226
+
2227
+ /* Typing Indicator Component */
2228
+ .andy-typing-indicator {
2229
+ display: flex;
2230
+ align-items: center;
2231
+ justify-content: center;
2232
+ padding: var(--andy-spacing-2) var(--andy-spacing-3);
2233
+ min-height: 32px;
2234
+ }
2235
+
2236
+ .andy-typing-indicator__dot {
2237
+ width: 6px;
2238
+ height: 6px;
2239
+ border-radius: var(--andy-radius-full);
2240
+ background-color: var(--andy-themable-text-secondary);
2241
+ margin: 0 2px;
2242
+ opacity: 0.7;
2243
+ animation: andy-dot-pulse var(--andy-duration-slow) infinite var(--andy-easing-ease-in-out);
2244
+ }
2245
+
2246
+ .andy-typing-indicator__dot:nth-child(1) {
2247
+ animation-delay: 0s;
2248
+ }
2249
+
2250
+ .andy-typing-indicator__dot:nth-child(2) {
2251
+ animation-delay: 0.2s;
2252
+ }
2253
+
2254
+ .andy-typing-indicator__dot:nth-child(3) {
2255
+ animation-delay: 0.4s;
2256
+ }
2257
+
2258
+
2259
+ .andy-message__greeting-title {
2260
+ font-size: var(--andy-font-size-lg);
2261
+ font-weight: var(--andy-font-weight-bold);
2262
+ margin-bottom: var(--andy-spacing-1);
2263
+ }
2264
+
2265
+ .andy-message__greeting-content {
2266
+ font-size: var(--andy-font-size-sm);
2267
+ opacity: 0.9;
2268
+ }
2269
+
2270
+ .andy-message--loading .andy-message__content {
2271
+ padding: var(--andy-spacing-2) 0;
2272
+ display: flex;
2273
+ align-items: center;
2274
+ justify-content: center;
2275
+ }
2276
+
2277
+ .andy-message--error {
2278
+ border-color: var(--andy-color-error);
2279
+ background: var(--andy-color-error-light);
2280
+ }
2281
+
2282
+ /* Input */
2283
+ .andy-input-container {
2284
+ display: flex;
2285
+ flex-direction: column;
2286
+ padding: var(--andy-spacing-5) var(--andy-spacing-6);
2287
+ border-top: 1px solid var(--andy-themable-border);
2288
+ background: var(--andy-themable-background-secondary);
2289
+ border-radius: 0 0 var(--andy-radius-xl) var(--andy-radius-xl);
2290
+ }
2291
+
2292
+ .andy-input {
2293
+ display: flex;
2294
+ position: relative;
2295
+ width: 100%;
2296
+ align-items: center;
2297
+ gap: var(--andy-spacing-2);
2298
+ }
2299
+
2300
+ .andy-input__field {
2301
+ flex: 1;
2302
+ padding: 12px var(--andy-spacing-4);
2303
+ padding-left: var(--andy-spacing-4);
2304
+ padding-right: 50px;
2305
+ border: 1px solid var(--andy-themable-border);
2306
+ border-radius: 24px;
2307
+ font-family: var(--andy-font-family-primary);
2308
+ font-size: var(--andy-font-size-sm);
2309
+ line-height: var(--andy-line-height-normal);
2310
+ background: var(--andy-themable-input-bg);
2311
+ color: var(--andy-themable-input-text);
2312
+ min-height: 48px;
2313
+ outline: none;
2314
+ resize: none;
2315
+ transition: all var(--andy-duration-normal) var(--andy-easing-ease-out);
2316
+ box-shadow: none;
2317
+ }
2318
+
2319
+ .andy-input__field:focus {
2320
+ border-color: var(--andy-themable-text-muted);
2321
+ background: var(--andy-themable-background-secondary);
2322
+ box-shadow: none;
2323
+ }
2324
+
2325
+ .andy-input__button {
2326
+ position: absolute;
2327
+ right: 5px;
2328
+ top: 50%;
2329
+ transform: translateY(-50%);
2330
+ width: 30px;
2331
+ height: 30px;
2332
+ border: none;
2333
+ border-radius: var(--andy-radius-full);
2334
+ background: var(--andy-themable-button-primary-bg);
2335
+ color: var(--andy-themable-button-primary-text);
2336
+ cursor: pointer;
2337
+ display: flex;
2338
+ align-items: center;
2339
+ justify-content: center;
2340
+ transition: all var(--andy-duration-normal) var(--andy-easing-ease-out);
2341
+ outline: none;
2342
+ z-index: 1;
2343
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.1);
2344
+ }
2345
+
2346
+ .andy-input__button:hover {
2347
+ background: var(--andy-themable-button-primary-hover);
2348
+ transform: translateY(-50%) scale(1.05);
2349
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.15);
2350
+ }
2351
+
2352
+ .andy-input__button:disabled {
2353
+ background: #cccccc;
2354
+ cursor: not-allowed;
2355
+ opacity: 0.6;
2356
+ }
2357
+
2358
+ .andy-input__button svg,
2359
+ .andy-input__button-icon {
2360
+ pointer-events: none;
2361
+ }
2362
+
2363
+ .andy-input__button:disabled:hover {
2364
+ background: #cccccc;
2365
+ transform: translateY(-50%);
2366
+ }
2367
+
2368
+ /* Bottom Section */
2369
+ .andy-chat-window__bottom {
2370
+ display: flex;
2371
+ flex-direction: column;
2372
+ background: var(--andy-themable-background-secondary);
2373
+ border-top: none;
2374
+ border-radius: 0 0 var(--andy-radius-xl) var(--andy-radius-xl);
2375
+ overflow: visible;
2376
+ }
2377
+
2378
+ /* Predefined Questions */
2379
+ .andy-predefined-questions {
2380
+ padding: 20px 24px 16px;
2381
+ display: flex;
2382
+ flex-direction: column;
2383
+ gap: 16px;
2384
+ overflow: visible;
2385
+ }
2386
+
2387
+ .andy-predefined-questions__header {
2388
+ display: none; /* Hide header as requested */
2389
+ }
2390
+
2391
+ .andy-predefined-questions__title {
2392
+ display: none; /* Hide title as requested */
2393
+ }
2394
+
2395
+ .andy-predefined-questions__list {
2396
+ display: flex;
2397
+ flex-direction: column;
2398
+ gap: 12px;
2399
+ overflow: visible;
2400
+ scrollbar-width: none;
2401
+ -ms-overflow-style: none;
2402
+ padding: 0;
2403
+ margin: 0;
2404
+ }
2405
+
2406
+ .andy-predefined-questions__list::-webkit-scrollbar {
2407
+ display: none;
2408
+ }
2409
+
2410
+ .andy-predefined-question {
2411
+ display: flex;
2412
+ align-items: flex-start;
2413
+ gap: var(--andy-spacing-1);
2414
+ padding: 8px 16px;
2415
+ background: var(--andy-themable-background-secondary);
2416
+ border: 1px solid var(--andy-themable-border);
2417
+ border-radius: var(--andy-radius-full);
2418
+ color: var(--andy-themable-text);
2419
+ font-size: 13px;
2420
+ font-weight: 500;
2421
+ cursor: pointer;
2422
+ transition: all var(--andy-duration-normal) var(--andy-easing-ease-out);
2423
+ white-space: normal;
2424
+ text-align: left;
2425
+ flex-shrink: 1;
2426
+ width: 100%;
2427
+ max-width: 100%;
2428
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
2429
+ min-height: 36px;
2430
+ position: relative;
2431
+ overflow: visible;
2432
+ }
2433
+
2434
+ .andy-predefined-question:hover {
2435
+ background: var(--andy-themable-hover);
2436
+ border-color: var(--andy-themable-text-muted);
2437
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
2438
+ transform: translateY(-1px);
2439
+ z-index: 10;
2440
+ }
2441
+
2442
+ .andy-predefined-question:active {
2443
+ transform: translateY(0);
2444
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
2445
+ }
2446
+
2447
+ .andy-predefined-question:disabled {
2448
+ opacity: 0.6;
2449
+ cursor: not-allowed;
2450
+ transform: none;
2451
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.04);
2452
+ }
2453
+
2454
+ .andy-predefined-question__icon {
2455
+ font-size: 12px;
2456
+ }
2457
+
2458
+ .andy-predefined-question__text {
2459
+ flex: 1 1 auto;
2460
+ line-height: 1.4;
2461
+ overflow-wrap: anywhere;
2462
+ }
2463
+
2464
+ /* Updated Input Container */
2465
+ .andy-chat-window__input {
2466
+ padding: var(--andy-spacing-3) var(--andy-spacing-5);
2467
+ }
2468
+
2469
+ .andy-input-container {
2470
+ display: flex;
2471
+ flex-direction: column;
2472
+ padding: 0;
2473
+ border-top: none;
2474
+ background: transparent;
2475
+ border-radius: 0;
2476
+ }
2477
+
2478
+ /* Footer */
2479
+ .andy-chat-window__footer {
2480
+ display: flex;
2481
+ align-items: center;
2482
+ justify-content: center;
2483
+ padding: var(--andy-spacing-1) var(--andy-spacing-3);
2484
+ background: transparent;
2485
+ border-radius: 0 0 var(--andy-radius-xl) var(--andy-radius-xl);
2486
+ }
2487
+
2488
+ .andy-chat-window__footer-link {
2489
+ text-decoration: none;
2490
+ display: inline-flex;
2491
+ align-items: center;
2492
+ gap: 3px;
2493
+ padding: 4px 10px;
2494
+ background: var(--andy-themable-hover);
2495
+ border-radius: 10px;
2496
+ transition: all 0.2s ease;
2497
+ font-size: 10px;
2498
+ opacity: 0.8;
2499
+ position: relative;
2500
+ /* Ensure reasonable touch target size for mobile */
2501
+ min-height: 28px;
2502
+ }
2503
+
2504
+ .andy-chat-window__footer-link:hover {
2505
+ opacity: 1;
2506
+ transform: translateY(-1px) scale(1.02);
2507
+ box-shadow: 0 2px 8px var(--andy-themable-shadow);
2508
+ background: var(--andy-themable-hover);
2509
+ }
2510
+
2511
+ .andy-chat-window__footer-link:active {
2512
+ transform: translateY(0) scale(1);
2513
+ box-shadow: 0 1px 4px var(--andy-themable-shadow-light);
2514
+ }
2515
+
2516
+ .andy-chat-window__footer-link:focus-visible {
2517
+ outline: 2px solid var(--andy-themable-button-primary-bg);
2518
+ outline-offset: 2px;
2519
+ opacity: 1;
2520
+ }
2521
+
2522
+ /* External link icon - using ::after instead of ::before */
2523
+ .andy-chat-window__footer-link::after {
2524
+ content: '↗';
2525
+ font-size: 9px;
2526
+ margin-left: 1px;
2527
+ transition: transform 0.2s ease;
2528
+ display: inline-block;
2529
+ color: var(--andy-themable-text-secondary);
2530
+ opacity: 0.8;
2531
+ }
2532
+
2533
+ .andy-chat-window__footer-link:hover::after {
2534
+ transform: translate(1px, -1px);
2535
+ opacity: 1;
2536
+ }
2537
+
2538
+ .andy-chat-window__footer-text {
2539
+ color: var(--andy-themable-text-secondary);
2540
+ font-size: 10px;
2541
+ font-weight: var(--andy-font-weight-medium);
2542
+ letter-spacing: 0.01em;
2543
+ line-height: 1.4;
2544
+ }
2545
+
2546
+ /* FAQ */
2547
+ .andy-chat-window__faq {
2548
+ display: none;
2549
+ flex: 1;
2550
+ flex-direction: column;
2551
+ background: var(--andy-themable-background);
2552
+ overflow-y: auto;
2553
+ }
2554
+
2555
+ .andy-faq {
2556
+ display: flex;
2557
+ flex-direction: column;
2558
+ height: 100%;
2559
+ }
2560
+
2561
+ .andy-faq__header {
2562
+ display: flex;
2563
+ flex-direction: column;
2564
+ padding: var(--andy-spacing-5) var(--andy-spacing-6) var(--andy-spacing-4);
2565
+ border-bottom: 1px solid var(--andy-themable-border);
2566
+ background: var(--andy-themable-background-secondary);
2567
+ }
2568
+
2569
+ .andy-faq__back-button {
2570
+ display: flex;
2571
+ align-items: center;
2572
+ gap: var(--andy-spacing-2);
2573
+ margin-bottom: var(--andy-spacing-4);
2574
+ padding: var(--andy-spacing-2) 0;
2575
+ background: none;
2576
+ border: none;
2577
+ color: var(--andy-themable-button-primary-bg);
2578
+ font-size: var(--andy-font-size-sm);
2579
+ cursor: pointer;
2580
+ transition: color var(--andy-duration-fast) var(--andy-easing-ease-out);
2581
+ outline: none;
2582
+ }
2583
+
2584
+ .andy-faq__back-button:hover {
2585
+ color: var(--andy-themable-button-primary-hover);
2586
+ }
2587
+
2588
+ .andy-faq__back-icon {
2589
+ width: 20px;
2590
+ height: 20px;
2591
+ flex-shrink: 0;
2592
+ }
2593
+
2594
+ .andy-faq__title {
2595
+ font-size: var(--andy-font-size-xl);
2596
+ font-weight: var(--andy-font-weight-bold);
2597
+ color: var(--andy-themable-text);
2598
+ margin: 0;
2599
+ text-align: center;
2600
+ }
2601
+
2602
+ .andy-faq__list {
2603
+ flex: 1;
2604
+ padding: var(--andy-spacing-4) var(--andy-spacing-6);
2605
+ overflow-y: auto;
2606
+ }
2607
+
2608
+ .andy-faq-item {
2609
+ margin-bottom: var(--andy-spacing-4);
2610
+ border: 1px solid var(--andy-themable-border);
2611
+ border-radius: var(--andy-radius-lg);
2612
+ background: var(--andy-themable-background-secondary);
2613
+ overflow: hidden;
2614
+ transition: all var(--andy-duration-fast) var(--andy-easing-ease-out);
2615
+ }
2616
+
2617
+ .andy-faq-item:hover {
2618
+ border-color: var(--andy-themable-button-primary-bg);
2619
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
2620
+ }
2621
+
2622
+ .andy-faq-item__question {
2623
+ display: flex;
2624
+ align-items: center;
2625
+ justify-content: space-between;
2626
+ width: 100%;
2627
+ padding: var(--andy-spacing-4) var(--andy-spacing-5);
2628
+ background: none;
2629
+ border: none;
2630
+ text-align: left;
2631
+ font-size: var(--andy-font-size-base);
2632
+ font-weight: var(--andy-font-weight-medium);
2633
+ color: var(--andy-themable-text);
2634
+ cursor: pointer;
2635
+ transition: all var(--andy-duration-fast) var(--andy-easing-ease-out);
2636
+ outline: none;
2637
+ }
2638
+
2639
+ .andy-faq-item__question:hover {
2640
+ background: var(--andy-themable-hover);
2641
+ }
2642
+
2643
+ .andy-faq-item__chevron {
2644
+ width: 16px;
2645
+ height: 16px;
2646
+ flex-shrink: 0;
2647
+ transition: transform var(--andy-duration-fast) var(--andy-easing-ease-out);
2648
+ color: var(--andy-themable-text-secondary);
2649
+ }
2650
+
2651
+ .andy-faq-item--open .andy-faq-item__chevron {
2652
+ transform: rotate(180deg);
2653
+ }
2654
+
2655
+ .andy-faq-item__answer {
2656
+ display: grid;
2657
+ grid-template-rows: 0fr;
2658
+ overflow: hidden;
2659
+ transition: grid-template-rows var(--andy-duration-normal) var(--andy-easing-ease-out);
2660
+ background: var(--andy-themable-background);
2661
+ }
2662
+
2663
+ .andy-faq-item--open .andy-faq-item__answer {
2664
+ grid-template-rows: 1fr;
2665
+ }
2666
+
2667
+ .andy-faq-item__answer-content {
2668
+ min-height: 0;
2669
+ padding: 0 var(--andy-spacing-5);
2670
+ }
2671
+
2672
+ .andy-faq-item--open .andy-faq-item__answer-content {
2673
+ padding: 0 var(--andy-spacing-5) var(--andy-spacing-4);
2674
+ }
2675
+
2676
+ .andy-faq-item__answer p {
2677
+ margin: 0 0 var(--andy-spacing-2) 0;
2678
+ color: var(--andy-themable-text-secondary);
2679
+ line-height: var(--andy-line-height-normal);
2680
+ }
2681
+
2682
+ .andy-faq-item__answer p:last-child {
2683
+ margin-bottom: 0;
2684
+ }
2685
+
2686
+ .andy-faq-item__answer ul,
2687
+ .andy-faq-item__answer ol {
2688
+ margin: var(--andy-spacing-2) 0;
2689
+ padding-left: var(--andy-spacing-4);
2690
+ color: var(--andy-themable-text-secondary);
2691
+ }
2692
+
2693
+ .andy-faq-item__answer li {
2694
+ margin-bottom: var(--andy-spacing-1);
2695
+ }
2696
+
2697
+ /* Welcome Popup */
2698
+ .andy-welcome-popup {
2699
+ position: fixed;
2700
+ bottom: 100px;
2701
+ right: 20px;
2702
+ z-index: 9998;
2703
+ display: none;
2704
+ max-width: 280px;
2705
+ animation: andy-fade-in-slide-up 0.3s ease-out;
2706
+ }
2707
+
2708
+ /* Left orientation */
2709
+ .andy-welcome-popup.andychat-popup-left,
2710
+ :host(.andy-orientation-left) .andy-welcome-popup {
2711
+ right: auto;
2712
+ left: 20px;
2713
+ }
2714
+
2715
+ .andy-welcome-popup__content {
2716
+ background: var(--andy-themable-background-secondary);
2717
+ border-radius: var(--andy-radius-lg);
2718
+ padding: 16px;
2719
+ box-shadow: var(--andy-shadow-lg);
2720
+ position: relative;
2721
+ border: 1px solid var(--andy-themable-border);
2722
+ }
2723
+
2724
+ .andy-welcome-popup__message {
2725
+ font-size: 14px;
2726
+ line-height: 1.5;
2727
+ color: var(--andy-themable-text);
2728
+ padding-right: 24px;
2729
+ }
2730
+
2731
+ .andy-welcome-popup__close {
2732
+ position: absolute;
2733
+ top: 8px;
2734
+ right: 8px;
2735
+ width: 24px;
2736
+ height: 24px;
2737
+ border: none;
2738
+ background: transparent;
2739
+ cursor: pointer;
2740
+ display: flex;
2741
+ align-items: center;
2742
+ justify-content: center;
2743
+ border-radius: 4px;
2744
+ color: var(--andy-themable-text-muted);
2745
+ transition: all 0.15s ease;
2746
+ }
2747
+
2748
+ .andy-welcome-popup__close:hover {
2749
+ background: var(--andy-themable-hover);
2750
+ color: var(--andy-themable-text);
2751
+ }
2752
+
2753
+ .andy-welcome-popup__close-icon {
2754
+ width: 16px;
2755
+ height: 16px;
2756
+ }
2757
+
2758
+ .andy-welcome-popup__arrow {
2759
+ position: absolute;
2760
+ bottom: -8px;
2761
+ right: 30px;
2762
+ width: 16px;
2763
+ height: 16px;
2764
+ background: var(--andy-themable-background-secondary);
2765
+ border-right: 1px solid var(--andy-themable-border);
2766
+ border-bottom: 1px solid var(--andy-themable-border);
2767
+ transform: rotate(45deg);
2768
+ }
2769
+
2770
+ /* Left orientation arrow */
2771
+ .andy-welcome-popup.andychat-popup-left .andy-welcome-popup__arrow,
2772
+ :host(.andy-orientation-left) .andy-welcome-popup .andy-welcome-popup__arrow {
2773
+ right: auto;
2774
+ left: 30px;
2775
+ }
2776
+
2777
+ /* Loading States */
2778
+ .andy-loading__dots {
2779
+ display: flex;
2780
+ align-items: center;
2781
+ justify-content: center;
2782
+ gap: var(--andy-spacing-2);
2783
+ padding: var(--andy-spacing-4);
2784
+ min-height: 60px;
2785
+ }
2786
+
2787
+ .andy-loading__dot {
2788
+ width: 12px;
2789
+ height: 12px;
2790
+ border-radius: var(--andy-radius-full);
2791
+ background-color: var(--andy-themable-text-muted);
2792
+ opacity: 0.6;
2793
+ }
2794
+
2795
+ .andy-loading__dot:nth-child(1) {
2796
+ animation: andy-dot-pulse var(--andy-duration-slow) infinite var(--andy-easing-ease-in-out) 0s;
2797
+ }
2798
+
2799
+ .andy-loading__dot:nth-child(2) {
2800
+ animation: andy-dot-pulse var(--andy-duration-slow) infinite var(--andy-easing-ease-in-out) 0.2s;
2801
+ }
2802
+
2803
+ .andy-loading__dot:nth-child(3) {
2804
+ animation: andy-dot-pulse var(--andy-duration-slow) infinite var(--andy-easing-ease-in-out) 0.4s;
2805
+ }
2806
+
2807
+ /* Responsive */
2808
+ @media (max-width: 520px) {
2809
+ .andy-chat-window {
2810
+ position: fixed !important;
2811
+ bottom: 1rem !important;
2812
+ left: 1rem !important;
2813
+ right: 1rem !important;
2814
+ top: 1rem !important;
2815
+ width: calc(100% - 2rem) !important;
2816
+ height: calc(100% - 2rem) !important;
2817
+ max-width: 500px !important;
2818
+ max-height: calc(100vh - 2rem) !important;
2819
+ max-height: calc(100dvh - 2rem) !important;
2820
+ margin: 0 auto !important;
2821
+ border-radius: var(--andy-radius-xl) !important;
2822
+ box-shadow: 0 0 40px rgba(0, 0, 0, 0.2) !important;
2823
+ overflow: hidden;
2824
+ display: flex;
2825
+ flex-direction: column;
2826
+ background: var(--andy-themable-background-secondary) !important;
2827
+ /* Card-like floating design */
2828
+ /* Add smooth scale animation on mobile */
2829
+ transform: scale(0.95);
2830
+ opacity: 0;
2831
+ }
2832
+
2833
+ .andy-chat-window--open {
2834
+ transform: scale(1);
2835
+ opacity: 1;
2836
+ }
2837
+
2838
+ /* Adjust for very small screens */
2839
+ @media (max-height: 600px) {
2840
+ .andy-chat-window {
2841
+ bottom: 0.5rem !important;
2842
+ top: 0.5rem !important;
2843
+ left: 0.5rem !important;
2844
+ right: 0.5rem !important;
2845
+ width: calc(100% - 1rem) !important;
2846
+ height: calc(100% - 1rem) !important;
2847
+ }
2848
+ }
2849
+
2850
+ /* No special handling for keyboard - let browser handle it naturally */
2851
+ .andy-chat-window--keyboard-open {
2852
+ /* Browser handles positioning automatically with our margin-based approach */
2853
+ }
2854
+
2855
+ /* Ensure the window content is properly laid out */
2856
+ .andy-chat-window__messages {
2857
+ flex: 1 1 auto;
2858
+ min-height: 0;
2859
+ overflow-y: auto;
2860
+ -webkit-overflow-scrolling: touch;
2861
+ }
2862
+
2863
+ .andy-chat-window__bottom {
2864
+ flex: 0 0 auto;
2865
+ position: relative;
2866
+ z-index: 10;
2867
+ }
2868
+
2869
+ /* Fix input font size to prevent iOS zoom */
2870
+ .andy-input__field {
2871
+ font-size: 16px !important; /* Prevents iOS zoom on focus */
2872
+ line-height: 1.5 !important;
2873
+ padding-right: 60px !important; /* More space for larger button */
2874
+ }
2875
+
2876
+ /* Larger send button on mobile for better touch targets */
2877
+ .andy-input__button {
2878
+ width: 40px !important;
2879
+ height: 40px !important;
2880
+ right: 8px !important;
2881
+ }
2882
+
2883
+ /* Add expanded touch target for submit button on mobile only */
2884
+ .andy-input__button::before {
2885
+ content: '';
2886
+ position: absolute;
2887
+ top: -15px;
2888
+ right: -15px;
2889
+ bottom: -15px;
2890
+ left: -15px;
2891
+ border-radius: var(--andy-radius-full);
2892
+ /* Invisible expanded touch target */
2893
+ }
2894
+
2895
+ /* Ensure proper header spacing on mobile */
2896
+ .andy-header {
2897
+ min-height: 60px;
2898
+ flex-shrink: 0;
2899
+ position: sticky;
2900
+ top: 0;
2901
+ z-index: 100;
2902
+ background: var(--andy-themable-background-secondary);
2903
+ }
2904
+
2905
+ /* Ensure messages don't go under header */
2906
+ .andy-chat-window__messages {
2907
+ padding-top: var(--andy-spacing-3);
2908
+ }
2909
+
2910
+ .andy-chat-bubble {
2911
+ width: 56px;
2912
+ height: 56px;
2913
+ bottom: 30px;
2914
+ /* Ensure bubble is tappable on mobile */
2915
+ cursor: pointer;
2916
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
2917
+ touch-action: manipulation;
2918
+ }
2919
+
2920
+ .andy-chat-bubble__icon {
2921
+ width: 20px;
2922
+ height: 20px;
2923
+ }
2924
+
2925
+ /* Pill style on mobile - override fixed dimensions */
2926
+ .andy-chat-bubble--pill {
2927
+ width: auto !important;
2928
+ height: auto !important;
2929
+ padding: 10px 16px 10px 12px;
2930
+ gap: 6px;
2931
+ }
2932
+
2933
+ .andy-chat-bubble--pill .andy-chat-bubble__label {
2934
+ font-size: 13px;
2935
+ }
2936
+
2937
+ .andy-chat-bubble--pill .andy-chat-bubble__icon--pill {
2938
+ width: 22px !important;
2939
+ height: 22px !important;
2940
+ }
2941
+
2942
+ /* When open on mobile, pill collapses to icon only */
2943
+ .andy-chat-bubble--pill.andy-chat-bubble--open {
2944
+ width: 56px !important;
2945
+ height: 56px !important;
2946
+ padding: 0;
2947
+ }
2948
+
2949
+ /* Show close button on mobile */
2950
+ .andy-header__close {
2951
+ display: flex;
2952
+ }
2953
+
2954
+ /* Add expanded touch target for close button on mobile only */
2955
+ .andy-header__close::before {
2956
+ content: '';
2957
+ position: absolute;
2958
+ top: -12px;
2959
+ right: -12px;
2960
+ bottom: -12px;
2961
+ left: -12px;
2962
+ border-radius: var(--andy-radius-full);
2963
+ /* Invisible expanded touch target */
2964
+ }
2965
+
2966
+ /* Hide FAQ button on mobile to make room for close button */
2967
+ .andy-header__faq {
2968
+ display: none;
2969
+ }
2970
+ }
2971
+
2972
+ /* Desktop keyboard handling (for testing) */
2973
+ @media (min-width: 521px) {
2974
+ /* Desktop retains original positioning behavior */
2975
+ .andy-chat-window {
2976
+ transition: transform 0.3s ease-out;
2977
+ }
2978
+
2979
+ /* Optional: Add keyboard handling for desktop if needed */
2980
+ .andy-chat-window--keyboard-open {
2981
+ /* No transform on desktop - keyboard doesn't affect viewport */
2982
+ }
2983
+ }
2984
+
2985
+ /* Orientation */
2986
+ :host(.andy-orientation-right) .andy-chat-bubble {
2987
+ right: 30px;
2988
+ left: auto;
2989
+ }
2990
+
2991
+ :host(.andy-orientation-left) .andy-chat-bubble {
2992
+ left: 30px;
2993
+ right: auto;
2994
+ }
2995
+
2996
+ :host(.andy-orientation-right) .andy-chat-window {
2997
+ right: 30px;
2998
+ left: auto;
2999
+ }
3000
+
3001
+ :host(.andy-orientation-left) .andy-chat-window {
3002
+ left: 30px;
3003
+ right: auto;
3004
+ }
3005
+ `
3006
+ };
3007
+
3008
+ /**
3009
+ * Gets theme definitions CSS string
3010
+ * @returns {string} Theme CSS content
3011
+ */
3012
+ const getThemeDefinitionsCssString = () => {
3013
+ return `
3014
+ /* Light Theme (Default) */
3015
+ :host(.andy-theme-light) {
3016
+ --andy-themable-background: var(--andy-color-background);
3017
+ --andy-themable-background-secondary: var(--andy-color-white);
3018
+ --andy-themable-background-tertiary: var(--andy-color-gray-100);
3019
+ --andy-themable-text: var(--andy-color-text);
3020
+ --andy-themable-text-secondary: var(--andy-color-text-secondary);
3021
+ --andy-themable-text-muted: var(--andy-color-text-muted);
3022
+ --andy-themable-border: var(--andy-color-border);
3023
+ --andy-themable-hover: var(--andy-color-hover);
3024
+ --andy-themable-button-primary-bg: var(--andy-color-primary);
3025
+ --andy-themable-button-primary-text: var(--andy-color-white);
3026
+ --andy-themable-button-primary-hover: var(--andy-color-primary-hover);
3027
+ --andy-themable-button-secondary-bg: var(--andy-color-gray-200);
3028
+ --andy-themable-button-secondary-text: var(--andy-color-text);
3029
+ --andy-themable-button-secondary-hover: var(--andy-color-gray-300);
3030
+ --andy-themable-input-bg: var(--andy-color-input-background);
3031
+ --andy-themable-input-border: var(--andy-color-border);
3032
+ --andy-themable-input-text: var(--andy-color-text);
3033
+ --andy-themable-input-placeholder: var(--andy-color-text-muted);
3034
+ --andy-themable-message-user-bg: var(--andy-color-user-message);
3035
+ --andy-themable-message-user-text: var(--andy-color-user-message-text);
3036
+ --andy-themable-message-bot-bg: var(--andy-color-bot-message);
3037
+ --andy-themable-message-bot-text: var(--andy-color-bot-message-text);
3038
+ --andy-themable-avatar-bg: var(--andy-color-primary);
3039
+ --andy-themable-avatar-text: var(--andy-color-white);
3040
+ --andy-themable-tooltip-bg: var(--andy-color-gray-800);
3041
+ --andy-themable-tooltip-text: var(--andy-color-white);
3042
+ }
3043
+
3044
+ /* Dark Theme */
3045
+ :host(.andy-theme-dark) {
3046
+ --andy-themable-background: var(--andy-color-background-dark);
3047
+ --andy-themable-background-secondary: var(--andy-color-background-secondary-dark);
3048
+ --andy-themable-background-tertiary: var(--andy-color-background-tertiary-dark);
3049
+ --andy-themable-text: var(--andy-color-text-dark);
3050
+ --andy-themable-text-secondary: var(--andy-color-text-dark);
3051
+ --andy-themable-text-muted: var(--andy-color-text-muted-dark);
3052
+ --andy-themable-border: var(--andy-color-border-dark);
3053
+ --andy-themable-hover: var(--andy-color-hover-dark);
3054
+ --andy-themable-shadow: rgba(0, 0, 0, 0.3);
3055
+ --andy-themable-shadow-light: rgba(0, 0, 0, 0.2);
3056
+ --andy-themable-button-primary-bg: var(--andy-color-primary);
3057
+ --andy-themable-button-primary-text: var(--andy-color-white);
3058
+ --andy-themable-button-primary-hover: var(--andy-color-primary-hover);
3059
+ --andy-themable-button-secondary-bg: var(--andy-color-background-tertiary-dark);
3060
+ --andy-themable-button-secondary-text: var(--andy-color-text-dark);
3061
+ --andy-themable-button-secondary-hover: var(--andy-color-hover-dark);
3062
+ --andy-themable-input-bg: var(--andy-color-background-tertiary-dark);
3063
+ --andy-themable-input-border: var(--andy-color-border-dark);
3064
+ --andy-themable-input-text: var(--andy-color-text-dark);
3065
+ --andy-themable-input-placeholder: var(--andy-color-text-muted-dark);
3066
+ --andy-themable-message-user-bg: var(--andy-color-user-message);
3067
+ --andy-themable-message-user-text: var(--andy-color-user-message-text);
3068
+ --andy-themable-message-bot-bg: #353535;
3069
+ --andy-themable-message-bot-text: var(--andy-color-text-dark);
3070
+ --andy-themable-message-bot-border: rgba(255, 255, 255, 0.08);
3071
+ --andy-themable-avatar-bg: var(--andy-color-primary);
3072
+ --andy-themable-avatar-text: var(--andy-color-white);
3073
+ --andy-themable-tooltip-bg: var(--andy-color-background-secondary-dark);
3074
+ --andy-themable-tooltip-text: var(--andy-color-text-dark);
3075
+ }
3076
+
3077
+ /* Auto Theme */
3078
+ :host(.andy-theme-auto) {
3079
+ --andy-themable-background: var(--andy-color-background);
3080
+ --andy-themable-background-secondary: var(--andy-color-white);
3081
+ --andy-themable-background-tertiary: var(--andy-color-gray-100);
3082
+ --andy-themable-text: var(--andy-color-text);
3083
+ --andy-themable-text-secondary: var(--andy-color-text-secondary);
3084
+ --andy-themable-text-muted: var(--andy-color-text-muted);
3085
+ --andy-themable-border: var(--andy-color-border);
3086
+ --andy-themable-hover: var(--andy-color-hover);
3087
+ --andy-themable-button-primary-bg: var(--andy-color-primary);
3088
+ --andy-themable-button-primary-text: var(--andy-color-white);
3089
+ --andy-themable-button-primary-hover: var(--andy-color-primary-hover);
3090
+ --andy-themable-button-secondary-bg: var(--andy-color-gray-200);
3091
+ --andy-themable-button-secondary-text: var(--andy-color-text);
3092
+ --andy-themable-button-secondary-hover: var(--andy-color-gray-300);
3093
+ --andy-themable-input-bg: var(--andy-color-input-background);
3094
+ --andy-themable-input-border: var(--andy-color-border);
3095
+ --andy-themable-input-text: var(--andy-color-text);
3096
+ --andy-themable-input-placeholder: var(--andy-color-text-muted);
3097
+ --andy-themable-message-user-bg: var(--andy-color-user-message);
3098
+ --andy-themable-message-user-text: var(--andy-color-user-message-text);
3099
+ --andy-themable-message-bot-bg: var(--andy-color-bot-message);
3100
+ --andy-themable-message-bot-text: var(--andy-color-bot-message-text);
3101
+ --andy-themable-avatar-bg: var(--andy-color-primary);
3102
+ --andy-themable-avatar-text: var(--andy-color-white);
3103
+ --andy-themable-tooltip-bg: var(--andy-color-gray-800);
3104
+ --andy-themable-tooltip-text: var(--andy-color-white);
3105
+ }
3106
+
3107
+ @media (prefers-color-scheme: dark) {
3108
+ :host(.andy-theme-auto) {
3109
+ --andy-themable-background: var(--andy-color-background-dark);
3110
+ --andy-themable-background-secondary: var(--andy-color-background-secondary-dark);
3111
+ --andy-themable-background-tertiary: var(--andy-color-background-tertiary-dark);
3112
+ --andy-themable-text: var(--andy-color-text-dark);
3113
+ --andy-themable-text-secondary: var(--andy-color-text-dark);
3114
+ --andy-themable-text-muted: var(--andy-color-text-muted-dark);
3115
+ --andy-themable-border: var(--andy-color-border-dark);
3116
+ --andy-themable-hover: var(--andy-color-hover-dark);
3117
+ --andy-themable-shadow: rgba(0, 0, 0, 0.3);
3118
+ --andy-themable-shadow-light: rgba(0, 0, 0, 0.2);
3119
+ --andy-themable-button-secondary-bg: var(--andy-color-background-tertiary-dark);
3120
+ --andy-themable-button-secondary-text: var(--andy-color-text-dark);
3121
+ --andy-themable-button-secondary-hover: var(--andy-color-hover-dark);
3122
+ --andy-themable-input-bg: var(--andy-color-background-tertiary-dark);
3123
+ --andy-themable-input-border: var(--andy-color-border-dark);
3124
+ --andy-themable-input-text: var(--andy-color-text-dark);
3125
+ --andy-themable-input-placeholder: var(--andy-color-text-muted-dark);
3126
+ --andy-themable-message-bot-bg: #353535;
3127
+ --andy-themable-message-bot-text: var(--andy-color-text-dark);
3128
+ --andy-themable-message-bot-border: rgba(255, 255, 255, 0.08);
3129
+ --andy-themable-tooltip-bg: var(--andy-color-background-secondary-dark);
3130
+ --andy-themable-tooltip-text: var(--andy-color-text-dark);
3131
+ }
3132
+ }
3133
+ `
3134
+ };
3135
+
3136
+ /**
3137
+ * Generates brand color override CSS
3138
+ * @param {string} brandColor - The brand color to apply
3139
+ * @param {boolean} useBrandColor - Whether to apply brand color (default: true)
3140
+ * @returns {string} CSS with brand color overrides
3141
+ */
3142
+ const getBrandColorOverrideCssString = (brandColor, useBrandColor = true) => {
3143
+ if (!useBrandColor) {
3144
+ // Brand color disabled - use theme-aware neutral colors
3145
+ return `
3146
+ /* Bubble: neutral background based on theme */
3147
+ .andy-chat-bubble,
3148
+ .andy-chat-bubble--pill {
3149
+ background: var(--andy-themable-background-tertiary) !important;
3150
+ color: var(--andy-themable-text) !important;
3151
+ }
3152
+ .andy-chat-bubble:hover,
3153
+ .andy-chat-bubble--pill:hover {
3154
+ background: var(--andy-themable-hover) !important;
3155
+ }
3156
+
3157
+ /* Bubble icon: use theme text color for contrast */
3158
+ .andy-chat-bubble__icon {
3159
+ color: var(--andy-themable-text) !important;
3160
+ stroke: var(--andy-themable-text) !important;
3161
+ fill: var(--andy-themable-text) !important;
3162
+ filter: none !important; /* Reset the brightness filter */
3163
+ }
3164
+ .andy-chat-bubble__icon path {
3165
+ stroke: inherit !important;
3166
+ fill: inherit !important;
3167
+ }
3168
+
3169
+ /* Dark mode: use filter to make icon white */
3170
+ .andy-chat-bubble--theme-dark .andy-chat-bubble__icon {
3171
+ filter: brightness(0) invert(1) !important;
3172
+ }
3173
+ .andy-chat-bubble--theme-dark .andy-chat-bubble__icon path {
3174
+ fill: currentColor !important;
3175
+ }
3176
+ .andy-chat-bubble__label {
3177
+ color: var(--andy-themable-text) !important;
3178
+ }
3179
+
3180
+ /* User messages: subtle neutral background */
3181
+ .andy-message--user {
3182
+ background: var(--andy-themable-background-tertiary) !important;
3183
+ color: var(--andy-themable-text) !important;
3184
+ }
3185
+
3186
+ /* Send button: neutral with proper icon contrast */
3187
+ .andy-input__button {
3188
+ background: var(--andy-themable-background-tertiary) !important;
3189
+ color: var(--andy-themable-text) !important;
3190
+ }
3191
+ .andy-input__button:hover {
3192
+ background: var(--andy-themable-hover) !important;
3193
+ }
3194
+ .andy-input__button-icon {
3195
+ color: var(--andy-themable-text) !important;
3196
+ fill: var(--andy-themable-text) !important;
3197
+ }
3198
+
3199
+ /* Avatar: neutral */
3200
+ .andy-bot-header__icon {
3201
+ background: var(--andy-themable-background-tertiary) !important;
3202
+ color: var(--andy-themable-text) !important;
3203
+ }
3204
+
3205
+ /* Header avatar: neutral */
3206
+ .andy-header__avatar {
3207
+ background: var(--andy-themable-background-tertiary) !important;
3208
+ color: var(--andy-themable-text) !important;
3209
+ }
3210
+ .andy-header__avatar-icon--default {
3211
+ filter: none !important;
3212
+ }
3213
+ /* Dark mode: make header avatar icon white */
3214
+ :host(.andy-theme-dark) .andy-header__avatar-icon--default {
3215
+ filter: brightness(0) invert(1) !important;
3216
+ }
3217
+
3218
+ /* Remove welcome popup accent border */
3219
+ .andy-welcome-popup__content {
3220
+ border-left: none !important;
3221
+ }
3222
+
3223
+ /* Predefined questions and links: neutral text color */
3224
+ .andy-predefined-question,
3225
+ .andy-message__link,
3226
+ .andy-chat-faq__back {
3227
+ color: var(--andy-themable-text-secondary) !important;
3228
+ }
3229
+ .andy-predefined-question:hover,
3230
+ .andy-message__link:hover,
3231
+ .andy-chat-faq__back:hover {
3232
+ color: var(--andy-themable-text) !important;
3233
+ }
3234
+ `
3235
+ }
3236
+
3237
+ // Use default blue if no brand color provided
3238
+ const effectiveBrandColor = brandColor || '#3B82F6';
3239
+
3240
+ // Calculate a hover variant (darker by default)
3241
+ const hoverColor = effectiveBrandColor; // For now, using same color. Could implement color manipulation
3242
+
3243
+ // Calculate contrast text color for accessibility
3244
+ const textColor = getContrastTextColor(effectiveBrandColor);
3245
+
3246
+ return `
3247
+ :host {
3248
+ --andy-color-primary: ${effectiveBrandColor};
3249
+ --andy-color-primary-hover: ${hoverColor};
3250
+ --andy-themable-button-primary-bg: ${effectiveBrandColor};
3251
+ --andy-themable-button-primary-hover: ${hoverColor};
3252
+ --andy-themable-button-primary-text: ${textColor};
3253
+ --andy-themable-message-user-bg: ${effectiveBrandColor};
3254
+ --andy-themable-message-user-text: ${textColor};
3255
+ --andy-themable-avatar-bg: ${effectiveBrandColor};
3256
+ }
3257
+
3258
+ /* Override theme-specific button text with contrast color */
3259
+ :host(.andy-theme-light),
3260
+ :host(.andy-theme-dark),
3261
+ :host(.andy-theme-auto) {
3262
+ --andy-themable-button-primary-text: ${textColor};
3263
+ --andy-themable-message-user-text: ${textColor};
3264
+ }
3265
+
3266
+ /* Chat bubble icon contrast color - use !important to override theme defaults */
3267
+ .andy-chat-bubble {
3268
+ color: ${textColor} !important;
3269
+ }
3270
+ .andy-chat-bubble__icon {
3271
+ color: ${textColor} !important;
3272
+ stroke: ${textColor} !important;
3273
+ fill: ${textColor} !important;
3274
+ }
3275
+ .andy-chat-bubble__icon path {
3276
+ stroke: inherit !important;
3277
+ fill: inherit !important;
3278
+ }
3279
+ .andy-chat-bubble__label {
3280
+ color: ${textColor} !important;
3281
+ }
3282
+
3283
+ /* Send button contrast color */
3284
+ .andy-input__button {
3285
+ color: ${textColor} !important;
3286
+ }
3287
+ .andy-input__button-icon {
3288
+ color: ${textColor} !important;
3289
+ fill: ${textColor} !important;
3290
+ }
3291
+
3292
+ /* User message text contrast */
3293
+ .andy-message--user {
3294
+ color: ${textColor} !important;
3295
+ }
3296
+
3297
+ /* Welcome popup accent border when brand color is enabled */
3298
+ .andy-welcome-popup__content {
3299
+ border-left: 3px solid ${effectiveBrandColor};
3300
+ }
3301
+ `
3302
+ };
3303
+
3304
+ /**
3305
+ * Master function: Gets complete stylesheet for the chatbot
3306
+ * @param {Object} customizationProps - Customization options
3307
+ * @param {string} [customizationProps.color] - Brand color
3308
+ * @param {boolean} [customizationProps.useBrandColor] - Whether to apply brand color (default: true)
3309
+ * @param {string} [customizationProps.theme] - Theme (light, dark, auto)
3310
+ * @param {string} [customizationProps.orientation] - Orientation (left, right)
3311
+ * @param {string} [customizationProps.fontFamily] - Custom font family
3312
+ * @param {string} [customizationProps.borderRadius] - Custom border radius
3313
+ * @returns {string} Complete CSS stylesheet
3314
+ */
3315
+ const getFullStylesheet = (customizationProps = {}) => {
3316
+ let finalCss = '';
3317
+
3318
+ // Add design token variables
3319
+ finalCss += generateDesignTokenVariablesString();
3320
+
3321
+ // Add core structural CSS
3322
+ finalCss += getCoreStructuralCssString();
3323
+
3324
+ // Add theme definitions
3325
+ finalCss += getThemeDefinitionsCssString();
3326
+
3327
+ // Apply brand color overrides (or neutral colors if useBrandColor is false)
3328
+ // Always call this function - it handles defaults internally
3329
+ finalCss += getBrandColorOverrideCssString(
3330
+ customizationProps.color,
3331
+ customizationProps.useBrandColor ?? true
3332
+ );
3333
+
3334
+ // Apply font family overrides
3335
+ if (customizationProps.fontFamily) {
3336
+ finalCss += `
3337
+ :host {
3338
+ --andy-font-family-primary: "${customizationProps.fontFamily.replace(/"/g, '\\"')}";
3339
+ }
3340
+ `;
3341
+ }
3342
+
3343
+ // Apply border radius overrides
3344
+ if (customizationProps.borderRadius) {
3345
+ finalCss += `
3346
+ :host {
3347
+ --andy-radius-base: ${customizationProps.borderRadius};
3348
+ --andy-radius-lg: ${customizationProps.borderRadius};
3349
+ }
3350
+ `;
3351
+ }
3352
+
3353
+ return finalCss
3354
+ };
3355
+
3356
+ /**
3357
+ * Gets host element classes for theme and orientation
3358
+ * @param {Object} props - Customization properties
3359
+ * @param {string} [props.theme='light'] - Theme (light, dark, auto)
3360
+ * @param {string} [props.orientation='right'] - Orientation (left, right)
3361
+ * @returns {string} Space-separated class names
3362
+ */
3363
+ const getHostClasses = (props = {}) => {
3364
+ const theme = props.theme || 'light';
3365
+ const orientation = props.orientation || 'right';
3366
+ return `andy-theme-${theme} andy-orientation-${orientation}`
3367
+ };
3368
+
3369
+ /**
3370
+ * AndyChat UI Constants
3371
+ * Single source of truth for all UI elements (SVGs, images, etc.)
3372
+ * Both vanilla and React implementations MUST use these constants
3373
+ */
3374
+
3375
+ // Chat Bubble Icons and Images
3376
+ // Embed the bot icon as a data URI so every bundler (React & vanilla) renders the same SVG.
3377
+ const chatBubbleIconUrl =
3378
+ 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWJvdC1pY29uIGx1Y2lkZS1ib3QiPjxwYXRoIGQ9Ik0xMiA4VjRIOCIvPjxyZWN0IHdpZHRoPSIxNiIgaGVpZ2h0PSIxMiIgeD0iNCIgeT0iOCIgcng9IjIiLz48cGF0aCBkPSJNMiAxNGgyIi8+PHBhdGggZD0iTTIwIDE0aDIiLz48cGF0aCBkPSJNMTUgMTN2MiIvPjxwYXRoIGQ9Ik05IDEzdjIiLz48L3N2Zz4K';
3379
+
3380
+ const CHAT_BUBBLE_DEFAULT_ICON_URL = chatBubbleIconUrl;
3381
+ const CHAT_BUBBLE_DEFAULT_ICON = `<img src="${CHAT_BUBBLE_DEFAULT_ICON_URL}" alt="Chat" class="andy-chat-bubble__icon" />`;
3382
+
3383
+ // Footer Constants
3384
+ const FOOTER_TEXT = 'Powered by Andy';
3385
+ const FOOTER_URL = 'https://andypartner.com/';
3386
+
3387
+ /**
3388
+ * Get footer URL with tracking parameter based on current hostname
3389
+ * @returns {string} Footer URL with ref parameter
3390
+ */
3391
+ const getFooterUrl = () => {
3392
+ try {
3393
+ // Check if we're in a browser environment
3394
+ if (typeof window === 'undefined' || !window.location) {
3395
+ return FOOTER_URL
3396
+ }
3397
+
3398
+ // Get hostname, handling various edge cases
3399
+ let hostname = window.location.hostname || 'direct';
3400
+
3401
+ // Handle localhost and IP addresses
3402
+ if (
3403
+ hostname === 'localhost' ||
3404
+ hostname === '127.0.0.1' ||
3405
+ hostname.match(/^\d+\.\d+\.\d+\.\d+$/)
3406
+ ) {
3407
+ hostname = 'localhost';
3408
+ } else {
3409
+ // Clean the hostname - remove www prefix
3410
+ hostname = hostname.replace(/^www\./, '').toLowerCase();
3411
+ }
3412
+
3413
+ // URL encode the hostname to handle international domains
3414
+ const encodedHost = encodeURIComponent(hostname);
3415
+
3416
+ // Build the URL with ref parameter
3417
+ return `${FOOTER_URL}?ref=${encodedHost}`
3418
+ } catch (error) {
3419
+ // Fallback for any errors (SSR, restricted environments, etc.)
3420
+ // Silently fall back to base URL without tracking
3421
+ return FOOTER_URL
3422
+ }
3423
+ };
3424
+
3425
+ // SVG Icons - extracted from vanilla implementation
3426
+ const SVG_ICONS = {
3427
+ // Chevron down (for close state) - same filled icon as Vanilla for parity
3428
+ chevronDown: `<svg class="andy-chat-bubble__icon" width="28" height="28" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
3429
+ <path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z" />
3430
+ </svg>`};
3431
+
3432
+ /**
3433
+ * Keyboard detection utility for handling mobile virtual keyboards
3434
+ * Provides cross-browser support for detecting keyboard open/close events
3435
+ */
3436
+
3437
+ class KeyboardDetector {
3438
+ constructor() {
3439
+ this.isKeyboardOpen = false;
3440
+ this.keyboardHeight = 0;
3441
+ this.listeners = new Set();
3442
+ this.inputElement = null;
3443
+ this.viewportHeight = window.innerHeight;
3444
+ this.visualViewport = window.visualViewport;
3445
+
3446
+ this.initializeDetection();
3447
+ }
3448
+
3449
+ /**
3450
+ * Initialize keyboard detection
3451
+ */
3452
+ initializeDetection() {
3453
+ // Use Visual Viewport API if available (most modern browsers)
3454
+ if (this.visualViewport) {
3455
+ this.visualViewport.addEventListener('resize', this.handleViewportResize.bind(this));
3456
+ this.visualViewport.addEventListener('scroll', this.handleViewportScroll.bind(this));
3457
+ } else {
3458
+ // Fallback for older browsers
3459
+ window.addEventListener('resize', this.handleWindowResize.bind(this));
3460
+ }
3461
+
3462
+ // Additional detection via focus/blur events
3463
+ document.addEventListener('focusin', this.handleFocusIn.bind(this), true);
3464
+ document.addEventListener('focusout', this.handleFocusOut.bind(this), true);
3465
+
3466
+ // Orientation change handler
3467
+ window.addEventListener('orientationchange', this.handleOrientationChange.bind(this));
3468
+ }
3469
+
3470
+ /**
3471
+ * Handle visual viewport resize (modern browsers)
3472
+ */
3473
+ handleViewportResize() {
3474
+ const currentHeight = this.visualViewport.height;
3475
+ const heightDifference = window.innerHeight - currentHeight;
3476
+
3477
+ // If viewport shrunk by more than 100px, keyboard is likely open
3478
+ if (heightDifference > 100) {
3479
+ this.setKeyboardOpen(true, heightDifference);
3480
+ } else if (heightDifference < 50) {
3481
+ this.setKeyboardOpen(false, 0);
3482
+ }
3483
+ }
3484
+
3485
+ /**
3486
+ * Handle visual viewport scroll
3487
+ */
3488
+ handleViewportScroll() {
3489
+ // On iOS, viewport scrolls when keyboard opens
3490
+ if (this.visualViewport.offsetTop > 0) {
3491
+ this.setKeyboardOpen(true, this.visualViewport.offsetTop);
3492
+ }
3493
+ }
3494
+
3495
+ /**
3496
+ * Handle window resize (fallback for older browsers)
3497
+ */
3498
+ handleWindowResize() {
3499
+ const currentHeight = window.innerHeight;
3500
+ const heightDifference = this.viewportHeight - currentHeight;
3501
+
3502
+ // Significant height reduction likely means keyboard opened
3503
+ if (heightDifference > 100) {
3504
+ this.setKeyboardOpen(true, heightDifference);
3505
+ } else if (Math.abs(heightDifference) < 50) {
3506
+ this.setKeyboardOpen(false, 0);
3507
+ this.viewportHeight = currentHeight;
3508
+ }
3509
+ }
3510
+
3511
+ /**
3512
+ * Handle input focus
3513
+ */
3514
+ handleFocusIn(e) {
3515
+ const isInput = ['input', 'textarea'].includes(e.target.tagName.toLowerCase());
3516
+ if (isInput) {
3517
+ this.inputElement = e.target;
3518
+ // Delay to allow keyboard animation
3519
+ setTimeout(() => {
3520
+ if (this.inputElement === e.target) {
3521
+ this.checkKeyboardStatus();
3522
+ }
3523
+ }, 300);
3524
+ }
3525
+ }
3526
+
3527
+ /**
3528
+ * Handle input blur
3529
+ */
3530
+ handleFocusOut(e) {
3531
+ const isInput = ['input', 'textarea'].includes(e.target.tagName.toLowerCase());
3532
+ if (isInput && this.inputElement === e.target) {
3533
+ this.inputElement = null;
3534
+ // Delay to allow keyboard animation
3535
+ setTimeout(() => {
3536
+ if (!this.inputElement) {
3537
+ this.setKeyboardOpen(false, 0);
3538
+ }
3539
+ }, 300);
3540
+ }
3541
+ }
3542
+
3543
+ /**
3544
+ * Handle orientation change
3545
+ */
3546
+ handleOrientationChange() {
3547
+ // Reset viewport height after orientation change
3548
+ setTimeout(() => {
3549
+ this.viewportHeight = window.innerHeight;
3550
+ this.checkKeyboardStatus();
3551
+ }, 500);
3552
+ }
3553
+
3554
+ /**
3555
+ * Check keyboard status (for browsers without Visual Viewport API)
3556
+ */
3557
+ checkKeyboardStatus() {
3558
+ if (!this.visualViewport) {
3559
+ const currentHeight = window.innerHeight;
3560
+ const heightDifference = this.viewportHeight - currentHeight;
3561
+
3562
+ if (this.inputElement && heightDifference > 100) {
3563
+ this.setKeyboardOpen(true, heightDifference);
3564
+ }
3565
+ }
3566
+ }
3567
+
3568
+ /**
3569
+ * Set keyboard state and notify listeners
3570
+ */
3571
+ setKeyboardOpen(isOpen, height) {
3572
+ const changed = this.isKeyboardOpen !== isOpen || this.keyboardHeight !== height;
3573
+
3574
+ this.isKeyboardOpen = isOpen;
3575
+ this.keyboardHeight = height;
3576
+
3577
+ if (changed) {
3578
+ this.notifyListeners();
3579
+ }
3580
+ }
3581
+
3582
+ /**
3583
+ * Add listener for keyboard state changes
3584
+ */
3585
+ addListener(callback) {
3586
+ this.listeners.add(callback);
3587
+ // Immediately notify with current state
3588
+ callback({
3589
+ isOpen: this.isKeyboardOpen,
3590
+ height: this.keyboardHeight
3591
+ });
3592
+ }
3593
+
3594
+ /**
3595
+ * Remove listener
3596
+ */
3597
+ removeListener(callback) {
3598
+ this.listeners.delete(callback);
3599
+ }
3600
+
3601
+ /**
3602
+ * Notify all listeners of state change
3603
+ */
3604
+ notifyListeners() {
3605
+ const state = {
3606
+ isOpen: this.isKeyboardOpen,
3607
+ height: this.keyboardHeight
3608
+ };
3609
+
3610
+ this.listeners.forEach(callback => {
3611
+ try {
3612
+ callback(state);
3613
+ } catch (error) {
3614
+ console.error('Error in keyboard detector listener:', error);
3615
+ }
3616
+ });
3617
+ }
3618
+
3619
+ /**
3620
+ * Get current keyboard state
3621
+ */
3622
+ getState() {
3623
+ return {
3624
+ isOpen: this.isKeyboardOpen,
3625
+ height: this.keyboardHeight
3626
+ }
3627
+ }
3628
+
3629
+ /**
3630
+ * Cleanup
3631
+ */
3632
+ destroy() {
3633
+ if (this.visualViewport) {
3634
+ this.visualViewport.removeEventListener('resize', this.handleViewportResize.bind(this));
3635
+ this.visualViewport.removeEventListener('scroll', this.handleViewportScroll.bind(this));
3636
+ } else {
3637
+ window.removeEventListener('resize', this.handleWindowResize.bind(this));
3638
+ }
3639
+
3640
+ document.removeEventListener('focusin', this.handleFocusIn.bind(this), true);
3641
+ document.removeEventListener('focusout', this.handleFocusOut.bind(this), true);
3642
+ window.removeEventListener('orientationchange', this.handleOrientationChange.bind(this));
3643
+
3644
+ this.listeners.clear();
3645
+ }
3646
+ }
3647
+
3648
+ // Create singleton instance
3649
+ let keyboardDetectorInstance = null;
3650
+
3651
+ /**
3652
+ * Get or create keyboard detector instance
3653
+ */
3654
+ function getKeyboardDetector() {
3655
+ if (!keyboardDetectorInstance) {
3656
+ keyboardDetectorInstance = new KeyboardDetector();
3657
+ }
3658
+ return keyboardDetectorInstance
3659
+ }
3660
+
3661
+ /**
3662
+ * Update CSS custom property on host element
3663
+ */
3664
+ function updateKeyboardHeight(hostElement, height) {
3665
+ if (hostElement) {
3666
+ hostElement.style.setProperty('--andy-keyboard-height', `${height}px`);
3667
+ }
3668
+ }
3669
+
3670
+ /**
3671
+ * Mobile detection utility for AndyChat
3672
+ * Provides reliable mobile device detection with multiple methods
3673
+ */
3674
+
3675
+ /**
3676
+ * Check if the current device is mobile
3677
+ * Uses multiple detection methods for reliability
3678
+ * @returns {boolean} True if device is mobile, false otherwise
3679
+ */
3680
+ const isMobileDevice = () => {
3681
+ // Method 1: Check for touch capability and screen size
3682
+ const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
3683
+ const isSmallScreen = window.innerWidth <= 768;
3684
+
3685
+ // Method 2: Check user agent for mobile patterns
3686
+ const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
3687
+ const isMobileUserAgent = mobileRegex.test(navigator.userAgent);
3688
+
3689
+ // Method 4: Check CSS media query
3690
+ const isMobileMediaQuery = window.matchMedia('(max-width: 768px) and (hover: none)').matches;
3691
+
3692
+ // Return true if any strong mobile indicators are present
3693
+ // Prioritize touch + small screen or explicit mobile user agent
3694
+ return (hasTouch && isSmallScreen) || isMobileUserAgent || isMobileMediaQuery
3695
+ };
3696
+
3697
+ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessage, bubbleLabel, theme, orientation, brandColor, bubbleStyle, useBrandColor, logoUrl, previewMode = false, initialOpen = false }) => {
3698
+ const [config, setConfig] = useState(null);
3699
+ const [isOpen, setIsOpen] = useState(initialOpen);
3700
+ const [messages, setMessages] = useState([]);
3701
+ const [isLoading, setIsLoading] = useState(false);
3702
+ const [sessionId, setSessionId] = useState(() => generateSessionId());
3703
+ const [shadowReady, setShadowReady] = useState(false);
3704
+ const [showPredefinedQuestions, setShowPredefinedQuestions] = useState(true);
3705
+ const [userInfoCollected, setUserInfoCollected] = useState(false);
3706
+ const [isCollectingUserInfo, setIsCollectingUserInfo] = useState(false);
3707
+ const [pendingMessage, setPendingMessage] = useState(null);
3708
+ const [showWelcomePopup, setShowWelcomePopup] = useState(false);
3709
+ const containerRef = useRef(null);
3710
+ const shadowRef = useRef(null);
3711
+ const portalRef = useRef(null);
3712
+ const hasOpenedRef = useRef(false);
3713
+ const messagesContainerRef = useRef(null);
3714
+ const welcomePopupShownRef = useRef(false);
3715
+ const welcomePopupTimeoutRef = useRef(null);
3716
+ // Create shadow DOM container on mount
3717
+ useEffect(() => {
3718
+ if (!containerRef.current)
3719
+ return;
3720
+ let shadow = shadowRef.current;
3721
+ if (!shadow) {
3722
+ if (containerRef.current.shadowRoot) {
3723
+ shadow = containerRef.current.shadowRoot;
3724
+ }
3725
+ else {
3726
+ shadow = containerRef.current.attachShadow({ mode: 'open' });
3727
+ }
3728
+ shadowRef.current = shadow;
3729
+ }
3730
+ let styleElement = shadow.querySelector('style');
3731
+ if (!styleElement) {
3732
+ styleElement = document.createElement('style');
3733
+ shadow.appendChild(styleElement);
3734
+ }
3735
+ styleElement.textContent = getFullStylesheet({
3736
+ color: config?.brandColor,
3737
+ useBrandColor: config?.useBrandColor ?? true,
3738
+ theme: config?.theme || 'light',
3739
+ orientation: config?.orientation || 'right'
3740
+ });
3741
+ if (containerRef.current) {
3742
+ const hostClasses = getHostClasses({
3743
+ theme: config?.theme || 'light',
3744
+ orientation: config?.orientation || 'right'
3745
+ });
3746
+ containerRef.current.className = hostClasses;
3747
+ }
3748
+ if (!portalRef.current) {
3749
+ let portalTarget = shadow.querySelector('.andy-react-portal');
3750
+ if (!portalTarget) {
3751
+ portalTarget = document.createElement('div');
3752
+ portalTarget.className = 'andy-react-portal';
3753
+ shadow.appendChild(portalTarget);
3754
+ }
3755
+ portalRef.current = portalTarget;
3756
+ }
3757
+ setShadowReady(true);
3758
+ const keyboardDetector = getKeyboardDetector();
3759
+ const keyboardListener = ({ isOpen: kbOpen, height }) => {
3760
+ updateKeyboardHeight(containerRef.current, height);
3761
+ const chatWindow = shadowRef.current?.querySelector('.andy-chat-window');
3762
+ if (chatWindow) {
3763
+ if (kbOpen && height > 0) {
3764
+ chatWindow.classList.add('andy-chat-window--keyboard-open');
3765
+ }
3766
+ else {
3767
+ chatWindow.classList.remove('andy-chat-window--keyboard-open');
3768
+ }
3769
+ }
3770
+ };
3771
+ keyboardDetector.addListener(keyboardListener);
3772
+ return () => {
3773
+ keyboardDetector.removeListener(keyboardListener);
3774
+ shadowRef.current = null;
3775
+ portalRef.current = null;
3776
+ setShadowReady(false);
3777
+ };
3778
+ }, []);
3779
+ // Update styles when config changes
3780
+ useEffect(() => {
3781
+ if (!shadowRef.current || !config)
3782
+ return;
3783
+ const styleElement = shadowRef.current.querySelector('style');
3784
+ if (styleElement) {
3785
+ styleElement.textContent = getFullStylesheet({
3786
+ color: config.brandColor,
3787
+ useBrandColor: config.useBrandColor ?? true,
3788
+ theme: config.theme || 'light',
3789
+ orientation: config.orientation || 'right'
3790
+ });
3791
+ }
3792
+ if (containerRef.current) {
3793
+ const hostClasses = getHostClasses({
3794
+ theme: config.theme || 'light',
3795
+ orientation: config.orientation || 'right'
3796
+ });
3797
+ containerRef.current.className = hostClasses;
3798
+ }
3799
+ }, [config]);
3800
+ // Close button handler
3801
+ useEffect(() => {
3802
+ if (!shadowReady || !shadowRef.current || !isOpen)
3803
+ return;
3804
+ const timer = setTimeout(() => {
3805
+ const closeButton = shadowRef.current?.querySelector('#andy-close-button');
3806
+ if (closeButton) {
3807
+ const handleCloseClick = () => {
3808
+ setIsOpen(false);
3809
+ };
3810
+ closeButton.addEventListener('click', handleCloseClick);
3811
+ closeButton._closeHandler = handleCloseClick;
3812
+ }
3813
+ }, 100);
3814
+ return () => {
3815
+ clearTimeout(timer);
3816
+ if (shadowRef.current) {
3817
+ const closeButton = shadowRef.current.querySelector('#andy-close-button');
3818
+ if (closeButton && closeButton._closeHandler) {
3819
+ closeButton.removeEventListener('click', closeButton._closeHandler);
3820
+ delete closeButton._closeHandler;
3821
+ }
3822
+ }
3823
+ };
3824
+ }, [isOpen, shadowReady]);
3825
+ // Click outside to close
3826
+ useEffect(() => {
3827
+ if (!shadowReady || !shadowRef.current)
3828
+ return;
3829
+ if (previewMode)
3830
+ return;
3831
+ const handleDocumentClick = (event) => {
3832
+ if (isMobileDevice())
3833
+ return;
3834
+ if (!isOpen)
3835
+ return;
3836
+ const selection = window.getSelection();
3837
+ if (selection && selection.toString().length > 0) {
3838
+ return;
3839
+ }
3840
+ const chatbotContainer = containerRef.current;
3841
+ let isClickInsideChatbot = false;
3842
+ if (chatbotContainer && chatbotContainer.contains(event.target)) {
3843
+ isClickInsideChatbot = true;
3844
+ }
3845
+ if (event.composedPath) {
3846
+ const path = event.composedPath();
3847
+ if (chatbotContainer && path.includes(chatbotContainer)) {
3848
+ isClickInsideChatbot = true;
3849
+ }
3850
+ }
3851
+ if (event.target === chatbotContainer) {
3852
+ isClickInsideChatbot = true;
3853
+ }
3854
+ if (!isClickInsideChatbot) {
3855
+ setIsOpen(false);
3856
+ }
3857
+ };
3858
+ const handleShadowClick = (event) => {
3859
+ const target = event.target;
3860
+ if (target.closest('#andy-close-button') ||
3861
+ target.closest('#andy-send-button') ||
3862
+ target.closest('.andy-predefined-question')) {
3863
+ return;
3864
+ }
3865
+ if (target.closest('.andy-chat-window')) {
3866
+ event.stopPropagation();
3867
+ }
3868
+ };
3869
+ document.addEventListener('click', handleDocumentClick);
3870
+ shadowRef.current.addEventListener('click', handleShadowClick, true);
3871
+ return () => {
3872
+ document.removeEventListener('click', handleDocumentClick);
3873
+ if (shadowRef.current) {
3874
+ shadowRef.current.removeEventListener('click', handleShadowClick, true);
3875
+ }
3876
+ };
3877
+ }, [isOpen, shadowReady, previewMode]);
3878
+ // Initialize config
3879
+ useEffect(() => {
3880
+ if (previewMode) {
3881
+ const previewConfig = {
3882
+ id: embedId || 'preview',
3883
+ name: name || 'Andy Bot',
3884
+ theme: theme || 'light',
3885
+ logoUrl: logoUrl || '',
3886
+ headline: headline || '',
3887
+ brandColor: brandColor || '#3B82F6',
3888
+ description: description || '',
3889
+ orientation: orientation || 'right',
3890
+ bubbleStyle: bubbleStyle || 'icon',
3891
+ bubbleLabel: bubbleLabel || null,
3892
+ useBrandColor: useBrandColor !== undefined ? useBrandColor : true,
3893
+ defaultLang: 'en',
3894
+ showBranding: true,
3895
+ showWelcomePopup: { enabled: false, seconds: 3, message: '' },
3896
+ collectUserInfo: { enabled: false, message: '' },
3897
+ welcomeMessage: welcomeMessage || 'Hi there! How can I help you today?',
3898
+ predefinedQuestions: [],
3899
+ faqs: []
3900
+ };
3901
+ setConfig(previewConfig);
3902
+ return;
3903
+ }
3904
+ if (!embedId || !isValidEmbedId(embedId)) {
3905
+ setConfig(null);
3906
+ console.warn('AndyChat: Invalid or missing embedId');
3907
+ return;
3908
+ }
3909
+ let cancelled = false;
3910
+ const loadConfig = async () => {
3911
+ try {
3912
+ setConfig(null);
3913
+ const rawConfig = await fetchChatbotConfig(embedId);
3914
+ const configWithOverrides = {
3915
+ ...rawConfig,
3916
+ ...(name && { name }),
3917
+ ...(headline && { headline }),
3918
+ ...(description && { description }),
3919
+ ...(welcomeMessage && { welcomeMessage }),
3920
+ ...(theme && { theme }),
3921
+ ...(orientation && { orientation }),
3922
+ ...(brandColor && { brandColor }),
3923
+ ...(bubbleStyle && { bubbleStyle }),
3924
+ ...(bubbleLabel && { bubbleLabel }),
3925
+ ...(useBrandColor !== undefined && { useBrandColor }),
3926
+ ...(logoUrl && { logoUrl })
3927
+ };
3928
+ if (!cancelled)
3929
+ setConfig(configWithOverrides);
3930
+ }
3931
+ catch {
3932
+ if (!cancelled) {
3933
+ setConfig(null);
3934
+ console.error('AndyChat: Failed to load configuration.');
3935
+ }
3936
+ }
3937
+ };
3938
+ loadConfig();
3939
+ return () => {
3940
+ cancelled = true;
3941
+ };
3942
+ }, [
3943
+ embedId,
3944
+ name,
3945
+ headline,
3946
+ description,
3947
+ welcomeMessage,
3948
+ previewMode,
3949
+ theme,
3950
+ orientation,
3951
+ brandColor,
3952
+ bubbleStyle,
3953
+ bubbleLabel,
3954
+ useBrandColor,
3955
+ logoUrl
3956
+ ]);
3957
+ // Welcome popup timer
3958
+ useEffect(() => {
3959
+ if (welcomePopupTimeoutRef.current) {
3960
+ clearTimeout(welcomePopupTimeoutRef.current);
3961
+ welcomePopupTimeoutRef.current = null;
3962
+ }
3963
+ if (config?.showWelcomePopup?.enabled && !welcomePopupShownRef.current && !isOpen) {
3964
+ const delaySeconds = config.showWelcomePopup.seconds ?? 3;
3965
+ welcomePopupTimeoutRef.current = setTimeout(() => {
3966
+ if (!isOpen) {
3967
+ setShowWelcomePopup(true);
3968
+ welcomePopupShownRef.current = true;
3969
+ }
3970
+ }, delaySeconds * 1000);
3971
+ }
3972
+ if (isOpen && showWelcomePopup) {
3973
+ setShowWelcomePopup(false);
3974
+ }
3975
+ return () => {
3976
+ if (welcomePopupTimeoutRef.current) {
3977
+ clearTimeout(welcomePopupTimeoutRef.current);
3978
+ }
3979
+ };
3980
+ }, [config, isOpen, showWelcomePopup]);
3981
+ // Add welcome message on first open
3982
+ useEffect(() => {
3983
+ if (isOpen && !hasOpenedRef.current && config?.welcomeMessage) {
3984
+ hasOpenedRef.current = true;
3985
+ const greeting = createGreetingMessage(config);
3986
+ if (greeting) {
3987
+ setMessages([
3988
+ {
3989
+ id: Date.now(),
3990
+ content: greeting,
3991
+ sender: 'bot',
3992
+ timestamp: new Date().toISOString()
3993
+ }
3994
+ ]);
3995
+ }
3996
+ }
3997
+ }, [isOpen, config]);
3998
+ // Auto-scroll
3999
+ useEffect(() => {
4000
+ if (messagesContainerRef.current) {
4001
+ messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
4002
+ }
4003
+ }, [messages, isLoading]);
4004
+ // Form submit handler
4005
+ useEffect(() => {
4006
+ if (isCollectingUserInfo && shadowRef.current) {
4007
+ const timer = setTimeout(() => {
4008
+ const form = shadowRef.current?.querySelector('#andy-user-info-form');
4009
+ if (form) {
4010
+ const handleSubmit = (e) => {
4011
+ handleUserInfoSubmit(e);
4012
+ };
4013
+ form.addEventListener('submit', handleSubmit);
4014
+ form._submitHandler = handleSubmit;
4015
+ }
4016
+ }, 100);
4017
+ return () => {
4018
+ clearTimeout(timer);
4019
+ if (shadowRef.current) {
4020
+ const form = shadowRef.current.querySelector('#andy-user-info-form');
4021
+ if (form && form._submitHandler) {
4022
+ form.removeEventListener('submit', form._submitHandler);
4023
+ delete form._submitHandler;
4024
+ }
4025
+ }
4026
+ };
4027
+ }
4028
+ }, [isCollectingUserInfo, messages]);
4029
+ const sendUserMessage = useCallback(async (rawMessageText, options = {}) => {
4030
+ const messageText = typeof rawMessageText === 'string' ? rawMessageText.trim() : '';
4031
+ if (!messageText) {
4032
+ return;
4033
+ }
4034
+ const { hidePredefined = true } = options;
4035
+ const timestamp = Date.now();
4036
+ const userMessage = {
4037
+ id: timestamp,
4038
+ content: messageText,
4039
+ sender: 'user',
4040
+ timestamp: new Date().toISOString()
4041
+ };
4042
+ setMessages(prev => [...prev, userMessage]);
4043
+ if (hidePredefined) {
4044
+ setShowPredefinedQuestions(false);
4045
+ }
4046
+ try {
4047
+ setIsLoading(true);
4048
+ const response = await sendMessage(embedId, sessionId, messageText);
4049
+ const botMessage = {
4050
+ id: timestamp + 1,
4051
+ content: response,
4052
+ sender: 'bot',
4053
+ timestamp: new Date().toISOString()
4054
+ };
4055
+ setMessages(prev => [...prev, botMessage]);
4056
+ if (config?.collectUserInfo?.enabled && !userInfoCollected && !isCollectingUserInfo) {
4057
+ setPendingMessage(messageText);
4058
+ setIsCollectingUserInfo(true);
4059
+ const formMessage = {
4060
+ id: timestamp + 2,
4061
+ content: 'USER_INFO_FORM',
4062
+ sender: 'bot',
4063
+ isForm: true,
4064
+ timestamp: new Date().toISOString()
4065
+ };
4066
+ setMessages(prev => [...prev, formMessage]);
4067
+ }
4068
+ }
4069
+ catch {
4070
+ console.error('AndyChat: Message request failed.');
4071
+ const errorMessage = {
4072
+ id: timestamp + 3,
4073
+ content: config?.defaultLang === 'es'
4074
+ ? 'Lo siento, no pude responder. Inténtalo de nuevo.'
4075
+ : 'Sorry, I could not answer. Please try again.',
4076
+ sender: 'bot',
4077
+ timestamp: new Date().toISOString(),
4078
+ isError: true
4079
+ };
4080
+ setMessages(prev => [...prev, errorMessage]);
4081
+ }
4082
+ finally {
4083
+ setIsLoading(false);
4084
+ }
4085
+ }, [config, embedId, sessionId, userInfoCollected, isCollectingUserInfo]);
4086
+ const handleSendMessage = useCallback((messageText) => {
4087
+ return sendUserMessage(messageText);
4088
+ }, [sendUserMessage]);
4089
+ const handlePredefinedQuestion = useCallback((question) => {
4090
+ if (isCollectingUserInfo || isLoading) {
4091
+ return;
4092
+ }
4093
+ const trimmedQuestion = typeof question === 'string' ? question.trim() : '';
4094
+ if (!trimmedQuestion) {
4095
+ return;
4096
+ }
4097
+ const input = shadowRef.current?.getElementById('andy-message-input');
4098
+ if (input) {
4099
+ input.value = '';
4100
+ }
4101
+ sendUserMessage(trimmedQuestion);
4102
+ }, [isCollectingUserInfo, isLoading, sendUserMessage]);
4103
+ // Predefined questions click handler
4104
+ useEffect(() => {
4105
+ if (!shadowRef.current) {
4106
+ return;
4107
+ }
4108
+ const shadowRoot = shadowRef.current;
4109
+ const captureClick = (event) => {
4110
+ const composedTarget = event.composedPath ? event.composedPath()[0] : event.target;
4111
+ const elementTarget = composedTarget instanceof Element ? composedTarget : null;
4112
+ const button = elementTarget?.closest?.('.andy-predefined-question') ||
4113
+ shadowRoot.querySelector('.andy-predefined-question:focus');
4114
+ if (!button) {
4115
+ return;
4116
+ }
4117
+ if (button.disabled) {
4118
+ return;
4119
+ }
4120
+ const question = button.getAttribute('data-question') || button.textContent || '';
4121
+ handlePredefinedQuestion(question);
4122
+ };
4123
+ shadowRoot.addEventListener('click', captureClick);
4124
+ return () => {
4125
+ shadowRoot.removeEventListener('click', captureClick);
4126
+ };
4127
+ }, [handlePredefinedQuestion, shadowReady]);
4128
+ const handleUserInfoSubmit = async (e) => {
4129
+ e.preventDefault();
4130
+ const form = e.target;
4131
+ const nameInput = form.querySelector('#andy-user-name');
4132
+ const emailInput = form.querySelector('#andy-user-email');
4133
+ const userName = nameInput?.value;
4134
+ const email = emailInput?.value;
4135
+ if (!userName || !email)
4136
+ return;
4137
+ setMessages(prev => prev.filter(msg => !msg.isForm));
4138
+ const loadingMessage = {
4139
+ id: Date.now(),
4140
+ content: 'LOADING',
4141
+ sender: 'bot',
4142
+ isLoading: true,
4143
+ timestamp: new Date().toISOString()
4144
+ };
4145
+ setMessages(prev => [...prev, loadingMessage]);
4146
+ try {
4147
+ await submitUserInfo(embedId, sessionId, { name: userName, email });
4148
+ setUserInfoCollected(true);
4149
+ setIsCollectingUserInfo(false);
4150
+ setMessages(prev => prev.filter(msg => !msg.isLoading));
4151
+ setPendingMessage(null);
4152
+ }
4153
+ catch (error) {
4154
+ console.error('Error submitting user info:', error);
4155
+ setMessages(prev => prev.filter(msg => !msg.isLoading));
4156
+ const errorMessage = {
4157
+ id: Date.now() + 1,
4158
+ content: 'Sorry, there was an error submitting your information. Please try again.',
4159
+ sender: 'bot',
4160
+ timestamp: new Date().toISOString()
4161
+ };
4162
+ setMessages(prev => [...prev, errorMessage]);
4163
+ setIsCollectingUserInfo(false);
4164
+ }
4165
+ };
4166
+ if (!previewMode && (!embedId || !isValidEmbedId(embedId))) {
4167
+ return null;
4168
+ }
4169
+ // Render bubble
4170
+ useEffect(() => {
4171
+ if (!shadowRef.current || !shadowReady)
4172
+ return;
4173
+ if (!config) {
4174
+ shadowRef.current.querySelector('.andy-chat-bubble')?.remove();
4175
+ return;
4176
+ }
4177
+ let bubble = shadowRef.current.querySelector('.andy-chat-bubble');
4178
+ if (!bubble) {
4179
+ bubble = document.createElement('button');
4180
+ bubble.type = 'button';
4181
+ bubble.className = 'andy-chat-bubble';
4182
+ bubble.setAttribute('aria-label', 'Open chat');
4183
+ const tooltipHTML = tooltip
4184
+ ? `<span class="andy-chat-bubble__tooltip" role="tooltip">${tooltip}</span>`
4185
+ : '';
4186
+ bubble.innerHTML = `${CHAT_BUBBLE_DEFAULT_ICON}${tooltipHTML}`;
4187
+ shadowRef.current.appendChild(bubble);
4188
+ bubble.addEventListener('click', () => setIsOpen(prev => !prev));
4189
+ }
4190
+ const isPillStyle = config?.bubbleStyle === 'pill';
4191
+ const displayLabel = bubbleLabel || config?.bubbleLabel || (isPillStyle ? getDefaultBubbleLabel(config?.defaultLang || 'en') : null);
4192
+ const bubbleLogoUrl = config?.logoUrl || CHAT_BUBBLE_DEFAULT_ICON_URL;
4193
+ if (bubble) {
4194
+ bubble.className = 'andy-chat-bubble';
4195
+ if (isPillStyle) {
4196
+ bubble.classList.add('andy-chat-bubble--pill');
4197
+ }
4198
+ if (isOpen) {
4199
+ bubble.classList.add('andy-chat-bubble--open');
4200
+ }
4201
+ if (config?.theme === 'dark') {
4202
+ bubble.classList.add('andy-chat-bubble--theme-dark');
4203
+ }
4204
+ if (config) {
4205
+ bubble.classList.add('andy-chat-bubble--loaded');
4206
+ }
4207
+ const tooltipHTML = !isOpen && tooltip && !isPillStyle
4208
+ ? `<span class="andy-chat-bubble__tooltip" role="tooltip">${tooltip}</span>`
4209
+ : '';
4210
+ const configUseBrandColor = config?.useBrandColor ?? true;
4211
+ const isDarkTheme = config?.theme === 'dark';
4212
+ const needsWhiteIcon = configUseBrandColor || isDarkTheme;
4213
+ const iconFilterStyle = needsWhiteIcon ? '' : 'filter: none;';
4214
+ const darkModeIconStyle = !configUseBrandColor && isDarkTheme ? 'filter: brightness(0) invert(1);' : iconFilterStyle;
4215
+ if (isOpen) {
4216
+ const chevronWithStyle = SVG_ICONS.chevronDown.replace('class="andy-chat-bubble__icon"', `class="andy-chat-bubble__icon" style="${darkModeIconStyle}"`);
4217
+ bubble.innerHTML = chevronWithStyle;
4218
+ }
4219
+ else if (isPillStyle && displayLabel) {
4220
+ bubble.innerHTML = `
4221
+ <span class="andy-chat-bubble__label">${displayLabel}</span>
4222
+ <img src="${bubbleLogoUrl}" alt="Chat" class="andy-chat-bubble__icon--pill" style="${darkModeIconStyle}" />
4223
+ `;
4224
+ }
4225
+ else {
4226
+ if (config?.logoUrl) {
4227
+ bubble.innerHTML = `<img src="${config.logoUrl}" alt="Chatbot Logo" class="andy-chat-bubble__logo" style="${darkModeIconStyle}" />${tooltipHTML}`;
4228
+ }
4229
+ else {
4230
+ const defaultIconWithStyle = CHAT_BUBBLE_DEFAULT_ICON.replace('class="andy-chat-bubble__icon"', `class="andy-chat-bubble__icon" style="${darkModeIconStyle}"`);
4231
+ bubble.innerHTML = `${defaultIconWithStyle}${tooltipHTML}`;
4232
+ }
4233
+ }
4234
+ }
4235
+ }, [shadowReady, config, isOpen, tooltip, bubbleLabel]);
4236
+ return (jsx("div", { ref: containerRef, id: 'andy-chatbot-container', children: portalRef.current && shadowReady && config && (jsxs(Fragment, { children: [ReactDOM.createPortal(jsxs("div", { className: `andy-chat-window ${isOpen ? 'andy-chat-window--open' : ''}`, children: [jsxs("header", { className: 'andy-header', children: [jsx("button", { type: 'button', className: 'andy-header__back', id: 'andy-back-button', "aria-label": 'Go back', onClick: () => setIsOpen(false), children: jsx("svg", { className: 'andy-header__back-icon', width: '20', height: '20', viewBox: '0 0 24 24', fill: 'none', children: jsx("path", { d: 'M15 18l-6-6 6-6', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' }) }) }), jsxs("div", { className: 'andy-header__bot-info', children: [jsx("div", { className: 'andy-header__avatar', children: config?.logoUrl ? (jsx("img", { src: config.logoUrl, alt: 'Bot Avatar', className: 'andy-header__avatar-image' })) : (jsx("img", { src: CHAT_BUBBLE_DEFAULT_ICON_URL, alt: 'Bot Avatar', className: 'andy-header__avatar-icon andy-header__avatar-icon--default' })) }), jsxs("div", { className: 'andy-header__text', children: [jsx("h1", { className: 'andy-header__name', children: config?.name || 'Andy bot' }), config?.headline && (jsx("p", { className: 'andy-header__subtitle', title: config.headline, children: config.headline }))] })] }), jsx("button", { type: 'button', className: 'andy-header__close', id: 'andy-close-button', "aria-label": 'Close chat', onClick: () => setIsOpen(false), children: jsx("svg", { className: 'andy-header__close-icon', width: '20', height: '20', viewBox: '0 0 24 24', fill: 'none', children: jsx("path", { d: 'M18 6L6 18M6 6l12 12', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' }) }) })] }), jsxs("div", { className: 'andy-chat-window__messages', id: 'andy-messages', ref: messagesContainerRef, children: [config?.description && (jsx("div", { className: 'andy-chat-subtitle', children: jsx("p", { className: 'andy-chat-subtitle__text', children: config.description }) })), messages.map(message => {
4237
+ if (message.isForm) {
4238
+ return (jsxs("div", { className: 'andy-message-container andy-message-container--form', children: [jsx("div", { className: 'andy-avatar', children: jsx("svg", { className: 'andy-avatar__icon', width: '24', height: '24', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', children: jsx("path", { d: 'M20 2H4C2.9 2 2 2.9 2 4V22L6 18H20C21.1 18 22 17.1 22 16V4C22 2.9 21.1 2 20 2Z', fill: 'currentColor' }) }) }), jsx("div", { className: 'andy-message andy-message--bot andy-message--form', children: jsx("div", { className: 'andy-user-info-form', children: jsxs("div", { className: 'andy-user-info-form__content', children: [jsx("h3", { className: 'andy-user-info-form__title', children: config?.collectUserInfo?.message ||
4239
+ 'Please provide your information to continue:' }), jsxs("form", { className: 'andy-user-info-form__form', id: 'andy-user-info-form', children: [jsxs("div", { className: 'andy-form-group', children: [jsx("label", { htmlFor: 'andy-user-name', className: 'andy-form-group__label', children: "Name" }), jsx("input", { type: 'text', id: 'andy-user-name', className: 'andy-form-group__input', placeholder: 'Your Name', required: true })] }), jsxs("div", { className: 'andy-form-group', children: [jsx("label", { htmlFor: 'andy-user-email', className: 'andy-form-group__label', children: "Email" }), jsx("input", { type: 'email', id: 'andy-user-email', className: 'andy-form-group__input', placeholder: 'Your Email', required: true })] }), jsx("button", { type: 'submit', className: 'andy-button andy-button--primary andy-user-info-form__submit', children: "Submit" })] })] }) }) })] }, message.id));
4240
+ }
4241
+ if (message.isLoading) {
4242
+ return (jsx("div", { className: 'andy-message-container andy-message-container--bot andy-message-container--loading', children: jsx("div", { className: 'andy-message andy-message--bot', children: jsxs("div", { className: 'andy-typing-indicator', children: [jsx("div", { className: 'andy-typing-indicator__dot' }), jsx("div", { className: 'andy-typing-indicator__dot' }), jsx("div", { className: 'andy-typing-indicator__dot' })] }) }) }, message.id));
4243
+ }
4244
+ return (jsxs("div", { className: `andy-message-container andy-message-container--${message.sender}`, children: [message.sender === 'bot' && (jsxs("div", { className: `andy-message andy-message--bot ${message.isError ? 'andy-message--error' : ''}`, children: [jsxs("div", { className: 'andy-bot-header', children: [jsx("div", { className: 'andy-bot-header__icon', children: config?.logoUrl ? (jsx("img", { src: config.logoUrl, alt: 'Bot Avatar', className: 'andy-bot-header__icon-image' })) : (jsx("img", { src: CHAT_BUBBLE_DEFAULT_ICON_URL, alt: 'Bot Avatar', className: 'andy-bot-header__icon-image andy-bot-header__icon-image--default' })) }), jsx("span", { className: 'andy-bot-header__name', children: config?.name || 'Andy bot' })] }), jsx("div", { className: 'andy-message__content', children: jsx("div", { dangerouslySetInnerHTML: { __html: parseMarkdown(message.content) } }) })] })), message.sender === 'user' && (jsx("div", { className: 'andy-message andy-message--user', children: jsx("div", { className: 'andy-message__content', children: jsx("p", { children: message.content }) }) }))] }, message.id));
4245
+ }), isLoading && (jsx("div", { className: 'andy-message-container andy-message-container--bot andy-message-container--typing', children: jsx("div", { className: 'andy-message andy-message--bot andy-message--typing', children: jsxs("div", { className: 'andy-typing-indicator', children: [jsx("div", { className: 'andy-typing-indicator__dot' }), jsx("div", { className: 'andy-typing-indicator__dot' }), jsx("div", { className: 'andy-typing-indicator__dot' })] }) }) }))] }), jsxs("div", { className: 'andy-chat-window__bottom', children: [config?.predefinedQuestions &&
4246
+ config.predefinedQuestions.length > 0 &&
4247
+ showPredefinedQuestions && (jsxs("div", { className: 'andy-predefined-questions', id: 'andy-predefined-questions', children: [jsx("div", { className: 'andy-predefined-questions__header', children: jsx("p", { className: 'andy-predefined-questions__title', children: "What would you like to know?" }) }), jsx("div", { className: 'andy-predefined-questions__list', children: config.predefinedQuestions.map((q, index) => (jsxs("button", { type: 'button', className: 'andy-predefined-question', "data-question": q.question, disabled: isCollectingUserInfo || isLoading || !config, children: [jsx("span", { className: 'andy-predefined-question__icon', children: "\uD83D\uDCAC" }), jsx("span", { className: 'andy-predefined-question__text', children: q.question })] }, index))) })] })), jsx("div", { className: 'andy-chat-window__input', children: jsx("div", { className: 'andy-input-container', children: jsxs("div", { className: 'andy-input', children: [jsx("input", { type: 'text', className: 'andy-input__field', id: 'andy-message-input', placeholder: config ? 'Type your message...' : '...', "aria-label": 'Message input', onKeyDown: e => {
4248
+ if (e.key === 'Enter' && e.target.value.trim()) {
4249
+ handleSendMessage(e.target.value);
4250
+ e.target.value = '';
4251
+ }
4252
+ }, disabled: isCollectingUserInfo || !config }), jsx("button", { type: 'button', className: 'andy-input__button', id: 'andy-send-button', "aria-label": 'Send message', onClick: () => {
4253
+ const input = shadowRef.current?.getElementById('andy-message-input');
4254
+ if (input && input.value.trim()) {
4255
+ handleSendMessage(input.value);
4256
+ input.value = '';
4257
+ }
4258
+ }, disabled: isCollectingUserInfo || !config, children: jsx("svg", { className: 'andy-input__button-icon', width: '18', height: '18', viewBox: '0 0 24 24', fill: 'currentColor', xmlns: 'http://www.w3.org/2000/svg', children: jsx("path", { d: 'M2.01 21L23 12 2.01 3 2 10l15 2-15 2z' }) }) })] }) }) }), config?.showBranding !== false && (jsx("footer", { className: 'andy-chat-window__footer', children: jsx("a", { href: getFooterUrl(), target: '_blank', rel: 'noopener noreferrer', className: 'andy-chat-window__footer-link', children: jsx("span", { className: 'andy-chat-window__footer-text', children: FOOTER_TEXT }) }) }))] })] }), portalRef.current), showWelcomePopup &&
4259
+ !isOpen &&
4260
+ ReactDOM.createPortal(jsxs("div", { className: `andy-welcome-popup ${config?.orientation === 'left' ? 'andychat-popup-left' : ''}`, style: { display: 'block' }, role: 'dialog', "aria-label": 'Welcome message', children: [jsxs("div", { className: 'andy-welcome-popup__content', children: [jsx("div", { className: 'andy-welcome-popup__message', children: config?.showWelcomePopup?.message ||
4261
+ config?.welcomeMessage ||
4262
+ 'Welcome! How can I help you today?' }), jsx("button", { type: 'button', className: 'andy-welcome-popup__close', "aria-label": 'Close welcome message', onClick: () => setShowWelcomePopup(false), children: jsx("svg", { className: 'andy-welcome-popup__close-icon', width: '20', height: '20', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', children: jsx("path", { d: 'M18 6L6 18M6 6l12 12', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' }) }) })] }), jsx("div", { className: 'andy-welcome-popup__arrow' })] }), portalRef.current)] })) }));
4263
+ };
4264
+
4265
+ const { SESSION_STORAGE_KEY, FORM_DISPLAY_DELAY_MS, PERSIST_SESSION } = CONFIG;
4266
+ const useAndyChat = (embedId) => {
4267
+ initializeConfig('andy-widget-react');
4268
+ const [config, setConfig] = useState({});
4269
+ const [isLoading, setIsLoading] = useState(true);
4270
+ const [isOpen, setIsOpen] = useState(false);
4271
+ const [error, setError] = useState(null);
4272
+ const [messages, setMessages] = useState([]);
4273
+ const [showingFaq, setShowingFaq] = useState(false);
4274
+ const [sessionId, setSessionId] = useState(() => generateSessionId());
4275
+ const [userInfoCollected, setUserInfoCollected] = useState(false);
4276
+ const [showingWelcomePopup, setShowingWelcomePopup] = useState(false);
4277
+ const [showingUserInfoForm, setShowingUserInfoForm] = useState(false);
4278
+ const [welcomePopupShown, setWelcomePopupShown] = useState(false);
4279
+ const welcomeTimeoutRef = useRef(null);
4280
+ const userInfoTimeoutRef = useRef(null);
4281
+ const initializeChatbot = useCallback(async () => {
4282
+ if (!embedId) {
4283
+ throw new Error('Embed ID is required');
4284
+ }
4285
+ try {
4286
+ setIsLoading(true);
4287
+ setError(null);
4288
+ const rawConfig = await fetchChatbotConfig(embedId);
4289
+ const validationResult = validateChatbotConfig(rawConfig);
4290
+ if (!validationResult.isValid) {
4291
+ throw new Error(`Invalid configuration: ${validationResult.errors.join(', ')}`);
4292
+ }
4293
+ const normalizedConfig = normalizeConfig(rawConfig);
4294
+ setConfig(normalizedConfig);
4295
+ if (PERSIST_SESSION) ;
4296
+ if (normalizedConfig.welcomeMessage) {
4297
+ const greeting = createGreetingMessage(normalizedConfig);
4298
+ if (greeting) {
4299
+ const greetingMessage = {
4300
+ id: Date.now(),
4301
+ content: greeting,
4302
+ sender: 'bot',
4303
+ timestamp: new Date().toISOString()
4304
+ };
4305
+ setMessages([greetingMessage]);
4306
+ }
4307
+ }
4308
+ setIsLoading(false);
4309
+ }
4310
+ catch (err) {
4311
+ console.error('Failed to initialize chatbot:', err);
4312
+ setError(err instanceof Error ? err.message : 'Unknown error');
4313
+ setIsLoading(false);
4314
+ throw err;
4315
+ }
4316
+ }, [embedId]);
4317
+ const toggleChat = useCallback(() => {
4318
+ setIsOpen(prev => {
4319
+ const newIsOpen = !prev;
4320
+ if (newIsOpen) {
4321
+ setShowingFaq(false);
4322
+ const typedConfig = config;
4323
+ if (typedConfig?.showWelcomePopup?.enabled && !welcomePopupShown) {
4324
+ setShowingWelcomePopup(true);
4325
+ setWelcomePopupShown(true);
4326
+ }
4327
+ if (typedConfig?.collectUserInfo?.enabled && !userInfoCollected) {
4328
+ if (userInfoTimeoutRef.current) {
4329
+ clearTimeout(userInfoTimeoutRef.current);
4330
+ }
4331
+ userInfoTimeoutRef.current = setTimeout(() => {
4332
+ setShowingUserInfoForm(true);
4333
+ }, FORM_DISPLAY_DELAY_MS);
4334
+ }
4335
+ }
4336
+ else {
4337
+ setShowingWelcomePopup(false);
4338
+ setShowingUserInfoForm(false);
4339
+ if (welcomeTimeoutRef.current) {
4340
+ clearTimeout(welcomeTimeoutRef.current);
4341
+ }
4342
+ if (userInfoTimeoutRef.current) {
4343
+ clearTimeout(userInfoTimeoutRef.current);
4344
+ }
4345
+ }
4346
+ return newIsOpen;
4347
+ });
4348
+ }, [config, welcomePopupShown, userInfoCollected]);
4349
+ const sendMessage$1 = useCallback(async (messageText) => {
4350
+ if (!messageText.trim() || !config || !sessionId) {
4351
+ return;
4352
+ }
4353
+ const userMessage = {
4354
+ id: Date.now(),
4355
+ content: messageText,
4356
+ sender: 'user',
4357
+ timestamp: new Date().toISOString()
4358
+ };
4359
+ setMessages(prev => [...prev, userMessage]);
4360
+ try {
4361
+ const response = await sendMessage(embedId, sessionId, messageText);
4362
+ const botMessage = {
4363
+ id: Date.now() + 1,
4364
+ content: response || 'I apologize, but I encountered an error processing your request.',
4365
+ sender: 'bot',
4366
+ timestamp: new Date().toISOString()
4367
+ };
4368
+ setMessages(prev => [...prev, botMessage]);
4369
+ return response;
4370
+ }
4371
+ catch (err) {
4372
+ console.error('Error sending message:', err);
4373
+ const errorMessage = {
4374
+ id: Date.now() + 1,
4375
+ content: 'Sorry, I encountered an error. Please try again.',
4376
+ sender: 'bot',
4377
+ timestamp: new Date().toISOString(),
4378
+ isError: true
4379
+ };
4380
+ setMessages(prev => [...prev, errorMessage]);
4381
+ throw err;
4382
+ }
4383
+ }, [embedId, sessionId, config]);
4384
+ const showFaq = useCallback(() => {
4385
+ setShowingFaq(true);
4386
+ }, []);
4387
+ const hideFaq = useCallback(() => {
4388
+ setShowingFaq(false);
4389
+ }, []);
4390
+ const showWelcomePopup = useCallback(() => {
4391
+ setShowingWelcomePopup(true);
4392
+ }, []);
4393
+ const hideWelcomePopup = useCallback(() => {
4394
+ setShowingWelcomePopup(false);
4395
+ }, []);
4396
+ const showUserInfoForm = useCallback(() => {
4397
+ setShowingUserInfoForm(true);
4398
+ }, []);
4399
+ const hideUserInfoForm = useCallback(() => {
4400
+ setShowingUserInfoForm(false);
4401
+ }, []);
4402
+ const submitUserInfo$1 = useCallback(async (userInfo) => {
4403
+ try {
4404
+ await submitUserInfo(embedId, sessionId, userInfo);
4405
+ setUserInfoCollected(true);
4406
+ setShowingUserInfoForm(false);
4407
+ const confirmMessage = {
4408
+ id: Date.now(),
4409
+ content: 'Thank you for providing your information! How can I help you today?',
4410
+ sender: 'bot',
4411
+ timestamp: new Date().toISOString()
4412
+ };
4413
+ setMessages(prev => [...prev, confirmMessage]);
4414
+ }
4415
+ catch (err) {
4416
+ console.error('Error submitting user info:', err);
4417
+ throw err;
4418
+ }
4419
+ }, [embedId, sessionId]);
4420
+ const startNewSession = useCallback(() => {
4421
+ const newSessionId = generateSessionId();
4422
+ setSessionId(newSessionId);
4423
+ {
4424
+ localStorage.removeItem(SESSION_STORAGE_KEY);
4425
+ }
4426
+ setMessages([]);
4427
+ setUserInfoCollected(false);
4428
+ setShowingFaq(false);
4429
+ setShowingUserInfoForm(false);
4430
+ setWelcomePopupShown(false);
4431
+ const typedConfig = config;
4432
+ if (typedConfig?.welcomeMessage) {
4433
+ const greeting = createGreetingMessage(typedConfig);
4434
+ if (greeting) {
4435
+ const greetingMessage = {
4436
+ id: Date.now(),
4437
+ content: greeting,
4438
+ sender: 'bot',
4439
+ timestamp: new Date().toISOString()
4440
+ };
4441
+ setMessages([greetingMessage]);
4442
+ }
4443
+ }
4444
+ }, [config]);
4445
+ const addMessage = useCallback((message) => {
4446
+ setMessages(prev => [...prev, message]);
4447
+ }, []);
4448
+ return {
4449
+ config,
4450
+ isLoading,
4451
+ isOpen,
4452
+ showingFaq,
4453
+ showingWelcomePopup,
4454
+ showingUserInfoForm,
4455
+ messages,
4456
+ userInfoCollected,
4457
+ sessionId,
4458
+ error,
4459
+ initializeChatbot,
4460
+ toggleChat,
4461
+ sendMessage: sendMessage$1,
4462
+ showFaq,
4463
+ hideFaq,
4464
+ showWelcomePopup,
4465
+ hideWelcomePopup,
4466
+ showUserInfoForm,
4467
+ hideUserInfoForm,
4468
+ submitUserInfo: submitUserInfo$1,
4469
+ startNewSession,
4470
+ addMessage
4471
+ };
4472
+ };
4473
+
4474
+ const ChatBubble = ({ isOpen, config, onClick }) => {
4475
+ const bubbleClasses = ['andy-bubble', isOpen && 'andy-bubble--open'].filter(Boolean).join(' ');
4476
+ return (jsx("div", { className: bubbleClasses, onClick: onClick, role: 'button', tabIndex: 0, "aria-label": isOpen ? 'Close chat' : 'Open chat', onKeyDown: e => {
4477
+ if (e.key === 'Enter' || e.key === ' ') {
4478
+ e.preventDefault();
4479
+ onClick();
4480
+ }
4481
+ }, children: isOpen ? (jsx("svg", { className: 'andy-chat-bubble__icon', width: '28', height: '28', viewBox: '0 0 24 24', fill: 'currentColor', xmlns: 'http://www.w3.org/2000/svg', children: jsx("path", { d: 'M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z' }) })) : config?.logoUrl ? (jsx("img", { src: config.logoUrl, alt: 'Chatbot Logo', className: 'andy-chat-bubble__logo' })) : (jsx("img", { src: CHAT_BUBBLE_DEFAULT_ICON_URL, alt: 'Chat', className: 'andy-chat-bubble__icon' })) }));
4482
+ };
4483
+
4484
+ const MessageComponent = ({ message, botName }) => {
4485
+ const isBot = message.sender === 'bot';
4486
+ const containerClasses = ['andy-message-container', `andy-message-container--${message.sender}`]
4487
+ .filter(Boolean)
4488
+ .join(' ');
4489
+ const messageClasses = ['andy-message', `andy-message--${message.sender}`].filter(Boolean).join(' ');
4490
+ const messageContent = isBot ? parseMarkdown(message.content) : message.content;
4491
+ if (isBot) {
4492
+ return (jsxs("div", { className: containerClasses, children: [jsxs("div", { className: 'andy-bot-header', children: [jsx("div", { className: 'andy-bot-header__icon', children: jsxs("svg", { width: '16', height: '16', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round', children: [jsx("path", { d: 'M12 6V2H8' }), jsx("path", { d: 'm8 18-4 4V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2Z' }), jsx("path", { d: 'M2 12h2' }), jsx("path", { d: 'M9 11v2' }), jsx("path", { d: 'M15 11v2' }), jsx("path", { d: 'M20 12h2' })] }) }), jsx("span", { className: 'andy-bot-header__name', children: botName || 'Andy bot' })] }), jsx("div", { className: messageClasses, children: jsx("div", { className: 'andy-message__content', dangerouslySetInnerHTML: { __html: messageContent } }) })] }));
4493
+ }
4494
+ return (jsx("div", { className: containerClasses, children: jsx("div", { className: messageClasses, children: jsx("div", { className: 'andy-message__content', children: jsx("p", { children: messageContent }) }) }) }));
4495
+ };
4496
+ const TypingIndicator = () => (jsx("div", { className: 'andy-message-container andy-message-container--bot andy-message-container--typing', children: jsx("div", { className: 'andy-message andy-message--bot andy-message--typing', children: jsxs("div", { className: 'andy-typing-indicator', children: [jsx("div", { className: 'andy-typing-indicator__dot' }), jsx("div", { className: 'andy-typing-indicator__dot' }), jsx("div", { className: 'andy-typing-indicator__dot' })] }) }) }));
4497
+ const MessageList = ({ messages, isLoading, logoUrl, botName }) => {
4498
+ const messagesEndRef = useRef(null);
4499
+ useEffect(() => {
4500
+ if (messagesEndRef.current) {
4501
+ messagesEndRef.current.scrollIntoView({
4502
+ behavior: 'smooth',
4503
+ block: 'end'
4504
+ });
4505
+ }
4506
+ }, [messages, isLoading]);
4507
+ return (jsxs(Fragment, { children: [messages.map(message => (jsx(MessageComponent, { message: message, logoUrl: logoUrl, botName: botName }, message.id || message.timestamp))), isLoading && jsx(TypingIndicator, {}), messages.length === 0 && !isLoading && (jsxs("div", { className: 'andy-empty-state', children: [jsx("div", { className: 'andy-empty-state__icon', children: "\uD83D\uDCAC" }), jsx("div", { className: 'andy-empty-state__text', children: "Start a conversation by typing a message below" })] })), jsx("div", { ref: messagesEndRef })] }));
4508
+ };
4509
+
4510
+ const MessageInput = ({ onSendMessage, disabled = false, placeholder = 'Type your message here...' }) => {
4511
+ const [message, setMessage] = useState('');
4512
+ const [isSubmitting, setIsSubmitting] = useState(false);
4513
+ const inputRef = useRef(null);
4514
+ const handleSubmit = async (e) => {
4515
+ e.preventDefault();
4516
+ const trimmedMessage = message.trim();
4517
+ if (!trimmedMessage || disabled || isSubmitting) {
4518
+ return;
4519
+ }
4520
+ try {
4521
+ setIsSubmitting(true);
4522
+ await onSendMessage(trimmedMessage);
4523
+ setMessage('');
4524
+ if (inputRef.current) {
4525
+ inputRef.current.focus();
4526
+ }
4527
+ }
4528
+ catch (error) {
4529
+ console.error('Error sending message:', error);
4530
+ }
4531
+ finally {
4532
+ setIsSubmitting(false);
4533
+ }
4534
+ };
4535
+ const handleInputChange = (e) => {
4536
+ setMessage(e.target.value);
4537
+ };
4538
+ const handleKeyDown = (e) => {
4539
+ if (e.key === 'Enter' && !e.shiftKey) {
4540
+ e.preventDefault();
4541
+ handleSubmit(e);
4542
+ }
4543
+ };
4544
+ const isInputDisabled = disabled || isSubmitting;
4545
+ const canSend = message.trim().length > 0 && !isInputDisabled;
4546
+ return (jsxs("div", { className: 'andy-input', children: [jsx("input", { ref: inputRef, type: 'text', value: message, onChange: handleInputChange, onKeyDown: handleKeyDown, placeholder: placeholder, disabled: isInputDisabled, maxLength: 1000, autoComplete: 'off', id: 'andy-input' }), jsx("button", { type: 'button', onClick: () => handleSubmit({ preventDefault: () => { } }), disabled: !canSend, id: 'andy-send', children: isSubmitting ? ('...') : (jsx("svg", { width: '18', height: '18', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', children: jsx("path", { d: 'M2.01 21L23 12 2.01 3 2 10l15 2-15 2z', fill: 'white' }) })) })] }));
4547
+ };
4548
+
4549
+ const ChatWindow = ({ config, messages, isLoading, onClose, onSendMessage }) => {
4550
+ const windowClasses = ['andy-chatbot', 'open'].filter(Boolean).join(' ');
4551
+ return (jsxs("div", { className: windowClasses, children: [jsxs("div", { className: 'andy-header', children: [jsx("div", { className: 'andy-title', children: config?.headline || 'Chat with our AI ✨' }), jsx("div", { className: 'andy-subtitle', children: config?.description || 'Ask any question and our AI will answer' }), jsx("div", { className: 'andy-close', onClick: onClose, role: 'button', tabIndex: 0, children: "\u2715" })] }), jsx("div", { className: 'andy-messages', id: 'andy-messages', children: jsx(MessageList, { messages: messages, isLoading: isLoading, logoUrl: config?.logoUrl, botName: config?.name }) }), jsx("div", { className: 'andy-input-container', children: jsx(MessageInput, { onSendMessage: onSendMessage, disabled: isLoading, placeholder: 'Type your message here...' }) }), config?.showBranding !== false && (jsx("div", { className: 'andy-footer', children: jsx("a", { href: FOOTER_URL, target: '_blank', rel: 'noopener noreferrer', children: FOOTER_TEXT }) }))] }));
4552
+ };
4553
+
4554
+ const FAQ = ({ faqs = [], onBack }) => {
4555
+ return (jsxs("div", { className: 'andy-chat-faq', children: [jsxs("button", { className: 'andy-chat-faq__back', onClick: onBack, type: 'button', "aria-label": 'Back to chat', children: [jsx("svg", { viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', width: '16', height: '16', children: jsx("path", { d: 'M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z', fill: 'currentColor' }) }), "Back to chat"] }), jsx("div", { className: 'andy-chat-faq__title', children: "Frequently Asked Questions" }), jsx("div", { className: 'andy-chat-faq__list', children: faqs.length > 0 ? (faqs.map((faq, index) => (jsxs("div", { className: 'andy-chat-faq__item', children: [jsx("div", { className: 'andy-chat-faq__question', children: faq.question }), jsx("div", { className: 'andy-chat-faq__answer', children: faq.answer })] }, index)))) : (jsxs("div", { className: 'andy-chat-faq__empty', children: [jsx("div", { className: 'andy-chat-faq__empty-icon', children: jsx("svg", { width: '48', height: '48', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', children: jsx("path", { d: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z', fill: 'currentColor', opacity: '0.3' }) }) }), jsx("div", { className: 'andy-chat-faq__empty-text', children: "No frequently asked questions available at this time." })] })) })] }));
4556
+ };
4557
+
4558
+ const WelcomePopup = ({ message, onClose }) => {
4559
+ useEffect(() => {
4560
+ const timer = setTimeout(() => {
4561
+ onClose();
4562
+ }, 5000);
4563
+ return () => clearTimeout(timer);
4564
+ }, [onClose]);
4565
+ const handleBackdropClick = (e) => {
4566
+ if (e.target === e.currentTarget) {
4567
+ onClose();
4568
+ }
4569
+ };
4570
+ useEffect(() => {
4571
+ const handleEscape = (e) => {
4572
+ if (e.key === 'Escape') {
4573
+ onClose();
4574
+ }
4575
+ };
4576
+ document.addEventListener('keydown', handleEscape);
4577
+ return () => document.removeEventListener('keydown', handleEscape);
4578
+ }, [onClose]);
4579
+ return (jsx("div", { className: 'andy-chat-welcome-popup__backdrop', onClick: handleBackdropClick, children: jsxs("div", { className: 'andy-chat-welcome-popup', children: [jsx("button", { className: 'andy-chat-welcome-popup__close', onClick: onClose, "aria-label": 'Close welcome message', type: 'button', children: "\u2715" }), jsx("div", { className: 'andy-chat-welcome-popup__icon', children: jsx("svg", { width: '32', height: '32', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', children: jsx("path", { d: 'M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z', fill: 'currentColor' }) }) }), jsx("div", { className: 'andy-chat-welcome-popup__message', children: message }), jsx("button", { className: 'andy-chat-welcome-popup__button', onClick: onClose, type: 'button', children: "Get Started" }), jsx("div", { className: 'andy-chat-welcome-popup__timer', children: jsx("div", { className: 'andy-chat-welcome-popup__timer-bar' }) })] }) }));
4580
+ };
4581
+
4582
+ const UserInfoForm = ({ headerText, onSubmit, onClose }) => {
4583
+ const [formData, setFormData] = useState({
4584
+ name: '',
4585
+ email: ''
4586
+ });
4587
+ const [errors, setErrors] = useState({});
4588
+ const [isSubmitting, setIsSubmitting] = useState(false);
4589
+ const validateForm = () => {
4590
+ const newErrors = {};
4591
+ if (!formData.name.trim()) {
4592
+ newErrors.name = 'Name is required';
4593
+ }
4594
+ else if (formData.name.trim().length < 2) {
4595
+ newErrors.name = 'Name must be at least 2 characters';
4596
+ }
4597
+ if (!formData.email.trim()) {
4598
+ newErrors.email = 'Email is required';
4599
+ }
4600
+ else {
4601
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
4602
+ if (!emailRegex.test(formData.email)) {
4603
+ newErrors.email = 'Please enter a valid email address';
4604
+ }
4605
+ }
4606
+ setErrors(newErrors);
4607
+ return Object.keys(newErrors).length === 0;
4608
+ };
4609
+ const handleSubmit = async (e) => {
4610
+ e.preventDefault();
4611
+ if (!validateForm() || isSubmitting) {
4612
+ return;
4613
+ }
4614
+ try {
4615
+ setIsSubmitting(true);
4616
+ await onSubmit(formData);
4617
+ }
4618
+ catch (error) {
4619
+ console.error('Error submitting user info:', error);
4620
+ setErrors({
4621
+ submit: 'Failed to save your information. Please try again.'
4622
+ });
4623
+ }
4624
+ finally {
4625
+ setIsSubmitting(false);
4626
+ }
4627
+ };
4628
+ const handleInputChange = (field) => (e) => {
4629
+ setFormData(prev => ({
4630
+ ...prev,
4631
+ [field]: e.target.value
4632
+ }));
4633
+ if (errors[field]) {
4634
+ setErrors(prev => ({
4635
+ ...prev,
4636
+ [field]: undefined
4637
+ }));
4638
+ }
4639
+ };
4640
+ useEffect(() => {
4641
+ const handleEscape = (e) => {
4642
+ if (e.key === 'Escape') {
4643
+ onClose();
4644
+ }
4645
+ };
4646
+ document.addEventListener('keydown', handleEscape);
4647
+ return () => document.removeEventListener('keydown', handleEscape);
4648
+ }, [onClose]);
4649
+ const handleBackdropClick = (e) => {
4650
+ if (e.target === e.currentTarget) {
4651
+ onClose();
4652
+ }
4653
+ };
4654
+ return (jsx("div", { className: 'andy-chat-user-form__backdrop', onClick: handleBackdropClick, children: jsxs("div", { className: 'andy-chat-user-form', children: [jsx("button", { className: 'andy-chat-user-form__close', onClick: onClose, "aria-label": 'Close form', type: 'button', children: "\u2715" }), jsxs("div", { className: 'andy-chat-user-form__header', children: [jsx("div", { className: 'andy-chat-user-form__icon', children: jsx("svg", { width: '24', height: '24', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', children: jsx("path", { d: 'M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z', fill: 'currentColor' }) }) }), jsx("div", { className: 'andy-chat-user-form__title', children: headerText || "Let's get to know you better" }), jsx("div", { className: 'andy-chat-user-form__subtitle', children: "Please provide your information to continue" })] }), jsxs("form", { className: 'andy-chat-user-form__form', onSubmit: handleSubmit, children: [jsxs("div", { className: 'andy-chat-user-form__field', children: [jsx("label", { htmlFor: 'andy-user-name', className: 'andy-chat-user-form__label', children: "Name *" }), jsx("input", { id: 'andy-user-name', type: 'text', className: `andy-chat-user-form__input ${errors.name ? 'andy-chat-user-form__input--error' : ''}`, value: formData.name, onChange: handleInputChange('name'), placeholder: 'Enter your name', disabled: isSubmitting, maxLength: 100 }), errors.name && jsx("div", { className: 'andy-chat-user-form__error', children: errors.name })] }), jsxs("div", { className: 'andy-chat-user-form__field', children: [jsx("label", { htmlFor: 'andy-user-email', className: 'andy-chat-user-form__label', children: "Email *" }), jsx("input", { id: 'andy-user-email', type: 'email', className: `andy-chat-user-form__input ${errors.email ? 'andy-chat-user-form__input--error' : ''}`, value: formData.email, onChange: handleInputChange('email'), placeholder: 'Enter your email', disabled: isSubmitting, maxLength: 100 }), errors.email && jsx("div", { className: 'andy-chat-user-form__error', children: errors.email })] }), errors.submit && jsx("div", { className: 'andy-chat-user-form__submit-error', children: errors.submit }), jsxs("div", { className: 'andy-chat-user-form__actions', children: [jsx("button", { type: 'button', className: 'andy-chat-user-form__button andy-chat-user-form__button--secondary', onClick: onClose, disabled: isSubmitting, children: "Skip for now" }), jsx("button", { type: 'submit', className: 'andy-chat-user-form__button andy-chat-user-form__button--primary', disabled: isSubmitting, children: isSubmitting ? (jsxs("span", { className: 'andy-chat-user-form__loading', children: [jsx("span", { className: 'andy-chat-user-form__loading-dot' }), jsx("span", { className: 'andy-chat-user-form__loading-dot' }), jsx("span", { className: 'andy-chat-user-form__loading-dot' })] })) : ('Continue') })] })] })] }) }));
4655
+ };
4656
+
4657
+ export { AndyChat as Andy, AndyChat, ChatBubble, ChatWindow, FAQ, MessageInput, MessageList, UserInfoForm, WelcomePopup, animation, borderRadius, chat, colors, AndyChat as default, generateSessionId, getApiBaseUrl, getEmbedId, isValidEmbedId, parseMarkdown, sanitizeUrl, setApiBaseUrl, shadows, spacing, theme, typography, useAndyChat, zIndex };
4658
+ //# sourceMappingURL=index.esm.js.map