@burdenoff/website-sdk 2026.828.4 → 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.
@@ -806,6 +806,8 @@ async function getRecaptchaV3Token(siteKey, action = EXPLORE_RECAPTCHA_ACTION) {
806
806
 
807
807
  // src/hooks/use-explore-chat.ts
808
808
  var DEFAULT_STORAGE_KEY = "boff.explore.v1";
809
+ var HISTORY_SUFFIX = ".history";
810
+ var MAX_ARCHIVED_CONVERSATIONS = 15;
809
811
  var DEFAULT_POLL_INTERVAL_MS = 1500;
810
812
  var DEFAULT_POLL_TIMEOUT_MS = 9e4;
811
813
  var POLL_REQUEST_TIMEOUT_MS = 1e4;
@@ -944,10 +946,58 @@ function classifyExploreError(code, serverMessage) {
944
946
  retryable: true
945
947
  };
946
948
  }
949
+ function historyKey(key) {
950
+ return `${key}${HISTORY_SUFFIX}`;
951
+ }
952
+ function readArchive(key) {
953
+ if (typeof window === "undefined") return [];
954
+ try {
955
+ const raw = window.localStorage.getItem(historyKey(key));
956
+ if (!raw) return [];
957
+ const parsed = JSON.parse(raw);
958
+ if (!Array.isArray(parsed)) return [];
959
+ return parsed.filter(
960
+ (entry) => typeof entry === "object" && entry !== null && typeof entry.token === "string" && Array.isArray(entry.messages)
961
+ );
962
+ } catch {
963
+ return [];
964
+ }
965
+ }
966
+ function writeArchive(key, entries) {
967
+ if (typeof window === "undefined") return;
968
+ try {
969
+ if (entries.length === 0) {
970
+ window.localStorage.removeItem(historyKey(key));
971
+ return;
972
+ }
973
+ window.localStorage.setItem(historyKey(key), JSON.stringify(entries));
974
+ } catch {
975
+ }
976
+ }
977
+ function upsertArchive(key, thread) {
978
+ if (!thread.token) return readArchive(key);
979
+ const real = thread.messages.filter(
980
+ (m) => m.content.trim() !== "" || m.role === "USER"
981
+ );
982
+ if (real.length === 0) return readArchive(key);
983
+ const firstUser = real.find((m) => m.role === "USER");
984
+ const entry = {
985
+ token: thread.token,
986
+ title: (firstUser?.content ?? "Conversation").trim().slice(0, 80),
987
+ updatedAt: real[real.length - 1]?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
988
+ messageCount: real.length,
989
+ focusProduct: thread.focusProduct,
990
+ messages: real.slice(-20)
991
+ };
992
+ const rest = readArchive(key).filter((e) => e.token !== entry.token);
993
+ const next = [entry, ...rest].slice(0, MAX_ARCHIVED_CONVERSATIONS);
994
+ writeArchive(key, next);
995
+ return next;
996
+ }
947
997
  function readPersisted(key) {
948
998
  if (typeof window === "undefined") return null;
949
999
  try {
950
- const raw = window.sessionStorage.getItem(key);
1000
+ const raw = window.localStorage.getItem(key);
951
1001
  if (!raw) return null;
952
1002
  const parsed = JSON.parse(raw);
953
1003
  if (!parsed || typeof parsed !== "object") return null;
@@ -992,6 +1042,8 @@ function useExploreChat(options = {}) {
992
1042
  const [remainingToday, setRemainingToday] = React.useState(null);
993
1043
  const [focusProduct, setFocusProductState] = React.useState(null);
994
1044
  const [turnCount, setTurnCount] = React.useState(0);
1045
+ const messagesRef = React.useRef([]);
1046
+ const [archive, setArchive] = React.useState([]);
995
1047
  const [hydrated, setHydrated] = React.useState(false);
996
1048
  const mountedRef = React.useRef(true);
997
1049
  const pollGenerationRef = React.useRef(0);
@@ -1029,6 +1081,7 @@ function useExploreChat(options = {}) {
1029
1081
  setHydrated(true);
1030
1082
  return;
1031
1083
  }
1084
+ setArchive(readArchive(storageKey));
1032
1085
  const stored = readPersisted(storageKey);
1033
1086
  if (stored) {
1034
1087
  conversationTokenRef.current = stored.conversationToken;
@@ -1057,13 +1110,28 @@ function useExploreChat(options = {}) {
1057
1110
  focusProduct
1058
1111
  };
1059
1112
  if (!payload.conversationToken && payload.messages.length === 0 && !payload.focusProduct) {
1060
- window.sessionStorage.removeItem(storageKey);
1113
+ window.localStorage.removeItem(storageKey);
1061
1114
  return;
1062
1115
  }
1063
- window.sessionStorage.setItem(storageKey, JSON.stringify(payload));
1116
+ window.localStorage.setItem(storageKey, JSON.stringify(payload));
1064
1117
  } catch {
1065
1118
  }
1066
1119
  }, [persist, hydrated, storageKey, messages, focusProduct]);
1120
+ React.useEffect(() => {
1121
+ messagesRef.current = messages;
1122
+ if (!persist || !hydrated) return;
1123
+ const settled = messages.some(
1124
+ (m) => m.role === "ASSISTANT" && (m.status === "COMPLETED" || m.status === "FAILED")
1125
+ );
1126
+ if (!settled) return;
1127
+ setArchive(
1128
+ upsertArchive(storageKey, {
1129
+ token: conversationTokenRef.current,
1130
+ messages,
1131
+ focusProduct: focusProductRef.current
1132
+ })
1133
+ );
1134
+ }, [messages, persist, hydrated, storageKey]);
1067
1135
  React.useEffect(() => {
1068
1136
  let cancelled = false;
1069
1137
  const load = async () => {
@@ -1349,6 +1417,13 @@ function useExploreChat(options = {}) {
1349
1417
  void send(text);
1350
1418
  }, [cancelPolling, send]);
1351
1419
  const reset = React.useCallback(() => {
1420
+ setArchive(
1421
+ upsertArchive(storageKey, {
1422
+ token: conversationTokenRef.current,
1423
+ messages: messagesRef.current,
1424
+ focusProduct: focusProductRef.current
1425
+ })
1426
+ );
1352
1427
  cancelPolling();
1353
1428
  inFlightRef.current = false;
1354
1429
  conversationTokenRef.current = null;
@@ -1361,11 +1436,48 @@ function useExploreChat(options = {}) {
1361
1436
  setPhase(catalogRef.current.enabled ? "ready" : "disabled");
1362
1437
  if (typeof window !== "undefined") {
1363
1438
  try {
1364
- window.sessionStorage.removeItem(storageKey);
1439
+ window.localStorage.removeItem(storageKey);
1365
1440
  } catch {
1366
1441
  }
1367
1442
  }
1368
1443
  }, [cancelPolling, storageKey]);
1444
+ const openConversation = React.useCallback(
1445
+ (token) => {
1446
+ const entry = readArchive(storageKey).find((e) => e.token === token);
1447
+ if (!entry) return;
1448
+ upsertArchive(storageKey, {
1449
+ token: conversationTokenRef.current,
1450
+ messages: messagesRef.current,
1451
+ focusProduct: focusProductRef.current
1452
+ });
1453
+ cancelPolling();
1454
+ inFlightRef.current = false;
1455
+ conversationTokenRef.current = entry.token;
1456
+ turnCountRef.current = entry.messages.filter(
1457
+ (m) => m.role === "USER"
1458
+ ).length;
1459
+ setTurnCount(turnCountRef.current);
1460
+ setMessages(entry.messages);
1461
+ focusProductRef.current = entry.focusProduct;
1462
+ setFocusProductState(entry.focusProduct);
1463
+ setError(null);
1464
+ setPhase(catalogRef.current.enabled ? "ready" : "disabled");
1465
+ setArchive(readArchive(storageKey));
1466
+ },
1467
+ [cancelPolling, storageKey]
1468
+ );
1469
+ const deleteConversation = React.useCallback(
1470
+ (token) => {
1471
+ const next = readArchive(storageKey).filter((e) => e.token !== token);
1472
+ writeArchive(storageKey, next);
1473
+ setArchive(next);
1474
+ },
1475
+ [storageKey]
1476
+ );
1477
+ const clearHistory = React.useCallback(() => {
1478
+ writeArchive(storageKey, []);
1479
+ setArchive([]);
1480
+ }, [storageKey]);
1369
1481
  const setFocusProduct = React.useCallback((slug) => {
1370
1482
  focusProductRef.current = slug;
1371
1483
  setFocusProductState(slug);
@@ -1394,7 +1506,11 @@ function useExploreChat(options = {}) {
1394
1506
  stop,
1395
1507
  retry,
1396
1508
  reset,
1397
- canSend
1509
+ canSend,
1510
+ history: archive,
1511
+ openConversation,
1512
+ deleteConversation,
1513
+ clearHistory
1398
1514
  };
1399
1515
  }
1400
1516
  var optimisticCounter = 0;
@@ -1402,6 +1518,182 @@ function makeOptimisticId() {
1402
1518
  optimisticCounter += 1;
1403
1519
  return `boff-explore-local-${Date.now()}-${optimisticCounter}`;
1404
1520
  }
1521
+ function getRecognitionCtor() {
1522
+ if (typeof window === "undefined") return null;
1523
+ const w = window;
1524
+ return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
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
+ }
1557
+ function describeError(code) {
1558
+ switch (code) {
1559
+ case "not-allowed":
1560
+ case "service-not-allowed":
1561
+ return MIC_DENIED_MESSAGE;
1562
+ case "no-speech":
1563
+ return "I didn't catch anything \u2014 try again a little closer to the mic.";
1564
+ case "audio-capture":
1565
+ return "No microphone was found.";
1566
+ case "network":
1567
+ return "Speech recognition needs a network connection.";
1568
+ case "aborted":
1569
+ return "";
1570
+ default:
1571
+ return "Dictation stopped unexpectedly. You can type instead.";
1572
+ }
1573
+ }
1574
+ function useSpeechInput({
1575
+ onFinalTranscript,
1576
+ lang,
1577
+ onError
1578
+ }) {
1579
+ const [supported] = React.useState(() => getRecognitionCtor() !== null);
1580
+ const [listening, setListening] = React.useState(false);
1581
+ const [interim, setInterim] = React.useState("");
1582
+ const [error, setError] = React.useState(null);
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
+ );
1592
+ const finalRef = React.useRef(onFinalTranscript);
1593
+ const errorRef = React.useRef(onError);
1594
+ finalRef.current = onFinalTranscript;
1595
+ errorRef.current = onError;
1596
+ const stop = React.useCallback(() => {
1597
+ startTokenRef.current += 1;
1598
+ setListening(false);
1599
+ setInterim("");
1600
+ const recognition = recognitionRef.current;
1601
+ if (!recognition) return;
1602
+ try {
1603
+ recognition.stop();
1604
+ } catch {
1605
+ }
1606
+ setListening(false);
1607
+ setInterim("");
1608
+ }, []);
1609
+ const start = React.useCallback(() => {
1610
+ const Ctor = getRecognitionCtor();
1611
+ if (!Ctor) return;
1612
+ if (recognitionRef.current) stop();
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;
1624
+ }
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);
1671
+ }
1672
+ },
1673
+ [lang]
1674
+ );
1675
+ beginRecognitionRef.current = beginRecognitionImpl;
1676
+ const toggle = React.useCallback(() => {
1677
+ if (listening) stop();
1678
+ else start();
1679
+ }, [listening, start, stop]);
1680
+ React.useEffect(
1681
+ () => () => {
1682
+ const recognition = recognitionRef.current;
1683
+ if (!recognition) return;
1684
+ recognition.onresult = null;
1685
+ recognition.onerror = null;
1686
+ recognition.onend = null;
1687
+ recognition.onstart = null;
1688
+ try {
1689
+ recognition.abort();
1690
+ } catch {
1691
+ }
1692
+ },
1693
+ []
1694
+ );
1695
+ return { supported, listening, interim, error, start, stop, toggle };
1696
+ }
1405
1697
  function canonicalFromLocation() {
1406
1698
  if (typeof window === "undefined" || !window.location) return void 0;
1407
1699
  const { origin, pathname } = window.location;
@@ -1621,10 +1913,33 @@ function initialOf(value, fallback) {
1621
1913
  const source = (value || fallback).trim();
1622
1914
  return source ? source.slice(0, 1).toUpperCase() : "?";
1623
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
+ }
1624
1925
  function ReferenceCard({ reference }) {
1625
1926
  const [imageFailed, setImageFailed] = React.useState(false);
1927
+ const [logoFailed, setLogoFailed] = React.useState(false);
1626
1928
  const host = hostnameOf(reference.url);
1929
+ const logoUrl = logoUrlOf(reference.url);
1627
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;
1628
1943
  return /* @__PURE__ */ jsxRuntime.jsxs(
1629
1944
  "a",
1630
1945
  {
@@ -1632,9 +1947,10 @@ function ReferenceCard({ reference }) {
1632
1947
  href: reference.url,
1633
1948
  target: "_blank",
1634
1949
  rel: "noopener noreferrer",
1635
- 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",
1636
1951
  children: [
1637
- /* @__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(
1638
1954
  "img",
1639
1955
  {
1640
1956
  src: reference.imageUrl ?? "",
@@ -1643,17 +1959,22 @@ function ReferenceCard({ reference }) {
1643
1959
  onError: () => setImageFailed(true),
1644
1960
  className: "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
1645
1961
  }
1646
- ) : (
1647
- /* Graceful fallback: a token-derived gradient plate, so a missing or
1648
- broken preview still reads as a deliberate card, not a hole. */
1649
- /* @__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) }) })
1650
- ) }),
1651
- /* @__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: [
1652
1973
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "line-clamp-2 text-sm font-semibold leading-snug text-card-foreground", children: reference.title || host || reference.url }),
1653
- reference.description ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "line-clamp-2 text-xs leading-relaxed text-muted-foreground", children: reference.description }) : null,
1654
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-auto flex items-center gap-2 pt-2", children: [
1655
- 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,
1656
- /* @__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: [
1657
1978
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ExternalLink, { className: "h-3 w-3 shrink-0" }),
1658
1979
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: host })
1659
1980
  ] })
@@ -1739,7 +2060,8 @@ function UserBubble({ message }) {
1739
2060
  function AssistantBubble({
1740
2061
  message,
1741
2062
  activity,
1742
- onNavigate
2063
+ onNavigate,
2064
+ anchorRef
1743
2065
  }) {
1744
2066
  const streaming = message.status === "PENDING" || message.status === "RUNNING";
1745
2067
  const body = message.content || message.partialContent || "";
@@ -1747,9 +2069,10 @@ function AssistantBubble({
1747
2069
  return /* @__PURE__ */ jsxRuntime.jsxs(
1748
2070
  "div",
1749
2071
  {
2072
+ ref: anchorRef,
1750
2073
  "data-boff-explore": "assistant",
1751
2074
  "data-status": message.status,
1752
- className: "flex gap-3",
2075
+ className: "flex scroll-mt-4 gap-3",
1753
2076
  children: [
1754
2077
  /* @__PURE__ */ jsxRuntime.jsx(
1755
2078
  "span",
@@ -1954,11 +2277,32 @@ function ExplorePage({
1954
2277
  stop,
1955
2278
  retry,
1956
2279
  reset,
1957
- canSend
2280
+ canSend,
2281
+ history,
2282
+ openConversation,
2283
+ deleteConversation,
2284
+ clearHistory
1958
2285
  } = useExploreChat({ productSlug, pageUrl, onSend, examplePrompts });
1959
2286
  const [draft, setDraft] = React.useState("");
1960
2287
  const textareaRef = React.useRef(null);
2288
+ const [historyOpen, setHistoryOpen] = React.useState(false);
2289
+ const speechStopRef = React.useRef(() => void 0);
2290
+ const stopDictation = React.useCallback(() => {
2291
+ speechStopRef.current();
2292
+ }, []);
2293
+ const speech = useSpeechInput({
2294
+ onFinalTranscript: (text) => {
2295
+ setDraft((current) => {
2296
+ const joined = current.trim() === "" ? text.trimStart() : `${current.trimEnd()} ${text.trim()}`;
2297
+ return joined;
2298
+ });
2299
+ textareaRef.current?.focus();
2300
+ }
2301
+ });
2302
+ speechStopRef.current = speech.stop;
1961
2303
  const scrollRef = React.useRef(null);
2304
+ const latestAssistantRef = React.useRef(null);
2305
+ const alignedForRef = React.useRef(null);
1962
2306
  const stickToBottomRef = React.useRef(true);
1963
2307
  const composingRef = React.useRef(false);
1964
2308
  const busy = phase === "sending" || phase === "streaming";
@@ -1984,20 +2328,41 @@ function ExplorePage({
1984
2328
  if (!el) return;
1985
2329
  stickToBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
1986
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]);
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;
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]);
1987
2349
  React.useEffect(() => {
1988
2350
  if (!stickToBottomRef.current) return;
2351
+ if (latestAssistantId && alignedForRef.current === latestAssistantId)
2352
+ return;
1989
2353
  const el = scrollRef.current;
1990
2354
  if (!el) return;
1991
2355
  el.scrollTop = el.scrollHeight;
1992
- }, [messages, activity]);
2356
+ }, [activity, latestAssistantId, messages]);
1993
2357
  const submitDraft = React.useCallback(() => {
1994
2358
  const text = draft.trim();
1995
2359
  if (!text || overLimit || busy || !canSend) return;
2360
+ stopDictation();
1996
2361
  setDraft("");
1997
2362
  stickToBottomRef.current = true;
1998
2363
  void send(text);
1999
2364
  textareaRef.current?.focus();
2000
- }, [busy, canSend, draft, overLimit, send]);
2365
+ }, [busy, canSend, draft, overLimit, send, stopDictation]);
2001
2366
  const sendPrompt = React.useCallback(
2002
2367
  (prompt) => {
2003
2368
  if (!canSend || busy) return;
@@ -2070,8 +2435,96 @@ function ExplorePage({
2070
2435
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "New chat" })
2071
2436
  ]
2072
2437
  }
2073
- )
2438
+ ),
2439
+ history.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(
2440
+ Button,
2441
+ {
2442
+ "data-boff-explore": "history-toggle",
2443
+ type: "button",
2444
+ size: "sm",
2445
+ variant: "ghost",
2446
+ className: "shrink-0 text-muted-foreground",
2447
+ "aria-expanded": historyOpen,
2448
+ onClick: () => {
2449
+ setHistoryOpen((open) => !open);
2450
+ },
2451
+ children: [
2452
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.History, { className: "h-3.5 w-3.5" }),
2453
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "hidden sm:inline", children: [
2454
+ "History (",
2455
+ history.length,
2456
+ ")"
2457
+ ] })
2458
+ ]
2459
+ }
2460
+ ) : null
2074
2461
  ] }) : null,
