@burdenoff/website-sdk 2026.922.5 → 2026.923.2

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,5 +1,5 @@
1
- export { D as DEFAULT_EXPLORE_EXAMPLE_PROMPTS, E as ExploreChatError, a as ExploreChatErrorKind, b as ExploreChatPhase, F as FALLBACK_EXPLORE_LIMITS, U as UseExploreChatOptions, c as UseExploreChatResult, d as UseSpeechOutputOptions, e as UseSpeechOutputResult, f as classifyExploreError, s as stopSpeaking, u as useExploreChat, g as useProgressiveReveal, h as useSpeechOutput } from '../use-progressive-reveal-Ha3gYPfs.mjs';
2
- import '../explore-types-DvQPiL1t.mjs';
1
+ export { D as DEFAULT_EXPLORE_EXAMPLE_PROMPTS, E as ExploreChatError, a as ExploreChatErrorKind, b as ExploreChatPhase, c as ExploreEscalateOutcome, d as ExploreEscalateResult, F as FALLBACK_EXPLORE_LIMITS, U as UseExploreChatOptions, e as UseExploreChatResult, f as UseSpeechOutputOptions, g as UseSpeechOutputResult, h as classifyExploreError, s as stopSpeaking, u as useExploreChat, i as useProgressiveReveal, j as useSpeechOutput } from '../use-progressive-reveal-Cmy8bA7L.mjs';
2
+ import '../explore-types-B5sdOC5M.mjs';
3
3
 
