@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.
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
- import { createContext, useMemo, useState, useRef, useCallback, useEffect, useContext } from 'react';
3
- import { Sparkles, RotateCcw, Link2, History, Trash2, Paperclip, X, Mic, Square, ArrowUp, BookOpenText, Layers, TriangleAlert, ChevronLeft, ChevronRight, ExternalLink, Check, Copy, ThumbsUp, ThumbsDown } from 'lucide-react';
2
+ import { createContext, useMemo, useState, useRef, useCallback, useEffect, useContext, Children, isValidElement, cloneElement } from 'react';
3
+ import { Sparkles, RotateCcw, Link2, History, Trash2, Paperclip, X, Mic, Square, ArrowUp, BookOpenText, Layers, TriangleAlert, ChevronLeft, ChevronRight, ExternalLink, Check, Copy, ThumbsUp, ThumbsDown, Volume2 } from 'lucide-react';
4
4
  import ReactMarkdown from 'react-markdown';
5
5
  import rehypeSanitize from 'rehype-sanitize';
6
6
  import remarkGfm from 'remark-gfm';
@@ -83,6 +83,9 @@ var EN = {
83
83
  conceptAskQuestion: "How does {title} work in {product}?",
84
84
  copyAnswer: "Copy answer",
85
85
  copyFailed: "Couldn't copy",
86
+ readAloud: "Read aloud",
87
+ stopReading: "Stop reading",
88
+ answerTable: "Table",
86
89
  groundedIn: "What this answer is based on",
87
90
  sourceCountOne: "1 source",
88
91
  sourceCountMany: "{count} sources",
@@ -165,6 +168,9 @@ var ZH = {
165
168
  conceptAskQuestion: "{product} \u7684 {title} \u662F\u600E\u4E48\u8FD0\u4F5C\u7684\uFF1F",
166
169
  copyAnswer: "\u590D\u5236\u56DE\u7B54",
167
170
  copyFailed: "\u590D\u5236\u5931\u8D25",
171
+ readAloud: "\u6717\u8BFB",
172
+ stopReading: "\u505C\u6B62\u6717\u8BFB",
173
+ answerTable: "\u8868\u683C",
168
174
  groundedIn: "\u8FD9\u4E2A\u56DE\u7B54\u7684\u4F9D\u636E",
169
175
  sourceCountOne: "1 \u4E2A\u6765\u6E90",
170
176
  sourceCountMany: "{count} \u4E2A\u6765\u6E90",
@@ -243,6 +249,9 @@ var HI = {
243
249
  conceptAskQuestion: "{product} \u092E\u0947\u0902 {title} \u0915\u0948\u0938\u0947 \u0915\u093E\u092E \u0915\u0930\u0924\u093E \u0939\u0948?",
244
250
  copyAnswer: "\u0909\u0924\u094D\u0924\u0930 \u0915\u0949\u092A\u0940 \u0915\u0930\u0947\u0902",
245
251
  copyFailed: "\u0915\u0949\u092A\u0940 \u0928\u0939\u0940\u0902 \u0939\u094B \u0938\u0915\u093E",
252
+ readAloud: "\u091C\u093C\u094B\u0930 \u0938\u0947 \u092A\u0922\u093C\u0947\u0902",
253
+ stopReading: "\u092A\u0922\u093C\u0928\u093E \u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
254
+ answerTable: "\u0924\u093E\u0932\u093F\u0915\u093E",
246
255
  groundedIn: "\u092F\u0939 \u0909\u0924\u094D\u0924\u0930 \u0915\u093F\u0938 \u092A\u0930 \u0906\u0927\u093E\u0930\u093F\u0924 \u0939\u0948",
247
256
  sourceCountOne: "1 \u0938\u094D\u0930\u094B\u0924",
248
257
  sourceCountMany: "{count} \u0938\u094D\u0930\u094B\u0924",
@@ -325,6 +334,9 @@ var ES = {
325
334
  conceptAskQuestion: "\xBFC\xF3mo funciona {title} en {product}?",
326
335
  copyAnswer: "Copiar respuesta",
327
336
  copyFailed: "No se pudo copiar",
337
+ readAloud: "Leer en voz alta",
338
+ stopReading: "Dejar de leer",
339
+ answerTable: "Tabla",
328
340
  groundedIn: "En qu\xE9 se basa esta respuesta",
329
341
  sourceCountOne: "1 fuente",
330
342
  sourceCountMany: "{count} fuentes",
@@ -407,6 +419,9 @@ var AR = {
407
419
  conceptAskQuestion: "\u0643\u064A\u0641 \u064A\u0639\u0645\u0644 {title} \u0641\u064A {product}\u061F",
408
420
  copyAnswer: "\u0646\u0633\u062E \u0627\u0644\u0625\u062C\u0627\u0628\u0629",
409
421
  copyFailed: "\u062A\u0639\u0630\u0651\u0631 \u0627\u0644\u0646\u0633\u062E",
422
+ readAloud: "\u0627\u0642\u0631\u0623 \u0628\u0635\u0648\u062A \u0639\u0627\u0644\u064D",
423
+ stopReading: "\u0625\u064A\u0642\u0627\u0641 \u0627\u0644\u0642\u0631\u0627\u0621\u0629",
424
+ answerTable: "\u062C\u062F\u0648\u0644",
410
425
  groundedIn: "\u0639\u0644\u0649 \u0645\u0627\u0630\u0627 \u062A\u0633\u062A\u0646\u062F \u0647\u0630\u0647 \u0627\u0644\u0625\u062C\u0627\u0628\u0629",
411
426
  sourceCountOne: "\u0645\u0635\u062F\u0631 \u0648\u0627\u062D\u062F",
412
427
  sourceCountMany: "{count} \u0645\u0635\u0627\u062F\u0631",
@@ -489,6 +504,9 @@ var TA = {
489
504
  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?",
490
505
  copyAnswer: "\u0BAA\u0BA4\u0BBF\u0BB2\u0BC8 \u0BA8\u0B95\u0BB2\u0BC6\u0B9F\u0BC1",
491
506
  copyFailed: "\u0BA8\u0B95\u0BB2\u0BC6\u0B9F\u0BC1\u0B95\u0BCD\u0B95 \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8",
507
+ readAloud: "\u0B9A\u0BA4\u0BCD\u0BA4\u0BAE\u0BBE\u0B95\u0BAA\u0BCD \u0BAA\u0B9F\u0BBF",
508
+ stopReading: "\u0BAA\u0B9F\u0BBF\u0BAA\u0BCD\u0BAA\u0BA4\u0BC8 \u0BA8\u0BBF\u0BB1\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1",
509
+ answerTable: "\u0B85\u0B9F\u0BCD\u0B9F\u0BB5\u0BA3\u0BC8",
492
510
  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",
493
511
  sourceCountOne: "1 \u0BAE\u0BC2\u0BB2\u0BAE\u0BCD",
494
512
  sourceCountMany: "{count} \u0BAE\u0BC2\u0BB2\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
@@ -520,6 +538,17 @@ var STRINGS = {
520
538
  ar: AR,
521
539
  ta: TA
522
540
  };
541
+ var SPEECH_TAGS = {
542
+ en: "en-US",
543
+ zh: "zh-CN",
544
+ hi: "hi-IN",
545
+ es: "es-ES",
546
+ ar: "ar-SA",
547
+ ta: "ta-IN"
548
+ };
549
+ function speechLocaleOf(locale) {
550
+ return SPEECH_TAGS[locale];
551
+ }
523
552
  function resolveExploreLocale(explicit) {
524
553
  const candidates = [
525
554
  explicit,
@@ -2289,6 +2318,224 @@ function makeOptimisticId() {
2289
2318
  optimisticCounter += 1;
2290
2319
  return `boff-explore-local-${Date.now()}-${optimisticCounter}`;
2291
2320
  }
2321
+ var DRAIN_TARGET_MS = 1200;
2322
+ var MIN_CHARS_PER_SECOND = 90;
2323
+ var MAX_CHARS_PER_SECOND = 2200;
2324
+ var MAX_STEP_MS = 250;
2325
+ var COMMIT_INTERVAL_MS = 45;
2326
+ var FALLBACK_STEP_MS = 50;
2327
+ var FENCE = /^ {0,3}(?:```|~~~)/;
2328
+ var TABLE_ROW = /^ {0,3}\|/;
2329
+ var EMPTY_BLOCK_MARKER = /^ {0,3}(?:[-+>]|#{1,6}|\d{1,9}[.)])\s*$/;
2330
+ function isWordCharacter(char) {
2331
+ return char !== void 0 && /[\p{L}\p{N}]/u.test(char);
2332
+ }
2333
+ function clamp(value, min, max) {
2334
+ return Math.min(Math.max(value, min), max);
2335
+ }
2336
+ function now() {
2337
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
2338
+ }
2339
+ function isHighSurrogate(code) {
2340
+ return code >= 55296 && code <= 56319;
2341
+ }
2342
+ function safeInlineEnd(line) {
2343
+ let codeSpan = null;
2344
+ let link = null;
2345
+ const emphasis = [];
2346
+ let at = 0;
2347
+ while (at < line.length) {
2348
+ const char = line[at] ?? "";
2349
+ if (char === "\\") {
2350
+ at += 2;
2351
+ continue;
2352
+ }
2353
+ if (char === "`") {
2354
+ let run = 1;
2355
+ while (line[at + run] === "`") run += 1;
2356
+ codeSpan = codeSpan === null ? at : null;
2357
+ at += run;
2358
+ continue;
2359
+ }
2360
+ if (codeSpan !== null) {
2361
+ at += 1;
2362
+ continue;
2363
+ }
2364
+ if (char === "[") {
2365
+ if (link === null) link = line[at - 1] === "!" ? at - 1 : at;
2366
+ at += 1;
2367
+ continue;
2368
+ }
2369
+ if (char === "]") {
2370
+ if (link !== null && line[at + 1] !== "(") link = null;
2371
+ at += 1;
2372
+ continue;
2373
+ }
2374
+ if (char === ")") {
2375
+ link = null;
2376
+ at += 1;
2377
+ continue;
2378
+ }
2379
+ if (char === "*" || char === "_" || char === "~") {
2380
+ let run = 1;
2381
+ while (line[at + run] === char) run += 1;
2382
+ const start = at;
2383
+ at += run;
2384
+ const before = line[start - 1];
2385
+ const after = line[start + run];
2386
+ if (run < 2) continue;
2387
+ if (char === "_" && isWordCharacter(before) && isWordCharacter(after)) {
2388
+ continue;
2389
+ }
2390
+ const marker = char.repeat(run);
2391
+ const open2 = emphasis.findIndex((entry) => entry.marker === marker);
2392
+ if (open2 !== -1) {
2393
+ emphasis.splice(open2, 1);
2394
+ continue;
2395
+ }
2396
+ if (after !== void 0 && /\s/.test(after)) continue;
2397
+ emphasis.push({ marker, start });
2398
+ continue;
2399
+ }
2400
+ at += 1;
2401
+ }
2402
+ const open = [codeSpan, link, emphasis[0]?.start ?? null].filter(
2403
+ (value) => value !== null
2404
+ );
2405
+ return open.length === 0 ? line.length : Math.min(...open);
2406
+ }
2407
+ function revealBoundary(text, limit) {
2408
+ if (limit <= 0) return 0;
2409
+ const caughtUp = limit >= text.length;
2410
+ const head = caughtUp ? text : text.slice(0, limit);
2411
+ const lineStart = head.lastIndexOf("\n") + 1;
2412
+ const line = head.slice(lineStart);
2413
+ let fenceStart = null;
2414
+ let tableStart = null;
2415
+ let tableRows = 0;
2416
+ let at = 0;
2417
+ while (at < lineStart) {
2418
+ const breakAt = head.indexOf("\n", at);
2419
+ const end = breakAt === -1 ? lineStart : breakAt;
2420
+ const row = head.slice(at, end);
2421
+ if (fenceStart !== null) {
2422
+ if (FENCE.test(row)) fenceStart = null;
2423
+ } else if (FENCE.test(row)) {
2424
+ fenceStart = at;
2425
+ } else if (TABLE_ROW.test(row)) {
2426
+ if (tableStart === null) {
2427
+ tableStart = at;
2428
+ tableRows = 0;
2429
+ }
2430
+ tableRows += 1;
2431
+ } else if (tableStart !== null) {
2432
+ tableStart = null;
2433
+ tableRows = 0;
2434
+ }
2435
+ at = end + 1;
2436
+ }
2437
+ if (fenceStart === null) {
2438
+ const lastIsRow = TABLE_ROW.test(line);
2439
+ if (lastIsRow && tableStart === null) {
2440
+ tableStart = lineStart;
2441
+ tableRows = 0;
2442
+ } else if (!lastIsRow && line !== "") {
2443
+ tableStart = null;
2444
+ tableRows = 0;
2445
+ }
2446
+ if (tableStart !== null && tableRows < 2) return tableStart;
2447
+ }
2448
+ if (caughtUp) return text.length;
2449
+ if (fenceStart !== null) return fenceStart;
2450
+ if (TABLE_ROW.test(line)) return lineStart;
2451
+ if (EMPTY_BLOCK_MARKER.test(line)) return lineStart;
2452
+ const cut = lineStart + safeInlineEnd(line);
2453
+ return isHighSurrogate(text.charCodeAt(cut - 1)) ? cut - 1 : cut;
2454
+ }
2455
+ function prefersReducedMotion() {
2456
+ if (typeof window === "undefined") return true;
2457
+ try {
2458
+ return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
2459
+ } catch {
2460
+ return false;
2461
+ }
2462
+ }
2463
+ function useProgressiveReveal(text, settled) {
2464
+ const [, setFrame] = useState(0);
2465
+ const [reduced] = useState(prefersReducedMotion);
2466
+ const immediate = settled || reduced;
2467
+ const textRef = useRef(text);
2468
+ const cursorRef = useRef(immediate ? text.length : 0);
2469
+ const cutRef = useRef(immediate ? text.length : 0);
2470
+ const committedRef = useRef(0);
2471
+ if (text !== textRef.current) {
2472
+ const revealed = textRef.current.slice(0, Math.floor(cursorRef.current));
2473
+ if (revealed !== "" && !text.startsWith(revealed)) {
2474
+ cursorRef.current = text.length;
2475
+ }
2476
+ cursorRef.current = Math.min(cursorRef.current, text.length);
2477
+ cutRef.current = Math.min(cutRef.current, text.length);
2478
+ textRef.current = text;
2479
+ }
2480
+ if (immediate) cursorRef.current = text.length;
2481
+ const cut = immediate ? text.length : Math.max(
2482
+ cutRef.current,
2483
+ revealBoundary(text, Math.floor(cursorRef.current))
2484
+ );
2485
+ cutRef.current = cut;
2486
+ useEffect(() => {
2487
+ if (immediate) return;
2488
+ if (cursorRef.current >= text.length) return;
2489
+ let cancelled = false;
2490
+ let cancelStep = null;
2491
+ let previous = now();
2492
+ const commit = (at, force) => {
2493
+ if (!force && at - committedRef.current < COMMIT_INTERVAL_MS) return;
2494
+ committedRef.current = at;
2495
+ setFrame((value) => value + 1);
2496
+ };
2497
+ const step = () => {
2498
+ cancelStep = null;
2499
+ if (cancelled) return;
2500
+ const at = now();
2501
+ const elapsed = clamp(at - previous, 0, MAX_STEP_MS);
2502
+ previous = at;
2503
+ const length = textRef.current.length;
2504
+ const pending = length - cursorRef.current;
2505
+ if (pending <= 0) {
2506
+ commit(at, true);
2507
+ return;
2508
+ }
2509
+ const perSecond = clamp(
2510
+ pending / (DRAIN_TARGET_MS / 1e3),
2511
+ MIN_CHARS_PER_SECOND,
2512
+ MAX_CHARS_PER_SECOND
2513
+ );
2514
+ cursorRef.current = Math.min(
2515
+ length,
2516
+ cursorRef.current + perSecond * elapsed / 1e3
2517
+ );
2518
+ const done = cursorRef.current >= length;
2519
+ commit(at, done);
2520
+ if (!done) schedule();
2521
+ };
2522
+ const schedule = () => {
2523
+ if (typeof window.requestAnimationFrame === "function") {
2524
+ const id2 = window.requestAnimationFrame(step);
2525
+ cancelStep = () => window.cancelAnimationFrame(id2);
2526
+ return;
2527
+ }
2528
+ const id = setTimeout(step, FALLBACK_STEP_MS);
2529
+ cancelStep = () => clearTimeout(id);
2530
+ };
2531
+ schedule();
2532
+ return () => {
2533
+ cancelled = true;
2534
+ cancelStep?.();
2535
+ };
2536
+ }, [text, immediate]);
2537
+ return text.slice(0, cut);
2538
+ }
2292
2539
  function getRecognitionCtor() {
2293
2540
  if (typeof window === "undefined") return null;
2294
2541
  const w = window;
@@ -2507,6 +2754,141 @@ function useSpeechInput({
2507
2754
  );
2508
2755
  return { supported, listening, interim, error, start, stop, toggle };
2509
2756
  }
2757
+ var SILENT_FAILURE_MS = 1500;
2758
+ function speechPlatform() {
2759
+ if (typeof window === "undefined") return null;
2760
+ const scope = window;
2761
+ const synth = scope.speechSynthesis;
2762
+ const Utterance = scope.SpeechSynthesisUtterance;
2763
+ if (!synth || typeof synth.speak !== "function") return null;
2764
+ if (typeof Utterance !== "function") return null;
2765
+ return { synth, Utterance };
2766
+ }
2767
+ function normaliseTag(tag) {
2768
+ return tag.trim().toLowerCase().replace(/_/g, "-");
2769
+ }
2770
+ function primaryOf(tag) {
2771
+ return normaliseTag(tag).split("-")[0] ?? "";
2772
+ }
2773
+ function pickVoice(voices, lang) {
2774
+ const wanted = normaliseTag(lang);
2775
+ if (wanted === "") return null;
2776
+ const primary = primaryOf(wanted);
2777
+ const exact = voices.filter((voice) => normaliseTag(voice.lang) === wanted);
2778
+ const related = voices.filter((voice) => primaryOf(voice.lang) === primary);
2779
+ return exact.find((voice) => voice.default) ?? exact[0] ?? related.find((voice) => voice.default) ?? related[0] ?? null;
2780
+ }
2781
+ var activeOwner = null;
2782
+ var activeStop = null;
2783
+ function stopSpeaking() {
2784
+ activeStop?.();
2785
+ }
2786
+ function useSpeechOutput({
2787
+ lang
2788
+ } = {}) {
2789
+ const [available, setAvailable] = useState(() => speechPlatform() !== null);
2790
+ const [speaking, setSpeaking] = useState(false);
2791
+ const utteranceRef = useRef(null);
2792
+ const watchdogRef = useRef(null);
2793
+ const ownerRef = useRef({});
2794
+ const stopRef = useRef(() => void 0);
2795
+ const langRef = useRef(lang);
2796
+ langRef.current = lang;
2797
+ const clearWatchdog = useCallback(() => {
2798
+ if (watchdogRef.current === null) return;
2799
+ clearTimeout(watchdogRef.current);
2800
+ watchdogRef.current = null;
2801
+ }, []);
2802
+ const settle = useCallback(() => {
2803
+ clearWatchdog();
2804
+ utteranceRef.current = null;
2805
+ if (activeOwner === ownerRef.current) {
2806
+ activeOwner = null;
2807
+ activeStop = null;
2808
+ }
2809
+ setSpeaking(false);
2810
+ }, [clearWatchdog]);
2811
+ const stop = useCallback(() => {
2812
+ const owned = utteranceRef.current !== null;
2813
+ settle();
2814
+ if (owned) speechPlatform()?.synth.cancel();
2815
+ }, [settle]);
2816
+ stopRef.current = stop;
2817
+ const speak = useCallback(
2818
+ (text) => {
2819
+ const platform = speechPlatform();
2820
+ if (!platform) {
2821
+ setAvailable(false);
2822
+ return;
2823
+ }
2824
+ const spoken = text.trim();
2825
+ if (spoken === "") return;
2826
+ if (activeStop && activeOwner !== ownerRef.current) activeStop();
2827
+ platform.synth.cancel();
2828
+ const utterance = new platform.Utterance(spoken);
2829
+ const wanted = langRef.current;
2830
+ if (wanted !== void 0 && wanted !== "") {
2831
+ const voice = pickVoice(platform.synth.getVoices(), wanted);
2832
+ utterance.lang = voice?.lang ?? wanted;
2833
+ if (voice) utterance.voice = voice;
2834
+ }
2835
+ const mine = () => utteranceRef.current === utterance;
2836
+ utterance.onstart = () => {
2837
+ if (!mine()) return;
2838
+ clearWatchdog();
2839
+ setSpeaking(true);
2840
+ };
2841
+ utterance.onend = () => {
2842
+ if (!mine()) return;
2843
+ settle();
2844
+ };
2845
+ utterance.onerror = (event) => {
2846
+ if (!mine()) return;
2847
+ const code = typeof event?.error === "string" ? event.error : "";
2848
+ settle();
2849
+ if (code !== "interrupted" && code !== "canceled") setAvailable(false);
2850
+ };
2851
+ utteranceRef.current = utterance;
2852
+ activeOwner = ownerRef.current;
2853
+ activeStop = () => {
2854
+ stopRef.current();
2855
+ };
2856
+ setSpeaking(true);
2857
+ platform.synth.speak(utterance);
2858
+ clearWatchdog();
2859
+ watchdogRef.current = setTimeout(() => {
2860
+ watchdogRef.current = null;
2861
+ if (!mine()) return;
2862
+ if (platform.synth.speaking) return;
2863
+ settle();
2864
+ setAvailable(false);
2865
+ }, SILENT_FAILURE_MS);
2866
+ },
2867
+ [clearWatchdog, settle]
2868
+ );
2869
+ const toggle = useCallback(
2870
+ (text) => {
2871
+ if (speaking) stop();
2872
+ else speak(text);
2873
+ },
2874
+ [speak, speaking, stop]
2875
+ );
2876
+ useEffect(() => {
2877
+ if (!speaking || typeof window === "undefined") return;
2878
+ const onHide = () => {
2879
+ stopRef.current();
2880
+ };
2881
+ window.addEventListener("pagehide", onHide);
2882
+ return () => window.removeEventListener("pagehide", onHide);
2883
+ }, [speaking]);
2884
+ useEffect(
2885
+ () => () => {
2886
+ stopRef.current();
2887
+ },
2888
+ []
2889
+ );
2890
+ return { supported: available, speaking, speak, stop, toggle };
2891
+ }
2510
2892
  var SWIPE_THRESHOLD = 40;
2511
2893
  function useSwipe({
2512
2894
  onSwipeLeft,
@@ -2902,6 +3284,115 @@ var EXPLORE_CSS = `
2902
3284
  height: auto;
2903
3285
  }
2904
3286
 
3287
+ /* ---- Comparison and pricing tables ----------------------------------------------------
3288
+ The harness emits a comparison, and a set of plans, as a GFM table whose FIRST column is
3289
+ the subject \u2014 the product, or the plan (CONTRACT.md \xA77). Two things have to be true of it:
3290
+ the page must not scroll sideways because of it, and it has to be readable on a 320px
3291
+ phone, which a four-column grid of 1px borders is not.
3292
+
3293
+ Wide: a contained scroller. \`max-width: 100%\` keeps the box inside the bubble and
3294
+ \`overflow-x: auto\` keeps the columns inside the box, so the widest possible table still
3295
+ cannot widen the document. \`overscroll-behavior-x: contain\` stops a swipe that runs out
3296
+ of table from chaining to the page \u2014 the same rule the transcript needed. */
3297
+ .boff-explore-body .boff-explore-table {
3298
+ max-width: 100%;
3299
+ overflow-x: auto;
3300
+ overscroll-behavior-x: contain;
3301
+ margin: 0.75rem 0;
3302
+ border: 1px solid hsl(var(--border));
3303
+ border-radius: 0.75rem;
3304
+ background-color: hsl(var(--card));
3305
+ }
3306
+ .boff-explore-body .boff-explore-table:focus-visible {
3307
+ outline: 2px solid hsl(var(--ring));
3308
+ outline-offset: 2px;
3309
+ }
3310
+ .boff-explore-body .boff-explore-table table {
3311
+ /* Beats the blanket \`display: block\` above, which is the unwrapped fallback. */
3312
+ display: table;
3313
+ width: 100%;
3314
+ max-width: none;
3315
+ margin: 0;
3316
+ border-collapse: collapse;
3317
+ font-size: 0.8125rem;
3318
+ }
3319
+ .boff-explore-body .boff-explore-table th,
3320
+ .boff-explore-body .boff-explore-table td {
3321
+ border: 0;
3322
+ border-top: 1px solid hsl(var(--border));
3323
+ padding: 0.5rem 0.75rem;
3324
+ /* \`start\`, never \`left\`: the page root carries dir="rtl" in Arabic, and a left-aligned
3325
+ table there reads against the grain of every other line on the page. */
3326
+ text-align: start;
3327
+ vertical-align: top;
3328
+ overflow-wrap: anywhere;
3329
+ /* Prices in a column line up under each other instead of drifting by digit width. */
3330
+ font-variant-numeric: tabular-nums;
3331
+ min-width: 6rem;
3332
+ }
3333
+ .boff-explore-body .boff-explore-table thead th {
3334
+ border-top: 0;
3335
+ background-color: hsl(var(--muted));
3336
+ color: hsl(var(--foreground));
3337
+ font-weight: 600;
3338
+ white-space: nowrap;
3339
+ }
3340
+ /* The first column is what the row is ABOUT \u2014 the product, or the plan name. Reading it as
3341
+ the row's heading is what makes a price list scannable rather than a grid of strings. */
3342
+ .boff-explore-body .boff-explore-table tbody td:first-child {
3343
+ font-weight: 600;
3344
+ color: hsl(var(--card-foreground));
3345
+ }
3346
+
3347
+ /* Narrow: one card per row. Each cell prints its own column heading, so a value is never
3348
+ stranded without the question it answers, and the subject cell becomes the card's title.
3349
+ A media query rather than a measured width \u2014 these pages are serialised post-hydration,
3350
+ so anything measured here would ship to every phone as static HTML. */
3351
+ @media (max-width: 30rem) {
3352
+ .boff-explore-body .boff-explore-table {
3353
+ overflow-x: visible;
3354
+ }
3355
+ .boff-explore-body .boff-explore-table table,
3356
+ .boff-explore-body .boff-explore-table tbody,
3357
+ .boff-explore-body .boff-explore-table tr,
3358
+ .boff-explore-body .boff-explore-table td {
3359
+ display: block;
3360
+ width: 100%;
3361
+ min-width: 0;
3362
+ }
3363
+ .boff-explore-body .boff-explore-table thead {
3364
+ display: none;
3365
+ }
3366
+ .boff-explore-body .boff-explore-table tr {
3367
+ border-top: 1px solid hsl(var(--border));
3368
+ padding: 0.5rem 0;
3369
+ }
3370
+ .boff-explore-body .boff-explore-table tr:first-child {
3371
+ border-top: 0;
3372
+ }
3373
+ .boff-explore-body .boff-explore-table td {
3374
+ border: 0;
3375
+ padding: 0.125rem 0.75rem;
3376
+ }
3377
+ .boff-explore-body .boff-explore-table td[data-label]::before {
3378
+ content: attr(data-label);
3379
+ display: block;
3380
+ font-size: 0.6875rem;
3381
+ font-weight: 600;
3382
+ text-transform: uppercase;
3383
+ letter-spacing: 0.04em;
3384
+ color: hsl(var(--muted-foreground));
3385
+ }
3386
+ .boff-explore-body .boff-explore-table tbody td:first-child {
3387
+ font-size: 0.9375rem;
3388
+ padding-bottom: 0.25rem;
3389
+ }
3390
+ .boff-explore-body .boff-explore-table tbody td:first-child::before {
3391
+ /* The plan's own name is the heading here; repeating "Plan" over it says nothing. */
3392
+ display: none;
3393
+ }
3394
+ }
3395
+
2905
3396
  /* ---- Ambient backdrop -----------------------------------------------------------------
2906
3397
  Two very low-opacity radial washes behind the welcome state, drifting slowly. Painted on
2907
3398
  a pseudo-element with pointer-events:none so it can never intercept a click \u2014 a
@@ -3073,7 +3564,7 @@ function isOwnElement(el, root) {
3073
3564
  function visualViewportHeight() {
3074
3565
  return window.visualViewport?.height ?? window.innerHeight;
3075
3566
  }
3076
- function clamp(value, min, max) {
3567
+ function clamp2(value, min, max) {
3077
3568
  return Math.min(Math.max(value, min), max);
3078
3569
  }
3079
3570
  function isRestoredNavigation() {
@@ -3223,11 +3714,80 @@ function summariseReferences(references, origins, productNames) {
3223
3714
  };
3224
3715
  }
3225
3716
  var CODE_FENCE = /^\s*(?:```|~~~)/;
3226
- function answerPlainText(markdown) {
3717
+ var TABLE_ROW_LINE = /^\s{0,3}\|/;
3718
+ var TABLE_DELIMITER_ROW = /^\s{0,3}\|[\s:|-]*-[\s:|-]*$/;
3719
+ function readableLine(raw, mode) {
3720
+ let line = raw;
3721
+ line = line.replace(/!\[[^\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g, "");
3722
+ line = mode === "speech" ? (
3723
+ // A voice reading "h-t-t-p-s colon slash slash…" mid-sentence is noise, and the
3724
+ // prompt asks for two to five links in every answer. The label IS the words.
3725
+ line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1")
3726
+ ) : line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1 ($2)");
3727
+ line = mode === "speech" ? line.replace(/<(https?:\/\/[^>\s]+)>/g, "") : line.replace(/<(https?:\/\/[^>\s]+)>/g, "$1");
3728
+ line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
3729
+ line = line.replace(/^\s{0,3}>\s?/, "");
3730
+ if (/^\s{0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/.test(line)) return "";
3731
+ line = line.replace(/`([^`]+)`/g, "$1");
3732
+ line = line.replace(/\*\*([^*]+)\*\*/g, "$1");
3733
+ line = line.replace(/__([^_]+)__/g, "$1");
3734
+ line = line.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, "$1$2");
3735
+ line = line.replace(/(^|[^_\w])_([^_\n]+)_(?![_\w])/g, "$1$2");
3736
+ return line.replace(/[ \t]+$/, "");
3737
+ }
3738
+ function tableCells(row) {
3739
+ const trimmed = row.trim();
3740
+ const cells = [];
3741
+ let cell = "";
3742
+ for (let at = 0; at < trimmed.length; at += 1) {
3743
+ const char = trimmed[at] ?? "";
3744
+ if (char === "\\") {
3745
+ cell += char + (trimmed[at + 1] ?? "");
3746
+ at += 1;
3747
+ continue;
3748
+ }
3749
+ if (char === "|") {
3750
+ cells.push(cell.trim());
3751
+ cell = "";
3752
+ continue;
3753
+ }
3754
+ cell += char;
3755
+ }
3756
+ cells.push(cell.trim());
3757
+ if (trimmed.startsWith("|") && cells[0] === "") cells.shift();
3758
+ if (trimmed.endsWith("|") && cells[cells.length - 1] === "") cells.pop();
3759
+ return cells;
3760
+ }
3761
+ function asSentence(text) {
3762
+ return /[.!?]$/.test(text) ? text : `${text}.`;
3763
+ }
3764
+ function spokenTable(rows) {
3765
+ if (rows.length < 2 || !TABLE_DELIMITER_ROW.test(rows[1] ?? "")) return rows;
3766
+ const labels = tableCells(rows[0] ?? "");
3767
+ const spoken = [];
3768
+ for (const row of rows.slice(2)) {
3769
+ const parts = [];
3770
+ tableCells(row).forEach((value, column) => {
3771
+ if (value === "") return;
3772
+ const label = column === 0 ? "" : labels[column] ?? "";
3773
+ parts.push(label === "" ? value : `${label}: ${value}`);
3774
+ });
3775
+ if (parts.length > 0) spoken.push(asSentence(parts.join(". ")));
3776
+ }
3777
+ return spoken;
3778
+ }
3779
+ function readableText(markdown, mode) {
3227
3780
  const lines = [];
3228
3781
  let inFence = false;
3782
+ let table = [];
3783
+ const flushTable = () => {
3784
+ if (table.length === 0) return;
3785
+ for (const line of spokenTable(table)) lines.push(readableLine(line, mode));
3786
+ table = [];
3787
+ };
3229
3788
  for (const raw of markdown.split("\n")) {
3230
3789
  if (CODE_FENCE.test(raw)) {
3790
+ flushTable();
3231
3791
  inFence = !inFence;
3232
3792
  continue;
3233
3793
  }
@@ -3235,33 +3795,30 @@ function answerPlainText(markdown) {
3235
3795
  lines.push(raw);
3236
3796
  continue;
3237
3797
  }
3238
- let line = raw;
3239
- line = line.replace(/!\[[^\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g, "");
3240
- line = line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1 ($2)");
3241
- line = line.replace(/<(https?:\/\/[^>\s]+)>/g, "$1");
3242
- line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
3243
- line = line.replace(/^\s{0,3}>\s?/, "");
3244
- if (/^\s{0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/.test(line)) {
3245
- lines.push("");
3798
+ if (mode === "speech" && TABLE_ROW_LINE.test(raw)) {
3799
+ table.push(raw);
3246
3800
  continue;
3247
3801
  }
3248
- line = line.replace(/`([^`]+)`/g, "$1");
3249
- line = line.replace(/\*\*([^*]+)\*\*/g, "$1");
3250
- line = line.replace(/__([^_]+)__/g, "$1");
3251
- line = line.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, "$1$2");
3252
- line = line.replace(/(^|[^_\w])_([^_\n]+)_(?![_\w])/g, "$1$2");
3253
- lines.push(line.replace(/[ \t]+$/, ""));
3802
+ flushTable();
3803
+ lines.push(readableLine(raw, mode));
3254
3804
  }
3805
+ flushTable();
3255
3806
  return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
3256
3807
  }
3257
- var InlineImageScopeContext = createContext(null);
3808
+ function answerPlainText(markdown) {
3809
+ return readableText(markdown, "clipboard");
3810
+ }
3811
+ function answerSpeechText(markdown) {
3812
+ return readableText(markdown, "speech");
3813
+ }
3814
+ var AnswerScopeContext = createContext(null);
3258
3815
  var InsideLinkContext = createContext(false);
3259
3816
  function AnswerLink(props) {
3260
3817
  return /* @__PURE__ */ jsx(InsideLinkContext.Provider, { value: true, children: /* @__PURE__ */ jsx(MarkdownLink, { ...props }) });
3261
3818
  }
3262
3819
  var INLINE_IMAGE_FRAME = "relative inline-block max-w-full overflow-hidden rounded-xl border border-border bg-muted align-top shadow-sm";
3263
3820
  function InlineConceptImage({ src, alt }) {
3264
- const scope = useContext(InlineImageScopeContext);
3821
+ const scope = useContext(AnswerScopeContext);
3265
3822
  const insideLink = useContext(InsideLinkContext);
3266
3823
  if (!scope || !scope.enabled || !isInlineImageAllowed(src, scope.origins) || scope.failed.has(src)) {
3267
3824
  return null;
@@ -3309,9 +3866,70 @@ function InlineConceptImage({ src, alt }) {
3309
3866
  }
3310
3867
  ) });
3311
3868
  }
3869
+ function markdownText(node) {
3870
+ if (!node) return "";
3871
+ if (typeof node.value === "string") return node.value;
3872
+ return (node.children ?? []).map(markdownText).join("");
3873
+ }
3874
+ function hasHeaderCells(node) {
3875
+ return (node?.children ?? []).some((cell) => cell.tagName === "th");
3876
+ }
3877
+ function findHeaderRow(node) {
3878
+ for (const child of node?.children ?? []) {
3879
+ if (child.tagName === "tr" && hasHeaderCells(child)) return child;
3880
+ const nested = findHeaderRow(child);
3881
+ if (nested !== null) return nested;
3882
+ }
3883
+ return null;
3884
+ }
3885
+ function tableColumnLabels(node) {
3886
+ const row = findHeaderRow(node);
3887
+ if (row === null) return [];
3888
+ return (row.children ?? []).filter((cell) => cell.tagName === "th").map((cell) => markdownText(cell).trim());
3889
+ }
3890
+ var TableColumnsContext = createContext([]);
3891
+ function AnswerTable({ node, children }) {
3892
+ const scope = useContext(AnswerScopeContext);
3893
+ const table = /* @__PURE__ */ jsx(TableColumnsContext.Provider, { value: tableColumnLabels(node), children: /* @__PURE__ */ jsx("table", { children }) });
3894
+ if (!scope) return table;
3895
+ return /* @__PURE__ */ jsx(
3896
+ "div",
3897
+ {
3898
+ "data-boff-explore": "table",
3899
+ role: "region",
3900
+ "aria-label": scope.strings.answerTable,
3901
+ tabIndex: 0,
3902
+ className: "boff-explore-table",
3903
+ children: table
3904
+ }
3905
+ );
3906
+ }
3907
+ function AnswerTableRow({
3908
+ node,
3909
+ children,
3910
+ ...props
3911
+ }) {
3912
+ const labels = useContext(TableColumnsContext);
3913
+ if (labels.length === 0 || hasHeaderCells(node)) {
3914
+ return /* @__PURE__ */ jsx("tr", { ...props, children });
3915
+ }
3916
+ let column = 0;
3917
+ const cells = Children.map(children, (cell) => {
3918
+ if (!isValidElement(cell)) return cell;
3919
+ const label = labels[column] ?? "";
3920
+ column += 1;
3921
+ if (label === "") return cell;
3922
+ return cloneElement(cell, {
3923
+ "data-label": label
3924
+ });
3925
+ });
3926
+ return /* @__PURE__ */ jsx("tr", { ...props, children: cells });
3927
+ }
3312
3928
  var ANSWER_MARKDOWN_COMPONENTS = {
3313
3929
  a: AnswerLink,
3314
- img: InlineConceptImage
3930
+ img: InlineConceptImage,
3931
+ table: AnswerTable,
3932
+ tr: AnswerTableRow
3315
3933
  };
3316
3934
  function ReferenceCard({
3317
3935
  reference,
@@ -3700,9 +4318,42 @@ function ProvenanceChips({
3700
4318
  );
3701
4319
  }
3702
4320
  var COPY_FEEDBACK_MS = 2500;
4321
+ 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";
4322
+ var ANSWER_ACTION_IDLE = "border-border bg-card text-muted-foreground hover:border-primary/40 hover:bg-accent hover:text-foreground";
4323
+ var ANSWER_ACTION_ACTIVE = "border-primary/40 bg-accent text-primary";
4324
+ function ReadAloudButton({
4325
+ strings: T,
4326
+ text,
4327
+ lang
4328
+ }) {
4329
+ const speech = useSpeechOutput({ lang });
4330
+ if (!speech.supported) return null;
4331
+ const label = speech.speaking ? T.stopReading : T.readAloud;
4332
+ return /* @__PURE__ */ jsxs(
4333
+ "button",
4334
+ {
4335
+ "data-boff-explore": "read-aloud",
4336
+ "data-speaking": speech.speaking ? "true" : "false",
4337
+ type: "button",
4338
+ title: label,
4339
+ "aria-pressed": speech.speaking,
4340
+ onClick: () => speech.toggle(text),
4341
+ className: cn(
4342
+ ANSWER_ACTION_PILL,
4343
+ speech.speaking ? ANSWER_ACTION_ACTIVE : ANSWER_ACTION_IDLE
4344
+ ),
4345
+ children: [
4346
+ /* @__PURE__ */ jsx(Volume2, { className: "h-3 w-3 shrink-0", "aria-hidden": "true" }),
4347
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: label })
4348
+ ]
4349
+ }
4350
+ );
4351
+ }
3703
4352
  function AnswerActions({
3704
4353
  strings: T,
3705
- text
4354
+ text,
4355
+ speechText,
4356
+ speechLang
3706
4357
  }) {
3707
4358
  const [outcome, setOutcome] = useState("idle");
3708
4359
  const resetRef = useRef(null);
@@ -3745,9 +4396,8 @@ function AnswerActions({
3745
4396
  void copy();
3746
4397
  },
3747
4398
  className: cn(
3748
- "inline-flex max-w-full items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs transition-colors",
3749
- "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
3750
- 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"
4399
+ ANSWER_ACTION_PILL,
4400
+ outcome === "failed" ? "border-destructive/40 bg-card text-destructive" : ANSWER_ACTION_IDLE
3751
4401
  ),
3752
4402
  children: [
3753
4403
  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" }),
@@ -3755,6 +4405,7 @@ function AnswerActions({
3755
4405
  ]
3756
4406
  }
3757
4407
  ),
4408
+ /* @__PURE__ */ jsx(ReadAloudButton, { strings: T, text: speechText, lang: speechLang }),
3758
4409
  /* @__PURE__ */ jsx("span", { "data-boff-explore": "copy-status", role: "status", className: "sr-only", children: outcome === "idle" ? "" : label })
3759
4410
  ]
3760
4411
  }
@@ -3806,6 +4457,7 @@ function AssistantBubble({
3806
4457
  activity,
3807
4458
  imageOrigins,
3808
4459
  productNames,
4460
+ speechLang,
3809
4461
  onNavigate,
3810
4462
  anchorRef,
3811
4463
  onFollow,
@@ -3816,6 +4468,7 @@ function AssistantBubble({
3816
4468
  const body = message.content || message.partialContent || "";
3817
4469
  const failed = message.status === "FAILED";
3818
4470
  const completed = message.status === "COMPLETED";
4471
+ const visible = useProgressiveReveal(body, !streaming);
3819
4472
  const [failedImages, setFailedImages] = useState(
3820
4473
  () => /* @__PURE__ */ new Set()
3821
4474
  );
@@ -3824,7 +4477,7 @@ function AssistantBubble({
3824
4477
  (current) => current.has(src) ? current : new Set(current).add(src)
3825
4478
  );
3826
4479
  }, []);
3827
- const imageScope = useMemo(
4480
+ const answerScope = useMemo(
3828
4481
  () => ({
3829
4482
  strings: T,
3830
4483
  enabled: completed,
@@ -3842,6 +4495,10 @@ function AssistantBubble({
3842
4495
  () => answerPlainText(message.content),
3843
4496
  [message.content]
3844
4497
  );
4498
+ const speechText = useMemo(
4499
+ () => answerSpeechText(message.content),
4500
+ [message.content]
4501
+ );
3845
4502
  const grounded = completed && message.answered !== false;
3846
4503
  return /* @__PURE__ */ jsxs(
3847
4504
  "div",
@@ -3888,8 +4545,8 @@ function AssistantBubble({
3888
4545
  ]
3889
4546
  }
3890
4547
  ) : null,
3891
- body ? /* @__PURE__ */ jsxs("div", { className: "rounded-2xl rounded-tl-md border border-border bg-card px-4 py-3 shadow-sm", children: [
3892
- /* @__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 }) }) }),
4548
+ visible ? /* @__PURE__ */ jsxs("div", { className: "rounded-2xl rounded-tl-md border border-border bg-card px-4 py-3 shadow-sm", children: [
4549
+ /* @__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 }) }) }),
3893
4550
  streaming ? /* @__PURE__ */ jsx("span", { className: "boff-explore-caret", "aria-hidden": "true" }) : null
3894
4551
  ] }) : null,
3895
4552
  grounded && provenance.total > 0 ? /* @__PURE__ */ jsx(ProvenanceChips, { strings: T, provenance }) : null,
@@ -3932,7 +4589,15 @@ function AssistantBubble({
3932
4589
  question
3933
4590
  )) })
3934
4591
  ] }) : null,
3935
- completed && copyText !== "" ? /* @__PURE__ */ jsx(AnswerActions, { strings: T, text: copyText }) : null,
4592
+ completed && copyText !== "" ? /* @__PURE__ */ jsx(
4593
+ AnswerActions,
4594
+ {
4595
+ strings: T,
4596
+ text: copyText,
4597
+ speechText,
4598
+ speechLang
4599
+ }
4600
+ ) : null,
3936
4601
  completed && onFeedback ? /* @__PURE__ */ jsx(
3937
4602
  AnswerFeedback,
3938
4603
  {
@@ -4166,7 +4831,7 @@ function ConceptStrip({
4166
4831
  "div",
4167
4832
  {
4168
4833
  "data-boff-explore": "concept-strip-block",
4169
- className: "boff-explore-rise boff-explore-rise-3 mt-6",
4834
+ className: "boff-explore-rise boff-explore-rise-1 mt-6",
4170
4835
  children: [
4171
4836
  /* @__PURE__ */ jsx("p", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: T.conceptStripTitle }),
4172
4837
  /* @__PURE__ */ jsx(
@@ -4356,6 +5021,15 @@ function WelcomeState({
4356
5021
  /* @__PURE__ */ jsx("h1", { className: "mt-4 text-balance break-words text-2xl font-bold tracking-tight sm:text-4xl", children: title }),
4357
5022
  /* @__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 })
4358
5023
  ] }),
5024
+ /* @__PURE__ */ jsx(
5025
+ ConceptStrip,
5026
+ {
5027
+ strings: T,
5028
+ highlights: conceptHighlights,
5029
+ onAsk: onPrompt,
5030
+ disabled
5031
+ }
5032
+ ),
4359
5033
  /* @__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(
4360
5034
  "li",
4361
5035
  {
@@ -4380,15 +5054,6 @@ function WelcomeState({
4380
5054
  }
4381
5055
  )
4382
5056
  ] }) : null,
4383
- /* @__PURE__ */ jsx(
4384
- ConceptStrip,
4385
- {
4386
- strings: T,
4387
- highlights: conceptHighlights,
4388
- onAsk: onPrompt,
4389
- disabled
4390
- }
4391
- ),
4392
5057
  products.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "boff-explore-rise boff-explore-rise-3 mt-6", children: [
4393
5058
  /* @__PURE__ */ jsx("p", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: T.focusProduct }),
4394
5059
  /* @__PURE__ */ jsx(
@@ -4426,6 +5091,7 @@ function ExplorePage({
4426
5091
  );
4427
5092
  const T = useMemo(() => exploreStrings(activeLocale), [activeLocale]);
4428
5093
  const rtl = isRtlLocale(activeLocale);
5094
+ const speechLang = speechLocaleOf(activeLocale);
4429
5095
  const {
4430
5096
  phase,
4431
5097
  catalog,
@@ -4530,11 +5196,11 @@ function ExplorePage({
4530
5196
  const layoutHeight = window.innerHeight;
4531
5197
  if (layoutHeight === 0) return;
4532
5198
  const top = Math.round(
4533
- clamp(el.getBoundingClientRect().top, 0, layoutHeight)
5199
+ clamp2(el.getBoundingClientRect().top, 0, layoutHeight)
4534
5200
  );
4535
5201
  const viewport = window.visualViewport;
4536
5202
  const inset = viewport ? Math.round(
4537
- clamp(
5203
+ clamp2(
4538
5204
  layoutHeight - viewport.height - viewport.offsetTop,
4539
5205
  0,
4540
5206
  layoutHeight
@@ -4815,6 +5481,7 @@ function ExplorePage({
4815
5481
  const text = draft.trim();
4816
5482
  if (!text || overLimit || busy || !canSend) return;
4817
5483
  stopDictation();
5484
+ stopSpeaking();
4818
5485
  setDraft("");
4819
5486
  stickToBottomRef.current = true;
4820
5487
  void send(text, toPayload(stagedRef.current));
@@ -4825,6 +5492,7 @@ function ExplorePage({
4825
5492
  const sendPrompt = useCallback(
4826
5493
  (prompt) => {
4827
5494
  if (!canSend || busy) return;
5495
+ stopSpeaking();
4828
5496
  setDraft("");
4829
5497
  stickToBottomRef.current = true;
4830
5498
  void send(prompt, toPayload(stagedRef.current));
@@ -5060,6 +5728,7 @@ function ExplorePage({
5060
5728
  activity,
5061
5729
  imageOrigins,
5062
5730
  productNames,
5731
+ speechLang,
5063
5732
  onNavigate,
5064
5733
  anchorRef: message.id === latestAssistantId ? latestAssistantRef : void 0,
5065
5734
  onFollow: (kind, url) => recordClick(message.id, kind, url),
@@ -5362,6 +6031,6 @@ function ExplorePage({
5362
6031
  );
5363
6032
  }
5364
6033
 
5365
- export { ExplorePage, answerPlainText, inlineImageOrigins, isInlineImageAllowed, logoUrlOf, productNamesByOrigin, referenceKindOf, summariseReferences };
6034
+ export { ExplorePage, answerPlainText, answerSpeechText, inlineImageOrigins, isInlineImageAllowed, logoUrlOf, productNamesByOrigin, referenceKindOf, summariseReferences };
5366
6035
  //# sourceMappingURL=explore.mjs.map
5367
6036
  //# sourceMappingURL=explore.mjs.map