@burdenoff/website-sdk 2026.828.5 → 2026.828.7

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.
@@ -1497,11 +1497,40 @@ function getRecognitionCtor() {
1497
1497
  const w = window;
1498
1498
  return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
1499
1499
  }
1500
+ var MIC_DENIED_MESSAGE = "Microphone access was blocked. Allow it in your browser's site settings to dictate.";
1501
+ async function ensureMicrophoneAccess() {
1502
+ const media = typeof navigator === "undefined" ? void 0 : navigator.mediaDevices;
1503
+ if (!media?.getUserMedia) return { ok: true, confirmed: false };
1504
+ try {
1505
+ const status = await navigator.permissions?.query({
1506
+ name: "microphone"
1507
+ });
1508
+ if (status?.state === "granted") return { ok: true, confirmed: true };
1509
+ } catch {
1510
+ }
1511
+ try {
1512
+ const stream = await media.getUserMedia({ audio: true });
1513
+ for (const track of stream.getTracks()) track.stop();
1514
+ return { ok: true, confirmed: true };
1515
+ } catch (error) {
1516
+ const name = typeof error === "object" && error !== null && "name" in error ? String(error.name) : "";
1517
+ if (name === "NotAllowedError" || name === "SecurityError") {
1518
+ return { ok: false, message: MIC_DENIED_MESSAGE };
1519
+ }
1520
+ if (name === "NotFoundError" || name === "DevicesNotFoundError") {
1521
+ return { ok: false, message: "No microphone was found." };
1522
+ }
1523
+ return {
1524
+ ok: false,
1525
+ message: "Dictation could not start. You can type instead."
1526
+ };
1527
+ }
1528
+ }
1500
1529
  function describeError(code) {
1501
1530
  switch (code) {
1502
1531
  case "not-allowed":
1503
1532
  case "service-not-allowed":
1504
- return "Microphone access was blocked. Allow it in your browser's site settings to dictate.";
1533
+ return MIC_DENIED_MESSAGE;
1505
1534
  case "no-speech":
1506
1535
  return "I didn't catch anything \u2014 try again a little closer to the mic.";
1507
1536
  case "audio-capture":
@@ -1524,11 +1553,23 @@ function useSpeechInput({
1524
1553
  const [interim, setInterim] = useState("");
1525
1554
  const [error, setError] = useState(null);
1526
1555
  const recognitionRef = useRef(null);
1556
+ const startTokenRef = useRef(0);
1557
+ const micConfirmedRef = useRef(false);
1558
+ const beginRecognitionRef = useRef(null);
1559
+ const beginRecognition = useCallback(
1560
+ (Ctor, token) => {
1561
+ beginRecognitionRef.current?.(Ctor, token);
1562
+ },
1563
+ []
1564
+ );
1527
1565
  const finalRef = useRef(onFinalTranscript);
1528
1566
  const errorRef = useRef(onError);
1529
1567
  finalRef.current = onFinalTranscript;
1530
1568
  errorRef.current = onError;
1531
1569
  const stop = useCallback(() => {
1570
+ startTokenRef.current += 1;
1571
+ setListening(false);
1572
+ setInterim("");
1532
1573
  const recognition = recognitionRef.current;
1533
1574
  if (!recognition) return;
1534
1575
  try {
@@ -1542,48 +1583,70 @@ function useSpeechInput({
1542
1583
  const Ctor = getRecognitionCtor();
1543
1584
  if (!Ctor) return;
1544
1585
  if (recognitionRef.current) stop();
1545
- const recognition = new Ctor();
1546
- recognition.lang = lang ?? (typeof navigator === "undefined" ? "en-US" : navigator.language || "en-US");
1547
- recognition.continuous = true;
1548
- recognition.interimResults = true;
1549
- recognition.maxAlternatives = 1;
1550
- recognition.onstart = () => {
1551
- setError(null);
1552
- setListening(true);
1553
- };
1554
- recognition.onresult = (event) => {
1555
- let settled = "";
1556
- let pending = "";
1557
- for (let i = event.resultIndex; i < event.results.length; i += 1) {
1558
- const result = event.results[i];
1559
- if (!result) continue;
1560
- const text = result[0]?.transcript ?? "";
1561
- if (result.isFinal) settled += text;
1562
- else pending += text;
1586
+ setError(null);
1587
+ setListening(true);
1588
+ startTokenRef.current += 1;
1589
+ const token = startTokenRef.current;
1590
+ void ensureMicrophoneAccess().then((access) => {
1591
+ if (token !== startTokenRef.current) return;
1592
+ micConfirmedRef.current = access.ok && access.confirmed;
1593
+ if (!access.ok) {
1594
+ setListening(false);
1595
+ setError(access.message);
1596
+ errorRef.current?.(access.message);
1597
+ return;
1563
1598
  }
1564
- setInterim(pending);
1565
- if (settled.trim() !== "") finalRef.current(settled);
1566
- };
1567
- recognition.onerror = (event) => {
1568
- const message = describeError(event.error);
1569
- setListening(false);
1570
- setInterim("");
1571
- if (message !== "") {
1572
- setError(message);
1573
- errorRef.current?.(message);
1599
+ beginRecognition(Ctor, token);
1600
+ });
1601
+ }, [beginRecognition, stop]);
1602
+ const beginRecognitionImpl = useCallback(
1603
+ (Ctor, token) => {
1604
+ const recognition = new Ctor();
1605
+ recognition.lang = lang ?? (typeof navigator === "undefined" ? "en-US" : navigator.language || "en-US");
1606
+ recognition.continuous = true;
1607
+ recognition.interimResults = true;
1608
+ recognition.maxAlternatives = 1;
1609
+ recognition.onstart = () => {
1610
+ if (token !== startTokenRef.current) return;
1611
+ setError(null);
1612
+ setListening(true);
1613
+ };
1614
+ recognition.onresult = (event) => {
1615
+ let settled = "";
1616
+ let pending = "";
1617
+ for (let i = event.resultIndex; i < event.results.length; i += 1) {
1618
+ const result = event.results[i];
1619
+ if (!result) continue;
1620
+ const text = result[0]?.transcript ?? "";
1621
+ if (result.isFinal) settled += text;
1622
+ else pending += text;
1623
+ }
1624
+ setInterim(pending);
1625
+ if (settled.trim() !== "") finalRef.current(settled);
1626
+ };
1627
+ recognition.onerror = (event) => {
1628
+ const message = micConfirmedRef.current && (event.error === "not-allowed" || event.error === "service-not-allowed") ? "Dictation isn't available in this browser. You can type instead." : describeError(event.error);
1629
+ setListening(false);
1630
+ setInterim("");
1631
+ if (message !== "") {
1632
+ setError(message);
1633
+ errorRef.current?.(message);
1634
+ }
1635
+ };
1636
+ recognition.onend = () => {
1637
+ setListening(false);
1638
+ setInterim("");
1639
+ };
1640
+ recognitionRef.current = recognition;
1641
+ try {
1642
+ recognition.start();
1643
+ } catch {
1644
+ setListening(false);
1574
1645
  }
1575
- };
1576
- recognition.onend = () => {
1577
- setListening(false);
1578
- setInterim("");
1579
- };
1580
- recognitionRef.current = recognition;
1581
- try {
1582
- recognition.start();
1583
- } catch {
1584
- setListening(false);
1585
- }
1586
- }, [lang, stop]);
1646
+ },
1647
+ [lang]
1648
+ );
1649
+ beginRecognitionRef.current = beginRecognitionImpl;
1587
1650
  const toggle = useCallback(() => {
1588
1651
  if (listening) stop();
1589
1652
  else start();
@@ -1824,10 +1887,33 @@ function initialOf(value, fallback) {
1824
1887
  const source = (value || fallback).trim();
1825
1888
  return source ? source.slice(0, 1).toUpperCase() : "?";
1826
1889
  }
1890
+ function logoUrlOf(url) {
1891
+ try {
1892
+ const { origin, protocol } = new URL(url);
1893
+ if (protocol !== "https:" && protocol !== "http:") return null;
1894
+ return `${origin}/favicon.svg`;
1895
+ } catch {
1896
+ return null;
1897
+ }
1898
+ }
1827
1899
  function ReferenceCard({ reference }) {
1828
1900
  const [imageFailed, setImageFailed] = useState(false);
1901
+ const [logoFailed, setLogoFailed] = useState(false);
1829
1902
  const host = hostnameOf(reference.url);
1903
+ const logoUrl = logoUrlOf(reference.url);
1830
1904
  const showImage = Boolean(reference.imageUrl) && !imageFailed;
1905
+ const showLogo = Boolean(logoUrl) && !logoFailed;
1906
+ const letterPlate = /* @__PURE__ */ 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__ */ jsx("span", { className: "text-sm font-semibold text-primary/70 sm:text-2xl", children: initialOf(reference.product, reference.title || host) }) });
1907
+ const logoImg = showLogo ? /* @__PURE__ */ jsx(
1908
+ "img",
1909
+ {
1910
+ src: logoUrl ?? "",
1911
+ alt: "",
1912
+ loading: "lazy",
1913
+ onError: () => setLogoFailed(true),
1914
+ className: "h-full w-full object-contain p-1.5 sm:p-0"
1915
+ }
1916
+ ) : letterPlate;
1831
1917
  return /* @__PURE__ */ jsxs(
1832
1918
  "a",
1833
1919
  {
@@ -1835,9 +1921,10 @@ function ReferenceCard({ reference }) {
1835
1921
  href: reference.url,
1836
1922
  target: "_blank",
1837
1923
  rel: "noopener noreferrer",
1838
- 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",
1924
+ 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",
1839
1925
  children: [
1840
- /* @__PURE__ */ jsx("div", { className: "relative aspect-[16/9] w-full overflow-hidden bg-muted", children: showImage ? /* @__PURE__ */ jsx(
1926
+ /* @__PURE__ */ jsx("div", { className: "relative h-11 w-11 shrink-0 overflow-hidden rounded-lg bg-muted sm:hidden", children: logoImg }),
1927
+ /* @__PURE__ */ jsx("div", { className: "relative hidden aspect-[16/9] w-full overflow-hidden bg-muted sm:block", children: showImage ? /* @__PURE__ */ jsx(
1841
1928
  "img",
1842
1929
  {
1843
1930
  src: reference.imageUrl ?? "",
@@ -1846,17 +1933,22 @@ function ReferenceCard({ reference }) {
1846
1933
  onError: () => setImageFailed(true),
1847
1934
  className: "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
1848
1935
  }
1849
- ) : (
1850
- /* Graceful fallback: a token-derived gradient plate, so a missing or
1851
- broken preview still reads as a deliberate card, not a hole. */
1852
- /* @__PURE__ */ 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__ */ jsx("span", { className: "text-2xl font-semibold text-primary/70", children: initialOf(reference.product, reference.title || host) }) })
1853
- ) }),
1854
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col gap-1.5 p-3", children: [
1936
+ ) : showLogo ? /* @__PURE__ */ 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__ */ jsx(
1937
+ "img",
1938
+ {
1939
+ src: logoUrl ?? "",
1940
+ alt: "",
1941
+ loading: "lazy",
1942
+ onError: () => setLogoFailed(true),
1943
+ className: "max-h-full max-w-full object-contain"
1944
+ }
1945
+ ) }) : letterPlate }),
1946
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5 sm:gap-1.5 sm:p-3", children: [
1855
1947
  /* @__PURE__ */ jsx("p", { className: "line-clamp-2 text-sm font-semibold leading-snug text-card-foreground", children: reference.title || host || reference.url }),
1856
- reference.description ? /* @__PURE__ */ jsx("p", { className: "line-clamp-2 text-xs leading-relaxed text-muted-foreground", children: reference.description }) : null,
1857
- /* @__PURE__ */ jsxs("div", { className: "mt-auto flex items-center gap-2 pt-2", children: [
1858
- reference.product ? /* @__PURE__ */ jsx("span", { className: "rounded-full bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground", children: reference.product }) : null,
1859
- /* @__PURE__ */ jsxs("span", { className: "ml-auto inline-flex min-w-0 items-center gap-1 text-xs text-muted-foreground", children: [
1948
+ reference.description ? /* @__PURE__ */ jsx("p", { className: "line-clamp-2 text-xs leading-relaxed text-muted-foreground max-sm:hidden", children: reference.description }) : null,
1949
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 sm:mt-auto sm:pt-2", children: [
1950
+ reference.product ? /* @__PURE__ */ 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,
1951
+ /* @__PURE__ */ jsxs("span", { className: "inline-flex min-w-0 items-center gap-1 text-xs text-muted-foreground sm:ml-auto", children: [
1860
1952
  /* @__PURE__ */ jsx(ExternalLink, { className: "h-3 w-3 shrink-0" }),
1861
1953
  /* @__PURE__ */ jsx("span", { className: "truncate", children: host })
1862
1954
  ] })
@@ -1942,7 +2034,8 @@ function UserBubble({ message }) {
1942
2034
  function AssistantBubble({
1943
2035
  message,
1944
2036
  activity,
1945
- onNavigate
2037
+ onNavigate,
2038
+ anchorRef
1946
2039
  }) {
1947
2040
  const streaming = message.status === "PENDING" || message.status === "RUNNING";
1948
2041
  const body = message.content || message.partialContent || "";
@@ -1950,9 +2043,10 @@ function AssistantBubble({
1950
2043
  return /* @__PURE__ */ jsxs(
1951
2044
  "div",
1952
2045
  {
2046
+ ref: anchorRef,
1953
2047
  "data-boff-explore": "assistant",
1954
2048
  "data-status": message.status,
1955
- className: "flex gap-3",
2049
+ className: "flex scroll-mt-4 gap-3",
1956
2050
  children: [
1957
2051
  /* @__PURE__ */ jsx(
1958
2052
  "span",
@@ -2181,6 +2275,8 @@ function ExplorePage({
2181
2275
  });
2182
2276
  speechStopRef.current = speech.stop;
2183
2277
  const scrollRef = useRef(null);
2278
+ const latestAssistantRef = useRef(null);
2279
+ const alignedForRef = useRef(null);
2184
2280
  const stickToBottomRef = useRef(true);
2185
2281
  const composingRef = useRef(false);
2186
2282
  const busy = phase === "sending" || phase === "streaming";
@@ -2206,12 +2302,32 @@ function ExplorePage({
2206
2302
  if (!el) return;
2207
2303
  stickToBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
2208
2304
  }, []);
2305
+ const latestAssistantId = useMemo(() => {
2306
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
2307
+ const message = messages[i];
2308
+ if (message && message.role === "ASSISTANT") return message.id;
2309
+ }
2310
+ return null;
2311
+ }, [messages]);
2209
2312
  useEffect(() => {
2313
+ const el = scrollRef.current;
2314
+ const anchor = latestAssistantRef.current;
2315
+ if (!el || !anchor || !latestAssistantId) return;
2316
+ if (alignedForRef.current === latestAssistantId) return;
2210
2317
  if (!stickToBottomRef.current) return;
2318
+ if (anchor.offsetHeight === 0) return;
2319
+ alignedForRef.current = latestAssistantId;
2320
+ const delta = anchor.getBoundingClientRect().top - el.getBoundingClientRect().top;
2321
+ el.scrollTop = Math.max(0, el.scrollTop + delta - 12);
2322
+ }, [latestAssistantId, messages]);
2323
+ useEffect(() => {
2324
+ if (!stickToBottomRef.current) return;
2325
+ if (latestAssistantId && alignedForRef.current === latestAssistantId)
2326
+ return;
2211
2327
  const el = scrollRef.current;
2212
2328
  if (!el) return;
2213
2329
  el.scrollTop = el.scrollHeight;
2214
- }, [messages, activity]);
2330
+ }, [activity, latestAssistantId, messages]);
2215
2331
  const submitDraft = useCallback(() => {
2216
2332
  const text = draft.trim();
2217
2333
  if (!text || overLimit || busy || !canSend) return;
@@ -2411,7 +2527,8 @@ function ExplorePage({
2411
2527
  {
2412
2528
  message,
2413
2529
  activity,
2414
- onNavigate
2530
+ onNavigate,
2531
+ anchorRef: message.id === latestAssistantId ? latestAssistantRef : void 0
2415
2532
  },
2416
2533
  message.id
2417
2534
  )
@@ -2595,6 +2712,6 @@ function ExplorePage({
2595
2712
  );
2596
2713
  }
2597
2714
 
2598
- export { ExplorePage };
2715
+ export { ExplorePage, logoUrlOf };
2599
2716
  //# sourceMappingURL=explore.mjs.map
2600
2717
  //# sourceMappingURL=explore.mjs.map