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.
@@ -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.5",
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.5"
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
- sendMessage as sendMessageToAPI,
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: 'en',
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 (config?.predefinedQuestions && config.predefinedQuestions.length > 0) {
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(config?.defaultLang || 'en') : null)
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
- className={`andy-message andy-message--bot ${message.isError ? 'andy-message--error' : ''}`}
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
- <span className='andy-bot-header__name'>{config?.name || 'Andy bot'}</span>
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
- {config?.predefinedQuestions &&
955
- config.predefinedQuestions.length > 0 &&
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'>What would you like to know?</p>
997
+ <p className='andy-predefined-questions__title'>{strings.predefinedQuestionsTitle}</p>
960
998
  </div>
961
999
  <div className='andy-predefined-questions__list'>
962
- {config.predefinedQuestions.map((q, index) => (
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 ? 'Type your message...' : '...'}
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()) {
@@ -3,7 +3,7 @@ import {
3
3
  fetchChatbotConfig,
4
4
  generateSessionId,
5
5
  createGreetingMessage,
6
- sendMessage as sendMessageToAPI,
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
  }
@@ -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') */