@eshal-bot/chat-widget 0.2.10 → 0.2.12
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/{chat-widget-MarkdownRenderer-CGbeN7oT.esm.js → chat-widget-MarkdownRenderer-CBSXpNd6.esm.js} +6 -6
- package/dist/{chat-widget-index-CJ4b6TSc.esm.js → chat-widget-index-DqpnMEPr.esm.js} +3 -3
- package/dist/chat-widget-markdown-chunk.js +63 -3
- package/dist/chat-widget-markdown-chunk.min.js +9 -9
- package/dist/chat-widget.esm.js +1 -1
- package/dist/chat-widget.js +183 -31
- package/dist/chat-widget.min.js +4 -4
- package/dist/chat-widget.umd.js +6 -6
- package/package.json +1 -1
package/dist/chat-widget.esm.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{S as default}from"./chat-widget-index-
|
|
1
|
+
export{S as default}from"./chat-widget-index-DqpnMEPr.esm.js";import"react";import"react-dom/client";import"react/jsx-runtime";import"react-dom";
|
package/dist/chat-widget.js
CHANGED
|
@@ -11945,6 +11945,11 @@
|
|
|
11945
11945
|
};
|
|
11946
11946
|
const useChatState = ({
|
|
11947
11947
|
welcomeMessage,
|
|
11948
|
+
// The org's configured fallback. Shown in place of the generic
|
|
11949
|
+
// t("connectionError") when a turn dies before producing a token, so a
|
|
11950
|
+
// branded assistant fails in its own words. Undefined / empty keeps the
|
|
11951
|
+
// localised connection-error copy, so an unconfigured org is unchanged.
|
|
11952
|
+
fallbackMessage,
|
|
11948
11953
|
quickQuestions = [],
|
|
11949
11954
|
apiBaseUrl,
|
|
11950
11955
|
wsBaseUrl,
|
|
@@ -12179,6 +12184,90 @@
|
|
|
12179
12184
|
// Null when the SDK's IP/geo resolver is blocked - conversations from
|
|
12180
12185
|
// those visitors fall into the "Other" bucket on the analytics dashboard.
|
|
12181
12186
|
const visitorProfileRef = reactExports.useRef(null);
|
|
12187
|
+
|
|
12188
|
+
// EV-4047: high-water mark of the newest agent message we have already
|
|
12189
|
+
// merged, as an ISO timestamp. Seeded from the history fetch and advanced by
|
|
12190
|
+
// every SSE frame and every backfill, so a reconnect asks the server only
|
|
12191
|
+
// for what it actually missed.
|
|
12192
|
+
const lastAgentMessageAtRef = reactExports.useRef(null);
|
|
12193
|
+
const noteAgentMessageAt = reactExports.useCallback(createdAt => {
|
|
12194
|
+
if (!createdAt) return;
|
|
12195
|
+
const t = new Date(createdAt).getTime();
|
|
12196
|
+
if (!Number.isFinite(t)) return;
|
|
12197
|
+
const current = lastAgentMessageAtRef.current ? new Date(lastAgentMessageAtRef.current).getTime() : -Infinity;
|
|
12198
|
+
if (t > current) lastAgentMessageAtRef.current = new Date(t).toISOString();
|
|
12199
|
+
}, []);
|
|
12200
|
+
|
|
12201
|
+
// Merge a batch of server-side agent messages into the transcript, deduped
|
|
12202
|
+
// by id and re-sorted by timestamp. Shared by the mount-time history fetch
|
|
12203
|
+
// and the SSE backfill so both agree on ordering and on what counts as an
|
|
12204
|
+
// agent message.
|
|
12205
|
+
const mergeAgentMessages = reactExports.useCallback(agentMsgs => {
|
|
12206
|
+
if (!Array.isArray(agentMsgs) || agentMsgs.length === 0) return;
|
|
12207
|
+
for (const m of agentMsgs) noteAgentMessageAt(m?.createdAt);
|
|
12208
|
+
setMessages(prev => {
|
|
12209
|
+
const seen = new Set(prev.map(m => m.id));
|
|
12210
|
+
const additions = agentMsgs.filter(m => m && m.id && !seen.has(m.id)).map(m => ({
|
|
12211
|
+
...createMessage({
|
|
12212
|
+
id: m.id,
|
|
12213
|
+
role: m.role || 'assistant',
|
|
12214
|
+
content: m.content,
|
|
12215
|
+
isProcessing: false,
|
|
12216
|
+
metadata: {
|
|
12217
|
+
senderName: m.senderName
|
|
12218
|
+
}
|
|
12219
|
+
}),
|
|
12220
|
+
timestamp: m.createdAt ? new Date(m.createdAt) : new Date()
|
|
12221
|
+
}));
|
|
12222
|
+
if (additions.length === 0) return prev;
|
|
12223
|
+
const tsMs = m => {
|
|
12224
|
+
const t = m.timestamp instanceof Date ? m.timestamp.getTime() : new Date(m.timestamp).getTime();
|
|
12225
|
+
return Number.isFinite(t) ? t : 0;
|
|
12226
|
+
};
|
|
12227
|
+
const merged = [...prev, ...additions];
|
|
12228
|
+
merged.sort((a, b) => tsMs(a) - tsMs(b));
|
|
12229
|
+
return merged;
|
|
12230
|
+
});
|
|
12231
|
+
}, [noteAgentMessageAt]);
|
|
12232
|
+
|
|
12233
|
+
/**
|
|
12234
|
+
* EV-4047 - backfill agent messages published while we had no live stream.
|
|
12235
|
+
*
|
|
12236
|
+
* `deliverWidgetMessageLocal` (backend) drops a push when the conversation
|
|
12237
|
+
* has no registered SSE connection: "nobody is listening" and "this replica
|
|
12238
|
+
* holds no connection" are the same early return. So any human-agent message
|
|
12239
|
+
* published in the window between the takeover and our EventSource actually
|
|
12240
|
+
* being registered server-side is gone for good - which is exactly the
|
|
12241
|
+
* reported symptom, the agent's FIRST reply missing while the second and
|
|
12242
|
+
* later ones arrive.
|
|
12243
|
+
*
|
|
12244
|
+
* Run on every `onopen`, which covers both the initial subscribe and the
|
|
12245
|
+
* browser's own silent EventSource auto-reconnect (our `onerror` never tore
|
|
12246
|
+
* the stream down, so a dropped stream previously lost everything sent
|
|
12247
|
+
* during the gap with nothing to notice it had happened).
|
|
12248
|
+
*/
|
|
12249
|
+
const backfillAgentMessages = reactExports.useCallback(async () => {
|
|
12250
|
+
if (!apiBaseUrl || !bidiSessionId) return;
|
|
12251
|
+
try {
|
|
12252
|
+
const headers = {};
|
|
12253
|
+
if (typeof getAuthToken === 'function') {
|
|
12254
|
+
const token = await getAuthToken();
|
|
12255
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
12256
|
+
}
|
|
12257
|
+
const since = lastAgentMessageAtRef.current;
|
|
12258
|
+
const url = `${apiBaseUrl.replace(/\/$/, '')}/api/v1/widget/history/${encodeURIComponent(bidiSessionId)}` + (since ? `?since=${encodeURIComponent(since)}` : '');
|
|
12259
|
+
const res = await fetch(url, {
|
|
12260
|
+
headers,
|
|
12261
|
+
credentials: 'include'
|
|
12262
|
+
});
|
|
12263
|
+
if (!res.ok) return;
|
|
12264
|
+
const data = await res.json();
|
|
12265
|
+
const serverMsgs = Array.isArray(data?.messages) ? data.messages : [];
|
|
12266
|
+
mergeAgentMessages(serverMsgs.filter(m => m?.contentType === 'human_reply' || m?.contentType === 'handoff_ack'));
|
|
12267
|
+
} catch {
|
|
12268
|
+
/* network/parse failure - the live stream still carries new messages */
|
|
12269
|
+
}
|
|
12270
|
+
}, [apiBaseUrl, bidiSessionId, getAuthToken, mergeAgentMessages]);
|
|
12182
12271
|
const startHandoffEventSource = reactExports.useCallback(async onConnected => {
|
|
12183
12272
|
if (handoffEventSourceRef.current) {
|
|
12184
12273
|
if (onConnected) onConnected();
|
|
@@ -12208,6 +12297,9 @@
|
|
|
12208
12297
|
try {
|
|
12209
12298
|
const data = JSON.parse(event.data);
|
|
12210
12299
|
if (data.type === 'message_created' && data.message) {
|
|
12300
|
+
// Advance the backfill watermark so a later reconnect doesn't
|
|
12301
|
+
// re-request everything we have already rendered live.
|
|
12302
|
+
noteAgentMessageAt(data.message.createdAt);
|
|
12211
12303
|
setMessages(prev => {
|
|
12212
12304
|
if (prev.some(m => m.id === data.message.id)) return prev;
|
|
12213
12305
|
return [...prev, createMessage({
|
|
@@ -12224,11 +12316,19 @@
|
|
|
12224
12316
|
} catch {}
|
|
12225
12317
|
};
|
|
12226
12318
|
es.onopen = () => {
|
|
12319
|
+
// EV-4047: close the delivery gap BEFORE anything else. This fires on
|
|
12320
|
+
// the first subscribe and again on every browser-driven reconnect, and
|
|
12321
|
+
// messages published while we were not registered server-side were
|
|
12322
|
+
// dropped outright - the backfill is the only thing that recovers them.
|
|
12323
|
+
void backfillAgentMessages();
|
|
12227
12324
|
if (onConnected) onConnected();
|
|
12228
12325
|
};
|
|
12326
|
+
// Left empty deliberately: EventSource reconnects on its own and will fire
|
|
12327
|
+
// `onopen` again, which re-runs the backfill above. Closing the stream here
|
|
12328
|
+
// would disable that recovery.
|
|
12229
12329
|
es.onerror = () => {};
|
|
12230
12330
|
handoffEventSourceRef.current = es;
|
|
12231
|
-
}, [apiBaseUrl, bidiSessionId, getAuthToken]);
|
|
12331
|
+
}, [apiBaseUrl, bidiSessionId, getAuthToken, backfillAgentMessages]);
|
|
12232
12332
|
reactExports.useEffect(() => {
|
|
12233
12333
|
return () => {
|
|
12234
12334
|
if (handoffEventSourceRef.current) {
|
|
@@ -12480,28 +12580,12 @@
|
|
|
12480
12580
|
const agentMsgs = serverMsgs.filter(m => m?.contentType === 'human_reply' || m?.contentType === 'handoff_ack');
|
|
12481
12581
|
|
|
12482
12582
|
// Merge past human-agent replies into the transcript (regardless of
|
|
12483
|
-
// current responder - they're real history worth showing).
|
|
12484
|
-
|
|
12485
|
-
|
|
12486
|
-
|
|
12487
|
-
|
|
12488
|
-
|
|
12489
|
-
id: m.id,
|
|
12490
|
-
role: m.role || 'assistant',
|
|
12491
|
-
content: m.content,
|
|
12492
|
-
isProcessing: false
|
|
12493
|
-
}),
|
|
12494
|
-
timestamp: m.createdAt ? new Date(m.createdAt) : new Date()
|
|
12495
|
-
}));
|
|
12496
|
-
if (additions.length === 0) return prev;
|
|
12497
|
-
const tsMs = m => {
|
|
12498
|
-
const t = m.timestamp instanceof Date ? m.timestamp.getTime() : new Date(m.timestamp).getTime();
|
|
12499
|
-
return Number.isFinite(t) ? t : 0;
|
|
12500
|
-
};
|
|
12501
|
-
const merged = [...prev, ...additions];
|
|
12502
|
-
merged.sort((a, b) => tsMs(a) - tsMs(b));
|
|
12503
|
-
return merged;
|
|
12504
|
-
});
|
|
12583
|
+
// current responder - they're real history worth showing). Shares
|
|
12584
|
+
// mergeAgentMessages with the EV-4047 SSE backfill, which also seeds
|
|
12585
|
+
// the `since` watermark from what we just merged so the stream's first
|
|
12586
|
+
// `onopen` only asks for genuinely newer messages.
|
|
12587
|
+
if (!cancelled) {
|
|
12588
|
+
mergeAgentMessages(agentMsgs);
|
|
12505
12589
|
}
|
|
12506
12590
|
|
|
12507
12591
|
// Suppress the AI loader + open the live SSE ONLY when a human agent
|
|
@@ -12519,7 +12603,7 @@
|
|
|
12519
12603
|
return () => {
|
|
12520
12604
|
cancelled = true;
|
|
12521
12605
|
};
|
|
12522
|
-
}, [apiBaseUrl, organizationId, bidiSessionId, getAuthToken, startHandoffEventSource, markHandedOff]);
|
|
12606
|
+
}, [apiBaseUrl, organizationId, bidiSessionId, getAuthToken, startHandoffEventSource, markHandedOff, mergeAgentMessages]);
|
|
12523
12607
|
|
|
12524
12608
|
// Persist prompt suggestions to localStorage so they survive page refresh
|
|
12525
12609
|
reactExports.useEffect(() => {
|
|
@@ -13015,13 +13099,19 @@
|
|
|
13015
13099
|
// apology bubble alone did not say WHICH message was lost, and the
|
|
13016
13100
|
// user bubble kept looking exactly like a delivered one.
|
|
13017
13101
|
setUserMessageStatus(sentUserMessageId, "failed");
|
|
13102
|
+
// Keep the diagnostic on the console REGARDLESS of what the customer
|
|
13103
|
+
// is shown: the org fallback reads as "I don't know that", so once it
|
|
13104
|
+
// is on screen an outage and a knowledge gap are indistinguishable to
|
|
13105
|
+
// whoever is looking at the widget. Support needs to tell them apart.
|
|
13106
|
+
console.error("[widget] send failed before any token; showing the org fallback. " + "This is a FAILED TURN, not a knowledge gap.");
|
|
13018
13107
|
setMessages(prev => {
|
|
13019
13108
|
const filtered = (prev || []).filter(msg => !msg.isProcessing);
|
|
13020
13109
|
const errorMessage = createMessage({
|
|
13021
13110
|
id: getNextMessageId(),
|
|
13022
13111
|
role: "assistant",
|
|
13023
|
-
content: t("connectionError"),
|
|
13112
|
+
content: fallbackMessage?.trim() || t("connectionError"),
|
|
13024
13113
|
isProcessing: false,
|
|
13114
|
+
// Still an error for styling / analytics: only the COPY changes.
|
|
13025
13115
|
isError: true
|
|
13026
13116
|
});
|
|
13027
13117
|
return [...filtered, errorMessage];
|
|
@@ -14051,7 +14141,7 @@
|
|
|
14051
14141
|
throw error;
|
|
14052
14142
|
}
|
|
14053
14143
|
}, [handleRecorderAudio]);
|
|
14054
|
-
const connectWebsocket = reactExports.useCallback(() => {
|
|
14144
|
+
const connectWebsocket = reactExports.useCallback(async () => {
|
|
14055
14145
|
if (typeof window === "undefined" || !websocketUrl) {
|
|
14056
14146
|
return;
|
|
14057
14147
|
}
|
|
@@ -14060,8 +14150,46 @@
|
|
|
14060
14150
|
}
|
|
14061
14151
|
setVoiceStatus("connecting");
|
|
14062
14152
|
setVoiceError(null);
|
|
14153
|
+
|
|
14154
|
+
// EV-4119: the voice socket now requires a ticket minted by the backend
|
|
14155
|
+
// behind verified widget auth. Previously the org id in the URL path WAS
|
|
14156
|
+
// the credential - and org ids are public, since this widget ships one
|
|
14157
|
+
// into the host page - so anyone could hold a voice session as any org.
|
|
14158
|
+
//
|
|
14159
|
+
// A browser cannot set headers on a WebSocket handshake (the same reason
|
|
14160
|
+
// the SSE route above takes `?token=`), so the ticket travels in the
|
|
14161
|
+
// query string. It is short-lived and bound to this session id, so it is
|
|
14162
|
+
// fetched per connect rather than cached.
|
|
14163
|
+
let voiceTicket;
|
|
14164
|
+
try {
|
|
14165
|
+
const hdrs = {
|
|
14166
|
+
'Content-Type': 'application/json'
|
|
14167
|
+
};
|
|
14168
|
+
if (typeof getAuthToken === 'function') {
|
|
14169
|
+
const token = await getAuthToken();
|
|
14170
|
+
if (token) hdrs['Authorization'] = `Bearer ${token}`;
|
|
14171
|
+
}
|
|
14172
|
+
const res = await fetch(`${apiBaseUrl.replace(/\/$/, '')}/api/v1/widget/voice-ticket`, {
|
|
14173
|
+
method: 'POST',
|
|
14174
|
+
headers: hdrs,
|
|
14175
|
+
credentials: 'include',
|
|
14176
|
+
body: JSON.stringify({
|
|
14177
|
+
sessionId: bidiSessionId
|
|
14178
|
+
})
|
|
14179
|
+
});
|
|
14180
|
+
if (!res.ok) throw new Error(`voice-ticket ${res.status}`);
|
|
14181
|
+
voiceTicket = (await res.json())?.ticket;
|
|
14182
|
+
if (!voiceTicket) throw new Error('voice-ticket returned no ticket');
|
|
14183
|
+
} catch (err) {
|
|
14184
|
+
// Surface through the existing voice error taxonomy rather than
|
|
14185
|
+
// half-opening a socket the server is going to refuse anyway.
|
|
14186
|
+
console.error('[BIDI] could not obtain a voice session ticket', err);
|
|
14187
|
+
setVoiceStatus('error');
|
|
14188
|
+
setVoiceError(voiceErrorCode(err));
|
|
14189
|
+
return;
|
|
14190
|
+
}
|
|
14063
14191
|
try {
|
|
14064
|
-
const ws = new WebSocket(websocketUrl);
|
|
14192
|
+
const ws = new WebSocket(`${websocketUrl}?ticket=${encodeURIComponent(voiceTicket)}`);
|
|
14065
14193
|
websocketRef.current = ws;
|
|
14066
14194
|
ws.onopen = () => {
|
|
14067
14195
|
setVoiceStatus("connected");
|
|
@@ -14110,7 +14238,7 @@
|
|
|
14110
14238
|
setVoiceStatus("error");
|
|
14111
14239
|
setVoiceError(voiceErrorCode(error));
|
|
14112
14240
|
}
|
|
14113
|
-
}, [handleBidiEvent, websocketUrl]);
|
|
14241
|
+
}, [handleBidiEvent, websocketUrl, apiBaseUrl, bidiSessionId, getAuthToken]);
|
|
14114
14242
|
const startVoiceSession = reactExports.useCallback(async () => {
|
|
14115
14243
|
if (!enableVoiceInteraction || !organizationId) {
|
|
14116
14244
|
return;
|
|
@@ -14262,6 +14390,10 @@
|
|
|
14262
14390
|
isHandedOffRef.current = false;
|
|
14263
14391
|
setIsHandedOff(false);
|
|
14264
14392
|
serverHistoryFetchedRef.current = null;
|
|
14393
|
+
// EV-4047: the backfill watermark belongs to the OLD conversation. Left
|
|
14394
|
+
// set, the new session's first `onopen` would ask for messages after a
|
|
14395
|
+
// timestamp from a conversation it has nothing to do with.
|
|
14396
|
+
lastAgentMessageAtRef.current = null;
|
|
14265
14397
|
if (handoffEventSourceRef.current) {
|
|
14266
14398
|
handoffEventSourceRef.current.close();
|
|
14267
14399
|
handoffEventSourceRef.current = null;
|
|
@@ -18270,7 +18402,14 @@
|
|
|
18270
18402
|
const handleKeyPress = event => {
|
|
18271
18403
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
18272
18404
|
event.preventDefault();
|
|
18273
|
-
|
|
18405
|
+
// EV-4179: the voice WS only carries text, so a file attached during a
|
|
18406
|
+
// live voice session can't ride it. When there's a ready attachment we
|
|
18407
|
+
// fall through to the normal text send (sendWithAttachments) instead,
|
|
18408
|
+
// which POSTs to /support with the SAME conversationId as the voice
|
|
18409
|
+
// session - so the file is ingested into the shared session store and
|
|
18410
|
+
// answered, and later spoken turns in the same session see it too. A
|
|
18411
|
+
// plain voice turn (no attachment) still goes over the WS.
|
|
18412
|
+
if (isVoiceSessionActive && onBidiSubmit && !hasReadyAttachment) {
|
|
18274
18413
|
onBidiSubmit();
|
|
18275
18414
|
return;
|
|
18276
18415
|
}
|
|
@@ -18652,7 +18791,7 @@
|
|
|
18652
18791
|
fill: "currentColor"
|
|
18653
18792
|
})
|
|
18654
18793
|
}) : /*#__PURE__*/jsxRuntimeExports.jsx("button", {
|
|
18655
|
-
onClick: isVoiceSessionActive && onBidiSubmit ? onBidiSubmit : sendWithAttachments,
|
|
18794
|
+
onClick: isVoiceSessionActive && onBidiSubmit && !hasReadyAttachment ? onBidiSubmit : sendWithAttachments,
|
|
18656
18795
|
disabled: csatVisible || isDisabled || isVoiceSessionActive && voiceStatus !== "connected",
|
|
18657
18796
|
className: `h-10 w-10 flex-shrink-0 rounded-xl transition-all duration-200 flex items-center justify-center shadow-md disabled:opacity-40 disabled:cursor-not-allowed disabled:shadow-none ${inputValue.trim() ? "scale-100" : "scale-95"}`,
|
|
18658
18797
|
style: {
|
|
@@ -20990,7 +21129,7 @@
|
|
|
20990
21129
|
* value by its own test (that constant sat at 0.1.113 while this package was
|
|
20991
21130
|
* at 0.2.2 precisely because nothing enforced it - EV-2431).
|
|
20992
21131
|
*/
|
|
20993
|
-
const WIDGET_VERSION = '0.2.
|
|
21132
|
+
const WIDGET_VERSION = '0.2.12';
|
|
20994
21133
|
|
|
20995
21134
|
/**
|
|
20996
21135
|
* API utility functions
|
|
@@ -21209,6 +21348,12 @@
|
|
|
21209
21348
|
const enBundle = messages.bundles?.["en"] ?? {};
|
|
21210
21349
|
const bundle = {
|
|
21211
21350
|
welcome: localeBundle.welcome || enBundle.welcome,
|
|
21351
|
+
// Same per-field English fallback as `welcome` above, for the same
|
|
21352
|
+
// reason. Read so useChatState can show the org's own words when a turn
|
|
21353
|
+
// dies before producing a token, instead of the generic connection
|
|
21354
|
+
// error: the customer is looking at a branded assistant, and that
|
|
21355
|
+
// sentence is the one thing on screen that is not theirs.
|
|
21356
|
+
fallback: localeBundle.fallback || enBundle.fallback,
|
|
21212
21357
|
quickQuestions: Array.isArray(localeBundle.quickQuestions) && localeBundle.quickQuestions.length > 0 ? localeBundle.quickQuestions : enBundle.quickQuestions
|
|
21213
21358
|
};
|
|
21214
21359
|
|
|
@@ -21225,6 +21370,10 @@
|
|
|
21225
21370
|
chatbotAvatarEnabled: branding.chatbotAvatarEnabled ?? false,
|
|
21226
21371
|
chatbotAvatar: branding.chatbotAvatar ?? null,
|
|
21227
21372
|
welcomeMessage: welcomeMessage ?? bundle.welcome ?? "Hi! How can we help?",
|
|
21373
|
+
// No literal default: an org that has not configured a fallback keeps
|
|
21374
|
+
// the widget's localised connection-error copy (t("connectionError")),
|
|
21375
|
+
// which is the pre-existing behaviour.
|
|
21376
|
+
fallbackMessage: bundle.fallback,
|
|
21228
21377
|
quickQuestions: quickQuestions ?? bundle.quickQuestions ?? [],
|
|
21229
21378
|
quickQuestionsEnabled: quickQuestionsEnabled ?? messages.quickQuestionsEnabled,
|
|
21230
21379
|
textColor: textColor ?? "#000000",
|
|
@@ -21364,6 +21513,9 @@
|
|
|
21364
21513
|
addAssistantMessage
|
|
21365
21514
|
} = useChatState(widgetConfig ? {
|
|
21366
21515
|
welcomeMessage: widgetConfig.welcomeMessage,
|
|
21516
|
+
// Per the EV-3813 note below, this hook produces customer-facing
|
|
21517
|
+
// failure copy, so it gets the org's configured fallback too.
|
|
21518
|
+
fallbackMessage: widgetConfig.fallbackMessage,
|
|
21367
21519
|
quickQuestions: widgetConfig.quickQuestions,
|
|
21368
21520
|
apiBaseUrl,
|
|
21369
21521
|
wsBaseUrl: wsBaseUrl ?? agentConfig?.wsBaseUrl,
|