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.esm.js CHANGED
@@ -758,6 +758,84 @@ const initializeConfig = (packageName = 'unknown') => {
758
758
  }
759
759
  };
760
760
 
761
+ /** Stream a reply while the server restores this session's conversation history. */
762
+ const sendMessageForWidget = async (embedId, sessionId, message, options = {}) => {
763
+ const response = await fetch(`${getApiBaseUrl()}/chat/stream`, {
764
+ method: 'POST',
765
+ headers: { 'Content-Type': 'application/json' },
766
+ signal: options.signal,
767
+ body: JSON.stringify({
768
+ chatbotId: embedId,
769
+ sessionId,
770
+ historySource: 'session',
771
+ messages: [{ role: 'user', parts: [{ type: 'text', text: message }] }]
772
+ })
773
+ });
774
+ if (!response.ok || !response.headers.get('content-type')?.includes('text/event-stream')) {
775
+ const data = await response.json().catch(() => null);
776
+ throw new Error(data?.error || data?.message || 'Chat is temporarily unavailable.')
777
+ }
778
+ if (!response.body) throw new Error('Missing reply stream.')
779
+
780
+ const reader = response.body.getReader();
781
+ const decoder = new TextDecoder();
782
+ let buffer = '';
783
+ let eventData = [];
784
+ let text = '';
785
+ let published = '';
786
+ let complete = false;
787
+ let waiting = false;
788
+ const event = () => {
789
+ if (!eventData.length) return
790
+ const data = eventData.join('\n');
791
+ eventData = [];
792
+ if (data === '[DONE]') { complete = true; return }
793
+ const part = JSON.parse(data);
794
+ if (part.type === 'error' || part.type === 'abort') {
795
+ throw new Error(part.errorText || 'Reply interrupted.')
796
+ }
797
+ if (part.type === 'data-handoff' && part.data?.status === 'waiting') {
798
+ waiting = true;
799
+ return
800
+ }
801
+ if (part.type === 'text-start' && text && !text.endsWith('\n')) text += '\n';
802
+ if (part.type === 'text-delta') {
803
+ if (typeof part.delta !== 'string') throw new Error('Invalid reply text.')
804
+ text += part.delta;
805
+ }
806
+ };
807
+ const line = value => {
808
+ const cleaned = value.endsWith('\r') ? value.slice(0, -1) : value;
809
+ if (!cleaned) event();
810
+ else if (cleaned.startsWith('data:')) eventData.push(cleaned.slice(5).replace(/^ /, ''));
811
+ };
812
+ try {
813
+ while (!complete) {
814
+ const { value, done } = await reader.read();
815
+ buffer += decoder.decode(value, { stream: !done });
816
+ let newline;
817
+ while ((newline = buffer.indexOf('\n')) !== -1) {
818
+ line(buffer.slice(0, newline));
819
+ buffer = buffer.slice(newline + 1);
820
+ if (complete) break
821
+ }
822
+ // Publish once per received chunk, including the first useful text.
823
+ if (!waiting && text.trim() && text !== published) {
824
+ published = text;
825
+ options.onText?.(text);
826
+ }
827
+ if (done) break
828
+ }
829
+ if (!complete) throw new Error('Reply stream ended before completion.')
830
+ if (waiting) return { status: 'waiting' }
831
+ if (!text.trim()) throw new Error('The reply was empty.')
832
+ return { status: 'agent', text }
833
+ } finally {
834
+ await reader.cancel().catch(() => {});
835
+ reader.releaseLock();
836
+ }
837
+ };
838
+
761
839
  /**
762
840
  * AndyChat API client
763
841
  * Handles all communication with the AndyChat backend API
@@ -982,60 +1060,6 @@ const fetchChatbotConfig = async embedId => {
982
1060
  }
983
1061
  };
984
1062
 
985
- /**
986
- * Sends a user message to the API
987
- * @param {string} embedId - The chatbot embed ID
988
- * @param {string} sessionId - The current session ID
989
- * @param {string} message - The user's message
990
- * @returns {Promise<string>} The bot's response text
991
- */
992
- const sendMessageForWidget = async (embedId, sessionId, message) => {
993
- try {
994
- const apiBaseUrl = getApiBaseUrl();
995
- const response = await fetch(`${apiBaseUrl}/messages`, {
996
- method: 'POST',
997
- headers: { 'Content-Type': 'application/json' },
998
- body: JSON.stringify({
999
- chatbotId: embedId,
1000
- sessionId: sessionId,
1001
- currentUserMessage: {
1002
- role: 'user',
1003
- content: message
1004
- }
1005
- })
1006
- });
1007
-
1008
- if (response.ok) {
1009
- const data = await response.json();
1010
- if (data.error || data.limitStatus) {
1011
- throw new Error(data.error || 'Chat is temporarily unavailable.')
1012
- }
1013
- if (data.handoffStatus === 'paused') {
1014
- return { status: 'waiting' }
1015
- }
1016
- if (typeof data.botResponse !== 'string' || !data.botResponse.trim()) {
1017
- throw new Error('Unexpected response format from server.')
1018
- }
1019
- return { status: 'agent', text: data.botResponse }
1020
- } else {
1021
- // Handle HTTP errors (e.g., 500, 404)
1022
- let errorData;
1023
- try {
1024
- errorData = await response.json();
1025
- } catch (e) {
1026
- // If parsing error JSON fails, use status text
1027
- // Also, if response.text() is available and gives more info, consider using it.
1028
- const responseText = response.text ? await response.text() : '';
1029
- throw new Error(response.statusText || `Server returned an error. ${responseText}`.trim())
1030
- }
1031
- throw new Error(errorData.error || errorData.message || response.statusText || 'Server returned an error.')
1032
- }
1033
- } catch (error) {
1034
- console.error('AndyChat: Message request failed.');
1035
- throw error
1036
- }
1037
- };
1038
-
1039
1063
  /**
1040
1064
  * Visitor information collection is disabled until Andy exposes a durable public submission
1041
1065
  * contract. This explicit failure prevents a Widget from reporting a mocked success.
@@ -2129,6 +2153,10 @@ const getCoreStructuralCssString = () => {
2129
2153
  }
2130
2154
 
2131
2155
  /* Message Sub-Components */
