andy-widget-react 2.1.0 → 2.2.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/dist/index.cjs CHANGED
@@ -762,6 +762,84 @@ const initializeConfig = (packageName = 'unknown') => {
762
762
  }
763
763
  };
764
764
 
765
+ /** Stream a reply while the server restores this session's conversation history. */
766
+ const sendMessageForWidget = async (embedId, sessionId, message, options = {}) => {
767
+ const response = await fetch(`${getApiBaseUrl()}/chat/stream`, {
768
+ method: 'POST',
769
+ headers: { 'Content-Type': 'application/json' },
770
+ signal: options.signal,
771
+ body: JSON.stringify({
772
+ chatbotId: embedId,
773
+ sessionId,
774
+ historySource: 'session',
775
+ messages: [{ role: 'user', parts: [{ type: 'text', text: message }] }]
776
+ })
777
+ });
778
+ if (!response.ok || !response.headers.get('content-type')?.includes('text/event-stream')) {
779
+ const data = await response.json().catch(() => null);
780
+ throw new Error(data?.error || data?.message || 'Chat is temporarily unavailable.')
781
+ }
782
+ if (!response.body) throw new Error('Missing reply stream.')
783
+
784
+ const reader = response.body.getReader();
785
+ const decoder = new TextDecoder();
786
+ let buffer = '';
787
+ let eventData = [];
788
+ let text = '';
789
+ let published = '';
790
+ let complete = false;
791
+ let waiting = false;
792
+ const event = () => {
793
+ if (!eventData.length) return
794
+ const data = eventData.join('\n');
795
+ eventData = [];
796
+ if (data === '[DONE]') { complete = true; return }
797
+ const part = JSON.parse(data);
798
+ if (part.type === 'error' || part.type === 'abort') {
799
+ throw new Error(part.errorText || 'Reply interrupted.')
800
+ }
801
+ if (part.type === 'data-handoff' && part.data?.status === 'waiting') {
802
+ waiting = true;
803
+ return
804
+ }
805
+ if (part.type === 'text-start' && text && !text.endsWith('\n')) text += '\n';
806
+ if (part.type === 'text-delta') {
807
+ if (typeof part.delta !== 'string') throw new Error('Invalid reply text.')
808
+ text += part.delta;
809
+ }
810
+ };
811
+ const line = value => {
812
+ const cleaned = value.endsWith('\r') ? value.slice(0, -1) : value;
813
+ if (!cleaned) event();
814
+ else if (cleaned.startsWith('data:')) eventData.push(cleaned.slice(5).replace(/^ /, ''));
815
+ };
816
+ try {
817
+ while (!complete) {
818
+ const { value, done } = await reader.read();
819
+ buffer += decoder.decode(value, { stream: !done });
820
+ let newline;
821
+ while ((newline = buffer.indexOf('\n')) !== -1) {
822
+ line(buffer.slice(0, newline));
823
+ buffer = buffer.slice(newline + 1);
824
+ if (complete) break
825
+ }
826
+ // Publish once per received chunk, including the first useful text.
827
+ if (!waiting && text.trim() && text !== published) {
828
+ published = text;
829
+ options.onText?.(text);
830
+ }
831
+ if (done) break
832
+ }
833
+ if (!complete) throw new Error('Reply stream ended before completion.')
834
+ if (waiting) return { status: 'waiting' }
835
+ if (!text.trim()) throw new Error('The reply was empty.')
836
+ return { status: 'agent', text }
837
+ } finally {
838
+ await reader.cancel().catch(() => {});
839
+ reader.releaseLock();
840
+ }
841
+ };
842
+
765
843
  /**
766
844
  * AndyChat API client
767
845
  * Handles all communication with the AndyChat backend API
@@ -986,60 +1064,6 @@ const fetchChatbotConfig = async embedId => {
986
1064
  }
987
1065
  };
988
1066
 
989
- /**
990
- * Sends a user message to the API
991
- * @param {string} embedId - The chatbot embed ID
992
- * @param {string} sessionId - The current session ID
993
- * @param {string} message - The user's message
994
- * @returns {Promise<string>} The bot's response text
995
- */
996
- const sendMessageForWidget = async (embedId, sessionId, message) => {
997
- try {
998
- const apiBaseUrl = getApiBaseUrl();
999
- const response = await fetch(`${apiBaseUrl}/messages`, {
1000
- method: 'POST',
1001
- headers: { 'Content-Type': 'application/json' },
1002
- body: JSON.stringify({
1003
- chatbotId: embedId,
1004
- sessionId: sessionId,
1005
- currentUserMessage: {
1006
- role: 'user',
1007
- content: message
1008
- }
1009
- })
1010
- });
1011
-
1012
- if (response.ok) {
1013
- const data = await response.json();
1014
- if (data.error || data.limitStatus) {
1015
- throw new Error(data.error || 'Chat is temporarily unavailable.')
1016
- }
1017
- if (data.handoffStatus === 'paused') {
1018
- return { status: 'waiting' }
1019
- }
1020
- if (typeof data.botResponse !== 'string' || !data.botResponse.trim()) {
1021
- throw new Error('Unexpected response format from server.')
1022
- }
1023
- return { status: 'agent', text: data.botResponse }
1024
- } else {
1025
- // Handle HTTP errors (e.g., 500, 404)
1026
- let errorData;
1027
- try {
1028
- errorData = await response.json();
1029
- } catch (e) {
1030
- // If parsing error JSON fails, use status text
1031
- // Also, if response.text() is available and gives more info, consider using it.
1032
- const responseText = response.text ? await response.text() : '';
1033
- throw new Error(response.statusText || `Server returned an error. ${responseText}`.trim())
1034
- }
1035
- throw new Error(errorData.error || errorData.message || response.statusText || 'Server returned an error.')
1036
- }
1037
- } catch (error) {
1038
- console.error('AndyChat: Message request failed.');
1039
- throw error
1040
- }
1041
- };
1042
-
1043
1067
  /**
1044
1068
  * Visitor information collection is disabled until Andy exposes a durable public submission
1045
1069
  * contract. This explicit failure prevents a Widget from reporting a mocked success.
@@ -2133,6 +2157,10 @@ const getCoreStructuralCssString = () => {
2133
2157
  }
2134
2158
 
2135
2159
  /* Message Sub-Components */
