andy-widget-react 2.1.1 → 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;
@@ -3771,12 +3799,12 @@ const callConvex = async (convexUrl, kind, path, wireArgs, keepalive = false) =>
3771
3799
  const sortMessages = messages => [...messages].sort((left, right) =>
3772
3800
  left.createdAt - right.createdAt || String(left.id).localeCompare(String(right.id)));
3773
3801
 
3774
- const createBrowserHandoff = (chatbotId, bootstrap, onProjection) => {
3802
+ const createBrowserHandoff = (chatbotId, bootstrap, onProjection, fresh = false) => {
3775
3803
  if (!bootstrap?.convexUrl || !bootstrap.surfaceKey || !bootstrap.surfaceProof) return null
3776
3804
  const storageKey = identityKey(chatbotId);
3777
3805
  let identity;
3778
3806
  try {
3779
- const stored = JSON.parse(window.sessionStorage.getItem(storageKey) || 'null');
3807
+ const stored = fresh ? null : JSON.parse(window.sessionStorage.getItem(storageKey) || 'null');
3780
3808
  identity = stored?.sessionId && stored?.capability && stored.surfaceKey === bootstrap.surfaceKey
3781
3809
  ? { sessionId: stored.sessionId, capability: stored.capability }
3782
3810
  : randomIdentity();
@@ -3865,6 +3893,14 @@ const createBrowserHandoff = (chatbotId, bootstrap, onProjection) => {
3865
3893
  if (active) onProjection({ offline: true, identity, messages: [] });
3866
3894
  }
3867
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
+ };
3868
3904
  return {
3869
3905
  identity: { sessionId: identity.sessionId },
3870
3906
  recordVisitorMessage: content => {
@@ -3886,14 +3922,11 @@ const createBrowserHandoff = (chatbotId, bootstrap, onProjection) => {
3886
3922
  acknowledging.delete(messageId);
3887
3923
  }
3888
3924
  },
3889
- stop: () => {
3890
- if (!active) return
3891
- active = false;
3892
- window.clearInterval(timer);
3893
- document.removeEventListener('visibilitychange', onVisibility);
3894
- window.removeEventListener('pagehide', release);
3895
- void release();
3896
- }
3925
+ restart: () => {
3926
+ stop();
3927
+ return createBrowserHandoff(chatbotId, bootstrap, onProjection, true)
3928
+ },
3929
+ stop
3897
3930
  }
3898
3931
  };
3899
3932
 
@@ -3907,6 +3940,8 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
3907
3940
  const [isOpen, setIsOpen] = react.useState(initialOpen);
3908
3941
  const [messages, setMessages] = react.useState([]);
3909
3942
  const [isLoading, setIsLoading] = react.useState(false);
3943
+ const [replyStarted, setReplyStarted] = react.useState(false);
3944
+ const activeReplyRef = react.useRef(null);
3910
3945
  const [sessionId, setSessionId] = react.useState(() => generateSessionId());
3911
3946
  const handoffRef = react.useRef(null);
3912
3947
  const pendingHandoffIdsRef = react.useRef([]);
@@ -3930,6 +3965,11 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
3930
3965
  const handoff = createBrowserHandoff(config.id, config.bootstrap, projection => {
3931
3966
  if (projection.offline)
3932
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;
3933
3973
  setSessionId(projection.identity.sessionId);
3934
3974
  pendingHandoffIdsRef.current = projection.pendingProjectionIds || [];
3935
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 }));
@@ -3941,8 +3981,11 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
3941
3981
  : projectedMessages);
3942
3982
  });
3943
3983
  handoffRef.current = handoff;
3984
+ if (handoff)
3985
+ setSessionId(handoff.identity.sessionId);
3944
3986
  return () => {
3945
- handoff?.stop();
3987
+ activeReplyRef.current?.abort();
3988
+ handoffRef.current?.stop();
3946
3989
  handoffRef.current = null;
3947
3990
  };
3948
3991
  }, [config?.bootstrap, config?.id]);
@@ -4264,11 +4307,13 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4264
4307
  }, [isCollectingUserInfo, messages]);