2156
+ .andy-message__stream {
2157
+ white-space: pre-wrap;
2158
+ }
2159
+
2132
2160
  .andy-message__content {
2133
2161
  line-height: 1.4;
2134
2162
  margin: 0;
@@ -3767,12 +3795,12 @@ const callConvex = async (convexUrl, kind, path, wireArgs, keepalive = false) =>
3767
3795
  const sortMessages = messages => [...messages].sort((left, right) =>
3768
3796
  left.createdAt - right.createdAt || String(left.id).localeCompare(String(right.id)));
3769
3797
 
3770
- const createBrowserHandoff = (chatbotId, bootstrap, onProjection) => {
3798
+ const createBrowserHandoff = (chatbotId, bootstrap, onProjection, fresh = false) => {
3771
3799
  if (!bootstrap?.convexUrl || !bootstrap.surfaceKey || !bootstrap.surfaceProof) return null
3772
3800
  const storageKey = identityKey(chatbotId);
3773
3801
  let identity;
3774
3802
  try {
3775
- const stored = JSON.parse(window.sessionStorage.getItem(storageKey) || 'null');
3803
+ const stored = fresh ? null : JSON.parse(window.sessionStorage.getItem(storageKey) || 'null');
3776
3804
  identity = stored?.sessionId && stored?.capability && stored.surfaceKey === bootstrap.surfaceKey
3777
3805
  ? { sessionId: stored.sessionId, capability: stored.capability }
3778
3806
  : randomIdentity();
@@ -3861,6 +3889,14 @@ const createBrowserHandoff = (chatbotId, bootstrap, onProjection) => {
3861
3889
  if (active) onProjection({ offline: true, identity, messages: [] });
3862
3890
  }
3863
3891
  }, 15000);
3892
+ const stop = () => {
3893
+ if (!active) return
3894
+ active = false;
3895
+ window.clearInterval(timer);
3896
+ document.removeEventListener('visibilitychange', onVisibility);
3897
+ window.removeEventListener('pagehide', release);
3898
+ void release();
3899
+ };
3864
3900
  return {
3865
3901
  identity: { sessionId: identity.sessionId },
3866
3902
  recordVisitorMessage: content => {
@@ -3882,14 +3918,11 @@ const createBrowserHandoff = (chatbotId, bootstrap, onProjection) => {
3882
3918
  acknowledging.delete(messageId);
3883
3919
  }
3884
3920
  },
3885
- stop: () => {
3886
- if (!active) return
3887
- active = false;
3888
- window.clearInterval(timer);
3889
- document.removeEventListener('visibilitychange', onVisibility);
3890
- window.removeEventListener('pagehide', release);
3891
- void release();
3892
- }
3921
+ restart: () => {
3922
+ stop();
3923
+ return createBrowserHandoff(chatbotId, bootstrap, onProjection, true)
3924
+ },
3925
+ stop
3893
3926
  }
3894
3927
  };
3895
3928
 
@@ -3903,6 +3936,8 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
3903
3936
  const [isOpen, setIsOpen] = useState(initialOpen);
3904
3937
  const [messages, setMessages] = useState([]);
3905
3938
  const [isLoading, setIsLoading] = useState(false);
3939
+ const [replyStarted, setReplyStarted] = useState(false);
3940
+ const activeReplyRef = useRef(null);
3906
3941
  const [sessionId, setSessionId] = useState(() => generateSessionId());
3907
3942
  const handoffRef = useRef(null);
3908
3943
  const pendingHandoffIdsRef = useRef([]);
@@ -3926,6 +3961,11 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
3926
3961
  const handoff = createBrowserHandoff(config.id, config.bootstrap, projection => {
3927
3962
  if (projection.offline)
3928
3963
  return;
3964
+ const paused = projection.control === 'requested' || projection.control === 'human';
3965
+ if (paused)
3966
+ activeReplyRef.current?.abort();
3967
+ else if (activeReplyRef.current)
3968
+ return;
3929
3969
  setSessionId(projection.identity.sessionId);
3930
3970
  pendingHandoffIdsRef.current = projection.pendingProjectionIds || [];
3931
3971
  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 }));
