andy-widget-react 2.0.5 → 2.1.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/README.md +2 -0
- package/dist/index.cjs +240 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +240 -20
- package/dist/index.esm.js.map +1 -1
- package/dist/types/types/index.d.ts +13 -1
- package/package.json +2 -2
- package/src/AndyChat.tsx +56 -18
- package/src/hooks/useAndyChat.ts +3 -2
- package/src/types/index.ts +13 -1
package/README.md
CHANGED
|
@@ -46,6 +46,8 @@ Widget as a short, language-aware retry message.
|
|
|
46
46
|
| `description` | `string` | Override the configured description |
|
|
47
47
|
| `welcomeMessage` | `string` | Override the configured first message |
|
|
48
48
|
| `bubbleLabel` | `string` | Override the configured pill label |
|
|
49
|
+
| `lang` | `'en' \| 'es'` | Language for the Widget's own chrome (placeholder, starter-question title, error and handoff text). Overrides the Agent's `defaultLang`, so a Spanish Agent on an English page still renders English chrome |
|
|
50
|
+
| `predefinedQuestions` | `{ order: number, question: string }[]` | Override the configured starter questions |
|
|
49
51
|
| `theme` | `'light' \| 'dark' \| 'auto'` | Override the configured theme |
|
|
50
52
|
| `orientation` | `'left' \| 'right'` | Override the configured position |
|
|
51
53
|
| `brandColor` | `string` | Override the configured six-digit hex color |
|
package/dist/index.cjs
CHANGED
|
@@ -873,7 +873,7 @@ const _mapApiDataToChatbotConfig = (apiData, defaultConfig) => {
|
|
|
873
873
|
throw new Error('Invalid chatbot configuration response.')
|
|
874
874
|
}
|
|
875
875
|
|
|
876
|
-
const { chatbot, faqs: rawFaqs } = apiData;
|
|
876
|
+
const { chatbot, faqs: rawFaqs, bootstrap } = apiData;
|
|
877
877
|
const appearance = chatbot.appearance || {};
|
|
878
878
|
const behavior = chatbot.behavior || {};
|
|
879
879
|
|
|
@@ -943,6 +943,7 @@ const _mapApiDataToChatbotConfig = (apiData, defaultConfig) => {
|
|
|
943
943
|
)
|
|
944
944
|
.map(faq => ({ question: faq.question.trim(), answer: faq.answer.trim() }));
|
|
945
945
|
}
|
|
946
|
+
config.bootstrap = bootstrap;
|
|
946
947
|
|
|
947
948
|
return config
|
|
948
949
|
};
|
|
@@ -992,7 +993,7 @@ const fetchChatbotConfig = async embedId => {
|
|
|
992
993
|
* @param {string} message - The user's message
|
|
993
994
|
* @returns {Promise<string>} The bot's response text
|
|
994
995
|
*/
|
|
995
|
-
const
|
|
996
|
+
const sendMessageForWidget = async (embedId, sessionId, message) => {
|
|
996
997
|
try {
|
|
997
998
|
const apiBaseUrl = getApiBaseUrl();
|
|
998
999
|
const response = await fetch(`${apiBaseUrl}/messages`, {
|
|
@@ -1013,10 +1014,13 @@ const sendMessage = async (embedId, sessionId, message) => {
|
|
|
1013
1014
|
if (data.error || data.limitStatus) {
|
|
1014
1015
|
throw new Error(data.error || 'Chat is temporarily unavailable.')
|
|
1015
1016
|
}
|
|
1017
|
+
if (data.handoffStatus === 'paused') {
|
|
1018
|
+
return { status: 'waiting' }
|
|
1019
|
+
}
|
|
1016
1020
|
if (typeof data.botResponse !== 'string' || !data.botResponse.trim()) {
|
|
1017
1021
|
throw new Error('Unexpected response format from server.')
|
|
1018
1022
|
}
|
|
1019
|
-
return data.botResponse
|
|
1023
|
+
return { status: 'agent', text: data.botResponse }
|
|
1020
1024
|
} else {
|
|
1021
1025
|
// Handle HTTP errors (e.g., 500, 404)
|
|
1022
1026
|
let errorData;
|
|
@@ -3433,6 +3437,34 @@ const SVG_ICONS = {
|
|
|
3433
3437
|
<path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z" />
|
|
3434
3438
|
</svg>`};
|
|
3435
3439
|
|
|
3440
|
+
// Localized UI Strings
|
|
3441
|
+
// Chrome the widget renders itself, as opposed to copy the Agent supplies.
|
|
3442
|
+
// Keyed by language so React and Vanilla render identical text for a visitor.
|
|
3443
|
+
const UI_STRINGS = {
|
|
3444
|
+
en: {
|
|
3445
|
+
inputPlaceholder: 'Type your message...',
|
|
3446
|
+
predefinedQuestionsTitle: 'What would you like to know?',
|
|
3447
|
+
sendFailure: 'Sorry, I could not answer. Please try again.',
|
|
3448
|
+
handoffWaiting: 'A team member will reply soon.',
|
|
3449
|
+
teamName: 'Team'
|
|
3450
|
+
},
|
|
3451
|
+
es: {
|
|
3452
|
+
inputPlaceholder: 'Escribe tu mensaje...',
|
|
3453
|
+
predefinedQuestionsTitle: '¿Qué te gustaría saber?',
|
|
3454
|
+
sendFailure: 'Lo siento, no pude responder. Inténtalo de nuevo.',
|
|
3455
|
+
handoffWaiting: 'Un miembro del equipo te responderá pronto.',
|
|
3456
|
+
teamName: 'Equipo'
|
|
3457
|
+
}
|
|
3458
|
+
};
|
|
3459
|
+
|
|
3460
|
+
/**
|
|
3461
|
+
* Get the widget's own UI strings for a language.
|
|
3462
|
+
* Unknown languages fall back to English rather than rendering blanks.
|
|
3463
|
+
* @param {string} [lang] - Language code ('en' or 'es')
|
|
3464
|
+
* @returns {typeof UI_STRINGS.en} The localized string set
|
|
3465
|
+
*/
|
|
3466
|
+
const getUiStrings = lang => UI_STRINGS[lang] || UI_STRINGS.en;
|
|
3467
|
+
|
|
3436
3468
|
/**
|
|
3437
3469
|
* Keyboard detection utility for handling mobile virtual keyboards
|
|
3438
3470
|
* Provides cross-browser support for detecting keyboard open/close events
|
|
@@ -3698,12 +3730,172 @@ const isMobileDevice = () => {
|
|
|
3698
3730
|
return (hasTouch && isSmallScreen) || isMobileUserAgent || isMobileMediaQuery
|
|
3699
3731
|
};
|
|
3700
3732
|
|
|
3701
|
-
const
|
|
3733
|
+
const identityKey = chatbotId => `andy-browser:${chatbotId}`;
|
|
3734
|
+
|
|
3735
|
+
const randomIdentity = () => ({
|
|
3736
|
+
sessionId: window.crypto.randomUUID(),
|
|
3737
|
+
capability: btoa(String.fromCharCode(...(() => {
|
|
3738
|
+
const bytes = new Uint8Array(32);
|
|
3739
|
+
window.crypto.getRandomValues(bytes);
|
|
3740
|
+
return bytes
|
|
3741
|
+
})()))
|
|
3742
|
+
});
|
|
3743
|
+
|
|
3744
|
+
const callConvex = async (convexUrl, kind, path, wireArgs, keepalive = false) => {
|
|
3745
|
+
const response = await fetch(`${convexUrl}/api/${kind}`, {
|
|
3746
|
+
method: 'POST',
|
|
3747
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3748
|
+
body: JSON.stringify({ path, format: 'convex_encoded_json', args: [wireArgs] }),
|
|
3749
|
+
keepalive
|
|
3750
|
+
});
|
|
3751
|
+
if (!response.ok) throw new Error(`Browser handoff ${kind} failed: ${response.status}`)
|
|
3752
|
+
const data = await response.json();
|
|
3753
|
+
if (data.status !== 'success') throw new Error(data.errorMessage || `Browser handoff ${path} failed`)
|
|
3754
|
+
return data.value
|
|
3755
|
+
};
|
|
3756
|
+
|
|
3757
|
+
const sortMessages = messages => [...messages].sort((left, right) =>
|
|
3758
|
+
left.createdAt - right.createdAt || String(left.id).localeCompare(String(right.id)));
|
|
3759
|
+
|
|
3760
|
+
const createBrowserHandoff = (chatbotId, bootstrap, onProjection) => {
|
|
3761
|
+
if (!bootstrap?.convexUrl || !bootstrap.surfaceKey || !bootstrap.surfaceProof) return null
|
|
3762
|
+
const storageKey = identityKey(chatbotId);
|
|
3763
|
+
let identity;
|
|
3764
|
+
try {
|
|
3765
|
+
const stored = JSON.parse(window.sessionStorage.getItem(storageKey) || 'null');
|
|
3766
|
+
identity = stored?.sessionId && stored?.capability && stored.surfaceKey === bootstrap.surfaceKey
|
|
3767
|
+
? { sessionId: stored.sessionId, capability: stored.capability }
|
|
3768
|
+
: randomIdentity();
|
|
3769
|
+
window.sessionStorage.setItem(storageKey, JSON.stringify({ ...identity, surfaceKey: bootstrap.surfaceKey }));
|
|
3770
|
+
} catch {
|
|
3771
|
+
identity = randomIdentity();
|
|
3772
|
+
}
|
|
3773
|
+
const wireArgs = () => ({ chatbotId, sessionId: identity.sessionId, capability: identity.capability, surfaceProof: bootstrap.surfaceProof });
|
|
3774
|
+
let active = true;
|
|
3775
|
+
let visible = document.visibilityState === 'visible';
|
|
3776
|
+
let cursor;
|
|
3777
|
+
let timer;
|
|
3778
|
+
const pending = new Set();
|
|
3779
|
+
const acknowledged = new Set();
|
|
3780
|
+
const acknowledging = new Set();
|
|
3781
|
+
const localMessages = new Map();
|
|
3782
|
+
const emit = projection => {
|
|
3783
|
+
if (!active) return
|
|
3784
|
+
if (projection?.ok === false && projection.code === 'browser_presence_unavailable') {
|
|
3785
|
+
onProjection({ offline: true, refreshBootstrap: Date.now() >= bootstrap.surfaceProofExpiresAt, identity, messages: [] });
|
|
3786
|
+
return
|
|
3787
|
+
}
|
|
3788
|
+
if (!projection?.ok) return
|
|
3789
|
+
cursor = projection.cursor || cursor;
|
|
3790
|
+
const transcript = projection.transcript || [];
|
|
3791
|
+
const projected = (projection.messages || []).map(message => ({ ...message, role: 'bot', sentByHuman: true }));
|
|
3792
|
+
const byId = new Map(transcript.map(message => [String(message.id), message]));
|
|
3793
|
+
for (const message of localMessages.values()) {
|
|
3794
|
+
const persisted = transcript.some(candidate => candidate.role === 'user' && candidate.content === message.content && Math.abs(candidate.createdAt - message.createdAt) < 30000);
|
|
3795
|
+
if (persisted) localMessages.delete(message.id);
|
|
3796
|
+
else byId.set(message.id, message);
|
|
3797
|
+
}
|
|
3798
|
+
projected.forEach(message => byId.set(String(message.id), message));
|
|
3799
|
+
const messages = sortMessages([...byId.values()]);
|
|
3800
|
+
const pendingIds = projected.map(message => String(message.id)).filter(id => !acknowledged.has(id));
|
|
3801
|
+
pendingIds.forEach(id => pending.add(id));
|
|
3802
|
+
onProjection({ identity, control: projection.control, messages, pendingProjectionIds: pendingIds });
|
|
3803
|
+
};
|
|
3804
|
+
const project = async () => {
|
|
3805
|
+
if (!active || !visible) return
|
|
3806
|
+
if (Date.now() >= bootstrap.surfaceProofExpiresAt) {
|
|
3807
|
+
emit({ ok: false, code: 'browser_presence_unavailable' });
|
|
3808
|
+
return
|
|
3809
|
+
}
|
|
3810
|
+
try {
|
|
3811
|
+
const result = await callConvex(bootstrap.convexUrl, 'query', 'browserHandoff:project', { ...wireArgs(), ...(cursor ? { cursor } : {}) });
|
|
3812
|
+
emit(result);
|
|
3813
|
+
} catch {
|
|
3814
|
+
if (active) onProjection({ offline: true, identity, messages: [] });
|
|
3815
|
+
}
|
|
3816
|
+
};
|
|
3817
|
+
const connect = async () => {
|
|
3818
|
+
if (!active || Date.now() >= bootstrap.surfaceProofExpiresAt) {
|
|
3819
|
+
emit({ ok: false, code: 'browser_presence_unavailable' });
|
|
3820
|
+
return
|
|
3821
|
+
}
|
|
3822
|
+
try {
|
|
3823
|
+
const result = await callConvex(bootstrap.convexUrl, 'mutation', 'browserHandoff:connect', wireArgs());
|
|
3824
|
+
if (result?.ok === false) return emit(result)
|
|
3825
|
+
await project();
|
|
3826
|
+
} catch {
|
|
3827
|
+
if (active) onProjection({ offline: true, identity, messages: [] });
|
|
3828
|
+
}
|
|
3829
|
+
};
|
|
3830
|
+
const release = () => callConvex(bootstrap.convexUrl, 'mutation', 'browserHandoff:release', wireArgs(), true).catch(() => undefined);
|
|
3831
|
+
const onVisibility = () => {
|
|
3832
|
+
visible = document.visibilityState === 'visible';
|
|
3833
|
+
if (visible) void connect();
|
|
3834
|
+
else void release();
|
|
3835
|
+
};
|
|
3836
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
3837
|
+
window.addEventListener('pagehide', release);
|
|
3838
|
+
void connect();
|
|
3839
|
+
timer = window.setInterval(async () => {
|
|
3840
|
+
if (!active || !visible) return
|
|
3841
|
+
if (Date.now() >= bootstrap.surfaceProofExpiresAt) {
|
|
3842
|
+
window.clearInterval(timer);
|
|
3843
|
+
emit({ ok: false, code: 'browser_presence_unavailable' });
|
|
3844
|
+
return
|
|
3845
|
+
}
|
|
3846
|
+
try {
|
|
3847
|
+
const result = await callConvex(bootstrap.convexUrl, 'mutation', 'browserHandoff:heartbeat', wireArgs());
|
|
3848
|
+
if (result?.ok === false) emit(result);
|
|
3849
|
+
else await project();
|
|
3850
|
+
} catch {
|
|
3851
|
+
if (active) onProjection({ offline: true, identity, messages: [] });
|
|
3852
|
+
}
|
|
3853
|
+
}, 15000);
|
|
3854
|
+
return {
|
|
3855
|
+
identity: { sessionId: identity.sessionId },
|
|
3856
|
+
recordVisitorMessage: content => {
|
|
3857
|
+
const id = `local-${window.crypto.randomUUID()}`;
|
|
3858
|
+
localMessages.set(id, { id, role: 'user', content, createdAt: Date.now(), sentByHuman: false });
|
|
3859
|
+
},
|
|
3860
|
+
markRendered: async messageId => {
|
|
3861
|
+
if (!active || !pending.has(messageId) || acknowledged.has(messageId) || acknowledging.has(messageId)) return
|
|
3862
|
+
acknowledging.add(messageId);
|
|
3863
|
+
try {
|
|
3864
|
+
const result = await callConvex(bootstrap.convexUrl, 'mutation', 'browserHandoff:acknowledge', { ...wireArgs(), messageId });
|
|
3865
|
+
if (result?.ok) {
|
|
3866
|
+
acknowledged.add(messageId);
|
|
3867
|
+
pending.delete(messageId);
|
|
3868
|
+
}
|
|
3869
|
+
} catch {
|
|
3870
|
+
if (active) onProjection({ offline: true, identity, messages: [] });
|
|
3871
|
+
} finally {
|
|
3872
|
+
acknowledging.delete(messageId);
|
|
3873
|
+
}
|
|
3874
|
+
},
|
|
3875
|
+
stop: () => {
|
|
3876
|
+
if (!active) return
|
|
3877
|
+
active = false;
|
|
3878
|
+
window.clearInterval(timer);
|
|
3879
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
3880
|
+
window.removeEventListener('pagehide', release);
|
|
3881
|
+
void release();
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
};
|
|
3885
|
+
|
|
3886
|
+
const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessage, bubbleLabel, lang, predefinedQuestions, theme, orientation, brandColor, bubbleStyle, useBrandColor, logoUrl, previewMode = false, initialOpen = false }) => {
|
|
3702
3887
|
const [config, setConfig] = react.useState(null);
|
|
3888
|
+
// The visitor's language wins over the Agent's own default: a Spanish Agent
|
|
3889
|
+
// embedded on an English page must still render English chrome.
|
|
3890
|
+
const uiLang = lang || config?.defaultLang || 'en';
|
|
3891
|
+
const strings = getUiStrings(uiLang);
|
|
3892
|
+
const activeQuestions = predefinedQuestions?.length ? predefinedQuestions : config?.predefinedQuestions;
|
|
3703
3893
|
const [isOpen, setIsOpen] = react.useState(initialOpen);
|
|
3704
3894
|
const [messages, setMessages] = react.useState([]);
|
|
3705
3895
|
const [isLoading, setIsLoading] = react.useState(false);
|
|
3706
3896
|
const [sessionId, setSessionId] = react.useState(() => generateSessionId());
|
|
3897
|
+
const handoffRef = react.useRef(null);
|
|
3898
|
+
const pendingHandoffIdsRef = react.useRef([]);
|
|
3707
3899
|
const [shadowReady, setShadowReady] = react.useState(false);
|
|
3708
3900
|
const [showPredefinedQuestions, setShowPredefinedQuestions] = react.useState(true);
|
|
3709
3901
|
const [userInfoCollected, setUserInfoCollected] = react.useState(false);
|
|
@@ -3717,6 +3909,29 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
|
|
|
3717
3909
|
const messagesContainerRef = react.useRef(null);
|
|
3718
3910
|
const welcomePopupShownRef = react.useRef(false);
|
|
3719
3911
|
const welcomePopupTimeoutRef = react.useRef(null);
|
|
3912
|
+
react.useEffect(() => {
|
|
3913
|
+
if (!config?.bootstrap || !config.id)
|
|
3914
|
+
return;
|
|
3915
|
+
handoffRef.current?.stop();
|
|
3916
|
+
const handoff = createBrowserHandoff(config.id, config.bootstrap, projection => {
|
|
3917
|
+
if (projection.offline)
|
|
3918
|
+
return;
|
|
3919
|
+
setSessionId(projection.identity.sessionId);
|
|
3920
|
+
pendingHandoffIdsRef.current = projection.pendingProjectionIds || [];
|
|
3921
|
+
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 }));
|
|
3922
|
+
setMessages(projection.control === 'requested' || projection.control === 'human'
|
|
3923
|
+
? [...projectedMessages, { id: 'handoff-waiting', content: strings.handoffWaiting, sender: 'bot', timestamp: new Date().toISOString(), isWaiting: true }]
|
|
3924
|
+
: projectedMessages);
|
|
3925
|
+
});
|
|
3926
|
+
handoffRef.current = handoff;
|
|
3927
|
+
return () => {
|
|
3928
|
+
handoff?.stop();
|
|
3929
|
+
handoffRef.current = null;
|
|
3930
|
+
};
|
|
3931
|
+
}, [config?.bootstrap, config?.id]);
|
|
3932
|
+
react.useEffect(() => {
|
|
3933
|
+
pendingHandoffIdsRef.current.forEach(messageId => void handoffRef.current?.markRendered(messageId));
|
|
3934
|
+
}, [messages]);
|
|
3720
3935
|
// Create shadow DOM container on mount
|
|
3721
3936
|
react.useEffect(() => {
|
|
3722
3937
|
if (!containerRef.current)
|
|
@@ -3894,12 +4109,12 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
|
|
|
3894
4109
|
bubbleStyle: bubbleStyle || 'icon',
|
|
3895
4110
|
bubbleLabel: bubbleLabel || null,
|
|
3896
4111
|
useBrandColor: useBrandColor !== undefined ? useBrandColor : true,
|
|
3897
|
-
defaultLang:
|
|
4112
|
+
defaultLang: uiLang,
|
|
3898
4113
|
showBranding: true,
|
|
3899
4114
|
showWelcomePopup: { enabled: false, seconds: 3, message: '' },
|
|
3900
4115
|
collectUserInfo: { enabled: false, message: '' },
|
|
3901
4116
|
welcomeMessage: welcomeMessage || 'Hi there! How can I help you today?',
|
|
3902
|
-
predefinedQuestions: [],
|
|
4117
|
+
predefinedQuestions: predefinedQuestions || [],
|
|
3903
4118
|
faqs: []
|
|
3904
4119
|
};
|
|
3905
4120
|
setConfig(previewConfig);
|
|
@@ -4043,16 +4258,21 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
|
|
|
4043
4258
|
sender: 'user',
|
|
4044
4259
|
timestamp: new Date().toISOString()
|
|
4045
4260
|
};
|
|
4261
|
+
handoffRef.current?.recordVisitorMessage(messageText);
|
|
4046
4262
|
setMessages(prev => [...prev, userMessage]);
|
|
4047
4263
|
if (hidePredefined) {
|
|
4048
4264
|
setShowPredefinedQuestions(false);
|
|
4049
4265
|
}
|
|
4050
4266
|
try {
|
|
4051
4267
|
setIsLoading(true);
|
|
4052
|
-
const response = await
|
|
4268
|
+
const response = await sendMessageForWidget(embedId, sessionId, messageText);
|
|
4269
|
+
if (response.status === 'waiting') {
|
|
4270
|
+
setMessages(prev => prev.some(message => message.isWaiting) ? prev : [...prev, { id: 'handoff-waiting', content: strings.handoffWaiting, sender: 'bot', timestamp: new Date().toISOString(), isWaiting: true }]);
|
|
4271
|
+
return;
|
|
4272
|
+
}
|
|
4053
4273
|
const botMessage = {
|
|
4054
4274
|
id: timestamp + 1,
|
|
4055
|
-
content: response,
|
|
4275
|
+
content: response.text,
|
|
4056
4276
|
sender: 'bot',
|
|
4057
4277
|
timestamp: new Date().toISOString()
|
|
4058
4278
|
};
|
|
@@ -4074,9 +4294,7 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
|
|
|
4074
4294
|
console.error('AndyChat: Message request failed.');
|
|
4075
4295
|
const errorMessage = {
|
|
4076
4296
|
id: timestamp + 3,
|
|
4077
|
-
content:
|
|
4078
|
-
? 'Lo siento, no pude responder. Inténtalo de nuevo.'
|
|
4079
|
-
: 'Sorry, I could not answer. Please try again.',
|
|
4297
|
+
content: strings.sendFailure,
|
|
4080
4298
|
sender: 'bot',
|
|
4081
4299
|
timestamp: new Date().toISOString(),
|
|
4082
4300
|
isError: true
|
|
@@ -4192,7 +4410,7 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
|
|
|
4192
4410
|
bubble.addEventListener('click', () => setIsOpen(prev => !prev));
|
|
4193
4411
|
}
|
|
4194
4412
|
const isPillStyle = config?.bubbleStyle === 'pill';
|
|
4195
|
-
const displayLabel = bubbleLabel || config?.bubbleLabel || (isPillStyle ? getDefaultBubbleLabel(
|
|
4413
|
+
const displayLabel = bubbleLabel || config?.bubbleLabel || (isPillStyle ? getDefaultBubbleLabel(uiLang) : null);
|
|
4196
4414
|
const bubbleLogoUrl = config?.logoUrl || CHAT_BUBBLE_DEFAULT_ICON_URL;
|
|
4197
4415
|
if (bubble) {
|
|
4198
4416
|
bubble.className = 'andy-chat-bubble';
|
|
@@ -4245,10 +4463,10 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
|
|
|
4245
4463
|
if (message.isLoading) {
|
|
4246
4464
|
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));
|
|
4247
4465
|
}
|
|
4248
|
-
return (jsxRuntime.jsxs("div", { className: `andy-message-container andy-message-container--${message.sender}`, children: [message.sender === 'bot' && (jsxRuntime.jsxs("div", { className: `andy-message andy-message--bot ${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: 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));
|
|
4249
|
-
}), 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: [
|
|
4250
|
-
|
|
4251
|
-
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:
|
|
4466
|
+
return (jsxRuntime.jsxs("div", { className: `andy-message-container andy-message-container--${message.sender} ${message.isWaiting ? 'andy-message-container--waiting' : ''}`, children: [message.sender === 'bot' && (jsxRuntime.jsxs("div", { className: `andy-message andy-message--bot ${message.isHuman ? 'andy-message--operator' : ''} ${message.isWaiting ? 'andy-message--waiting' : ''} ${message.isError ? 'andy-message--error' : ''}`, children: [jsxRuntime.jsxs("div", { className: 'andy-bot-header', children: [jsxRuntime.jsx("div", { className: 'andy-bot-header__icon', children: config?.logoUrl ? (jsxRuntime.jsx("img", { src: config.logoUrl, alt: 'Bot Avatar', className: 'andy-bot-header__icon-image' })) : (jsxRuntime.jsx("img", { src: CHAT_BUBBLE_DEFAULT_ICON_URL, alt: 'Bot Avatar', className: 'andy-bot-header__icon-image andy-bot-header__icon-image--default' })) }), jsxRuntime.jsx("span", { className: 'andy-bot-header__name', children: message.isHuman ? strings.teamName : config?.name || 'Andy bot' })] }), jsxRuntime.jsx("div", { className: 'andy-message__content', children: jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: parseMarkdown(message.content) } }) })] })), message.sender === 'user' && (jsxRuntime.jsx("div", { className: 'andy-message andy-message--user', children: jsxRuntime.jsx("div", { className: 'andy-message__content', children: jsxRuntime.jsx("p", { children: message.content }) }) }))] }, message.id));
|
|
4467
|
+
}), isLoading && (jsxRuntime.jsx("div", { className: 'andy-message-container andy-message-container--bot andy-message-container--typing', children: jsxRuntime.jsx("div", { className: 'andy-message andy-message--bot andy-message--typing', children: jsxRuntime.jsxs("div", { className: 'andy-typing-indicator', children: [jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' }), jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' }), jsxRuntime.jsx("div", { className: 'andy-typing-indicator__dot' })] }) }) }))] }), jsxRuntime.jsxs("div", { className: 'andy-chat-window__bottom', children: [activeQuestions &&
|
|
4468
|
+
activeQuestions.length > 0 &&
|
|
4469
|
+
showPredefinedQuestions && (jsxRuntime.jsxs("div", { className: 'andy-predefined-questions', id: 'andy-predefined-questions', children: [jsxRuntime.jsx("div", { className: 'andy-predefined-questions__header', children: jsxRuntime.jsx("p", { className: 'andy-predefined-questions__title', children: strings.predefinedQuestionsTitle }) }), jsxRuntime.jsx("div", { className: 'andy-predefined-questions__list', children: activeQuestions.map((q, index) => (jsxRuntime.jsxs("button", { type: 'button', className: 'andy-predefined-question', "data-question": q.question, disabled: isCollectingUserInfo || isLoading || !config, children: [jsxRuntime.jsx("span", { className: 'andy-predefined-question__icon', children: "\uD83D\uDCAC" }), jsxRuntime.jsx("span", { className: 'andy-predefined-question__text', children: q.question })] }, index))) })] })), jsxRuntime.jsx("div", { className: 'andy-chat-window__input', children: jsxRuntime.jsx("div", { className: 'andy-input-container', children: jsxRuntime.jsxs("div", { className: 'andy-input', children: [jsxRuntime.jsx("input", { type: 'text', className: 'andy-input__field', id: 'andy-message-input', placeholder: config ? strings.inputPlaceholder : '...', "aria-label": 'Message input', onKeyDown: e => {
|
|
4252
4470
|
if (e.key === 'Enter' && e.target.value.trim()) {
|
|
4253
4471
|
handleSendMessage(e.target.value);
|
|
4254
4472
|
e.target.value = '';
|
|
@@ -4350,7 +4568,7 @@ const useAndyChat = (embedId) => {
|
|
|
4350
4568
|
return newIsOpen;
|
|
4351
4569
|
});
|
|
4352
4570
|
}, [config, welcomePopupShown, userInfoCollected]);
|
|
4353
|
-
const sendMessage
|
|
4571
|
+
const sendMessage = react.useCallback(async (messageText) => {
|
|
4354
4572
|
if (!messageText.trim() || !config || !sessionId) {
|
|
4355
4573
|
return;
|
|
4356
4574
|
}
|
|
@@ -4362,10 +4580,12 @@ const useAndyChat = (embedId) => {
|
|
|
4362
4580
|
};
|
|
4363
4581
|
setMessages(prev => [...prev, userMessage]);
|
|
4364
4582
|
try {
|
|
4365
|
-
const response = await
|
|
4583
|
+
const response = await sendMessageForWidget(embedId, sessionId, messageText);
|
|
4584
|
+
if (response.status === 'waiting')
|
|
4585
|
+
return response;
|
|
4366
4586
|
const botMessage = {
|
|
4367
4587
|
id: Date.now() + 1,
|
|
4368
|
-
content: response || 'I apologize, but I encountered an error processing your request.',
|
|
4588
|
+
content: response.text || 'I apologize, but I encountered an error processing your request.',
|
|
4369
4589
|
sender: 'bot',
|
|
4370
4590
|
timestamp: new Date().toISOString()
|
|
4371
4591
|
};
|
|
@@ -4462,7 +4682,7 @@ const useAndyChat = (embedId) => {
|
|
|
4462
4682
|
error,
|
|
4463
4683
|
initializeChatbot,
|
|
4464
4684
|
toggleChat,
|
|
4465
|
-
sendMessage
|
|
4685
|
+
sendMessage,
|
|
4466
4686
|
showFaq,
|
|
4467
4687
|
hideFaq,
|
|
4468
4688
|
showWelcomePopup,
|