2160
+ .andy-message__stream {
2161
+ white-space: pre-wrap;
2162
+ }
2163
+
2136
2164
  .andy-message__content {
2137
2165
  line-height: 1.4;
2138
2166
  margin: 0;
@@ -3442,6 +3470,13 @@ const SVG_ICONS = {
3442
3470
  // Keyed by language so React and Vanilla render identical text for a visitor.
3443
3471
  const UI_STRINGS = {
3444
3472
  en: {
3473
+ openChat: 'Open chat',
3474
+ closeChat: 'Close chat',
3475
+ goBack: 'Go back',
3476
+ messageInput: 'Message input',
3477
+ sendMessage: 'Send message',
3478
+ welcomeMessage: 'Welcome message',
3479
+ closeWelcomeMessage: 'Close welcome message',
3445
3480
  inputPlaceholder: 'Type your message...',
3446
3481
  predefinedQuestionsTitle: 'What would you like to know?',
3447
3482
  sendFailure: 'Sorry, I could not answer. Please try again.',
@@ -3449,6 +3484,13 @@ const UI_STRINGS = {
3449
3484
  teamName: 'Team'
3450
3485
  },
3451
3486
  es: {
3487
+ openChat: 'Abrir chat',
3488
+ closeChat: 'Cerrar chat',
3489
+ goBack: 'Volver',
3490
+ messageInput: 'Entrada de mensaje',
3491
+ sendMessage: 'Enviar mensaje',
3492
+ welcomeMessage: 'Mensaje de bienvenida',
3493
+ closeWelcomeMessage: 'Cerrar mensaje de bienvenida',
3452
3494
  inputPlaceholder: 'Escribe tu mensaje...',
3453
3495
  predefinedQuestionsTitle: '¿Qué te gustaría saber?',
3454
3496
  sendFailure: 'Lo siento, no pude responder. Inténtalo de nuevo.',
@@ -3757,12 +3799,12 @@ const callConvex = async (convexUrl, kind, path, wireArgs, keepalive = false) =>
3757
3799
  const sortMessages = messages => [...messages].sort((left, right) =>
3758
3800
  left.createdAt - right.createdAt || String(left.id).localeCompare(String(right.id)));
3759
3801
 
3760
- const createBrowserHandoff = (chatbotId, bootstrap, onProjection) => {
3802
+ const createBrowserHandoff = (chatbotId, bootstrap, onProjection, fresh = false) => {
3761
3803
  if (!bootstrap?.convexUrl || !bootstrap.surfaceKey || !bootstrap.surfaceProof) return null
3762
3804
  const storageKey = identityKey(chatbotId);
3763
3805
  let identity;
3764
3806
  try {
3765
- const stored = JSON.parse(window.sessionStorage.getItem(storageKey) || 'null');
3807
+ const stored = fresh ? null : JSON.parse(window.sessionStorage.getItem(storageKey) || 'null');
3766
3808
  identity = stored?.sessionId && stored?.capability && stored.surfaceKey === bootstrap.surfaceKey
3767
3809
  ? { sessionId: stored.sessionId, capability: stored.capability }
3768
3810
  : randomIdentity();
@@ -3851,6 +3893,14 @@ const createBrowserHandoff = (chatbotId, bootstrap, onProjection) => {
3851
3893
  if (active) onProjection({ offline: true, identity, messages: [] });
3852
3894
  }
3853
3895
  }, 15000);
3896
+ const stop = () => {
3897
+ if (!active) return
3898
+ active = false;
3899
+ window.clearInterval(timer);
3900
+ document.removeEventListener('visibilitychange', onVisibility);
3901
+ window.removeEventListener('pagehide', release);
3902
+ void release();
3903
+ };
3854
3904
  return {
3855
3905
  identity: { sessionId: identity.sessionId },
3856
3906
  recordVisitorMessage: content => {
@@ -3872,14 +3922,11 @@ const createBrowserHandoff = (chatbotId, bootstrap, onProjection) => {
3872
3922
  acknowledging.delete(messageId);
3873
3923
  }
3874
3924
  },
3875
- stop: () => {
3876
- if (!active) return
3877
- active = false;
3878
- window.clearInterval(timer);
3879
- document.removeEventListener('visibilitychange', onVisibility);
3880
- window.removeEventListener('pagehide', release);
3881
- void release();
3882
- }
3925
+ restart: () => {
3926
+ stop();
3927
+ return createBrowserHandoff(chatbotId, bootstrap, onProjection, true)
3928
+ },
3929
+ stop
3883
3930
  }
3884
3931
  };
3885
3932
 
@@ -3893,6 +3940,8 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
3893
3940
  const [isOpen, setIsOpen] = react.useState(initialOpen);
3894
3941
  const [messages, setMessages] = react.useState([]);
3895
3942
  const [isLoading, setIsLoading] = react.useState(false);
3943
+ const [replyStarted, setReplyStarted] = react.useState(false);
3944
+ const activeReplyRef = react.useRef(null);
3896
3945
  const [sessionId, setSessionId] = react.useState(() => generateSessionId());
3897
3946
  const handoffRef = react.useRef(null);
3898
3947
  const pendingHandoffIdsRef = react.useRef([]);
@@ -3916,16 +3965,27 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
3916
3965
  const handoff = createBrowserHandoff(config.id, config.bootstrap, projection => {
3917
3966
  if (projection.offline)
3918
3967
  return;
3968
+ const paused = projection.control === 'requested' || projection.control === 'human';
3969
+ if (paused)
3970
+ activeReplyRef.current?.abort();
3971
+ else if (activeReplyRef.current)
3972
+ return;
3919
3973
  setSessionId(projection.identity.sessionId);
3920
3974
  pendingHandoffIdsRef.current = projection.pendingProjectionIds || [];
3921
3975
  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 }));
3976
+ // An empty agent projection does not mean the client-only greeting is gone.
3977
+ if (projection.control === 'agent' && projectedMessages.length === 0)
3978
+ return;
3922
3979
  setMessages(projection.control === 'requested' || projection.control === 'human'
3923
3980
  ? [...projectedMessages, { id: 'handoff-waiting', content: strings.handoffWaiting, sender: 'bot', timestamp: new Date().toISOString(), isWaiting: true }]
3924
3981
  : projectedMessages);
3925
3982
  });
3926
3983
  handoffRef.current = handoff;
3984
+ if (handoff)
3985
+ setSessionId(handoff.identity.sessionId);
3927
3986
  return () => {
3928
- handoff?.stop();
3987
+ activeReplyRef.current?.abort();
3988
+ handoffRef.current?.stop();
3929
3989
  handoffRef.current = null;
3930
3990
  };
3931
3991
  }, [config?.bootstrap, config?.id]);
