andy-widget-react 2.0.5 → 2.1.0
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 +2 -0
- package/dist/index.cjs +240 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +240 -20
- package/dist/index.esm.js.map +1 -1
- package/dist/types/types/index.d.ts +13 -1
- package/package.json +2 -2
- package/src/AndyChat.tsx +56 -18
- package/src/hooks/useAndyChat.ts +3 -2
- package/src/types/index.ts +13 -1
|
@@ -37,15 +37,23 @@ export interface ChatbotConfig {
|
|
|
37
37
|
collectUserInfo: CollectUserInfoConfig;
|
|
38
38
|
predefinedQuestions: PredefinedQuestion[];
|
|
39
39
|
faqs: FAQItem[];
|
|
40
|
+
bootstrap?: {
|
|
41
|
+
convexUrl: string;
|
|
42
|
+
surfaceKey: string;
|
|
43
|
+
surfaceProof: string;
|
|
44
|
+
surfaceProofExpiresAt: number;
|
|
45
|
+
};
|
|
40
46
|
}
|
|
41
47
|
export interface Message {
|
|
42
|
-
id: number;
|
|
48
|
+
id: string | number;
|
|
43
49
|
content: string;
|
|
44
50
|
sender: 'user' | 'bot';
|
|
45
51
|
timestamp: string;
|
|
46
52
|
isForm?: boolean;
|
|
47
53
|
isLoading?: boolean;
|
|
48
54
|
isError?: boolean;
|
|
55
|
+
isHuman?: boolean;
|
|
56
|
+
isWaiting?: boolean;
|
|
49
57
|
}
|
|
50
58
|
export interface UserInfo {
|
|
51
59
|
name: string;
|
|
@@ -66,6 +74,10 @@ export interface AndyChatProps {
|
|
|
66
74
|
welcomeMessage?: string;
|
|
67
75
|
/** Optional custom label for pill-style bubble (overrides default) */
|
|
68
76
|
bubbleLabel?: string;
|
|
77
|
+
/** Optional UI language for the widget's own chrome, overrides the Agent's defaultLang */
|
|
78
|
+
lang?: 'en' | 'es';
|
|
79
|
+
/** Optional starter questions (overrides API value) */
|
|
80
|
+
predefinedQuestions?: PredefinedQuestion[];
|
|
69
81
|
/** Optional theme ('light' | 'dark' | 'auto') */
|
|
70
82
|
theme?: 'light' | 'dark' | 'auto';
|
|
71
83
|
/** Optional position ('left' | 'right') */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "andy-widget-react",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Official React Widget for embeddable Andy agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -67,6 +67,6 @@
|
|
|
67
67
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"andy-widget-core": "2.0
|
|
70
|
+
"andy-widget-core": "2.1.0"
|
|
71
71
|
}
|
|
72
72
|
}
|
package/src/AndyChat.tsx
CHANGED
|
@@ -5,11 +5,12 @@ import {
|
|
|
5
5
|
fetchChatbotConfig,
|
|
6
6
|
generateSessionId,
|
|
7
7
|
createGreetingMessage,
|
|
8
|
-
|
|
8
|
+
sendMessageForWidget as sendMessageToAPI,
|
|
9
9
|
submitUserInfo,
|
|
10
10
|
isValidEmbedId,
|
|
11
11
|
getFullStylesheet,
|
|
12
12
|
getHostClasses,
|
|
13
|
+
createBrowserHandoff,
|
|
13
14
|
parseMarkdown,
|
|
14
15
|
CHAT_BUBBLE_DEFAULT_ICON,
|
|
15
16
|
CHAT_BUBBLE_DEFAULT_ICON_URL,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
FOOTER_TEXT,
|
|
18
19
|
getFooterUrl,
|
|
19
20
|
getDefaultBubbleLabel,
|
|
21
|
+
getUiStrings,
|
|
20
22
|
getKeyboardDetector,
|
|
21
23
|
updateKeyboardHeight,
|
|
22
24
|
isMobileDevice
|
|
@@ -73,6 +75,8 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
73
75
|
description,
|
|
74
76
|
welcomeMessage,
|
|
75
77
|
bubbleLabel,
|
|
78
|
+
lang,
|
|
79
|
+
predefinedQuestions,
|
|
76
80
|
theme,
|
|
77
81
|
orientation,
|
|
78
82
|
brandColor,
|
|
@@ -83,10 +87,17 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
83
87
|
initialOpen = false
|
|
84
88
|
}) => {
|
|
85
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
|
|
86
95
|
const [isOpen, setIsOpen] = useState(initialOpen)
|
|
87
96
|
const [messages, setMessages] = useState<Message[]>([])
|
|
88
97
|
const [isLoading, setIsLoading] = useState(false)
|
|
89
98
|
const [sessionId, setSessionId] = useState(() => generateSessionId())
|
|
99
|
+
const handoffRef = useRef<ReturnType<typeof createBrowserHandoff>>(null)
|
|
100
|
+
const pendingHandoffIdsRef = useRef<string[]>([])
|
|
90
101
|
const [shadowReady, setShadowReady] = useState(false)
|
|
91
102
|
const [showPredefinedQuestions, setShowPredefinedQuestions] = useState(true)
|
|
92
103
|
const [userInfoCollected, setUserInfoCollected] = useState(false)
|
|
@@ -102,6 +113,29 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
102
113
|
const welcomePopupShownRef = useRef(false)
|
|
103
114
|
const welcomePopupTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
104
115
|
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!config?.bootstrap || !config.id) return
|
|
118
|
+
handoffRef.current?.stop()
|
|
119
|
+
const handoff = createBrowserHandoff(config.id, config.bootstrap, projection => {
|
|
120
|
+
if (projection.offline) return
|
|
121
|
+
setSessionId(projection.identity.sessionId)
|
|
122
|
+
pendingHandoffIdsRef.current = projection.pendingProjectionIds || []
|
|
123
|
+
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 }))
|
|
124
|
+
setMessages(projection.control === 'requested' || projection.control === 'human'
|
|
125
|
+
? [...projectedMessages, { id: 'handoff-waiting', content: strings.handoffWaiting, sender: 'bot', timestamp: new Date().toISOString(), isWaiting: true }]
|
|
126
|
+
: projectedMessages)
|
|
127
|
+
})
|
|
128
|
+
handoffRef.current = handoff
|
|
129
|
+
return () => {
|
|
130
|
+
handoff?.stop()
|
|
131
|
+
handoffRef.current = null
|
|
132
|
+
}
|
|
133
|
+
}, [config?.bootstrap, config?.id])
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
pendingHandoffIdsRef.current.forEach(messageId => void handoffRef.current?.markRendered(messageId))
|
|
137
|
+
}, [messages])
|
|
138
|
+
|
|
105
139
|
// Create shadow DOM container on mount
|
|
106
140
|
useEffect(() => {
|
|
107
141
|
if (!containerRef.current) return
|
|
@@ -300,12 +334,12 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
300
334
|
bubbleStyle: bubbleStyle || 'icon',
|
|
301
335
|
bubbleLabel: bubbleLabel || null,
|
|
302
336
|
useBrandColor: useBrandColor !== undefined ? useBrandColor : true,
|
|
303
|
-
defaultLang:
|
|
337
|
+
defaultLang: uiLang,
|
|
304
338
|
showBranding: true,
|
|
305
339
|
showWelcomePopup: { enabled: false, seconds: 3, message: '' },
|
|
306
340
|
collectUserInfo: { enabled: false, message: '' },
|
|
307
341
|
welcomeMessage: welcomeMessage || 'Hi there! How can I help you today?',
|
|
308
|
-
predefinedQuestions: [],
|
|
342
|
+
predefinedQuestions: predefinedQuestions || [],
|
|
309
343
|
faqs: []
|
|
310
344
|
}
|
|
311
345
|
setConfig(previewConfig)
|
|
@@ -469,7 +503,7 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
469
503
|
setMessages([])
|
|
470
504
|
}
|
|
471
505
|
|
|
472
|
-
if (
|
|
506
|
+
if (activeQuestions && activeQuestions.length > 0) {
|
|
473
507
|
setShowPredefinedQuestions(true)
|
|
474
508
|
}
|
|
475
509
|
}
|
|
@@ -490,6 +524,7 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
490
524
|
sender: 'user',
|
|
491
525
|
timestamp: new Date().toISOString()
|
|
492
526
|
}
|
|
527
|
+
handoffRef.current?.recordVisitorMessage(messageText)
|
|
493
528
|
setMessages(prev => [...prev, userMessage])
|
|
494
529
|
|
|
495
530
|
if (hidePredefined) {
|
|
@@ -499,10 +534,14 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
499
534
|
try {
|
|
500
535
|
setIsLoading(true)
|
|
501
536
|
const response = await sendMessageToAPI(embedId!, sessionId, messageText)
|
|
537
|
+
if (response.status === 'waiting') {
|
|
538
|
+
setMessages(prev => prev.some(message => message.isWaiting) ? prev : [...prev, { id: 'handoff-waiting', content: strings.handoffWaiting, sender: 'bot', timestamp: new Date().toISOString(), isWaiting: true }])
|
|
539
|
+
return
|
|
540
|
+
}
|
|
502
541
|
|
|
503
542
|
const botMessage: Message = {
|
|
504
543
|
id: timestamp + 1,
|
|
505
|
-
content: response,
|
|
544
|
+
content: response.text,
|
|
506
545
|
sender: 'bot',
|
|
507
546
|
timestamp: new Date().toISOString()
|
|
508
547
|
}
|
|
@@ -525,10 +564,7 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
525
564
|
console.error('AndyChat: Message request failed.')
|
|
526
565
|
const errorMessage: Message = {
|
|
527
566
|
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.',
|
|
567
|
+
content: strings.sendFailure,
|
|
532
568
|
sender: 'bot',
|
|
533
569
|
timestamp: new Date().toISOString(),
|
|
534
570
|
isError: true
|
|
@@ -673,7 +709,7 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
673
709
|
|
|
674
710
|
const isPillStyle = config?.bubbleStyle === 'pill'
|
|
675
711
|
const displayLabel =
|
|
676
|
-
bubbleLabel || config?.bubbleLabel || (isPillStyle ? getDefaultBubbleLabel(
|
|
712
|
+
bubbleLabel || config?.bubbleLabel || (isPillStyle ? getDefaultBubbleLabel(uiLang) : null)
|
|
677
713
|
const bubbleLogoUrl = config?.logoUrl || CHAT_BUBBLE_DEFAULT_ICON_URL
|
|
678
714
|
|
|
679
715
|
if (bubble) {
|
|
@@ -897,11 +933,11 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
897
933
|
return (
|
|
898
934
|
<div
|
|
899
935
|
key={message.id}
|
|
900
|
-
className={`andy-message-container andy-message-container--${message.sender}`}
|
|
936
|
+
className={`andy-message-container andy-message-container--${message.sender} ${message.isWaiting ? 'andy-message-container--waiting' : ''}`}
|
|
901
937
|
>
|
|
902
938
|
{message.sender === 'bot' && (
|
|
903
939
|
<div
|
|
904
|
-
|
|
940
|
+
className={`andy-message andy-message--bot ${message.isHuman ? 'andy-message--operator' : ''} ${message.isWaiting ? 'andy-message--waiting' : ''} ${message.isError ? 'andy-message--error' : ''}`}
|
|
905
941
|
>
|
|
906
942
|
<div className='andy-bot-header'>
|
|
907
943
|
<div className='andy-bot-header__icon'>
|
|
@@ -919,7 +955,9 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
919
955
|
/>
|
|
920
956
|
)}
|
|
921
957
|
</div>
|
|
922
|
-
|
|
958
|
+
<span className='andy-bot-header__name'>
|
|
959
|
+
{message.isHuman ? strings.teamName : config?.name || 'Andy bot'}
|
|
960
|
+
</span>
|
|
923
961
|
</div>
|
|
924
962
|
<div className='andy-message__content'>
|
|
925
963
|
<div dangerouslySetInnerHTML={{ __html: parseMarkdown(message.content) }} />
|
|
@@ -951,15 +989,15 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
951
989
|
</div>
|
|
952
990
|
|
|
953
991
|
<div className='andy-chat-window__bottom'>
|
|
954
|
-
{
|
|
955
|
-
|
|
992
|
+
{activeQuestions &&
|
|
993
|
+
activeQuestions.length > 0 &&
|
|
956
994
|
showPredefinedQuestions && (
|
|
957
995
|
<div className='andy-predefined-questions' id='andy-predefined-questions'>
|
|
958
996
|
<div className='andy-predefined-questions__header'>
|
|
959
|
-
<p className='andy-predefined-questions__title'>
|
|
997
|
+
<p className='andy-predefined-questions__title'>{strings.predefinedQuestionsTitle}</p>
|
|
960
998
|
</div>
|
|
961
999
|
<div className='andy-predefined-questions__list'>
|
|
962
|
-
{
|
|
1000
|
+
{activeQuestions.map((q, index) => (
|
|
963
1001
|
<button
|
|
964
1002
|
key={index}
|
|
965
1003
|
type='button'
|
|
@@ -982,7 +1020,7 @@ const AndyChat: React.FC<AndyChatProps> = ({
|
|
|
982
1020
|
type='text'
|
|
983
1021
|
className='andy-input__field'
|
|
984
1022
|
id='andy-message-input'
|
|
985
|
-
placeholder={config ?
|
|
1023
|
+
placeholder={config ? strings.inputPlaceholder : '...'}
|
|
986
1024
|
aria-label='Message input'
|
|
987
1025
|
onKeyDown={e => {
|
|
988
1026
|
if (e.key === 'Enter' && (e.target as HTMLInputElement).value.trim()) {
|
package/src/hooks/useAndyChat.ts
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
fetchChatbotConfig,
|
|
4
4
|
generateSessionId,
|
|
5
5
|
createGreetingMessage,
|
|
6
|
-
|
|
6
|
+
sendMessageForWidget as sendMessageToAPI,
|
|
7
7
|
submitUserInfo as submitUserInfoAPI,
|
|
8
8
|
initializeConfig,
|
|
9
9
|
validateChatbotConfig,
|
|
@@ -142,10 +142,11 @@ export const useAndyChat = (embedId: string): UseAndyChatReturn => {
|
|
|
142
142
|
|
|
143
143
|
try {
|
|
144
144
|
const response = await sendMessageToAPI(embedId, sessionId, messageText)
|
|
145
|
+
if (response.status === 'waiting') return response
|
|
145
146
|
|
|
146
147
|
const botMessage: Message = {
|
|
147
148
|
id: Date.now() + 1,
|
|
148
|
-
content: response || 'I apologize, but I encountered an error processing your request.',
|
|
149
|
+
content: response.text || 'I apologize, but I encountered an error processing your request.',
|
|
149
150
|
sender: 'bot',
|
|
150
151
|
timestamp: new Date().toISOString()
|
|
151
152
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -47,17 +47,25 @@ export interface ChatbotConfig {
|
|
|
47
47
|
collectUserInfo: CollectUserInfoConfig
|
|
48
48
|
predefinedQuestions: PredefinedQuestion[]
|
|
49
49
|
faqs: FAQItem[]
|
|
50
|
+
bootstrap?: {
|
|
51
|
+
convexUrl: string
|
|
52
|
+
surfaceKey: string
|
|
53
|
+
surfaceProof: string
|
|
54
|
+
surfaceProofExpiresAt: number
|
|
55
|
+
}
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
// Message in the chat
|
|
53
59
|
export interface Message {
|
|
54
|
-
id: number
|
|
60
|
+
id: string | number
|
|
55
61
|
content: string
|
|
56
62
|
sender: 'user' | 'bot'
|
|
57
63
|
timestamp: string
|
|
58
64
|
isForm?: boolean
|
|
59
65
|
isLoading?: boolean
|
|
60
66
|
isError?: boolean
|
|
67
|
+
isHuman?: boolean
|
|
68
|
+
isWaiting?: boolean
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
// User info for form submission
|
|
@@ -82,6 +90,10 @@ export interface AndyChatProps {
|
|
|
82
90
|
welcomeMessage?: string
|
|
83
91
|
/** Optional custom label for pill-style bubble (overrides default) */
|
|
84
92
|
bubbleLabel?: string
|
|
93
|
+
/** Optional UI language for the widget's own chrome, overrides the Agent's defaultLang */
|
|
94
|
+
lang?: 'en' | 'es'
|
|
95
|
+
/** Optional starter questions (overrides API value) */
|
|
96
|
+
predefinedQuestions?: PredefinedQuestion[]
|
|
85
97
|
/** Optional theme ('light' | 'dark' | 'auto') */
|
|
86
98
|
theme?: 'light' | 'dark' | 'auto'
|
|
87
99
|
/** Optional position ('left' | 'right') */
|