2462
+ historyOpen && history.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
2463
+ "div",
2464
+ {
2465
+ "data-boff-explore": "history-panel",
2466
+ className: "border-b border-border bg-muted/30 px-4 py-3",
2467
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto w-full max-w-3xl", children: [
2468
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [
2469
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Your recent conversations" }),
2470
+ /* @__PURE__ */ jsxRuntime.jsxs(
2471
+ Button,
2472
+ {
2473
+ "data-boff-explore": "history-clear",
2474
+ type: "button",
2475
+ size: "sm",
2476
+ variant: "ghost",
2477
+ className: "h-7 shrink-0 text-xs text-muted-foreground hover:text-destructive",
2478
+ onClick: () => {
2479
+ clearHistory();
2480
+ setHistoryOpen(false);
2481
+ },
2482
+ children: [
2483
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3.5 w-3.5" }),
2484
+ "Clear history"
2485
+ ]
2486
+ }
2487
+ )
2488
+ ] }),
2489
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "space-y-1", children: history.map((entry) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [
2490
+ /* @__PURE__ */ jsxRuntime.jsxs(
2491
+ "button",
2492
+ {
2493
+ "data-boff-explore": "history-item",
2494
+ type: "button",
2495
+ className: "flex-1 truncate rounded-md px-2 py-1.5 text-left text-sm text-foreground transition-colors hover:bg-accent hover:text-accent-foreground",
2496
+ onClick: () => {
2497
+ openConversation(entry.token);
2498
+ setHistoryOpen(false);
2499
+ },
2500
+ children: [
2501
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: entry.title }),
2502
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-2 text-xs text-muted-foreground", children: [
2503
+ entry.messageCount,
2504
+ " message",
2505
+ entry.messageCount === 1 ? "" : "s"
2506
+ ] })
2507
+ ]
2508
+ }
2509
+ ),
2510
+ /* @__PURE__ */ jsxRuntime.jsx(
2511
+ Button,
2512
+ {
2513
+ type: "button",
2514
+ size: "icon",
2515
+ variant: "ghost",
2516
+ className: "h-7 w-7 shrink-0 text-muted-foreground hover:text-destructive",
2517
+ "aria-label": `Delete conversation: ${entry.title}`,
2518
+ onClick: () => {
2519
+ deleteConversation(entry.token);
2520
+ },
2521
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3.5 w-3.5" })
2522
+ }
2523
+ )
2524
+ ] }, entry.token)) })
2525
+ ] })
2526
+ }
2527
+ ) : null,
2075
2528
  /* @__PURE__ */ jsxRuntime.jsx(
2076
2529
  "div",
2077
2530
  {
@@ -2100,7 +2553,8 @@ function ExplorePage({
2100
2553
  {
2101
2554
  message,
2102
2555
  activity,
2103
- onNavigate
2556
+ onNavigate,
2557
+ anchorRef: message.id === latestAssistantId ? latestAssistantRef : void 0
2104
2558
  },
2105
2559
  message.id
2106
2560
  )
@@ -2146,6 +2600,23 @@ function ExplorePage({
2146
2600
  className: "max-h-[200px] min-h-[44px] flex-1 resize-none border-0 bg-transparent px-2 py-2.5 text-sm shadow-none focus-visible:ring-0 md:text-sm"
2147
2601
  }
2148
2602
  ),
2603
+ speech.supported ? /* @__PURE__ */ jsxRuntime.jsx(
2604
+ Button,
2605
+ {
2606
+ "data-boff-explore": "mic",
2607
+ "data-listening": speech.listening ? "true" : "false",
2608
+ type: "button",
2609
+ size: "icon",
2610
+ variant: speech.listening ? "default" : "ghost",
2611
+ disabled: composerDisabled,
2612
+ "aria-label": speech.listening ? "Stop dictating" : "Dictate your question",
2613
+ "aria-pressed": speech.listening,
2614
+ title: speech.listening ? "Stop dictating" : "Dictate your question",
2615
+ onClick: speech.toggle,
2616
+ className: cn(speech.listening && "animate-pulse"),
2617
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Mic, { className: "h-4 w-4" })
2618
+ }
2619
+ ) : null,
2149
2620
  busy ? /* @__PURE__ */ jsxRuntime.jsx(
2150
2621
  Button,
2151
2622
  {
@@ -2171,6 +2642,29 @@ function ExplorePage({
2171
2642
  ]
2172
2643
  }
2173
2644
  ),
2645
+ speech.listening || speech.interim !== "" ? /* @__PURE__ */ jsxRuntime.jsxs(
2646
+ "p",
2647
+ {
2648
+ "data-boff-explore": "dictation",
2649
+ className: "mt-2 flex items-center gap-2 text-xs text-muted-foreground",
2650
+ "aria-live": "polite",
2651
+ children: [
2652
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "relative flex h-2 w-2 shrink-0", children: [
2653
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75" }),
2654
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative inline-flex h-2 w-2 rounded-full bg-primary" })
2655
+ ] }),
2656
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "italic", children: speech.interim !== "" ? speech.interim : "Listening\u2026" })
2657
+ ]
2658
+ }
2659
+ ) : null,
2660
+ speech.error !== null ? /* @__PURE__ */ jsxRuntime.jsx(
2661
+ "p",
2662
+ {
2663
+ "data-boff-explore": "dictation-error",
2664
+ className: "mt-2 text-xs text-destructive",
2665
+ children: speech.error
2666
+ }
2667
+ ) : null,
2174
2668
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [
2175
2669
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "Enter to send \xB7 Shift + Enter for a new line" }),
2176
2670
  remainingToday !== null ? /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-boff-explore": "remaining", children: [
