@burdenoff/website-sdk 2026.922.4 → 2026.922.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.
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
- import { createContext, useMemo, useContext, useState, useCallback, useLayoutEffect, useRef, useEffect, useId, version } from 'react';
2
+ import { createContext, useMemo, useContext, useState, useCallback, useLayoutEffect, useRef, useEffect, useId, version, Children, isValidElement, cloneElement } 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, ImageIcon, ChevronRight, Maximize2, Check, ArrowLeft, Sparkles, RotateCcw, Link2, History, Trash2, Paperclip, X, Mic, Square, ArrowUp, Info, ChevronLeft, BookOpenText, Layers, TriangleAlert, FileText, ExternalLink, Copy, ThumbsUp, ThumbsDown } from 'lucide-react';
4
+ import { Instagram, Youtube, Facebook, Github, Linkedin, Twitter, Mail, Phone, MapPin, Clock, Send, CheckCircle, Upload, Globe, ChevronDown, Search, Rocket, ArrowRight, Download, ImageIcon, ChevronRight, Maximize2, Check, ArrowLeft, Sparkles, RotateCcw, Link2, History, Trash2, Paperclip, X, Mic, Square, ArrowUp, Info, ChevronLeft, BookOpenText, Layers, TriangleAlert, FileText, ExternalLink, Copy, ThumbsUp, ThumbsDown, Volume2 } 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';
@@ -2408,16 +2408,16 @@ function useRateLimit(maxAttempts, windowMinutes) {
2408
2408
  }
2409
2409
  }, [maxAttempts]);
