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 +190 -96
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +190 -96
- package/dist/index.esm.js.map +1 -1
- package/dist/types/types/index.d.ts +1 -0
- package/package.json +2 -2
- package/src/AndyChat.tsx +49 -17
- package/src/components/MessageInput.tsx +3 -2
- package/src/components/MessageList.tsx +5 -3
- package/src/hooks/useAndyChat.ts +30 -13
- package/src/types/index.ts +1 -0
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
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =>
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
4601
|
-
|
|
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:
|
|
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:
|
|
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 }) => {
|