@burdenoff/website-sdk 2026.828.5 → 2026.828.6

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.
@@ -38,6 +38,16 @@ interface ExplorePageProps {
38
38
  /** Overrides `exploreCatalog.welcomeBody`. */
39
39
  welcomeBody?: string;
40
40
  }
41
+ /**
42
+ * The product's own logo, derived from the reference's origin.
43
+ *
44
+ * Every Burdenoff product site serves `/favicon.svg`, so the logo needs no registry and no
45
+ * bundled assets — the reference URL already names the product. Deliberately NOT
46
+ * `/favicon-512.png`: that path returns 200 with `text/html` because the SPA fallback
47
+ * answers unknown paths, so it would render as a broken image rather than 404 into the
48
+ * fallback below.
49
+ */
50
+ declare function logoUrlOf(url: string): string | null;
41
51
  declare function ExplorePage({ seo, productSlug, examplePrompts, className, heightMode, viewportOffset, onNavigate, onSend, welcomeTitle, welcomeBody, }: ExplorePageProps): react.JSX.Element;
42
52
 
43
- export { ExplorePage, type ExplorePageProps };
53
+ export { ExplorePage, type ExplorePageProps, logoUrlOf };
@@ -38,6 +38,16 @@ interface ExplorePageProps {
38
38
  /** Overrides `exploreCatalog.welcomeBody`. */
39
39
  welcomeBody?: string;
40
40
  }
41
+ /**
42
+ * The product's own logo, derived from the reference's origin.
43
+ *
44
+ * Every Burdenoff product site serves `/favicon.svg`, so the logo needs no registry and no
45
+ * bundled assets — the reference URL already names the product. Deliberately NOT
46
+ * `/favicon-512.png`: that path returns 200 with `text/html` because the SPA fallback
47
+ * answers unknown paths, so it would render as a broken image rather than 404 into the
48
+ * fallback below.
49
+ */
50
+ declare function logoUrlOf(url: string): string | null;
41
51
  declare function ExplorePage({ seo, productSlug, examplePrompts, className, heightMode, viewportOffset, onNavigate, onSend, welcomeTitle, welcomeBody, }: ExplorePageProps): react.JSX.Element;
42
52
 
43
- export { ExplorePage, type ExplorePageProps };
53
+ export { ExplorePage, type ExplorePageProps, logoUrlOf };
@@ -1523,11 +1523,42 @@ function getRecognitionCtor() {
1523
1523
  const w = window;
1524
1524
  return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
1525
1525
  }
