@burdenoff/website-sdk 2026.922.3 → 2026.922.5

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.
@@ -275,11 +275,9 @@ var EXPLORE_OWN_MESSAGE_FIELDS = (
275
275
  }
276
276
  `
277
277
  );
278
- var EXPLORE_CATALOG_QUERY = (
278
+ var EXPLORE_CATALOG_ESTABLISHED_FIELDS = (
279
279
  /* GraphQL */
280
280
  `
281
- query ExploreCatalog($productSlug: String) {
282
- exploreCatalog(productSlug: $productSlug) {
283
281
  enabled
284
282
  products {
285
283
  slug
@@ -297,11 +295,35 @@ var EXPLORE_CATALOG_QUERY = (
297
295
  historyTurns
298
296
  }
299
297
  captchaRequired
300
- recaptchaSiteKey
298
+ recaptchaSiteKey`
299
+ );
300
+ var EXPLORE_CATALOG_HIGHLIGHT_FIELD = (
301
+ /* GraphQL */
302
+ `
303
+ conceptHighlights {
304
+ product
305
+ productName
306
+ title
307
+ url
308
+ imageUrl
309
+ alt
310
+ }`
311
+ );
312
+ function exploreCatalogDocument(extraFields) {
313
+ return (
314
+ /* GraphQL */
315
+ `
316
+ query ExploreCatalog($productSlug: String) {
317
+ exploreCatalog(productSlug: $productSlug) {${EXPLORE_CATALOG_ESTABLISHED_FIELDS}${extraFields}
301
318
  }
302
319
  }
303
320
  `
321
+ );
322
+ }
323
+ var EXPLORE_CATALOG_QUERY = exploreCatalogDocument(
324
+ EXPLORE_CATALOG_HIGHLIGHT_FIELD
304
325
  );
326
+ var EXPLORE_CATALOG_QUERY_WITHOUT_HIGHLIGHTS = exploreCatalogDocument("");
305
327
  var EXPLORE_MESSAGE_QUERY = (
306
328
  /* GraphQL */
307
329
  `
@@ -665,7 +687,12 @@ function buildFallbackCatalog(examplePrompts, welcome) {
665
687
  welcomeBody: welcome?.body ?? FALLBACK_WELCOME_BODY,
666
688
  limits: FALLBACK_EXPLORE_LIMITS,
667
689
  captchaRequired: false,
668
- recaptchaSiteKey: null
690
+ recaptchaSiteKey: null,
691
+ // Empty, and deliberately not synthesised from `ECOSYSTEM_PRODUCTS`: a concept highlight
692
+ // needs a real published illustration URL, and this SDK has no registry of them. An
693
+ // invented path would render 38 broken frames on the one screen every prerender captures.
694
+ // The strip renders nothing at all when the list is empty, which is the honest fallback.
695
+ conceptHighlights: []
669
696
  };
670
697
  }
671
698
  function normalizeCatalog(raw, fallback) {
@@ -682,9 +709,27 @@ function normalizeCatalog(raw, fallback) {
682
709
  historyTurns: raw.limits?.historyTurns || fallback.limits.historyTurns
683
710
  },
684
711
  captchaRequired: raw.captchaRequired === true,
685
- recaptchaSiteKey: raw.recaptchaSiteKey ?? null
712
+ recaptchaSiteKey: raw.recaptchaSiteKey ?? null,
713
+ // `[ExploreConceptHighlight!]!` on the wire, so a gateway that KNOWS the field always
714
+ // sends an array. The one response that arrives without it is the retry against an older
715
+ // supergraph (`EXPLORE_CATALOG_QUERY_WITHOUT_HIGHLIGHTS`) — that gateway does not answer
716
+ // the full document at all, it refuses it. Either way
717
+ // `catalog.conceptHighlights.filter(...)` on `undefined` would throw inside render,
718
+ // blanking the welcome screen over a decorative strip.
719
+ conceptHighlights: raw.conceptHighlights?.length ? raw.conceptHighlights : fallback.conceptHighlights
686
720
  };
687
721
  }
722
+ var CATALOG_DOCUMENT_REJECTED_CODES = /* @__PURE__ */ new Set([
723
+ "GRAPHQL_VALIDATION_FAILED",
724
+ "HTTP_400"
725
+ ]);
726
+ function catalogRejectedTheHighlightField(res) {
727
+ if (res.data?.exploreCatalog) return false;
728
+ return (res.errors ?? []).some((error) => {
729
+ const code = error.extensions?.code;
730
+ return typeof code === "string" && CATALOG_DOCUMENT_REJECTED_CODES.has(code) || error.message.includes("conceptHighlights");
731
+ });
732
+ }
688
733
  var RATE_LIMIT_CODES = /* @__PURE__ */ new Set([
689
734
  "RATE_LIMITED",
690
735
  "HTTP_429",
@@ -1024,11 +1069,19 @@ function useExploreChat(options = {}) {
1024
1069
  useEffect(() => {
1025
1070
  let cancelled = false;
1026
1071
  const load = async () => {
1027
- const res = await client.query(
1072
+ const variables = { productSlug: productSlug ?? null };
1073
+ let res = await client.query(
1028
1074
  EXPLORE_CATALOG_QUERY,
1029
- { productSlug: productSlug ?? null }
1075
+ variables
1030
1076
  );
1031
1077
  if (cancelled || !mountedRef.current) return;
1078
+ if (catalogRejectedTheHighlightField(res)) {
1079
+ res = await client.query(
1080
+ EXPLORE_CATALOG_QUERY_WITHOUT_HIGHLIGHTS,
1081
+ variables
1082
+ );
1083
+ if (cancelled || !mountedRef.current) return;
1084
+ }
1032
1085
  const raw = res.data?.exploreCatalog;
1033
1086
  if (res.errors?.length || !raw) {
1034
1087
  console.warn(
@@ -1692,7 +1745,360 @@ function useSpeechInput({
1692
1745
  );
1693
1746
  return { supported, listening, interim, error, start, stop, toggle };
1694
1747
  }
1748
+ var SILENT_FAILURE_MS = 1500;
1749
+ function speechPlatform() {
1750
+ if (typeof window === "undefined") return null;
1751
+ const scope = window;
1752
+ const synth = scope.speechSynthesis;
1753
+ const Utterance = scope.SpeechSynthesisUtterance;
1754
+ if (!synth || typeof synth.speak !== "function") return null;
1755
+ if (typeof Utterance !== "function") return null;
1756
+ return { synth, Utterance };
1757
+ }
1758
+ function normaliseTag(tag) {
1759
+ return tag.trim().toLowerCase().replace(/_/g, "-");
1760
+ }
1761
+ function primaryOf(tag) {
1762
+ return normaliseTag(tag).split("-")[0] ?? "";
1763
+ }
1764
+ function pickVoice(voices, lang) {
1765
+ const wanted = normaliseTag(lang);
1766
+ if (wanted === "") return null;
1767
+ const primary = primaryOf(wanted);
1768
+ const exact = voices.filter((voice) => normaliseTag(voice.lang) === wanted);
1769
+ const related = voices.filter((voice) => primaryOf(voice.lang) === primary);
1770
+ return exact.find((voice) => voice.default) ?? exact[0] ?? related.find((voice) => voice.default) ?? related[0] ?? null;
1771
+ }
1772
+ var activeOwner = null;
1773
+ var activeStop = null;
1774
+ function stopSpeaking() {
1775
+ activeStop?.();
1776
+ }
1777
+ function useSpeechOutput({
1778
+ lang
1779
+ } = {}) {
1780
+ const [available, setAvailable] = useState(() => speechPlatform() !== null);
1781
+ const [speaking, setSpeaking] = useState(false);
1782
+ const utteranceRef = useRef(null);
1783
+ const watchdogRef = useRef(null);
1784
+ const ownerRef = useRef({});
1785
+ const stopRef = useRef(() => void 0);
1786
+ const langRef = useRef(lang);
1787
+ langRef.current = lang;
1788
+ const clearWatchdog = useCallback(() => {
1789
+ if (watchdogRef.current === null) return;
1790
+ clearTimeout(watchdogRef.current);
1791
+ watchdogRef.current = null;
1792
+ }, []);
1793
+ const settle = useCallback(() => {
1794
+ clearWatchdog();
1795
+ utteranceRef.current = null;
1796
+ if (activeOwner === ownerRef.current) {
1797
+ activeOwner = null;
1798
+ activeStop = null;
1799
+ }
1800
+ setSpeaking(false);
1801
+ }, [clearWatchdog]);
1802
+ const stop = useCallback(() => {
1803
+ const owned = utteranceRef.current !== null;
1804
+ settle();
1805
+ if (owned) speechPlatform()?.synth.cancel();
1806
+ }, [settle]);
1807
+ stopRef.current = stop;
1808
+ const speak = useCallback(
1809
+ (text) => {
1810
+ const platform = speechPlatform();
1811
+ if (!platform) {
1812
+ setAvailable(false);
1813
+ return;
1814
+ }
1815
+ const spoken = text.trim();
1816
+ if (spoken === "") return;
1817
+ if (activeStop && activeOwner !== ownerRef.current) activeStop();
1818
+ platform.synth.cancel();
1819
+ const utterance = new platform.Utterance(spoken);
1820
+ const wanted = langRef.current;
1821
+ if (wanted !== void 0 && wanted !== "") {
1822
+ const voice = pickVoice(platform.synth.getVoices(), wanted);
1823
+ utterance.lang = voice?.lang ?? wanted;
1824
+ if (voice) utterance.voice = voice;
1825
+ }
1826
+ const mine = () => utteranceRef.current === utterance;
1827
+ utterance.onstart = () => {
1828
+ if (!mine()) return;
1829
+ clearWatchdog();
1830
+ setSpeaking(true);
1831
+ };
1832
+ utterance.onend = () => {
1833
+ if (!mine()) return;
1834
+ settle();
1835
+ };
1836
+ utterance.onerror = (event) => {
1837
+ if (!mine()) return;
1838
+ const code = typeof event?.error === "string" ? event.error : "";
1839
+ settle();
1840
+ if (code !== "interrupted" && code !== "canceled") setAvailable(false);
1841
+ };
1842
+ utteranceRef.current = utterance;
1843
+ activeOwner = ownerRef.current;
1844
+ activeStop = () => {
1845
+ stopRef.current();
1846
+ };
1847
+ setSpeaking(true);
1848
+ platform.synth.speak(utterance);
1849
+ clearWatchdog();
1850
+ watchdogRef.current = setTimeout(() => {
1851
+ watchdogRef.current = null;
1852
+ if (!mine()) return;
1853
+ if (platform.synth.speaking) return;
1854
+ settle();
1855
+ setAvailable(false);
1856
+ }, SILENT_FAILURE_MS);
1857
+ },
1858
+ [clearWatchdog, settle]
1859
+ );
1860
+ const toggle = useCallback(
1861
+ (text) => {
1862
+ if (speaking) stop();
1863
+ else speak(text);
1864
+ },
1865
+ [speak, speaking, stop]
1866
+ );
1867
+ useEffect(() => {
1868
+ if (!speaking || typeof window === "undefined") return;
1869
+ const onHide = () => {
1870
+ stopRef.current();
1871
+ };
1872
+ window.addEventListener("pagehide", onHide);
1873
+ return () => window.removeEventListener("pagehide", onHide);
1874
+ }, [speaking]);
1875
+ useEffect(
1876
+ () => () => {
1877
+ stopRef.current();
1878
+ },
1879
+ []
1880
+ );
1881
+ return { supported: available, speaking, speak, stop, toggle };
1882
+ }
1883
+ var DRAIN_TARGET_MS = 1200;
1884
+ var MIN_CHARS_PER_SECOND = 90;
1885
+ var MAX_CHARS_PER_SECOND = 2200;
1886
+ var MAX_STEP_MS = 250;
1887
+ var COMMIT_INTERVAL_MS = 45;
1888
+ var FALLBACK_STEP_MS = 50;
1889
+ var FENCE = /^ {0,3}(?:```|~~~)/;
1890
+ var TABLE_ROW = /^ {0,3}\|/;
1891
+ var EMPTY_BLOCK_MARKER = /^ {0,3}(?:[-+>]|#{1,6}|\d{1,9}[.)])\s*$/;
1892
+ function isWordCharacter(char) {
1893
+ return char !== void 0 && /[\p{L}\p{N}]/u.test(char);
1894
+ }
1895
+ function clamp(value, min, max) {
1896
+ return Math.min(Math.max(value, min), max);
1897
+ }
1898
+ function now() {
1899
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
1900
+ }
1901
+ function isHighSurrogate(code) {
1902
+ return code >= 55296 && code <= 56319;
1903
+ }
1904
+ function safeInlineEnd(line) {
1905
+ let codeSpan = null;
1906
+ let link = null;
1907
+ const emphasis = [];
1908
+ let at = 0;
1909
+ while (at < line.length) {
1910
+ const char = line[at] ?? "";
1911
+ if (char === "\\") {
1912
+ at += 2;
1913
+ continue;
1914
+ }
1915
+ if (char === "`") {
1916
+ let run = 1;
1917
+ while (line[at + run] === "`") run += 1;
1918
+ codeSpan = codeSpan === null ? at : null;
1919
+ at += run;
1920
+ continue;
1921
+ }
1922
+ if (codeSpan !== null) {
1923
+ at += 1;
1924
+ continue;
1925
+ }
1926
+ if (char === "[") {
1927
+ if (link === null) link = line[at - 1] === "!" ? at - 1 : at;
1928
+ at += 1;
1929
+ continue;
1930
+ }
1931
+ if (char === "]") {
1932
+ if (link !== null && line[at + 1] !== "(") link = null;
1933
+ at += 1;
1934
+ continue;
1935
+ }
1936
+ if (char === ")") {
1937
+ link = null;
1938
+ at += 1;
1939
+ continue;
1940
+ }
1941
+ if (char === "*" || char === "_" || char === "~") {
1942
+ let run = 1;
1943
+ while (line[at + run] === char) run += 1;
1944
+ const start = at;
1945
+ at += run;
1946
+ const before = line[start - 1];
1947
+ const after = line[start + run];
1948
+ if (run < 2) continue;
1949
+ if (char === "_" && isWordCharacter(before) && isWordCharacter(after)) {
1950
+ continue;
1951
+ }
1952
+ const marker = char.repeat(run);
1953
+ const open2 = emphasis.findIndex((entry) => entry.marker === marker);
1954
+ if (open2 !== -1) {
1955
+ emphasis.splice(open2, 1);
1956
+ continue;
1957
+ }
1958
+ if (after !== void 0 && /\s/.test(after)) continue;
1959
+ emphasis.push({ marker, start });
1960
+ continue;
1961
+ }
1962
+ at += 1;
1963
+ }
1964
+ const open = [codeSpan, link, emphasis[0]?.start ?? null].filter(
1965
+ (value) => value !== null
1966
+ );
1967
+ return open.length === 0 ? line.length : Math.min(...open);
1968
+ }
1969
+ function revealBoundary(text, limit) {
1970
+ if (limit <= 0) return 0;
1971
+ const caughtUp = limit >= text.length;
1972
+ const head = caughtUp ? text : text.slice(0, limit);
1973
+ const lineStart = head.lastIndexOf("\n") + 1;
1974
+ const line = head.slice(lineStart);
1975
+ let fenceStart = null;
1976
+ let tableStart = null;
1977
+ let tableRows = 0;
1978
+ let at = 0;
1979
+ while (at < lineStart) {
1980
+ const breakAt = head.indexOf("\n", at);
1981
+ const end = breakAt === -1 ? lineStart : breakAt;
1982
+ const row = head.slice(at, end);
1983
+ if (fenceStart !== null) {
1984
+ if (FENCE.test(row)) fenceStart = null;
1985
+ } else if (FENCE.test(row)) {
1986
+ fenceStart = at;
1987
+ } else if (TABLE_ROW.test(row)) {
1988
+ if (tableStart === null) {
1989
+ tableStart = at;
1990
+ tableRows = 0;
1991
+ }
1992
+ tableRows += 1;
1993
+ } else if (tableStart !== null) {
1994
+ tableStart = null;
1995
+ tableRows = 0;
1996
+ }
1997
+ at = end + 1;
1998
+ }
1999
+ if (fenceStart === null) {
2000
+ const lastIsRow = TABLE_ROW.test(line);
2001
+ if (lastIsRow && tableStart === null) {
2002
+ tableStart = lineStart;
2003
+ tableRows = 0;
2004
+ } else if (!lastIsRow && line !== "") {
2005
+ tableStart = null;
2006
+ tableRows = 0;
2007
+ }
2008
+ if (tableStart !== null && tableRows < 2) return tableStart;
2009
+ }
2010
+ if (caughtUp) return text.length;
2011
+ if (fenceStart !== null) return fenceStart;
2012
+ if (TABLE_ROW.test(line)) return lineStart;
2013
+ if (EMPTY_BLOCK_MARKER.test(line)) return lineStart;
2014
+ const cut = lineStart + safeInlineEnd(line);
2015
+ return isHighSurrogate(text.charCodeAt(cut - 1)) ? cut - 1 : cut;
2016
+ }
2017
+ function prefersReducedMotion() {
2018
+ if (typeof window === "undefined") return true;
2019
+ try {
2020
+ return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
2021
+ } catch {
2022
+ return false;
2023
+ }
2024
+ }
2025
+ function useProgressiveReveal(text, settled) {
2026
+ const [, setFrame] = useState(0);
2027
+ const [reduced] = useState(prefersReducedMotion);
2028
+ const immediate = settled || reduced;
2029
+ const textRef = useRef(text);
2030
+ const cursorRef = useRef(immediate ? text.length : 0);
2031
+ const cutRef = useRef(immediate ? text.length : 0);
2032
+ const committedRef = useRef(0);
2033
+ if (text !== textRef.current) {
2034
+ const revealed = textRef.current.slice(0, Math.floor(cursorRef.current));
2035
+ if (revealed !== "" && !text.startsWith(revealed)) {
2036
+ cursorRef.current = text.length;
2037
+ }
2038
+ cursorRef.current = Math.min(cursorRef.current, text.length);
2039
+ cutRef.current = Math.min(cutRef.current, text.length);
2040
+ textRef.current = text;
2041
+ }
2042
+ if (immediate) cursorRef.current = text.length;
2043
+ const cut = immediate ? text.length : Math.max(
2044
+ cutRef.current,
2045
+ revealBoundary(text, Math.floor(cursorRef.current))
2046
+ );
2047
+ cutRef.current = cut;
2048
+ useEffect(() => {
2049
+ if (immediate) return;
2050
+ if (cursorRef.current >= text.length) return;
2051
+ let cancelled = false;
2052
+ let cancelStep = null;
2053
+ let previous = now();
2054
+ const commit = (at, force) => {
2055
+ if (!force && at - committedRef.current < COMMIT_INTERVAL_MS) return;
2056
+ committedRef.current = at;
2057
+ setFrame((value) => value + 1);
2058
+ };
2059
+ const step = () => {
2060
+ cancelStep = null;
2061
+ if (cancelled) return;
2062
+ const at = now();
2063
+ const elapsed = clamp(at - previous, 0, MAX_STEP_MS);
2064
+ previous = at;
2065
+ const length = textRef.current.length;
2066
+ const pending = length - cursorRef.current;
2067
+ if (pending <= 0) {
2068
+ commit(at, true);
2069
+ return;
2070
+ }
2071
+ const perSecond = clamp(
2072
+ pending / (DRAIN_TARGET_MS / 1e3),
2073
+ MIN_CHARS_PER_SECOND,
2074
+ MAX_CHARS_PER_SECOND
2075
+ );
2076
+ cursorRef.current = Math.min(
2077
+ length,
2078
+ cursorRef.current + perSecond * elapsed / 1e3
2079
+ );
2080
+ const done = cursorRef.current >= length;
2081
+ commit(at, done);
2082
+ if (!done) schedule();
2083
+ };
2084
+ const schedule = () => {
2085
+ if (typeof window.requestAnimationFrame === "function") {
2086
+ const id2 = window.requestAnimationFrame(step);
2087
+ cancelStep = () => window.cancelAnimationFrame(id2);
2088
+ return;
2089
+ }
2090
+ const id = setTimeout(step, FALLBACK_STEP_MS);
2091
+ cancelStep = () => clearTimeout(id);
2092
+ };
2093
+ schedule();
2094
+ return () => {
2095
+ cancelled = true;
2096
+ cancelStep?.();
2097
+ };
2098
+ }, [text, immediate]);
2099
+ return text.slice(0, cut);
2100
+ }
1695
2101
 
1696
- export { DEFAULT_EXPLORE_EXAMPLE_PROMPTS, FALLBACK_EXPLORE_LIMITS, classifyExploreError, useExploreChat, useSpeechInput };
2102
+ export { DEFAULT_EXPLORE_EXAMPLE_PROMPTS, FALLBACK_EXPLORE_LIMITS, classifyExploreError, stopSpeaking, useExploreChat, useProgressiveReveal, useSpeechInput, useSpeechOutput };
1697
2103
  //# sourceMappingURL=index.mjs.map
1698
2104
  //# sourceMappingURL=index.mjs.map