@@ -3937,8 +3977,11 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
3937
3977
  : projectedMessages);
3938
3978
  });
3939
3979
  handoffRef.current = handoff;
3980
+ if (handoff)
3981
+ setSessionId(handoff.identity.sessionId);
3940
3982
  return () => {
3941
- handoff?.stop();
3983
+ activeReplyRef.current?.abort();
3984
+ handoffRef.current?.stop();
3942
3985
  handoffRef.current = null;
3943
3986
  };
3944
3987
  }, [config?.bootstrap, config?.id]);
@@ -4260,11 +4303,13 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4260
4303
  }, [isCollectingUserInfo, messages]);
4261
4304
  const sendUserMessage = useCallback(async (rawMessageText, options = {}) => {
4262
4305
  const messageText = typeof rawMessageText === 'string' ? rawMessageText.trim() : '';
4263
- if (!messageText) {
4306
+ if (!messageText || activeReplyRef.current) {
4264
4307
  return;
4265
4308
  }
4266
4309
  const { hidePredefined = true } = options;
4267
4310
  const timestamp = Date.now();
4311
+ const controller = new AbortController();
4312
+ activeReplyRef.current = controller;
4268
4313
  const userMessage = {
4269
4314
  id: timestamp,
4270
4315
  content: messageText,
@@ -4278,18 +4323,30 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4278
4323
  }
4279
4324
  try {
4280
4325
  setIsLoading(true);
4281
- const response = await sendMessageForWidget(embedId, sessionId, messageText);
4326
+ setReplyStarted(false);
4327
+ const showReply = (content, isStreaming) => {
4328
+ if (controller.signal.aborted)
4329
+ return;
4330
+ setReplyStarted(true);
4331
+ const reply = { id: timestamp + 1, content, sender: 'bot', timestamp: new Date(timestamp).toISOString(), isStreaming };
4332
+ setMessages(prev => prev.some(message => message.id === reply.id)
4333
+ ? prev.map(message => message.id === reply.id ? reply : message)
4334
+ : [...prev, reply]);
4335
+ };
4336
+ const response = await sendMessageForWidget(embedId, sessionId, messageText, {
4337
+ signal: controller.signal,
4338
+ onText: text => showReply(text, true)
4339
+ });
4340
+ if (controller.signal.aborted)
4341
+ return;
4282
4342
  if (response.status === 'waiting') {
4283
- setMessages(prev => prev.some(message => message.isWaiting) ? prev : [...prev, { id: 'handoff-waiting', content: strings.handoffWaiting, sender: 'bot', timestamp: new Date().toISOString(), isWaiting: true }]);
4343
+ setMessages(prev => {
4344
+ const visible = prev.filter(message => message.id !== timestamp + 1);
4345
+ return visible.some(message => message.isWaiting) ? visible : [...visible, { id: 'handoff-waiting', content: strings.handoffWaiting, sender: 'bot', timestamp: new Date().toISOString(), isWaiting: true }];
4346
+ });
4284
4347
  return;
4285
4348
  }
4286
- const botMessage = {
4287
- id: timestamp + 1,
4288
- content: response.text,
4289
- sender: 'bot',
4290
- timestamp: new Date().toISOString()
4291
- };
4292
- setMessages(prev => [...prev, botMessage]);
4349
+ showReply(response.text, false);
4293
4350
  if (config?.collectUserInfo?.enabled && !userInfoCollected && !isCollectingUserInfo) {
4294
4351
  setPendingMessage(messageText);
4295
4352
  setIsCollectingUserInfo(true);
@@ -4304,6 +4361,8 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4304
4361
  }
4305
4362
  }
4306
4363
  catch {
4364
+ if (controller.signal.aborted)
4365
+ return;
4307
4366
  console.error('AndyChat: Message request failed.');
4308
4367
  const errorMessage = {
4309
4368
  id: timestamp + 3,
@@ -4312,10 +4371,14 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4312
4371
  timestamp: new Date().toISOString(),
4313
4372
  isError: true
4314
4373
  };
4315
- setMessages(prev => [...prev, errorMessage]);
4374
+ setMessages(prev => [...prev.filter(message => message.id !== timestamp + 1), errorMessage]);
4316
4375
  }
4317
4376
  finally {
4318
- setIsLoading(false);
4377
+ if (activeReplyRef.current === controller) {
4378
+ activeReplyRef.current = null;
4379
+ setIsLoading(false);
4380
+ setReplyStarted(false);
4381
+ }
4319
4382
  }
4320
4383
  }, [config, embedId, sessionId, userInfoCollected, isCollectingUserInfo]);
4321
4384
  const handleSendMessage = useCallback((messageText) => {
@@ -4477,21 +4540,27 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4477
4540
  if (message.isLoading) {
4478
4541
  return (jsx("div", { className: 'andy-message-container andy-message-container--bot andy-message-container--loading', children: jsx("div", { className: 'andy-message andy-message--bot', children: jsxs("div", { className: 'andy-typing-indicator', children: [jsx("div", { className: 'andy-typing-indicator__dot' }), jsx("div", { className: 'andy-typing-indicator__dot' }), jsx("div", { className: 'andy-typing-indicator__dot' })] }) }) }, message.id));
4479
4542
  }
4480
- return (jsxs("div", { className: `andy-message-container andy-message-container--${message.sender} ${message.isWaiting ? 'andy-message-container--waiting' : ''}`, children: [message.sender === 'bot' && (jsxs("div", { className: `andy-message andy-message--bot ${message.isHuman ? 'andy-message--operator' : ''} ${message.isWaiting ? 'andy-message--waiting' : ''} ${message.isError ? 'andy-message--error' : ''}`, children: [jsxs("div", { className: 'andy-bot-header', children: [jsx("div", { className: 'andy-bot-header__icon', children: config?.logoUrl ? (jsx("img", { src: config.logoUrl, alt: 'Bot Avatar', className: 'andy-bot-header__icon-image' })) : (jsx("img", { src: CHAT_BUBBLE_DEFAULT_ICON_URL, alt: 'Bot Avatar', className: 'andy-bot-header__icon-image andy-bot-header__icon-image--default' })) }), jsx("span", { className: 'andy-bot-header__name', children: message.isHuman ? strings.teamName : config?.name || 'Andy bot' })] }), jsx("div", { className: 'andy-message__content', children: jsx("div", { dangerouslySetInnerHTML: { __html: parseMarkdown(message.content) } }) })] })), message.sender === 'user' && (jsx("div", { className: 'andy-message andy-message--user', children: jsx("div", { className: 'andy-message__content', children: jsx("p", { children: message.content }) }) }))] }, message.id));
4481
- }), isLoading && (jsx("div", { className: 'andy-message-container andy-message-container--bot andy-message-container--typing', children: jsx("div", { className: 'andy-message andy-message--bot andy-message--typing', children: jsxs("div", { className: 'andy-typing-indicator', children: [jsx("div", { className: 'andy-typing-indicator__dot' }), jsx("div", { className: 'andy-typing-indicator__dot' }), jsx("div", { className: 'andy-typing-indicator__dot' })] }) }) }))] }), jsxs("div", { className: 'andy-chat-window__bottom', children: [activeQuestions &&
4543
+ return (jsxs("div", { className: `andy-message-container andy-message-container--${message.sender} ${message.isWaiting ? 'andy-message-container--waiting' : ''}`, children: [message.sender === 'bot' && (jsxs("div", { className: `andy-message andy-message--bot ${message.isHuman ? 'andy-message--operator' : ''} ${message.isWaiting ? 'andy-message--waiting' : ''} ${message.isError ? 'andy-message--error' : ''}`, children: [jsxs("div", { className: 'andy-bot-header', children: [jsx("div", { className: 'andy-bot-header__icon', children: config?.logoUrl ? (jsx("img", { src: config.logoUrl, alt: 'Bot Avatar', className: 'andy-bot-header__icon-image' })) : (jsx("img", { src: CHAT_BUBBLE_DEFAULT_ICON_URL, alt: 'Bot Avatar', className: 'andy-bot-header__icon-image andy-bot-header__icon-image--default' })) }), jsx("span", { className: 'andy-bot-header__name', children: message.isHuman ? strings.teamName : config?.name || 'Andy bot' })] }), jsx("div", { className: 'andy-message__content', children: message.isStreaming ? jsx("div", { className: 'andy-message__stream', children: message.content }) : jsx("div", { dangerouslySetInnerHTML: { __html: parseMarkdown(message.content) } }) })] })), message.sender === 'user' && (jsx("div", { className: 'andy-message andy-message--user', children: jsx("div", { className: 'andy-message__content', children: jsx("p", { children: message.content }) }) }))] }, message.id));
4544
+ }), isLoading && !replyStarted && (jsx("div", { className: 'andy-message-container andy-message-container--bot andy-message-container--typing', children: jsx("div", { className: 'andy-message andy-message--bot andy-message--typing', children: jsxs("div", { className: 'andy-typing-indicator', children: [jsx("div", { className: 'andy-typing-indicator__dot' }), jsx("div", { className: 'andy-typing-indicator__dot' }), jsx("div", { className: 'andy-typing-indicator__dot' })] }) }) }))] }), jsxs("div", { className: 'andy-chat-window__bottom', children: [activeQuestions &&
4482
4545
  activeQuestions.length > 0 &&
4483
4546
  showPredefinedQuestions && (jsxs("div", { className: 'andy-predefined-questions', id: 'andy-predefined-questions', children: [jsx("div", { className: 'andy-predefined-questions__header', children: jsx("p", { className: 'andy-predefined-questions__title', children: strings.predefinedQuestionsTitle }) }), jsx("div", { className: 'andy-predefined-questions__list', children: activeQuestions.map((q, index) => (jsxs("button", { type: 'button', className: 'andy-predefined-question', "data-question": q.question, disabled: isCollectingUserInfo || isLoading || !config, children: [jsx("span", { className: 'andy-predefined-question__icon', children: "\uD83D\uDCAC" }), jsx("span", { className: 'andy-predefined-question__text', children: q.question })] }, index))) })] })), jsx("div", { className: 'andy-chat-window__input', children: jsx("div", { className: 'andy-input-container', children: jsxs("div", { className: 'andy-input', children: [jsx("input", { type: 'text', className: 'andy-input__field', id: 'andy-message-input', placeholder: config ? strings.inputPlaceholder : '...', "aria-label": strings.messageInput, onKeyDown: e => {
4547
+ if (e.key === 'Enter' && activeReplyRef.current) {
4548
+ e.preventDefault();
4549
+ return;
4550
+ }
4484
4551
  if (e.key === 'Enter' && e.target.value.trim()) {
4485
4552
  handleSendMessage(e.target.value);
4486
4553
  e.target.value = '';
4487
4554
  }
4488
4555
  }, disabled: isCollectingUserInfo || !config }), jsx("button", { type: 'button', className: 'andy-input__button', id: 'andy-send-button', "aria-label": strings.sendMessage, onClick: () => {
4556
+ if (activeReplyRef.current)
4557
+ return;
4489
4558
  const input = shadowRef.current?.getElementById('andy-message-input');
4490
4559
  if (input && input.value.trim()) {
4491
4560
  handleSendMessage(input.value);
4492
4561
  input.value = '';
4493
4562
  }
4494
- }, disabled: isCollectingUserInfo || !config, children: 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: jsx("path", { d: 'M2.01 21L23 12 2.01 3 2 10l15 2-15 2z' }) }) })] }) }) }), config?.showBranding !== false && (jsx("footer", { className: 'andy-chat-window__footer', children: jsx("a", { href: getFooterUrl(), target: '_blank', rel: 'noopener noreferrer', className: 'andy-chat-window__footer-link', children: jsx("span", { className: 'andy-chat-window__footer-text', children: FOOTER_TEXT }) }) }))] })] }), portalRef.current), showWelcomePopup &&
4563
+ }, disabled: isCollectingUserInfo || isLoading || !config, children: 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: jsx("path", { d: 'M2.01 21L23 12 2.01 3 2 10l15 2-15 2z' }) }) })] }) }) }), config?.showBranding !== false && (jsx("footer", { className: 'andy-chat-window__footer', children: jsx("a", { href: getFooterUrl(), target: '_blank', rel: 'noopener noreferrer', className: 'andy-chat-window__footer-link', children: jsx("span", { className: 'andy-chat-window__footer-text', children: FOOTER_TEXT }) }) }))] })] }), portalRef.current), showWelcomePopup &&
4495
4564
  !isOpen &&
