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