4265
4308
  const sendUserMessage = react.useCallback(async (rawMessageText, options = {}) => {
4266
4309
  const messageText = typeof rawMessageText === 'string' ? rawMessageText.trim() : '';
4267
- if (!messageText) {
4310
+ if (!messageText || activeReplyRef.current) {
4268
4311
  return;
4269
4312
  }
4270
4313
  const { hidePredefined = true } = options;
4271
4314
  const timestamp = Date.now();
4315
+ const controller = new AbortController();
4316
+ activeReplyRef.current = controller;
4272
4317
  const userMessage = {
4273
4318
  id: timestamp,
4274
4319
  content: messageText,
@@ -4282,18 +4327,30 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4282
4327
  }
4283
4328
  try {
4284
4329
  setIsLoading(true);
4285
- 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;
4286
4346
  if (response.status === 'waiting') {
4287
- 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
+ });
4288
4351
  return;
4289
4352
  }
4290
- const botMessage = {
4291
- id: timestamp + 1,
4292
- content: response.text,
4293
- sender: 'bot',
4294
- timestamp: new Date().toISOString()
4295
- };
4296
- setMessages(prev => [...prev, botMessage]);
4353
+ showReply(response.text, false);
4297
4354
  if (config?.collectUserInfo?.enabled && !userInfoCollected && !isCollectingUserInfo) {
4298
4355
  setPendingMessage(messageText);
4299
4356
  setIsCollectingUserInfo(true);
@@ -4308,6 +4365,8 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4308
4365
  }
4309
4366
  }
4310
4367
  catch {
4368
+ if (controller.signal.aborted)
4369
+ return;
4311
4370
  console.error('AndyChat: Message request failed.');
4312
4371
  const errorMessage = {
4313
4372
  id: timestamp + 3,
@@ -4316,10 +4375,14 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4316
4375
  timestamp: new Date().toISOString(),
4317
4376
  isError: true
4318
4377
  };
4319
- setMessages(prev => [...prev, errorMessage]);
4378
+ setMessages(prev => [...prev.filter(message => message.id !== timestamp + 1), errorMessage]);
4320
4379
  }
4321
4380
  finally {
4322
- setIsLoading(false);
4381
+ if (activeReplyRef.current === controller) {
4382
+ activeReplyRef.current = null;
4383
+ setIsLoading(false);
4384
+ setReplyStarted(false);
4385
+ }
4323
4386
  }
4324
4387
  }, [config, embedId, sessionId, userInfoCollected, isCollectingUserInfo]);
4325
4388
  const handleSendMessage = react.useCallback((messageText) => {
@@ -4481,21 +4544,27 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4481
4544
  if (message.isLoading) {
4482
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));
4483
4546
  }
4484
- 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));
4485
- }), 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 &&
4486
4549
  activeQuestions.length > 0 &&
4487
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
+ }
4488
4555
  if (e.key === 'Enter' && e.target.value.trim()) {
4489
4556
  handleSendMessage(e.target.value);
4490
4557
  e.target.value = '';
4491
4558
  }
4492
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;
4493
4562
  const input = shadowRef.current?.getElementById('andy-message-input');
4494
4563
  if (input && input.value.trim()) {
4495
4564
  handleSendMessage(input.value);
4496
4565
  input.value = '';
4497
4566
  }
4498
- }, 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 &&
4499
4568
  !isOpen &&
4500
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 ||
4501
4570
  config?.welcomeMessage ||
@@ -4516,6 +4585,11 @@ const useAndyChat = (embedId) => {
4516
4585
  const [showingWelcomePopup, setShowingWelcomePopup] = react.useState(false);
4517
4586
  const [showingUserInfoForm, setShowingUserInfoForm] = react.useState(false);
4518
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]);
4519
4593
  const welcomeTimeoutRef = react.useRef(null);
4520
4594
  const userInfoTimeoutRef = react.useRef(null);
4521
4595
  const initializeChatbot = react.useCallback(async () => {
@@ -4587,9 +4661,11 @@ const useAndyChat = (embedId) => {
4587
4661
  });
4588
4662
  }, [config, welcomePopupShown, userInfoCollected]);
