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.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
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =>
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
4605
|
-
|
|
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:
|
|
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:
|
|
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 }) => {
|