@@ -4247,11 +4307,13 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4247
4307
  }, [isCollectingUserInfo, messages]);
4248
4308
  const sendUserMessage = react.useCallback(async (rawMessageText, options = {}) => {
4249
4309
  const messageText = typeof rawMessageText === 'string' ? rawMessageText.trim() : '';
4250
- if (!messageText) {
4310
+ if (!messageText || activeReplyRef.current) {
4251
4311
  return;
4252
4312
  }
4253
4313
  const { hidePredefined = true } = options;
4254
4314
  const timestamp = Date.now();
4315
+ const controller = new AbortController();
4316
+ activeReplyRef.current = controller;
4255
4317
  const userMessage = {
4256
4318
  id: timestamp,
4257
4319
  content: messageText,
@@ -4265,18 +4327,30 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4265
4327
  }
4266
4328
  try {
4267
4329
  setIsLoading(true);
4268
- const response = await sendMessageForWidget(embedId, sessionId, messageText);
4330
+ setReplyStarted(false);
4331
+ const showReply = (content, isStreaming) => {
4332
+ if (controller.signal.aborted)
4333
+ return;
4334
+ setReplyStarted(true);
4335
+ const reply = { id: timestamp + 1, content, sender: 'bot', timestamp: new Date(timestamp).toISOString(), isStreaming };
4336
+ setMessages(prev => prev.some(message => message.id === reply.id)
4337
+ ? prev.map(message => message.id === reply.id ? reply : message)
4338
+ : [...prev, reply]);
4339
+ };
4340
+ const response = await sendMessageForWidget(embedId, sessionId, messageText, {
4341
+ signal: controller.signal,
4342
+ onText: text => showReply(text, true)
4343
+ });
4344
+ if (controller.signal.aborted)
4345
+ return;
4269
4346
  if (response.status === 'waiting') {
4270
- setMessages(prev => prev.some(message => message.isWaiting) ? prev : [...prev, { id: 'handoff-waiting', content: strings.handoffWaiting, sender: 'bot', timestamp: new Date().toISOString(), isWaiting: true }]);
4347
+ setMessages(prev => {
4348
+ const visible = prev.filter(message => message.id !== timestamp + 1);
4349
+ return visible.some(message => message.isWaiting) ? visible : [...visible, { id: 'handoff-waiting', content: strings.handoffWaiting, sender: 'bot', timestamp: new Date().toISOString(), isWaiting: true }];
4350
+ });
4271
4351
  return;
4272
4352
  }
4273
- const botMessage = {
4274
- id: timestamp + 1,
4275
- content: response.text,
4276
- sender: 'bot',
4277
- timestamp: new Date().toISOString()
4278
- };
4279
- setMessages(prev => [...prev, botMessage]);
4353
+ showReply(response.text, false);
4280
4354
  if (config?.collectUserInfo?.enabled && !userInfoCollected && !isCollectingUserInfo) {
4281
4355
  setPendingMessage(messageText);
4282
4356
  setIsCollectingUserInfo(true);
@@ -4291,6 +4365,8 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4291
4365
  }
4292
4366
  }
4293
4367
  catch {
4368
+ if (controller.signal.aborted)
4369
+ return;
4294
4370
  console.error('AndyChat: Message request failed.');
4295
4371
  const errorMessage = {
4296
4372
  id: timestamp + 3,
@@ -4299,10 +4375,14 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4299
4375
  timestamp: new Date().toISOString(),
4300
4376
  isError: true
4301
4377
  };
4302
- setMessages(prev => [...prev, errorMessage]);
4378
+ setMessages(prev => [...prev.filter(message => message.id !== timestamp + 1), errorMessage]);
4303
4379
  }
4304
4380
  finally {
4305
- setIsLoading(false);
4381
+ if (activeReplyRef.current === controller) {
4382
+ activeReplyRef.current = null;
4383
+ setIsLoading(false);
4384
+ setReplyStarted(false);
4385
+ }
4306
4386
  }
4307
4387
  }, [config, embedId, sessionId, userInfoCollected, isCollectingUserInfo]);
4308
4388
  const handleSendMessage = react.useCallback((messageText) => {
@@ -4401,7 +4481,7 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4401
4481
  bubble = document.createElement('button');
4402
4482
  bubble.type = 'button';
4403
4483
  bubble.className = 'andy-chat-bubble';
4404
- bubble.setAttribute('aria-label', 'Open chat');
4484
+ bubble.setAttribute('aria-label', strings.openChat);
4405
4485
  const tooltipHTML = tooltip
4406
4486
  ? `<span class="andy-chat-bubble__tooltip" role="tooltip">${tooltip}</span>`
4407
4487
  : '';
@@ -4413,6 +4493,7 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4413
4493
  const displayLabel = bubbleLabel || config?.bubbleLabel || (isPillStyle ? getDefaultBubbleLabel(uiLang) : null);
4414
4494
  const bubbleLogoUrl = config?.logoUrl || CHAT_BUBBLE_DEFAULT_ICON_URL;
4415
4495
  if (bubble) {
4496
+ bubble.setAttribute('aria-label', isOpen ? strings.closeChat : strings.openChat);
4416
4497
  bubble.className = 'andy-chat-bubble';
4417
4498
  if (isPillStyle) {
4418
4499
  bubble.classList.add('andy-chat-bubble--pill');
@@ -4455,7 +4536,7 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4455
4536
  }
4456
4537
  }
4457
4538
  }, [shadowReady, config, isOpen, tooltip, bubbleLabel]);
4458
- return (jsxRuntime.jsx("div", { ref: containerRef, id: 'andy-chatbot-container', children: portalRef.current && shadowReady && config && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ReactDOM.createPortal(jsxRuntime.jsxs("div", { className: `andy-chat-window ${isOpen ? 'andy-chat-window--open' : ''}`, children: [jsxRuntime.jsxs("header", { className: 'andy-header', children: [jsxRuntime.jsx("button", { type: 'button', className: 'andy-header__back', id: 'andy-back-button', "aria-label": 'Go back', onClick: () => setIsOpen(false), children: jsxRuntime.jsx("svg", { className: 'andy-header__back-icon', width: '20', height: '20', viewBox: '0 0 24 24', fill: 'none', children: jsxRuntime.jsx("path", { d: 'M15 18l-6-6 6-6', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' }) }) }), jsxRuntime.jsxs("div", { className: 'andy-header__bot-info', children: [jsxRuntime.jsx("div", { className: 'andy-header__avatar', children: config?.logoUrl ? (jsxRuntime.jsx("img", { src: config.logoUrl, alt: 'Bot Avatar', className: 'andy-header__avatar-image' })) : (jsxRuntime.jsx("img", { src: CHAT_BUBBLE_DEFAULT_ICON_URL, alt: 'Bot Avatar', className: 'andy-header__avatar-icon andy-header__avatar-icon--default' })) }), jsxRuntime.jsxs("div", { className: 'andy-header__text', children: [jsxRuntime.jsx("h1", { className: 'andy-header__name', children: config?.name || 'Andy bot' }), config?.headline && (jsxRuntime.jsx("p", { className: 'andy-header__subtitle', title: config.headline, children: config.headline }))] })] }), jsxRuntime.jsx("button", { type: 'button', className: 'andy-header__close', id: 'andy-close-button', "aria-label": 'Close chat', onClick: () => setIsOpen(false), children: jsxRuntime.jsx("svg", { className: 'andy-header__close-icon', width: '20', height: '20', viewBox: '0 0 24 24', fill: 'none', children: jsxRuntime.jsx("path", { d: 'M18 6L6 18M6 6l12 12', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' }) }) })] }), jsxRuntime.jsxs("div", { className: 'andy-chat-window__messages', id: 'andy-messages', ref: messagesContainerRef, children: [config?.description && (jsxRuntime.jsx("div", { className: 'andy-chat-subtitle', children: jsxRuntime.jsx("p", { className: 'andy-chat-subtitle__text', children: config.description }) })), messages.map(message => {
4539
+ return (jsxRuntime.jsx("div", { ref: containerRef, id: 'andy-chatbot-container', children: portalRef.current && shadowReady && config && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ReactDOM.createPortal(jsxRuntime.jsxs("div", { className: `andy-chat-window ${isOpen ? 'andy-chat-window--open' : ''}`, children: [jsxRuntime.jsxs("header", { className: 'andy-header', children: [jsxRuntime.jsx("button", { type: 'button', className: 'andy-header__back', id: 'andy-back-button', "aria-label": strings.goBack, onClick: () => setIsOpen(false), children: jsxRuntime.jsx("svg", { className: 'andy-header__back-icon', width: '20', height: '20', viewBox: '0 0 24 24', fill: 'none', children: jsxRuntime.jsx("path", { d: 'M15 18l-6-6 6-6', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' }) }) }), jsxRuntime.jsxs("div", { className: 'andy-header__bot-info', children: [jsxRuntime.jsx("div", { className: 'andy-header__avatar', children: config?.logoUrl ? (jsxRuntime.jsx("img", { src: config.logoUrl, alt: 'Bot Avatar', className: 'andy-header__avatar-image' })) : (jsxRuntime.jsx("img", { src: CHAT_BUBBLE_DEFAULT_ICON_URL, alt: 'Bot Avatar', className: 'andy-header__avatar-icon andy-header__avatar-icon--default' })) }), jsxRuntime.jsxs("div", { className: 'andy-header__text', children: [jsxRuntime.jsx("h1", { className: 'andy-header__name', children: config?.name || 'Andy bot' }), config?.headline && (jsxRuntime.jsx("p", { className: 'andy-header__subtitle', title: config.headline, children: config.headline }))] })] }), jsxRuntime.jsx("button", { type: 'button', className: 'andy-header__close', id: 'andy-close-button', "aria-label": strings.closeChat, onClick: () => setIsOpen(false), children: jsxRuntime.jsx("svg", { className: 'andy-header__close-icon', width: '20', height: '20', viewBox: '0 0 24 24', fill: 'none', children: jsxRuntime.jsx("path", { d: 'M18 6L6 18M6 6l12 12', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' }) }) })] }), jsxRuntime.jsxs("div", { className: 'andy-chat-window__messages', id: 'andy-messages', ref: messagesContainerRef, children: [config?.description && (jsxRuntime.jsx("div", { className: 'andy-chat-subtitle', children: jsxRuntime.jsx("p", { className: 'andy-chat-subtitle__text', children: config.description }) })), messages.map(message => {
4459
4540
  if (message.isForm) {
4460
4541
  return (jsxRuntime.jsxs("div", { className: 'andy-message-container andy-message-container--form', children: [jsxRuntime.jsx("div", { className: 'andy-avatar', children: jsxRuntime.jsx("svg", { className: 'andy-avatar__icon', width: '24', height: '24', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', children: jsxRuntime.jsx("path", { 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', fill: 'currentColor' }) }) }), jsxRuntime.jsx("div", { className: 'andy-message andy-message--bot andy-message--form', children: jsxRuntime.jsx("div", { className: 'andy-user-info-form', children: jsxRuntime.jsxs("div", { className: 'andy-user-info-form__content', children: [jsxRuntime.jsx("h3", { className: 'andy-user-info-form__title', children: config?.collectUserInfo?.message ||
4461
4542
  'Please provide your information to continue:' }), jsxRuntime.jsxs("form", { className: 'andy-user-info-form__form', id: 'andy-user-info-form', children: [jsxRuntime.jsxs("div", { className: 'andy-form-group', children: [jsxRuntime.jsx("label", { htmlFor: 'andy-user-name', className: 'andy-form-group__label', children: "Name" }), jsxRuntime.jsx("input", { type: 'text', id: 'andy-user-name', className: 'andy-form-group__input', placeholder: 'Your Name', required: true })] }), jsxRuntime.jsxs("div", { className: 'andy-form-group', children: [jsxRuntime.jsx("label", { htmlFor: 'andy-user-email', className: 'andy-form-group__label', children: "Email" }), jsxRuntime.jsx("input", { type: 'email', id: 'andy-user-email', className: 'andy-form-group__input', placeholder: 'Your Email', required: true })] }), jsxRuntime.jsx("button", { type: 'submit', className: 'andy-button andy-button--primary andy-user-info-form__submit', children: "Submit" })] })] }) }) })] }, message.id));
@@ -4463,25 +4544,31 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4463
4544
  if (message.isLoading) {
4464
4545
  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));
4465
4546
  }
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 &&
4547
+ 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: message.isStreaming ? jsxRuntime.jsx("div", { className: 'andy-message__stream', children: message.content }) : 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));
4548
+ }), isLoading && !replyStarted && (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
4549
  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 => {
4550
+ 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": strings.messageInput, onKeyDown: e => {
4551
+ if (e.key === 'Enter' && activeReplyRef.current) {
4552
+ e.preventDefault();
4553
+ return;
4554
+ }
4470
4555
  if (e.key === 'Enter' && e.target.value.trim()) {
4471
4556
  handleSendMessage(e.target.value);
4472
4557
  e.target.value = '';
4473
4558
  }
4474
- }, disabled: isCollectingUserInfo || !config }), jsxRuntime.jsx("button", { type: 'button', className: 'andy-input__button', id: 'andy-send-button', "aria-label": 'Send message', onClick: () => {
4559
+ }, disabled: isCollectingUserInfo || !config }), jsxRuntime.jsx("button", { type: 'button', className: 'andy-input__button', id: 'andy-send-button', "aria-label": strings.sendMessage, onClick: () => {
4560
+ if (activeReplyRef.current)
4561
+ return;
4475
4562
  const input = shadowRef.current?.getElementById('andy-message-input');
4476
4563
  if (input && input.value.trim()) {
4477
4564
  handleSendMessage(input.value);
4478
4565
  input.value = '';
4479
4566
  }
4480
- }, disabled: isCollectingUserInfo || !config, children: jsxRuntime.jsx("svg", { className: 'andy-input__button-icon', width: '18', height: '18', viewBox: '0 0 24 24', fill: 'currentColor', xmlns: 'http://www.w3.org/2000/svg', children: jsxRuntime.jsx("path", { d: 'M2.01 21L23 12 2.01 3 2 10l15 2-15 2z' }) }) })] }) }) }), config?.showBranding !== false && (jsxRuntime.jsx("footer", { className: 'andy-chat-window__footer', children: jsxRuntime.jsx("a", { href: getFooterUrl(), target: '_blank', rel: 'noopener noreferrer', className: 'andy-chat-window__footer-link', children: jsxRuntime.jsx("span", { className: 'andy-chat-window__footer-text', children: FOOTER_TEXT }) }) }))] })] }), portalRef.current), showWelcomePopup &&
4567
+ }, disabled: isCollectingUserInfo || isLoading || !config, children: jsxRuntime.jsx("svg", { className: 'andy-input__button-icon', width: '18', height: '18', viewBox: '0 0 24 24', fill: 'currentColor', xmlns: 'http://www.w3.org/2000/svg', children: jsxRuntime.jsx("path", { d: 'M2.01 21L23 12 2.01 3 2 10l15 2-15 2z' }) }) })] }) }) }), config?.showBranding !== false && (jsxRuntime.jsx("footer", { className: 'andy-chat-window__footer', children: jsxRuntime.jsx("a", { href: getFooterUrl(), target: '_blank', rel: 'noopener noreferrer', className: 'andy-chat-window__footer-link', children: jsxRuntime.jsx("span", { className: 'andy-chat-window__footer-text', children: FOOTER_TEXT }) }) }))] })] }), portalRef.current), showWelcomePopup &&
4481
4568
  !isOpen &&