@@ -2194,39 +2688,48 @@ function ExplorePage({
2194
2688
  }
2195
2689
  )
2196
2690
  ] }),
2197
- /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "mt-2 text-xs leading-relaxed text-muted-foreground", children: [
2198
- "Answers are generated from our public documentation and can be imperfect \u2014 check anything important against the linked sources.",
2199
- captchaOn ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2200
- " ",
2201
- "This site is protected by reCAPTCHA; the Google",
2202
- " ",
2203
- /* @__PURE__ */ jsxRuntime.jsx(
2204
- "a",
2205
- {
2206
- href: "https://policies.google.com/privacy",
2207
- target: "_blank",
2208
- rel: "noopener noreferrer",
2209
- className: "underline underline-offset-2 hover:text-foreground",
2210
- children: "Privacy Policy"
2211
- }
2212
- ),
2213
- " ",
2214
- "and",
2215
- " ",
2216
- /* @__PURE__ */ jsxRuntime.jsx(
2217
- "a",
2218
- {
2219
- href: "https://policies.google.com/terms",
2220
- target: "_blank",
2221
- rel: "noopener noreferrer",
2222
- className: "underline underline-offset-2 hover:text-foreground",
2223
- children: "Terms of Service"
2224
- }
2225
- ),
2226
- " ",
2227
- "apply."
2228
- ] }) : null
2229
- ] })
2691
+ /* @__PURE__ */ jsxRuntime.jsxs(
2692
+ "p",
2693
+ {
2694
+ "data-boff-explore": "disclaimer",
2695
+ className: "mt-2 text-xs leading-relaxed text-muted-foreground",
2696
+ children: [
2697
+ "Answers are generated from our public documentation and can be imperfect \u2014 check anything important against the linked sources.",
2698
+ " ",
2699
+ "Conversations are saved in this browser so you can come back to them, and are also stored on our servers to help us improve these answers. Please don't share personal or confidential information.",
2700
+ captchaOn ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2701
+ " ",
2702
+ "This site is protected by reCAPTCHA; the Google",
2703
+ " ",
2704
+ /* @__PURE__ */ jsxRuntime.jsx(
2705
+ "a",
2706
+ {
2707
+ href: "https://policies.google.com/privacy",
2708
+ target: "_blank",
2709
+ rel: "noopener noreferrer",
2710
+ className: "underline underline-offset-2 hover:text-foreground",
2711
+ children: "Privacy Policy"
2712
+ }
2713
+ ),
2714
+ " ",
2715
+ "and",
2716
+ " ",
2717
+ /* @__PURE__ */ jsxRuntime.jsx(
2718
+ "a",
2719
+ {
2720
+ href: "https://policies.google.com/terms",
2721
+ target: "_blank",
2722
+ rel: "noopener noreferrer",
2723
+ className: "underline underline-offset-2 hover:text-foreground",
2724
+ children: "Terms of Service"
2725
+ }
2726
+ ),
2727
+ " ",
2728
+ "apply."
2729
+ ] }) : null
2730
+ ]
2731
+ }
2732
+ )
2230
2733
  ]
2231
2734
  }
2232
2735
  ) })
@@ -2236,5 +2739,6 @@ function ExplorePage({
2236
2739
  }
2237
2740
 
2238
2741
  exports.ExplorePage = ExplorePage;
2742
+ exports.logoUrlOf = logoUrlOf;
2239
2743
  //# sourceMappingURL=explore.js.map
2240
2744
  //# sourceMappingURL=explore.js.map