4
4
  interface UseSpeechInputOptions {
5
5
  /** Receives settled text as it is recognised. Append it to the draft. */
@@ -1,5 +1,5 @@
1
- export { D as DEFAULT_EXPLORE_EXAMPLE_PROMPTS, E as ExploreChatError, a as ExploreChatErrorKind, b as ExploreChatPhase, F as FALLBACK_EXPLORE_LIMITS, U as UseExploreChatOptions, c as UseExploreChatResult, d as UseSpeechOutputOptions, e as UseSpeechOutputResult, f as classifyExploreError, s as stopSpeaking, u as useExploreChat, g as useProgressiveReveal, h as useSpeechOutput } from '../use-progressive-reveal-BfU_IHip.js';
2
- import '../explore-types-DvQPiL1t.js';
1
+ export { D as DEFAULT_EXPLORE_EXAMPLE_PROMPTS, E as ExploreChatError, a as ExploreChatErrorKind, b as ExploreChatPhase, c as ExploreEscalateOutcome, d as ExploreEscalateResult, F as FALLBACK_EXPLORE_LIMITS, U as UseExploreChatOptions, e as UseExploreChatResult, f as UseSpeechOutputOptions, g as UseSpeechOutputResult, h as classifyExploreError, s as stopSpeaking, u as useExploreChat, i as useProgressiveReveal, j as useSpeechOutput } from '../use-progressive-reveal-DHPGGxZu.js';
2
+ import '../explore-types-B5sdOC5M.js';
3
3
 
4
4
  interface UseSpeechInputOptions {
5
5
  /** Receives settled text as it is recognised. Append it to the draft. */
@@ -226,6 +226,10 @@ function useWebSDK() {
226
226
  }
227
227
  return client;
228
228
  }
229
+ function useWebSDKConfig() {
230
+ const client = useWebSDK();
231
+ return client.getConfig();
232
+ }
229
233
 
230
234
  // src/components/explore-types.ts
231
235
  var EXPLORE_MESSAGE_FIELDS = (
@@ -385,6 +389,22 @@ var SUBMIT_EXPLORE_FEEDBACK_MUTATION = (
385
389
  }
386
390
  `
387
391
  );
392
+ var ESCALATE_EXPLORE_CONVERSATION_MUTATION = (
393
+ /* GraphQL */
394
+ `
395
+ mutation EscalateExploreConversation(
396
+ $input: EscalateExploreConversationInput!
397
+ ) {
398
+ escalateExploreConversation(input: $input) {
399
+ created
400
+ escalation {
401
+ email
402
+ createdAt
403
+ }
404
+ }
405
+ }
406
+ `
407
+ );
388
408
  var CREATE_EXPLORE_SHARE_LINK_MUTATION = (
389
409
  /* GraphQL */
390
410
  `
@@ -751,6 +771,25 @@ var VALIDATION_CODES = /* @__PURE__ */ new Set([
751
771
  "CONVERSATION_LIMIT",
752
772
  "INVALID_INPUT"
753
773
  ]);
774
+ function escalateOutcomeForCode(code) {
775
+ switch (code) {
776
+ case "INVALID_EMAIL":
777
+ return "invalid-email";
778
+ case "RATE_LIMITED":
779
+ case "HTTP_429":
780
+ return "rate-limited";
781
+ case "CAPTCHA_REQUIRED":
782
+ case "CAPTCHA_FAILED":
783
+ return "captcha";
784
+ // The token owns nothing, or owns a different conversation. The server answers NOT_FOUND
785
+ // rather than FORBIDDEN so the endpoint is not an oracle for whether a thread exists;
786
+ // either way there is nothing this visitor can do about it, so it reads as a failure.
787
+ case "NOT_FOUND":
788
+ return "failed";
789
+ default:
790
+ return "failed";
791
+ }
792
+ }
754
793
  function classifyExploreError(code, serverMessage) {
755
794
  const c = code ?? "UNKNOWN";
756
795
  if (RATE_LIMIT_CODES.has(c)) {
@@ -917,6 +956,7 @@ function useExploreChat(options = {}) {
917
956
  onSend
918
957
  } = options;
919
958
  const client = useWebSDK();
959
+ const { productId } = useWebSDKConfig();
920
960
  const promptKey = JSON.stringify(examplePrompts ?? []);
921
961
  const welcomeKey = JSON.stringify(fallbackWelcome ?? null);
922
962
  const fallbackCatalog = react.useMemo(
@@ -940,6 +980,7 @@ function useExploreChat(options = {}) {
940
980
  const pollGenerationRef = react.useRef(0);
941
981
  const pollTimerRef = react.useRef(null);
942
982
  const conversationTokenRef = react.useRef(null);
983
+ const [hasConversation, setHasConversation] = react.useState(false);
943
984
  const shareTokenRef = react.useRef(void 0);
944
985
  if (shareTokenRef.current === void 0) {
945
986
  shareTokenRef.current = readShareToken();
@@ -954,6 +995,10 @@ function useExploreChat(options = {}) {
954
995
  );
955
996
  const inFlightRef = react.useRef(false);
956
997
  catalogRef.current = catalog;
998
+ const rememberConversationToken = react.useCallback((token) => {
999
+ conversationTokenRef.current = token;
1000
+ setHasConversation(token !== null);
1001
+ }, []);
957
1002
  const clearPollTimer = react.useCallback(() => {
958
1003
  if (pollTimerRef.current !== null) {
959
1004
  clearTimeout(pollTimerRef.current);
@@ -1020,7 +1065,7 @@ function useExploreChat(options = {}) {
1020
1065
  setArchive(readArchive(storageKey));
1021
1066
  const stored = readPersisted(storageKey);
1022
1067
  if (stored) {
1023
- conversationTokenRef.current = stored.conversationToken;
1068
+ rememberConversationToken(stored.conversationToken);
1024
1069
  focusProductRef.current = stored.focusProduct;
1025
1070
  const settled = stored.messages.flatMap((m) => {
1026
1071
  if (m.role !== "ASSISTANT") return [m];
@@ -1036,7 +1081,7 @@ function useExploreChat(options = {}) {
1036
1081
  setTurnCount(turns);
1037
1082
  }
1038
1083
  setHydrated(true);
1039
- }, [persist, storageKey]);
1084
+ }, [persist, rememberConversationToken, storageKey]);
1040
1085
  react.useEffect(() => {
1041
1086
  if (!persist || !hydrated || typeof window === "undefined") return;
1042
1087
  try {
@@ -1229,7 +1274,7 @@ function useExploreChat(options = {}) {
1229
1274
  setPhase(classified.kind === "rate-limit" ? "limited" : "error");
1230
1275
  return;
1231
1276
  }
1232
- conversationTokenRef.current = result.conversation.token;
1277
+ rememberConversationToken(result.conversation.token);
1233
1278
  turnCountRef.current = result.conversation.turnCount;
1234
1279
  setTurnCount(result.conversation.turnCount);
1235
1280
  setRemainingToday(result.remainingToday ?? null);
@@ -1255,7 +1300,14 @@ function useExploreChat(options = {}) {
1255
1300
  setPhase("streaming");
1256
1301
  startPolling(result.conversation.token, assistantMessage.id);
1257
1302
  },
1258
- [client, locale, pageUrl, productSlug, startPolling]
1303
+ [
1304
+ client,
1305
+ locale,
1306
+ pageUrl,
1307
+ productSlug,
1308
+ rememberConversationToken,
1309
+ startPolling
1310
+ ]
1259
1311
  );
1260
1312
  const send = react.useCallback(
1261
1313
  async (message, attachments = []) => {
@@ -1372,7 +1424,7 @@ function useExploreChat(options = {}) {
1372
1424
  );
1373
1425
  cancelPolling();
1374
1426
  inFlightRef.current = false;
1375
- conversationTokenRef.current = null;
1427
+ rememberConversationToken(null);
1376
1428
  lastSentTextRef.current = null;
1377
1429
  lastSentAttachmentsRef.current = [];
1378
1430
  turnCountRef.current = 0;
@@ -1387,7 +1439,7 @@ function useExploreChat(options = {}) {
1387
1439
  } catch {
1388
1440
  }
1389
1441
  }
1390
- }, [cancelPolling, storageKey]);
1442
+ }, [cancelPolling, rememberConversationToken, storageKey]);
1391
1443
  const openConversation = react.useCallback(
1392
1444
  (token) => {
1393
1445
  const entry = readArchive(storageKey).find((e) => e.token === token);
@@ -1399,7 +1451,7 @@ function useExploreChat(options = {}) {
1399
1451
  });
1400
1452
  cancelPolling();
1401
1453
  inFlightRef.current = false;
1402
- conversationTokenRef.current = entry.token;
1454
+ rememberConversationToken(entry.token);
1403
1455
  turnCountRef.current = entry.messages.filter(
1404
1456
  (m) => m.role === "USER"
1405
1457
  ).length;
@@ -1411,7 +1463,7 @@ function useExploreChat(options = {}) {
1411
1463
  setPhase(catalogRef.current.enabled ? "ready" : "disabled");
1412
1464
  setArchive(readArchive(storageKey));
1413
1465
  },
1414
- [cancelPolling, storageKey]
1466
+ [cancelPolling, rememberConversationToken, storageKey]
1415
1467
  );
1416
1468
  const deleteConversation = react.useCallback(
1417
1469
  (token) => {
@@ -1500,6 +1552,69 @@ function useExploreChat(options = {}) {
1500
1552
  },
1501
1553
  [client]
1502
1554
  );
1555
+ const escalate = react.useCallback(
1556
+ async (email, note) => {
1557
+ const token = conversationTokenRef.current;
1558
+ if (!token) {
1559
+ return { outcome: "no-conversation", email: null, createdAt: null };
1560
+ }
1561
+ const attempt = async (captchaAttempt) => {
1562
+ const activeCatalog = catalogRef.current;
1563
+ let recaptchaToken;
1564
+ if (activeCatalog.captchaRequired && activeCatalog.recaptchaSiteKey) {
1565
+ try {
1566
+ recaptchaToken = await getRecaptchaV3Token(
1567
+ activeCatalog.recaptchaSiteKey
1568
+ );
1569
+ } catch (err) {
1570
+ console.warn(
1571
+ "[WebSDK] reCAPTCHA v3 token could not be minted",
1572
+ err
1573
+ );
1574
+ }
1575
+ }
1576
+ const input = {
1577
+ conversationToken: token,
1578
+ email: email.trim(),
1579
+ note: note?.trim() ? note.trim() : null,
1580
+ ...recaptchaToken ? { recaptchaToken } : {},
1581
+ ...productId ? { productId } : {}
1582
+ };
1583
+ try {
1584
+ const res = await client.mutate(
1585
+ ESCALATE_EXPLORE_CONVERSATION_MUTATION,
1586
+ { input }
1587
+ );
1588
+ const first = res?.errors?.[0];
1589
+ const result = res?.data?.escalateExploreConversation;
1590
+ if (first || !result) {
1591
+ const code = typeof first?.extensions?.code === "string" ? first.extensions.code : "ESCALATION_FAILED";
1592
+ if (code === "CAPTCHA_FAILED" && captchaAttempt === 0) {
1593
+ return attempt(1);
1594
+ }
1595
+ return {
1596
+ outcome: escalateOutcomeForCode(code),
1597
+ email: null,
1598
+ createdAt: null
1599
+ };
1600
+ }
1601
+ return {
1602
+ // `created: false` is the double-click, the retry, the reload. The handover
1603
+ // exists, so this is a success — and the address comes from the SERVER, because
1604
+ // on a repeat submit the reply goes to whatever the FIRST handover used.
1605
+ outcome: result.created ? "sent" : "already-handed-over",
1606
+ email: result.escalation?.email ?? null,
1607
+ createdAt: result.escalation?.createdAt ?? null
1608
+ };
1609
+ } catch {
1610
+ return { outcome: "failed", email: null, createdAt: null };
1611
+ }
1612
+ };
1613
+ return attempt(0);
1614
+ },
1615
+ [client, productId]
1616
+ );
1617
+ const canEscalate = hasConversation && !isSharedView;
1503
1618
  return {
1504
1619
  phase,
1505
1620
  catalog,
@@ -1520,6 +1635,8 @@ function useExploreChat(options = {}) {
1520
1635
  clearHistory,
1521
1636
  recordClick,
1522
1637
  submitFeedback,
1638
+ escalate,
1639
+ canEscalate,
1523
1640
  createShareLink,
1524
1641
  isSharedView
1525
1642
  };