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/src/AndyChat.tsx
ADDED
|
@@ -0,0 +1,1091 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, useCallback, FormEvent } from 'react'
|
|
2
|
+
import ReactDOM from 'react-dom'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
fetchChatbotConfig,
|
|
6
|
+
generateSessionId,
|
|
7
|
+
createGreetingMessage,
|
|
8
|
+
sendMessage as sendMessageToAPI,
|
|
9
|
+
submitUserInfo,
|
|
10
|
+
isValidEmbedId,
|
|
11
|
+
getFullStylesheet,
|
|
12
|
+
getHostClasses,
|
|
13
|
+
parseMarkdown,
|
|
14
|
+
CHAT_BUBBLE_DEFAULT_ICON,
|
|
15
|
+
CHAT_BUBBLE_DEFAULT_ICON_URL,
|
|
16
|
+
SVG_ICONS,
|
|
17
|
+
FOOTER_TEXT,
|
|
18
|
+
getFooterUrl,
|
|
19
|
+
getDefaultBubbleLabel,
|
|
20
|
+
getKeyboardDetector,
|
|
21
|
+
updateKeyboardHeight,
|
|
22
|
+
isMobileDevice
|
|
23
|
+
} from 'andy-widget-core'
|
|
24
|
+
|
|
25
|
+
import type { AndyChatProps, ChatbotConfig, Message } from './types'
|
|
26
|
+
|
|
27
|
+
interface FaqItemProps {
|
|
28
|
+
question: string
|
|
29
|
+
answer: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const FaqItem: React.FC<FaqItemProps> = ({ question, answer }) => {
|
|
33
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className={`andy-faq-item ${isOpen ? 'andy-faq-item--open' : ''}`}>
|
|
37
|
+
<button
|
|
38
|
+
type='button'
|
|
39
|
+
className='andy-faq-item__question'
|
|
40
|
+
aria-expanded={isOpen}
|
|
41
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
42
|
+
>
|
|
43
|
+
<span className='andy-faq-item__question-text'>{question}</span>
|
|
44
|
+
<svg
|
|
45
|
+
className='andy-faq-item__chevron'
|
|
46
|
+
width='16'
|
|
47
|
+
height='16'
|
|
48
|
+
viewBox='0 0 24 24'
|
|
49
|
+
fill='none'
|
|
50
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
51
|
+
>
|
|
52
|
+
<path
|
|
53
|
+
d='M6 9l6 6 6-6'
|
|
54
|
+
stroke='currentColor'
|
|
55
|
+
strokeWidth='2'
|
|
56
|
+
strokeLinecap='round'
|
|
57
|
+
strokeLinejoin='round'
|
|
58
|
+
/>
|
|
59
|
+
</svg>
|
|
60
|
+
</button>
|
|
61
|
+
<div className='andy-faq-item__answer'>
|
|
62
|
+
<div className='andy-faq-item__answer-content'>{answer}</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const AndyChat: React.FC<AndyChatProps> = ({
|
|
69
|
+
embedId,
|
|
70
|
+
tooltip,
|
|
71
|
+
name,
|
|
72
|
+
headline,
|
|
73
|
+
description,
|
|
74
|
+
welcomeMessage,
|
|
75
|
+
bubbleLabel,
|
|
76
|
+
theme,
|
|
77
|
+
orientation,
|
|
78
|
+
brandColor,
|
|
79
|
+
bubbleStyle,
|
|
80
|
+
useBrandColor,
|
|
81
|
+
logoUrl,
|
|
82
|
+
previewMode = false,
|
|
83
|
+
initialOpen = false
|
|
84
|
+
}) => {
|
|
85
|
+
const [config, setConfig] = useState<ChatbotConfig | null>(null)
|
|
86
|
+
const [isOpen, setIsOpen] = useState(initialOpen)
|
|
87
|
+
const [messages, setMessages] = useState<Message[]>([])
|
|
88
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
89
|
+
const [sessionId, setSessionId] = useState(() => generateSessionId())
|
|
90
|
+
const [shadowReady, setShadowReady] = useState(false)
|
|
91
|
+
const [showPredefinedQuestions, setShowPredefinedQuestions] = useState(true)
|
|
92
|
+
const [userInfoCollected, setUserInfoCollected] = useState(false)
|
|
93
|
+
const [isCollectingUserInfo, setIsCollectingUserInfo] = useState(false)
|
|
94
|
+
const [pendingMessage, setPendingMessage] = useState<string | null>(null)
|
|
95
|
+
const [showWelcomePopup, setShowWelcomePopup] = useState(false)
|
|
96
|
+
|
|
97
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
98
|
+
const shadowRef = useRef<ShadowRoot | null>(null)
|
|
99
|
+
const portalRef = useRef<HTMLDivElement | null>(null)
|
|
100
|
+
const hasOpenedRef = useRef(false)
|
|
101
|
+
const messagesContainerRef = useRef<HTMLDivElement>(null)
|
|
102
|
+
const welcomePopupShownRef = useRef(false)
|
|
103
|
+
const welcomePopupTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
104
|
+
|
|
105
|
+
// Create shadow DOM container on mount
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!containerRef.current) return
|
|
108
|
+
|
|
109
|
+
let shadow = shadowRef.current
|
|
110
|
+
if (!shadow) {
|
|
111
|
+
if (containerRef.current.shadowRoot) {
|
|
112
|
+
shadow = containerRef.current.shadowRoot
|
|
113
|
+
} else {
|
|
114
|
+
shadow = containerRef.current.attachShadow({ mode: 'open' })
|
|
115
|
+
}
|
|
116
|
+
shadowRef.current = shadow
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let styleElement = shadow.querySelector('style')
|
|
120
|
+
if (!styleElement) {
|
|
121
|
+
styleElement = document.createElement('style')
|
|
122
|
+
shadow.appendChild(styleElement)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
styleElement.textContent = getFullStylesheet({
|
|
126
|
+
color: config?.brandColor,
|
|
127
|
+
useBrandColor: config?.useBrandColor ?? true,
|
|
128
|
+
theme: config?.theme || 'light',
|
|
129
|
+
orientation: config?.orientation || 'right'
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
if (containerRef.current) {
|
|
133
|
+
const hostClasses = getHostClasses({
|
|
134
|
+
theme: config?.theme || 'light',
|
|
135
|
+
orientation: config?.orientation || 'right'
|
|
136
|
+
})
|
|
137
|
+
containerRef.current.className = hostClasses
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!portalRef.current) {
|
|
141
|
+
let portalTarget = shadow.querySelector('.andy-react-portal') as HTMLDivElement | null
|
|
142
|
+
if (!portalTarget) {
|
|
143
|
+
portalTarget = document.createElement('div')
|
|
144
|
+
portalTarget.className = 'andy-react-portal'
|
|
145
|
+
shadow.appendChild(portalTarget)
|
|
146
|
+
}
|
|
147
|
+
portalRef.current = portalTarget
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
setShadowReady(true)
|
|
151
|
+
|
|
152
|
+
const keyboardDetector = getKeyboardDetector()
|
|
153
|
+
const keyboardListener = ({ isOpen: kbOpen, height }: { isOpen: boolean; height: number }) => {
|
|
154
|
+
updateKeyboardHeight(containerRef.current, height)
|
|
155
|
+
|
|
156
|
+
const chatWindow = shadowRef.current?.querySelector('.andy-chat-window')
|
|
157
|
+
if (chatWindow) {
|
|
158
|
+
if (kbOpen && height > 0) {
|
|
159
|
+
chatWindow.classList.add('andy-chat-window--keyboard-open')
|
|
160
|
+
} else {
|
|
161
|
+
chatWindow.classList.remove('andy-chat-window--keyboard-open')
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
keyboardDetector.addListener(keyboardListener)
|
|
167
|
+
|
|
168
|
+
return () => {
|
|
169
|
+
keyboardDetector.removeListener(keyboardListener)
|
|
170
|
+
shadowRef.current = null
|
|
171
|
+
portalRef.current = null
|
|
172
|
+
setShadowReady(false)
|
|
173
|
+
}
|
|
174
|
+
}, [])
|
|
175
|
+
|
|
176
|
+
// Update styles when config changes
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (!shadowRef.current || !config) return
|
|
179
|
+
|
|
180
|
+
const styleElement = shadowRef.current.querySelector('style')
|
|
181
|
+
if (styleElement) {
|
|
182
|
+
styleElement.textContent = getFullStylesheet({
|
|
183
|
+
color: config.brandColor,
|
|
184
|
+
useBrandColor: config.useBrandColor ?? true,
|
|
185
|
+
theme: config.theme || 'light',
|
|
186
|
+
orientation: config.orientation || 'right'
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (containerRef.current) {
|
|
191
|
+
const hostClasses = getHostClasses({
|
|
192
|
+
theme: config.theme || 'light',
|
|
193
|
+
orientation: config.orientation || 'right'
|
|
194
|
+
})
|
|
195
|
+
containerRef.current.className = hostClasses
|
|
196
|
+
}
|
|
197
|
+
}, [config])
|
|
198
|
+
|
|
199
|
+
// Close button handler
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
if (!shadowReady || !shadowRef.current || !isOpen) return
|
|
202
|
+
|
|
203
|
+
const timer = setTimeout(() => {
|
|
204
|
+
const closeButton = shadowRef.current?.querySelector('#andy-close-button') as HTMLButtonElement | null
|
|
205
|
+
if (closeButton) {
|
|
206
|
+
const handleCloseClick = () => {
|
|
207
|
+
setIsOpen(false)
|
|
208
|
+
}
|
|
209
|
+
closeButton.addEventListener('click', handleCloseClick)
|
|
210
|
+
;(closeButton as any)._closeHandler = handleCloseClick
|
|
211
|
+
}
|
|
212
|
+
}, 100)
|
|
213
|
+
|
|
214
|
+
return () => {
|
|
215
|
+
clearTimeout(timer)
|
|
216
|
+
if (shadowRef.current) {
|
|
217
|
+
const closeButton = shadowRef.current.querySelector('#andy-close-button') as HTMLButtonElement | null
|
|
218
|
+
if (closeButton && (closeButton as any)._closeHandler) {
|
|
219
|
+
closeButton.removeEventListener('click', (closeButton as any)._closeHandler)
|
|
220
|
+
delete (closeButton as any)._closeHandler
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}, [isOpen, shadowReady])
|
|
225
|
+
|
|
226
|
+
// Click outside to close
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
if (!shadowReady || !shadowRef.current) return
|
|
229
|
+
if (previewMode) return
|
|
230
|
+
|
|
231
|
+
const handleDocumentClick = (event: MouseEvent) => {
|
|
232
|
+
if (isMobileDevice()) return
|
|
233
|
+
if (!isOpen) return
|
|
234
|
+
|
|
235
|
+
const selection = window.getSelection()
|
|
236
|
+
if (selection && selection.toString().length > 0) {
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const chatbotContainer = containerRef.current
|
|
241
|
+
let isClickInsideChatbot = false
|
|
242
|
+
|
|
243
|
+
if (chatbotContainer && chatbotContainer.contains(event.target as Node)) {
|
|
244
|
+
isClickInsideChatbot = true
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (event.composedPath) {
|
|
248
|
+
const path = event.composedPath()
|
|
249
|
+
if (chatbotContainer && path.includes(chatbotContainer)) {
|
|
250
|
+
isClickInsideChatbot = true
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (event.target === chatbotContainer) {
|
|
255
|
+
isClickInsideChatbot = true
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!isClickInsideChatbot) {
|
|
259
|
+
setIsOpen(false)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const handleShadowClick = (event: Event) => {
|
|
264
|
+
const target = event.target as Element
|
|
265
|
+
if (
|
|
266
|
+
target.closest('#andy-close-button') ||
|
|
267
|
+
target.closest('#andy-send-button') ||
|
|
268
|
+
target.closest('.andy-predefined-question')
|
|
269
|
+
) {
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
if (target.closest('.andy-chat-window')) {
|
|
273
|
+
event.stopPropagation()
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
document.addEventListener('click', handleDocumentClick)
|
|
278
|
+
shadowRef.current.addEventListener('click', handleShadowClick, true)
|
|
279
|
+
|
|
280
|
+
return () => {
|
|
281
|
+
document.removeEventListener('click', handleDocumentClick)
|
|
282
|
+
if (shadowRef.current) {
|
|
283
|
+
shadowRef.current.removeEventListener('click', handleShadowClick, true)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}, [isOpen, shadowReady, previewMode])
|
|
287
|
+
|
|
288
|
+
// Initialize config
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
if (previewMode) {
|
|
291
|
+
const previewConfig: ChatbotConfig = {
|
|
292
|
+
id: embedId || 'preview',
|
|
293
|
+
name: name || 'Andy Bot',
|
|
294
|
+
theme: theme || 'light',
|
|
295
|
+
logoUrl: logoUrl || '',
|
|
296
|
+
headline: headline || '',
|
|
297
|
+
brandColor: brandColor || '#3B82F6',
|
|
298
|
+
description: description || '',
|
|
299
|
+
orientation: orientation || 'right',
|
|
300
|
+
bubbleStyle: bubbleStyle || 'icon',
|
|
301
|
+
bubbleLabel: bubbleLabel || null,
|
|
302
|
+
useBrandColor: useBrandColor !== undefined ? useBrandColor : true,
|
|
303
|
+
defaultLang: 'en',
|
|
304
|
+
showBranding: true,
|
|
305
|
+
showWelcomePopup: { enabled: false, seconds: 3, message: '' },
|
|
306
|
+
collectUserInfo: { enabled: false, message: '' },
|
|
307
|
+
welcomeMessage: welcomeMessage || 'Hi there! How can I help you today?',
|
|
308
|
+
predefinedQuestions: [],
|
|
309
|
+
faqs: []
|
|
310
|
+
}
|
|
311
|
+
setConfig(previewConfig)
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!embedId || !isValidEmbedId(embedId)) {
|
|
316
|
+
setConfig(null)
|
|
317
|
+
console.warn('AndyChat: Invalid or missing embedId')
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
let cancelled = false
|
|
322
|
+
|
|
323
|
+
const loadConfig = async () => {
|
|
324
|
+
try {
|
|
325
|
+
setConfig(null)
|
|
326
|
+
const rawConfig = await fetchChatbotConfig(embedId)
|
|
327
|
+
|
|
328
|
+
const configWithOverrides: ChatbotConfig = {
|
|
329
|
+
...rawConfig,
|
|
330
|
+
...(name && { name }),
|
|
331
|
+
...(headline && { headline }),
|
|
332
|
+
...(description && { description }),
|
|
333
|
+
...(welcomeMessage && { welcomeMessage }),
|
|
334
|
+
...(theme && { theme }),
|
|
335
|
+
...(orientation && { orientation }),
|
|
336
|
+
...(brandColor && { brandColor }),
|
|
337
|
+
...(bubbleStyle && { bubbleStyle }),
|
|
338
|
+
...(bubbleLabel && { bubbleLabel }),
|
|
339
|
+
...(useBrandColor !== undefined && { useBrandColor }),
|
|
340
|
+
...(logoUrl && { logoUrl })
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!cancelled) setConfig(configWithOverrides)
|
|
344
|
+
} catch {
|
|
345
|
+
if (!cancelled) {
|
|
346
|
+
setConfig(null)
|
|
347
|
+
console.error('AndyChat: Failed to load configuration.')
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
loadConfig()
|
|
353
|
+
|
|
354
|
+
return () => {
|
|
355
|
+
cancelled = true
|
|
356
|
+
}
|
|
357
|
+
}, [
|
|
358
|
+
embedId,
|
|
359
|
+
name,
|
|
360
|
+
headline,
|
|
361
|
+
description,
|
|
362
|
+
welcomeMessage,
|
|
363
|
+
previewMode,
|
|
364
|
+
theme,
|
|
365
|
+
orientation,
|
|
366
|
+
brandColor,
|
|
367
|
+
bubbleStyle,
|
|
368
|
+
bubbleLabel,
|
|
369
|
+
useBrandColor,
|
|
370
|
+
logoUrl
|
|
371
|
+
])
|
|
372
|
+
|
|
373
|
+
// Welcome popup timer
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
if (welcomePopupTimeoutRef.current) {
|
|
376
|
+
clearTimeout(welcomePopupTimeoutRef.current)
|
|
377
|
+
welcomePopupTimeoutRef.current = null
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (config?.showWelcomePopup?.enabled && !welcomePopupShownRef.current && !isOpen) {
|
|
381
|
+
const delaySeconds = config.showWelcomePopup.seconds ?? 3
|
|
382
|
+
welcomePopupTimeoutRef.current = setTimeout(() => {
|
|
383
|
+
if (!isOpen) {
|
|
384
|
+
setShowWelcomePopup(true)
|
|
385
|
+
welcomePopupShownRef.current = true
|
|
386
|
+
}
|
|
387
|
+
}, delaySeconds * 1000)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (isOpen && showWelcomePopup) {
|
|
391
|
+
setShowWelcomePopup(false)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return () => {
|
|
395
|
+
if (welcomePopupTimeoutRef.current) {
|
|
396
|
+
clearTimeout(welcomePopupTimeoutRef.current)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}, [config, isOpen, showWelcomePopup])
|
|
400
|
+
|
|
401
|
+
// Add welcome message on first open
|
|
402
|
+
useEffect(() => {
|
|
403
|
+
if (isOpen && !hasOpenedRef.current && config?.welcomeMessage) {
|
|
404
|
+
hasOpenedRef.current = true
|
|
405
|
+
const greeting = createGreetingMessage(config)
|
|
406
|
+
if (greeting) {
|
|
407
|
+
setMessages([
|
|
408
|
+
{
|
|
409
|
+
id: Date.now(),
|
|
410
|
+
content: greeting,
|
|
411
|
+
sender: 'bot',
|
|
412
|
+
timestamp: new Date().toISOString()
|
|
413
|
+
}
|
|
414
|
+
])
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}, [isOpen, config])
|
|
418
|
+
|
|
419
|
+
// Auto-scroll
|
|
420
|
+
useEffect(() => {
|
|
421
|
+
if (messagesContainerRef.current) {
|
|
422
|
+
messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight
|
|
423
|
+
}
|
|
424
|
+
}, [messages, isLoading])
|
|
425
|
+
|
|
426
|
+
// Form submit handler
|
|
427
|
+
useEffect(() => {
|
|
428
|
+
if (isCollectingUserInfo && shadowRef.current) {
|
|
429
|
+
const timer = setTimeout(() => {
|
|
430
|
+
const form = shadowRef.current?.querySelector('#andy-user-info-form') as HTMLFormElement | null
|
|
431
|
+
if (form) {
|
|
432
|
+
const handleSubmit = (e: Event) => {
|
|
433
|
+
handleUserInfoSubmit(e as unknown as FormEvent<HTMLFormElement>)
|
|
434
|
+
}
|
|
435
|
+
form.addEventListener('submit', handleSubmit)
|
|
436
|
+
;(form as any)._submitHandler = handleSubmit
|
|
437
|
+
}
|
|
438
|
+
}, 100)
|
|
439
|
+
|
|
440
|
+
return () => {
|
|
441
|
+
clearTimeout(timer)
|
|
442
|
+
if (shadowRef.current) {
|
|
443
|
+
const form = shadowRef.current.querySelector('#andy-user-info-form') as HTMLFormElement | null
|
|
444
|
+
if (form && (form as any)._submitHandler) {
|
|
445
|
+
form.removeEventListener('submit', (form as any)._submitHandler)
|
|
446
|
+
delete (form as any)._submitHandler
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}, [isCollectingUserInfo, messages])
|
|
452
|
+
|
|
453
|
+
const handleNewChat = () => {
|
|
454
|
+
setSessionId(generateSessionId())
|
|
455
|
+
|
|
456
|
+
if (config?.welcomeMessage) {
|
|
457
|
+
const greeting = createGreetingMessage(config)
|
|
458
|
+
if (greeting) {
|
|
459
|
+
setMessages([
|
|
460
|
+
{
|
|
461
|
+
id: Date.now(),
|
|
462
|
+
content: greeting,
|
|
463
|
+
sender: 'bot',
|
|
464
|
+
timestamp: new Date().toISOString()
|
|
465
|
+
}
|
|
466
|
+
])
|
|
467
|
+
}
|
|
468
|
+
} else {
|
|
469
|
+
setMessages([])
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (config?.predefinedQuestions && config.predefinedQuestions.length > 0) {
|
|
473
|
+
setShowPredefinedQuestions(true)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const sendUserMessage = useCallback(
|
|
478
|
+
async (rawMessageText: string, options: { hidePredefined?: boolean } = {}) => {
|
|
479
|
+
const messageText = typeof rawMessageText === 'string' ? rawMessageText.trim() : ''
|
|
480
|
+
if (!messageText) {
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const { hidePredefined = true } = options
|
|
485
|
+
const timestamp = Date.now()
|
|
486
|
+
|
|
487
|
+
const userMessage: Message = {
|
|
488
|
+
id: timestamp,
|
|
489
|
+
content: messageText,
|
|
490
|
+
sender: 'user',
|
|
491
|
+
timestamp: new Date().toISOString()
|
|
492
|
+
}
|
|
493
|
+
setMessages(prev => [...prev, userMessage])
|
|
494
|
+
|
|
495
|
+
if (hidePredefined) {
|
|
496
|
+
setShowPredefinedQuestions(false)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
setIsLoading(true)
|
|
501
|
+
const response = await sendMessageToAPI(embedId!, sessionId, messageText)
|
|
502
|
+
|
|
503
|
+
const botMessage: Message = {
|
|
504
|
+
id: timestamp + 1,
|
|
505
|
+
content: response,
|
|
506
|
+
sender: 'bot',
|
|
507
|
+
timestamp: new Date().toISOString()
|
|
508
|
+
}
|
|
509
|
+
setMessages(prev => [...prev, botMessage])
|
|
510
|
+
|
|
511
|
+
if (config?.collectUserInfo?.enabled && !userInfoCollected && !isCollectingUserInfo) {
|
|
512
|
+
setPendingMessage(messageText)
|
|
513
|
+
setIsCollectingUserInfo(true)
|
|
514
|
+
|
|
515
|
+
const formMessage: Message = {
|
|
516
|
+
id: timestamp + 2,
|
|
517
|
+
content: 'USER_INFO_FORM',
|
|
518
|
+
sender: 'bot',
|
|
519
|
+
isForm: true,
|
|
520
|
+
timestamp: new Date().toISOString()
|
|
521
|
+
}
|
|
522
|
+
setMessages(prev => [...prev, formMessage])
|
|
523
|
+
}
|
|
524
|
+
} catch {
|
|
525
|
+
console.error('AndyChat: Message request failed.')
|
|
526
|
+
const errorMessage: Message = {
|
|
527
|
+
id: timestamp + 3,
|
|
528
|
+
content:
|
|
529
|
+
config?.defaultLang === 'es'
|
|
530
|
+
? 'Lo siento, no pude responder. Inténtalo de nuevo.'
|
|
531
|
+
: 'Sorry, I could not answer. Please try again.',
|
|
532
|
+
sender: 'bot',
|
|
533
|
+
timestamp: new Date().toISOString(),
|
|
534
|
+
isError: true
|
|
535
|
+
}
|
|
536
|
+
setMessages(prev => [...prev, errorMessage])
|
|
537
|
+
} finally {
|
|
538
|
+
setIsLoading(false)
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
[config, embedId, sessionId, userInfoCollected, isCollectingUserInfo]
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
const handleSendMessage = useCallback(
|
|
545
|
+
(messageText: string) => {
|
|
546
|
+
return sendUserMessage(messageText)
|
|
547
|
+
},
|
|
548
|
+
[sendUserMessage]
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
const handlePredefinedQuestion = useCallback(
|
|
552
|
+
(question: string) => {
|
|
553
|
+
if (isCollectingUserInfo || isLoading) {
|
|
554
|
+
return
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const trimmedQuestion = typeof question === 'string' ? question.trim() : ''
|
|
558
|
+
if (!trimmedQuestion) {
|
|
559
|
+
return
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const input = shadowRef.current?.getElementById('andy-message-input') as HTMLInputElement | null
|
|
563
|
+
if (input) {
|
|
564
|
+
input.value = ''
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
sendUserMessage(trimmedQuestion)
|
|
568
|
+
},
|
|
569
|
+
[isCollectingUserInfo, isLoading, sendUserMessage]
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
// Predefined questions click handler
|
|
573
|
+
useEffect(() => {
|
|
574
|
+
if (!shadowRef.current) {
|
|
575
|
+
return
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const shadowRoot = shadowRef.current
|
|
579
|
+
const captureClick = (event: Event) => {
|
|
580
|
+
const composedTarget = event.composedPath ? event.composedPath()[0] : event.target
|
|
581
|
+
const elementTarget = composedTarget instanceof Element ? composedTarget : null
|
|
582
|
+
const button =
|
|
583
|
+
elementTarget?.closest?.('.andy-predefined-question') ||
|
|
584
|
+
shadowRoot.querySelector('.andy-predefined-question:focus')
|
|
585
|
+
if (!button) {
|
|
586
|
+
return
|
|
587
|
+
}
|
|
588
|
+
if ((button as HTMLButtonElement).disabled) {
|
|
589
|
+
return
|
|
590
|
+
}
|
|
591
|
+
const question = button.getAttribute('data-question') || button.textContent || ''
|
|
592
|
+
handlePredefinedQuestion(question)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
shadowRoot.addEventListener('click', captureClick)
|
|
596
|
+
return () => {
|
|
597
|
+
shadowRoot.removeEventListener('click', captureClick)
|
|
598
|
+
}
|
|
599
|
+
}, [handlePredefinedQuestion, shadowReady])
|
|
600
|
+
|
|
601
|
+
const handleUserInfoSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
|
602
|
+
e.preventDefault()
|
|
603
|
+
|
|
604
|
+
const form = e.target as HTMLFormElement
|
|
605
|
+
const nameInput = form.querySelector('#andy-user-name') as HTMLInputElement | null
|
|
606
|
+
const emailInput = form.querySelector('#andy-user-email') as HTMLInputElement | null
|
|
607
|
+
const userName = nameInput?.value
|
|
608
|
+
const email = emailInput?.value
|
|
609
|
+
|
|
610
|
+
if (!userName || !email) return
|
|
611
|
+
|
|
612
|
+
setMessages(prev => prev.filter(msg => !msg.isForm))
|
|
613
|
+
|
|
614
|
+
const loadingMessage: Message = {
|
|
615
|
+
id: Date.now(),
|
|
616
|
+
content: 'LOADING',
|
|
617
|
+
sender: 'bot',
|
|
618
|
+
isLoading: true,
|
|
619
|
+
timestamp: new Date().toISOString()
|
|
620
|
+
}
|
|
621
|
+
setMessages(prev => [...prev, loadingMessage])
|
|
622
|
+
|
|
623
|
+
try {
|
|
624
|
+
await submitUserInfo(embedId!, sessionId, { name: userName, email })
|
|
625
|
+
|
|
626
|
+
setUserInfoCollected(true)
|
|
627
|
+
setIsCollectingUserInfo(false)
|
|
628
|
+
|
|
629
|
+
setMessages(prev => prev.filter(msg => !msg.isLoading))
|
|
630
|
+
setPendingMessage(null)
|
|
631
|
+
} catch (error) {
|
|
632
|
+
console.error('Error submitting user info:', error)
|
|
633
|
+
setMessages(prev => prev.filter(msg => !msg.isLoading))
|
|
634
|
+
const errorMessage: Message = {
|
|
635
|
+
id: Date.now() + 1,
|
|
636
|
+
content: 'Sorry, there was an error submitting your information. Please try again.',
|
|
637
|
+
sender: 'bot',
|
|
638
|
+
timestamp: new Date().toISOString()
|
|
639
|
+
}
|
|
640
|
+
setMessages(prev => [...prev, errorMessage])
|
|
641
|
+
|
|
642
|
+
setIsCollectingUserInfo(false)
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (!previewMode && (!embedId || !isValidEmbedId(embedId))) {
|
|
647
|
+
return null
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Render bubble
|
|
651
|
+
useEffect(() => {
|
|
652
|
+
if (!shadowRef.current || !shadowReady) return
|
|
653
|
+
if (!config) {
|
|
654
|
+
shadowRef.current.querySelector('.andy-chat-bubble')?.remove()
|
|
655
|
+
return
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
let bubble = shadowRef.current.querySelector('.andy-chat-bubble') as HTMLButtonElement | null
|
|
659
|
+
if (!bubble) {
|
|
660
|
+
bubble = document.createElement('button')
|
|
661
|
+
bubble.type = 'button'
|
|
662
|
+
bubble.className = 'andy-chat-bubble'
|
|
663
|
+
bubble.setAttribute('aria-label', 'Open chat')
|
|
664
|
+
|
|
665
|
+
const tooltipHTML = tooltip
|
|
666
|
+
? `<span class="andy-chat-bubble__tooltip" role="tooltip">${tooltip}</span>`
|
|
667
|
+
: ''
|
|
668
|
+
bubble.innerHTML = `${CHAT_BUBBLE_DEFAULT_ICON}${tooltipHTML}`
|
|
669
|
+
shadowRef.current.appendChild(bubble)
|
|
670
|
+
|
|
671
|
+
bubble.addEventListener('click', () => setIsOpen(prev => !prev))
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const isPillStyle = config?.bubbleStyle === 'pill'
|
|
675
|
+
const displayLabel =
|
|
676
|
+
bubbleLabel || config?.bubbleLabel || (isPillStyle ? getDefaultBubbleLabel(config?.defaultLang || 'en') : null)
|
|
677
|
+
const bubbleLogoUrl = config?.logoUrl || CHAT_BUBBLE_DEFAULT_ICON_URL
|
|
678
|
+
|
|
679
|
+
if (bubble) {
|
|
680
|
+
bubble.className = 'andy-chat-bubble'
|
|
681
|
+
if (isPillStyle) {
|
|
682
|
+
bubble.classList.add('andy-chat-bubble--pill')
|
|
683
|
+
}
|
|
684
|
+
if (isOpen) {
|
|
685
|
+
bubble.classList.add('andy-chat-bubble--open')
|
|
686
|
+
}
|
|
687
|
+
if (config?.theme === 'dark') {
|
|
688
|
+
bubble.classList.add('andy-chat-bubble--theme-dark')
|
|
689
|
+
}
|
|
690
|
+
if (config) {
|
|
691
|
+
bubble.classList.add('andy-chat-bubble--loaded')
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const tooltipHTML =
|
|
695
|
+
!isOpen && tooltip && !isPillStyle
|
|
696
|
+
? `<span class="andy-chat-bubble__tooltip" role="tooltip">${tooltip}</span>`
|
|
697
|
+
: ''
|
|
698
|
+
|
|
699
|
+
const configUseBrandColor = config?.useBrandColor ?? true
|
|
700
|
+
const isDarkTheme = config?.theme === 'dark'
|
|
701
|
+
const needsWhiteIcon = configUseBrandColor || isDarkTheme
|
|
702
|
+
const iconFilterStyle = needsWhiteIcon ? '' : 'filter: none;'
|
|
703
|
+
const darkModeIconStyle =
|
|
704
|
+
!configUseBrandColor && isDarkTheme ? 'filter: brightness(0) invert(1);' : iconFilterStyle
|
|
705
|
+
|
|
706
|
+
if (isOpen) {
|
|
707
|
+
const chevronWithStyle = SVG_ICONS.chevronDown.replace(
|
|
708
|
+
'class="andy-chat-bubble__icon"',
|
|
709
|
+
`class="andy-chat-bubble__icon" style="${darkModeIconStyle}"`
|
|
710
|
+
)
|
|
711
|
+
bubble.innerHTML = chevronWithStyle
|
|
712
|
+
} else if (isPillStyle && displayLabel) {
|
|
713
|
+
bubble.innerHTML = `
|
|
714
|
+
<span class="andy-chat-bubble__label">${displayLabel}</span>
|
|
715
|
+
<img src="${bubbleLogoUrl}" alt="Chat" class="andy-chat-bubble__icon--pill" style="${darkModeIconStyle}" />
|
|
716
|
+
`
|
|
717
|
+
} else {
|
|
718
|
+
if (config?.logoUrl) {
|
|
719
|
+
bubble.innerHTML = `<img src="${config.logoUrl}" alt="Chatbot Logo" class="andy-chat-bubble__logo" style="${darkModeIconStyle}" />${tooltipHTML}`
|
|
720
|
+
} else {
|
|
721
|
+
const defaultIconWithStyle = CHAT_BUBBLE_DEFAULT_ICON.replace(
|
|
722
|
+
'class="andy-chat-bubble__icon"',
|
|
723
|
+
`class="andy-chat-bubble__icon" style="${darkModeIconStyle}"`
|
|
724
|
+
)
|
|
725
|
+
bubble.innerHTML = `${defaultIconWithStyle}${tooltipHTML}`
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}, [shadowReady, config, isOpen, tooltip, bubbleLabel])
|
|
730
|
+
|
|
731
|
+
return (
|
|
732
|
+
<div ref={containerRef} id='andy-chatbot-container'>
|
|
733
|
+
{portalRef.current && shadowReady && config && (
|
|
734
|
+
<>
|
|
735
|
+
{ReactDOM.createPortal(
|
|
736
|
+
<div className={`andy-chat-window ${isOpen ? 'andy-chat-window--open' : ''}`}>
|
|
737
|
+
<header className='andy-header'>
|
|
738
|
+
<button
|
|
739
|
+
type='button'
|
|
740
|
+
className='andy-header__back'
|
|
741
|
+
id='andy-back-button'
|
|
742
|
+
aria-label='Go back'
|
|
743
|
+
onClick={() => setIsOpen(false)}
|
|
744
|
+
>
|
|
745
|
+
<svg
|
|
746
|
+
className='andy-header__back-icon'
|
|
747
|
+
width='20'
|
|
748
|
+
height='20'
|
|
749
|
+
viewBox='0 0 24 24'
|
|
750
|
+
fill='none'
|
|
751
|
+
>
|
|
752
|
+
<path
|
|
753
|
+
d='M15 18l-6-6 6-6'
|
|
754
|
+
stroke='currentColor'
|
|
755
|
+
strokeWidth='2'
|
|
756
|
+
strokeLinecap='round'
|
|
757
|
+
strokeLinejoin='round'
|
|
758
|
+
/>
|
|
759
|
+
</svg>
|
|
760
|
+
</button>
|
|
761
|
+
|
|
762
|
+
<div className='andy-header__bot-info'>
|
|
763
|
+
<div className='andy-header__avatar'>
|
|
764
|
+
{config?.logoUrl ? (
|
|
765
|
+
<img src={config.logoUrl} alt='Bot Avatar' className='andy-header__avatar-image' />
|
|
766
|
+
) : (
|
|
767
|
+
<img
|
|
768
|
+
src={CHAT_BUBBLE_DEFAULT_ICON_URL}
|
|
769
|
+
alt='Bot Avatar'
|
|
770
|
+
className='andy-header__avatar-icon andy-header__avatar-icon--default'
|
|
771
|
+
/>
|
|
772
|
+
)}
|
|
773
|
+
</div>
|
|
774
|
+
<div className='andy-header__text'>
|
|
775
|
+
<h1 className='andy-header__name'>{config?.name || 'Andy bot'}</h1>
|
|
776
|
+
{config?.headline && (
|
|
777
|
+
<p className='andy-header__subtitle' title={config.headline}>
|
|
778
|
+
{config.headline}
|
|
779
|
+
</p>
|
|
780
|
+
)}
|
|
781
|
+
</div>
|
|
782
|
+
</div>
|
|
783
|
+
|
|
784
|
+
<button
|
|
785
|
+
type='button'
|
|
786
|
+
className='andy-header__close'
|
|
787
|
+
id='andy-close-button'
|
|
788
|
+
aria-label='Close chat'
|
|
789
|
+
onClick={() => setIsOpen(false)}
|
|
790
|
+
>
|
|
791
|
+
<svg
|
|
792
|
+
className='andy-header__close-icon'
|
|
793
|
+
width='20'
|
|
794
|
+
height='20'
|
|
795
|
+
viewBox='0 0 24 24'
|
|
796
|
+
fill='none'
|
|
797
|
+
>
|
|
798
|
+
<path
|
|
799
|
+
d='M18 6L6 18M6 6l12 12'
|
|
800
|
+
stroke='currentColor'
|
|
801
|
+
strokeWidth='2'
|
|
802
|
+
strokeLinecap='round'
|
|
803
|
+
strokeLinejoin='round'
|
|
804
|
+
/>
|
|
805
|
+
</svg>
|
|
806
|
+
</button>
|
|
807
|
+
</header>
|
|
808
|
+
|
|
809
|
+
<div className='andy-chat-window__messages' id='andy-messages' ref={messagesContainerRef}>
|
|
810
|
+
{config?.description && (
|
|
811
|
+
<div className='andy-chat-subtitle'>
|
|
812
|
+
<p className='andy-chat-subtitle__text'>{config.description}</p>
|
|
813
|
+
</div>
|
|
814
|
+
)}
|
|
815
|
+
{messages.map(message => {
|
|
816
|
+
if (message.isForm) {
|
|
817
|
+
return (
|
|
818
|
+
<div key={message.id} className='andy-message-container andy-message-container--form'>
|
|
819
|
+
<div className='andy-avatar'>
|
|
820
|
+
<svg
|
|
821
|
+
className='andy-avatar__icon'
|
|
822
|
+
width='24'
|
|
823
|
+
height='24'
|
|
824
|
+
viewBox='0 0 24 24'
|
|
825
|
+
fill='none'
|
|
826
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
827
|
+
>
|
|
828
|
+
<path
|
|
829
|
+
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'
|
|
830
|
+
fill='currentColor'
|
|
831
|
+
/>
|
|
832
|
+
</svg>
|
|
833
|
+
</div>
|
|
834
|
+
<div className='andy-message andy-message--bot andy-message--form'>
|
|
835
|
+
<div className='andy-user-info-form'>
|
|
836
|
+
<div className='andy-user-info-form__content'>
|
|
837
|
+
<h3 className='andy-user-info-form__title'>
|
|
838
|
+
{config?.collectUserInfo?.message ||
|
|
839
|
+
'Please provide your information to continue:'}
|
|
840
|
+
</h3>
|
|
841
|
+
<form className='andy-user-info-form__form' id='andy-user-info-form'>
|
|
842
|
+
<div className='andy-form-group'>
|
|
843
|
+
<label htmlFor='andy-user-name' className='andy-form-group__label'>
|
|
844
|
+
Name
|
|
845
|
+
</label>
|
|
846
|
+
<input
|
|
847
|
+
type='text'
|
|
848
|
+
id='andy-user-name'
|
|
849
|
+
className='andy-form-group__input'
|
|
850
|
+
placeholder='Your Name'
|
|
851
|
+
required
|
|
852
|
+
/>
|
|
853
|
+
</div>
|
|
854
|
+
<div className='andy-form-group'>
|
|
855
|
+
<label htmlFor='andy-user-email' className='andy-form-group__label'>
|
|
856
|
+
Email
|
|
857
|
+
</label>
|
|
858
|
+
<input
|
|
859
|
+
type='email'
|
|
860
|
+
id='andy-user-email'
|
|
861
|
+
className='andy-form-group__input'
|
|
862
|
+
placeholder='Your Email'
|
|
863
|
+
required
|
|
864
|
+
/>
|
|
865
|
+
</div>
|
|
866
|
+
<button
|
|
867
|
+
type='submit'
|
|
868
|
+
className='andy-button andy-button--primary andy-user-info-form__submit'
|
|
869
|
+
>
|
|
870
|
+
Submit
|
|
871
|
+
</button>
|
|
872
|
+
</form>
|
|
873
|
+
</div>
|
|
874
|
+
</div>
|
|
875
|
+
</div>
|
|
876
|
+
</div>
|
|
877
|
+
)
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (message.isLoading) {
|
|
881
|
+
return (
|
|
882
|
+
<div
|
|
883
|
+
key={message.id}
|
|
884
|
+
className='andy-message-container andy-message-container--bot andy-message-container--loading'
|
|
885
|
+
>
|
|
886
|
+
<div className='andy-message andy-message--bot'>
|
|
887
|
+
<div className='andy-typing-indicator'>
|
|
888
|
+
<div className='andy-typing-indicator__dot'></div>
|
|
889
|
+
<div className='andy-typing-indicator__dot'></div>
|
|
890
|
+
<div className='andy-typing-indicator__dot'></div>
|
|
891
|
+
</div>
|
|
892
|
+
</div>
|
|
893
|
+
</div>
|
|
894
|
+
)
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return (
|
|
898
|
+
<div
|
|
899
|
+
key={message.id}
|
|
900
|
+
className={`andy-message-container andy-message-container--${message.sender}`}
|
|
901
|
+
>
|
|
902
|
+
{message.sender === 'bot' && (
|
|
903
|
+
<div
|
|
904
|
+
className={`andy-message andy-message--bot ${message.isError ? 'andy-message--error' : ''}`}
|
|
905
|
+
>
|
|
906
|
+
<div className='andy-bot-header'>
|
|
907
|
+
<div className='andy-bot-header__icon'>
|
|
908
|
+
{config?.logoUrl ? (
|
|
909
|
+
<img
|
|
910
|
+
src={config.logoUrl}
|
|
911
|
+
alt='Bot Avatar'
|
|
912
|
+
className='andy-bot-header__icon-image'
|
|
913
|
+
/>
|
|
914
|
+
) : (
|
|
915
|
+
<img
|
|
916
|
+
src={CHAT_BUBBLE_DEFAULT_ICON_URL}
|
|
917
|
+
alt='Bot Avatar'
|
|
918
|
+
className='andy-bot-header__icon-image andy-bot-header__icon-image--default'
|
|
919
|
+
/>
|
|
920
|
+
)}
|
|
921
|
+
</div>
|
|
922
|
+
<span className='andy-bot-header__name'>{config?.name || 'Andy bot'}</span>
|
|
923
|
+
</div>
|
|
924
|
+
<div className='andy-message__content'>
|
|
925
|
+
<div dangerouslySetInnerHTML={{ __html: parseMarkdown(message.content) }} />
|
|
926
|
+
</div>
|
|
927
|
+
</div>
|
|
928
|
+
)}
|
|
929
|
+
{message.sender === 'user' && (
|
|
930
|
+
<div className='andy-message andy-message--user'>
|
|
931
|
+
<div className='andy-message__content'>
|
|
932
|
+
<p>{message.content}</p>
|
|
933
|
+
</div>
|
|
934
|
+
</div>
|
|
935
|
+
)}
|
|
936
|
+
</div>
|
|
937
|
+
)
|
|
938
|
+
})}
|
|
939
|
+
|
|
940
|
+
{isLoading && (
|
|
941
|
+
<div className='andy-message-container andy-message-container--bot andy-message-container--typing'>
|
|
942
|
+
<div className='andy-message andy-message--bot andy-message--typing'>
|
|
943
|
+
<div className='andy-typing-indicator'>
|
|
944
|
+
<div className='andy-typing-indicator__dot'></div>
|
|
945
|
+
<div className='andy-typing-indicator__dot'></div>
|
|
946
|
+
<div className='andy-typing-indicator__dot'></div>
|
|
947
|
+
</div>
|
|
948
|
+
</div>
|
|
949
|
+
</div>
|
|
950
|
+
)}
|
|
951
|
+
</div>
|
|
952
|
+
|
|
953
|
+
<div className='andy-chat-window__bottom'>
|
|
954
|
+
{config?.predefinedQuestions &&
|
|
955
|
+
config.predefinedQuestions.length > 0 &&
|
|
956
|
+
showPredefinedQuestions && (
|
|
957
|
+
<div className='andy-predefined-questions' id='andy-predefined-questions'>
|
|
958
|
+
<div className='andy-predefined-questions__header'>
|
|
959
|
+
<p className='andy-predefined-questions__title'>What would you like to know?</p>
|
|
960
|
+
</div>
|
|
961
|
+
<div className='andy-predefined-questions__list'>
|
|
962
|
+
{config.predefinedQuestions.map((q, index) => (
|
|
963
|
+
<button
|
|
964
|
+
key={index}
|
|
965
|
+
type='button'
|
|
966
|
+
className='andy-predefined-question'
|
|
967
|
+
data-question={q.question}
|
|
968
|
+
disabled={isCollectingUserInfo || isLoading || !config}
|
|
969
|
+
>
|
|
970
|
+
<span className='andy-predefined-question__icon'>💬</span>
|
|
971
|
+
<span className='andy-predefined-question__text'>{q.question}</span>
|
|
972
|
+
</button>
|
|
973
|
+
))}
|
|
974
|
+
</div>
|
|
975
|
+
</div>
|
|
976
|
+
)}
|
|
977
|
+
|
|
978
|
+
<div className='andy-chat-window__input'>
|
|
979
|
+
<div className='andy-input-container'>
|
|
980
|
+
<div className='andy-input'>
|
|
981
|
+
<input
|
|
982
|
+
type='text'
|
|
983
|
+
className='andy-input__field'
|
|
984
|
+
id='andy-message-input'
|
|
985
|
+
placeholder={config ? 'Type your message...' : '...'}
|
|
986
|
+
aria-label='Message input'
|
|
987
|
+
onKeyDown={e => {
|
|
988
|
+
if (e.key === 'Enter' && (e.target as HTMLInputElement).value.trim()) {
|
|
989
|
+
handleSendMessage((e.target as HTMLInputElement).value)
|
|
990
|
+
;(e.target as HTMLInputElement).value = ''
|
|
991
|
+
}
|
|
992
|
+
}}
|
|
993
|
+
disabled={isCollectingUserInfo || !config}
|
|
994
|
+
/>
|
|
995
|
+
<button
|
|
996
|
+
type='button'
|
|
997
|
+
className='andy-input__button'
|
|
998
|
+
id='andy-send-button'
|
|
999
|
+
aria-label='Send message'
|
|
1000
|
+
onClick={() => {
|
|
1001
|
+
const input = shadowRef.current?.getElementById(
|
|
1002
|
+
'andy-message-input'
|
|
1003
|
+
) as HTMLInputElement | null
|
|
1004
|
+
if (input && input.value.trim()) {
|
|
1005
|
+
handleSendMessage(input.value)
|
|
1006
|
+
input.value = ''
|
|
1007
|
+
}
|
|
1008
|
+
}}
|
|
1009
|
+
disabled={isCollectingUserInfo || !config}
|
|
1010
|
+
>
|
|
1011
|
+
<svg
|
|
1012
|
+
className='andy-input__button-icon'
|
|
1013
|
+
width='18'
|
|
1014
|
+
height='18'
|
|
1015
|
+
viewBox='0 0 24 24'
|
|
1016
|
+
fill='currentColor'
|
|
1017
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
1018
|
+
>
|
|
1019
|
+
<path d='M2.01 21L23 12 2.01 3 2 10l15 2-15 2z' />
|
|
1020
|
+
</svg>
|
|
1021
|
+
</button>
|
|
1022
|
+
</div>
|
|
1023
|
+
</div>
|
|
1024
|
+
</div>
|
|
1025
|
+
|
|
1026
|
+
{config?.showBranding !== false && (
|
|
1027
|
+
<footer className='andy-chat-window__footer'>
|
|
1028
|
+
<a
|
|
1029
|
+
href={getFooterUrl()}
|
|
1030
|
+
target='_blank'
|
|
1031
|
+
rel='noopener noreferrer'
|
|
1032
|
+
className='andy-chat-window__footer-link'
|
|
1033
|
+
>
|
|
1034
|
+
<span className='andy-chat-window__footer-text'>{FOOTER_TEXT}</span>
|
|
1035
|
+
</a>
|
|
1036
|
+
</footer>
|
|
1037
|
+
)}
|
|
1038
|
+
</div>
|
|
1039
|
+
</div>,
|
|
1040
|
+
portalRef.current
|
|
1041
|
+
)}
|
|
1042
|
+
{showWelcomePopup &&
|
|
1043
|
+
!isOpen &&
|
|
1044
|
+
ReactDOM.createPortal(
|
|
1045
|
+
<div
|
|
1046
|
+
className={`andy-welcome-popup ${config?.orientation === 'left' ? 'andychat-popup-left' : ''}`}
|
|
1047
|
+
style={{ display: 'block' }}
|
|
1048
|
+
role='dialog'
|
|
1049
|
+
aria-label='Welcome message'
|
|
1050
|
+
>
|
|
1051
|
+
<div className='andy-welcome-popup__content'>
|
|
1052
|
+
<div className='andy-welcome-popup__message'>
|
|
1053
|
+
{config?.showWelcomePopup?.message ||
|
|
1054
|
+
config?.welcomeMessage ||
|
|
1055
|
+
'Welcome! How can I help you today?'}
|
|
1056
|
+
</div>
|
|
1057
|
+
<button
|
|
1058
|
+
type='button'
|
|
1059
|
+
className='andy-welcome-popup__close'
|
|
1060
|
+
aria-label='Close welcome message'
|
|
1061
|
+
onClick={() => setShowWelcomePopup(false)}
|
|
1062
|
+
>
|
|
1063
|
+
<svg
|
|
1064
|
+
className='andy-welcome-popup__close-icon'
|
|
1065
|
+
width='20'
|
|
1066
|
+
height='20'
|
|
1067
|
+
viewBox='0 0 24 24'
|
|
1068
|
+
fill='none'
|
|
1069
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
1070
|
+
>
|
|
1071
|
+
<path
|
|
1072
|
+
d='M18 6L6 18M6 6l12 12'
|
|
1073
|
+
stroke='currentColor'
|
|
1074
|
+
strokeWidth='2'
|
|
1075
|
+
strokeLinecap='round'
|
|
1076
|
+
strokeLinejoin='round'
|
|
1077
|
+
/>
|
|
1078
|
+
</svg>
|
|
1079
|
+
</button>
|
|
1080
|
+
</div>
|
|
1081
|
+
<div className='andy-welcome-popup__arrow'></div>
|
|
1082
|
+
</div>,
|
|
1083
|
+
portalRef.current
|
|
1084
|
+
)}
|
|
1085
|
+
</>
|
|
1086
|
+
)}
|
|
1087
|
+
</div>
|
|
1088
|
+
)
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
export default AndyChat
|