4496
4565
  ReactDOM.createPortal(jsxs("div", { className: `andy-welcome-popup ${config?.orientation === 'left' ? 'andychat-popup-left' : ''}`, style: { display: 'block' }, role: 'dialog', "aria-label": strings.welcomeMessage, children: [jsxs("div", { className: 'andy-welcome-popup__content', children: [jsx("div", { className: 'andy-welcome-popup__message', children: config?.showWelcomePopup?.message ||
4497
4566
  config?.welcomeMessage ||
@@ -4512,6 +4581,11 @@ const useAndyChat = (embedId) => {
4512
4581
  const [showingWelcomePopup, setShowingWelcomePopup] = useState(false);
4513
4582
  const [showingUserInfoForm, setShowingUserInfoForm] = useState(false);
4514
4583
  const [welcomePopupShown, setWelcomePopupShown] = useState(false);
4584
+ const activeReplyRef = useRef(null);
4585
+ useEffect(() => () => {
4586
+ activeReplyRef.current?.abort();
4587
+ activeReplyRef.current = null;
4588
+ }, [embedId]);
4515
4589
  const welcomeTimeoutRef = useRef(null);
4516
4590
  const userInfoTimeoutRef = useRef(null);
4517
4591
  const initializeChatbot = useCallback(async () => {
@@ -4583,9 +4657,11 @@ const useAndyChat = (embedId) => {
4583
4657
  });
4584
4658
  }, [config, welcomePopupShown, userInfoCollected]);
4585
4659
  const sendMessage = useCallback(async (messageText) => {
4586
- if (!messageText.trim() || !config || !sessionId) {
4660
+ if (!messageText.trim() || !config || !sessionId || activeReplyRef.current) {
4587
4661
  return;
4588
4662
  }
4663
+ const controller = new AbortController();
4664
+ activeReplyRef.current = controller;
4589
4665
  const userMessage = {
4590
4666
  id: Date.now(),
4591
4667
  content: messageText,
@@ -4593,20 +4669,29 @@ const useAndyChat = (embedId) => {
4593
4669
  timestamp: new Date().toISOString()
4594
4670
  };
4595
4671
  setMessages(prev => [...prev, userMessage]);
4672
+ const replyId = Date.now() + 1;
4673
+ const showReply = (content, isStreaming) => {
4674
+ if (controller.signal.aborted)
4675
+ return;
4676
+ const reply = { id: replyId, content, sender: 'bot', timestamp: new Date().toISOString(), isStreaming };
4677
+ setMessages(prev => prev.some(message => message.id === replyId)
4678
+ ? prev.map(message => message.id === replyId ? reply : message)
4679
+ : [...prev, reply]);
4680
+ };
4596
4681
  try {
4597
- const response = await sendMessageForWidget(embedId, sessionId, messageText);
4598
- if (response.status === 'waiting')
4682
+ const response = await sendMessageForWidget(embedId, sessionId, messageText, { signal: controller.signal, onText: text => showReply(text, true) });
4683
+ if (controller.signal.aborted)
4684
+ return;
4685
+ if (response.status === 'waiting') {
4686
+ setMessages(prev => prev.filter(message => message.id !== replyId));
4599
4687
  return response;
4600
- const botMessage = {
4601
- id: Date.now() + 1,
4602
- content: response.text || 'I apologize, but I encountered an error processing your request.',
4603
- sender: 'bot',
4604
- timestamp: new Date().toISOString()
4605
- };
4606
- setMessages(prev => [...prev, botMessage]);
4688
+ }
4689
+ showReply(response.text, false);
4607
4690
  return response;
4608
4691
  }
4609
4692
  catch (err) {
4693
+ if (controller.signal.aborted)
4694
+ return;
4610
4695
  console.error('Error sending message:', err);
4611
4696
  const errorMessage = {
4612
4697
  id: Date.now() + 1,
@@ -4615,9 +4700,13 @@ const useAndyChat = (embedId) => {
4615
4700
  timestamp: new Date().toISOString(),
4616
4701
  isError: true
4617
4702
  };
4618
- setMessages(prev => [...prev, errorMessage]);
4703
+ setMessages(prev => [...prev.filter(message => message.id !== replyId), errorMessage]);
4619
4704
  throw err;
4620
4705
  }
4706
+ finally {
4707
+ if (activeReplyRef.current === controller)
4708
+ activeReplyRef.current = null;
4709
+ }
4621
4710
  }, [embedId, sessionId, config]);
4622
4711
  const showFaq = useCallback(() => {
4623
4712
  setShowingFaq(true);
@@ -4656,6 +4745,8 @@ const useAndyChat = (embedId) => {
4656
4745
  }
4657
4746
  }, [embedId, sessionId]);
4658
4747
  const startNewSession = useCallback(() => {
4748
+ activeReplyRef.current?.abort();
4749
+ activeReplyRef.current = null;
4659
4750
  const newSessionId = generateSessionId();
4660
4751
  setSessionId(newSessionId);
4661
4752
  {
@@ -4725,9 +4816,11 @@ const MessageComponent = ({ message, botName }) => {
4725
4816
  .filter(Boolean)
4726
4817
  .join(' ');
4727
4818
  const messageClasses = ['andy-message', `andy-message--${message.sender}`].filter(Boolean).join(' ');
4728
- const messageContent = isBot ? parseMarkdown(message.content) : message.content;
4819
+ const messageContent = isBot && !message.isStreaming ? parseMarkdown(message.content) : message.content;
4729
4820
  if (isBot) {
4730
- return (jsxs("div", { className: containerClasses, children: [jsxs("div", { className: 'andy-bot-header', children: [jsx("div", { className: 'andy-bot-header__icon', children: 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: [jsx("path", { d: 'M12 6V2H8' }), 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' }), jsx("path", { d: 'M2 12h2' }), jsx("path", { d: 'M9 11v2' }), jsx("path", { d: 'M15 11v2' }), jsx("path", { d: 'M20 12h2' })] }) }), jsx("span", { className: 'andy-bot-header__name', children: botName || 'Andy bot' })] }), jsx("div", { className: messageClasses, children: jsx("div", { className: 'andy-message__content', dangerouslySetInnerHTML: { __html: messageContent } }) })] }));
4821
+ return (jsxs("div", { className: containerClasses, children: [jsxs("div", { className: 'andy-bot-header', children: [jsx("div", { className: 'andy-bot-header__icon', children: 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: [jsx("path", { d: 'M12 6V2H8' }), 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' }), jsx("path", { d: 'M2 12h2' }), jsx("path", { d: 'M9 11v2' }), jsx("path", { d: 'M15 11v2' }), jsx("path", { d: 'M20 12h2' })] }) }), jsx("span", { className: 'andy-bot-header__name', children: botName || 'Andy bot' })] }), jsx("div", { className: messageClasses, children: message.isStreaming
4822
+ ? jsx("div", { className: 'andy-message__content andy-message__stream', children: message.content })
4823
+ : jsx("div", { className: 'andy-message__content', dangerouslySetInnerHTML: { __html: messageContent } }) })] }));
4731
4824
  }
4732
4825
  return (jsx("div", { className: containerClasses, children: jsx("div", { className: messageClasses, children: jsx("div", { className: 'andy-message__content', children: jsx("p", { children: messageContent }) }) }) }));
4733
4826
  };
@@ -4742,7 +4835,7 @@ const MessageList = ({ messages, isLoading, logoUrl, botName }) => {
4742
4835
  });
4743
4836
  }
4744
4837
  }, [messages, isLoading]);
4745
- return (jsxs(Fragment, { children: [messages.map(message => (jsx(MessageComponent, { message: message, logoUrl: logoUrl, botName: botName }, message.id || message.timestamp))), isLoading && jsx(TypingIndicator, {}), messages.length === 0 && !isLoading && (jsxs("div", { className: 'andy-empty-state', children: [jsx("div", { className: 'andy-empty-state__icon', children: "\uD83D\uDCAC" }), jsx("div", { className: 'andy-empty-state__text', children: "Start a conversation by typing a message below" })] })), jsx("div", { ref: messagesEndRef })] }));
4838
+ return (jsxs(Fragment, { children: [messages.map(message => (jsx(MessageComponent, { message: message, logoUrl: logoUrl, botName: botName }, message.id || message.timestamp))), isLoading && !messages.some(message => message.isStreaming) && jsx(TypingIndicator, {}), messages.length === 0 && !isLoading && (jsxs("div", { className: 'andy-empty-state', children: [jsx("div", { className: 'andy-empty-state__icon', children: "\uD83D\uDCAC" }), jsx("div", { className: 'andy-empty-state__text', children: "Start a conversation by typing a message below" })] })), jsx("div", { ref: messagesEndRef })] }));
4746
4839
  };
4747
4840
 
4748
4841
  const MessageInput = ({ onSendMessage, disabled = false, placeholder = 'Type your message here...' }) => {
@@ -4757,14 +4850,15 @@ const MessageInput = ({ onSendMessage, disabled = false, placeholder = 'Type you
4757
4850
  }
4758
4851
  try {
4759
4852
  setIsSubmitting(true);
4760
- await onSendMessage(trimmedMessage);
4761
4853
  setMessage('');
4854
+ await onSendMessage(trimmedMessage);
4762
4855
  if (inputRef.current) {
4763
4856
  inputRef.current.focus();
4764
4857
  }
4765
4858
  }
4766
4859
  catch (error) {
4767
4860
  console.error('Error sending message:', error);
4861
+ setMessage(current => current || trimmedMessage);
4768
4862
  }
4769
4863
  finally {
4770
4864
  setIsSubmitting(false);
@@ -4781,7 +4875,7 @@ const MessageInput = ({ onSendMessage, disabled = false, placeholder = 'Type you
4781
4875
  };
4782
4876
  const isInputDisabled = disabled || isSubmitting;
4783
4877
  const canSend = message.trim().length > 0 && !isInputDisabled;
4784
- return (jsxs("div", { className: 'andy-input', children: [jsx("input", { ref: inputRef, type: 'text', value: message, onChange: handleInputChange, onKeyDown: handleKeyDown, placeholder: placeholder, disabled: isInputDisabled, maxLength: 1000, autoComplete: 'off', id: 'andy-input' }), jsx("button", { type: 'button', onClick: () => handleSubmit({ preventDefault: () => { } }), disabled: !canSend, id: 'andy-send', children: isSubmitting ? ('...') : (jsx("svg", { width: '18', height: '18', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', children: jsx("path", { d: 'M2.01 21L23 12 2.01 3 2 10l15 2-15 2z', fill: 'white' }) })) })] }));
4878
+ return (jsxs("div", { className: 'andy-input', children: [jsx("input", { ref: inputRef, type: 'text', value: message, onChange: handleInputChange, onKeyDown: handleKeyDown, placeholder: placeholder, disabled: disabled, maxLength: 1000, autoComplete: 'off', id: 'andy-input' }), jsx("button", { type: 'button', onClick: () => handleSubmit({ preventDefault: () => { } }), disabled: !canSend, id: 'andy-send', children: isSubmitting ? ('...') : (jsx("svg", { width: '18', height: '18', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', children: jsx("path", { d: 'M2.01 21L23 12 2.01 3 2 10l15 2-15 2z', fill: 'white' }) })) })] }));
4785
4879
  };
4786
4880
 
4787
4881
  const ChatWindow = ({ config, messages, isLoading, onClose, onSendMessage }) => {