@eshal-bot/chat-widget 0.2.19 → 0.2.21
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-D50nDQ4t.esm.js → chat-widget-MarkdownRenderer-C2X2GCO6.esm.js} +1 -1
- package/dist/chat-widget-index-BcyXZWaW.esm.js +20 -0
- package/dist/chat-widget.esm.js +1 -1
- package/dist/chat-widget.js +139 -10
- package/dist/chat-widget.min.js +4 -4
- package/dist/chat-widget.umd.js +8 -8
- package/package.json +2 -2
- package/dist/chat-widget-index-ClEAcv15.esm.js +0 -20
package/dist/chat-widget.esm.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{e as default}from"./chat-widget-index-
|
|
1
|
+
export{e as default}from"./chat-widget-index-BcyXZWaW.esm.js";import"react";import"react-dom/client";import"react/jsx-runtime";import"react-dom";
|
package/dist/chat-widget.js
CHANGED
|
@@ -7955,6 +7955,22 @@
|
|
|
7955
7955
|
}
|
|
7956
7956
|
}
|
|
7957
7957
|
|
|
7958
|
+
/** HTTP status the backend answers for an erased conversation. */
|
|
7959
|
+
const CONVERSATION_ERASED_STATUS = 410;
|
|
7960
|
+
|
|
7961
|
+
/**
|
|
7962
|
+
* Thrown when the conversation this widget holds was erased in the console
|
|
7963
|
+
* ("Erase permanently", on the conversation or its contact). The backend
|
|
7964
|
+
* answers every widget call on it with 410 `conversation_erased`; the caller
|
|
7965
|
+
* starts a new conversation instead of treating it as a failed send.
|
|
7966
|
+
*/
|
|
7967
|
+
class ConversationErasedError extends Error {
|
|
7968
|
+
constructor(message = 'This conversation was erased') {
|
|
7969
|
+
super(message);
|
|
7970
|
+
this.name = 'ConversationErasedError';
|
|
7971
|
+
}
|
|
7972
|
+
}
|
|
7973
|
+
|
|
7958
7974
|
// ============================================================================
|
|
7959
7975
|
// NETWORK
|
|
7960
7976
|
// ============================================================================
|
|
@@ -7991,6 +8007,9 @@
|
|
|
7991
8007
|
signal
|
|
7992
8008
|
} : {})
|
|
7993
8009
|
});
|
|
8010
|
+
if (response.status === CONVERSATION_ERASED_STATUS) {
|
|
8011
|
+
throw new ConversationErasedError();
|
|
8012
|
+
}
|
|
7994
8013
|
if (!response.ok) {
|
|
7995
8014
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
7996
8015
|
}
|
|
@@ -8372,7 +8391,13 @@
|
|
|
8372
8391
|
stale: s.stale
|
|
8373
8392
|
})).filter(s => s.source_id || s.url || s.source_type === 'database') : [];
|
|
8374
8393
|
};
|
|
8375
|
-
|
|
8394
|
+
|
|
8395
|
+
/**
|
|
8396
|
+
* @param {Function} [onHumanHandoff] - called when the server answers with the
|
|
8397
|
+
* human_handoff marker: a person owns the conversation, so the caller starts
|
|
8398
|
+
* listening for their replies (and anything else pushed to the widget).
|
|
8399
|
+
*/
|
|
8400
|
+
const processStream = async (stream, setMessages, onTerminal, onHumanHandoff) => {
|
|
8376
8401
|
const updateMessage = createMessageUpdater(setMessages);
|
|
8377
8402
|
const reader = stream.getReader();
|
|
8378
8403
|
const decoder = new TextDecoder();
|
|
@@ -8490,6 +8515,7 @@
|
|
|
8490
8515
|
// human agent's reply arrives via the SSE EventSource instead.
|
|
8491
8516
|
if (parsed.type === 'human_handoff') {
|
|
8492
8517
|
markTerminal();
|
|
8518
|
+
if (typeof onHumanHandoff === 'function') onHumanHandoff();
|
|
8493
8519
|
continue;
|
|
8494
8520
|
}
|
|
8495
8521
|
|
|
@@ -12242,6 +12268,16 @@
|
|
|
12242
12268
|
return generateWidgetSessionId();
|
|
12243
12269
|
});
|
|
12244
12270
|
|
|
12271
|
+
// Erase permanently (console): the conversation this widget holds was
|
|
12272
|
+
// erased, and every widget call on it now answers 410 conversation_erased.
|
|
12273
|
+
// Set below once resetConversation exists; the history fetch, the live
|
|
12274
|
+
// stream, the hand-off and sendMessage all call it to start a new
|
|
12275
|
+
// conversation instead of showing or reviving the erased one.
|
|
12276
|
+
const conversationErasedRef = reactExports.useRef(() => {});
|
|
12277
|
+
// The visitor's message whose send was refused because the conversation
|
|
12278
|
+
// was erased. Re-sent on the new conversation once it exists.
|
|
12279
|
+
const pendingResendRef = reactExports.useRef(null);
|
|
12280
|
+
|
|
12245
12281
|
// Handoff EventSource - receives human agent messages in real-time
|
|
12246
12282
|
const handoffEventSourceRef = reactExports.useRef(null);
|
|
12247
12283
|
const HANDOFF_TRIGGER = "Connect with a human representative";
|
|
@@ -12252,6 +12288,11 @@
|
|
|
12252
12288
|
// over the SSE EventSource instead. The ref mirrors the state for a
|
|
12253
12289
|
// synchronous read inside sendMessage (whose useCallback deps are fixed).
|
|
12254
12290
|
const [isHandedOff, setIsHandedOff] = reactExports.useState(false);
|
|
12291
|
+
// A rating an agent asked for from the Inbox (backend
|
|
12292
|
+
// services/csatRequest.ts): { format, prompt, requestedAt }, or null. Set by
|
|
12293
|
+
// the live csat_request event, or by /history when the widget was not
|
|
12294
|
+
// listening at the time; the chat window shows its rating card for it.
|
|
12295
|
+
const [csatRequest, setCsatRequest] = reactExports.useState(null);
|
|
12255
12296
|
const isHandedOffRef = reactExports.useRef(false);
|
|
12256
12297
|
const markHandedOff = reactExports.useCallback(() => {
|
|
12257
12298
|
isHandedOffRef.current = true;
|
|
@@ -12413,6 +12454,10 @@
|
|
|
12413
12454
|
headers,
|
|
12414
12455
|
credentials: 'include'
|
|
12415
12456
|
});
|
|
12457
|
+
if (res.status === CONVERSATION_ERASED_STATUS) {
|
|
12458
|
+
conversationErasedRef.current(null);
|
|
12459
|
+
return;
|
|
12460
|
+
}
|
|
12416
12461
|
if (!res.ok) return;
|
|
12417
12462
|
const data = await res.json();
|
|
12418
12463
|
const serverMsgs = Array.isArray(data?.messages) ? data.messages : [];
|
|
@@ -12449,6 +12494,18 @@
|
|
|
12449
12494
|
es.onmessage = event => {
|
|
12450
12495
|
try {
|
|
12451
12496
|
const data = JSON.parse(event.data);
|
|
12497
|
+
// Erased in the console while open: stop listening and start over.
|
|
12498
|
+
// An agent asked this visitor for a rating.
|
|
12499
|
+
if (data.type === 'csat_request' && data.csat) {
|
|
12500
|
+
setCsatRequest(data.csat);
|
|
12501
|
+
return;
|
|
12502
|
+
}
|
|
12503
|
+
if (data.type === 'conversation_erased') {
|
|
12504
|
+
es.close();
|
|
12505
|
+
if (handoffEventSourceRef.current === es) handoffEventSourceRef.current = null;
|
|
12506
|
+
conversationErasedRef.current(null);
|
|
12507
|
+
return;
|
|
12508
|
+
}
|
|
12452
12509
|
if (data.type === 'message_created' && data.message) {
|
|
12453
12510
|
// Advance the backfill watermark so a later reconnect doesn't
|
|
12454
12511
|
// re-request everything we have already rendered live.
|
|
@@ -12729,10 +12786,18 @@
|
|
|
12729
12786
|
headers,
|
|
12730
12787
|
credentials: 'include'
|
|
12731
12788
|
});
|
|
12732
|
-
if (
|
|
12789
|
+
if (cancelled) return;
|
|
12790
|
+
// Erased in the console: the saved transcript must not be shown.
|
|
12791
|
+
if (res.status === CONVERSATION_ERASED_STATUS) {
|
|
12792
|
+
conversationErasedRef.current(null);
|
|
12793
|
+
return;
|
|
12794
|
+
}
|
|
12795
|
+
if (!res.ok) return;
|
|
12733
12796
|
const data = await res.json();
|
|
12734
12797
|
const serverMsgs = Array.isArray(data?.messages) ? data.messages : [];
|
|
12735
12798
|
const agentMsgs = serverMsgs.filter(isRepliedOutsideStream);
|
|
12799
|
+
// An open rating request sent while this widget was not listening.
|
|
12800
|
+
if (!cancelled && data?.csatRequest) setCsatRequest(data.csatRequest);
|
|
12736
12801
|
|
|
12737
12802
|
// Merge past human-agent replies into the transcript (regardless of
|
|
12738
12803
|
// current responder - they're real history worth showing). Shares
|
|
@@ -12942,6 +13007,10 @@
|
|
|
12942
13007
|
body: JSON.stringify({
|
|
12943
13008
|
reason: 'User clicked handoff button'
|
|
12944
13009
|
})
|
|
13010
|
+
}).then(res => {
|
|
13011
|
+
// Erased: nobody to hand off to. Start a new conversation,
|
|
13012
|
+
// where the visitor can ask for a person again.
|
|
13013
|
+
if (res.status === CONVERSATION_ERASED_STATUS) conversationErasedRef.current(null);
|
|
12945
13014
|
}).catch(() => {});
|
|
12946
13015
|
}
|
|
12947
13016
|
};
|
|
@@ -13184,6 +13253,8 @@
|
|
|
13184
13253
|
if (done) break;
|
|
13185
13254
|
}
|
|
13186
13255
|
} catch (err) {
|
|
13256
|
+
// Erased: handled once, below, like any other send.
|
|
13257
|
+
if (err instanceof ConversationErasedError) throw err;
|
|
13187
13258
|
// L4: this catch used to swallow the failure - but in a handed-off
|
|
13188
13259
|
// conversation this POST is the ONLY path to the human agent, so a
|
|
13189
13260
|
// dropped send meant the customer's message never reached anyone
|
|
@@ -13217,7 +13288,14 @@
|
|
|
13217
13288
|
// it unable to type their next question. Retries are unaffected:
|
|
13218
13289
|
// IncompleteStreamError is only thrown when NO terminal event
|
|
13219
13290
|
// arrived, so nothing that reaches this callback is ever retried.
|
|
13220
|
-
await processStream(stream, setMessages, releaseComposer)
|
|
13291
|
+
await processStream(stream, setMessages, releaseComposer, () => {
|
|
13292
|
+
// The server says a person owns this conversation (an agent
|
|
13293
|
+
// took it over from the Inbox): listen for their replies, as
|
|
13294
|
+
// the hand-off path already expects, with EV-4047's backfill
|
|
13295
|
+
// catching anything sent before the stream opened.
|
|
13296
|
+
markHandedOff();
|
|
13297
|
+
void startHandoffEventSource();
|
|
13298
|
+
});
|
|
13221
13299
|
setUserMessageStatus(sentUserMessageId, "sent");
|
|
13222
13300
|
updateActivity(organizationId);
|
|
13223
13301
|
return {
|
|
@@ -13265,6 +13343,21 @@
|
|
|
13265
13343
|
success: true
|
|
13266
13344
|
};
|
|
13267
13345
|
} catch (error) {
|
|
13346
|
+
// Erased in the console: not a failed send. Start a new conversation
|
|
13347
|
+
// and send this message again there, with no error bubble.
|
|
13348
|
+
if (error instanceof ConversationErasedError) {
|
|
13349
|
+
const isChat = apiType === 'inquiry' || apiType === 'complaint' || apiType === 'technical';
|
|
13350
|
+
conversationErasedRef.current(isChat ? {
|
|
13351
|
+
text: trimmed,
|
|
13352
|
+
attachments: attachmentsPayload,
|
|
13353
|
+
sessionId: bidiSessionId
|
|
13354
|
+
} : null);
|
|
13355
|
+
return {
|
|
13356
|
+
success: false,
|
|
13357
|
+
error: 'conversation_erased',
|
|
13358
|
+
erased: true
|
|
13359
|
+
};
|
|
13360
|
+
}
|
|
13268
13361
|
console.error("Error sending message:", error);
|
|
13269
13362
|
if (apiType === 'inquiry' || apiType === 'complaint' || apiType === 'technical') {
|
|
13270
13363
|
setIsLoading(false);
|
|
@@ -13317,6 +13410,16 @@
|
|
|
13317
13410
|
// bubble first, then routes back through sendMessage so the retry gets the
|
|
13318
13411
|
// full pipeline (concurrency guard, stream retries, delivery state) instead
|
|
13319
13412
|
// of a bespoke resend path.
|
|
13413
|
+
// A send refused because the conversation was erased goes out again on the
|
|
13414
|
+
// new conversation, once the reset has rotated the session id.
|
|
13415
|
+
reactExports.useEffect(() => {
|
|
13416
|
+
const pending = pendingResendRef.current;
|
|
13417
|
+
if (!pending || pending.sessionId === bidiSessionId) return;
|
|
13418
|
+
pendingResendRef.current = null;
|
|
13419
|
+
void sendMessage(pending.text, undefined, {
|
|
13420
|
+
attachments: pending.attachments
|
|
13421
|
+
});
|
|
13422
|
+
}, [bidiSessionId, sendMessage]);
|
|
13320
13423
|
const retryMessage = reactExports.useCallback(failedMessage => {
|
|
13321
13424
|
if (!failedMessage || failedMessage.status !== 'failed') {
|
|
13322
13425
|
return Promise.resolve({
|
|
@@ -14618,6 +14721,8 @@
|
|
|
14618
14721
|
// Reset messages
|
|
14619
14722
|
setMessages([]);
|
|
14620
14723
|
setBidiMessages([]);
|
|
14724
|
+
// The rating request belonged to the conversation being left.
|
|
14725
|
+
setCsatRequest(null);
|
|
14621
14726
|
|
|
14622
14727
|
// Drop the localStorage transcript for the conversation we're abandoning
|
|
14623
14728
|
// BEFORE we rotate to a fresh sessionId, otherwise the old bucket leaks.
|
|
@@ -14688,6 +14793,22 @@
|
|
|
14688
14793
|
resetConversationRef.current = resetConversation;
|
|
14689
14794
|
}, [resetConversation]);
|
|
14690
14795
|
|
|
14796
|
+
// The conversation was erased in the console: drop the saved transcript and
|
|
14797
|
+
// start a new conversation, the same reset as after CSAT. Idempotent per
|
|
14798
|
+
// conversation id, since the history fetch, the stream and a send can all
|
|
14799
|
+
// learn it at once. `resend` is the visitor's refused message, sent again
|
|
14800
|
+
// on the new conversation unless onboarding has to run first.
|
|
14801
|
+
const erasedHandledForRef = reactExports.useRef(null);
|
|
14802
|
+
reactExports.useEffect(() => {
|
|
14803
|
+
conversationErasedRef.current = resend => {
|
|
14804
|
+
if (erasedHandledForRef.current === bidiSessionId) return;
|
|
14805
|
+
erasedHandledForRef.current = bidiSessionId;
|
|
14806
|
+
const needsOnboarding = onboardingEnabled && getOnboardingQuestions(onboardingQuestions).length > 0;
|
|
14807
|
+
pendingResendRef.current = resend && !needsOnboarding ? resend : null;
|
|
14808
|
+
resetConversationRef.current();
|
|
14809
|
+
};
|
|
14810
|
+
}, [bidiSessionId, onboardingEnabled, onboardingQuestions]);
|
|
14811
|
+
|
|
14691
14812
|
// Precise inactivity timeout: schedules a setTimeout for exact remaining time,
|
|
14692
14813
|
// then reschedules itself after each check (handles activity updates in localStorage).
|
|
14693
14814
|
//
|
|
@@ -14850,7 +14971,9 @@
|
|
|
14850
14971
|
// Session lifecycle
|
|
14851
14972
|
sessionExpiringSoon,
|
|
14852
14973
|
resetConversation,
|
|
14853
|
-
updateMessageFeedback
|
|
14974
|
+
updateMessageFeedback,
|
|
14975
|
+
// Rating request from the Inbox (null when none is open).
|
|
14976
|
+
csatRequest
|
|
14854
14977
|
};
|
|
14855
14978
|
};
|
|
14856
14979
|
|
|
@@ -20627,6 +20750,9 @@
|
|
|
20627
20750
|
csatEnabled = false,
|
|
20628
20751
|
csatFormat = "EMOJI",
|
|
20629
20752
|
csatPrompt = "How was your experience?",
|
|
20753
|
+
// A rating an agent asked for from the Inbox: { format, prompt } or null.
|
|
20754
|
+
// Shown whether or not the automatic survey (csatEnabled) is on.
|
|
20755
|
+
csatRequest = null,
|
|
20630
20756
|
onPromptSuggestionClick,
|
|
20631
20757
|
onboardingEnabled = false,
|
|
20632
20758
|
onResetConversation,
|
|
@@ -20941,7 +21067,7 @@
|
|
|
20941
21067
|
const handleCsatRating = async (rating, format) => {
|
|
20942
21068
|
setCsatSubmitted(true); // optimistic UI
|
|
20943
21069
|
setShowCsat(false);
|
|
20944
|
-
markCsatSubmitted(
|
|
21070
|
+
markCsatSubmitted(organizationId); // persist so page refresh doesn't re-show CSAT
|
|
20945
21071
|
|
|
20946
21072
|
try {
|
|
20947
21073
|
const headers = {
|
|
@@ -21419,11 +21545,12 @@
|
|
|
21419
21545
|
label: t("chooseTopicLabel")
|
|
21420
21546
|
}) : null,
|
|
21421
21547
|
quickQuestionsHeadingLabel: filterEnabled ? t("quickQuestionsHeadingLabel") : undefined
|
|
21422
|
-
}), showCsat && csatEnabled && !csatSubmitted && /*#__PURE__*/jsxRuntimeExports.jsx("div", {
|
|
21548
|
+
}), (showCsat && csatEnabled || csatRequest) && !csatSubmitted && /*#__PURE__*/jsxRuntimeExports.jsx("div", {
|
|
21423
21549
|
className: "",
|
|
21550
|
+
"data-testid": "widget-csat-card",
|
|
21424
21551
|
children: /*#__PURE__*/jsxRuntimeExports.jsx(CSATWidget, {
|
|
21425
|
-
format: normalizeCsatFormat(csatFormat),
|
|
21426
|
-
prompt: csatPrompt || t("howWasYourExperience"),
|
|
21552
|
+
format: normalizeCsatFormat(csatRequest?.format || csatFormat),
|
|
21553
|
+
prompt: csatRequest?.prompt || csatPrompt || t("howWasYourExperience"),
|
|
21427
21554
|
onRating: handleCsatRating,
|
|
21428
21555
|
disabled: false,
|
|
21429
21556
|
isDark: isDark,
|
|
@@ -21773,7 +21900,7 @@
|
|
|
21773
21900
|
* value by its own test (that constant sat at 0.1.113 while this package was
|
|
21774
21901
|
* at 0.2.2 precisely because nothing enforced it - EV-2431).
|
|
21775
21902
|
*/
|
|
21776
|
-
const WIDGET_VERSION = '0.2.
|
|
21903
|
+
const WIDGET_VERSION = '0.2.21';
|
|
21777
21904
|
|
|
21778
21905
|
/**
|
|
21779
21906
|
* API utility functions
|
|
@@ -22180,7 +22307,8 @@
|
|
|
22180
22307
|
// CategoryTabsBar / CategoryFilterInline components in ChatWindow.
|
|
22181
22308
|
widgetFilter,
|
|
22182
22309
|
setWidgetFilter,
|
|
22183
|
-
addAssistantMessage
|
|
22310
|
+
addAssistantMessage,
|
|
22311
|
+
csatRequest
|
|
22184
22312
|
} = useChatState(widgetConfig ? {
|
|
22185
22313
|
welcomeMessage: widgetConfig.welcomeMessage,
|
|
22186
22314
|
// Per the EV-3813 note below, this hook produces customer-facing
|
|
@@ -22403,6 +22531,7 @@
|
|
|
22403
22531
|
csatEnabled: widgetConfig.csatEnabled,
|
|
22404
22532
|
csatFormat: widgetConfig.csatFormat,
|
|
22405
22533
|
csatPrompt: widgetConfig.csatPrompt,
|
|
22534
|
+
csatRequest: csatRequest,
|
|
22406
22535
|
onboardingEnabled: widgetConfig.onboardingEnabled,
|
|
22407
22536
|
onResetConversation: resetConversation,
|
|
22408
22537
|
sessionExpiringSoon: sessionExpiringSoon,
|