@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.
@@ -1 +1 @@
1
- export{e as default}from"./chat-widget-index-ClEAcv15.esm.js";import"react";import"react-dom/client";import"react/jsx-runtime";import"react-dom";
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";
@@ -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
- const processStream = async (stream, setMessages, onTerminal) => {
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 (!res.ok || cancelled) return;
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(undefined); // persist so page refresh doesn't re-show CSAT
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.19';
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,