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