@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
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
3239
|
-
|
|
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
|
-
|
|
3249
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
3749
|
-
"
|
|
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
|
|
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
|
-
|
|
3892
|
-
/* @__PURE__ */ jsx("div", { className: "boff-prose boff-prose-sm boff-explore-body min-w-0 text-card-foreground", children: /* @__PURE__ */ jsx(
|
|
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(
|
|
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-
|
|
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
|
-
|
|
5199
|
+
clamp2(el.getBoundingClientRect().top, 0, layoutHeight)
|
|
4534
5200
|
);
|
|
4535
5201
|
const viewport = window.visualViewport;
|
|
4536
5202
|
const inset = viewport ? Math.round(
|
|
4537
|
-
|
|
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
|