2410
2410
  function recordAttempt() {
2411
- const now = Date.now();
2411
+ const now2 = Date.now();
2412
2412
  const state = rateLimitRef.current;
2413
- if (state && now > state.resetTime) {
2413
+ if (state && now2 > state.resetTime) {
2414
2414
  rateLimitRef.current = null;
2415
2415
  }
2416
2416
  const current = rateLimitRef.current;
2417
2417
  if (!current) {
2418
2418
  rateLimitRef.current = {
2419
2419
  attempts: 1,
2420
- resetTime: now + windowMinutes * 60 * 1e3
2420
+ resetTime: now2 + windowMinutes * 60 * 1e3
2421
2421
  };
2422
2422
  return false;
2423
2423
  }
@@ -3512,16 +3512,16 @@ function useRateLimit2(maxAttempts, windowMinutes) {
3512
3512
  const [isLimited, setIsLimited] = useState(false);
3513
3513
  const [resetTime, setResetTime] = useState(null);
3514
3514
  function recordAttempt() {
3515
- const now = Date.now();
3515
+ const now2 = Date.now();
3516
3516
  const state = rateLimitRef.current;
3517
- if (state && now > state.resetTime) {
3517
+ if (state && now2 > state.resetTime) {
3518
3518
  rateLimitRef.current = null;
3519
3519
  }
3520
3520
  const current = rateLimitRef.current;
3521
3521
  if (!current) {
3522
3522
  rateLimitRef.current = {
3523
3523
  attempts: 1,
3524
- resetTime: now + windowMinutes * 60 * 1e3
3524
+ resetTime: now2 + windowMinutes * 60 * 1e3
3525
3525
  };
3526
3526
  return false;
3527
3527
  }
@@ -4555,8 +4555,8 @@ function LaunchCountdown({
4555
4555
  launchedMessage = "We have launched!",
4556
4556
  className
4557
4557
  }) {
4558
- const [now, setNow] = useState(() => Date.now());
4559
- const remainingMs = launchDate - now;
4558
+ const [now2, setNow] = useState(() => Date.now());
4559
+ const remainingMs = launchDate - now2;
4560
4560
  const hasLaunched = remainingMs <= 0;
4561
4561
  useEffect(() => {
4562
4562
  if (hasLaunched) return;
@@ -5980,6 +5980,9 @@ var EN = {
5980
5980
  conceptAskQuestion: "How does {title} work in {product}?",
5981
5981
  copyAnswer: "Copy answer",
5982
5982
  copyFailed: "Couldn't copy",
5983
+ readAloud: "Read aloud",
5984
+ stopReading: "Stop reading",
5985
+ answerTable: "Table",
5983
5986
  groundedIn: "What this answer is based on",
5984
5987
  sourceCountOne: "1 source",
5985
5988
  sourceCountMany: "{count} sources",
@@ -6062,6 +6065,9 @@ var ZH = {
6062
6065
  conceptAskQuestion: "{product} \u7684 {title} \u662F\u600E\u4E48\u8FD0\u4F5C\u7684\uFF1F",
6063
6066
  copyAnswer: "\u590D\u5236\u56DE\u7B54",
6064
6067
  copyFailed: "\u590D\u5236\u5931\u8D25",
6068
+ readAloud: "\u6717\u8BFB",
6069
+ stopReading: "\u505C\u6B62\u6717\u8BFB",
6070
+ answerTable: "\u8868\u683C",
6065
6071
  groundedIn: "\u8FD9\u4E2A\u56DE\u7B54\u7684\u4F9D\u636E",
6066
6072
  sourceCountOne: "1 \u4E2A\u6765\u6E90",
6067
6073
  sourceCountMany: "{count} \u4E2A\u6765\u6E90",
@@ -6140,6 +6146,9 @@ var HI = {
6140
6146
  conceptAskQuestion: "{product} \u092E\u0947\u0902 {title} \u0915\u0948\u0938\u0947 \u0915\u093E\u092E \u0915\u0930\u0924\u093E \u0939\u0948?",
6141
6147
  copyAnswer: "\u0909\u0924\u094D\u0924\u0930 \u0915\u0949\u092A\u0940 \u0915\u0930\u0947\u0902",
6142
6148
  copyFailed: "\u0915\u0949\u092A\u0940 \u0928\u0939\u0940\u0902 \u0939\u094B \u0938\u0915\u093E",
6149
+ readAloud: "\u091C\u093C\u094B\u0930 \u0938\u0947 \u092A\u0922\u093C\u0947\u0902",
6150
+ stopReading: "\u092A\u0922\u093C\u0928\u093E \u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
6151
+ answerTable: "\u0924\u093E\u0932\u093F\u0915\u093E",
6143
6152
  groundedIn: "\u092F\u0939 \u0909\u0924\u094D\u0924\u0930 \u0915\u093F\u0938 \u092A\u0930 \u0906\u0927\u093E\u0930\u093F\u0924 \u0939\u0948",
6144
6153
  sourceCountOne: "1 \u0938\u094D\u0930\u094B\u0924",
6145
6154
  sourceCountMany: "{count} \u0938\u094D\u0930\u094B\u0924",
@@ -6222,6 +6231,9 @@ var ES = {
6222
6231
  conceptAskQuestion: "\xBFC\xF3mo funciona {title} en {product}?",
6223
6232
  copyAnswer: "Copiar respuesta",
6224
6233
  copyFailed: "No se pudo copiar",
6234
+ readAloud: "Leer en voz alta",
6235
+ stopReading: "Dejar de leer",
6236
+ answerTable: "Tabla",
6225
6237
  groundedIn: "En qu\xE9 se basa esta respuesta",
6226
6238
  sourceCountOne: "1 fuente",
6227
6239
  sourceCountMany: "{count} fuentes",
@@ -6304,6 +6316,9 @@ var AR = {
6304
6316
  conceptAskQuestion: "\u0643\u064A\u0641 \u064A\u0639\u0645\u0644 {title} \u0641\u064A {product}\u061F",
6305
6317
  copyAnswer: "\u0646\u0633\u062E \u0627\u0644\u0625\u062C\u0627\u0628\u0629",
6306
6318
  copyFailed: "\u062A\u0639\u0630\u0651\u0631 \u0627\u0644\u0646\u0633\u062E",
6319
+ readAloud: "\u0627\u0642\u0631\u0623 \u0628\u0635\u0648\u062A \u0639\u0627\u0644\u064D",
6320
+ stopReading: "\u0625\u064A\u0642\u0627\u0641 \u0627\u0644\u0642\u0631\u0627\u0621\u0629",
6321
+ answerTable: "\u062C\u062F\u0648\u0644",
6307
6322
  groundedIn: "\u0639\u0644\u0649 \u0645\u0627\u0630\u0627 \u062A\u0633\u062A\u0646\u062F \u0647\u0630\u0647 \u0627\u0644\u0625\u062C\u0627\u0628\u0629",
6308
6323
  sourceCountOne: "\u0645\u0635\u062F\u0631 \u0648\u0627\u062D\u062F",
6309
6324
  sourceCountMany: "{count} \u0645\u0635\u0627\u062F\u0631",
@@ -6386,6 +6401,9 @@ var TA = {
6386
6401
  conceptAskQuestion: "{product} \u0B87\u0BB2\u0BCD {title} \u0B8E\u0BAA\u0BCD\u0BAA\u0B9F\u0BBF \u0BB5\u0BC7\u0BB2\u0BC8 \u0B9A\u0BC6\u0BAF\u0BCD\u0B95\u0BBF\u0BB1\u0BA4\u0BC1?",
6387
6402
  copyAnswer: "\u0BAA\u0BA4\u0BBF\u0BB2\u0BC8 \u0BA8\u0B95\u0BB2\u0BC6\u0B9F\u0BC1",
6388
6403
  copyFailed: "\u0BA8\u0B95\u0BB2\u0BC6\u0B9F\u0BC1\u0B95\u0BCD\u0B95 \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8",
6404
+ readAloud: "\u0B9A\u0BA4\u0BCD\u0BA4\u0BAE\u0BBE\u0B95\u0BAA\u0BCD \u0BAA\u0B9F\u0BBF",
6405
+ stopReading: "\u0BAA\u0B9F\u0BBF\u0BAA\u0BCD\u0BAA\u0BA4\u0BC8 \u0BA8\u0BBF\u0BB1\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1",
6406
+ answerTable: "\u0B85\u0B9F\u0BCD\u0B9F\u0BB5\u0BA3\u0BC8",
6389
6407
  groundedIn: "\u0B87\u0BA8\u0BCD\u0BA4\u0BAA\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BCD \u0B8E\u0BA4\u0BC8 \u0B85\u0B9F\u0BBF\u0BAA\u0BCD\u0BAA\u0B9F\u0BC8\u0BAF\u0BBE\u0B95\u0B95\u0BCD \u0B95\u0BCA\u0BA3\u0BCD\u0B9F\u0BA4\u0BC1",
6390
6408
  sourceCountOne: "1 \u0BAE\u0BC2\u0BB2\u0BAE\u0BCD",
6391
6409
  sourceCountMany: "{count} \u0BAE\u0BC2\u0BB2\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
@@ -6417,6 +6435,17 @@ var STRINGS = {
6417
6435
  ar: AR,
6418
6436
  ta: TA
6419
6437
  };
6438
+ var SPEECH_TAGS = {
6439
+ en: "en-US",
6440
+ zh: "zh-CN",
6441
+ hi: "hi-IN",
6442
+ es: "es-ES",
6443
+ ar: "ar-SA",
6444
+ ta: "ta-IN"
6445
+ };
6446
+ function speechLocaleOf(locale) {
6447
+ return SPEECH_TAGS[locale];
6448
+ }
6420
6449
  function resolveExploreLocale(explicit) {
6421
6450
  const candidates = [
6422
6451
  explicit,
@@ -7924,6 +7953,224 @@ function makeOptimisticId() {
7924
7953
  optimisticCounter += 1;
7925
7954
  return `boff-explore-local-${Date.now()}-${optimisticCounter}`;
7926
7955
  }
7956
+ var DRAIN_TARGET_MS = 1200;
7957
+ var MIN_CHARS_PER_SECOND = 90;
7958
+ var MAX_CHARS_PER_SECOND = 2200;
7959
+ var MAX_STEP_MS = 250;
7960
+ var COMMIT_INTERVAL_MS = 45;
7961
+ var FALLBACK_STEP_MS = 50;
7962
+ var FENCE = /^ {0,3}(?:```|~~~)/;
7963
+ var TABLE_ROW = /^ {0,3}\|/;
7964
+ var EMPTY_BLOCK_MARKER = /^ {0,3}(?:[-+>]|#{1,6}|\d{1,9}[.)])\s*$/;
7965
+ function isWordCharacter(char) {
7966
+ return char !== void 0 && /[\p{L}\p{N}]/u.test(char);
7967
+ }
7968
+ function clamp(value, min, max) {
7969
+ return Math.min(Math.max(value, min), max);
7970
+ }
7971
+ function now() {
7972
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
7973
+ }
7974
+ function isHighSurrogate(code) {
7975
+ return code >= 55296 && code <= 56319;
7976
+ }
7977
+ function safeInlineEnd(line) {
7978
+ let codeSpan = null;
7979
+ let link = null;
7980
+ const emphasis = [];
7981
+ let at = 0;
7982
+ while (at < line.length) {
7983
+ const char = line[at] ?? "";
7984
+ if (char === "\\") {
7985
+ at += 2;
7986
+ continue;
7987
+ }
7988
+ if (char === "`") {
7989
+ let run = 1;
7990
+ while (line[at + run] === "`") run += 1;
7991
+ codeSpan = codeSpan === null ? at : null;
7992
+ at += run;
7993
+ continue;
7994
+ }
7995
+ if (codeSpan !== null) {
7996
+ at += 1;
7997
+ continue;
7998
+ }
7999
+ if (char === "[") {
8000
+ if (link === null) link = line[at - 1] === "!" ? at - 1 : at;
8001
+ at += 1;
8002
+ continue;
8003
+ }
8004
+ if (char === "]") {
8005
+ if (link !== null && line[at + 1] !== "(") link = null;
8006
+ at += 1;
8007
+ continue;
8008
+ }
8009
+ if (char === ")") {
8010
+ link = null;
8011
+ at += 1;
8012
+ continue;
8013
+ }
8014
+ if (char === "*" || char === "_" || char === "~") {
8015
+ let run = 1;
8016
+ while (line[at + run] === char) run += 1;
8017
+ const start = at;
8018
+ at += run;
8019
+ const before = line[start - 1];
8020
+ const after = line[start + run];
8021
+ if (run < 2) continue;
8022
+ if (char === "_" && isWordCharacter(before) && isWordCharacter(after)) {
8023
+ continue;
8024
+ }
8025
+ const marker = char.repeat(run);
8026
+ const open2 = emphasis.findIndex((entry) => entry.marker === marker);
8027
+ if (open2 !== -1) {
8028
+ emphasis.splice(open2, 1);
8029
+ continue;
8030
+ }
8031
+ if (after !== void 0 && /\s/.test(after)) continue;
8032
+ emphasis.push({ marker, start });
8033
+ continue;
8034
+ }
8035
+ at += 1;
8036
+ }
8037
+ const open = [codeSpan, link, emphasis[0]?.start ?? null].filter(
8038
+ (value) => value !== null
8039
+ );
8040
+ return open.length === 0 ? line.length : Math.min(...open);
8041
+ }
8042
+ function revealBoundary(text, limit) {
8043
+ if (limit <= 0) return 0;
8044
+ const caughtUp = limit >= text.length;
8045
+ const head = caughtUp ? text : text.slice(0, limit);
8046
+ const lineStart = head.lastIndexOf("\n") + 1;
8047
+ const line = head.slice(lineStart);
8048
+ let fenceStart = null;
8049
+ let tableStart = null;
8050
+ let tableRows = 0;
8051
+ let at = 0;
8052
+ while (at < lineStart) {
8053
+ const breakAt = head.indexOf("\n", at);
8054
+ const end = breakAt === -1 ? lineStart : breakAt;
8055
+ const row = head.slice(at, end);
8056
+ if (fenceStart !== null) {
8057
+ if (FENCE.test(row)) fenceStart = null;
8058
+ } else if (FENCE.test(row)) {
8059
+ fenceStart = at;
8060
+ } else if (TABLE_ROW.test(row)) {
8061
+ if (tableStart === null) {
8062
+ tableStart = at;
8063
+ tableRows = 0;
8064
+ }
8065
+ tableRows += 1;
8066
+ } else if (tableStart !== null) {
8067
+ tableStart = null;
8068
+ tableRows = 0;
8069
+ }
8070
+ at = end + 1;
8071
+ }
8072
+ if (fenceStart === null) {
8073
+ const lastIsRow = TABLE_ROW.test(line);
8074
+ if (lastIsRow && tableStart === null) {
8075
+ tableStart = lineStart;
8076
+ tableRows = 0;
8077
+ } else if (!lastIsRow && line !== "") {
8078
+ tableStart = null;
8079
+ tableRows = 0;
8080
+ }
8081
+ if (tableStart !== null && tableRows < 2) return tableStart;
8082
+ }
8083
+ if (caughtUp) return text.length;
8084
+ if (fenceStart !== null) return fenceStart;
8085
+ if (TABLE_ROW.test(line)) return lineStart;
8086
+ if (EMPTY_BLOCK_MARKER.test(line)) return lineStart;
8087
+ const cut = lineStart + safeInlineEnd(line);
8088
+ return isHighSurrogate(text.charCodeAt(cut - 1)) ? cut - 1 : cut;
8089
+ }
8090
+ function prefersReducedMotion() {
8091
+ if (typeof window === "undefined") return true;
8092
+ try {
8093
+ return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
8094
+ } catch {
8095
+ return false;
8096
+ }
8097
+ }
8098
+ function useProgressiveReveal(text, settled) {
8099
+ const [, setFrame] = useState(0);
8100
+ const [reduced] = useState(prefersReducedMotion);
8101
+ const immediate = settled || reduced;
8102
+ const textRef = useRef(text);
8103
+ const cursorRef = useRef(immediate ? text.length : 0);
8104
+ const cutRef = useRef(immediate ? text.length : 0);
8105
+ const committedRef = useRef(0);
8106
+ if (text !== textRef.current) {
8107
+ const revealed = textRef.current.slice(0, Math.floor(cursorRef.current));
8108
+ if (revealed !== "" && !text.startsWith(revealed)) {
8109
+ cursorRef.current = text.length;
8110
+ }
8111
+ cursorRef.current = Math.min(cursorRef.current, text.length);
8112
+ cutRef.current = Math.min(cutRef.current, text.length);
8113
+ textRef.current = text;
8114
+ }
8115
+ if (immediate) cursorRef.current = text.length;
8116
+ const cut = immediate ? text.length : Math.max(
8117
+ cutRef.current,
8118
+ revealBoundary(text, Math.floor(cursorRef.current))
8119
+ );
8120
+ cutRef.current = cut;
8121
+ useEffect(() => {
8122
+ if (immediate) return;
8123
+ if (cursorRef.current >= text.length) return;
8124
+ let cancelled = false;
8125
+ let cancelStep = null;
8126
+ let previous = now();
8127
+ const commit = (at, force) => {
8128
+ if (!force && at - committedRef.current < COMMIT_INTERVAL_MS) return;
8129
+ committedRef.current = at;
8130
+ setFrame((value) => value + 1);
8131
+ };
8132
+ const step = () => {
8133
+ cancelStep = null;
8134
+ if (cancelled) return;
8135
+ const at = now();
8136
+ const elapsed = clamp(at - previous, 0, MAX_STEP_MS);
8137
+ previous = at;
8138
+ const length = textRef.current.length;
8139
+ const pending = length - cursorRef.current;
8140
+ if (pending <= 0) {
8141
+ commit(at, true);
8142
+ return;
8143
+ }
8144
+ const perSecond = clamp(
8145
+ pending / (DRAIN_TARGET_MS / 1e3),
8146
+ MIN_CHARS_PER_SECOND,
8147
+ MAX_CHARS_PER_SECOND
8148
+ );
8149
+ cursorRef.current = Math.min(
8150
+ length,
8151
+ cursorRef.current + perSecond * elapsed / 1e3
8152
+ );
8153
+ const done = cursorRef.current >= length;
8154
+ commit(at, done);
8155
+ if (!done) schedule();
8156
+ };
8157
+ const schedule = () => {
8158
+ if (typeof window.requestAnimationFrame === "function") {
8159
+ const id2 = window.requestAnimationFrame(step);
8160
+ cancelStep = () => window.cancelAnimationFrame(id2);
8161
+ return;
8162
+ }
8163
+ const id = setTimeout(step, FALLBACK_STEP_MS);
8164
+ cancelStep = () => clearTimeout(id);
8165
+ };
8166
+ schedule();
8167
+ return () => {
8168
+ cancelled = true;
8169
+ cancelStep?.();
8170
+ };
8171
+ }, [text, immediate]);
8172
+ return text.slice(0, cut);
8173
+ }
7927
8174
  function getRecognitionCtor() {
7928
8175
  if (typeof window === "undefined") return null;
7929
8176
  const w = window;
@@ -8142,6 +8389,141 @@ function useSpeechInput({
8142
8389
  );
8143
8390
  return { supported, listening, interim, error, start, stop, toggle };
8144
8391
  }
8392
+ var SILENT_FAILURE_MS = 1500;
8393
+ function speechPlatform() {
8394
+ if (typeof window === "undefined") return null;
8395
+ const scope = window;
8396
+ const synth = scope.speechSynthesis;
8397
+ const Utterance = scope.SpeechSynthesisUtterance;
8398
+ if (!synth || typeof synth.speak !== "function") return null;
8399
+ if (typeof Utterance !== "function") return null;
8400
+ return { synth, Utterance };
8401
+ }
8402
+ function normaliseTag(tag) {
8403
+ return tag.trim().toLowerCase().replace(/_/g, "-");
8404
+ }
8405
+ function primaryOf(tag) {
8406
+ return normaliseTag(tag).split("-")[0] ?? "";
8407
+ }
8408
+ function pickVoice(voices, lang) {
8409
+ const wanted = normaliseTag(lang);
8410
+ if (wanted === "") return null;
8411
+ const primary = primaryOf(wanted);
8412
+ const exact = voices.filter((voice) => normaliseTag(voice.lang) === wanted);
8413
+ const related = voices.filter((voice) => primaryOf(voice.lang) === primary);
8414
+ return exact.find((voice) => voice.default) ?? exact[0] ?? related.find((voice) => voice.default) ?? related[0] ?? null;
8415
+ }
8416
+ var activeOwner = null;
8417
+ var activeStop = null;
8418
+ function stopSpeaking() {
8419
+ activeStop?.();
8420
+ }
8421
+ function useSpeechOutput({
8422
+ lang
8423
+ } = {}) {
8424
+ const [available, setAvailable] = useState(() => speechPlatform() !== null);
8425
+ const [speaking, setSpeaking] = useState(false);
8426
+ const utteranceRef = useRef(null);
8427
+ const watchdogRef = useRef(null);
8428
+ const ownerRef = useRef({});
8429
+ const stopRef = useRef(() => void 0);
8430
+ const langRef = useRef(lang);
8431
+ langRef.current = lang;
8432
+ const clearWatchdog = useCallback(() => {
8433
+ if (watchdogRef.current === null) return;
8434
+ clearTimeout(watchdogRef.current);
8435
+ watchdogRef.current = null;
8436
+ }, []);
8437
+ const settle = useCallback(() => {
8438
+ clearWatchdog();
8439
+ utteranceRef.current = null;
8440
+ if (activeOwner === ownerRef.current) {
8441
+ activeOwner = null;
8442
+ activeStop = null;
8443
+ }
8444
+ setSpeaking(false);
8445
+ }, [clearWatchdog]);
8446
+ const stop = useCallback(() => {
8447
+ const owned = utteranceRef.current !== null;
8448
+ settle();
8449
+ if (owned) speechPlatform()?.synth.cancel();
8450
+ }, [settle]);
8451
+ stopRef.current = stop;
8452
+ const speak = useCallback(
8453
+ (text) => {
8454
+ const platform = speechPlatform();
8455
+ if (!platform) {
8456
+ setAvailable(false);
8457
+ return;
8458
+ }
8459
+ const spoken = text.trim();
8460
+ if (spoken === "") return;
8461
+ if (activeStop && activeOwner !== ownerRef.current) activeStop();
8462
+ platform.synth.cancel();
8463
+ const utterance = new platform.Utterance(spoken);
8464
+ const wanted = langRef.current;
8465
+ if (wanted !== void 0 && wanted !== "") {
8466
+ const voice = pickVoice(platform.synth.getVoices(), wanted);
8467
+ utterance.lang = voice?.lang ?? wanted;
8468
+ if (voice) utterance.voice = voice;
8469
+ }
8470
+ const mine = () => utteranceRef.current === utterance;
8471
+ utterance.onstart = () => {
8472
+ if (!mine()) return;
8473
+ clearWatchdog();
8474
+ setSpeaking(true);
8475
+ };
8476
+ utterance.onend = () => {
8477
+ if (!mine()) return;
8478
+ settle();
8479
+ };
8480
+ utterance.onerror = (event) => {
8481
+ if (!mine()) return;
8482
+ const code = typeof event?.error === "string" ? event.error : "";
8483
+ settle();
8484
+ if (code !== "interrupted" && code !== "canceled") setAvailable(false);
8485
+ };
8486
+ utteranceRef.current = utterance;
8487
+ activeOwner = ownerRef.current;
8488
+ activeStop = () => {
8489
+ stopRef.current();
8490
+ };
8491
+ setSpeaking(true);
8492
+ platform.synth.speak(utterance);
8493
+ clearWatchdog();
8494
+ watchdogRef.current = setTimeout(() => {
8495
+ watchdogRef.current = null;
8496
+ if (!mine()) return;
8497
+ if (platform.synth.speaking) return;
8498
+ settle();
8499
+ setAvailable(false);
8500
+ }, SILENT_FAILURE_MS);
8501
+ },
8502
+ [clearWatchdog, settle]
8503
+ );
8504
+ const toggle = useCallback(
8505
+ (text) => {
8506
+ if (speaking) stop();
8507
+ else speak(text);
8508
+ },
8509
+ [speak, speaking, stop]
8510
+ );
8511
+ useEffect(() => {
8512
+ if (!speaking || typeof window === "undefined") return;
8513
+ const onHide = () => {
8514
+ stopRef.current();
8515
+ };
8516
+ window.addEventListener("pagehide", onHide);
8517
+ return () => window.removeEventListener("pagehide", onHide);
8518
+ }, [speaking]);
8519
+ useEffect(
8520
+ () => () => {
8521
+ stopRef.current();
8522
+ },
8523
+ []
8524
+ );
8525
+ return { supported: available, speaking, speak, stop, toggle };
8526
+ }
8145
8527
  var SWIPE_THRESHOLD = 40;
8146
8528
  function useSwipe({
8147
8529
  onSwipeLeft,
@@ -8409,6 +8791,115 @@ var EXPLORE_CSS = `
8409
8791
  height: auto;
8410
8792
  }
8411
8793
 
8794
+ /* ---- Comparison and pricing tables ----------------------------------------------------
8795
+ The harness emits a comparison, and a set of plans, as a GFM table whose FIRST column is
8796
+ the subject \u2014 the product, or the plan (CONTRACT.md \xA77). Two things have to be true of it:
8797
+ the page must not scroll sideways because of it, and it has to be readable on a 320px
8798
+ phone, which a four-column grid of 1px borders is not.
8799
+
8800
+ Wide: a contained scroller. \`max-width: 100%\` keeps the box inside the bubble and
8801
+ \`overflow-x: auto\` keeps the columns inside the box, so the widest possible table still
8802
+ cannot widen the document. \`overscroll-behavior-x: contain\` stops a swipe that runs out
8803
+ of table from chaining to the page \u2014 the same rule the transcript needed. */
8804
+ .boff-explore-body .boff-explore-table {
8805
+ max-width: 100%;
8806
+ overflow-x: auto;
8807
+ overscroll-behavior-x: contain;
8808
+ margin: 0.75rem 0;
8809
+ border: 1px solid hsl(var(--border));
8810
+ border-radius: 0.75rem;
8811
+ background-color: hsl(var(--card));
8812
+ }
8813
+ .boff-explore-body .boff-explore-table:focus-visible {
8814
+ outline: 2px solid hsl(var(--ring));
8815
+ outline-offset: 2px;
8816
+ }
8817
+ .boff-explore-body .boff-explore-table table {
8818
+ /* Beats the blanket \`display: block\` above, which is the unwrapped fallback. */
8819
+ display: table;
8820
+ width: 100%;
8821
+ max-width: none;
8822
+ margin: 0;
8823
+ border-collapse: collapse;
8824
+ font-size: 0.8125rem;
8825
+ }
8826
+ .boff-explore-body .boff-explore-table th,
8827
+ .boff-explore-body .boff-explore-table td {
8828
+ border: 0;
8829
+ border-top: 1px solid hsl(var(--border));
8830
+ padding: 0.5rem 0.75rem;
8831
+ /* \`start\`, never \`left\`: the page root carries dir="rtl" in Arabic, and a left-aligned
8832
+ table there reads against the grain of every other line on the page. */
8833
+ text-align: start;
8834
+ vertical-align: top;
8835
+ overflow-wrap: anywhere;
8836
+ /* Prices in a column line up under each other instead of drifting by digit width. */
8837
+ font-variant-numeric: tabular-nums;
8838
+ min-width: 6rem;
8839
+ }
8840
+ .boff-explore-body .boff-explore-table thead th {
8841
+ border-top: 0;
8842
+ background-color: hsl(var(--muted));
8843
+ color: hsl(var(--foreground));
8844
+ font-weight: 600;
8845
+ white-space: nowrap;
8846
+ }
8847
+ /* The first column is what the row is ABOUT \u2014 the product, or the plan name. Reading it as
8848
+ the row's heading is what makes a price list scannable rather than a grid of strings. */
8849
+ .boff-explore-body .boff-explore-table tbody td:first-child {
8850
+ font-weight: 600;
8851
+ color: hsl(var(--card-foreground));
8852
+ }
8853
+
8854
+ /* Narrow: one card per row. Each cell prints its own column heading, so a value is never
8855
+ stranded without the question it answers, and the subject cell becomes the card's title.
8856
+ A media query rather than a measured width \u2014 these pages are serialised post-hydration,
8857
+ so anything measured here would ship to every phone as static HTML. */
8858
+ @media (max-width: 30rem) {
8859
+ .boff-explore-body .boff-explore-table {
8860
+ overflow-x: visible;
8861
+ }
8862
+ .boff-explore-body .boff-explore-table table,
8863
+ .boff-explore-body .boff-explore-table tbody,
8864
+ .boff-explore-body .boff-explore-table tr,
8865
+ .boff-explore-body .boff-explore-table td {
8866
+ display: block;
8867
+ width: 100%;
8868
+ min-width: 0;
8869
+ }
8870
+ .boff-explore-body .boff-explore-table thead {
8871
+ display: none;
8872
+ }
8873
+ .boff-explore-body .boff-explore-table tr {
8874
+ border-top: 1px solid hsl(var(--border));
8875
+ padding: 0.5rem 0;
8876
+ }
8877
+ .boff-explore-body .boff-explore-table tr:first-child {
8878
+ border-top: 0;
8879
+ }
8880
+ .boff-explore-body .boff-explore-table td {
8881
+ border: 0;
8882
+ padding: 0.125rem 0.75rem;
8883
+ }
8884
+ .boff-explore-body .boff-explore-table td[data-label]::before {
8885
+ content: attr(data-label);
8886
+ display: block;
8887
+ font-size: 0.6875rem;
8888
+ font-weight: 600;
8889
+ text-transform: uppercase;
8890
+ letter-spacing: 0.04em;
8891
+ color: hsl(var(--muted-foreground));
8892
+ }
8893
+ .boff-explore-body .boff-explore-table tbody td:first-child {
8894
+ font-size: 0.9375rem;
8895
+ padding-bottom: 0.25rem;
8896
+ }
8897
+ .boff-explore-body .boff-explore-table tbody td:first-child::before {
8898
+ /* The plan's own name is the heading here; repeating "Plan" over it says nothing. */
8899
+ display: none;
8900
+ }
8901
+ }
8902
+
8412
8903
  /* ---- Ambient backdrop -----------------------------------------------------------------
8413
8904
  Two very low-opacity radial washes behind the welcome state, drifting slowly. Painted on
8414
8905
  a pseudo-element with pointer-events:none so it can never intercept a click \u2014 a
@@ -8580,7 +9071,7 @@ function isOwnElement(el, root) {
8580
9071
  function visualViewportHeight() {
8581
9072
  return window.visualViewport?.height ?? window.innerHeight;
8582
9073
  }
8583
- function clamp(value, min, max) {
9074
+ function clamp2(value, min, max) {
8584
9075
  return Math.min(Math.max(value, min), max);
8585
9076
  }
8586
9077
  function isRestoredNavigation() {
@@ -8730,11 +9221,80 @@ function summariseReferences(references, origins, productNames) {
8730
9221
  };
8731
9222
  }
8732
9223
  var CODE_FENCE = /^\s*(?:```|~~~)/;
8733
- function answerPlainText(markdown) {
9224
+ var TABLE_ROW_LINE = /^\s{0,3}\|/;
9225
+ var TABLE_DELIMITER_ROW = /^\s{0,3}\|[\s:|-]*-[\s:|-]*$/;
9226
+ function readableLine(raw, mode) {
9227
+ let line = raw;
9228
+ line = line.replace(/!\[[^\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g, "");
9229
+ line = mode === "speech" ? (
9230
+ // A voice reading "h-t-t-p-s colon slash slash…" mid-sentence is noise, and the
9231
+ // prompt asks for two to five links in every answer. The label IS the words.
9232
+ line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1")
9233
+ ) : line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1 ($2)");
9234
+ line = mode === "speech" ? line.replace(/<(https?:\/\/[^>\s]+)>/g, "") : line.replace(/<(https?:\/\/[^>\s]+)>/g, "$1");
9235
+ line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
9236
+ line = line.replace(/^\s{0,3}>\s?/, "");
9237
+ if (/^\s{0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/.test(line)) return "";
9238
+ line = line.replace(/`([^`]+)`/g, "$1");
9239
+ line = line.replace(/\*\*([^*]+)\*\*/g, "$1");
9240
+ line = line.replace(/__([^_]+)__/g, "$1");
9241
+ line = line.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, "$1$2");
9242
+ line = line.replace(/(^|[^_\w])_([^_\n]+)_(?![_\w])/g, "$1$2");
9243
+ return line.replace(/[ \t]+$/, "");
9244
+ }
9245
+ function tableCells(row) {
9246
+ const trimmed = row.trim();
9247
+ const cells = [];
9248
+ let cell = "";
9249
+ for (let at = 0; at < trimmed.length; at += 1) {
9250
+ const char = trimmed[at] ?? "";
9251
+ if (char === "\\") {
9252
+ cell += char + (trimmed[at + 1] ?? "");
9253
+ at += 1;
9254
+ continue;
9255
+ }
9256
+ if (char === "|") {
9257
+ cells.push(cell.trim());
9258
+ cell = "";
9259
+ continue;
9260
+ }
9261
+ cell += char;
9262
+ }
9263
+ cells.push(cell.trim());
9264
+ if (trimmed.startsWith("|") && cells[0] === "") cells.shift();
9265
+ if (trimmed.endsWith("|") && cells[cells.length - 1] === "") cells.pop();
9266
+ return cells;
9267
+ }
9268
+ function asSentence(text) {
9269
+ return /[.!?]$/.test(text) ? text : `${text}.`;
9270
+ }
9271
+ function spokenTable(rows) {
9272
+ if (rows.length < 2 || !TABLE_DELIMITER_ROW.test(rows[1] ?? "")) return rows;
9273
+ const labels = tableCells(rows[0] ?? "");
9274
+ const spoken = [];
9275
+ for (const row of rows.slice(2)) {
9276
+ const parts = [];
9277
+ tableCells(row).forEach((value, column) => {
9278
+ if (value === "") return;
9279
+ const label = column === 0 ? "" : labels[column] ?? "";
9280
+ parts.push(label === "" ? value : `${label}: ${value}`);
9281
+ });
9282
+ if (parts.length > 0) spoken.push(asSentence(parts.join(". ")));
9283
+ }
9284
+ return spoken;
9285
+ }
9286
+ function readableText(markdown, mode) {
8734
9287
  const lines = [];
8735
9288
  let inFence = false;
9289
+ let table = [];
9290
+ const flushTable = () => {
9291
+ if (table.length === 0) return;
9292
+ for (const line of spokenTable(table)) lines.push(readableLine(line, mode));
9293
+ table = [];
9294
+ };
8736
9295
  for (const raw of markdown.split("\n")) {
8737
9296
  if (CODE_FENCE.test(raw)) {
9297
+ flushTable();
8738
9298
  inFence = !inFence;
8739
9299
  continue;
8740
9300
  }
@@ -8742,33 +9302,30 @@ function answerPlainText(markdown) {
8742
9302
  lines.push(raw);
8743
9303
  continue;
8744
9304
  }
8745
- let line = raw;
8746
- line = line.replace(/!\[[^\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g, "");
8747
- line = line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1 ($2)");
8748
- line = line.replace(/<(https?:\/\/[^>\s]+)>/g, "$1");
8749
- line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
8750
- line = line.replace(/^\s{0,3}>\s?/, "");
8751
- if (/^\s{0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/.test(line)) {
8752
- lines.push("");
9305
+ if (mode === "speech" && TABLE_ROW_LINE.test(raw)) {
9306
+ table.push(raw);
8753
9307
  continue;
8754
9308
  }
8755
- line = line.replace(/`([^`]+)`/g, "$1");
8756
- line = line.replace(/\*\*([^*]+)\*\*/g, "$1");
8757
- line = line.replace(/__([^_]+)__/g, "$1");
8758
- line = line.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, "$1$2");
8759
- line = line.replace(/(^|[^_\w])_([^_\n]+)_(?![_\w])/g, "$1$2");
8760
- lines.push(line.replace(/[ \t]+$/, ""));
9309
+ flushTable();
9310
+ lines.push(readableLine(raw, mode));
8761
9311
  }
9312
+ flushTable();
8762
9313
  return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
8763
9314
  }
8764
- var InlineImageScopeContext = createContext(null);
9315
+ function answerPlainText(markdown) {
9316
+ return readableText(markdown, "clipboard");
9317
+ }
9318
+ function answerSpeechText(markdown) {
9319
+ return readableText(markdown, "speech");
9320
+ }
9321
+ var AnswerScopeContext = createContext(null);
8765
9322
  var InsideLinkContext = createContext(false);
8766
9323
  function AnswerLink(props) {
8767
9324
  return /* @__PURE__ */ jsx(InsideLinkContext.Provider, { value: true, children: /* @__PURE__ */ jsx(MarkdownLink, { ...props }) });
8768
9325
  }
8769
9326
  var INLINE_IMAGE_FRAME = "relative inline-block max-w-full overflow-hidden rounded-xl border border-border bg-muted align-top shadow-sm";
8770
9327
  function InlineConceptImage({ src, alt }) {
8771
- const scope = useContext(InlineImageScopeContext);
9328
+ const scope = useContext(AnswerScopeContext);
8772
9329
  const insideLink = useContext(InsideLinkContext);
8773
9330
  if (!scope || !scope.enabled || !isInlineImageAllowed(src, scope.origins) || scope.failed.has(src)) {
8774
9331
  return null;
@@ -8816,9 +9373,70 @@ function InlineConceptImage({ src, alt }) {
8816
9373
  }
8817
9374
  ) });
8818
9375
  }
9376
+ function markdownText(node) {
9377
+ if (!node) return "";
9378
+ if (typeof node.value === "string") return node.value;
9379
+ return (node.children ?? []).map(markdownText).join("");
9380
+ }
9381
+ function hasHeaderCells(node) {
9382
+ return (node?.children ?? []).some((cell) => cell.tagName === "th");
9383
+ }
9384
+ function findHeaderRow(node) {
9385
+ for (const child of node?.children ?? []) {
9386
+ if (child.tagName === "tr" && hasHeaderCells(child)) return child;
9387
+ const nested = findHeaderRow(child);
9388
+ if (nested !== null) return nested;
9389
+ }
9390
+ return null;
9391
+ }
9392
+ function tableColumnLabels(node) {
9393
+ const row = findHeaderRow(node);
9394
+ if (row === null) return [];
9395
+ return (row.children ?? []).filter((cell) => cell.tagName === "th").map((cell) => markdownText(cell).trim());
9396
+ }
9397
+ var TableColumnsContext = createContext([]);
9398
+ function AnswerTable({ node, children }) {
9399
+ const scope = useContext(AnswerScopeContext);
9400
+ const table = /* @__PURE__ */ jsx(TableColumnsContext.Provider, { value: tableColumnLabels(node), children: /* @__PURE__ */ jsx("table", { children }) });
9401
+ if (!scope) return table;
9402
+ return /* @__PURE__ */ jsx(
9403
+ "div",
9404
+ {
9405
+ "data-boff-explore": "table",
9406
+ role: "region",
9407
+ "aria-label": scope.strings.answerTable,
9408
+ tabIndex: 0,
9409
+ className: "boff-explore-table",
9410
+ children: table
9411
+ }
9412
+ );
9413
+ }
9414
+ function AnswerTableRow({
9415
+ node,
9416
+ children,
9417
+ ...props
9418
+ }) {
9419
+ const labels = useContext(TableColumnsContext);
9420
+ if (labels.length === 0 || hasHeaderCells(node)) {
9421
+ return /* @__PURE__ */ jsx("tr", { ...props, children });
9422
+ }
9423
+ let column = 0;
9424
+ const cells = Children.map(children, (cell) => {
9425
+ if (!isValidElement(cell)) return cell;
9426
+ const label = labels[column] ?? "";
9427
+ column += 1;
9428
+ if (label === "") return cell;
9429
+ return cloneElement(cell, {
9430
+ "data-label": label
9431
+ });
9432
+ });
9433
+ return /* @__PURE__ */ jsx("tr", { ...props, children: cells });
9434
+ }
8819
9435
  var ANSWER_MARKDOWN_COMPONENTS = {
8820
9436
  a: AnswerLink,
8821
- img: InlineConceptImage
9437
+ img: InlineConceptImage,
9438
+ table: AnswerTable,
9439
+ tr: AnswerTableRow
8822
9440
  };
8823
9441
  function ReferenceCard({
8824
9442
  reference,
@@ -9207,9 +9825,42 @@ function ProvenanceChips({
9207
9825
  );
9208
9826
  }
9209
9827
  var COPY_FEEDBACK_MS = 2500;
9828
+ var ANSWER_ACTION_PILL = "inline-flex max-w-full items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring";
9829
+ var ANSWER_ACTION_IDLE = "border-border bg-card text-muted-foreground hover:border-primary/40 hover:bg-accent hover:text-foreground";
9830
+ var ANSWER_ACTION_ACTIVE = "border-primary/40 bg-accent text-primary";
9831
+ function ReadAloudButton({
9832
+ strings: T,
9833
+ text,
9834
+ lang
9835
+ }) {
9836
+ const speech = useSpeechOutput({ lang });
9837
+ if (!speech.supported) return null;
9838
+ const label = speech.speaking ? T.stopReading : T.readAloud;
9839
+ return /* @__PURE__ */ jsxs(
9840
+ "button",
9841
+ {
9842
+ "data-boff-explore": "read-aloud",
9843
+ "data-speaking": speech.speaking ? "true" : "false",
9844
+ type: "button",
9845
+ title: label,
9846
+ "aria-pressed": speech.speaking,
9847
+ onClick: () => speech.toggle(text),
9848
+ className: cn(
9849
+ ANSWER_ACTION_PILL,
9850
+ speech.speaking ? ANSWER_ACTION_ACTIVE : ANSWER_ACTION_IDLE
9851
+ ),
9852
+ children: [
9853
+ /* @__PURE__ */ jsx(Volume2, { className: "h-3 w-3 shrink-0", "aria-hidden": "true" }),
9854
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: label })
9855
+ ]
9856
+ }
9857
+ );
9858
+ }
9210
9859
  function AnswerActions({
9211
9860
  strings: T,
9212
- text
9861
+ text,
9862
+ speechText,
9863
+ speechLang
9213
9864
  }) {
9214
9865
  const [outcome, setOutcome] = useState("idle");
9215
9866
  const resetRef = useRef(null);
@@ -9252,9 +9903,8 @@ function AnswerActions({
9252
9903
  void copy();
9253
9904
  },
9254
9905
  className: cn(
9255
- "inline-flex max-w-full items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs transition-colors",
9256
- "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
9257
- outcome === "failed" ? "border-destructive/40 bg-card text-destructive" : "border-border bg-card text-muted-foreground hover:border-primary/40 hover:bg-accent hover:text-foreground"
9906
+ ANSWER_ACTION_PILL,
9907
+ outcome === "failed" ? "border-destructive/40 bg-card text-destructive" : ANSWER_ACTION_IDLE
9258
9908
  ),
9259
9909
  children: [
9260
9910
  outcome === "copied" ? /* @__PURE__ */ jsx(Check, { className: "h-3 w-3 shrink-0", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3 w-3 shrink-0", "aria-hidden": "true" }),
@@ -9262,6 +9912,7 @@ function AnswerActions({
9262
9912
  ]
9263
9913
  }
9264
9914
  ),
9915
+ /* @__PURE__ */ jsx(ReadAloudButton, { strings: T, text: speechText, lang: speechLang }),
9265
9916
  /* @__PURE__ */ jsx("span", { "data-boff-explore": "copy-status", role: "status", className: "sr-only", children: outcome === "idle" ? "" : label })
9266
9917
  ]
9267
9918
  }
@@ -9313,6 +9964,7 @@ function AssistantBubble({
9313
9964
  activity,
9314
9965
  imageOrigins,
9315
9966
  productNames,
9967
+ speechLang,
9316
9968
  onNavigate,
9317
9969
  anchorRef,
9318
9970
  onFollow,
@@ -9323,6 +9975,7 @@ function AssistantBubble({
9323
9975
  const body = message.content || message.partialContent || "";
9324
9976
  const failed = message.status === "FAILED";
9325
9977
  const completed = message.status === "COMPLETED";
9978
+ const visible = useProgressiveReveal(body, !streaming);
9326
9979
  const [failedImages, setFailedImages] = useState(
9327
9980
  () => /* @__PURE__ */ new Set()
9328
9981
  );
@@ -9331,7 +9984,7 @@ function AssistantBubble({
9331
9984
  (current) => current.has(src) ? current : new Set(current).add(src)
9332
9985
  );
9333
9986
  }, []);
9334
- const imageScope = useMemo(
9987
+ const answerScope = useMemo(
9335
9988
  () => ({
9336
9989
  strings: T,
9337
9990
  enabled: completed,
@@ -9349,6 +10002,10 @@ function AssistantBubble({
9349
10002
  () => answerPlainText(message.content),
9350
10003
  [message.content]
9351
10004
  );
10005
+ const speechText = useMemo(
10006
+ () => answerSpeechText(message.content),
10007
+ [message.content]
10008
+ );
9352
10009
  const grounded = completed && message.answered !== false;
9353
10010
  return /* @__PURE__ */ jsxs(
9354
10011
  "div",
@@ -9395,8 +10052,8 @@ function AssistantBubble({
9395
10052
  ]
9396
10053
  }
9397
10054
  ) : null,
9398
- body ? /* @__PURE__ */ jsxs("div", { className: "rounded-2xl rounded-tl-md border border-border bg-card px-4 py-3 shadow-sm", children: [
9399
- /* @__PURE__ */ jsx("div", { className: "boff-prose boff-prose-sm boff-explore-body min-w-0 text-card-foreground", children: /* @__PURE__ */ jsx(InlineImageScopeContext.Provider, { value: imageScope, children: /* @__PURE__ */ jsx(Markdown, { components: ANSWER_MARKDOWN_COMPONENTS, children: body }) }) }),
10055
+ visible ? /* @__PURE__ */ jsxs("div", { className: "rounded-2xl rounded-tl-md border border-border bg-card px-4 py-3 shadow-sm", children: [
10056
+ /* @__PURE__ */ jsx("div", { className: "boff-prose boff-prose-sm boff-explore-body min-w-0 text-card-foreground", children: /* @__PURE__ */ jsx(AnswerScopeContext.Provider, { value: answerScope, children: /* @__PURE__ */ jsx(Markdown, { components: ANSWER_MARKDOWN_COMPONENTS, children: visible }) }) }),
9400
10057
  streaming ? /* @__PURE__ */ jsx("span", { className: "boff-explore-caret", "aria-hidden": "true" }) : null
9401
10058
  ] }) : null,
9402
10059
  grounded && provenance.total > 0 ? /* @__PURE__ */ jsx(ProvenanceChips, { strings: T, provenance }) : null,
@@ -9439,7 +10096,15 @@ function AssistantBubble({
9439
10096
  question
9440
10097
  )) })
9441
10098
  ] }) : null,
9442
- completed && copyText !== "" ? /* @__PURE__ */ jsx(AnswerActions, { strings: T, text: copyText }) : null,
10099
+ completed && copyText !== "" ? /* @__PURE__ */ jsx(
10100
+ AnswerActions,
10101
+ {
10102
+ strings: T,
10103
+ text: copyText,
10104
+ speechText,
10105
+ speechLang
10106
+ }
10107
+ ) : null,
9443
10108
  completed && onFeedback ? /* @__PURE__ */ jsx(
9444
10109
  AnswerFeedback,
9445
10110
  {
@@ -9673,7 +10338,7 @@ function ConceptStrip({
9673
10338
  "div",
9674
10339
  {
9675
10340
  "data-boff-explore": "concept-strip-block",
9676
- className: "boff-explore-rise boff-explore-rise-3 mt-6",
10341
+ className: "boff-explore-rise boff-explore-rise-1 mt-6",
9677
10342
  children: [
9678
10343
  /* @__PURE__ */ jsx("p", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: T.conceptStripTitle }),
9679
10344
  /* @__PURE__ */ jsx(
@@ -9863,6 +10528,15 @@ function WelcomeState({
9863
10528
  /* @__PURE__ */ jsx("h1", { className: "mt-4 text-balance break-words text-2xl font-bold tracking-tight sm:text-4xl", children: title }),
9864
10529
  /* @__PURE__ */ jsx("p", { className: "mx-auto mt-2.5 max-w-2xl text-pretty text-sm leading-relaxed text-muted-foreground sm:text-lg", children: body })
9865
10530
  ] }),
10531
+ /* @__PURE__ */ jsx(
10532
+ ConceptStrip,
10533
+ {
10534
+ strings: T,
10535
+ highlights: conceptHighlights,
10536
+ onAsk: onPrompt,
10537
+ disabled
10538
+ }
10539
+ ),
9866
10540
  /* @__PURE__ */ jsx("ul", { className: "boff-explore-rise boff-explore-rise-1 mt-6 grid grid-cols-3 gap-2 sm:mt-7 sm:gap-2.5", children: CAPABILITIES.map(({ icon: Icon, body: capBody }, i) => /* @__PURE__ */ jsxs(
9867
10541
  "li",
9868
10542
  {
@@ -9887,15 +10561,6 @@ function WelcomeState({
9887
10561
  }
9888
10562
  )
9889
10563
  ] }) : null,
9890
- /* @__PURE__ */ jsx(
9891
- ConceptStrip,
9892
- {
9893
- strings: T,
9894
- highlights: conceptHighlights,
9895
- onAsk: onPrompt,
9896
- disabled
9897
- }
9898
- ),
9899
10564
  products.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "boff-explore-rise boff-explore-rise-3 mt-6", children: [
9900
10565
  /* @__PURE__ */ jsx("p", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: T.focusProduct }),
9901
10566
  /* @__PURE__ */ jsx(
@@ -9933,6 +10598,7 @@ function ExplorePage({
9933
10598
  );
9934
10599
  const T = useMemo(() => exploreStrings(activeLocale), [activeLocale]);
9935
10600
  const rtl = isRtlLocale(activeLocale);
10601
+ const speechLang = speechLocaleOf(activeLocale);
9936
10602
  const {
9937
10603
  phase,
9938
10604
  catalog,
@@ -10037,11 +10703,11 @@ function ExplorePage({
10037
10703
  const layoutHeight = window.innerHeight;
10038
10704
  if (layoutHeight === 0) return;
10039
10705
  const top = Math.round(
10040
- clamp(el.getBoundingClientRect().top, 0, layoutHeight)
10706
+ clamp2(el.getBoundingClientRect().top, 0, layoutHeight)
10041
10707
  );
10042
10708
  const viewport = window.visualViewport;
10043
10709
  const inset = viewport ? Math.round(
10044
- clamp(
10710
+ clamp2(
10045
10711
  layoutHeight - viewport.height - viewport.offsetTop,
10046
10712
  0,
10047
10713
  layoutHeight
@@ -10322,6 +10988,7 @@ function ExplorePage({
10322
10988
  const text = draft.trim();
10323
10989
  if (!text || overLimit || busy || !canSend) return;
10324
10990
  stopDictation();
10991
+ stopSpeaking();
10325
10992
  setDraft("");
10326
10993
  stickToBottomRef.current = true;
10327
10994
  void send(text, toPayload(stagedRef.current));
@@ -10332,6 +10999,7 @@ function ExplorePage({
10332
10999
  const sendPrompt = useCallback(
10333
11000
  (prompt) => {
10334
11001
  if (!canSend || busy) return;
11002
+ stopSpeaking();
10335
11003
  setDraft("");
10336
11004
  stickToBottomRef.current = true;
10337
11005
  void send(prompt, toPayload(stagedRef.current));
@@ -10567,6 +11235,7 @@ function ExplorePage({
10567
11235
  activity,
10568
11236
  imageOrigins,
10569
11237
  productNames,
11238
+ speechLang,
10570
11239
  onNavigate,
10571
11240
  anchorRef: message.id === latestAssistantId ? latestAssistantRef : void 0,
10572
11241
  onFollow: (kind, url) => recordClick(message.id, kind, url),
@@ -15528,6 +16197,6 @@ function StoreProductDetailPage({
15528
16197
  ] });
15529
16198
  }
15530
16199
 
15531
- export { BlogListPage, BlogPostPage, Button, CONCEPTS_PATH, CONCEPT_ILLUSTRATION_NOTICE, CONCEPT_PATH_PREFIX, CareersApplyForm, CareersSubmissionForm, CaseStudiesListPage, CaseStudyPage, ChangelogPage, ConceptDetailPage, ConceptsPage, ContactPage, ContentRenderer, ContractsPage, DEFAULT_CONTACT_CATEGORIES, DEFAULT_EXPLORE_EXAMPLE_PROMPTS, ECOSYSTEM_PRODUCTS, EXPLORE_CATALOG_QUERY, EXPLORE_CATALOG_QUERY_WITHOUT_HIGHLIGHTS, EXPLORE_LOCALES, EXPLORE_MESSAGE_QUERY, EXPLORE_RECAPTCHA_ACTION, ElectronDownloadLink, ExploreCta, ExplorePage, FALLBACK_EXPLORE_LIMITS, Input, Label, LaunchCountdown, Markdown, NewsletterForm, NewsletterPage, PageHead, PartnersPage, PressKitPage, PricingPage, PricingSection, ProblemsSolvedSection, ProductCard, ProductProvider, ResourceLinks, RybbitAnalytics, SEND_EXPLORE_MESSAGE_MUTATION, Select, SocialLinks, StarRating, StoreBrowsePage, StoreHomePage, StoreProductDetailPage, Textarea, TypeBadge, UnsubscribePage, WaitlistForm, WaitlistPage, WebSDKClient, WebSDKProvider, WebsiteSwitcher, buttonVariants, classifyExploreError, cn, conceptPath, detectElectronPlatform, fetchPublicJobPosting, fetchPublicJobPostings, fetchPublicJobPostingsForSubmission, formatDownloads, formatPrice2 as formatPrice, getConceptBySlug, getRecaptchaV3Token, isValidUrl, loadRecaptchaV3, useContentPage, useContentPages, useExploreChat, useProduct, useProductConfig, usePublicJobPosting, usePublicJobPostings, usePublicJobPostingsForSubmission, useWebSDK, useWebSDKConfig };
16200
+ export { BlogListPage, BlogPostPage, Button, CONCEPTS_PATH, CONCEPT_ILLUSTRATION_NOTICE, CONCEPT_PATH_PREFIX, CareersApplyForm, CareersSubmissionForm, CaseStudiesListPage, CaseStudyPage, ChangelogPage, ConceptDetailPage, ConceptsPage, ContactPage, ContentRenderer, ContractsPage, DEFAULT_CONTACT_CATEGORIES, DEFAULT_EXPLORE_EXAMPLE_PROMPTS, ECOSYSTEM_PRODUCTS, EXPLORE_CATALOG_QUERY, EXPLORE_CATALOG_QUERY_WITHOUT_HIGHLIGHTS, EXPLORE_LOCALES, EXPLORE_MESSAGE_QUERY, EXPLORE_RECAPTCHA_ACTION, ElectronDownloadLink, ExploreCta, ExplorePage, FALLBACK_EXPLORE_LIMITS, Input, Label, LaunchCountdown, Markdown, NewsletterForm, NewsletterPage, PageHead, PartnersPage, PressKitPage, PricingPage, PricingSection, ProblemsSolvedSection, ProductCard, ProductProvider, ResourceLinks, RybbitAnalytics, SEND_EXPLORE_MESSAGE_MUTATION, Select, SocialLinks, StarRating, StoreBrowsePage, StoreHomePage, StoreProductDetailPage, Textarea, TypeBadge, UnsubscribePage, WaitlistForm, WaitlistPage, WebSDKClient, WebSDKProvider, WebsiteSwitcher, buttonVariants, classifyExploreError, cn, conceptPath, detectElectronPlatform, fetchPublicJobPosting, fetchPublicJobPostings, fetchPublicJobPostingsForSubmission, formatDownloads, formatPrice2 as formatPrice, getConceptBySlug, getRecaptchaV3Token, isValidUrl, loadRecaptchaV3, speechLocaleOf, stopSpeaking, useContentPage, useContentPages, useExploreChat, useProduct, useProductConfig, useProgressiveReveal, usePublicJobPosting, usePublicJobPostings, usePublicJobPostingsForSubmission, useSpeechOutput, useWebSDK, useWebSDKConfig };
15532
16201
  //# sourceMappingURL=index.mjs.map
15533
16202
  //# sourceMappingURL=index.mjs.map