@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.
@@ -224,6 +224,10 @@ function useWebSDK() {
224
224
  }
225
225
  return client;
226
226
  }
227
+ function useWebSDKConfig() {
228
+ const client = useWebSDK();
229
+ return client.getConfig();
230
+ }
227
231
 
228
232
  // src/components/explore-types.ts
229
233
  var EXPLORE_MESSAGE_FIELDS = (
@@ -383,6 +387,22 @@ var SUBMIT_EXPLORE_FEEDBACK_MUTATION = (
383
387
  }
384
388
  `
385
389
  );
390
+ var ESCALATE_EXPLORE_CONVERSATION_MUTATION = (
391
+ /* GraphQL */
392
+ `
393
+ mutation EscalateExploreConversation(
394
+ $input: EscalateExploreConversationInput!
395
+ ) {
396
+ escalateExploreConversation(input: $input) {
397
+ created
398
+ escalation {
399
+ email
400
+ createdAt
401
+ }
402
+ }
403
+ }
404
+ `
405
+ );
386
406
  var CREATE_EXPLORE_SHARE_LINK_MUTATION = (
387
407
  /* GraphQL */
388
408
  `
@@ -749,6 +769,25 @@ var VALIDATION_CODES = /* @__PURE__ */ new Set([
749
769
  "CONVERSATION_LIMIT",
750
770
  "INVALID_INPUT"
751
771
  ]);
772
+ function escalateOutcomeForCode(code) {
773
+ switch (code) {
774
+ case "INVALID_EMAIL":
775
+ return "invalid-email";
776
+ case "RATE_LIMITED":
777
+ case "HTTP_429":
778
+ return "rate-limited";
779
+ case "CAPTCHA_REQUIRED":
780
+ case "CAPTCHA_FAILED":
781
+ return "captcha";
782
+ // The token owns nothing, or owns a different conversation. The server answers NOT_FOUND
783
+ // rather than FORBIDDEN so the endpoint is not an oracle for whether a thread exists;
784
+ // either way there is nothing this visitor can do about it, so it reads as a failure.
785
+ case "NOT_FOUND":
786
+ return "failed";
787
+ default:
788
+ return "failed";
789
+ }
790
+ }
752
791
  function classifyExploreError(code, serverMessage) {
753
792
  const c = code ?? "UNKNOWN";
754
793
  if (RATE_LIMIT_CODES.has(c)) {
@@ -915,6 +954,7 @@ function useExploreChat(options = {}) {
915
954
  onSend
916
955
  } = options;
917
956
  const client = useWebSDK();
957
+ const { productId } = useWebSDKConfig();
918
958
  const promptKey = JSON.stringify(examplePrompts ?? []);
919
959
  const welcomeKey = JSON.stringify(fallbackWelcome ?? null);
920
960
  const fallbackCatalog = useMemo(
@@ -938,6 +978,7 @@ function useExploreChat(options = {}) {
938
978
  const pollGenerationRef = useRef(0);
939
979
  const pollTimerRef = useRef(null);
940
980
  const conversationTokenRef = useRef(null);
981
+ const [hasConversation, setHasConversation] = useState(false);
941
982
  const shareTokenRef = useRef(void 0);
942
983
  if (shareTokenRef.current === void 0) {
943
984
  shareTokenRef.current = readShareToken();
@@ -952,6 +993,10 @@ function useExploreChat(options = {}) {
952
993
  );
953
994
  const inFlightRef = useRef(false);
954
995
  catalogRef.current = catalog;
996
+ const rememberConversationToken = useCallback((token) => {
997
+ conversationTokenRef.current = token;
998
+ setHasConversation(token !== null);
999
+ }, []);
955
1000
  const clearPollTimer = useCallback(() => {
956
1001
  if (pollTimerRef.current !== null) {
957
1002
  clearTimeout(pollTimerRef.current);
@@ -1018,7 +1063,7 @@ function useExploreChat(options = {}) {
1018
1063
  setArchive(readArchive(storageKey));
1019
1064
  const stored = readPersisted(storageKey);
1020
1065
  if (stored) {
1021
- conversationTokenRef.current = stored.conversationToken;
1066
+ rememberConversationToken(stored.conversationToken);
1022
1067
  focusProductRef.current = stored.focusProduct;
1023
1068
  const settled = stored.messages.flatMap((m) => {
1024
1069
  if (m.role !== "ASSISTANT") return [m];
@@ -1034,7 +1079,7 @@ function useExploreChat(options = {}) {
1034
1079
  setTurnCount(turns);
1035
1080
  }
1036
1081
  setHydrated(true);
1037
- }, [persist, storageKey]);
1082
+ }, [persist, rememberConversationToken, storageKey]);
1038
1083
  useEffect(() => {
1039
1084
  if (!persist || !hydrated || typeof window === "undefined") return;
1040
1085
  try {
@@ -1227,7 +1272,7 @@ function useExploreChat(options = {}) {
1227
1272
  setPhase(classified.kind === "rate-limit" ? "limited" : "error");
1228
1273
  return;
1229
1274
  }
1230
- conversationTokenRef.current = result.conversation.token;
1275
+ rememberConversationToken(result.conversation.token);
1231
1276
  turnCountRef.current = result.conversation.turnCount;
1232
1277
  setTurnCount(result.conversation.turnCount);
1233
1278
  setRemainingToday(result.remainingToday ?? null);
@@ -1253,7 +1298,14 @@ function useExploreChat(options = {}) {
1253
1298
  setPhase("streaming");
1254
1299
  startPolling(result.conversation.token, assistantMessage.id);
1255
1300
  },
1256
- [client, locale, pageUrl, productSlug, startPolling]
1301
+ [
1302
+ client,
1303
+ locale,
1304
+ pageUrl,
1305
+ productSlug,
1306
+ rememberConversationToken,
1307
+ startPolling
1308
+ ]
1257
1309
  );
1258
1310
  const send = useCallback(
1259
1311
  async (message, attachments = []) => {
@@ -1370,7 +1422,7 @@ function useExploreChat(options = {}) {
1370
1422
  );
1371
1423
  cancelPolling();
1372
1424
  inFlightRef.current = false;
1373
- conversationTokenRef.current = null;
1425
+ rememberConversationToken(null);
1374
1426
  lastSentTextRef.current = null;
1375
1427
  lastSentAttachmentsRef.current = [];
1376
1428
  turnCountRef.current = 0;
@@ -1385,7 +1437,7 @@ function useExploreChat(options = {}) {
1385
1437
  } catch {
1386
1438
  }
1387
1439
  }
1388
- }, [cancelPolling, storageKey]);
1440
+ }, [cancelPolling, rememberConversationToken, storageKey]);
1389
1441
  const openConversation = useCallback(
1390
1442
  (token) => {
1391
1443
  const entry = readArchive(storageKey).find((e) => e.token === token);
@@ -1397,7 +1449,7 @@ function useExploreChat(options = {}) {
1397
1449
  });
1398
1450
  cancelPolling();
1399
1451
  inFlightRef.current = false;
1400
- conversationTokenRef.current = entry.token;
1452
+ rememberConversationToken(entry.token);
1401
1453
  turnCountRef.current = entry.messages.filter(
1402
1454
  (m) => m.role === "USER"
1403
1455
  ).length;
@@ -1409,7 +1461,7 @@ function useExploreChat(options = {}) {
1409
1461
  setPhase(catalogRef.current.enabled ? "ready" : "disabled");
1410
1462
  setArchive(readArchive(storageKey));
1411
1463
  },
1412
- [cancelPolling, storageKey]
1464
+ [cancelPolling, rememberConversationToken, storageKey]
1413
1465
  );
1414
1466
  const deleteConversation = useCallback(
1415
1467
  (token) => {
@@ -1498,6 +1550,69 @@ function useExploreChat(options = {}) {
1498
1550
  },
1499
1551
  [client]
1500
1552
  );
1553
+ const escalate = useCallback(
1554
+ async (email, note) => {
1555
+ const token = conversationTokenRef.current;
1556
+ if (!token) {
1557
+ return { outcome: "no-conversation", email: null, createdAt: null };
1558
+ }
1559
+ const attempt = async (captchaAttempt) => {
1560
+ const activeCatalog = catalogRef.current;
1561
+ let recaptchaToken;
1562
+ if (activeCatalog.captchaRequired && activeCatalog.recaptchaSiteKey) {
1563
+ try {
1564
+ recaptchaToken = await getRecaptchaV3Token(
1565
+ activeCatalog.recaptchaSiteKey
1566
+ );
1567
+ } catch (err) {
1568
+ console.warn(
1569
+ "[WebSDK] reCAPTCHA v3 token could not be minted",
1570
+ err
1571
+ );
1572
+ }
1573
+ }
1574
+ const input = {
1575
+ conversationToken: token,
1576
+ email: email.trim(),
1577
+ note: note?.trim() ? note.trim() : null,
1578
+ ...recaptchaToken ? { recaptchaToken } : {},
1579
+ ...productId ? { productId } : {}
1580
+ };
1581
+ try {
1582
+ const res = await client.mutate(
1583
+ ESCALATE_EXPLORE_CONVERSATION_MUTATION,
1584
+ { input }
1585
+ );
1586
+ const first = res?.errors?.[0];
1587
+ const result = res?.data?.escalateExploreConversation;
1588
+ if (first || !result) {
1589
+ const code = typeof first?.extensions?.code === "string" ? first.extensions.code : "ESCALATION_FAILED";
1590
+ if (code === "CAPTCHA_FAILED" && captchaAttempt === 0) {
1591
+ return attempt(1);
1592
+ }
1593
+ return {
1594
+ outcome: escalateOutcomeForCode(code),
1595
+ email: null,
1596
+ createdAt: null
1597
+ };
1598
+ }
1599
+ return {
1600
+ // `created: false` is the double-click, the retry, the reload. The handover
1601
+ // exists, so this is a success — and the address comes from the SERVER, because
1602
+ // on a repeat submit the reply goes to whatever the FIRST handover used.
1603
+ outcome: result.created ? "sent" : "already-handed-over",
1604
+ email: result.escalation?.email ?? null,
1605
+ createdAt: result.escalation?.createdAt ?? null
1606
+ };
1607
+ } catch {
1608
+ return { outcome: "failed", email: null, createdAt: null };
1609
+ }
1610
+ };
1611
+ return attempt(0);
1612
+ },
1613
+ [client, productId]
1614
+ );
1615
+ const canEscalate = hasConversation && !isSharedView;
1501
1616
  return {
1502
1617
  phase,
1503
1618
  catalog,
@@ -1518,6 +1633,8 @@ function useExploreChat(options = {}) {
1518
1633
  clearHistory,
1519
1634
  recordClick,
1520
1635
  submitFeedback,
1636
+ escalate,
1637
+ canEscalate,
1521
1638
  createShareLink,
1522
1639
  isSharedView
1523
1640
  };