@burdenoff/website-sdk 2026.828.3 → 2026.828.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/client/index.js.map +1 -1
  2. package/dist/client/index.mjs.map +1 -1
  3. package/dist/components/contact.js.map +1 -1
  4. package/dist/components/contact.mjs.map +1 -1
  5. package/dist/components/explore-cta.js +9 -7
  6. package/dist/components/explore-cta.js.map +1 -1
  7. package/dist/components/explore-cta.mjs +9 -7
  8. package/dist/components/explore-cta.mjs.map +1 -1
  9. package/dist/components/explore.js +427 -41
  10. package/dist/components/explore.js.map +1 -1
  11. package/dist/components/explore.mjs +429 -43
  12. package/dist/components/explore.mjs.map +1 -1
  13. package/dist/components/newsletter.js.map +1 -1
  14. package/dist/components/newsletter.mjs.map +1 -1
  15. package/dist/components/partners.js.map +1 -1
  16. package/dist/components/partners.mjs.map +1 -1
  17. package/dist/components/pricing.js.map +1 -1
  18. package/dist/components/pricing.mjs.map +1 -1
  19. package/dist/components/resource-links.js.map +1 -1
  20. package/dist/components/resource-links.mjs.map +1 -1
  21. package/dist/components/social-links.js.map +1 -1
  22. package/dist/components/social-links.mjs.map +1 -1
  23. package/dist/components/unsubscribe.js.map +1 -1
  24. package/dist/components/unsubscribe.mjs.map +1 -1
  25. package/dist/components/waitlist.js.map +1 -1
  26. package/dist/components/waitlist.mjs.map +1 -1
  27. package/dist/content/index.js.map +1 -1
  28. package/dist/content/index.mjs.map +1 -1
  29. package/dist/hooks/index.d.mts +24 -1
  30. package/dist/hooks/index.d.ts +24 -1
  31. package/dist/hooks/index.js +235 -5
  32. package/dist/hooks/index.js.map +1 -1
  33. package/dist/hooks/index.mjs +235 -6
  34. package/dist/hooks/index.mjs.map +1 -1
  35. package/dist/index.d.mts +1 -1
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.js +436 -48
  38. package/dist/index.js.map +1 -1
  39. package/dist/index.mjs +437 -49
  40. package/dist/index.mjs.map +1 -1
  41. package/dist/product/index.js.map +1 -1
  42. package/dist/product/index.mjs.map +1 -1
  43. package/dist/store/index.js.map +1 -1
  44. package/dist/store/index.mjs.map +1 -1
  45. package/dist/{index-CUJTTlYx.d.mts → use-explore-chat-DhvZhH8f.d.mts} +21 -2
  46. package/dist/{index-CUJTTlYx.d.ts → use-explore-chat-DhvZhH8f.d.ts} +21 -2
  47. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { createContext, useMemo, useContext, useState, useCallback, useLayoutEffect, useRef, useEffect } from 'react';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
- import { Instagram, Youtube, Facebook, Github, Linkedin, Twitter, Mail, Phone, MapPin, Clock, Send, CheckCircle, Upload, Globe, ChevronDown, Search, Rocket, ArrowRight, Download, Sparkles, RotateCcw, Square, ArrowUp, History, BookOpenText, Layers, Link2, TriangleAlert, ChevronLeft, ChevronRight, FileText, ExternalLink } from 'lucide-react';
4
+ import { Instagram, Youtube, Facebook, Github, Linkedin, Twitter, Mail, Phone, MapPin, Clock, Send, CheckCircle, Upload, Globe, ChevronDown, Search, Rocket, ArrowRight, Download, Sparkles, RotateCcw, History, Trash2, Mic, Square, ArrowUp, BookOpenText, Layers, Link2, TriangleAlert, ChevronLeft, ChevronRight, FileText, ExternalLink } from 'lucide-react';
5
5
  import ReCAPTCHA4 from 'react-google-recaptcha';
6
6
  import { toast } from 'sonner';
7
7
  import { Helmet } from 'react-helmet-async';
