@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.
@@ -109,6 +109,9 @@ var EN = {
109
109
  conceptAskQuestion: "How does {title} work in {product}?",
110
110
  copyAnswer: "Copy answer",
111
111
  copyFailed: "Couldn't copy",
112
+ readAloud: "Read aloud",
113
+ stopReading: "Stop reading",
114
+ answerTable: "Table",
112
115
  groundedIn: "What this answer is based on",
113
116
  sourceCountOne: "1 source",
114
117
  sourceCountMany: "{count} sources",
@@ -191,6 +194,9 @@ var ZH = {
191
194
  conceptAskQuestion: "{product} \u7684 {title} \u662F\u600E\u4E48\u8FD0\u4F5C\u7684\uFF1F",
192
195
  copyAnswer: "\u590D\u5236\u56DE\u7B54",
193
196
  copyFailed: "\u590D\u5236\u5931\u8D25",
197
+ readAloud: "\u6717\u8BFB",
198
+ stopReading: "\u505C\u6B62\u6717\u8BFB",
199
+ answerTable: "\u8868\u683C",
194
200
  groundedIn: "\u8FD9\u4E2A\u56DE\u7B54\u7684\u4F9D\u636E",
195
201
  sourceCountOne: "1 \u4E2A\u6765\u6E90",
196
202
  sourceCountMany: "{count} \u4E2A\u6765\u6E90",
@@ -269,6 +275,9 @@ var HI = {
269
275
  conceptAskQuestion: "{product} \u092E\u0947\u0902 {title} \u0915\u0948\u0938\u0947 \u0915\u093E\u092E \u0915\u0930\u0924\u093E \u0939\u0948?",
270
276
  copyAnswer: "\u0909\u0924\u094D\u0924\u0930 \u0915\u0949\u092A\u0940 \u0915\u0930\u0947\u0902",
271
277
  copyFailed: "\u0915\u0949\u092A\u0940 \u0928\u0939\u0940\u0902 \u0939\u094B \u0938\u0915\u093E",
278
+ readAloud: "\u091C\u093C\u094B\u0930 \u0938\u0947 \u092A\u0922\u093C\u0947\u0902",
279
+ stopReading: "\u092A\u0922\u093C\u0928\u093E \u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
280
+ answerTable: "\u0924\u093E\u0932\u093F\u0915\u093E",
272
281
  groundedIn: "\u092F\u0939 \u0909\u0924\u094D\u0924\u0930 \u0915\u093F\u0938 \u092A\u0930 \u0906\u0927\u093E\u0930\u093F\u0924 \u0939\u0948",
273
282
  sourceCountOne: "1 \u0938\u094D\u0930\u094B\u0924",
274
283
  sourceCountMany: "{count} \u0938\u094D\u0930\u094B\u0924",
@@ -351,6 +360,9 @@ var ES = {
351
360
  conceptAskQuestion: "\xBFC\xF3mo funciona {title} en {product}?",
352
361
  copyAnswer: "Copiar respuesta",
353
362
  copyFailed: "No se pudo copiar",
363
+ readAloud: "Leer en voz alta",
364
+ stopReading: "Dejar de leer",
365
+ answerTable: "Tabla",
354
366
  groundedIn: "En qu\xE9 se basa esta respuesta",
355
367
  sourceCountOne: "1 fuente",
356
368
  sourceCountMany: "{count} fuentes",
@@ -433,6 +445,9 @@ var AR = {
433
445
  conceptAskQuestion: "\u0643\u064A\u0641 \u064A\u0639\u0645\u0644 {title} \u0641\u064A {product}\u061F",
434
446
  copyAnswer: "\u0646\u0633\u062E \u0627\u0644\u0625\u062C\u0627\u0628\u0629",
435
447
  copyFailed: "\u062A\u0639\u0630\u0651\u0631 \u0627\u0644\u0646\u0633\u062E",
448
+ readAloud: "\u0627\u0642\u0631\u0623 \u0628\u0635\u0648\u062A \u0639\u0627\u0644\u064D",
449
+ stopReading: "\u0625\u064A\u0642\u0627\u0641 \u0627\u0644\u0642\u0631\u0627\u0621\u0629",
450
+ answerTable: "\u062C\u062F\u0648\u0644",
436
451
  groundedIn: "\u0639\u0644\u0649 \u0645\u0627\u0630\u0627 \u062A\u0633\u062A\u0646\u062F \u0647\u0630\u0647 \u0627\u0644\u0625\u062C\u0627\u0628\u0629",
437
452
  sourceCountOne: "\u0645\u0635\u062F\u0631 \u0648\u0627\u062D\u062F",
438
453
  sourceCountMany: "{count} \u0645\u0635\u0627\u062F\u0631",
@@ -515,6 +530,9 @@ var TA = {
515
530
  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?",
516
531
  copyAnswer: "\u0BAA\u0BA4\u0BBF\u0BB2\u0BC8 \u0BA8\u0B95\u0BB2\u0BC6\u0B9F\u0BC1",
517
532
  copyFailed: "\u0BA8\u0B95\u0BB2\u0BC6\u0B9F\u0BC1\u0B95\u0BCD\u0B95 \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8",
533
+ readAloud: "\u0B9A\u0BA4\u0BCD\u0BA4\u0BAE\u0BBE\u0B95\u0BAA\u0BCD \u0BAA\u0B9F\u0BBF",
534
+ stopReading: "\u0BAA\u0B9F\u0BBF\u0BAA\u0BCD\u0BAA\u0BA4\u0BC8 \u0BA8\u0BBF\u0BB1\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1",
535
+ answerTable: "\u0B85\u0B9F\u0BCD\u0B9F\u0BB5\u0BA3\u0BC8",
518
536
  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",
519
537
  sourceCountOne: "1 \u0BAE\u0BC2\u0BB2\u0BAE\u0BCD",
520
538
  sourceCountMany: "{count} \u0BAE\u0BC2\u0BB2\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
@@ -546,6 +564,17 @@ var STRINGS = {
546
564
  ar: AR,
547
565
  ta: TA
548
566
  };
567
+ var SPEECH_TAGS = {
568
+ en: "en-US",
569
+ zh: "zh-CN",
570
+ hi: "hi-IN",
571
+ es: "es-ES",
572
+ ar: "ar-SA",
573
+ ta: "ta-IN"
574
+ };
575
+ function speechLocaleOf(locale) {
576
+ return SPEECH_TAGS[locale];
577
+ }
549
578
  function resolveExploreLocale(explicit) {
550
579
  const candidates = [
551
580
  explicit,
@@ -2315,6 +2344,224 @@ function makeOptimisticId() {
2315
2344
  optimisticCounter += 1;
2316
2345
  return `boff-explore-local-${Date.now()}-${optimisticCounter}`;
2317
2346
  }
2347
+ var DRAIN_TARGET_MS = 1200;
2348
+ var MIN_CHARS_PER_SECOND = 90;
2349
+ var MAX_CHARS_PER_SECOND = 2200;
2350
+ var MAX_STEP_MS = 250;
2351
+ var COMMIT_INTERVAL_MS = 45;
2352
+ var FALLBACK_STEP_MS = 50;
2353
+ var FENCE = /^ {0,3}(?:```|~~~)/;
2354
+ var TABLE_ROW = /^ {0,3}\|/;
2355
+ var EMPTY_BLOCK_MARKER = /^ {0,3}(?:[-+>]|#{1,6}|\d{1,9}[.)])\s*$/;
2356
+ function isWordCharacter(char) {
2357
+ return char !== void 0 && /[\p{L}\p{N}]/u.test(char);
2358
+ }
2359
+ function clamp(value, min, max) {
2360
+ return Math.min(Math.max(value, min), max);
2361
+ }
2362
+ function now() {
2363
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
2364
+ }
2365
+ function isHighSurrogate(code) {
2366
+ return code >= 55296 && code <= 56319;
2367
+ }
2368
+ function safeInlineEnd(line) {
2369
+ let codeSpan = null;
2370
+ let link = null;
2371
+ const emphasis = [];
2372
+ let at = 0;
2373
+ while (at < line.length) {
2374
+ const char = line[at] ?? "";
2375
+ if (char === "\\") {
2376
+ at += 2;
2377
+ continue;
2378
+ }
2379
+ if (char === "`") {
2380
+ let run = 1;
2381
+ while (line[at + run] === "`") run += 1;
2382
+ codeSpan = codeSpan === null ? at : null;
2383
+ at += run;
2384
+ continue;
2385
+ }
2386
+ if (codeSpan !== null) {
2387
+ at += 1;
2388
+ continue;
2389
+ }
2390
+ if (char === "[") {
2391
+ if (link === null) link = line[at - 1] === "!" ? at - 1 : at;
2392
+ at += 1;
2393
+ continue;
2394
+ }
2395
+ if (char === "]") {
2396
+ if (link !== null && line[at + 1] !== "(") link = null;
2397
+ at += 1;
2398
+ continue;
2399
+ }
2400
+ if (char === ")") {
2401
+ link = null;
2402
+ at += 1;
2403
+ continue;
2404
+ }
2405
+ if (char === "*" || char === "_" || char === "~") {
2406
+ let run = 1;
2407
+ while (line[at + run] === char) run += 1;
2408
+ const start = at;
2409
+ at += run;
2410
+ const before = line[start - 1];
2411
+ const after = line[start + run];
2412
+ if (run < 2) continue;
2413
+ if (char === "_" && isWordCharacter(before) && isWordCharacter(after)) {
2414
+ continue;
2415
+ }
2416
+ const marker = char.repeat(run);
2417
+ const open2 = emphasis.findIndex((entry) => entry.marker === marker);
2418
+ if (open2 !== -1) {
2419
+ emphasis.splice(open2, 1);
2420
+ continue;
2421
+ }
2422
+ if (after !== void 0 && /\s/.test(after)) continue;
2423
+ emphasis.push({ marker, start });
2424
+ continue;
2425
+ }
2426
+ at += 1;
2427
+ }
2428
+ const open = [codeSpan, link, emphasis[0]?.start ?? null].filter(
2429
+ (value) => value !== null
2430
+ );
2431
+ return open.length === 0 ? line.length : Math.min(...open);
2432
+ }
2433
+ function revealBoundary(text, limit) {
2434
+ if (limit <= 0) return 0;
2435
+ const caughtUp = limit >= text.length;
2436
+ const head = caughtUp ? text : text.slice(0, limit);
2437
+ const lineStart = head.lastIndexOf("\n") + 1;
2438
+ const line = head.slice(lineStart);
2439
+ let fenceStart = null;
2440
+ let tableStart = null;
2441
+ let tableRows = 0;
2442
+ let at = 0;
2443
+ while (at < lineStart) {
2444
+ const breakAt = head.indexOf("\n", at);
2445
+ const end = breakAt === -1 ? lineStart : breakAt;
2446
+ const row = head.slice(at, end);
2447
+ if (fenceStart !== null) {
2448
+ if (FENCE.test(row)) fenceStart = null;
2449
+ } else if (FENCE.test(row)) {
2450
+ fenceStart = at;
2451
+ } else if (TABLE_ROW.test(row)) {
2452
+ if (tableStart === null) {
2453
+ tableStart = at;
2454
+ tableRows = 0;
2455
+ }
2456
+ tableRows += 1;
2457
+ } else if (tableStart !== null) {
2458
+ tableStart = null;
2459
+ tableRows = 0;
2460
+ }
2461
+ at = end + 1;
2462
+ }
2463
+ if (fenceStart === null) {
2464
+ const lastIsRow = TABLE_ROW.test(line);
2465
+ if (lastIsRow && tableStart === null) {
2466
+ tableStart = lineStart;
2467
+ tableRows = 0;
2468
+ } else if (!lastIsRow && line !== "") {
2469
+ tableStart = null;
2470
+ tableRows = 0;
2471
+ }
2472
+ if (tableStart !== null && tableRows < 2) return tableStart;
2473
+ }
2474
+ if (caughtUp) return text.length;
2475
+ if (fenceStart !== null) return fenceStart;
2476
+ if (TABLE_ROW.test(line)) return lineStart;
2477
+ if (EMPTY_BLOCK_MARKER.test(line)) return lineStart;
2478
+ const cut = lineStart + safeInlineEnd(line);
2479
+ return isHighSurrogate(text.charCodeAt(cut - 1)) ? cut - 1 : cut;
2480
+ }
2481
+ function prefersReducedMotion() {
2482
+ if (typeof window === "undefined") return true;
2483
+ try {
2484
+ return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
2485
+ } catch {
2486
+ return false;
2487
+ }
2488
+ }
2489
+ function useProgressiveReveal(text, settled) {
2490
+ const [, setFrame] = React.useState(0);
2491
+ const [reduced] = React.useState(prefersReducedMotion);
2492
+ const immediate = settled || reduced;
2493
+ const textRef = React.useRef(text);
2494
+ const cursorRef = React.useRef(immediate ? text.length : 0);
2495
+ const cutRef = React.useRef(immediate ? text.length : 0);
2496
+ const committedRef = React.useRef(0);
2497
+ if (text !== textRef.current) {
2498
+ const revealed = textRef.current.slice(0, Math.floor(cursorRef.current));
2499
+ if (revealed !== "" && !text.startsWith(revealed)) {
2500
+ cursorRef.current = text.length;
2501
+ }
2502
+ cursorRef.current = Math.min(cursorRef.current, text.length);
2503
+ cutRef.current = Math.min(cutRef.current, text.length);
2504
+ textRef.current = text;
2505
+ }
2506
+ if (immediate) cursorRef.current = text.length;
2507
+ const cut = immediate ? text.length : Math.max(
2508
+ cutRef.current,
2509
+ revealBoundary(text, Math.floor(cursorRef.current))
2510
+ );
2511
+ cutRef.current = cut;
2512
+ React.useEffect(() => {
2513
+ if (immediate) return;
2514
+ if (cursorRef.current >= text.length) return;
2515
+ let cancelled = false;
2516
+ let cancelStep = null;
2517
+ let previous = now();
2518
+ const commit = (at, force) => {
2519
+ if (!force && at - committedRef.current < COMMIT_INTERVAL_MS) return;
2520
+ committedRef.current = at;
2521
+ setFrame((value) => value + 1);
2522
+ };
2523
+ const step = () => {
2524
+ cancelStep = null;
2525
+ if (cancelled) return;
2526
+ const at = now();
2527
+ const elapsed = clamp(at - previous, 0, MAX_STEP_MS);
2528
+ previous = at;
2529
+ const length = textRef.current.length;
2530
+ const pending = length - cursorRef.current;
2531
+ if (pending <= 0) {
2532
+ commit(at, true);
2533
+ return;
2534
+ }
2535
+ const perSecond = clamp(
2536
+ pending / (DRAIN_TARGET_MS / 1e3),
2537
+ MIN_CHARS_PER_SECOND,
2538
+ MAX_CHARS_PER_SECOND
2539
+ );
2540
+ cursorRef.current = Math.min(
2541
+ length,
2542
+ cursorRef.current + perSecond * elapsed / 1e3
2543
+ );
2544
+ const done = cursorRef.current >= length;
2545
+ commit(at, done);
2546
+ if (!done) schedule();
2547
+ };
2548
+ const schedule = () => {
2549
+ if (typeof window.requestAnimationFrame === "function") {
2550
+ const id2 = window.requestAnimationFrame(step);
2551
+ cancelStep = () => window.cancelAnimationFrame(id2);
2552
+ return;
2553
+ }
2554
+ const id = setTimeout(step, FALLBACK_STEP_MS);
2555
+ cancelStep = () => clearTimeout(id);
2556
+ };
2557
+ schedule();
2558
+ return () => {
2559
+ cancelled = true;
2560
+ cancelStep?.();
2561
+ };
2562
+ }, [text, immediate]);
2563
+ return text.slice(0, cut);
2564
+ }
2318
2565
  function getRecognitionCtor() {
2319
2566
  if (typeof window === "undefined") return null;
2320
2567
  const w = window;
@@ -2533,6 +2780,141 @@ function useSpeechInput({
2533
2780
  );
2534
2781
  return { supported, listening, interim, error, start, stop, toggle };
2535
2782
  }
2783
+ var SILENT_FAILURE_MS = 1500;
2784
+ function speechPlatform() {
2785
+ if (typeof window === "undefined") return null;
2786
+ const scope = window;
2787
+ const synth = scope.speechSynthesis;
2788
+ const Utterance = scope.SpeechSynthesisUtterance;
2789
+ if (!synth || typeof synth.speak !== "function") return null;
2790
+ if (typeof Utterance !== "function") return null;
2791
+ return { synth, Utterance };
2792
+ }
2793
+ function normaliseTag(tag) {
2794
+ return tag.trim().toLowerCase().replace(/_/g, "-");
2795
+ }
2796
+ function primaryOf(tag) {
2797
+ return normaliseTag(tag).split("-")[0] ?? "";
2798
+ }
2799
+ function pickVoice(voices, lang) {
2800
+ const wanted = normaliseTag(lang);
2801
+ if (wanted === "") return null;
2802
+ const primary = primaryOf(wanted);
2803
+ const exact = voices.filter((voice) => normaliseTag(voice.lang) === wanted);
2804
+ const related = voices.filter((voice) => primaryOf(voice.lang) === primary);
2805
+ return exact.find((voice) => voice.default) ?? exact[0] ?? related.find((voice) => voice.default) ?? related[0] ?? null;
2806
+ }
2807
+ var activeOwner = null;
2808
+ var activeStop = null;
2809
+ function stopSpeaking() {
2810
+ activeStop?.();
2811
+ }
2812
+ function useSpeechOutput({
2813
+ lang
2814
+ } = {}) {
2815
+ const [available, setAvailable] = React.useState(() => speechPlatform() !== null);
2816
+ const [speaking, setSpeaking] = React.useState(false);
2817
+ const utteranceRef = React.useRef(null);
2818
+ const watchdogRef = React.useRef(null);
2819
+ const ownerRef = React.useRef({});
2820
+ const stopRef = React.useRef(() => void 0);
2821
+ const langRef = React.useRef(lang);
2822
+ langRef.current = lang;
2823
+ const clearWatchdog = React.useCallback(() => {
2824
+ if (watchdogRef.current === null) return;
2825
+ clearTimeout(watchdogRef.current);
2826
+ watchdogRef.current = null;
2827
+ }, []);
2828
+ const settle = React.useCallback(() => {
2829
+ clearWatchdog();
2830
+ utteranceRef.current = null;
2831
+ if (activeOwner === ownerRef.current) {
2832
+ activeOwner = null;
2833
+ activeStop = null;
2834
+ }
2835
+ setSpeaking(false);
2836
+ }, [clearWatchdog]);
2837
+ const stop = React.useCallback(() => {
2838
+ const owned = utteranceRef.current !== null;
2839
+ settle();
2840
+ if (owned) speechPlatform()?.synth.cancel();
2841
+ }, [settle]);
2842
+ stopRef.current = stop;
2843
+ const speak = React.useCallback(
2844
+ (text) => {
2845
+ const platform = speechPlatform();
2846
+ if (!platform) {
2847
+ setAvailable(false);
2848
+ return;
2849
+ }
2850
+ const spoken = text.trim();
2851
+ if (spoken === "") return;
2852
+ if (activeStop && activeOwner !== ownerRef.current) activeStop();
2853
+ platform.synth.cancel();
2854
+ const utterance = new platform.Utterance(spoken);
2855
+ const wanted = langRef.current;
2856
+ if (wanted !== void 0 && wanted !== "") {
2857
+ const voice = pickVoice(platform.synth.getVoices(), wanted);
2858
+ utterance.lang = voice?.lang ?? wanted;
2859
+ if (voice) utterance.voice = voice;
2860
+ }
2861
+ const mine = () => utteranceRef.current === utterance;
2862
+ utterance.onstart = () => {
2863
+ if (!mine()) return;
2864
+ clearWatchdog();
2865
+ setSpeaking(true);
2866
+ };
2867
+ utterance.onend = () => {
2868
+ if (!mine()) return;
2869
+ settle();
2870
+ };
2871
+ utterance.onerror = (event) => {
2872
+ if (!mine()) return;
2873
+ const code = typeof event?.error === "string" ? event.error : "";
2874
+ settle();
2875
+ if (code !== "interrupted" && code !== "canceled") setAvailable(false);
2876
+ };
2877
+ utteranceRef.current = utterance;
2878
+ activeOwner = ownerRef.current;
2879
+ activeStop = () => {
2880
+ stopRef.current();
2881
+ };
2882
+ setSpeaking(true);
2883
+ platform.synth.speak(utterance);
2884
+ clearWatchdog();
2885
+ watchdogRef.current = setTimeout(() => {
2886
+ watchdogRef.current = null;
2887
+ if (!mine()) return;
2888
+ if (platform.synth.speaking) return;
2889
+ settle();
2890
+ setAvailable(false);
2891
+ }, SILENT_FAILURE_MS);
2892
+ },
2893
+ [clearWatchdog, settle]
2894
+ );
2895
+ const toggle = React.useCallback(
2896
+ (text) => {
2897
+ if (speaking) stop();
2898
+ else speak(text);
2899
+ },
2900
+ [speak, speaking, stop]
2901
+ );
2902
+ React.useEffect(() => {
2903
+ if (!speaking || typeof window === "undefined") return;
2904
+ const onHide = () => {
2905
+ stopRef.current();
2906
+ };
2907
+ window.addEventListener("pagehide", onHide);
2908
+ return () => window.removeEventListener("pagehide", onHide);
2909
+ }, [speaking]);
2910
+ React.useEffect(
2911
+ () => () => {
2912
+ stopRef.current();
2913
+ },
2914
+ []
2915
+ );
2916
+ return { supported: available, speaking, speak, stop, toggle };
2917
+ }
2536
2918
  var SWIPE_THRESHOLD = 40;
2537
2919
  function useSwipe({
2538
2920
  onSwipeLeft,
@@ -2928,6 +3310,115 @@ var EXPLORE_CSS = `
2928
3310
  height: auto;
2929
3311
  }
2930
3312
 
3313
+ /* ---- Comparison and pricing tables ----------------------------------------------------
3314
+ The harness emits a comparison, and a set of plans, as a GFM table whose FIRST column is
3315
+ the subject \u2014 the product, or the plan (CONTRACT.md \xA77). Two things have to be true of it:
3316
+ the page must not scroll sideways because of it, and it has to be readable on a 320px
3317
+ phone, which a four-column grid of 1px borders is not.
3318
+
3319
+ Wide: a contained scroller. \`max-width: 100%\` keeps the box inside the bubble and
3320
+ \`overflow-x: auto\` keeps the columns inside the box, so the widest possible table still
3321
+ cannot widen the document. \`overscroll-behavior-x: contain\` stops a swipe that runs out
3322
+ of table from chaining to the page \u2014 the same rule the transcript needed. */
3323
+ .boff-explore-body .boff-explore-table {
3324
+ max-width: 100%;
3325
+ overflow-x: auto;
3326
+ overscroll-behavior-x: contain;
3327
+ margin: 0.75rem 0;
3328
+ border: 1px solid hsl(var(--border));
3329
+ border-radius: 0.75rem;
3330
+ background-color: hsl(var(--card));
3331
+ }
3332
+ .boff-explore-body .boff-explore-table:focus-visible {
3333
+ outline: 2px solid hsl(var(--ring));
3334
+ outline-offset: 2px;
3335
+ }
3336
+ .boff-explore-body .boff-explore-table table {
3337
+ /* Beats the blanket \`display: block\` above, which is the unwrapped fallback. */
3338
+ display: table;
3339
+ width: 100%;
3340
+ max-width: none;
3341
+ margin: 0;
3342
+ border-collapse: collapse;
3343
+ font-size: 0.8125rem;
3344
+ }
3345
+ .boff-explore-body .boff-explore-table th,
3346
+ .boff-explore-body .boff-explore-table td {
3347
+ border: 0;
3348
+ border-top: 1px solid hsl(var(--border));
3349
+ padding: 0.5rem 0.75rem;
3350
+ /* \`start\`, never \`left\`: the page root carries dir="rtl" in Arabic, and a left-aligned
3351
+ table there reads against the grain of every other line on the page. */
3352
+ text-align: start;
3353
+ vertical-align: top;
3354
+ overflow-wrap: anywhere;
3355
+ /* Prices in a column line up under each other instead of drifting by digit width. */
3356
+ font-variant-numeric: tabular-nums;
3357
+ min-width: 6rem;
3358
+ }
3359
+ .boff-explore-body .boff-explore-table thead th {
3360
+ border-top: 0;
3361
+ background-color: hsl(var(--muted));
3362
+ color: hsl(var(--foreground));
3363
+ font-weight: 600;
3364
+ white-space: nowrap;
3365
+ }
3366
+ /* The first column is what the row is ABOUT \u2014 the product, or the plan name. Reading it as
3367
+ the row's heading is what makes a price list scannable rather than a grid of strings. */
3368
+ .boff-explore-body .boff-explore-table tbody td:first-child {
3369
+ font-weight: 600;
3370
+ color: hsl(var(--card-foreground));
3371
+ }
3372
+
3373
+ /* Narrow: one card per row. Each cell prints its own column heading, so a value is never
3374
+ stranded without the question it answers, and the subject cell becomes the card's title.
3375
+ A media query rather than a measured width \u2014 these pages are serialised post-hydration,
3376
+ so anything measured here would ship to every phone as static HTML. */
3377
+ @media (max-width: 30rem) {
3378
+ .boff-explore-body .boff-explore-table {
3379
+ overflow-x: visible;
3380
+ }
3381
+ .boff-explore-body .boff-explore-table table,
3382
+ .boff-explore-body .boff-explore-table tbody,
3383
+ .boff-explore-body .boff-explore-table tr,
3384
+ .boff-explore-body .boff-explore-table td {
3385
+ display: block;
3386
+ width: 100%;
3387
+ min-width: 0;
3388
+ }
3389
+ .boff-explore-body .boff-explore-table thead {
3390
+ display: none;
3391
+ }
3392
+ .boff-explore-body .boff-explore-table tr {
3393
+ border-top: 1px solid hsl(var(--border));
3394
+ padding: 0.5rem 0;
3395
+ }
3396
+ .boff-explore-body .boff-explore-table tr:first-child {
3397
+ border-top: 0;
3398
+ }
3399
+ .boff-explore-body .boff-explore-table td {
3400
+ border: 0;
3401
+ padding: 0.125rem 0.75rem;
3402
+ }
3403
+ .boff-explore-body .boff-explore-table td[data-label]::before {
3404
+ content: attr(data-label);
3405
+ display: block;
3406
+ font-size: 0.6875rem;
3407
+ font-weight: 600;
3408
+ text-transform: uppercase;
3409
+ letter-spacing: 0.04em;
3410
+ color: hsl(var(--muted-foreground));
3411
+ }
3412
+ .boff-explore-body .boff-explore-table tbody td:first-child {
3413
+ font-size: 0.9375rem;
3414
+ padding-bottom: 0.25rem;
3415
+ }
3416
+ .boff-explore-body .boff-explore-table tbody td:first-child::before {
3417
+ /* The plan's own name is the heading here; repeating "Plan" over it says nothing. */
3418
+ display: none;
3419
+ }
3420
+ }
3421
+
2931
3422
  /* ---- Ambient backdrop -----------------------------------------------------------------
2932
3423
  Two very low-opacity radial washes behind the welcome state, drifting slowly. Painted on
2933
3424
  a pseudo-element with pointer-events:none so it can never intercept a click \u2014 a
@@ -3099,7 +3590,7 @@ function isOwnElement(el, root) {
3099
3590
  function visualViewportHeight() {
3100
3591
  return window.visualViewport?.height ?? window.innerHeight;
3101
3592
  }
3102
- function clamp(value, min, max) {
3593
+ function clamp2(value, min, max) {
3103
3594
  return Math.min(Math.max(value, min), max);
3104
3595
  }
3105
3596
  function isRestoredNavigation() {
@@ -3249,11 +3740,80 @@ function summariseReferences(references, origins, productNames) {
3249
3740
  };
3250
3741
  }
3251
3742
  var CODE_FENCE = /^\s*(?:```|~~~)/;
3252
- function answerPlainText(markdown) {
3743
+ var TABLE_ROW_LINE = /^\s{0,3}\|/;
3744
+ var TABLE_DELIMITER_ROW = /^\s{0,3}\|[\s:|-]*-[\s:|-]*$/;
3745
+ function readableLine(raw, mode) {
3746
+ let line = raw;
3747
+ line = line.replace(/!\[[^\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g, "");
3748
+ line = mode === "speech" ? (
3749
+ // A voice reading "h-t-t-p-s colon slash slash…" mid-sentence is noise, and the
3750
+ // prompt asks for two to five links in every answer. The label IS the words.
3751
+ line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1")
3752
+ ) : line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1 ($2)");
3753
+ line = mode === "speech" ? line.replace(/<(https?:\/\/[^>\s]+)>/g, "") : line.replace(/<(https?:\/\/[^>\s]+)>/g, "$1");
3754
+ line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
3755
+ line = line.replace(/^\s{0,3}>\s?/, "");
3756
+ if (/^\s{0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/.test(line)) return "";
3757
+ line = line.replace(/`([^`]+)`/g, "$1");
3758
+ line = line.replace(/\*\*([^*]+)\*\*/g, "$1");
3759
+ line = line.replace(/__([^_]+)__/g, "$1");
3760
+ line = line.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, "$1$2");
3761
+ line = line.replace(/(^|[^_\w])_([^_\n]+)_(?![_\w])/g, "$1$2");
3762
+ return line.replace(/[ \t]+$/, "");
3763
+ }
3764
+ function tableCells(row) {
3765
+ const trimmed = row.trim();
3766
+ const cells = [];
3767
+ let cell = "";
3768
+ for (let at = 0; at < trimmed.length; at += 1) {
3769
+ const char = trimmed[at] ?? "";
3770
+ if (char === "\\") {
3771
+ cell += char + (trimmed[at + 1] ?? "");
3772
+ at += 1;
3773
+ continue;
3774
+ }
3775
+ if (char === "|") {
3776
+ cells.push(cell.trim());
3777
+ cell = "";
3778
+ continue;
3779
+ }
3780
+ cell += char;
3781
+ }
3782
+ cells.push(cell.trim());
3783
+ if (trimmed.startsWith("|") && cells[0] === "") cells.shift();
3784
+ if (trimmed.endsWith("|") && cells[cells.length - 1] === "") cells.pop();
3785
+ return cells;
3786
+ }
3787
+ function asSentence(text) {
3788
+ return /[.!?]$/.test(text) ? text : `${text}.`;
3789
+ }
3790
+ function spokenTable(rows) {
3791
+ if (rows.length < 2 || !TABLE_DELIMITER_ROW.test(rows[1] ?? "")) return rows;
3792
+ const labels = tableCells(rows[0] ?? "");
3793
+ const spoken = [];
3794
+ for (const row of rows.slice(2)) {
3795
+ const parts = [];
3796
+ tableCells(row).forEach((value, column) => {
3797
+ if (value === "") return;
3798
+ const label = column === 0 ? "" : labels[column] ?? "";
3799
+ parts.push(label === "" ? value : `${label}: ${value}`);
3800
+ });
3801
+ if (parts.length > 0) spoken.push(asSentence(parts.join(". ")));
3802
+ }
3803
+ return spoken;
3804
+ }
3805
+ function readableText(markdown, mode) {
3253
3806
  const lines = [];
3254
3807
  let inFence = false;
3808
+ let table = [];
3809
+ const flushTable = () => {
3810
+ if (table.length === 0) return;
3811
+ for (const line of spokenTable(table)) lines.push(readableLine(line, mode));
3812
+ table = [];
3813
+ };
3255
3814
  for (const raw of markdown.split("\n")) {
3256
3815
  if (CODE_FENCE.test(raw)) {
3816
+ flushTable();
3257
3817
  inFence = !inFence;
3258
3818
  continue;
3259
3819
  }
@@ -3261,33 +3821,30 @@ function answerPlainText(markdown) {
3261
3821
  lines.push(raw);
3262
3822
  continue;
3263
3823
  }
3264
- let line = raw;
3265
- line = line.replace(/!\[[^\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g, "");
3266
- line = line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1 ($2)");
3267
- line = line.replace(/<(https?:\/\/[^>\s]+)>/g, "$1");
3268
- line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
3269
- line = line.replace(/^\s{0,3}>\s?/, "");
3270
- if (/^\s{0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/.test(line)) {
3271
- lines.push("");
3824
+ if (mode === "speech" && TABLE_ROW_LINE.test(raw)) {
3825
+ table.push(raw);
3272
3826
  continue;
3273
3827
  }
3274
- line = line.replace(/`([^`]+)`/g, "$1");
3275
- line = line.replace(/\*\*([^*]+)\*\*/g, "$1");
3276
- line = line.replace(/__([^_]+)__/g, "$1");
3277
- line = line.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, "$1$2");
3278
- line = line.replace(/(^|[^_\w])_([^_\n]+)_(?![_\w])/g, "$1$2");
3279
- lines.push(line.replace(/[ \t]+$/, ""));
3828
+ flushTable();
3829
+ lines.push(readableLine(raw, mode));
3280
3830
  }
3831
+ flushTable();
3281
3832
  return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
3282
3833
  }
3283
- var InlineImageScopeContext = React.createContext(null);
3834
+ function answerPlainText(markdown) {
3835
+ return readableText(markdown, "clipboard");
3836
+ }
3837
+ function answerSpeechText(markdown) {
3838
+ return readableText(markdown, "speech");
3839
+ }
3840
+ var AnswerScopeContext = React.createContext(null);
3284
3841
  var InsideLinkContext = React.createContext(false);
3285
3842
  function AnswerLink(props) {
3286
3843
  return /* @__PURE__ */ jsxRuntime.jsx(InsideLinkContext.Provider, { value: true, children: /* @__PURE__ */ jsxRuntime.jsx(MarkdownLink, { ...props }) });
3287
3844
  }
3288
3845
  var INLINE_IMAGE_FRAME = "relative inline-block max-w-full overflow-hidden rounded-xl border border-border bg-muted align-top shadow-sm";
3289
3846
  function InlineConceptImage({ src, alt }) {
3290
- const scope = React.useContext(InlineImageScopeContext);
3847
+ const scope = React.useContext(AnswerScopeContext);
3291
3848
  const insideLink = React.useContext(InsideLinkContext);
3292
3849
  if (!scope || !scope.enabled || !isInlineImageAllowed(src, scope.origins) || scope.failed.has(src)) {
3293
3850
  return null;
@@ -3335,9 +3892,70 @@ function InlineConceptImage({ src, alt }) {
3335
3892
  }
3336
3893
  ) });
3337
3894
  }
3895
+ function markdownText(node) {
3896
+ if (!node) return "";
3897
+ if (typeof node.value === "string") return node.value;
3898
+ return (node.children ?? []).map(markdownText).join("");
3899
+ }
3900
+ function hasHeaderCells(node) {
3901
+ return (node?.children ?? []).some((cell) => cell.tagName === "th");
3902
+ }
3903
+ function findHeaderRow(node) {
3904
+ for (const child of node?.children ?? []) {
3905
+ if (child.tagName === "tr" && hasHeaderCells(child)) return child;
3906
+ const nested = findHeaderRow(child);
3907
+ if (nested !== null) return nested;
3908
+ }
3909
+ return null;
3910
+ }
3911
+ function tableColumnLabels(node) {
3912
+ const row = findHeaderRow(node);
3913
+ if (row === null) return [];
3914
+ return (row.children ?? []).filter((cell) => cell.tagName === "th").map((cell) => markdownText(cell).trim());
3915
+ }
3916
+ var TableColumnsContext = React.createContext([]);
3917
+ function AnswerTable({ node, children }) {
3918
+ const scope = React.useContext(AnswerScopeContext);
3919
+ const table = /* @__PURE__ */ jsxRuntime.jsx(TableColumnsContext.Provider, { value: tableColumnLabels(node), children: /* @__PURE__ */ jsxRuntime.jsx("table", { children }) });
3920
+ if (!scope) return table;
3921
+ return /* @__PURE__ */ jsxRuntime.jsx(
3922
+ "div",
3923
+ {
3924
+ "data-boff-explore": "table",
3925
+ role: "region",
3926
+ "aria-label": scope.strings.answerTable,
3927
+ tabIndex: 0,
3928
+ className: "boff-explore-table",
3929
+ children: table
3930
+ }
3931
+ );
3932
+ }
3933
+ function AnswerTableRow({
3934
+ node,
3935
+ children,
3936
+ ...props
3937
+ }) {
3938
+ const labels = React.useContext(TableColumnsContext);
3939
+ if (labels.length === 0 || hasHeaderCells(node)) {
3940
+ return /* @__PURE__ */ jsxRuntime.jsx("tr", { ...props, children });
3941
+ }
3942
+ let column = 0;
3943
+ const cells = React.Children.map(children, (cell) => {
3944
+ if (!React.isValidElement(cell)) return cell;
3945
+ const label = labels[column] ?? "";
3946
+ column += 1;
3947
+ if (label === "") return cell;
3948
+ return React.cloneElement(cell, {
3949
+ "data-label": label
3950
+ });
3951
+ });
3952
+ return /* @__PURE__ */ jsxRuntime.jsx("tr", { ...props, children: cells });
3953
+ }
3338
3954
  var ANSWER_MARKDOWN_COMPONENTS = {
3339
3955
  a: AnswerLink,
3340
- img: InlineConceptImage
3956
+ img: InlineConceptImage,
3957
+ table: AnswerTable,
3958
+ tr: AnswerTableRow
3341
3959
  };
3342
3960
  function ReferenceCard({
3343
3961
  reference,
@@ -3726,9 +4344,42 @@ function ProvenanceChips({
3726
4344
  );
3727
4345
  }
3728
4346
  var COPY_FEEDBACK_MS = 2500;
4347
+ 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";
4348
+ var ANSWER_ACTION_IDLE = "border-border bg-card text-muted-foreground hover:border-primary/40 hover:bg-accent hover:text-foreground";
4349
+ var ANSWER_ACTION_ACTIVE = "border-primary/40 bg-accent text-primary";
4350
+ function ReadAloudButton({
4351
+ strings: T,
4352
+ text,
4353
+ lang
4354
+ }) {
4355
+ const speech = useSpeechOutput({ lang });
4356
+ if (!speech.supported) return null;
4357
+ const label = speech.speaking ? T.stopReading : T.readAloud;
4358
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4359
+ "button",
4360
+ {
4361
+ "data-boff-explore": "read-aloud",
4362
+ "data-speaking": speech.speaking ? "true" : "false",
4363
+ type: "button",
4364
+ title: label,
4365
+ "aria-pressed": speech.speaking,
4366
+ onClick: () => speech.toggle(text),
4367
+ className: cn(
4368
+ ANSWER_ACTION_PILL,
4369
+ speech.speaking ? ANSWER_ACTION_ACTIVE : ANSWER_ACTION_IDLE
4370
+ ),
4371
+ children: [
4372
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Volume2, { className: "h-3 w-3 shrink-0", "aria-hidden": "true" }),
4373
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: label })
4374
+ ]
4375
+ }
4376
+ );
4377
+ }
3729
4378
  function AnswerActions({
3730
4379
  strings: T,
3731
- text
4380
+ text,
4381
+ speechText,
4382
+ speechLang
3732
4383
  }) {
3733
4384
  const [outcome, setOutcome] = React.useState("idle");
3734
4385
  const resetRef = React.useRef(null);
@@ -3771,9 +4422,8 @@ function AnswerActions({
3771
4422
  void copy();
3772
4423
  },
3773
4424
  className: cn(
3774
- "inline-flex max-w-full items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs transition-colors",
3775
- "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
3776
- 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"
4425
+ ANSWER_ACTION_PILL,
4426
+ outcome === "failed" ? "border-destructive/40 bg-card text-destructive" : ANSWER_ACTION_IDLE
3777
4427
  ),
3778
4428
  children: [
3779
4429
  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" }),
@@ -3781,6 +4431,7 @@ function AnswerActions({
3781
4431
  ]
3782
4432
  }
3783
4433
  ),
4434
+ /* @__PURE__ */ jsxRuntime.jsx(ReadAloudButton, { strings: T, text: speechText, lang: speechLang }),
3784
4435
  /* @__PURE__ */ jsxRuntime.jsx("span", { "data-boff-explore": "copy-status", role: "status", className: "sr-only", children: outcome === "idle" ? "" : label })
3785
4436
  ]
3786
4437
  }
@@ -3832,6 +4483,7 @@ function AssistantBubble({
3832
4483
  activity,
3833
4484
  imageOrigins,
3834
4485
  productNames,
4486
+ speechLang,
3835
4487
  onNavigate,
3836
4488
  anchorRef,
3837
4489
  onFollow,
@@ -3842,6 +4494,7 @@ function AssistantBubble({
3842
4494
  const body = message.content || message.partialContent || "";
3843
4495
  const failed = message.status === "FAILED";
3844
4496
  const completed = message.status === "COMPLETED";
4497
+ const visible = useProgressiveReveal(body, !streaming);
3845
4498
  const [failedImages, setFailedImages] = React.useState(
3846
4499
  () => /* @__PURE__ */ new Set()
3847
4500
  );
@@ -3850,7 +4503,7 @@ function AssistantBubble({
3850
4503
  (current) => current.has(src) ? current : new Set(current).add(src)
3851
4504
  );
3852
4505
  }, []);
3853
- const imageScope = React.useMemo(
4506
+ const answerScope = React.useMemo(
3854
4507
  () => ({
3855
4508
  strings: T,
3856
4509
  enabled: completed,
@@ -3868,6 +4521,10 @@ function AssistantBubble({
3868
4521
  () => answerPlainText(message.content),
3869
4522
  [message.content]
3870
4523
  );
4524
+ const speechText = React.useMemo(
4525
+ () => answerSpeechText(message.content),
4526
+ [message.content]
4527
+ );
3871
4528
  const grounded = completed && message.answered !== false;
3872
4529
  return /* @__PURE__ */ jsxRuntime.jsxs(
3873
4530
  "div",
@@ -3914,8 +4571,8 @@ function AssistantBubble({
3914
4571
  ]
3915
4572
  }
3916
4573
  ) : null,
3917
- body ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-2xl rounded-tl-md border border-border bg-card px-4 py-3 shadow-sm", children: [
3918
- /* @__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 }) }) }),
4574
+ visible ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-2xl rounded-tl-md border border-border bg-card px-4 py-3 shadow-sm", children: [
4575
+ /* @__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 }) }) }),
3919
4576
  streaming ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "boff-explore-caret", "aria-hidden": "true" }) : null
3920
4577
  ] }) : null,
3921
4578
  grounded && provenance.total > 0 ? /* @__PURE__ */ jsxRuntime.jsx(ProvenanceChips, { strings: T, provenance }) : null,
@@ -3958,7 +4615,15 @@ function AssistantBubble({
3958
4615
  question
3959
4616
  )) })
3960
4617
  ] }) : null,
3961
- completed && copyText !== "" ? /* @__PURE__ */ jsxRuntime.jsx(AnswerActions, { strings: T, text: copyText }) : null,
4618
+ completed && copyText !== "" ? /* @__PURE__ */ jsxRuntime.jsx(
4619
+ AnswerActions,
4620
+ {
4621
+ strings: T,
4622
+ text: copyText,
4623
+ speechText,
4624
+ speechLang
4625
+ }
4626
+ ) : null,
3962
4627
  completed && onFeedback ? /* @__PURE__ */ jsxRuntime.jsx(
3963
4628
  AnswerFeedback,
3964
4629
  {
@@ -4192,7 +4857,7 @@ function ConceptStrip({
4192
4857
  "div",
4193
4858
  {
4194
4859
  "data-boff-explore": "concept-strip-block",
4195
- className: "boff-explore-rise boff-explore-rise-3 mt-6",
4860
+ className: "boff-explore-rise boff-explore-rise-1 mt-6",
4196
4861
  children: [
4197
4862
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: T.conceptStripTitle }),
4198
4863
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4382,6 +5047,15 @@ function WelcomeState({
4382
5047
  /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "mt-4 text-balance break-words text-2xl font-bold tracking-tight sm:text-4xl", children: title }),
4383
5048
  /* @__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 })
4384
5049
  ] }),
5050
+ /* @__PURE__ */ jsxRuntime.jsx(
5051
+ ConceptStrip,
5052
+ {
5053
+ strings: T,
5054
+ highlights: conceptHighlights,
5055
+ onAsk: onPrompt,
5056
+ disabled
5057
+ }
5058
+ ),
4385
5059
  /* @__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(
4386
5060
  "li",
4387
5061
  {
@@ -4406,15 +5080,6 @@ function WelcomeState({
4406
5080
  }
4407
5081
  )
4408
5082
  ] }) : null,
4409
- /* @__PURE__ */ jsxRuntime.jsx(
4410
- ConceptStrip,
4411
- {
4412
- strings: T,
4413
- highlights: conceptHighlights,
4414
- onAsk: onPrompt,
4415
- disabled
4416
- }
4417
- ),
4418
5083
  products.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "boff-explore-rise boff-explore-rise-3 mt-6", children: [
4419
5084
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: T.focusProduct }),
4420
5085
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4452,6 +5117,7 @@ function ExplorePage({
4452
5117
  );
4453
5118
  const T = React.useMemo(() => exploreStrings(activeLocale), [activeLocale]);
4454
5119
  const rtl = isRtlLocale(activeLocale);
5120
+ const speechLang = speechLocaleOf(activeLocale);
4455
5121
  const {
4456
5122
  phase,
4457
5123
  catalog,
@@ -4556,11 +5222,11 @@ function ExplorePage({
4556
5222
  const layoutHeight = window.innerHeight;
4557
5223
  if (layoutHeight === 0) return;
4558
5224
  const top = Math.round(
4559
- clamp(el.getBoundingClientRect().top, 0, layoutHeight)
5225
+ clamp2(el.getBoundingClientRect().top, 0, layoutHeight)
4560
5226
  );
4561
5227
  const viewport = window.visualViewport;
4562
5228
  const inset = viewport ? Math.round(
4563
- clamp(
5229
+ clamp2(
4564
5230
  layoutHeight - viewport.height - viewport.offsetTop,
4565
5231
  0,
4566
5232
  layoutHeight
@@ -4841,6 +5507,7 @@ function ExplorePage({
4841
5507
  const text = draft.trim();
4842
5508
  if (!text || overLimit || busy || !canSend) return;
4843
5509
  stopDictation();
5510
+ stopSpeaking();
4844
5511
  setDraft("");
4845
5512
  stickToBottomRef.current = true;
4846
5513
  void send(text, toPayload(stagedRef.current));
@@ -4851,6 +5518,7 @@ function ExplorePage({
4851
5518
  const sendPrompt = React.useCallback(
4852
5519
  (prompt) => {
4853
5520
  if (!canSend || busy) return;
5521
+ stopSpeaking();
4854
5522
  setDraft("");
4855
5523
  stickToBottomRef.current = true;
4856
5524
  void send(prompt, toPayload(stagedRef.current));
@@ -5086,6 +5754,7 @@ function ExplorePage({
5086
5754
  activity,
5087
5755
  imageOrigins,
5088
5756
  productNames,
5757
+ speechLang,
5089
5758
  onNavigate,
5090
5759
  anchorRef: message.id === latestAssistantId ? latestAssistantRef : void 0,
5091
5760
  onFollow: (kind, url) => recordClick(message.id, kind, url),
@@ -5390,6 +6059,7 @@ function ExplorePage({
5390
6059
 
5391
6060
  exports.ExplorePage = ExplorePage;
5392
6061
  exports.answerPlainText = answerPlainText;
6062
+ exports.answerSpeechText = answerSpeechText;
5393
6063
  exports.inlineImageOrigins = inlineImageOrigins;
5394
6064
  exports.isInlineImageAllowed = isInlineImageAllowed;
5395
6065
  exports.logoUrlOf = logoUrlOf;