1526
+ var MIC_DENIED_MESSAGE = "Microphone access was blocked. Allow it in your browser's site settings to dictate.";
1527
+ async function ensureMicrophoneAccess() {
1528
+ const media = typeof navigator === "undefined" ? void 0 : navigator.mediaDevices;
1529
+ if (!media?.getUserMedia) return { ok: true };
1530
+ try {
1531
+ const status = await navigator.permissions?.query({
1532
+ name: "microphone"
1533
+ });
1534
+ if (status?.state === "granted") return { ok: true };
1535
+ if (status?.state === "denied")
1536
+ return { ok: false, message: MIC_DENIED_MESSAGE };
1537
+ } catch {
1538
+ }
1539
+ try {
1540
+ const stream = await media.getUserMedia({ audio: true });
1541
+ for (const track of stream.getTracks()) track.stop();
1542
+ return { ok: true };
1543
+ } catch (error) {
1544
+ const name = typeof error === "object" && error !== null && "name" in error ? String(error.name) : "";
1545
+ if (name === "NotAllowedError" || name === "SecurityError") {
1546
+ return { ok: false, message: MIC_DENIED_MESSAGE };
1547
+ }
1548
+ if (name === "NotFoundError" || name === "DevicesNotFoundError") {
1549
+ return { ok: false, message: "No microphone was found." };
1550
+ }
1551
+ return {
1552
+ ok: false,
1553
+ message: "Dictation could not start. You can type instead."
1554
+ };
1555
+ }
1556
+ }
1526
1557
  function describeError(code) {
1527
1558
  switch (code) {
1528
1559
  case "not-allowed":
1529
1560
  case "service-not-allowed":
1530
- return "Microphone access was blocked. Allow it in your browser's site settings to dictate.";
1561
+ return MIC_DENIED_MESSAGE;
1531
1562
  case "no-speech":
1532
1563
  return "I didn't catch anything \u2014 try again a little closer to the mic.";
1533
1564
  case "audio-capture":
@@ -1550,11 +1581,22 @@ function useSpeechInput({
1550
1581
  const [interim, setInterim] = React.useState("");
1551
1582
  const [error, setError] = React.useState(null);
1552
1583
  const recognitionRef = React.useRef(null);
1584
+ const startTokenRef = React.useRef(0);
1585
+ const beginRecognitionRef = React.useRef(null);
1586
+ const beginRecognition = React.useCallback(
1587
+ (Ctor, token) => {
1588
+ beginRecognitionRef.current?.(Ctor, token);
1589
+ },
1590
+ []
1591
+ );
1553
1592
  const finalRef = React.useRef(onFinalTranscript);
1554
1593
  const errorRef = React.useRef(onError);
1555
1594
  finalRef.current = onFinalTranscript;
1556
1595
  errorRef.current = onError;
1557
1596
  const stop = React.useCallback(() => {
1597
+ startTokenRef.current += 1;
1598
+ setListening(false);
1599
+ setInterim("");
1558
1600
  const recognition = recognitionRef.current;
1559
1601
  if (!recognition) return;
1560
1602
  try {
@@ -1568,48 +1610,69 @@ function useSpeechInput({
1568
1610
  const Ctor = getRecognitionCtor();
1569
1611
  if (!Ctor) return;
1570
1612
  if (recognitionRef.current) stop();
1571
- const recognition = new Ctor();
1572
- recognition.lang = lang ?? (typeof navigator === "undefined" ? "en-US" : navigator.language || "en-US");
1573
- recognition.continuous = true;
1574
- recognition.interimResults = true;
1575
- recognition.maxAlternatives = 1;
1576
- recognition.onstart = () => {
1577
- setError(null);
1578
- setListening(true);
1579
- };
1580
- recognition.onresult = (event) => {
1581
- let settled = "";
1582
- let pending = "";
1583
- for (let i = event.resultIndex; i < event.results.length; i += 1) {
1584
- const result = event.results[i];
1585
- if (!result) continue;
1586
- const text = result[0]?.transcript ?? "";
1587
- if (result.isFinal) settled += text;
1588
- else pending += text;
1613
+ setError(null);
1614
+ setListening(true);
1615
+ startTokenRef.current += 1;
1616
+ const token = startTokenRef.current;
1617
+ void ensureMicrophoneAccess().then((access) => {
1618
+ if (token !== startTokenRef.current) return;
1619
+ if (!access.ok) {
1620
+ setListening(false);
1621
+ setError(access.message);
1622
+ errorRef.current?.(access.message);
1623
+ return;
1589
1624
  }
1590
- setInterim(pending);
1591
- if (settled.trim() !== "") finalRef.current(settled);
1592
- };
1593
- recognition.onerror = (event) => {
1594
- const message = describeError(event.error);
1595
- setListening(false);
1596
- setInterim("");
1597
- if (message !== "") {
1598
- setError(message);
1599
- errorRef.current?.(message);
1625
+ beginRecognition(Ctor, token);
1626
+ });
1627
+ }, [beginRecognition, stop]);
1628
+ const beginRecognitionImpl = React.useCallback(
1629
+ (Ctor, token) => {
1630
+ const recognition = new Ctor();
1631
+ recognition.lang = lang ?? (typeof navigator === "undefined" ? "en-US" : navigator.language || "en-US");
1632
+ recognition.continuous = true;
1633
+ recognition.interimResults = true;
1634
+ recognition.maxAlternatives = 1;
1635
+ recognition.onstart = () => {
1636
+ if (token !== startTokenRef.current) return;
1637
+ setError(null);
1638
+ setListening(true);
1639
+ };
1640
+ recognition.onresult = (event) => {
1641
+ let settled = "";
1642
+ let pending = "";
1643
+ for (let i = event.resultIndex; i < event.results.length; i += 1) {
1644
+ const result = event.results[i];
1645
+ if (!result) continue;
1646
+ const text = result[0]?.transcript ?? "";
1647
+ if (result.isFinal) settled += text;
1648
+ else pending += text;
1649
+ }
1650
+ setInterim(pending);
1651
+ if (settled.trim() !== "") finalRef.current(settled);
1652
+ };
1653
+ recognition.onerror = (event) => {
1654
+ const message = describeError(event.error);
1655
+ setListening(false);
1656
+ setInterim("");
1657
+ if (message !== "") {
1658
+ setError(message);
1659
+ errorRef.current?.(message);
1660
+ }
1661
+ };
1662
+ recognition.onend = () => {
1663
+ setListening(false);
1664
+ setInterim("");
1665
+ };
1666
+ recognitionRef.current = recognition;
1667
+ try {
1668
+ recognition.start();
1669
+ } catch {
1670
+ setListening(false);
1600
1671
  }
1601
- };
1602
- recognition.onend = () => {
1603
- setListening(false);
1604
- setInterim("");
1605
- };
1606
- recognitionRef.current = recognition;
1607
- try {
1608
- recognition.start();
1609
- } catch {
1610
- setListening(false);
1611
- }
1612
- }, [lang, stop]);
1672
+ },
1673
+ [lang]
1674
+ );
1675
+ beginRecognitionRef.current = beginRecognitionImpl;
1613
1676
  const toggle = React.useCallback(() => {
1614
1677
  if (listening) stop();
1615
1678
  else start();
@@ -1850,10 +1913,33 @@ function initialOf(value, fallback) {
1850
1913
  const source = (value || fallback).trim();
1851
1914
  return source ? source.slice(0, 1).toUpperCase() : "?";
1852
1915
  }
1916
+ function logoUrlOf(url) {
1917
+ try {
1918
+ const { origin, protocol } = new URL(url);
1919
+ if (protocol !== "https:" && protocol !== "http:") return null;
1920
+ return `${origin}/favicon.svg`;
1921
+ } catch {
1922
+ return null;
1923
+ }
1924
+ }
1853
1925
  function ReferenceCard({ reference }) {
1854
1926
  const [imageFailed, setImageFailed] = React.useState(false);
1927
+ const [logoFailed, setLogoFailed] = React.useState(false);
1855
1928
  const host = hostnameOf(reference.url);
1929
+ const logoUrl = logoUrlOf(reference.url);
1856
1930
  const showImage = Boolean(reference.imageUrl) && !imageFailed;
1931
+ const showLogo = Boolean(logoUrl) && !logoFailed;
1932
+ const letterPlate = /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full w-full items-center justify-center bg-gradient-to-br from-primary/25 via-primary/5 to-secondary", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold text-primary/70 sm:text-2xl", children: initialOf(reference.product, reference.title || host) }) });
1933
+ const logoImg = showLogo ? /* @__PURE__ */ jsxRuntime.jsx(
1934
+ "img",
1935
+ {
1936
+ src: logoUrl ?? "",
1937
+ alt: "",
1938
+ loading: "lazy",
1939
+ onError: () => setLogoFailed(true),
1940
+ className: "h-full w-full object-contain p-1.5 sm:p-0"
1941
+ }
1942
+ ) : letterPlate;
1857
1943
  return /* @__PURE__ */ jsxRuntime.jsxs(
1858
1944
  "a",
1859
1945
  {
@@ -1861,9 +1947,10 @@ function ReferenceCard({ reference }) {
1861
1947
  href: reference.url,
1862
1948
  target: "_blank",
1863
1949
  rel: "noopener noreferrer",
1864
- className: "group flex flex-col overflow-hidden rounded-xl border border-border bg-card transition-colors hover:border-primary/40 hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
1950
+ className: "group flex flex-row items-center gap-3 overflow-hidden rounded-xl border border-border bg-card p-2 transition-colors hover:border-primary/40 hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring sm:flex-col sm:items-stretch sm:gap-0 sm:p-0",
1865
1951
  children: [
1866
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative aspect-[16/9] w-full overflow-hidden bg-muted", children: showImage ? /* @__PURE__ */ jsxRuntime.jsx(
1952
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative h-11 w-11 shrink-0 overflow-hidden rounded-lg bg-muted sm:hidden", children: logoImg }),
1953
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative hidden aspect-[16/9] w-full overflow-hidden bg-muted sm:block", children: showImage ? /* @__PURE__ */ jsxRuntime.jsx(
1867
1954
  "img",
1868
1955
  {
1869
1956
  src: reference.imageUrl ?? "",
@@ -1872,17 +1959,22 @@ function ReferenceCard({ reference }) {
1872
1959
  onError: () => setImageFailed(true),
1873
1960
  className: "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
1874
1961
  }
1875
- ) : (
1876
- /* Graceful fallback: a token-derived gradient plate, so a missing or
1877
- broken preview still reads as a deliberate card, not a hole. */
1878
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full w-full items-center justify-center bg-gradient-to-br from-primary/25 via-primary/5 to-secondary", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-2xl font-semibold text-primary/70", children: initialOf(reference.product, reference.title || host) }) })
1879
- ) }),
1880
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col gap-1.5 p-3", children: [
1962
+ ) : showLogo ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full w-full items-center justify-center bg-gradient-to-br from-primary/15 via-transparent to-secondary/60 p-6", children: /* @__PURE__ */ jsxRuntime.jsx(
1963
+ "img",
1964
+ {
1965
+ src: logoUrl ?? "",
1966
+ alt: "",
1967
+ loading: "lazy",
1968
+ onError: () => setLogoFailed(true),
1969
+ className: "max-h-full max-w-full object-contain"
1970
+ }
1971
+ ) }) : letterPlate }),
1972
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5 sm:gap-1.5 sm:p-3", children: [
1881
1973
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "line-clamp-2 text-sm font-semibold leading-snug text-card-foreground", children: reference.title || host || reference.url }),
1882
- reference.description ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "line-clamp-2 text-xs leading-relaxed text-muted-foreground", children: reference.description }) : null,
1883
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-auto flex items-center gap-2 pt-2", children: [
1884
- reference.product ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rounded-full bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground", children: reference.product }) : null,
1885
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-auto inline-flex min-w-0 items-center gap-1 text-xs text-muted-foreground", children: [
1974
+ reference.description ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "line-clamp-2 text-xs leading-relaxed text-muted-foreground max-sm:hidden", children: reference.description }) : null,
1975
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 sm:mt-auto sm:pt-2", children: [
1976
+ reference.product ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rounded-full bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground max-sm:hidden", children: reference.product }) : null,
1977
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex min-w-0 items-center gap-1 text-xs text-muted-foreground sm:ml-auto", children: [
1886
1978
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ExternalLink, { className: "h-3 w-3 shrink-0" }),
1887
1979
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: host })
1888
1980
  ] })
