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/README.md +79 -0
- package/dist/index.cjs +4688 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.esm.js +4658 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/types/AndyChat.d.ts +4 -0
- package/dist/types/components/ChatBubble.d.ts +9 -0
- package/dist/types/components/ChatWindow.d.ts +11 -0
- package/dist/types/components/FAQ.d.ts +8 -0
- package/dist/types/components/MessageInput.d.ts +8 -0
- package/dist/types/components/MessageList.d.ts +10 -0
- package/dist/types/components/UserInfoForm.d.ts +10 -0
- package/dist/types/components/WelcomePopup.d.ts +8 -0
- package/dist/types/hooks/index.d.ts +1 -0
- package/dist/types/hooks/useAndyChat.d.ts +2 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/types/index.d.ts +180 -0
- package/package.json +72 -0
- package/src/AndyChat.tsx +1091 -0
- package/src/components/ChatBubble.tsx +48 -0
- package/src/components/ChatWindow.tsx +53 -0
- package/src/components/FAQ.tsx +48 -0
- package/src/components/MessageInput.tsx +82 -0
- package/src/components/MessageList.tsx +122 -0
- package/src/components/UserInfoForm.tsx +186 -0
- package/src/components/WelcomePopup.tsx +70 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useAndyChat.ts +281 -0
- package/src/index.ts +65 -0
- package/src/styles/main.css +14 -0
- package/src/types/index.ts +222 -0
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
|