@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.
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
- import { createContext, useMemo, useState, useRef, useEffect, useCallback, useContext } from 'react';
3
- import { Sparkles, RotateCcw, Square, ArrowUp, BookOpenText, Layers, Link2, TriangleAlert, ExternalLink } from 'lucide-react';
2
+ import { createContext, useMemo, useState, useRef, useCallback, useEffect, useContext } from 'react';
3
+ import { Sparkles, RotateCcw, History, Trash2, Mic, Square, ArrowUp, BookOpenText, Layers, Link2, TriangleAlert, ExternalLink } from 'lucide-react';
4
4
  import ReactMarkdown from 'react-markdown';
5
5
  import rehypeSanitize from 'rehype-sanitize';
6
6
  import remarkGfm from 'remark-gfm';
@@ -780,6 +780,8 @@ async function getRecaptchaV3Token(siteKey, action = EXPLORE_RECAPTCHA_ACTION) {
780
780
 
781
781
  // src/hooks/use-explore-chat.ts
782
782
  var DEFAULT_STORAGE_KEY = "boff.explore.v1";
783
+ var HISTORY_SUFFIX = ".history";
784
+ var MAX_ARCHIVED_CONVERSATIONS = 15;
783
785
  var DEFAULT_POLL_INTERVAL_MS = 1500;
784
786
  var DEFAULT_POLL_TIMEOUT_MS = 9e4;
785
787
  var POLL_REQUEST_TIMEOUT_MS = 1e4;
@@ -918,10 +920,58 @@ function classifyExploreError(code, serverMessage) {
918
920
  retryable: true
919
921
  };
920
922
  }
