@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/components/explore.d.mts +23 -1
- package/dist/components/explore.d.ts +23 -1
- package/dist/components/explore.js +709 -39
- package/dist/components/explore.js.map +1 -1
- package/dist/components/explore.mjs +711 -42
- package/dist/components/explore.mjs.map +1 -1
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.js +356 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/index.mjs +354 -1
- package/dist/hooks/index.mjs.map +1 -1
- package/dist/index.d.mts +18 -2
- package/dist/index.d.ts +18 -2
- package/dist/index.js +720 -47
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +719 -50
- package/dist/index.mjs.map +1 -1
- package/dist/{use-explore-chat-CNEdKBc1.d.ts → use-progressive-reveal-BfU_IHip.d.ts} +35 -1
- package/dist/{use-explore-chat-Dgm-jf96.d.mts → use-progressive-reveal-Ha3gYPfs.d.mts} +35 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
3265
|
-
|
|
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
|
-
|
|
3275
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
3775
|
-
"
|
|
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
|
|
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
|
-
|
|
3918
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "boff-prose boff-prose-sm boff-explore-body min-w-0 text-card-foreground", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
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(
|
|
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-
|
|
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
|
-
|
|
5225
|
+
clamp2(el.getBoundingClientRect().top, 0, layoutHeight)
|
|
4560
5226
|
);
|
|
4561
5227
|
const viewport = window.visualViewport;
|
|
4562
5228
|
const inset = viewport ? Math.round(
|
|
4563
|
-
|
|
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;
|