@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.js CHANGED
@@ -2438,16 +2438,16 @@ function useRateLimit(maxAttempts, windowMinutes) {
2438
2438
  }
2439
2439
  }, [maxAttempts]);
2440
2440
  function recordAttempt() {
2441
- const now = Date.now();
2441
+ const now2 = Date.now();
2442
2442
  const state = rateLimitRef.current;
2443
- if (state && now > state.resetTime) {
2443
+ if (state && now2 > state.resetTime) {
2444
2444
  rateLimitRef.current = null;
2445
2445
  }
2446
2446
  const current = rateLimitRef.current;
2447
2447
  if (!current) {
2448
2448
  rateLimitRef.current = {
2449
2449
  attempts: 1,
2450
- resetTime: now + windowMinutes * 60 * 1e3
2450
+ resetTime: now2 + windowMinutes * 60 * 1e3
2451
2451
  };
2452
2452
  return false;
2453
2453
  }
@@ -3542,16 +3542,16 @@ function useRateLimit2(maxAttempts, windowMinutes) {
3542
3542
  const [isLimited, setIsLimited] = React.useState(false);
3543
3543
  const [resetTime, setResetTime] = React.useState(null);
3544
3544
  function recordAttempt() {
3545
- const now = Date.now();
3545
+ const now2 = Date.now();
3546
3546
  const state = rateLimitRef.current;
3547
- if (state && now > state.resetTime) {
3547
+ if (state && now2 > state.resetTime) {
3548
3548
  rateLimitRef.current = null;
3549
3549
  }
3550
3550
  const current = rateLimitRef.current;
3551
3551
  if (!current) {
3552
3552
  rateLimitRef.current = {
3553
3553
  attempts: 1,
3554
- resetTime: now + windowMinutes * 60 * 1e3
3554
+ resetTime: now2 + windowMinutes * 60 * 1e3
3555
3555
  };
3556
3556
  return false;
3557
3557
  }
@@ -4585,8 +4585,8 @@ function LaunchCountdown({
4585
4585
  launchedMessage = "We have launched!",
4586
4586
  className
4587
4587
  }) {
4588
- const [now, setNow] = React.useState(() => Date.now());
4589
- const remainingMs = launchDate - now;
4588
+ const [now2, setNow] = React.useState(() => Date.now());
4589
+ const remainingMs = launchDate - now2;
4590
4590
  const hasLaunched = remainingMs <= 0;
4591
4591
  React.useEffect(() => {
4592
4592
  if (hasLaunched) return;
@@ -6010,6 +6010,9 @@ var EN = {
6010
6010
  conceptAskQuestion: "How does {title} work in {product}?",
6011
6011
  copyAnswer: "Copy answer",
6012
6012
  copyFailed: "Couldn't copy",
6013
+ readAloud: "Read aloud",
6014
+ stopReading: "Stop reading",
6015
+ answerTable: "Table",
6013
6016
  groundedIn: "What this answer is based on",
6014
6017
  sourceCountOne: "1 source",
6015
6018
  sourceCountMany: "{count} sources",
@@ -6092,6 +6095,9 @@ var ZH = {
6092
6095
  conceptAskQuestion: "{product} \u7684 {title} \u662F\u600E\u4E48\u8FD0\u4F5C\u7684\uFF1F",
6093
6096
  copyAnswer: "\u590D\u5236\u56DE\u7B54",
6094
6097
  copyFailed: "\u590D\u5236\u5931\u8D25",
6098
+ readAloud: "\u6717\u8BFB",
6099
+ stopReading: "\u505C\u6B62\u6717\u8BFB",
6100
+ answerTable: "\u8868\u683C",
6095
6101
  groundedIn: "\u8FD9\u4E2A\u56DE\u7B54\u7684\u4F9D\u636E",
6096
6102
  sourceCountOne: "1 \u4E2A\u6765\u6E90",
6097
6103
  sourceCountMany: "{count} \u4E2A\u6765\u6E90",
@@ -6170,6 +6176,9 @@ var HI = {
6170
6176
  conceptAskQuestion: "{product} \u092E\u0947\u0902 {title} \u0915\u0948\u0938\u0947 \u0915\u093E\u092E \u0915\u0930\u0924\u093E \u0939\u0948?",
6171
6177
  copyAnswer: "\u0909\u0924\u094D\u0924\u0930 \u0915\u0949\u092A\u0940 \u0915\u0930\u0947\u0902",
6172
6178
  copyFailed: "\u0915\u0949\u092A\u0940 \u0928\u0939\u0940\u0902 \u0939\u094B \u0938\u0915\u093E",
6179
+ readAloud: "\u091C\u093C\u094B\u0930 \u0938\u0947 \u092A\u0922\u093C\u0947\u0902",
6180
+ stopReading: "\u092A\u0922\u093C\u0928\u093E \u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
6181
+ answerTable: "\u0924\u093E\u0932\u093F\u0915\u093E",
6173
6182
  groundedIn: "\u092F\u0939 \u0909\u0924\u094D\u0924\u0930 \u0915\u093F\u0938 \u092A\u0930 \u0906\u0927\u093E\u0930\u093F\u0924 \u0939\u0948",
6174
6183
  sourceCountOne: "1 \u0938\u094D\u0930\u094B\u0924",
6175
6184
  sourceCountMany: "{count} \u0938\u094D\u0930\u094B\u0924",
@@ -6252,6 +6261,9 @@ var ES = {
6252
6261
  conceptAskQuestion: "\xBFC\xF3mo funciona {title} en {product}?",
6253
6262
  copyAnswer: "Copiar respuesta",
6254
6263
  copyFailed: "No se pudo copiar",
6264
+ readAloud: "Leer en voz alta",
6265
+ stopReading: "Dejar de leer",
6266
+ answerTable: "Tabla",
6255
6267
  groundedIn: "En qu\xE9 se basa esta respuesta",
6256
6268
  sourceCountOne: "1 fuente",
6257
6269
  sourceCountMany: "{count} fuentes",
@@ -6334,6 +6346,9 @@ var AR = {
6334
6346
  conceptAskQuestion: "\u0643\u064A\u0641 \u064A\u0639\u0645\u0644 {title} \u0641\u064A {product}\u061F",
6335
6347
  copyAnswer: "\u0646\u0633\u062E \u0627\u0644\u0625\u062C\u0627\u0628\u0629",
6336
6348
  copyFailed: "\u062A\u0639\u0630\u0651\u0631 \u0627\u0644\u0646\u0633\u062E",
6349
+ readAloud: "\u0627\u0642\u0631\u0623 \u0628\u0635\u0648\u062A \u0639\u0627\u0644\u064D",
6350
+ stopReading: "\u0625\u064A\u0642\u0627\u0641 \u0627\u0644\u0642\u0631\u0627\u0621\u0629",
6351
+ answerTable: "\u062C\u062F\u0648\u0644",
6337
6352
  groundedIn: "\u0639\u0644\u0649 \u0645\u0627\u0630\u0627 \u062A\u0633\u062A\u0646\u062F \u0647\u0630\u0647 \u0627\u0644\u0625\u062C\u0627\u0628\u0629",
6338
6353
  sourceCountOne: "\u0645\u0635\u062F\u0631 \u0648\u0627\u062D\u062F",
6339
6354
  sourceCountMany: "{count} \u0645\u0635\u0627\u062F\u0631",
@@ -6416,6 +6431,9 @@ var TA = {
6416
6431
  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?",
6417
6432
  copyAnswer: "\u0BAA\u0BA4\u0BBF\u0BB2\u0BC8 \u0BA8\u0B95\u0BB2\u0BC6\u0B9F\u0BC1",
6418
6433
  copyFailed: "\u0BA8\u0B95\u0BB2\u0BC6\u0B9F\u0BC1\u0B95\u0BCD\u0B95 \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8",
6434
+ readAloud: "\u0B9A\u0BA4\u0BCD\u0BA4\u0BAE\u0BBE\u0B95\u0BAA\u0BCD \u0BAA\u0B9F\u0BBF",
6435
+ stopReading: "\u0BAA\u0B9F\u0BBF\u0BAA\u0BCD\u0BAA\u0BA4\u0BC8 \u0BA8\u0BBF\u0BB1\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1",
6436
+ answerTable: "\u0B85\u0B9F\u0BCD\u0B9F\u0BB5\u0BA3\u0BC8",
6419
6437
  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",
6420
6438
  sourceCountOne: "1 \u0BAE\u0BC2\u0BB2\u0BAE\u0BCD",
6421
6439
  sourceCountMany: "{count} \u0BAE\u0BC2\u0BB2\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
@@ -6447,6 +6465,17 @@ var STRINGS = {
6447
6465
  ar: AR,
6448
6466
  ta: TA
6449
6467
  };
6468
+ var SPEECH_TAGS = {
6469
+ en: "en-US",
6470
+ zh: "zh-CN",
6471
+ hi: "hi-IN",
6472
+ es: "es-ES",
6473
+ ar: "ar-SA",
6474
+ ta: "ta-IN"
6475
+ };
6476
+ function speechLocaleOf(locale) {
6477
+ return SPEECH_TAGS[locale];
6478
+ }
6450
6479
  function resolveExploreLocale(explicit) {
6451
6480
  const candidates = [
6452
6481
  explicit,
@@ -7954,6 +7983,224 @@ function makeOptimisticId() {
7954
7983
  optimisticCounter += 1;
7955
7984
  return `boff-explore-local-${Date.now()}-${optimisticCounter}`;
7956
7985
  }
7986
+ var DRAIN_TARGET_MS = 1200;
7987
+ var MIN_CHARS_PER_SECOND = 90;
7988
+ var MAX_CHARS_PER_SECOND = 2200;
7989
+ var MAX_STEP_MS = 250;
7990
+ var COMMIT_INTERVAL_MS = 45;
7991
+ var FALLBACK_STEP_MS = 50;
7992
+ var FENCE = /^ {0,3}(?:```|~~~)/;
7993
+ var TABLE_ROW = /^ {0,3}\|/;
7994
+ var EMPTY_BLOCK_MARKER = /^ {0,3}(?:[-+>]|#{1,6}|\d{1,9}[.)])\s*$/;
7995
+ function isWordCharacter(char) {
7996
+ return char !== void 0 && /[\p{L}\p{N}]/u.test(char);
7997
+ }
7998
+ function clamp(value, min, max) {
7999
+ return Math.min(Math.max(value, min), max);
8000
+ }
8001
+ function now() {
8002
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
8003
+ }
8004
+ function isHighSurrogate(code) {
8005
+ return code >= 55296 && code <= 56319;
8006
+ }
8007
+ function safeInlineEnd(line) {
8008
+ let codeSpan = null;
8009
+ let link = null;
8010
+ const emphasis = [];
8011
+ let at = 0;
8012
+ while (at < line.length) {
8013
+ const char = line[at] ?? "";
8014
+ if (char === "\\") {
8015
+ at += 2;
8016
+ continue;
8017
+ }
8018
+ if (char === "`") {
8019
+ let run = 1;
8020
+ while (line[at + run] === "`") run += 1;
8021
+ codeSpan = codeSpan === null ? at : null;
8022
+ at += run;
8023
+ continue;
8024
+ }
8025
+ if (codeSpan !== null) {
8026
+ at += 1;
8027
+ continue;
8028
+ }
8029
+ if (char === "[") {
8030
+ if (link === null) link = line[at - 1] === "!" ? at - 1 : at;
8031
+ at += 1;
8032
+ continue;
8033
+ }
8034
+ if (char === "]") {
8035
+ if (link !== null && line[at + 1] !== "(") link = null;
8036
+ at += 1;
8037
+ continue;
8038
+ }
8039
+ if (char === ")") {
8040
+ link = null;
8041
+ at += 1;
8042
+ continue;
8043
+ }
8044
+ if (char === "*" || char === "_" || char === "~") {
8045
+ let run = 1;
8046
+ while (line[at + run] === char) run += 1;
8047
+ const start = at;
8048
+ at += run;
8049
+ const before = line[start - 1];
8050
+ const after = line[start + run];
8051
+ if (run < 2) continue;
8052
+ if (char === "_" && isWordCharacter(before) && isWordCharacter(after)) {
8053
+ continue;
8054
+ }
8055
+ const marker = char.repeat(run);
8056
+ const open2 = emphasis.findIndex((entry) => entry.marker === marker);
8057
+ if (open2 !== -1) {
8058
+ emphasis.splice(open2, 1);
8059
+ continue;
8060
+ }
8061
+ if (after !== void 0 && /\s/.test(after)) continue;
8062
+ emphasis.push({ marker, start });
8063
+ continue;
8064
+ }
8065
+ at += 1;
8066
+ }
8067
+ const open = [codeSpan, link, emphasis[0]?.start ?? null].filter(
8068
+ (value) => value !== null
8069
+ );
8070
+ return open.length === 0 ? line.length : Math.min(...open);
8071
+ }
8072
+ function revealBoundary(text, limit) {
8073
+ if (limit <= 0) return 0;
8074
+ const caughtUp = limit >= text.length;
8075
+ const head = caughtUp ? text : text.slice(0, limit);
8076
+ const lineStart = head.lastIndexOf("\n") + 1;
8077
+ const line = head.slice(lineStart);
8078
+ let fenceStart = null;
8079
+ let tableStart = null;
8080
+ let tableRows = 0;
8081
+ let at = 0;
8082
+ while (at < lineStart) {
8083
+ const breakAt = head.indexOf("\n", at);
8084
+ const end = breakAt === -1 ? lineStart : breakAt;
8085
+ const row = head.slice(at, end);
8086
+ if (fenceStart !== null) {
8087
+ if (FENCE.test(row)) fenceStart = null;
8088
+ } else if (FENCE.test(row)) {
8089
+ fenceStart = at;
8090
+ } else if (TABLE_ROW.test(row)) {
8091
+ if (tableStart === null) {
8092
+ tableStart = at;
8093
+ tableRows = 0;
8094
+ }
8095
+ tableRows += 1;
8096
+ } else if (tableStart !== null) {
8097
+ tableStart = null;
8098
+ tableRows = 0;
8099
+ }
8100
+ at = end + 1;
8101
+ }
8102
+ if (fenceStart === null) {
8103
+ const lastIsRow = TABLE_ROW.test(line);
8104
+ if (lastIsRow && tableStart === null) {
8105
+ tableStart = lineStart;
8106
+ tableRows = 0;
8107
+ } else if (!lastIsRow && line !== "") {
8108
+ tableStart = null;
8109
+ tableRows = 0;
8110
+ }
8111
+ if (tableStart !== null && tableRows < 2) return tableStart;
8112
+ }
8113
+ if (caughtUp) return text.length;
8114
+ if (fenceStart !== null) return fenceStart;
8115
+ if (TABLE_ROW.test(line)) return lineStart;
8116
+ if (EMPTY_BLOCK_MARKER.test(line)) return lineStart;
8117
+ const cut = lineStart + safeInlineEnd(line);
8118
+ return isHighSurrogate(text.charCodeAt(cut - 1)) ? cut - 1 : cut;
8119
+ }
8120
+ function prefersReducedMotion() {
8121
+ if (typeof window === "undefined") return true;
8122
+ try {
8123
+ return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
8124
+ } catch {
8125
+ return false;
8126
+ }
8127
+ }
8128
+ function useProgressiveReveal(text, settled) {
8129
+ const [, setFrame] = React.useState(0);
8130
+ const [reduced] = React.useState(prefersReducedMotion);
8131
+ const immediate = settled || reduced;
8132
+ const textRef = React.useRef(text);
8133
+ const cursorRef = React.useRef(immediate ? text.length : 0);
8134
+ const cutRef = React.useRef(immediate ? text.length : 0);
8135
+ const committedRef = React.useRef(0);
8136
+ if (text !== textRef.current) {
8137
+ const revealed = textRef.current.slice(0, Math.floor(cursorRef.current));
8138
+ if (revealed !== "" && !text.startsWith(revealed)) {
8139
+ cursorRef.current = text.length;
8140
+ }
8141
+ cursorRef.current = Math.min(cursorRef.current, text.length);
8142
+ cutRef.current = Math.min(cutRef.current, text.length);
8143
+ textRef.current = text;
8144
+ }
8145
+ if (immediate) cursorRef.current = text.length;
8146
+ const cut = immediate ? text.length : Math.max(
8147
+ cutRef.current,
8148
+ revealBoundary(text, Math.floor(cursorRef.current))
8149
+ );
8150
+ cutRef.current = cut;
8151
+ React.useEffect(() => {
8152
+ if (immediate) return;
8153
+ if (cursorRef.current >= text.length) return;
8154
+ let cancelled = false;
8155
+ let cancelStep = null;
8156
+ let previous = now();
8157
+ const commit = (at, force) => {
8158
+ if (!force && at - committedRef.current < COMMIT_INTERVAL_MS) return;
8159
+ committedRef.current = at;
8160
+ setFrame((value) => value + 1);
8161
+ };
8162
+ const step = () => {
8163
+ cancelStep = null;
8164
+ if (cancelled) return;
8165
+ const at = now();
8166
+ const elapsed = clamp(at - previous, 0, MAX_STEP_MS);
8167
+ previous = at;
8168
+ const length = textRef.current.length;
8169
+ const pending = length - cursorRef.current;
8170
+ if (pending <= 0) {
8171
+ commit(at, true);
8172
+ return;
8173
+ }
8174
+ const perSecond = clamp(
8175
+ pending / (DRAIN_TARGET_MS / 1e3),
8176
+ MIN_CHARS_PER_SECOND,
8177
+ MAX_CHARS_PER_SECOND
8178
+ );
8179
+ cursorRef.current = Math.min(
8180
+ length,
8181
+ cursorRef.current + perSecond * elapsed / 1e3
8182
+ );
8183
+ const done = cursorRef.current >= length;
8184
+ commit(at, done);
8185
+ if (!done) schedule();
8186
+ };
8187
+ const schedule = () => {
8188
+ if (typeof window.requestAnimationFrame === "function") {
8189
+ const id2 = window.requestAnimationFrame(step);
8190
+ cancelStep = () => window.cancelAnimationFrame(id2);
8191
+ return;
8192
+ }
8193
+ const id = setTimeout(step, FALLBACK_STEP_MS);
8194
+ cancelStep = () => clearTimeout(id);
8195
+ };
8196
+ schedule();
8197
+ return () => {
8198
+ cancelled = true;
8199
+ cancelStep?.();
8200
+ };
8201
+ }, [text, immediate]);
8202
+ return text.slice(0, cut);
8203
+ }
7957
8204
  function getRecognitionCtor() {
7958
8205
  if (typeof window === "undefined") return null;
7959
8206
  const w = window;
@@ -8172,6 +8419,141 @@ function useSpeechInput({
8172
8419
  );
8173
8420
  return { supported, listening, interim, error, start, stop, toggle };
8174
8421
  }
8422
+ var SILENT_FAILURE_MS = 1500;
8423
+ function speechPlatform() {
8424
+ if (typeof window === "undefined") return null;
8425
+ const scope = window;
8426
+ const synth = scope.speechSynthesis;
8427
+ const Utterance = scope.SpeechSynthesisUtterance;
8428
+ if (!synth || typeof synth.speak !== "function") return null;
8429
+ if (typeof Utterance !== "function") return null;
8430
+ return { synth, Utterance };
8431
+ }
8432
+ function normaliseTag(tag) {
8433
+ return tag.trim().toLowerCase().replace(/_/g, "-");
8434
+ }
8435
+ function primaryOf(tag) {
8436
+ return normaliseTag(tag).split("-")[0] ?? "";
8437
+ }
8438
+ function pickVoice(voices, lang) {
8439
+ const wanted = normaliseTag(lang);
8440
+ if (wanted === "") return null;
8441
+ const primary = primaryOf(wanted);
8442
+ const exact = voices.filter((voice) => normaliseTag(voice.lang) === wanted);
8443
+ const related = voices.filter((voice) => primaryOf(voice.lang) === primary);
8444
+ return exact.find((voice) => voice.default) ?? exact[0] ?? related.find((voice) => voice.default) ?? related[0] ?? null;
8445
+ }
8446
+ var activeOwner = null;
8447
+ var activeStop = null;
8448
+ function stopSpeaking() {
8449
+ activeStop?.();
8450
+ }
8451
+ function useSpeechOutput({
8452
+ lang
8453
+ } = {}) {
8454
+ const [available, setAvailable] = React.useState(() => speechPlatform() !== null);
8455
+ const [speaking, setSpeaking] = React.useState(false);
8456
+ const utteranceRef = React.useRef(null);
8457
+ const watchdogRef = React.useRef(null);
8458
+ const ownerRef = React.useRef({});
8459
+ const stopRef = React.useRef(() => void 0);
8460
+ const langRef = React.useRef(lang);
8461
+ langRef.current = lang;
8462
+ const clearWatchdog = React.useCallback(() => {
8463
+ if (watchdogRef.current === null) return;
8464
+ clearTimeout(watchdogRef.current);
8465
+ watchdogRef.current = null;
8466
+ }, []);
8467
+ const settle = React.useCallback(() => {
8468
+ clearWatchdog();
8469
+ utteranceRef.current = null;
8470
+ if (activeOwner === ownerRef.current) {
8471
+ activeOwner = null;
8472
+ activeStop = null;
8473
+ }
8474
+ setSpeaking(false);
8475
+ }, [clearWatchdog]);
8476
+ const stop = React.useCallback(() => {
8477
+ const owned = utteranceRef.current !== null;
8478
+ settle();
8479
+ if (owned) speechPlatform()?.synth.cancel();
8480
+ }, [settle]);
8481
+ stopRef.current = stop;
8482
+ const speak = React.useCallback(
8483
+ (text) => {
8484
+ const platform = speechPlatform();
8485
+ if (!platform) {
8486
+ setAvailable(false);
8487
+ return;
8488
+ }
8489
+ const spoken = text.trim();
8490
+ if (spoken === "") return;
8491
+ if (activeStop && activeOwner !== ownerRef.current) activeStop();
8492
+ platform.synth.cancel();
8493
+ const utterance = new platform.Utterance(spoken);
8494
+ const wanted = langRef.current;
8495
+ if (wanted !== void 0 && wanted !== "") {
8496
+ const voice = pickVoice(platform.synth.getVoices(), wanted);
8497
+ utterance.lang = voice?.lang ?? wanted;
8498
+ if (voice) utterance.voice = voice;
8499
+ }
8500
+ const mine = () => utteranceRef.current === utterance;
8501
+ utterance.onstart = () => {
8502
+ if (!mine()) return;
8503
+ clearWatchdog();
8504
+ setSpeaking(true);
8505
+ };
8506
+ utterance.onend = () => {
8507
+ if (!mine()) return;
8508
+ settle();
8509
+ };
8510
+ utterance.onerror = (event) => {
8511
+ if (!mine()) return;
8512
+ const code = typeof event?.error === "string" ? event.error : "";
8513
+ settle();
8514
+ if (code !== "interrupted" && code !== "canceled") setAvailable(false);
8515
+ };
8516
+ utteranceRef.current = utterance;
8517
+ activeOwner = ownerRef.current;
8518
+ activeStop = () => {
8519
+ stopRef.current();
8520
+ };
8521
+ setSpeaking(true);
8522
+ platform.synth.speak(utterance);
8523
+ clearWatchdog();
8524
+ watchdogRef.current = setTimeout(() => {
8525
+ watchdogRef.current = null;
8526
+ if (!mine()) return;
8527
+ if (platform.synth.speaking) return;
8528
+ settle();
8529
+ setAvailable(false);
8530
+ }, SILENT_FAILURE_MS);
8531
+ },
8532
+ [clearWatchdog, settle]
8533
+ );
8534
+ const toggle = React.useCallback(
8535
+ (text) => {
8536
+ if (speaking) stop();
8537
+ else speak(text);
8538
+ },
8539
+ [speak, speaking, stop]
8540
+ );
8541
+ React.useEffect(() => {
8542
+ if (!speaking || typeof window === "undefined") return;
8543
+ const onHide = () => {
8544
+ stopRef.current();
8545
+ };
8546
+ window.addEventListener("pagehide", onHide);
8547
+ return () => window.removeEventListener("pagehide", onHide);
8548
+ }, [speaking]);
8549
+ React.useEffect(
8550
+ () => () => {
8551
+ stopRef.current();
8552
+ },
8553
+ []
8554
+ );
8555
+ return { supported: available, speaking, speak, stop, toggle };
8556
+ }
8175
8557
  var SWIPE_THRESHOLD = 40;
8176
8558
  function useSwipe({
8177
8559
  onSwipeLeft,
@@ -8439,6 +8821,115 @@ var EXPLORE_CSS = `
8439
8821
  height: auto;
8440
8822
  }
8441
8823
 
8824
+ /* ---- Comparison and pricing tables ----------------------------------------------------
8825
+ The harness emits a comparison, and a set of plans, as a GFM table whose FIRST column is
8826
+ the subject \u2014 the product, or the plan (CONTRACT.md \xA77). Two things have to be true of it:
8827
+ the page must not scroll sideways because of it, and it has to be readable on a 320px
8828
+ phone, which a four-column grid of 1px borders is not.
8829
+
8830
+ Wide: a contained scroller. \`max-width: 100%\` keeps the box inside the bubble and
8831
+ \`overflow-x: auto\` keeps the columns inside the box, so the widest possible table still
8832
+ cannot widen the document. \`overscroll-behavior-x: contain\` stops a swipe that runs out
8833
+ of table from chaining to the page \u2014 the same rule the transcript needed. */
8834
+ .boff-explore-body .boff-explore-table {
8835
+ max-width: 100%;
8836
+ overflow-x: auto;
8837
+ overscroll-behavior-x: contain;
8838
+ margin: 0.75rem 0;
8839
+ border: 1px solid hsl(var(--border));
8840
+ border-radius: 0.75rem;
8841
+ background-color: hsl(var(--card));
8842
+ }
8843
+ .boff-explore-body .boff-explore-table:focus-visible {
8844
+ outline: 2px solid hsl(var(--ring));
8845
+ outline-offset: 2px;
8846
+ }
8847
+ .boff-explore-body .boff-explore-table table {
8848
+ /* Beats the blanket \`display: block\` above, which is the unwrapped fallback. */
8849
+ display: table;
8850
+ width: 100%;
8851
+ max-width: none;
8852
+ margin: 0;
8853
+ border-collapse: collapse;
8854
+ font-size: 0.8125rem;
8855
+ }
8856
+ .boff-explore-body .boff-explore-table th,
8857
+ .boff-explore-body .boff-explore-table td {
8858
+ border: 0;
8859
+ border-top: 1px solid hsl(var(--border));
8860
+ padding: 0.5rem 0.75rem;
8861
+ /* \`start\`, never \`left\`: the page root carries dir="rtl" in Arabic, and a left-aligned
8862
+ table there reads against the grain of every other line on the page. */
8863
+ text-align: start;
8864
+ vertical-align: top;
8865
+ overflow-wrap: anywhere;
8866
+ /* Prices in a column line up under each other instead of drifting by digit width. */
8867
+ font-variant-numeric: tabular-nums;
8868
+ min-width: 6rem;
8869
+ }
8870
+ .boff-explore-body .boff-explore-table thead th {
8871
+ border-top: 0;
8872
+ background-color: hsl(var(--muted));
8873
+ color: hsl(var(--foreground));
8874
+ font-weight: 600;
8875
+ white-space: nowrap;
8876
+ }
8877
+ /* The first column is what the row is ABOUT \u2014 the product, or the plan name. Reading it as
8878
+ the row's heading is what makes a price list scannable rather than a grid of strings. */
8879
+ .boff-explore-body .boff-explore-table tbody td:first-child {
8880
+ font-weight: 600;
8881
+ color: hsl(var(--card-foreground));
8882
+ }
8883
+
8884
+ /* Narrow: one card per row. Each cell prints its own column heading, so a value is never
8885
+ stranded without the question it answers, and the subject cell becomes the card's title.
8886
+ A media query rather than a measured width \u2014 these pages are serialised post-hydration,
8887
+ so anything measured here would ship to every phone as static HTML. */
8888
+ @media (max-width: 30rem) {
8889
+ .boff-explore-body .boff-explore-table {
8890
+ overflow-x: visible;
8891
+ }
8892
+ .boff-explore-body .boff-explore-table table,
8893
+ .boff-explore-body .boff-explore-table tbody,
8894
+ .boff-explore-body .boff-explore-table tr,
8895
+ .boff-explore-body .boff-explore-table td {
8896
+ display: block;
8897
+ width: 100%;
8898
+ min-width: 0;
8899
+ }
8900
+ .boff-explore-body .boff-explore-table thead {
8901
+ display: none;
8902
+ }
8903
+ .boff-explore-body .boff-explore-table tr {
8904
+ border-top: 1px solid hsl(var(--border));
8905
+ padding: 0.5rem 0;
8906
+ }
8907
+ .boff-explore-body .boff-explore-table tr:first-child {
8908
+ border-top: 0;
8909
+ }
8910
+ .boff-explore-body .boff-explore-table td {
8911
+ border: 0;
8912
+ padding: 0.125rem 0.75rem;
8913
+ }
8914
+ .boff-explore-body .boff-explore-table td[data-label]::before {
8915
+ content: attr(data-label);
8916
+ display: block;
8917
+ font-size: 0.6875rem;
8918
+ font-weight: 600;
8919
+ text-transform: uppercase;
8920
+ letter-spacing: 0.04em;
8921
+ color: hsl(var(--muted-foreground));
8922
+ }
8923
+ .boff-explore-body .boff-explore-table tbody td:first-child {
8924
+ font-size: 0.9375rem;
8925
+ padding-bottom: 0.25rem;
8926
+ }
8927
+ .boff-explore-body .boff-explore-table tbody td:first-child::before {
8928
+ /* The plan's own name is the heading here; repeating "Plan" over it says nothing. */
8929
+ display: none;
8930
+ }
8931
+ }
8932
+
8442
8933
  /* ---- Ambient backdrop -----------------------------------------------------------------
8443
8934
  Two very low-opacity radial washes behind the welcome state, drifting slowly. Painted on
8444
8935
  a pseudo-element with pointer-events:none so it can never intercept a click \u2014 a
@@ -8610,7 +9101,7 @@ function isOwnElement(el, root) {
8610
9101
  function visualViewportHeight() {
8611
9102
  return window.visualViewport?.height ?? window.innerHeight;
8612
9103
  }
8613
- function clamp(value, min, max) {
9104
+ function clamp2(value, min, max) {
8614
9105
  return Math.min(Math.max(value, min), max);
8615
9106
  }
8616
9107
  function isRestoredNavigation() {
@@ -8760,11 +9251,80 @@ function summariseReferences(references, origins, productNames) {
8760
9251
  };
8761
9252
  }
8762
9253
  var CODE_FENCE = /^\s*(?:```|~~~)/;
8763
- function answerPlainText(markdown) {
9254
+ var TABLE_ROW_LINE = /^\s{0,3}\|/;
9255
+ var TABLE_DELIMITER_ROW = /^\s{0,3}\|[\s:|-]*-[\s:|-]*$/;
9256
+ function readableLine(raw, mode) {
9257
+ let line = raw;
9258
+ line = line.replace(/!\[[^\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g, "");
9259
+ line = mode === "speech" ? (
9260
+ // A voice reading "h-t-t-p-s colon slash slash…" mid-sentence is noise, and the
9261
+ // prompt asks for two to five links in every answer. The label IS the words.
9262
+ line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1")
9263
+ ) : line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1 ($2)");
9264
+ line = mode === "speech" ? line.replace(/<(https?:\/\/[^>\s]+)>/g, "") : line.replace(/<(https?:\/\/[^>\s]+)>/g, "$1");
9265
+ line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
9266
+ line = line.replace(/^\s{0,3}>\s?/, "");
9267
+ if (/^\s{0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/.test(line)) return "";
9268
+ line = line.replace(/`([^`]+)`/g, "$1");
9269
+ line = line.replace(/\*\*([^*]+)\*\*/g, "$1");
9270
+ line = line.replace(/__([^_]+)__/g, "$1");
9271
+ line = line.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, "$1$2");
9272
+ line = line.replace(/(^|[^_\w])_([^_\n]+)_(?![_\w])/g, "$1$2");
9273
+ return line.replace(/[ \t]+$/, "");
9274
+ }
9275
+ function tableCells(row) {
9276
+ const trimmed = row.trim();
9277
+ const cells = [];
9278
+ let cell = "";
9279
+ for (let at = 0; at < trimmed.length; at += 1) {
9280
+ const char = trimmed[at] ?? "";
9281
+ if (char === "\\") {
9282
+ cell += char + (trimmed[at + 1] ?? "");
9283
+ at += 1;
9284
+ continue;
9285
+ }
9286
+ if (char === "|") {
9287
+ cells.push(cell.trim());
9288
+ cell = "";
9289
+ continue;
9290
+ }
9291
+ cell += char;
9292
+ }
9293
+ cells.push(cell.trim());
9294
+ if (trimmed.startsWith("|") && cells[0] === "") cells.shift();
9295
+ if (trimmed.endsWith("|") && cells[cells.length - 1] === "") cells.pop();
9296
+ return cells;
9297
+ }
9298
+ function asSentence(text) {
9299
+ return /[.!?]$/.test(text) ? text : `${text}.`;
9300
+ }
9301
+ function spokenTable(rows) {
9302
+ if (rows.length < 2 || !TABLE_DELIMITER_ROW.test(rows[1] ?? "")) return rows;
9303
+ const labels = tableCells(rows[0] ?? "");
9304
+ const spoken = [];
9305
+ for (const row of rows.slice(2)) {
9306
+ const parts = [];
9307
+ tableCells(row).forEach((value, column) => {
9308
+ if (value === "") return;
9309
+ const label = column === 0 ? "" : labels[column] ?? "";
9310
+ parts.push(label === "" ? value : `${label}: ${value}`);
9311
+ });
9312
+ if (parts.length > 0) spoken.push(asSentence(parts.join(". ")));
9313
+ }
9314
+ return spoken;
9315
+ }
9316
+ function readableText(markdown, mode) {
8764
9317
  const lines = [];
8765
9318
  let inFence = false;
9319
+ let table = [];
9320
+ const flushTable = () => {
9321
+ if (table.length === 0) return;
9322
+ for (const line of spokenTable(table)) lines.push(readableLine(line, mode));
9323
+ table = [];
9324
+ };
8766
9325
  for (const raw of markdown.split("\n")) {
8767
9326
  if (CODE_FENCE.test(raw)) {
9327
+ flushTable();
8768
9328
  inFence = !inFence;
8769
9329
  continue;
8770
9330
  }
@@ -8772,33 +9332,30 @@ function answerPlainText(markdown) {
8772
9332
  lines.push(raw);
8773
9333
  continue;
8774
9334
  }
8775
- let line = raw;
8776
- line = line.replace(/!\[[^\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g, "");
8777
- line = line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1 ($2)");
8778
- line = line.replace(/<(https?:\/\/[^>\s]+)>/g, "$1");
8779
- line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
8780
- line = line.replace(/^\s{0,3}>\s?/, "");
8781
- if (/^\s{0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/.test(line)) {
8782
- lines.push("");
9335
+ if (mode === "speech" && TABLE_ROW_LINE.test(raw)) {
9336
+ table.push(raw);
8783
9337
  continue;
8784
9338
  }
8785
- line = line.replace(/`([^`]+)`/g, "$1");
8786
- line = line.replace(/\*\*([^*]+)\*\*/g, "$1");
8787
- line = line.replace(/__([^_]+)__/g, "$1");
8788
- line = line.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, "$1$2");
8789
- line = line.replace(/(^|[^_\w])_([^_\n]+)_(?![_\w])/g, "$1$2");
8790
- lines.push(line.replace(/[ \t]+$/, ""));
9339
+ flushTable();
9340
+ lines.push(readableLine(raw, mode));
8791
9341
  }
9342
+ flushTable();
8792
9343
  return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
8793
9344
  }
8794
- var InlineImageScopeContext = React.createContext(null);
9345
+ function answerPlainText(markdown) {
9346
+ return readableText(markdown, "clipboard");
9347
+ }
9348
+ function answerSpeechText(markdown) {
9349
+ return readableText(markdown, "speech");
9350
+ }
9351
+ var AnswerScopeContext = React.createContext(null);
8795
9352
  var InsideLinkContext = React.createContext(false);
8796
9353
  function AnswerLink(props) {
8797
9354
  return /* @__PURE__ */ jsxRuntime.jsx(InsideLinkContext.Provider, { value: true, children: /* @__PURE__ */ jsxRuntime.jsx(MarkdownLink, { ...props }) });
8798
9355
  }
8799
9356
  var INLINE_IMAGE_FRAME = "relative inline-block max-w-full overflow-hidden rounded-xl border border-border bg-muted align-top shadow-sm";
8800
9357
  function InlineConceptImage({ src, alt }) {
8801
- const scope = React.useContext(InlineImageScopeContext);
9358
+ const scope = React.useContext(AnswerScopeContext);
8802
9359
  const insideLink = React.useContext(InsideLinkContext);
8803
9360
  if (!scope || !scope.enabled || !isInlineImageAllowed(src, scope.origins) || scope.failed.has(src)) {
8804
9361
  return null;
@@ -8846,9 +9403,70 @@ function InlineConceptImage({ src, alt }) {
8846
9403
  }
8847
9404
  ) });
8848
9405
  }
9406
+ function markdownText(node) {
9407
+ if (!node) return "";
9408
+ if (typeof node.value === "string") return node.value;
9409
+ return (node.children ?? []).map(markdownText).join("");
9410
+ }
9411
+ function hasHeaderCells(node) {
9412
+ return (node?.children ?? []).some((cell) => cell.tagName === "th");
9413
+ }
9414
+ function findHeaderRow(node) {
9415
+ for (const child of node?.children ?? []) {
9416
+ if (child.tagName === "tr" && hasHeaderCells(child)) return child;
9417
+ const nested = findHeaderRow(child);
9418
+ if (nested !== null) return nested;
9419
+ }
9420
+ return null;
9421
+ }
9422
+ function tableColumnLabels(node) {
9423
+ const row = findHeaderRow(node);
9424
+ if (row === null) return [];
9425
+ return (row.children ?? []).filter((cell) => cell.tagName === "th").map((cell) => markdownText(cell).trim());
9426
+ }
9427
+ var TableColumnsContext = React.createContext([]);
9428
+ function AnswerTable({ node, children }) {
9429
+ const scope = React.useContext(AnswerScopeContext);
9430
+ const table = /* @__PURE__ */ jsxRuntime.jsx(TableColumnsContext.Provider, { value: tableColumnLabels(node), children: /* @__PURE__ */ jsxRuntime.jsx("table", { children }) });
9431
+ if (!scope) return table;
9432
+ return /* @__PURE__ */ jsxRuntime.jsx(
9433
+ "div",
9434
+ {
9435
+ "data-boff-explore": "table",
9436
+ role: "region",
9437
+ "aria-label": scope.strings.answerTable,
9438
+ tabIndex: 0,
9439
+ className: "boff-explore-table",
9440
+ children: table
9441
+ }
9442
+ );
9443
+ }
9444
+ function AnswerTableRow({
9445
+ node,
9446
+ children,
9447
+ ...props
9448
+ }) {
9449
+ const labels = React.useContext(TableColumnsContext);
9450
+ if (labels.length === 0 || hasHeaderCells(node)) {
9451
+ return /* @__PURE__ */ jsxRuntime.jsx("tr", { ...props, children });
9452
+ }
9453
+ let column = 0;
9454
+ const cells = React.Children.map(children, (cell) => {
9455
+ if (!React.isValidElement(cell)) return cell;
9456
+ const label = labels[column] ?? "";
9457
+ column += 1;
9458
+ if (label === "") return cell;
9459
+ return React.cloneElement(cell, {
9460
+ "data-label": label
9461
+ });
9462
+ });
9463
+ return /* @__PURE__ */ jsxRuntime.jsx("tr", { ...props, children: cells });
9464
+ }
8849
9465
  var ANSWER_MARKDOWN_COMPONENTS = {
8850
9466
  a: AnswerLink,
8851
- img: InlineConceptImage
9467
+ img: InlineConceptImage,
9468
+ table: AnswerTable,
9469
+ tr: AnswerTableRow
8852
9470
  };
8853
9471
  function ReferenceCard({
8854
9472
  reference,
@@ -9237,9 +9855,42 @@ function ProvenanceChips({
9237
9855
  );
9238
9856
  }
9239
9857
  var COPY_FEEDBACK_MS = 2500;
9858
+ 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";
9859
+ var ANSWER_ACTION_IDLE = "border-border bg-card text-muted-foreground hover:border-primary/40 hover:bg-accent hover:text-foreground";
9860
+ var ANSWER_ACTION_ACTIVE = "border-primary/40 bg-accent text-primary";
9861
+ function ReadAloudButton({
9862
+ strings: T,
9863
+ text,
9864
+ lang
9865
+ }) {
9866
+ const speech = useSpeechOutput({ lang });
9867
+ if (!speech.supported) return null;
9868
+ const label = speech.speaking ? T.stopReading : T.readAloud;
9869
+ return /* @__PURE__ */ jsxRuntime.jsxs(
9870
+ "button",
9871
+ {
9872
+ "data-boff-explore": "read-aloud",
9873
+ "data-speaking": speech.speaking ? "true" : "false",
9874
+ type: "button",
9875
+ title: label,
9876
+ "aria-pressed": speech.speaking,
9877
+ onClick: () => speech.toggle(text),
9878
+ className: cn(
9879
+ ANSWER_ACTION_PILL,
9880
+ speech.speaking ? ANSWER_ACTION_ACTIVE : ANSWER_ACTION_IDLE
9881
+ ),
9882
+ children: [
9883
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Volume2, { className: "h-3 w-3 shrink-0", "aria-hidden": "true" }),
9884
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: label })
9885
+ ]
9886
+ }
9887
+ );
9888
+ }
9240
9889
  function AnswerActions({
9241
9890
  strings: T,
9242
- text
9891
+ text,
9892
+ speechText,
9893
+ speechLang
9243
9894
  }) {
9244
9895
  const [outcome, setOutcome] = React.useState("idle");
9245
9896
  const resetRef = React.useRef(null);
@@ -9282,9 +9933,8 @@ function AnswerActions({
9282
9933
  void copy();
9283
9934
  },
9284
9935
  className: cn(
9285
- "inline-flex max-w-full items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs transition-colors",
9286
- "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
9287
- 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"
9936
+ ANSWER_ACTION_PILL,
9937
+ outcome === "failed" ? "border-destructive/40 bg-card text-destructive" : ANSWER_ACTION_IDLE
9288
9938
  ),
9289
9939
  children: [
9290
9940
  outcome === "copied" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-3 w-3 shrink-0", "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { className: "h-3 w-3 shrink-0", "aria-hidden": "true" }),
@@ -9292,6 +9942,7 @@ function AnswerActions({
9292
9942
  ]
9293
9943
  }
9294
9944
  ),
9945
+ /* @__PURE__ */ jsxRuntime.jsx(ReadAloudButton, { strings: T, text: speechText, lang: speechLang }),
9295
9946
  /* @__PURE__ */ jsxRuntime.jsx("span", { "data-boff-explore": "copy-status", role: "status", className: "sr-only", children: outcome === "idle" ? "" : label })
9296
9947
  ]
9297
9948
  }
@@ -9343,6 +9994,7 @@ function AssistantBubble({
9343
9994
  activity,
9344
9995
  imageOrigins,
9345
9996
  productNames,
9997
+ speechLang,
9346
9998
  onNavigate,
9347
9999
  anchorRef,
9348
10000
  onFollow,
@@ -9353,6 +10005,7 @@ function AssistantBubble({
9353
10005
  const body = message.content || message.partialContent || "";
9354
10006
  const failed = message.status === "FAILED";
9355
10007
  const completed = message.status === "COMPLETED";
10008
+ const visible = useProgressiveReveal(body, !streaming);
9356
10009
  const [failedImages, setFailedImages] = React.useState(
9357
10010
  () => /* @__PURE__ */ new Set()
9358
10011
  );
@@ -9361,7 +10014,7 @@ function AssistantBubble({
9361
10014
  (current) => current.has(src) ? current : new Set(current).add(src)
9362
10015
  );
9363
10016
  }, []);
9364
- const imageScope = React.useMemo(
10017
+ const answerScope = React.useMemo(
9365
10018
  () => ({
9366
10019
  strings: T,
9367
10020
  enabled: completed,
@@ -9379,6 +10032,10 @@ function AssistantBubble({
9379
10032
  () => answerPlainText(message.content),
9380
10033
  [message.content]
9381
10034
  );
10035
+ const speechText = React.useMemo(
10036
+ () => answerSpeechText(message.content),
10037
+ [message.content]
10038
+ );
9382
10039
  const grounded = completed && message.answered !== false;
9383
10040
  return /* @__PURE__ */ jsxRuntime.jsxs(
9384
10041
  "div",
@@ -9425,8 +10082,8 @@ function AssistantBubble({
9425
10082
  ]
9426
10083
  }
9427
10084
  ) : null,
9428
- body ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-2xl rounded-tl-md border border-border bg-card px-4 py-3 shadow-sm", children: [
9429
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "boff-prose boff-prose-sm boff-explore-body min-w-0 text-card-foreground", children: /* @__PURE__ */ jsxRuntime.jsx(InlineImageScopeContext.Provider, { value: imageScope, children: /* @__PURE__ */ jsxRuntime.jsx(Markdown, { components: ANSWER_MARKDOWN_COMPONENTS, children: body }) }) }),
10085
+ visible ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-2xl rounded-tl-md border border-border bg-card px-4 py-3 shadow-sm", children: [
10086
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "boff-prose boff-prose-sm boff-explore-body min-w-0 text-card-foreground", children: /* @__PURE__ */ jsxRuntime.jsx(AnswerScopeContext.Provider, { value: answerScope, children: /* @__PURE__ */ jsxRuntime.jsx(Markdown, { components: ANSWER_MARKDOWN_COMPONENTS, children: visible }) }) }),
9430
10087
  streaming ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "boff-explore-caret", "aria-hidden": "true" }) : null
9431
10088
  ] }) : null,
9432
10089
  grounded && provenance.total > 0 ? /* @__PURE__ */ jsxRuntime.jsx(ProvenanceChips, { strings: T, provenance }) : null,
@@ -9469,7 +10126,15 @@ function AssistantBubble({
9469
10126
  question
9470
10127
  )) })
9471
10128
  ] }) : null,
9472
- completed && copyText !== "" ? /* @__PURE__ */ jsxRuntime.jsx(AnswerActions, { strings: T, text: copyText }) : null,
10129
+ completed && copyText !== "" ? /* @__PURE__ */ jsxRuntime.jsx(
10130
+ AnswerActions,
10131
+ {
10132
+ strings: T,
10133
+ text: copyText,
10134
+ speechText,
10135
+ speechLang
10136
+ }
10137
+ ) : null,
9473
10138
  completed && onFeedback ? /* @__PURE__ */ jsxRuntime.jsx(
9474
10139
  AnswerFeedback,
9475
10140
  {
@@ -9703,7 +10368,7 @@ function ConceptStrip({
9703
10368
  "div",
9704
10369
  {
9705
10370
  "data-boff-explore": "concept-strip-block",
9706
- className: "boff-explore-rise boff-explore-rise-3 mt-6",
10371
+ className: "boff-explore-rise boff-explore-rise-1 mt-6",
9707
10372
  children: [
9708
10373
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: T.conceptStripTitle }),
9709
10374
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -9893,6 +10558,15 @@ function WelcomeState({
9893
10558
  /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "mt-4 text-balance break-words text-2xl font-bold tracking-tight sm:text-4xl", children: title }),
9894
10559
  /* @__PURE__ */ jsxRuntime.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 })
9895
10560
  ] }),
10561
+ /* @__PURE__ */ jsxRuntime.jsx(
10562
+ ConceptStrip,
10563
+ {
10564
+ strings: T,
10565
+ highlights: conceptHighlights,
10566
+ onAsk: onPrompt,
10567
+ disabled
10568
+ }
10569
+ ),
9896
10570
  /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsxs(
9897
10571
  "li",
9898
10572
  {
@@ -9917,15 +10591,6 @@ function WelcomeState({
9917
10591
  }
9918
10592
  )
9919
10593
  ] }) : null,
9920
- /* @__PURE__ */ jsxRuntime.jsx(
9921
- ConceptStrip,
9922
- {
9923
- strings: T,
9924
- highlights: conceptHighlights,
9925
- onAsk: onPrompt,
9926
- disabled
9927
- }
9928
- ),
9929
10594
  products.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "boff-explore-rise boff-explore-rise-3 mt-6", children: [
9930
10595
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: T.focusProduct }),
9931
10596
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -9963,6 +10628,7 @@ function ExplorePage({
9963
10628
  );
9964
10629
  const T = React.useMemo(() => exploreStrings(activeLocale), [activeLocale]);
9965
10630
  const rtl = isRtlLocale(activeLocale);
10631
+ const speechLang = speechLocaleOf(activeLocale);
9966
10632
  const {
9967
10633
  phase,
9968
10634
  catalog,
@@ -10067,11 +10733,11 @@ function ExplorePage({
10067
10733
  const layoutHeight = window.innerHeight;
10068
10734
  if (layoutHeight === 0) return;
10069
10735
  const top = Math.round(
10070
- clamp(el.getBoundingClientRect().top, 0, layoutHeight)
10736
+ clamp2(el.getBoundingClientRect().top, 0, layoutHeight)
10071
10737
  );
10072
10738
  const viewport = window.visualViewport;
10073
10739
  const inset = viewport ? Math.round(
10074
- clamp(
10740
+ clamp2(
10075
10741
  layoutHeight - viewport.height - viewport.offsetTop,
10076
10742
  0,
10077
10743
  layoutHeight
@@ -10352,6 +11018,7 @@ function ExplorePage({
10352
11018
  const text = draft.trim();
10353
11019
  if (!text || overLimit || busy || !canSend) return;
10354
11020
  stopDictation();
11021
+ stopSpeaking();
10355
11022
  setDraft("");
10356
11023
  stickToBottomRef.current = true;
10357
11024
  void send(text, toPayload(stagedRef.current));
@@ -10362,6 +11029,7 @@ function ExplorePage({
10362
11029
  const sendPrompt = React.useCallback(
10363
11030
  (prompt) => {
10364
11031
  if (!canSend || busy) return;
11032
+ stopSpeaking();
10365
11033
  setDraft("");
10366
11034
  stickToBottomRef.current = true;
10367
11035
  void send(prompt, toPayload(stagedRef.current));
@@ -10597,6 +11265,7 @@ function ExplorePage({
10597
11265
  activity,
10598
11266
  imageOrigins,
10599
11267
  productNames,
11268
+ speechLang,
10600
11269
  onNavigate,
10601
11270
  anchorRef: message.id === latestAssistantId ? latestAssistantRef : void 0,
10602
11271
  onFollow: (kind, url) => recordClick(message.id, kind, url),
@@ -15631,14 +16300,18 @@ exports.getConceptBySlug = getConceptBySlug;
15631
16300
  exports.getRecaptchaV3Token = getRecaptchaV3Token;
15632
16301
  exports.isValidUrl = isValidUrl;
15633
16302
  exports.loadRecaptchaV3 = loadRecaptchaV3;
16303
+ exports.speechLocaleOf = speechLocaleOf;
16304
+ exports.stopSpeaking = stopSpeaking;
15634
16305
  exports.useContentPage = useContentPage;
15635
16306
  exports.useContentPages = useContentPages;
15636
16307
  exports.useExploreChat = useExploreChat;
15637
16308
  exports.useProduct = useProduct;
15638
16309
  exports.useProductConfig = useProductConfig;
16310
+ exports.useProgressiveReveal = useProgressiveReveal;
15639
16311
  exports.usePublicJobPosting = usePublicJobPosting;
15640
16312
  exports.usePublicJobPostings = usePublicJobPostings;
15641
16313
  exports.usePublicJobPostingsForSubmission = usePublicJobPostingsForSubmission;
16314
+ exports.useSpeechOutput = useSpeechOutput;
15642
16315
  exports.useWebSDK = useWebSDK;
15643
16316
  exports.useWebSDKConfig = useWebSDKConfig;
15644
16317
  //# sourceMappingURL=index.js.map