@burdenoff/website-sdk 2026.922.4 → 2026.923.1

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 classifyExploreError, u as useExploreChat } from '../use-explore-chat-Dgm-jf96.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 classifyExploreError, u as useExploreChat } from '../use-explore-chat-CNEdKBc1.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
  };
@@ -1747,11 +1864,367 @@ function useSpeechInput({
1747
1864
  );
1748
1865
  return { supported, listening, interim, error, start, stop, toggle };
1749
1866
  }
1867
+ var SILENT_FAILURE_MS = 1500;
1868
+ function speechPlatform() {
1869
+ if (typeof window === "undefined") return null;
1870
+ const scope = window;
1871
+ const synth = scope.speechSynthesis;
1872
+ const Utterance = scope.SpeechSynthesisUtterance;
1873
+ if (!synth || typeof synth.speak !== "function") return null;
1874
+ if (typeof Utterance !== "function") return null;
1875
+ return { synth, Utterance };
1876
+ }
1877
+ function normaliseTag(tag) {
1878
+ return tag.trim().toLowerCase().replace(/_/g, "-");
1879
+ }
1880
+ function primaryOf(tag) {
1881
+ return normaliseTag(tag).split("-")[0] ?? "";
1882
+ }
1883
+ function pickVoice(voices, lang) {
1884
+ const wanted = normaliseTag(lang);
1885
+ if (wanted === "") return null;
1886
+ const primary = primaryOf(wanted);
1887
+ const exact = voices.filter((voice) => normaliseTag(voice.lang) === wanted);
1888
+ const related = voices.filter((voice) => primaryOf(voice.lang) === primary);
1889
+ return exact.find((voice) => voice.default) ?? exact[0] ?? related.find((voice) => voice.default) ?? related[0] ?? null;
1890
+ }
1891
+ var activeOwner = null;
1892
+ var activeStop = null;
1893
+ function stopSpeaking() {
1894
+ activeStop?.();
1895
+ }
1896
+ function useSpeechOutput({
1897
+ lang
1898
+ } = {}) {
1899
+ const [available, setAvailable] = react.useState(() => speechPlatform() !== null);
1900
+ const [speaking, setSpeaking] = react.useState(false);
1901
+ const utteranceRef = react.useRef(null);
1902
+ const watchdogRef = react.useRef(null);
1903
+ const ownerRef = react.useRef({});
1904
+ const stopRef = react.useRef(() => void 0);
1905
+ const langRef = react.useRef(lang);
1906
+ langRef.current = lang;
1907
+ const clearWatchdog = react.useCallback(() => {
1908
+ if (watchdogRef.current === null) return;
1909
+ clearTimeout(watchdogRef.current);
1910
+ watchdogRef.current = null;
1911
+ }, []);
1912
+ const settle = react.useCallback(() => {
1913
+ clearWatchdog();
1914
+ utteranceRef.current = null;
1915
+ if (activeOwner === ownerRef.current) {
1916
+ activeOwner = null;
1917
+ activeStop = null;
1918
+ }
1919
+ setSpeaking(false);
1920
+ }, [clearWatchdog]);
1921
+ const stop = react.useCallback(() => {
1922
+ const owned = utteranceRef.current !== null;
1923
+ settle();
1924
+ if (owned) speechPlatform()?.synth.cancel();
1925
+ }, [settle]);
1926
+ stopRef.current = stop;
1927
+ const speak = react.useCallback(
1928
+ (text) => {
1929
+ const platform = speechPlatform();
1930
+ if (!platform) {
1931
+ setAvailable(false);
1932
+ return;
1933
+ }
1934
+ const spoken = text.trim();
1935
+ if (spoken === "") return;
1936
+ if (activeStop && activeOwner !== ownerRef.current) activeStop();
1937
+ platform.synth.cancel();
1938
+ const utterance = new platform.Utterance(spoken);
1939
+ const wanted = langRef.current;
1940
+ if (wanted !== void 0 && wanted !== "") {
1941
+ const voice = pickVoice(platform.synth.getVoices(), wanted);
1942
+ utterance.lang = voice?.lang ?? wanted;
1943
+ if (voice) utterance.voice = voice;
1944
+ }
1945
+ const mine = () => utteranceRef.current === utterance;
1946
+ utterance.onstart = () => {
1947
+ if (!mine()) return;
1948
+ clearWatchdog();
1949
+ setSpeaking(true);
1950
+ };
1951
+ utterance.onend = () => {
1952
+ if (!mine()) return;
1953
+ settle();
1954
+ };
1955
+ utterance.onerror = (event) => {
1956
+ if (!mine()) return;
1957
+ const code = typeof event?.error === "string" ? event.error : "";
1958
+ settle();
1959
+ if (code !== "interrupted" && code !== "canceled") setAvailable(false);
1960
+ };
1961
+ utteranceRef.current = utterance;
1962
+ activeOwner = ownerRef.current;
1963
+ activeStop = () => {
1964
+ stopRef.current();
1965
+ };
1966
+ setSpeaking(true);
1967
+ platform.synth.speak(utterance);
1968
+ clearWatchdog();
1969
+ watchdogRef.current = setTimeout(() => {
1970
+ watchdogRef.current = null;
1971
+ if (!mine()) return;
1972
+ if (platform.synth.speaking) return;
1973
+ settle();
1974
+ setAvailable(false);
1975
+ }, SILENT_FAILURE_MS);
1976
+ },
1977
+ [clearWatchdog, settle]
1978
+ );
1979
+ const toggle = react.useCallback(
1980
+ (text) => {
1981
+ if (speaking) stop();
1982
+ else speak(text);
1983
+ },
1984
+ [speak, speaking, stop]
1985
+ );
1986
+ react.useEffect(() => {
1987
+ if (!speaking || typeof window === "undefined") return;
1988
+ const onHide = () => {
1989
+ stopRef.current();
1990
+ };
1991
+ window.addEventListener("pagehide", onHide);
1992
+ return () => window.removeEventListener("pagehide", onHide);
1993
+ }, [speaking]);
1994
+ react.useEffect(
1995
+ () => () => {
1996
+ stopRef.current();
1997
+ },
1998
+ []
1999
+ );
2000
+ return { supported: available, speaking, speak, stop, toggle };
2001
+ }
2002
+ var DRAIN_TARGET_MS = 1200;
2003
+ var MIN_CHARS_PER_SECOND = 90;
2004
+ var MAX_CHARS_PER_SECOND = 2200;
2005
+ var MAX_STEP_MS = 250;
2006
+ var COMMIT_INTERVAL_MS = 45;
2007
+ var FALLBACK_STEP_MS = 50;
2008
+ var FENCE = /^ {0,3}(?:```|~~~)/;
2009
+ var TABLE_ROW = /^ {0,3}\|/;
2010
+ var EMPTY_BLOCK_MARKER = /^ {0,3}(?:[-+>]|#{1,6}|\d{1,9}[.)])\s*$/;
2011
+ function isWordCharacter(char) {
2012
+ return char !== void 0 && /[\p{L}\p{N}]/u.test(char);
2013
+ }
2014
+ function clamp(value, min, max) {
2015
+ return Math.min(Math.max(value, min), max);
2016
+ }
2017
+ function now() {
2018
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
2019
+ }
2020
+ function isHighSurrogate(code) {
2021
+ return code >= 55296 && code <= 56319;
2022
+ }
2023
+ function safeInlineEnd(line) {
2024
+ let codeSpan = null;
2025
+ let link = null;
2026
+ const emphasis = [];
2027
+ let at = 0;
2028
+ while (at < line.length) {
2029
+ const char = line[at] ?? "";
2030
+ if (char === "\\") {
2031
+ at += 2;
2032
+ continue;
2033
+ }
2034
+ if (char === "`") {
2035
+ let run = 1;
2036
+ while (line[at + run] === "`") run += 1;
2037
+ codeSpan = codeSpan === null ? at : null;
2038
+ at += run;
2039
+ continue;
2040
+ }
2041
+ if (codeSpan !== null) {
2042
+ at += 1;
2043
+ continue;
2044
+ }
2045
+ if (char === "[") {
2046
+ if (link === null) link = line[at - 1] === "!" ? at - 1 : at;
2047
+ at += 1;
2048
+ continue;
2049
+ }
2050
+ if (char === "]") {
2051
+ if (link !== null && line[at + 1] !== "(") link = null;
2052
+ at += 1;
2053
+ continue;
2054
+ }
2055
+ if (char === ")") {
2056
+ link = null;
2057
+ at += 1;
2058
+ continue;
2059
+ }
2060
+ if (char === "*" || char === "_" || char === "~") {
2061
+ let run = 1;
2062
+ while (line[at + run] === char) run += 1;
2063
+ const start = at;
2064
+ at += run;
2065
+ const before = line[start - 1];
2066
+ const after = line[start + run];
2067
+ if (run < 2) continue;
2068
+ if (char === "_" && isWordCharacter(before) && isWordCharacter(after)) {
2069
+ continue;
2070
+ }
2071
+ const marker = char.repeat(run);
2072
+ const open2 = emphasis.findIndex((entry) => entry.marker === marker);
2073
+ if (open2 !== -1) {
2074
+ emphasis.splice(open2, 1);
2075
+ continue;
2076
+ }
2077
+ if (after !== void 0 && /\s/.test(after)) continue;
2078
+ emphasis.push({ marker, start });
2079
+ continue;
2080
+ }
2081
+ at += 1;
2082
+ }
2083
+ const open = [codeSpan, link, emphasis[0]?.start ?? null].filter(
2084
+ (value) => value !== null
2085
+ );
2086
+ return open.length === 0 ? line.length : Math.min(...open);
2087
+ }
2088
+ function revealBoundary(text, limit) {
2089
+ if (limit <= 0) return 0;
2090
+ const caughtUp = limit >= text.length;
2091
+ const head = caughtUp ? text : text.slice(0, limit);
2092
+ const lineStart = head.lastIndexOf("\n") + 1;
2093
+ const line = head.slice(lineStart);
2094
+ let fenceStart = null;
2095
+ let tableStart = null;
2096
+ let tableRows = 0;
2097
+ let at = 0;
2098
+ while (at < lineStart) {
2099
+ const breakAt = head.indexOf("\n", at);
2100
+ const end = breakAt === -1 ? lineStart : breakAt;
2101
+ const row = head.slice(at, end);
2102
+ if (fenceStart !== null) {
2103
+ if (FENCE.test(row)) fenceStart = null;
2104
+ } else if (FENCE.test(row)) {
2105
+ fenceStart = at;
2106
+ } else if (TABLE_ROW.test(row)) {
2107
+ if (tableStart === null) {
2108
+ tableStart = at;
2109
+ tableRows = 0;
2110
+ }
2111
+ tableRows += 1;
2112
+ } else if (tableStart !== null) {
2113
+ tableStart = null;
2114
+ tableRows = 0;
2115
+ }
2116
+ at = end + 1;
2117
+ }
2118
+ if (fenceStart === null) {
2119
+ const lastIsRow = TABLE_ROW.test(line);
2120
+ if (lastIsRow && tableStart === null) {
2121
+ tableStart = lineStart;
2122
+ tableRows = 0;
2123
+ } else if (!lastIsRow && line !== "") {
2124
+ tableStart = null;
2125
+ tableRows = 0;
2126
+ }
2127
+ if (tableStart !== null && tableRows < 2) return tableStart;
2128
+ }
2129
+ if (caughtUp) return text.length;
2130
+ if (fenceStart !== null) return fenceStart;
2131
+ if (TABLE_ROW.test(line)) return lineStart;
2132
+ if (EMPTY_BLOCK_MARKER.test(line)) return lineStart;
2133
+ const cut = lineStart + safeInlineEnd(line);
2134
+ return isHighSurrogate(text.charCodeAt(cut - 1)) ? cut - 1 : cut;
2135
+ }
2136
+ function prefersReducedMotion() {
2137
+ if (typeof window === "undefined") return true;
2138
+ try {
2139
+ return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
2140
+ } catch {
2141
+ return false;
2142
+ }
2143
+ }
2144
+ function useProgressiveReveal(text, settled) {
2145
+ const [, setFrame] = react.useState(0);
2146
+ const [reduced] = react.useState(prefersReducedMotion);
2147
+ const immediate = settled || reduced;
2148
+ const textRef = react.useRef(text);
2149
+ const cursorRef = react.useRef(immediate ? text.length : 0);
2150
+ const cutRef = react.useRef(immediate ? text.length : 0);
2151
+ const committedRef = react.useRef(0);
2152
+ if (text !== textRef.current) {
2153
+ const revealed = textRef.current.slice(0, Math.floor(cursorRef.current));
2154
+ if (revealed !== "" && !text.startsWith(revealed)) {
2155
+ cursorRef.current = text.length;
2156
+ }
2157
+ cursorRef.current = Math.min(cursorRef.current, text.length);
2158
+ cutRef.current = Math.min(cutRef.current, text.length);
2159
+ textRef.current = text;
2160
+ }
2161
+ if (immediate) cursorRef.current = text.length;
2162
+ const cut = immediate ? text.length : Math.max(
2163
+ cutRef.current,
2164
+ revealBoundary(text, Math.floor(cursorRef.current))
2165
+ );
2166
+ cutRef.current = cut;
2167
+ react.useEffect(() => {
2168
+ if (immediate) return;
2169
+ if (cursorRef.current >= text.length) return;
2170
+ let cancelled = false;
2171
+ let cancelStep = null;
2172
+ let previous = now();
2173
+ const commit = (at, force) => {
2174
+ if (!force && at - committedRef.current < COMMIT_INTERVAL_MS) return;
2175
+ committedRef.current = at;
2176
+ setFrame((value) => value + 1);
2177
+ };
2178
+ const step = () => {
2179
+ cancelStep = null;
2180
+ if (cancelled) return;
2181
+ const at = now();
2182
+ const elapsed = clamp(at - previous, 0, MAX_STEP_MS);
2183
+ previous = at;
2184
+ const length = textRef.current.length;
2185
+ const pending = length - cursorRef.current;
2186
+ if (pending <= 0) {
2187
+ commit(at, true);
2188
+ return;
2189
+ }
2190
+ const perSecond = clamp(
2191
+ pending / (DRAIN_TARGET_MS / 1e3),
2192
+ MIN_CHARS_PER_SECOND,
2193
+ MAX_CHARS_PER_SECOND
2194
+ );
2195
+ cursorRef.current = Math.min(
2196
+ length,
2197
+ cursorRef.current + perSecond * elapsed / 1e3
2198
+ );
2199
+ const done = cursorRef.current >= length;
2200
+ commit(at, done);
2201
+ if (!done) schedule();
2202
+ };
2203
+ const schedule = () => {
2204
+ if (typeof window.requestAnimationFrame === "function") {
2205
+ const id2 = window.requestAnimationFrame(step);
2206
+ cancelStep = () => window.cancelAnimationFrame(id2);
2207
+ return;
2208
+ }
2209
+ const id = setTimeout(step, FALLBACK_STEP_MS);
2210
+ cancelStep = () => clearTimeout(id);
2211
+ };
2212
+ schedule();
2213
+ return () => {
2214
+ cancelled = true;
2215
+ cancelStep?.();
2216
+ };
2217
+ }, [text, immediate]);
2218
+ return text.slice(0, cut);
2219
+ }
1750
2220
 
1751
2221
  exports.DEFAULT_EXPLORE_EXAMPLE_PROMPTS = DEFAULT_EXPLORE_EXAMPLE_PROMPTS;
1752
2222
  exports.FALLBACK_EXPLORE_LIMITS = FALLBACK_EXPLORE_LIMITS;
1753
2223
  exports.classifyExploreError = classifyExploreError;
2224
+ exports.stopSpeaking = stopSpeaking;
1754
2225
  exports.useExploreChat = useExploreChat;
2226
+ exports.useProgressiveReveal = useProgressiveReveal;
1755
2227
  exports.useSpeechInput = useSpeechInput;
2228
+ exports.useSpeechOutput = useSpeechOutput;
1756
2229
  //# sourceMappingURL=index.js.map
1757
2230
  //# sourceMappingURL=index.js.map