@@ -1968,7 +2060,8 @@ function UserBubble({ message }) {
1968
2060
  function AssistantBubble({
1969
2061
  message,
1970
2062
  activity,
1971
- onNavigate
2063
+ onNavigate,
2064
+ anchorRef
1972
2065
  }) {
1973
2066
  const streaming = message.status === "PENDING" || message.status === "RUNNING";
1974
2067
  const body = message.content || message.partialContent || "";
@@ -1976,9 +2069,10 @@ function AssistantBubble({
1976
2069
  return /* @__PURE__ */ jsxRuntime.jsxs(
1977
2070
  "div",
1978
2071
  {
2072
+ ref: anchorRef,
1979
2073
  "data-boff-explore": "assistant",
1980
2074
  "data-status": message.status,
1981
- className: "flex gap-3",
2075
+ className: "flex scroll-mt-4 gap-3",
1982
2076
  children: [
1983
2077
  /* @__PURE__ */ jsxRuntime.jsx(
1984
2078
  "span",
@@ -2207,6 +2301,8 @@ function ExplorePage({
2207
2301
  });
2208
2302
  speechStopRef.current = speech.stop;
2209
2303
  const scrollRef = React.useRef(null);
2304
+ const latestAssistantRef = React.useRef(null);
2305
+ const alignedForRef = React.useRef(null);
2210
2306
  const stickToBottomRef = React.useRef(true);
2211
2307
  const composingRef = React.useRef(false);
2212
2308
  const busy = phase === "sending" || phase === "streaming";
@@ -2232,12 +2328,32 @@ function ExplorePage({
2232
2328
  if (!el) return;
2233
2329
  stickToBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
2234
2330
  }, []);
2331
+ const latestAssistantId = React.useMemo(() => {
2332
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
2333
+ const message = messages[i];
2334
+ if (message && message.role === "ASSISTANT") return message.id;
2335
+ }
2336
+ return null;
2337
+ }, [messages]);
2235
2338
  React.useEffect(() => {
2339
+ const el = scrollRef.current;
2340
+ const anchor = latestAssistantRef.current;
2341
+ if (!el || !anchor || !latestAssistantId) return;
2342
+ if (alignedForRef.current === latestAssistantId) return;
2236
2343
  if (!stickToBottomRef.current) return;
2344
+ if (anchor.offsetHeight === 0) return;
2345
+ alignedForRef.current = latestAssistantId;
2346
+ const delta = anchor.getBoundingClientRect().top - el.getBoundingClientRect().top;
2347
+ el.scrollTop = Math.max(0, el.scrollTop + delta - 12);
2348
+ }, [latestAssistantId, messages]);
2349
+ React.useEffect(() => {
2350
+ if (!stickToBottomRef.current) return;
2351
+ if (latestAssistantId && alignedForRef.current === latestAssistantId)
2352
+ return;
2237
2353
  const el = scrollRef.current;
2238
2354
  if (!el) return;
2239
2355
  el.scrollTop = el.scrollHeight;
2240
- }, [messages, activity]);
2356
+ }, [activity, latestAssistantId, messages]);
2241
2357
  const submitDraft = React.useCallback(() => {
2242
2358
  const text = draft.trim();
2243
2359
  if (!text || overLimit || busy || !canSend) return;
@@ -2437,7 +2553,8 @@ function ExplorePage({
2437
2553
  {
2438
2554
  message,
2439
2555
  activity,
2440
- onNavigate
2556
+ onNavigate,
2557
+ anchorRef: message.id === latestAssistantId ? latestAssistantRef : void 0
2441
2558
  },
2442
2559
  message.id
2443
2560
  )
@@ -2622,5 +2739,6 @@ function ExplorePage({
2622
2739
  }
2623
2740
 
2624
2741
  exports.ExplorePage = ExplorePage;
2742
+ exports.logoUrlOf = logoUrlOf;
2625
2743
  //# sourceMappingURL=explore.js.map
2626
2744
  //# sourceMappingURL=explore.js.map