4482
- ReactDOM.createPortal(jsxRuntime.jsxs("div", { className: `andy-welcome-popup ${config?.orientation === 'left' ? 'andychat-popup-left' : ''}`, style: { display: 'block' }, role: 'dialog', "aria-label": 'Welcome message', children: [jsxRuntime.jsxs("div", { className: 'andy-welcome-popup__content', children: [jsxRuntime.jsx("div", { className: 'andy-welcome-popup__message', children: config?.showWelcomePopup?.message ||
4569
+ ReactDOM.createPortal(jsxRuntime.jsxs("div", { className: `andy-welcome-popup ${config?.orientation === 'left' ? 'andychat-popup-left' : ''}`, style: { display: 'block' }, role: 'dialog', "aria-label": strings.welcomeMessage, children: [jsxRuntime.jsxs("div", { className: 'andy-welcome-popup__content', children: [jsxRuntime.jsx("div", { className: 'andy-welcome-popup__message', children: config?.showWelcomePopup?.message ||
4483
4570
  config?.welcomeMessage ||
4484
- 'Welcome! How can I help you today?' }), jsxRuntime.jsx("button", { type: 'button', className: 'andy-welcome-popup__close', "aria-label": 'Close welcome message', onClick: () => setShowWelcomePopup(false), children: jsxRuntime.jsx("svg", { className: 'andy-welcome-popup__close-icon', width: '20', height: '20', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', children: jsxRuntime.jsx("path", { d: 'M18 6L6 18M6 6l12 12', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' }) }) })] }), jsxRuntime.jsx("div", { className: 'andy-welcome-popup__arrow' })] }), portalRef.current)] })) }));
4571
+ 'Welcome! How can I help you today?' }), jsxRuntime.jsx("button", { type: 'button', className: 'andy-welcome-popup__close', "aria-label": strings.closeWelcomeMessage, onClick: () => setShowWelcomePopup(false), children: jsxRuntime.jsx("svg", { className: 'andy-welcome-popup__close-icon', width: '20', height: '20', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', children: jsxRuntime.jsx("path", { d: 'M18 6L6 18M6 6l12 12', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' }) }) })] }), jsxRuntime.jsx("div", { className: 'andy-welcome-popup__arrow' })] }), portalRef.current)] })) }));
4485
4572
  };
