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