4589
4663
  const sendMessage = react.useCallback(async (messageText) => {
4590
- if (!messageText.trim() || !config || !sessionId) {
4664
+ if (!messageText.trim() || !config || !sessionId || activeReplyRef.current) {
4591
4665
  return;
4592
4666
  }
4667
+ const controller = new AbortController();
4668
+ activeReplyRef.current = controller;
4593
4669
  const userMessage = {
4594
4670
  id: Date.now(),
4595
4671
  content: messageText,
@@ -4597,20 +4673,29 @@ const useAndyChat = (embedId) => {
4597
4673
  timestamp: new Date().toISOString()
4598
4674
  };
4599
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
+ };
4600
4685
  try {
4601
- const response = await sendMessageForWidget(embedId, sessionId, messageText);
4602
- 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));
4603
4691
  return response;
4604
- const botMessage = {
4605
- id: Date.now() + 1,
4606
- content: response.text || 'I apologize, but I encountered an error processing your request.',
4607
- sender: 'bot',
4608
- timestamp: new Date().toISOString()
4609
- };
4610
- setMessages(prev => [...prev, botMessage]);
4692
+ }
4693
+ showReply(response.text, false);
4611
4694
  return response;
4612
4695
  }
4613
4696
  catch (err) {
4697
+ if (controller.signal.aborted)
4698
+ return;
4614
4699
  console.error('Error sending message:', err);
4615
4700
  const errorMessage = {
4616
4701
  id: Date.now() + 1,
@@ -4619,9 +4704,13 @@ const useAndyChat = (embedId) => {
4619
4704
  timestamp: new Date().toISOString(),
4620
4705
  isError: true
4621
4706
  };
4622
- setMessages(prev => [...prev, errorMessage]);
4707
+ setMessages(prev => [...prev.filter(message => message.id !== replyId), errorMessage]);
4623
4708
  throw err;
4624
4709
  }
4710
+ finally {
4711
+ if (activeReplyRef.current === controller)
4712
+ activeReplyRef.current = null;
4713
+ }
4625
4714
  }, [embedId, sessionId, config]);
4626
4715
  const showFaq = react.useCallback(() => {
4627
4716
  setShowingFaq(true);
@@ -4660,6 +4749,8 @@ const useAndyChat = (embedId) => {
4660
4749
  }
4661
4750
  }, [embedId, sessionId]);
4662
4751
  const startNewSession = react.useCallback(() => {
4752
+ activeReplyRef.current?.abort();
4753
+ activeReplyRef.current = null;
4663
4754
  const newSessionId = generateSessionId();
4664
4755
  setSessionId(newSessionId);
4665
4756
  {
@@ -4729,9 +4820,11 @@ const MessageComponent = ({ message, botName }) => {
4729
4820
  .filter(Boolean)
4730
4821
  .join(' ');
4731
4822
  const messageClasses = ['andy-message', `andy-message--${message.sender}`].filter(Boolean).join(' ');
4732
- const messageContent = isBot ? parseMarkdown(message.content) : message.content;
4823
+ const messageContent = isBot && !message.isStreaming ? parseMarkdown(message.content) : message.content;
4733
4824
  if (isBot) {
4734
- 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 } }) })] }));
4735
4828
  }
4736
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 }) }) }) }));
4737
4830
  };
@@ -4746,7 +4839,7 @@ const MessageList = ({ messages, isLoading, logoUrl, botName }) => {
4746
4839
  });
4747
4840
  }
4748
4841
  }, [messages, isLoading]);
4749
- 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 })] }));
4750
4843
  };
4751
4844
 
4752
4845
  const MessageInput = ({ onSendMessage, disabled = false, placeholder = 'Type your message here...' }) => {
@@ -4761,14 +4854,15 @@ const MessageInput = ({ onSendMessage, disabled = false, placeholder = 'Type you
4761
4854
  }
4762
4855
  try {
4763
4856
  setIsSubmitting(true);
4764
- await onSendMessage(trimmedMessage);
4765
4857
  setMessage('');
4858
+ await onSendMessage(trimmedMessage);
4766
4859
  if (inputRef.current) {
4767
4860
  inputRef.current.focus();
4768
4861
  }
4769
4862
  }
4770
4863
  catch (error) {
4771
4864
  console.error('Error sending message:', error);
4865
+ setMessage(current => current || trimmedMessage);
4772
4866
  }
4773
4867
  finally {
4774
4868
  setIsSubmitting(false);
@@ -4785,7 +4879,7 @@ const MessageInput = ({ onSendMessage, disabled = false, placeholder = 'Type you
4785
4879
  };
4786
4880
  const isInputDisabled = disabled || isSubmitting;
4787
4881
  const canSend = message.trim().length > 0 && !isInputDisabled;
4788
- 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' }) })) })] }));
4789
4883
  };
4790
4884
 
4791
4885
  const ChatWindow = ({ config, messages, isLoading, onClose, onSendMessage }) => {