923
+ function historyKey(key) {
924
+ return `${key}${HISTORY_SUFFIX}`;
925
+ }
926
+ function readArchive(key) {
927
+ if (typeof window === "undefined") return [];
928
+ try {
929
+ const raw = window.localStorage.getItem(historyKey(key));
930
+ if (!raw) return [];
931
+ const parsed = JSON.parse(raw);
932
+ if (!Array.isArray(parsed)) return [];
933
+ return parsed.filter(
934
+ (entry) => typeof entry === "object" && entry !== null && typeof entry.token === "string" && Array.isArray(entry.messages)
935
+ );
936
+ } catch {
937
+ return [];
938
+ }
939
+ }
940
+ function writeArchive(key, entries) {
941
+ if (typeof window === "undefined") return;
942
+ try {
943
+ if (entries.length === 0) {
944
+ window.localStorage.removeItem(historyKey(key));
945
+ return;
946
+ }
947
+ window.localStorage.setItem(historyKey(key), JSON.stringify(entries));
948
+ } catch {
949
+ }
950
+ }
951
+ function upsertArchive(key, thread) {
952
+ if (!thread.token) return readArchive(key);
953
+ const real = thread.messages.filter(
954
+ (m) => m.content.trim() !== "" || m.role === "USER"
955
+ );
956
+ if (real.length === 0) return readArchive(key);
957
+ const firstUser = real.find((m) => m.role === "USER");
958
+ const entry = {
959
+ token: thread.token,
960
+ title: (firstUser?.content ?? "Conversation").trim().slice(0, 80),
961
+ updatedAt: real[real.length - 1]?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
962
+ messageCount: real.length,
963
+ focusProduct: thread.focusProduct,
964
+ messages: real.slice(-20)
965
+ };
966
+ const rest = readArchive(key).filter((e) => e.token !== entry.token);
967
+ const next = [entry, ...rest].slice(0, MAX_ARCHIVED_CONVERSATIONS);
968
+ writeArchive(key, next);
969
+ return next;
970
+ }
921
971
  function readPersisted(key) {
922
972
  if (typeof window === "undefined") return null;
923
973
  try {
924
- const raw = window.sessionStorage.getItem(key);
974
+ const raw = window.localStorage.getItem(key);
925
975
  if (!raw) return null;
926
976
  const parsed = JSON.parse(raw);
927
977
  if (!parsed || typeof parsed !== "object") return null;
@@ -966,6 +1016,8 @@ function useExploreChat(options = {}) {
966
1016
  const [remainingToday, setRemainingToday] = useState(null);
967
1017
  const [focusProduct, setFocusProductState] = useState(null);
968
1018
  const [turnCount, setTurnCount] = useState(0);
1019
+ const messagesRef = useRef([]);
1020
+ const [archive, setArchive] = useState([]);
969
1021
  const [hydrated, setHydrated] = useState(false);
970
1022
  const mountedRef = useRef(true);
971
1023
  const pollGenerationRef = useRef(0);
@@ -1003,6 +1055,7 @@ function useExploreChat(options = {}) {
1003
1055
  setHydrated(true);
1004
1056
  return;
1005
1057
  }
1058
+ setArchive(readArchive(storageKey));
1006
1059
  const stored = readPersisted(storageKey);
1007
1060
  if (stored) {
1008
1061
  conversationTokenRef.current = stored.conversationToken;
@@ -1031,13 +1084,28 @@ function useExploreChat(options = {}) {
1031
1084
  focusProduct
1032
1085
  };
1033
1086
  if (!payload.conversationToken && payload.messages.length === 0 && !payload.focusProduct) {
1034
- window.sessionStorage.removeItem(storageKey);
1087
+ window.localStorage.removeItem(storageKey);
1035
1088
  return;
1036
1089
  }
1037
- window.sessionStorage.setItem(storageKey, JSON.stringify(payload));
1090
+ window.localStorage.setItem(storageKey, JSON.stringify(payload));
1038
1091
  } catch {
1039
1092
  }
1040
1093
  }, [persist, hydrated, storageKey, messages, focusProduct]);
1094
+ useEffect(() => {
1095
+ messagesRef.current = messages;
1096
+ if (!persist || !hydrated) return;
1097
+ const settled = messages.some(
1098
+ (m) => m.role === "ASSISTANT" && (m.status === "COMPLETED" || m.status === "FAILED")
1099
+ );
1100
+ if (!settled) return;
1101
+ setArchive(
1102
+ upsertArchive(storageKey, {
1103
+ token: conversationTokenRef.current,
1104
+ messages,
1105
+ focusProduct: focusProductRef.current
1106
+ })
1107
+ );
1108
+ }, [messages, persist, hydrated, storageKey]);
1041
1109
  useEffect(() => {
1042
1110
  let cancelled = false;
1043
1111
  const load = async () => {
@@ -1323,6 +1391,13 @@ function useExploreChat(options = {}) {
1323
1391
  void send(text);
1324
1392
  }, [cancelPolling, send]);
1325
1393
  const reset = useCallback(() => {
1394
+ setArchive(
1395
+ upsertArchive(storageKey, {
1396
+ token: conversationTokenRef.current,
1397
+ messages: messagesRef.current,
1398
+ focusProduct: focusProductRef.current
1399
+ })
1400
+ );
1326
1401
  cancelPolling();
1327
1402
  inFlightRef.current = false;
1328
1403
  conversationTokenRef.current = null;
@@ -1335,11 +1410,48 @@ function useExploreChat(options = {}) {
1335
1410
  setPhase(catalogRef.current.enabled ? "ready" : "disabled");
1336
1411
  if (typeof window !== "undefined") {
1337
1412
  try {
1338
- window.sessionStorage.removeItem(storageKey);
1413
+ window.localStorage.removeItem(storageKey);
1339
1414
  } catch {
1340
1415
  }
1341
1416
  }
1342
1417
  }, [cancelPolling, storageKey]);
1418
+ const openConversation = useCallback(
1419
+ (token) => {
1420
+ const entry = readArchive(storageKey).find((e) => e.token === token);
1421
+ if (!entry) return;
1422
+ upsertArchive(storageKey, {
1423
+ token: conversationTokenRef.current,
1424
+ messages: messagesRef.current,
1425
+ focusProduct: focusProductRef.current
1426
+ });
1427
+ cancelPolling();
1428
+ inFlightRef.current = false;
1429
+ conversationTokenRef.current = entry.token;
1430
+ turnCountRef.current = entry.messages.filter(
1431
+ (m) => m.role === "USER"
1432
+ ).length;
1433
+ setTurnCount(turnCountRef.current);
1434
+ setMessages(entry.messages);
1435
+ focusProductRef.current = entry.focusProduct;
1436
+ setFocusProductState(entry.focusProduct);
1437
+ setError(null);
1438
+ setPhase(catalogRef.current.enabled ? "ready" : "disabled");
1439
+ setArchive(readArchive(storageKey));
1440
+ },
1441
+ [cancelPolling, storageKey]
1442
+ );
1443
+ const deleteConversation = useCallback(
1444
+ (token) => {
1445
+ const next = readArchive(storageKey).filter((e) => e.token !== token);
1446
+ writeArchive(storageKey, next);
1447
+ setArchive(next);
1448
+ },
1449
+ [storageKey]
1450
+ );
1451
+ const clearHistory = useCallback(() => {
1452
+ writeArchive(storageKey, []);
1453
+ setArchive([]);
1454
+ }, [storageKey]);
1343
1455
  const setFocusProduct = useCallback((slug) => {
1344
1456
  focusProductRef.current = slug;
1345
1457
  setFocusProductState(slug);
@@ -1368,7 +1480,11 @@ function useExploreChat(options = {}) {
1368
1480
  stop,
1369
1481
  retry,
1370
1482
  reset,
1371
- canSend
1483
+ canSend,
1484
+ history: archive,
1485
+ openConversation,
1486
+ deleteConversation,
1487
+ clearHistory
1372
1488
  };
1373
1489
  }
1374
1490
  var optimisticCounter = 0;
@@ -1376,6 +1492,182 @@ function makeOptimisticId() {
1376
1492
  optimisticCounter += 1;
1377
1493
  return `boff-explore-local-${Date.now()}-${optimisticCounter}`;
1378
1494
  }
1495
+ function getRecognitionCtor() {
1496
+ if (typeof window === "undefined") return null;
1497
+ const w = window;
1498
+ return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
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 };
1504
+ try {
1505
+ const status = await navigator.permissions?.query({
1506
+ name: "microphone"
1507
+ });
1508
+ if (status?.state === "granted") return { ok: true };
1509
+ if (status?.state === "denied")
1510
+ return { ok: false, message: MIC_DENIED_MESSAGE };
1511
+ } catch {
1512
+ }
1513
+ try {
1514
+ const stream = await media.getUserMedia({ audio: true });
1515
+ for (const track of stream.getTracks()) track.stop();
1516
+ return { ok: true };
1517
+ } catch (error) {
1518
+ const name = typeof error === "object" && error !== null && "name" in error ? String(error.name) : "";
1519
+ if (name === "NotAllowedError" || name === "SecurityError") {
1520
+ return { ok: false, message: MIC_DENIED_MESSAGE };
1521
+ }
1522
+ if (name === "NotFoundError" || name === "DevicesNotFoundError") {
1523
+ return { ok: false, message: "No microphone was found." };
1524
+ }
1525
+ return {
1526
+ ok: false,
1527
+ message: "Dictation could not start. You can type instead."
1528
+ };
1529
+ }
1530
+ }
1531
+ function describeError(code) {
1532
+ switch (code) {
1533
+ case "not-allowed":
1534
+ case "service-not-allowed":
1535
+ return MIC_DENIED_MESSAGE;
1536
+ case "no-speech":
1537
+ return "I didn't catch anything \u2014 try again a little closer to the mic.";
1538
+ case "audio-capture":
1539
+ return "No microphone was found.";
1540
+ case "network":
1541
+ return "Speech recognition needs a network connection.";
1542
+ case "aborted":
1543
+ return "";
1544
+ default:
1545
+ return "Dictation stopped unexpectedly. You can type instead.";
1546
+ }
1547
+ }
1548
+ function useSpeechInput({
1549
+ onFinalTranscript,
1550
+ lang,
1551
+ onError
1552
+ }) {
1553
+ const [supported] = useState(() => getRecognitionCtor() !== null);
1554
+ const [listening, setListening] = useState(false);
1555
+ const [interim, setInterim] = useState("");
1556
+ const [error, setError] = useState(null);
1557
+ const recognitionRef = useRef(null);
1558
+ const startTokenRef = useRef(0);
1559
+ const beginRecognitionRef = useRef(null);
1560
+ const beginRecognition = useCallback(
1561
+ (Ctor, token) => {
1562
+ beginRecognitionRef.current?.(Ctor, token);
1563
+ },
1564
+ []
1565
+ );
1566
+ const finalRef = useRef(onFinalTranscript);
1567
+ const errorRef = useRef(onError);
1568
+ finalRef.current = onFinalTranscript;
1569
+ errorRef.current = onError;
1570
+ const stop = useCallback(() => {
1571
+ startTokenRef.current += 1;
1572
+ setListening(false);
1573
+ setInterim("");
1574
+ const recognition = recognitionRef.current;
1575
+ if (!recognition) return;
1576
+ try {
1577
+ recognition.stop();
1578
+ } catch {
1579
+ }
1580
+ setListening(false);
1581
+ setInterim("");
1582
+ }, []);
1583
+ const start = useCallback(() => {
1584
+ const Ctor = getRecognitionCtor();
1585
+ if (!Ctor) return;
1586
+ if (recognitionRef.current) stop();
1587
+ setError(null);
1588
+ setListening(true);
1589
+ startTokenRef.current += 1;
1590
+ const token = startTokenRef.current;
1591
+ void ensureMicrophoneAccess().then((access) => {
1592
+ if (token !== startTokenRef.current) return;
1593
+ if (!access.ok) {
1594
+ setListening(false);
1595
+ setError(access.message);
1596
+ errorRef.current?.(access.message);
1597
+ return;
1598
+ }
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 = 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);
1645
+ }
1646
+ },
1647
+ [lang]
1648
+ );
1649
+ beginRecognitionRef.current = beginRecognitionImpl;
1650
+ const toggle = useCallback(() => {
1651
+ if (listening) stop();
1652
+ else start();
1653
+ }, [listening, start, stop]);
1654
+ useEffect(
1655
+ () => () => {
1656
+ const recognition = recognitionRef.current;
1657
+ if (!recognition) return;
1658
+ recognition.onresult = null;
1659
+ recognition.onerror = null;
1660
+ recognition.onend = null;
1661
+ recognition.onstart = null;
1662
+ try {
1663
+ recognition.abort();
1664
+ } catch {
1665
+ }
1666
+ },
1667
+ []
1668
+ );
1669
+ return { supported, listening, interim, error, start, stop, toggle };
1670
+ }
1379
1671
  function canonicalFromLocation() {
1380
1672
  if (typeof window === "undefined" || !window.location) return void 0;
1381
1673
  const { origin, pathname } = window.location;
@@ -1595,10 +1887,33 @@ function initialOf(value, fallback) {
1595
1887
  const source = (value || fallback).trim();
1596
1888
  return source ? source.slice(0, 1).toUpperCase() : "?";
1597
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
+ }
1598
1899
  function ReferenceCard({ reference }) {
1599
1900
  const [imageFailed, setImageFailed] = useState(false);
1901
+ const [logoFailed, setLogoFailed] = useState(false);
1600
1902
  const host = hostnameOf(reference.url);
1903
+ const logoUrl = logoUrlOf(reference.url);
1601
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;
1602
1917
  return /* @__PURE__ */ jsxs(
1603
1918
  "a",
1604
1919
  {
@@ -1606,9 +1921,10 @@ function ReferenceCard({ reference }) {
1606
1921
  href: reference.url,
1607
1922
  target: "_blank",
1608
1923
  rel: "noopener noreferrer",
1609
- 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",
1610
1925
  children: [
1611
- /* @__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(
1612
1928
  "img",
1613
1929
  {
1614
1930
  src: reference.imageUrl ?? "",
@@ -1617,17 +1933,22 @@ function ReferenceCard({ reference }) {
1617
1933
  onError: () => setImageFailed(true),
1618
1934
  className: "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
1619
1935
  }
1620
- ) : (
1621
- /* Graceful fallback: a token-derived gradient plate, so a missing or
1622
- broken preview still reads as a deliberate card, not a hole. */
1623
- /* @__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) }) })
1624
- ) }),
1625
- /* @__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: [
1626
1947
  /* @__PURE__ */ jsx("p", { className: "line-clamp-2 text-sm font-semibold leading-snug text-card-foreground", children: reference.title || host || reference.url }),
1627
- reference.description ? /* @__PURE__ */ jsx("p", { className: "line-clamp-2 text-xs leading-relaxed text-muted-foreground", children: reference.description }) : null,
1628
- /* @__PURE__ */ jsxs("div", { className: "mt-auto flex items-center gap-2 pt-2", children: [
1629
- 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,
1630
- /* @__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: [
1631
1952
  /* @__PURE__ */ jsx(ExternalLink, { className: "h-3 w-3 shrink-0" }),
1632
1953
  /* @__PURE__ */ jsx("span", { className: "truncate", children: host })
1633
1954
  ] })
@@ -1713,7 +2034,8 @@ function UserBubble({ message }) {
1713
2034
  function AssistantBubble({
1714
2035
  message,
1715
2036
  activity,
1716
- onNavigate
2037
+ onNavigate,
2038
+ anchorRef
1717
2039
  }) {
1718
2040
  const streaming = message.status === "PENDING" || message.status === "RUNNING";
1719
2041
  const body = message.content || message.partialContent || "";
@@ -1721,9 +2043,10 @@ function AssistantBubble({
1721
2043
  return /* @__PURE__ */ jsxs(
1722
2044
  "div",
1723
2045
  {
2046
+ ref: anchorRef,
1724
2047
  "data-boff-explore": "assistant",
1725
2048
  "data-status": message.status,
1726
- className: "flex gap-3",
2049
+ className: "flex scroll-mt-4 gap-3",
1727
2050
  children: [
1728
2051
  /* @__PURE__ */ jsx(
1729
2052
  "span",
@@ -1928,11 +2251,32 @@ function ExplorePage({
1928
2251
  stop,
1929
2252
  retry,
1930
2253
  reset,
1931
- canSend
2254
+ canSend,
2255
+ history,
2256
+ openConversation,
2257
+ deleteConversation,
2258
+ clearHistory
1932
2259
  } = useExploreChat({ productSlug, pageUrl, onSend, examplePrompts });
1933
2260
  const [draft, setDraft] = useState("");
1934
2261
  const textareaRef = useRef(null);
2262
+ const [historyOpen, setHistoryOpen] = useState(false);
2263
+ const speechStopRef = useRef(() => void 0);
2264
+ const stopDictation = useCallback(() => {
2265
+ speechStopRef.current();
2266
+ }, []);
2267
+ const speech = useSpeechInput({
2268
+ onFinalTranscript: (text) => {
2269
+ setDraft((current) => {
2270
+ const joined = current.trim() === "" ? text.trimStart() : `${current.trimEnd()} ${text.trim()}`;
2271
+ return joined;
2272
+ });
2273
+ textareaRef.current?.focus();
2274
+ }
2275
+ });
2276
+ speechStopRef.current = speech.stop;
1935
2277
  const scrollRef = useRef(null);
2278
+ const latestAssistantRef = useRef(null);
2279
+ const alignedForRef = useRef(null);
1936
2280
  const stickToBottomRef = useRef(true);
1937
2281
  const composingRef = useRef(false);
1938
2282
  const busy = phase === "sending" || phase === "streaming";
@@ -1958,20 +2302,41 @@ function ExplorePage({
1958
2302
  if (!el) return;
1959
2303
  stickToBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
1960
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]);
2312
+ useEffect(() => {
2313
+ const el = scrollRef.current;
2314
+ const anchor = latestAssistantRef.current;
2315
+ if (!el || !anchor || !latestAssistantId) return;
2316
+ if (alignedForRef.current === latestAssistantId) return;
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]);
1961
2323
  useEffect(() => {
1962
2324
  if (!stickToBottomRef.current) return;
2325
+ if (latestAssistantId && alignedForRef.current === latestAssistantId)
2326
+ return;
1963
2327
  const el = scrollRef.current;
1964
2328
  if (!el) return;
1965
2329
  el.scrollTop = el.scrollHeight;
1966
- }, [messages, activity]);
2330
+ }, [activity, latestAssistantId, messages]);
1967
2331
  const submitDraft = useCallback(() => {
1968
2332
  const text = draft.trim();
1969
2333
  if (!text || overLimit || busy || !canSend) return;
2334
+ stopDictation();
1970
2335
  setDraft("");
1971
2336
  stickToBottomRef.current = true;
1972
2337
  void send(text);
1973
2338
  textareaRef.current?.focus();
1974
- }, [busy, canSend, draft, overLimit, send]);
2339
+ }, [busy, canSend, draft, overLimit, send, stopDictation]);
1975
2340
  const sendPrompt = useCallback(
1976
2341
  (prompt) => {
1977
2342
  if (!canSend || busy) return;
@@ -2044,8 +2409,96 @@ function ExplorePage({
2044
2409
  /* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: "New chat" })
2045
2410
  ]
2046
2411
  }
2047
- )
2412
+ ),
2413
+ history.length > 0 ? /* @__PURE__ */ jsxs(
2414
+ Button,
2415
+ {
2416
+ "data-boff-explore": "history-toggle",
2417
+ type: "button",
2418
+ size: "sm",
2419
+ variant: "ghost",
2420
+ className: "shrink-0 text-muted-foreground",
2421
+ "aria-expanded": historyOpen,
2422
+ onClick: () => {
2423
+ setHistoryOpen((open) => !open);
2424
+ },
2425
+ children: [
2426
+ /* @__PURE__ */ jsx(History, { className: "h-3.5 w-3.5" }),
2427
+ /* @__PURE__ */ jsxs("span", { className: "hidden sm:inline", children: [
2428
+ "History (",
2429
+ history.length,
2430
+ ")"
2431
+ ] })
2432
+ ]
2433
+ }
2434
+ ) : null
2048
2435
  ] }) : null,
2436
+ historyOpen && history.length > 0 ? /* @__PURE__ */ jsx(
2437
+ "div",
2438
+ {
2439
+ "data-boff-explore": "history-panel",
2440
+ className: "border-b border-border bg-muted/30 px-4 py-3",
2441
+ children: /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-3xl", children: [
2442
+ /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [
2443
+ /* @__PURE__ */ jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Your recent conversations" }),
2444
+ /* @__PURE__ */ jsxs(
2445
+ Button,
2446
+ {
2447
+ "data-boff-explore": "history-clear",
2448
+ type: "button",
2449
+ size: "sm",
2450
+ variant: "ghost",
2451
+ className: "h-7 shrink-0 text-xs text-muted-foreground hover:text-destructive",
2452
+ onClick: () => {
2453
+ clearHistory();
2454
+ setHistoryOpen(false);
2455
+ },
2456
+ children: [
2457
+ /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" }),
2458
+ "Clear history"
2459
+ ]
2460
+ }
2461
+ )
2462
+ ] }),
2463
+ /* @__PURE__ */ jsx("ul", { className: "space-y-1", children: history.map((entry) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-1", children: [
2464
+ /* @__PURE__ */ jsxs(
2465
+ "button",
2466
+ {
2467
+ "data-boff-explore": "history-item",
2468
+ type: "button",
2469
+ 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",
2470
+ onClick: () => {
2471
+ openConversation(entry.token);
2472
+ setHistoryOpen(false);
2473
+ },
2474
+ children: [
2475
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: entry.title }),
2476
+ /* @__PURE__ */ jsxs("span", { className: "ml-2 text-xs text-muted-foreground", children: [
2477
+ entry.messageCount,
2478
+ " message",
2479
+ entry.messageCount === 1 ? "" : "s"
2480
+ ] })
2481
+ ]
2482
+ }
2483
+ ),
2484
+ /* @__PURE__ */ jsx(
2485
+ Button,
2486
+ {
2487
+ type: "button",
2488
+ size: "icon",
2489
+ variant: "ghost",
2490
+ className: "h-7 w-7 shrink-0 text-muted-foreground hover:text-destructive",
2491
+ "aria-label": `Delete conversation: ${entry.title}`,
2492
+ onClick: () => {
2493
+ deleteConversation(entry.token);
2494
+ },
2495
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
2496
+ }
2497
+ )
2498
+ ] }, entry.token)) })
2499
+ ] })
2500
+ }
2501
+ ) : null,
2049
2502
  /* @__PURE__ */ jsx(
2050
2503
  "div",
2051
2504
  {
@@ -2074,7 +2527,8 @@ function ExplorePage({
2074
2527
  {
2075
2528
  message,
2076
2529
  activity,
2077
- onNavigate
2530
+ onNavigate,
2531
+ anchorRef: message.id === latestAssistantId ? latestAssistantRef : void 0
2078
2532
  },
2079
2533
  message.id
2080
2534
  )
@@ -2120,6 +2574,23 @@ function ExplorePage({
2120
2574
  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"
2121
2575
  }
2122
2576
  ),
2577
+ speech.supported ? /* @__PURE__ */ jsx(
2578
+ Button,
2579
+ {
2580
+ "data-boff-explore": "mic",
2581
+ "data-listening": speech.listening ? "true" : "false",
2582
+ type: "button",
2583
+ size: "icon",
2584
+ variant: speech.listening ? "default" : "ghost",
2585
+ disabled: composerDisabled,
2586
+ "aria-label": speech.listening ? "Stop dictating" : "Dictate your question",
2587
+ "aria-pressed": speech.listening,
2588
+ title: speech.listening ? "Stop dictating" : "Dictate your question",
2589
+ onClick: speech.toggle,
2590
+ className: cn(speech.listening && "animate-pulse"),
2591
+ children: /* @__PURE__ */ jsx(Mic, { className: "h-4 w-4" })
2592
+ }
2593
+ ) : null,
2123
2594
  busy ? /* @__PURE__ */ jsx(
2124
2595
  Button,
2125
2596
  {
@@ -2145,6 +2616,29 @@ function ExplorePage({
2145
2616
  ]
2146
2617
  }
2147
2618
  ),
2619
+ speech.listening || speech.interim !== "" ? /* @__PURE__ */ jsxs(
2620
+ "p",
2621
+ {
2622
+ "data-boff-explore": "dictation",
2623
+ className: "mt-2 flex items-center gap-2 text-xs text-muted-foreground",
2624
+ "aria-live": "polite",
2625
+ children: [
2626
+ /* @__PURE__ */ jsxs("span", { className: "relative flex h-2 w-2 shrink-0", children: [
2627
+ /* @__PURE__ */ jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75" }),
2628
+ /* @__PURE__ */ jsx("span", { className: "relative inline-flex h-2 w-2 rounded-full bg-primary" })
2629
+ ] }),
2630
+ /* @__PURE__ */ jsx("span", { className: "italic", children: speech.interim !== "" ? speech.interim : "Listening\u2026" })
2631
+ ]
2632
+ }
2633
+ ) : null,
2634
+ speech.error !== null ? /* @__PURE__ */ jsx(
2635
+ "p",
2636
+ {
2637
+ "data-boff-explore": "dictation-error",
2638
+ className: "mt-2 text-xs text-destructive",
2639
+ children: speech.error
2640
+ }
2641
+ ) : null,
2148
2642
  /* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [
2149
2643
  /* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: "Enter to send \xB7 Shift + Enter for a new line" }),
2150
2644
  remainingToday !== null ? /* @__PURE__ */ jsxs("span", { "data-boff-explore": "remaining", children: [
@@ -2168,39 +2662,48 @@ function ExplorePage({
2168
2662
  }
2169
2663
  )
2170
2664
  ] }),
2171
- /* @__PURE__ */ jsxs("p", { className: "mt-2 text-xs leading-relaxed text-muted-foreground", children: [
2172
- "Answers are generated from our public documentation and can be imperfect \u2014 check anything important against the linked sources.",
2173
- captchaOn ? /* @__PURE__ */ jsxs(Fragment, { children: [
2174
- " ",
2175
- "This site is protected by reCAPTCHA; the Google",
2176
- " ",
2177
- /* @__PURE__ */ jsx(
2178
- "a",
2179
- {
2180
- href: "https://policies.google.com/privacy",
2181
- target: "_blank",
2182
- rel: "noopener noreferrer",
2183
- className: "underline underline-offset-2 hover:text-foreground",
2184
- children: "Privacy Policy"
2185
- }
2186
- ),
2187
- " ",
2188
- "and",
2189
- " ",
2190
- /* @__PURE__ */ jsx(
2191
- "a",
2192
- {
2193
- href: "https://policies.google.com/terms",
2194
- target: "_blank",
2195
- rel: "noopener noreferrer",
2196
- className: "underline underline-offset-2 hover:text-foreground",
2197
- children: "Terms of Service"
2198
- }
2199
- ),
2200
- " ",
2201
- "apply."
2202
- ] }) : null
2203
- ] })
2665
+ /* @__PURE__ */ jsxs(
2666
+ "p",
2667
+ {
2668
+ "data-boff-explore": "disclaimer",
2669
+ className: "mt-2 text-xs leading-relaxed text-muted-foreground",
2670
+ children: [
2671
+ "Answers are generated from our public documentation and can be imperfect \u2014 check anything important against the linked sources.",
2672
+ " ",
2673
+ "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.",
2674
+ captchaOn ? /* @__PURE__ */ jsxs(Fragment, { children: [
2675
+ " ",
2676
+ "This site is protected by reCAPTCHA; the Google",
2677
+ " ",
2678
+ /* @__PURE__ */ jsx(
2679
+ "a",
2680
+ {
2681
+ href: "https://policies.google.com/privacy",
2682
+ target: "_blank",
2683
+ rel: "noopener noreferrer",
2684
+ className: "underline underline-offset-2 hover:text-foreground",
2685
+ children: "Privacy Policy"
2686
+ }
2687
+ ),
2688
+ " ",
2689
+ "and",
2690
+ " ",
2691
+ /* @__PURE__ */ jsx(
2692
+ "a",
2693
+ {
2694
+ href: "https://policies.google.com/terms",
2695
+ target: "_blank",
2696
+ rel: "noopener noreferrer",
2697
+ className: "underline underline-offset-2 hover:text-foreground",
2698
+ children: "Terms of Service"
2699
+ }
2700
+ ),
2701
+ " ",
2702
+ "apply."
2703
+ ] }) : null
2704
+ ]
2705
+ }
2706
+ )
2204
2707
  ]
2205
2708
  }
2206
2709
  ) })
@@ -2209,6 +2712,6 @@ function ExplorePage({
2209
2712
  );
2210
2713
  }
2211
2714
 
2212
- export { ExplorePage };
2715
+ export { ExplorePage, logoUrlOf };
2213
2716
  //# sourceMappingURL=explore.mjs.map
2214
2717
  //# sourceMappingURL=explore.mjs.map