@@ -5246,6 +5246,8 @@ async function getRecaptchaV3Token(siteKey, action = EXPLORE_RECAPTCHA_ACTION) {
5246
5246
 
5247
5247
  // src/hooks/use-explore-chat.ts
5248
5248
  var DEFAULT_STORAGE_KEY = "boff.explore.v1";
5249
+ var HISTORY_SUFFIX = ".history";
5250
+ var MAX_ARCHIVED_CONVERSATIONS = 15;
5249
5251
  var DEFAULT_POLL_INTERVAL_MS = 1500;
5250
5252
  var DEFAULT_POLL_TIMEOUT_MS = 9e4;
5251
5253
  var POLL_REQUEST_TIMEOUT_MS = 1e4;
@@ -5384,10 +5386,58 @@ function classifyExploreError(code, serverMessage) {
5384
5386
  retryable: true
5385
5387
  };
5386
5388
  }
5389
+ function historyKey(key) {
5390
+ return `${key}${HISTORY_SUFFIX}`;
5391
+ }
5392
+ function readArchive(key) {
5393
+ if (typeof window === "undefined") return [];
5394
+ try {
5395
+ const raw = window.localStorage.getItem(historyKey(key));
5396
+ if (!raw) return [];
5397
+ const parsed = JSON.parse(raw);
5398
+ if (!Array.isArray(parsed)) return [];
5399
+ return parsed.filter(
5400
+ (entry) => typeof entry === "object" && entry !== null && typeof entry.token === "string" && Array.isArray(entry.messages)
5401
+ );
5402
+ } catch {
5403
+ return [];
5404
+ }
5405
+ }
5406
+ function writeArchive(key, entries) {
5407
+ if (typeof window === "undefined") return;
5408
+ try {
5409
+ if (entries.length === 0) {
5410
+ window.localStorage.removeItem(historyKey(key));
5411
+ return;
5412
+ }
5413
+ window.localStorage.setItem(historyKey(key), JSON.stringify(entries));
5414
+ } catch {
5415
+ }
5416
+ }
5417
+ function upsertArchive(key, thread) {
5418
+ if (!thread.token) return readArchive(key);
5419
+ const real = thread.messages.filter(
5420
+ (m) => m.content.trim() !== "" || m.role === "USER"
5421
+ );
5422
+ if (real.length === 0) return readArchive(key);
5423
+ const firstUser = real.find((m) => m.role === "USER");
5424
+ const entry = {
5425
+ token: thread.token,
5426
+ title: (firstUser?.content ?? "Conversation").trim().slice(0, 80),
5427
+ updatedAt: real[real.length - 1]?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
5428
+ messageCount: real.length,
5429
+ focusProduct: thread.focusProduct,
5430
+ messages: real.slice(-20)
5431
+ };
5432
+ const rest = readArchive(key).filter((e) => e.token !== entry.token);
5433
+ const next = [entry, ...rest].slice(0, MAX_ARCHIVED_CONVERSATIONS);
5434
+ writeArchive(key, next);
5435
+ return next;
5436
+ }
5387
5437
  function readPersisted(key) {
5388
5438
  if (typeof window === "undefined") return null;
5389
5439
  try {
5390
- const raw = window.sessionStorage.getItem(key);
5440
+ const raw = window.localStorage.getItem(key);
5391
5441
  if (!raw) return null;
5392
5442
  const parsed = JSON.parse(raw);
5393
5443
  if (!parsed || typeof parsed !== "object") return null;
@@ -5432,6 +5482,8 @@ function useExploreChat(options = {}) {
5432
5482
  const [remainingToday, setRemainingToday] = useState(null);
5433
5483
  const [focusProduct, setFocusProductState] = useState(null);
5434
5484
  const [turnCount, setTurnCount] = useState(0);
5485
+ const messagesRef = useRef([]);
5486
+ const [archive, setArchive] = useState([]);
5435
5487
  const [hydrated, setHydrated] = useState(false);
5436
5488
  const mountedRef = useRef(true);
5437
5489
  const pollGenerationRef = useRef(0);
@@ -5469,6 +5521,7 @@ function useExploreChat(options = {}) {
5469
5521
  setHydrated(true);
5470
5522
  return;
5471
5523
  }
5524
+ setArchive(readArchive(storageKey));
5472
5525
  const stored = readPersisted(storageKey);
5473
5526
  if (stored) {
5474
5527
  conversationTokenRef.current = stored.conversationToken;
@@ -5497,13 +5550,28 @@ function useExploreChat(options = {}) {
5497
5550
  focusProduct
5498
5551
  };
5499
5552
  if (!payload.conversationToken && payload.messages.length === 0 && !payload.focusProduct) {
5500
- window.sessionStorage.removeItem(storageKey);
5553
+ window.localStorage.removeItem(storageKey);
5501
5554
  return;
5502
5555
  }
5503
- window.sessionStorage.setItem(storageKey, JSON.stringify(payload));
5556
+ window.localStorage.setItem(storageKey, JSON.stringify(payload));
5504
5557
  } catch {
5505
5558
  }
5506
5559
  }, [persist, hydrated, storageKey, messages, focusProduct]);
5560
+ useEffect(() => {
5561
+ messagesRef.current = messages;
5562
+ if (!persist || !hydrated) return;
5563
+ const settled = messages.some(
5564
+ (m) => m.role === "ASSISTANT" && (m.status === "COMPLETED" || m.status === "FAILED")
5565
+ );
5566
+ if (!settled) return;
5567
+ setArchive(
5568
+ upsertArchive(storageKey, {
5569
+ token: conversationTokenRef.current,
5570
+ messages,
5571
+ focusProduct: focusProductRef.current
5572
+ })
5573
+ );
5574
+ }, [messages, persist, hydrated, storageKey]);
5507
5575
  useEffect(() => {
5508
5576
  let cancelled = false;
5509
5577
  const load = async () => {
@@ -5789,6 +5857,13 @@ function useExploreChat(options = {}) {
5789
5857
  void send(text);
5790
5858
  }, [cancelPolling, send]);
5791
5859
  const reset = useCallback(() => {
5860
+ setArchive(
5861
+ upsertArchive(storageKey, {
5862
+ token: conversationTokenRef.current,
5863
+ messages: messagesRef.current,
5864
+ focusProduct: focusProductRef.current
5865
+ })
5866
+ );
5792
5867
  cancelPolling();
5793
5868
  inFlightRef.current = false;
5794
5869
  conversationTokenRef.current = null;
@@ -5801,11 +5876,48 @@ function useExploreChat(options = {}) {
5801
5876
  setPhase(catalogRef.current.enabled ? "ready" : "disabled");
5802
5877
  if (typeof window !== "undefined") {
5803
5878
  try {
5804
- window.sessionStorage.removeItem(storageKey);
5879
+ window.localStorage.removeItem(storageKey);
5805
5880
  } catch {
5806
5881
  }
5807
5882
  }
5808
5883
  }, [cancelPolling, storageKey]);
5884
+ const openConversation = useCallback(
5885
+ (token) => {
5886
+ const entry = readArchive(storageKey).find((e) => e.token === token);
5887
+ if (!entry) return;
5888
+ upsertArchive(storageKey, {
5889
+ token: conversationTokenRef.current,
5890
+ messages: messagesRef.current,
5891
+ focusProduct: focusProductRef.current
5892
+ });
5893
+ cancelPolling();
5894
+ inFlightRef.current = false;
5895
+ conversationTokenRef.current = entry.token;
5896
+ turnCountRef.current = entry.messages.filter(
5897
+ (m) => m.role === "USER"
5898
+ ).length;
5899
+ setTurnCount(turnCountRef.current);
5900
+ setMessages(entry.messages);
5901
+ focusProductRef.current = entry.focusProduct;
5902
+ setFocusProductState(entry.focusProduct);
5903
+ setError(null);
5904
+ setPhase(catalogRef.current.enabled ? "ready" : "disabled");
5905
+ setArchive(readArchive(storageKey));
5906
+ },
5907
+ [cancelPolling, storageKey]
5908
+ );
5909
+ const deleteConversation = useCallback(
5910
+ (token) => {
5911
+ const next = readArchive(storageKey).filter((e) => e.token !== token);
5912
+ writeArchive(storageKey, next);
5913
+ setArchive(next);
5914
+ },
5915
+ [storageKey]
5916
+ );
5917
+ const clearHistory = useCallback(() => {
5918
+ writeArchive(storageKey, []);
5919
+ setArchive([]);
5920
+ }, [storageKey]);
5809
5921
  const setFocusProduct = useCallback((slug) => {
5810
5922
  focusProductRef.current = slug;
5811
5923
  setFocusProductState(slug);
@@ -5834,7 +5946,11 @@ function useExploreChat(options = {}) {
5834
5946
  stop,
5835
5947
  retry,
5836
5948
  reset,
5837
- canSend
5949
+ canSend,
5950
+ history: archive,
5951
+ openConversation,
5952
+ deleteConversation,
5953
+ clearHistory
5838
5954
  };
5839
5955
  }
5840
5956
  var optimisticCounter = 0;
@@ -5842,6 +5958,119 @@ function makeOptimisticId() {
5842
5958
  optimisticCounter += 1;
5843
5959
  return `boff-explore-local-${Date.now()}-${optimisticCounter}`;
5844
5960
  }
5961
+ function getRecognitionCtor() {
5962
+ if (typeof window === "undefined") return null;
5963
+ const w = window;
5964
+ return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
5965
+ }
5966
+ function describeError(code) {
5967
+ switch (code) {
5968
+ case "not-allowed":
5969
+ case "service-not-allowed":
5970
+ return "Microphone access was blocked. Allow it in your browser's site settings to dictate.";
5971
+ case "no-speech":
5972
+ return "I didn't catch anything \u2014 try again a little closer to the mic.";
5973
+ case "audio-capture":
5974
+ return "No microphone was found.";
5975
+ case "network":
5976
+ return "Speech recognition needs a network connection.";
5977
+ case "aborted":
5978
+ return "";
5979
+ default:
5980
+ return "Dictation stopped unexpectedly. You can type instead.";
5981
+ }
5982
+ }
5983
+ function useSpeechInput({
5984
+ onFinalTranscript,
5985
+ lang,
5986
+ onError
5987
+ }) {
5988
+ const [supported] = useState(() => getRecognitionCtor() !== null);
5989
+ const [listening, setListening] = useState(false);
5990
+ const [interim, setInterim] = useState("");
5991
+ const [error, setError] = useState(null);
5992
+ const recognitionRef = useRef(null);
5993
+ const finalRef = useRef(onFinalTranscript);
5994
+ const errorRef = useRef(onError);
5995
+ finalRef.current = onFinalTranscript;
5996
+ errorRef.current = onError;
5997
+ const stop = useCallback(() => {
5998
+ const recognition = recognitionRef.current;
5999
+ if (!recognition) return;
6000
+ try {
6001
+ recognition.stop();
6002
+ } catch {
6003
+ }
6004
+ setListening(false);
6005
+ setInterim("");
6006
+ }, []);
6007
+ const start = useCallback(() => {
6008
+ const Ctor = getRecognitionCtor();
6009
+ if (!Ctor) return;
6010
+ if (recognitionRef.current) stop();
6011
+ const recognition = new Ctor();
6012
+ recognition.lang = lang ?? (typeof navigator === "undefined" ? "en-US" : navigator.language || "en-US");
6013
+ recognition.continuous = true;
6014
+ recognition.interimResults = true;
6015
+ recognition.maxAlternatives = 1;
6016
+ recognition.onstart = () => {
6017
+ setError(null);
6018
+ setListening(true);
6019
+ };
6020
+ recognition.onresult = (event) => {
6021
+ let settled = "";
6022
+ let pending = "";
6023
+ for (let i = event.resultIndex; i < event.results.length; i += 1) {
6024
+ const result = event.results[i];
6025
+ if (!result) continue;
6026
+ const text = result[0]?.transcript ?? "";
6027
+ if (result.isFinal) settled += text;
6028
+ else pending += text;
6029
+ }
6030
+ setInterim(pending);
6031
+ if (settled.trim() !== "") finalRef.current(settled);
6032
+ };
6033
+ recognition.onerror = (event) => {
6034
+ const message = describeError(event.error);
6035
+ setListening(false);
6036
+ setInterim("");
6037
+ if (message !== "") {
6038
+ setError(message);
6039
+ errorRef.current?.(message);
6040
+ }
6041
+ };
6042
+ recognition.onend = () => {
6043
+ setListening(false);
6044
+ setInterim("");
6045
+ };
6046
+ recognitionRef.current = recognition;
6047
+ try {
6048
+ recognition.start();
6049
+ } catch {
6050
+ setListening(false);
6051
+ }
6052
+ }, [lang, stop]);
6053
+ const toggle = useCallback(() => {
6054
+ if (listening) stop();
6055
+ else start();
6056
+ }, [listening, start, stop]);
6057
+ useEffect(
6058
+ () => () => {
6059
+ const recognition = recognitionRef.current;
6060
+ if (!recognition) return;
6061
+ recognition.onresult = null;
6062
+ recognition.onerror = null;
6063
+ recognition.onend = null;
6064
+ recognition.onstart = null;
6065
+ try {
6066
+ recognition.abort();
6067
+ } catch {
6068
+ }
6069
+ },
6070
+ []
6071
+ );
6072
+ return { supported, listening, interim, error, start, stop, toggle };
6073
+ }
5845
6074
  var EXPLORE_CSS = `
5846
6075
  @keyframes boff-explore-pulse {
5847
6076
  0%, 80%, 100% { opacity: 0.25; transform: translateY(0); }
@@ -6269,10 +6498,29 @@ function ExplorePage({
6269
6498
  stop,
6270
6499
  retry,
6271
6500
  reset,
6272
- canSend
6501
+ canSend,
6502
+ history,
6503
+ openConversation,
6504
+ deleteConversation,
6505
+ clearHistory
6273
6506
  } = useExploreChat({ productSlug, pageUrl, onSend, examplePrompts });
6274
6507
  const [draft, setDraft] = useState("");
6275
6508
  const textareaRef = useRef(null);
6509
+ const [historyOpen, setHistoryOpen] = useState(false);
6510
+ const speechStopRef = useRef(() => void 0);
6511
+ const stopDictation = useCallback(() => {
6512
+ speechStopRef.current();
6513
+ }, []);
6514
+ const speech = useSpeechInput({
6515
+ onFinalTranscript: (text) => {
6516
+ setDraft((current) => {
6517
+ const joined = current.trim() === "" ? text.trimStart() : `${current.trimEnd()} ${text.trim()}`;
6518
+ return joined;
6519
+ });
6520
+ textareaRef.current?.focus();
6521
+ }
6522
+ });
6523
+ speechStopRef.current = speech.stop;
6276
6524
  const scrollRef = useRef(null);
6277
6525
  const stickToBottomRef = useRef(true);
6278
6526
  const composingRef = useRef(false);
@@ -6308,11 +6556,12 @@ function ExplorePage({
6308
6556
  const submitDraft = useCallback(() => {
6309
6557
  const text = draft.trim();
6310
6558
  if (!text || overLimit || busy || !canSend) return;
6559
+ stopDictation();
6311
6560
  setDraft("");
6312
6561
  stickToBottomRef.current = true;
6313
6562
  void send(text);
6314
6563
  textareaRef.current?.focus();
6315
- }, [busy, canSend, draft, overLimit, send]);
6564
+ }, [busy, canSend, draft, overLimit, send, stopDictation]);
6316
6565
  const sendPrompt = useCallback(
6317
6566
  (prompt) => {
6318
6567
  if (!canSend || busy) return;
@@ -6385,8 +6634,96 @@ function ExplorePage({
6385
6634
  /* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: "New chat" })
6386
6635
  ]
6387
6636
  }
6388
- )
6637
+ ),
6638
+ history.length > 0 ? /* @__PURE__ */ jsxs(
6639
+ Button,
6640
+ {
6641
+ "data-boff-explore": "history-toggle",
6642
+ type: "button",
6643
+ size: "sm",
6644
+ variant: "ghost",
6645
+ className: "shrink-0 text-muted-foreground",
6646
+ "aria-expanded": historyOpen,
6647
+ onClick: () => {
6648
+ setHistoryOpen((open) => !open);
6649
+ },
6650
+ children: [
6651
+ /* @__PURE__ */ jsx(History, { className: "h-3.5 w-3.5" }),
6652
+ /* @__PURE__ */ jsxs("span", { className: "hidden sm:inline", children: [
6653
+ "History (",
6654
+ history.length,
6655
+ ")"
6656
+ ] })
6657
+ ]
6658
+ }
6659
+ ) : null
6389
6660
  ] }) : null,
6661
+ historyOpen && history.length > 0 ? /* @__PURE__ */ jsx(
6662
+ "div",
6663
+ {
6664
+ "data-boff-explore": "history-panel",
6665
+ className: "border-b border-border bg-muted/30 px-4 py-3",
6666
+ children: /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-3xl", children: [
6667
+ /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [
6668
+ /* @__PURE__ */ jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Your recent conversations" }),
6669
+ /* @__PURE__ */ jsxs(
6670
+ Button,
6671
+ {
6672
+ "data-boff-explore": "history-clear",
6673
+ type: "button",
6674
+ size: "sm",
6675
+ variant: "ghost",
6676
+ className: "h-7 shrink-0 text-xs text-muted-foreground hover:text-destructive",
6677
+ onClick: () => {
6678
+ clearHistory();
6679
+ setHistoryOpen(false);
6680
+ },
6681
+ children: [
6682
+ /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" }),
6683
+ "Clear history"
6684
+ ]
6685
+ }
6686
+ )
6687
+ ] }),
6688
+ /* @__PURE__ */ jsx("ul", { className: "space-y-1", children: history.map((entry) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-1", children: [
6689
+ /* @__PURE__ */ jsxs(
6690
+ "button",
6691
+ {
6692
+ "data-boff-explore": "history-item",
6693
+ type: "button",
6694
+ 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",
6695
+ onClick: () => {
6696
+ openConversation(entry.token);
6697
+ setHistoryOpen(false);
6698
+ },
6699
+ children: [
6700
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: entry.title }),
6701
+ /* @__PURE__ */ jsxs("span", { className: "ml-2 text-xs text-muted-foreground", children: [
6702
+ entry.messageCount,
6703
+ " message",
6704
+ entry.messageCount === 1 ? "" : "s"
6705
+ ] })
6706
+ ]
6707
+ }
6708
+ ),
6709
+ /* @__PURE__ */ jsx(
6710
+ Button,
6711
+ {
6712
+ type: "button",
6713
+ size: "icon",
6714
+ variant: "ghost",
6715
+ className: "h-7 w-7 shrink-0 text-muted-foreground hover:text-destructive",
6716
+ "aria-label": `Delete conversation: ${entry.title}`,
6717
+ onClick: () => {
6718
+ deleteConversation(entry.token);
6719
+ },
6720
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
6721
+ }
6722
+ )
6723
+ ] }, entry.token)) })
6724
+ ] })
6725
+ }
6726
+ ) : null,
6390
6727
  /* @__PURE__ */ jsx(
6391
6728
  "div",
6392
6729
  {
@@ -6461,6 +6798,23 @@ function ExplorePage({
6461
6798
  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"
6462
6799
  }
6463
6800
  ),
6801
+ speech.supported ? /* @__PURE__ */ jsx(
6802
+ Button,
6803
+ {
6804
+ "data-boff-explore": "mic",
6805
+ "data-listening": speech.listening ? "true" : "false",
6806
+ type: "button",
6807
+ size: "icon",
6808
+ variant: speech.listening ? "default" : "ghost",
6809
+ disabled: composerDisabled,
6810
+ "aria-label": speech.listening ? "Stop dictating" : "Dictate your question",
6811
+ "aria-pressed": speech.listening,
6812
+ title: speech.listening ? "Stop dictating" : "Dictate your question",
6813
+ onClick: speech.toggle,
6814
+ className: cn(speech.listening && "animate-pulse"),
6815
+ children: /* @__PURE__ */ jsx(Mic, { className: "h-4 w-4" })
6816
+ }
6817
+ ) : null,
6464
6818
  busy ? /* @__PURE__ */ jsx(
6465
6819
  Button,
6466
6820
  {
@@ -6486,6 +6840,29 @@ function ExplorePage({
6486
6840
  ]
6487
6841
  }
6488
6842
  ),
6843
+ speech.listening || speech.interim !== "" ? /* @__PURE__ */ jsxs(
6844
+ "p",
6845
+ {
6846
+ "data-boff-explore": "dictation",
6847
+ className: "mt-2 flex items-center gap-2 text-xs text-muted-foreground",
6848
+ "aria-live": "polite",
6849
+ children: [
6850
+ /* @__PURE__ */ jsxs("span", { className: "relative flex h-2 w-2 shrink-0", children: [
6851
+ /* @__PURE__ */ jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75" }),
6852
+ /* @__PURE__ */ jsx("span", { className: "relative inline-flex h-2 w-2 rounded-full bg-primary" })
6853
+ ] }),
6854
+ /* @__PURE__ */ jsx("span", { className: "italic", children: speech.interim !== "" ? speech.interim : "Listening\u2026" })
6855
+ ]
6856
+ }
6857
+ ) : null,
6858
+ speech.error !== null ? /* @__PURE__ */ jsx(
6859
+ "p",
6860
+ {
6861
+ "data-boff-explore": "dictation-error",
6862
+ className: "mt-2 text-xs text-destructive",
6863
+ children: speech.error
6864
+ }
6865
+ ) : null,
6489
6866
  /* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [
6490
6867
  /* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: "Enter to send \xB7 Shift + Enter for a new line" }),
6491
6868
  remainingToday !== null ? /* @__PURE__ */ jsxs("span", { "data-boff-explore": "remaining", children: [
@@ -6509,39 +6886,48 @@ function ExplorePage({
6509
6886
  }
6510
6887
  )
6511
6888
  ] }),
6512
- /* @__PURE__ */ jsxs("p", { className: "mt-2 text-xs leading-relaxed text-muted-foreground", children: [
6513
- "Answers are generated from our public documentation and can be imperfect \u2014 check anything important against the linked sources.",
6514
- captchaOn ? /* @__PURE__ */ jsxs(Fragment, { children: [
6515
- " ",
6516
- "This site is protected by reCAPTCHA; the Google",
6517
- " ",
6518
- /* @__PURE__ */ jsx(
6519
- "a",
6520
- {
6521
- href: "https://policies.google.com/privacy",
6522
- target: "_blank",
6523
- rel: "noopener noreferrer",
6524
- className: "underline underline-offset-2 hover:text-foreground",
6525
- children: "Privacy Policy"
6526
- }
6527
- ),
6528
- " ",
6529
- "and",
6530
- " ",
6531
- /* @__PURE__ */ jsx(
6532
- "a",
6533
- {
6534
- href: "https://policies.google.com/terms",
6535
- target: "_blank",
6536
- rel: "noopener noreferrer",
6537
- className: "underline underline-offset-2 hover:text-foreground",
6538
- children: "Terms of Service"
6539
- }
6540
- ),
6541
- " ",
6542
- "apply."
6543
- ] }) : null
6544
- ] })
6889
+ /* @__PURE__ */ jsxs(
6890
+ "p",
6891
+ {
6892
+ "data-boff-explore": "disclaimer",
6893
+ className: "mt-2 text-xs leading-relaxed text-muted-foreground",
6894
+ children: [
6895
+ "Answers are generated from our public documentation and can be imperfect \u2014 check anything important against the linked sources.",
6896
+ " ",
6897
+ "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.",
6898
+ captchaOn ? /* @__PURE__ */ jsxs(Fragment, { children: [
6899
+ " ",
6900
+ "This site is protected by reCAPTCHA; the Google",
6901
+ " ",
6902
+ /* @__PURE__ */ jsx(
6903
+ "a",
6904
+ {
6905
+ href: "https://policies.google.com/privacy",
6906
+ target: "_blank",
6907
+ rel: "noopener noreferrer",
6908
+ className: "underline underline-offset-2 hover:text-foreground",
6909
+ children: "Privacy Policy"
6910
+ }
6911
+ ),
6912
+ " ",
6913
+ "and",
6914
+ " ",
6915
+ /* @__PURE__ */ jsx(
6916
+ "a",
6917
+ {
6918
+ href: "https://policies.google.com/terms",
6919
+ target: "_blank",
6920
+ rel: "noopener noreferrer",
6921
+ className: "underline underline-offset-2 hover:text-foreground",
6922
+ children: "Terms of Service"
6923
+ }
6924
+ ),
6925
+ " ",
6926
+ "apply."
6927
+ ] }) : null
6928
+ ]
6929
+ }
6930
+ )
6545
6931
  ]
6546
6932
  }
6547
6933
  ) })
@@ -6591,11 +6977,12 @@ function ExploreCta({
6591
6977
  onNavigate(href);
6592
6978
  };
6593
6979
  const classes = {
6594
- // `shrink-0` matters: this sits in a host header's flex row, and without it the
6595
- // surrounding nav gets squeezed and its links wrap onto two lines. The label is hidden
6596
- // below 2xl for the same reason most product headers are already close to full at
6597
- // 1440px, and a 90px pill there pushes them over.
6598
- header: "inline-flex shrink-0 items-center gap-1.5 rounded-full bg-gradient-to-r from-primary to-primary/70 px-2.5 py-1.5 text-sm font-medium text-primary-foreground shadow-sm transition-opacity hover:opacity-90 2xl:px-3.5",
6980
+ // A quiet icon control, deliberately NOT a second gradient pill: a header should carry one
6981
+ // primary CTA ("Get started"), and a competing coloured button next to it reads as clutter.
6982
+ // This matches the theme toggle's visual weight, so the row scans as [utilities] [CTA].
6983
+ // The prominent, labelled entry point to /explore is the floating bubble, which is visible
6984
+ // on every page without scrolling. `shrink-0` keeps it from squeezing the nav.
6985
+ header: "inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground",
6599
6986
  mobile: "inline-flex w-full items-center gap-2 rounded-lg border border-border bg-card px-3 py-2 text-sm font-medium text-card-foreground transition-colors hover:bg-accent hover:text-accent-foreground",
6600
6987
  floating: "fixed bottom-5 right-5 z-40 inline-flex items-center gap-2 rounded-full bg-gradient-to-r from-primary to-primary/70 px-4 py-3 text-sm font-medium text-primary-foreground shadow-lg transition-opacity hover:opacity-90"
6601
6988
  };
@@ -6623,7 +7010,7 @@ function ExploreCta({
6623
7010
  {
6624
7011
  className: cn(
6625
7012
  "shrink-0",
6626
- variant === "floating" ? "h-4 w-4" : "h-3.5 w-3.5"
7013
+ variant === "floating" ? "h-4 w-4" : variant === "header" ? "h-5 w-5" : "h-3.5 w-3.5"
6627
7014
  ),
6628
7015
  "aria-hidden": "true"
6629
7016
  }
@@ -6633,7 +7020,8 @@ function ExploreCta({
6633
7020
  {
6634
7021
  className: cn(
6635
7022
  variant === "floating" && "hidden sm:inline",
6636
- variant === "header" && "hidden 2xl:inline"
7023
+ // Never labelled in the header see the class comment above.
7024
+ variant === "header" && "hidden"
6637
7025
  ),
6638
7026
  children: label
6639
7027
  }