4486
4573
 
4487
4574
  const { SESSION_STORAGE_KEY, FORM_DISPLAY_DELAY_MS, PERSIST_SESSION } = CONFIG;
@@ -4498,6 +4585,11 @@ const useAndyChat = (embedId) => {
4498
4585
  const [showingWelcomePopup, setShowingWelcomePopup] = react.useState(false);
4499
4586
  const [showingUserInfoForm, setShowingUserInfoForm] = react.useState(false);
4500
4587
  const [welcomePopupShown, setWelcomePopupShown] = react.useState(false);
4588
+ const activeReplyRef = react.useRef(null);
4589
+ react.useEffect(() => () => {
4590
+ activeReplyRef.current?.abort();
4591
+ activeReplyRef.current = null;
4592
+ }, [embedId]);
4501
4593
  const welcomeTimeoutRef = react.useRef(null);
4502
4594
  const userInfoTimeoutRef = react.useRef(null);
4503
4595
  const initializeChatbot = react.useCallback(async () => {
@@ -4569,9 +4661,11 @@ const useAndyChat = (embedId) => {
4569
4661
  });
4570
4662
  }, [config, welcomePopupShown, userInfoCollected]);
4571
4663
  const sendMessage = react.useCallback(async (messageText) => {
4572
- if (!messageText.trim() || !config || !sessionId) {
4664
+ if (!messageText.trim() || !config || !sessionId || activeReplyRef.current) {
4573
4665
  return;
4574
4666
  }
4667
+ const controller = new AbortController();
4668
+ activeReplyRef.current = controller;
4575
4669
  const userMessage = {
4576
4670
  id: Date.now(),
4577
4671
  content: messageText,
@@ -4579,20 +4673,29 @@ const useAndyChat = (embedId) => {
4579
4673
  timestamp: new Date().toISOString()
4580
4674
  };
4581
4675
  setMessages(prev => [...prev, userMessage]);
4676
+ const replyId = Date.now() + 1;
4677
+ const showReply = (content, isStreaming) => {
4678
+ if (controller.signal.aborted)
4679
+ return;
4680
+ const reply = { id: replyId, content, sender: 'bot', timestamp: new Date().toISOString(), isStreaming };
4681
+ setMessages(prev => prev.some(message => message.id === replyId)
4682
+ ? prev.map(message => message.id === replyId ? reply : message)
4683
+ : [...prev, reply]);
4684
+ };
4582
4685
  try {
4583
- const response = await sendMessageForWidget(embedId, sessionId, messageText);
4584
- if (response.status === 'waiting')
4686
+ const response = await sendMessageForWidget(embedId, sessionId, messageText, { signal: controller.signal, onText: text => showReply(text, true) });
4687
+ if (controller.signal.aborted)
4688
+ return;
4689
+ if (response.status === 'waiting') {
4690
+ setMessages(prev => prev.filter(message => message.id !== replyId));
4585
4691
  return response;
4586
- const botMessage = {
4587
- id: Date.now() + 1,
4588
- content: response.text || 'I apologize, but I encountered an error processing your request.',
4589
- sender: 'bot',
4590
- timestamp: new Date().toISOString()
4591
- };
4592
- setMessages(prev => [...prev, botMessage]);
4692
+ }
4693
+ showReply(response.text, false);
4593
4694
  return response;
4594
4695
  }
4595
4696
  catch (err) {
4697
+ if (controller.signal.aborted)
4698
+ return;
4596
4699
  console.error('Error sending message:', err);
4597
4700
  const errorMessage = {
4598
4701
  id: Date.now() + 1,
@@ -4601,9 +4704,13 @@ const useAndyChat = (embedId) => {
4601
4704
  timestamp: new Date().toISOString(),
4602
4705
  isError: true
4603
4706
  };
4604
- setMessages(prev => [...prev, errorMessage]);
4707
+ setMessages(prev => [...prev.filter(message => message.id !== replyId), errorMessage]);
4605
4708
  throw err;
4606
4709
  }
4710
+ finally {
4711
+ if (activeReplyRef.current === controller)
4712
+ activeReplyRef.current = null;
4713
+ }
4607
4714
  }, [embedId, sessionId, config]);
4608
4715
  const showFaq = react.useCallback(() => {
4609
4716
  setShowingFaq(true);
@@ -4642,6 +4749,8 @@ const useAndyChat = (embedId) => {
4642
4749
  }
4643
4750
  }, [embedId, sessionId]);
4644
4751
  const startNewSession = react.useCallback(() => {
4752
+ activeReplyRef.current?.abort();
4753
+ activeReplyRef.current = null;
4645
4754
  const newSessionId = generateSessionId();
4646
4755
  setSessionId(newSessionId);
4647
4756
  {
@@ -4711,9 +4820,11 @@ const MessageComponent = ({ message, botName }) => {
4711
4820
  .filter(Boolean)
4712
4821
  .join(' ');
4713
4822
  const messageClasses = ['andy-message', `andy-message--${message.sender}`].filter(Boolean).join(' ');
4714
- const messageContent = isBot ? parseMarkdown(message.content) : message.content;
4823
+ const messageContent = isBot && !message.isStreaming ? parseMarkdown(message.content) : message.content;
4715
4824
  if (isBot) {
4716
- return (jsxRuntime.jsxs("div", { className: containerClasses, children: [jsxRuntime.jsxs("div", { className: 'andy-bot-header', children: [jsxRuntime.jsx("div", { className: 'andy-bot-header__icon', children: jsxRuntime.jsxs("svg", { width: '16', height: '16', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round', children: [jsxRuntime.jsx("path", { d: 'M12 6V2H8' }), jsxRuntime.jsx("path", { d: 'm8 18-4 4V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2Z' }), jsxRuntime.jsx("path", { d: 'M2 12h2' }), jsxRuntime.jsx("path", { d: 'M9 11v2' }), jsxRuntime.jsx("path", { d: 'M15 11v2' }), jsxRuntime.jsx("path", { d: 'M20 12h2' })] }) }), jsxRuntime.jsx("span", { className: 'andy-bot-header__name', children: botName || 'Andy bot' })] }), jsxRuntime.jsx("div", { className: messageClasses, children: jsxRuntime.jsx("div", { className: 'andy-message__content', dangerouslySetInnerHTML: { __html: messageContent } }) })] }));
4825
+ return (jsxRuntime.jsxs("div", { className: containerClasses, children: [jsxRuntime.jsxs("div", { className: 'andy-bot-header', children: [jsxRuntime.jsx("div", { className: 'andy-bot-header__icon', children: jsxRuntime.jsxs("svg", { width: '16', height: '16', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round', children: [jsxRuntime.jsx("path", { d: 'M12 6V2H8' }), jsxRuntime.jsx("path", { d: 'm8 18-4 4V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2Z' }), jsxRuntime.jsx("path", { d: 'M2 12h2' }), jsxRuntime.jsx("path", { d: 'M9 11v2' }), jsxRuntime.jsx("path", { d: 'M15 11v2' }), jsxRuntime.jsx("path", { d: 'M20 12h2' })] }) }), jsxRuntime.jsx("span", { className: 'andy-bot-header__name', children: botName || 'Andy bot' })] }), jsxRuntime.jsx("div", { className: messageClasses, children: message.isStreaming
4826
+ ? jsxRuntime.jsx("div", { className: 'andy-message__content andy-message__stream', children: message.content })
4827
+ : jsxRuntime.jsx("div", { className: 'andy-message__content', dangerouslySetInnerHTML: { __html: messageContent } }) })] }));
4717
4828
  }
4718
4829
  return (jsxRuntime.jsx("div", { className: containerClasses, children: jsxRuntime.jsx("div", { className: messageClasses, children: jsxRuntime.jsx("div", { className: 'andy-message__content', children: jsxRuntime.jsx("p", { children: messageContent }) }) }) }));
4719
4830
  };
@@ -4728,7 +4839,7 @@ const MessageList = ({ messages, isLoading, logoUrl, botName }) => {
4728
4839
  });
4729
4840
  }
4730
4841
  }, [messages, isLoading]);
4731
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [messages.map(message => (jsxRuntime.jsx(MessageComponent, { message: message, logoUrl: logoUrl, botName: botName }, message.id || message.timestamp))), isLoading && jsxRuntime.jsx(TypingIndicator, {}), messages.length === 0 && !isLoading && (jsxRuntime.jsxs("div", { className: 'andy-empty-state', children: [jsxRuntime.jsx("div", { className: 'andy-empty-state__icon', children: "\uD83D\uDCAC" }), jsxRuntime.jsx("div", { className: 'andy-empty-state__text', children: "Start a conversation by typing a message below" })] })), jsxRuntime.jsx("div", { ref: messagesEndRef })] }));
4842
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [messages.map(message => (jsxRuntime.jsx(MessageComponent, { message: message, logoUrl: logoUrl, botName: botName }, message.id || message.timestamp))), isLoading && !messages.some(message => message.isStreaming) && jsxRuntime.jsx(TypingIndicator, {}), messages.length === 0 && !isLoading && (jsxRuntime.jsxs("div", { className: 'andy-empty-state', children: [jsxRuntime.jsx("div", { className: 'andy-empty-state__icon', children: "\uD83D\uDCAC" }), jsxRuntime.jsx("div", { className: 'andy-empty-state__text', children: "Start a conversation by typing a message below" })] })), jsxRuntime.jsx("div", { ref: messagesEndRef })] }));
4732
4843
  };
4733
4844
 
4734
4845
  const MessageInput = ({ onSendMessage, disabled = false, placeholder = 'Type your message here...' }) => {
@@ -4743,14 +4854,15 @@ const MessageInput = ({ onSendMessage, disabled = false, placeholder = 'Type you
4743
4854
  }
4744
4855
  try {
4745
4856
  setIsSubmitting(true);
4746
- await onSendMessage(trimmedMessage);
4747
4857
  setMessage('');
4858
+ await onSendMessage(trimmedMessage);
4748
4859
  if (inputRef.current) {
4749
4860
  inputRef.current.focus();
4750
4861
  }
4751
4862
  }
4752
4863
  catch (error) {
4753
4864
  console.error('Error sending message:', error);
4865
+ setMessage(current => current || trimmedMessage);
4754
4866
  }
4755
4867
  finally {
4756
4868
  setIsSubmitting(false);
@@ -4767,7 +4879,7 @@ const MessageInput = ({ onSendMessage, disabled = false, placeholder = 'Type you
4767
4879
  };
4768
4880
  const isInputDisabled = disabled || isSubmitting;
4769
4881
  const canSend = message.trim().length > 0 && !isInputDisabled;
4770
- return (jsxRuntime.jsxs("div", { className: 'andy-input', children: [jsxRuntime.jsx("input", { ref: inputRef, type: 'text', value: message, onChange: handleInputChange, onKeyDown: handleKeyDown, placeholder: placeholder, disabled: isInputDisabled, maxLength: 1000, autoComplete: 'off', id: 'andy-input' }), jsxRuntime.jsx("button", { type: 'button', onClick: () => handleSubmit({ preventDefault: () => { } }), disabled: !canSend, id: 'andy-send', children: isSubmitting ? ('...') : (jsxRuntime.jsx("svg", { width: '18', height: '18', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', children: jsxRuntime.jsx("path", { d: 'M2.01 21L23 12 2.01 3 2 10l15 2-15 2z', fill: 'white' }) })) })] }));
4882
+ return (jsxRuntime.jsxs("div", { className: 'andy-input', children: [jsxRuntime.jsx("input", { ref: inputRef, type: 'text', value: message, onChange: handleInputChange, onKeyDown: handleKeyDown, placeholder: placeholder, disabled: disabled, maxLength: 1000, autoComplete: 'off', id: 'andy-input' }), jsxRuntime.jsx("button", { type: 'button', onClick: () => handleSubmit({ preventDefault: () => { } }), disabled: !canSend, id: 'andy-send', children: isSubmitting ? ('...') : (jsxRuntime.jsx("svg", { width: '18', height: '18', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', children: jsxRuntime.jsx("path", { d: 'M2.01 21L23 12 2.01 3 2 10l15 2-15 2z', fill: 'white' }) })) })] }));
4771
4883
  };
4772
4884
 
4773
4885
  const ChatWindow = ({ config, messages, isLoading, onClose, onSendMessage }) => {