andy-widget-react 2.0.6 → 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 CHANGED
@@ -46,6 +46,8 @@ Widget as a short, language-aware retry message.
46
46
  | `description` | `string` | Override the configured description |
47
47
  | `welcomeMessage` | `string` | Override the configured first message |
48
48
  | `bubbleLabel` | `string` | Override the configured pill label |
49
+ | `lang` | `'en' \| 'es'` | Language for the Widget's own chrome (placeholder, starter-question title, error and handoff text). Overrides the Agent's `defaultLang`, so a Spanish Agent on an English page still renders English chrome |
50
+ | `predefinedQuestions` | `{ order: number, question: string }[]` | Override the configured starter questions |
49
51
  | `theme` | `'light' \| 'dark' \| 'auto'` | Override the configured theme |
50
52
  | `orientation` | `'left' \| 'right'` | Override the configured position |
51
53
  | `brandColor` | `string` | Override the configured six-digit hex color |
package/dist/index.cjs CHANGED
@@ -3437,6 +3437,34 @@ const SVG_ICONS = {
3437
3437
  <path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z" />
3438
3438
  </svg>`};
3439
3439
 
3440
+ // Localized UI Strings
3441
+ // Chrome the widget renders itself, as opposed to copy the Agent supplies.
3442
+ // Keyed by language so React and Vanilla render identical text for a visitor.
3443
+ const UI_STRINGS = {
3444
+ en: {
3445
+ inputPlaceholder: 'Type your message...',
3446
+ predefinedQuestionsTitle: 'What would you like to know?',
3447
+ sendFailure: 'Sorry, I could not answer. Please try again.',
3448
+ handoffWaiting: 'A team member will reply soon.',
3449
+ teamName: 'Team'
3450
+ },
3451
+ es: {
3452
+ inputPlaceholder: 'Escribe tu mensaje...',
3453
+ predefinedQuestionsTitle: '¿Qué te gustaría saber?',
3454
+ sendFailure: 'Lo siento, no pude responder. Inténtalo de nuevo.',
3455
+ handoffWaiting: 'Un miembro del equipo te responderá pronto.',
3456
+ teamName: 'Equipo'
3457
+ }
3458
+ };
3459
+
3460
+ /**
3461
+ * Get the widget's own UI strings for a language.
3462
+ * Unknown languages fall back to English rather than rendering blanks.
3463
+ * @param {string} [lang] - Language code ('en' or 'es')
3464
+ * @returns {typeof UI_STRINGS.en} The localized string set
3465
+ */
3466
+ const getUiStrings = lang => UI_STRINGS[lang] || UI_STRINGS.en;
3467
+
3440
3468
  /**
3441
3469
  * Keyboard detection utility for handling mobile virtual keyboards
3442
3470
  * Provides cross-browser support for detecting keyboard open/close events
@@ -3855,8 +3883,13 @@ const createBrowserHandoff = (chatbotId, bootstrap, onProjection) => {
3855
3883
  }
3856
3884
  };
3857
3885
 
3858
- const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessage, bubbleLabel, theme, orientation, brandColor, bubbleStyle, useBrandColor, logoUrl, previewMode = false, initialOpen = false }) => {
3886
+ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessage, bubbleLabel, lang, predefinedQuestions, theme, orientation, brandColor, bubbleStyle, useBrandColor, logoUrl, previewMode = false, initialOpen = false }) => {
3859
3887
  const [config, setConfig] = react.useState(null);
3888
+ // The visitor's language wins over the Agent's own default: a Spanish Agent
3889
+ // embedded on an English page must still render English chrome.
3890
+ const uiLang = lang || config?.defaultLang || 'en';
3891
+ const strings = getUiStrings(uiLang);
3892
+ const activeQuestions = predefinedQuestions?.length ? predefinedQuestions : config?.predefinedQuestions;
3860
3893
  const [isOpen, setIsOpen] = react.useState(initialOpen);
3861
3894
  const [messages, setMessages] = react.useState([]);
3862
3895
  const [isLoading, setIsLoading] = react.useState(false);
@@ -3887,7 +3920,7 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
3887
3920
  pendingHandoffIdsRef.current = projection.pendingProjectionIds || [];
3888
3921
  const projectedMessages = projection.messages.map(message => ({ id: message.id, content: message.content, sender: message.role === 'user' ? 'user' : 'bot', timestamp: new Date(message.createdAt).toISOString(), isHuman: message.sentByHuman }));
3889
3922
  setMessages(projection.control === 'requested' || projection.control === 'human'
3890
- ? [...projectedMessages, { id: 'handoff-waiting', content: config.defaultLang === 'es' ? 'Un miembro del equipo te responderá pronto.' : 'A team member will reply soon.', sender: 'bot', timestamp: new Date().toISOString(), isWaiting: true }]
3923
+ ? [...projectedMessages, { id: 'handoff-waiting', content: strings.handoffWaiting, sender: 'bot', timestamp: new Date().toISOString(), isWaiting: true }]
3891
3924
  : projectedMessages);
3892
3925
  });
3893
3926
  handoffRef.current = handoff;
@@ -4076,12 +4109,12 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4076
4109
  bubbleStyle: bubbleStyle || 'icon',
4077
4110
  bubbleLabel: bubbleLabel || null,
4078
4111
  useBrandColor: useBrandColor !== undefined ? useBrandColor : true,
4079
- defaultLang: 'en',
4112
+ defaultLang: uiLang,
4080
4113
  showBranding: true,
4081
4114
  showWelcomePopup: { enabled: false, seconds: 3, message: '' },
4082
4115
  collectUserInfo: { enabled: false, message: '' },
4083
4116
  welcomeMessage: welcomeMessage || 'Hi there! How can I help you today?',
4084
- predefinedQuestions: [],
4117
+ predefinedQuestions: predefinedQuestions || [],
4085
4118
  faqs: []
4086
4119
  };
4087
4120
  setConfig(previewConfig);
@@ -4234,7 +4267,7 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4234
4267
  setIsLoading(true);
4235
4268
  const response = await sendMessageForWidget(embedId, sessionId, messageText);
4236
4269
  if (response.status === 'waiting') {
4237
- setMessages(prev => prev.some(message => message.isWaiting) ? prev : [...prev, { id: 'handoff-waiting', content: config?.defaultLang === 'es' ? 'Un miembro del equipo te responderá pronto.' : 'A team member will reply soon.', sender: 'bot', timestamp: new Date().toISOString(), isWaiting: true }]);
4270
+ setMessages(prev => prev.some(message => message.isWaiting) ? prev : [...prev, { id: 'handoff-waiting', content: strings.handoffWaiting, sender: 'bot', timestamp: new Date().toISOString(), isWaiting: true }]);
4238
4271
  return;
4239
4272
  }
4240
4273
  const botMessage = {
@@ -4261,9 +4294,7 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4261
4294
  console.error('AndyChat: Message request failed.');
4262
4295
  const errorMessage = {
4263
4296
  id: timestamp + 3,
4264
- content: config?.defaultLang === 'es'
4265
- ? 'Lo siento, no pude responder. Inténtalo de nuevo.'
4266
- : 'Sorry, I could not answer. Please try again.',
4297
+ content: strings.sendFailure,
4267
4298
  sender: 'bot',
4268
4299
  timestamp: new Date().toISOString(),
4269
4300
  isError: true
@@ -4379,7 +4410,7 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4379
4410
  bubble.addEventListener('click', () => setIsOpen(prev => !prev));
4380
4411
  }
4381
4412
  const isPillStyle = config?.bubbleStyle === 'pill';
4382
- const displayLabel = bubbleLabel || config?.bubbleLabel || (isPillStyle ? getDefaultBubbleLabel(config?.defaultLang || 'en') : null);
4413
+ const displayLabel = bubbleLabel || config?.bubbleLabel || (isPillStyle ? getDefaultBubbleLabel(uiLang) : null);
4383
4414
  const bubbleLogoUrl = config?.logoUrl || CHAT_BUBBLE_DEFAULT_ICON_URL;
4384
4415
  if (bubble) {
4385
4416
  bubble.className = 'andy-chat-bubble';
@@ -4432,10 +4463,10 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4432
4463
  if (message.isLoading) {
4433
4464
  return (jsxRuntime.jsx("div", { className: 'andy-message-container andy-message-container--bot andy-message-container--loading', children: jsxRuntime.jsx("div", { className: 'andy-message andy-message--bot', children: jsxRuntime.jsxs("div", { className: 'andy-typing-indicator', children: [jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' }), jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' }), jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' })] }) }) }, message.id));
4434
4465
  }
4435
- return (jsxRuntime.jsxs("div", { className: `andy-message-container andy-message-container--${message.sender} ${message.isWaiting ? 'andy-message-container--waiting' : ''}`, children: [message.sender === 'bot' && (jsxRuntime.jsxs("div", { className: `andy-message andy-message--bot ${message.isHuman ? 'andy-message--operator' : ''} ${message.isWaiting ? 'andy-message--waiting' : ''} ${message.isError ? 'andy-message--error' : ''}`, children: [jsxRuntime.jsxs("div", { className: 'andy-bot-header', children: [jsxRuntime.jsx("div", { className: 'andy-bot-header__icon', children: config?.logoUrl ? (jsxRuntime.jsx("img", { src: config.logoUrl, alt: 'Bot Avatar', className: 'andy-bot-header__icon-image' })) : (jsxRuntime.jsx("img", { src: CHAT_BUBBLE_DEFAULT_ICON_URL, alt: 'Bot Avatar', className: 'andy-bot-header__icon-image andy-bot-header__icon-image--default' })) }), jsxRuntime.jsx("span", { className: 'andy-bot-header__name', children: message.isHuman ? (config?.defaultLang === 'es' ? 'Equipo' : 'Team') : config?.name || 'Andy bot' })] }), jsxRuntime.jsx("div", { className: 'andy-message__content', children: jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: parseMarkdown(message.content) } }) })] })), message.sender === 'user' && (jsxRuntime.jsx("div", { className: 'andy-message andy-message--user', children: jsxRuntime.jsx("div", { className: 'andy-message__content', children: jsxRuntime.jsx("p", { children: message.content }) }) }))] }, message.id));
4436
- }), isLoading && (jsxRuntime.jsx("div", { className: 'andy-message-container andy-message-container--bot andy-message-container--typing', children: jsxRuntime.jsx("div", { className: 'andy-message andy-message--bot andy-message--typing', children: jsxRuntime.jsxs("div", { className: 'andy-typing-indicator', children: [jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' }), jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' }), jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' })] }) }) }))] }), jsxRuntime.jsxs("div", { className: 'andy-chat-window__bottom', children: [config?.predefinedQuestions &&
4437
- config.predefinedQuestions.length > 0 &&
4438
- showPredefinedQuestions && (jsxRuntime.jsxs("div", { className: 'andy-predefined-questions', id: 'andy-predefined-questions', children: [jsxRuntime.jsx("div", { className: 'andy-predefined-questions__header', children: jsxRuntime.jsx("p", { className: 'andy-predefined-questions__title', children: "What would you like to know?" }) }), jsxRuntime.jsx("div", { className: 'andy-predefined-questions__list', children: config.predefinedQuestions.map((q, index) => (jsxRuntime.jsxs("button", { type: 'button', className: 'andy-predefined-question', "data-question": q.question, disabled: isCollectingUserInfo || isLoading || !config, children: [jsxRuntime.jsx("span", { className: 'andy-predefined-question__icon', children: "\uD83D\uDCAC" }), jsxRuntime.jsx("span", { className: 'andy-predefined-question__text', children: q.question })] }, index))) })] })), jsxRuntime.jsx("div", { className: 'andy-chat-window__input', children: jsxRuntime.jsx("div", { className: 'andy-input-container', children: jsxRuntime.jsxs("div", { className: 'andy-input', children: [jsxRuntime.jsx("input", { type: 'text', className: 'andy-input__field', id: 'andy-message-input', placeholder: config ? 'Type your message...' : '...', "aria-label": 'Message input', onKeyDown: e => {
4466
+ return (jsxRuntime.jsxs("div", { className: `andy-message-container andy-message-container--${message.sender} ${message.isWaiting ? 'andy-message-container--waiting' : ''}`, children: [message.sender === 'bot' && (jsxRuntime.jsxs("div", { className: `andy-message andy-message--bot ${message.isHuman ? 'andy-message--operator' : ''} ${message.isWaiting ? 'andy-message--waiting' : ''} ${message.isError ? 'andy-message--error' : ''}`, children: [jsxRuntime.jsxs("div", { className: 'andy-bot-header', children: [jsxRuntime.jsx("div", { className: 'andy-bot-header__icon', children: config?.logoUrl ? (jsxRuntime.jsx("img", { src: config.logoUrl, alt: 'Bot Avatar', className: 'andy-bot-header__icon-image' })) : (jsxRuntime.jsx("img", { src: CHAT_BUBBLE_DEFAULT_ICON_URL, alt: 'Bot Avatar', className: 'andy-bot-header__icon-image andy-bot-header__icon-image--default' })) }), jsxRuntime.jsx("span", { className: 'andy-bot-header__name', children: message.isHuman ? strings.teamName : config?.name || 'Andy bot' })] }), jsxRuntime.jsx("div", { className: 'andy-message__content', children: jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: parseMarkdown(message.content) } }) })] })), message.sender === 'user' && (jsxRuntime.jsx("div", { className: 'andy-message andy-message--user', children: jsxRuntime.jsx("div", { className: 'andy-message__content', children: jsxRuntime.jsx("p", { children: message.content }) }) }))] }, message.id));
4467
+ }), isLoading && (jsxRuntime.jsx("div", { className: 'andy-message-container andy-message-container--bot andy-message-container--typing', children: jsxRuntime.jsx("div", { className: 'andy-message andy-message--bot andy-message--typing', children: jsxRuntime.jsxs("div", { className: 'andy-typing-indicator', children: [jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' }), jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' }), jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' })] }) }) }))] }), jsxRuntime.jsxs("div", { className: 'andy-chat-window__bottom', children: [activeQuestions &&
4468
+ activeQuestions.length > 0 &&
4469
+ showPredefinedQuestions && (jsxRuntime.jsxs("div", { className: 'andy-predefined-questions', id: 'andy-predefined-questions', children: [jsxRuntime.jsx("div", { className: 'andy-predefined-questions__header', children: jsxRuntime.jsx("p", { className: 'andy-predefined-questions__title', children: strings.predefinedQuestionsTitle }) }), jsxRuntime.jsx("div", { className: 'andy-predefined-questions__list', children: activeQuestions.map((q, index) => (jsxRuntime.jsxs("button", { type: 'button', className: 'andy-predefined-question', "data-question": q.question, disabled: isCollectingUserInfo || isLoading || !config, children: [jsxRuntime.jsx("span", { className: 'andy-predefined-question__icon', children: "\uD83D\uDCAC" }), jsxRuntime.jsx("span", { className: 'andy-predefined-question__text', children: q.question })] }, index))) })] })), jsxRuntime.jsx("div", { className: 'andy-chat-window__input', children: jsxRuntime.jsx("div", { className: 'andy-input-container', children: jsxRuntime.jsxs("div", { className: 'andy-input', children: [jsxRuntime.jsx("input", { type: 'text', className: 'andy-input__field', id: 'andy-message-input', placeholder: config ? strings.inputPlaceholder : '...', "aria-label": 'Message input', onKeyDown: e => {
4439
4470
  if (e.key === 'Enter' && e.target.value.trim()) {
4440
4471
  handleSendMessage(e.target.value);
4441
4472
  e.target.value = '';