@burdenoff/website-sdk 2026.922.4 → 2026.923.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,485 @@
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';
7
7
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
8
+ import { clsx } from 'clsx';
9
+ import { twMerge } from 'tailwind-merge';
8
10
  import { Helmet } from 'react-helmet-async';
9
11
  import { Slot } from '@radix-ui/react-slot';
10
12
  import { cva } from 'class-variance-authority';
11
- import { clsx } from 'clsx';
12
- import { twMerge } from 'tailwind-merge';
13
13
 
14
14
  var __defProp = Object.defineProperty;
15
15
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
16
16
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
17
+ function MarkdownLink({
18
+ href,
19
+ children,
20
+ // react-markdown passes the hast node to every override; it is not a DOM attribute.
21
+ node: _node,
22
+ ...props
23
+ }) {
24
+ const isExternal = typeof href === "string" && (href.startsWith("http://") || href.startsWith("https://"));
25
+ return /* @__PURE__ */ jsx(
26
+ "a",
27
+ {
28
+ href,
29
+ ...props,
30
+ ...isExternal ? { target: "_blank", rel: "noopener noreferrer" } : { rel: "noopener noreferrer" },
31
+ children
32
+ }
33
+ );
34
+ }
35
+ var defaultComponents = {
36
+ a: MarkdownLink
37
+ };
38
+ function Markdown({
39
+ children,
40
+ className,
41
+ inline,
42
+ components
43
+ }) {
44
+ const content = /* @__PURE__ */ jsx(
45
+ ReactMarkdown,
46
+ {
47
+ remarkPlugins: [remarkGfm],
48
+ rehypePlugins: [rehypeSanitize],
49
+ components: { ...defaultComponents, ...components },
50
+ ...inline ? { disallowedElements: ["p"], unwrapDisallowed: true } : {},
51
+ children
52
+ }
53
+ );
54
+ if (inline) {
55
+ return /* @__PURE__ */ jsx("span", { className, children: content });
56
+ }
57
+ return /* @__PURE__ */ jsx("div", { className, children: content });
58
+ }
59
+ function cn(...inputs) {
60
+ return twMerge(clsx(inputs));
61
+ }
62
+ var EXPLORE_ANSWER_CSS = `
63
+ /* Nothing inside an answer may widen the page. Model output carries long URLs, code and
64
+ tables \u2014 each of those overflows a phone by default, and one overflowing child makes the
65
+ WHOLE document horizontally scrollable, not just the bubble. */
66
+ .boff-explore-body,
67
+ .boff-explore-body p,
68
+ .boff-explore-body li,
69
+ .boff-explore-body a,
70
+ .boff-explore-body h1,
71
+ .boff-explore-body h2,
72
+ .boff-explore-body h3,
73
+ /* h4-h6 as well: a published answer page pushes the model's own headings down a level or
74
+ two so they nest under the page's, and a long unbroken word is no narrower for it. */
75
+ .boff-explore-body h4,
76
+ .boff-explore-body h5,
77
+ .boff-explore-body h6 {
78
+ overflow-wrap: anywhere;
79
+ word-break: break-word;
80
+ }
81
+ .boff-explore-body pre {
82
+ overflow-x: auto;
83
+ max-width: 100%;
84
+ }
85
+ .boff-explore-body table {
86
+ display: block;
87
+ overflow-x: auto;
88
+ max-width: 100%;
89
+ }
90
+ .boff-explore-body img {
91
+ max-width: 100%;
92
+ height: auto;
93
+ }
94
+
95
+ /* ---- Comparison and pricing tables ----------------------------------------------------
96
+ The harness emits a comparison, and a set of plans, as a GFM table whose FIRST column is
97
+ the subject \u2014 the product, or the plan (CONTRACT.md \xA77). Two things have to be true of it:
98
+ the page must not scroll sideways because of it, and it has to be readable on a 320px
99
+ phone, which a four-column grid of 1px borders is not.
100
+
101
+ Wide: a contained scroller. \`max-width: 100%\` keeps the box inside the bubble and
102
+ \`overflow-x: auto\` keeps the columns inside the box, so the widest possible table still
103
+ cannot widen the document. \`overscroll-behavior-x: contain\` stops a swipe that runs out
104
+ of table from chaining to the page \u2014 the same rule the transcript needed. */
105
+ .boff-explore-body .boff-explore-table {
106
+ max-width: 100%;
107
+ overflow-x: auto;
108
+ overscroll-behavior-x: contain;
109
+ margin: 0.75rem 0;
110
+ border: 1px solid hsl(var(--border));
111
+ border-radius: 0.75rem;
112
+ background-color: hsl(var(--card));
113
+ }
114
+ .boff-explore-body .boff-explore-table:focus-visible {
115
+ outline: 2px solid hsl(var(--ring));
116
+ outline-offset: 2px;
117
+ }
118
+ .boff-explore-body .boff-explore-table table {
119
+ /* Beats the blanket \`display: block\` above, which is the unwrapped fallback. */
120
+ display: table;
121
+ width: 100%;
122
+ max-width: none;
123
+ margin: 0;
124
+ border-collapse: collapse;
125
+ font-size: 0.8125rem;
126
+ }
127
+ .boff-explore-body .boff-explore-table th,
128
+ .boff-explore-body .boff-explore-table td {
129
+ border: 0;
130
+ border-top: 1px solid hsl(var(--border));
131
+ padding: 0.5rem 0.75rem;
132
+ /* \`start\`, never \`left\`: the page root carries dir="rtl" in Arabic, and a left-aligned
133
+ table there reads against the grain of every other line on the page. */
134
+ text-align: start;
135
+ vertical-align: top;
136
+ overflow-wrap: anywhere;
137
+ /* Prices in a column line up under each other instead of drifting by digit width. */
138
+ font-variant-numeric: tabular-nums;
139
+ min-width: 6rem;
140
+ }
141
+ .boff-explore-body .boff-explore-table thead th {
142
+ border-top: 0;
143
+ background-color: hsl(var(--muted));
144
+ color: hsl(var(--foreground));
145
+ font-weight: 600;
146
+ white-space: nowrap;
147
+ }
148
+ /* The first column is what the row is ABOUT \u2014 the product, or the plan name. Reading it as
149
+ the row's heading is what makes a price list scannable rather than a grid of strings. */
150
+ .boff-explore-body .boff-explore-table tbody td:first-child {
151
+ font-weight: 600;
152
+ color: hsl(var(--card-foreground));
153
+ }
154
+
155
+ /* Narrow: one card per row. Each cell prints its own column heading, so a value is never
156
+ stranded without the question it answers, and the subject cell becomes the card's title.
157
+ A media query rather than a measured width \u2014 these pages are serialised post-hydration,
158
+ so anything measured here would ship to every phone as static HTML. */
159
+ @media (max-width: 30rem) {
160
+ .boff-explore-body .boff-explore-table {
161
+ overflow-x: visible;
162
+ }
163
+ .boff-explore-body .boff-explore-table table,
164
+ .boff-explore-body .boff-explore-table tbody,
165
+ .boff-explore-body .boff-explore-table tr,
166
+ .boff-explore-body .boff-explore-table td {
167
+ display: block;
168
+ width: 100%;
169
+ min-width: 0;
170
+ }
171
+ .boff-explore-body .boff-explore-table thead {
172
+ display: none;
173
+ }
174
+ .boff-explore-body .boff-explore-table tr {
175
+ border-top: 1px solid hsl(var(--border));
176
+ padding: 0.5rem 0;
177
+ }
178
+ .boff-explore-body .boff-explore-table tr:first-child {
179
+ border-top: 0;
180
+ }
181
+ .boff-explore-body .boff-explore-table td {
182
+ border: 0;
183
+ padding: 0.125rem 0.75rem;
184
+ }
185
+ .boff-explore-body .boff-explore-table td[data-label]::before {
186
+ content: attr(data-label);
187
+ display: block;
188
+ font-size: 0.6875rem;
189
+ font-weight: 600;
190
+ text-transform: uppercase;
191
+ letter-spacing: 0.04em;
192
+ color: hsl(var(--muted-foreground));
193
+ }
194
+ .boff-explore-body .boff-explore-table tbody td:first-child {
195
+ font-size: 0.9375rem;
196
+ padding-bottom: 0.25rem;
197
+ }
198
+ .boff-explore-body .boff-explore-table tbody td:first-child::before {
199
+ /* The plan's own name is the heading here; repeating "Plan" over it says nothing. */
200
+ display: none;
201
+ }
202
+ }
203
+ `;
204
+ function ExploreAnswerStyles() {
205
+ return /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: EXPLORE_ANSWER_CSS } });
206
+ }
207
+ function inlineImageOrigins(products) {
208
+ const origins = /* @__PURE__ */ new Set();
209
+ for (const product of products) {
210
+ let url;
211
+ try {
212
+ url = new URL(product.website);
213
+ } catch {
214
+ continue;
215
+ }
216
+ if (url.protocol !== "https:") continue;
217
+ const apex = url.hostname.replace(/^www\./, "");
218
+ const port = url.port ? `:${url.port}` : "";
219
+ origins.add(`https://${apex}${port}`);
220
+ origins.add(`https://www.${apex}${port}`);
221
+ }
222
+ return origins;
223
+ }
224
+ function isInlineImageAllowed(src, origins) {
225
+ if (typeof src !== "string" || src === "") return false;
226
+ let url;
227
+ try {
228
+ url = new URL(src);
229
+ } catch {
230
+ return false;
231
+ }
232
+ if (url.protocol !== "https:" || url.username || url.password) return false;
233
+ return origins.has(url.origin);
234
+ }
235
+ var CODE_FENCE = /^\s*(?:```|~~~)/;
236
+ var TABLE_ROW_LINE = /^\s{0,3}\|/;
237
+ var TABLE_DELIMITER_ROW = /^\s{0,3}\|[\s:|-]*-[\s:|-]*$/;
238
+ function readableLine(raw, mode) {
239
+ let line = raw;
240
+ line = line.replace(/!\[[^\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g, "");
241
+ line = mode === "speech" ? (
242
+ // A voice reading "h-t-t-p-s colon slash slash…" mid-sentence is noise, and the
243
+ // prompt asks for two to five links in every answer. The label IS the words.
244
+ line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1")
245
+ ) : line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1 ($2)");
246
+ line = mode === "speech" ? line.replace(/<(https?:\/\/[^>\s]+)>/g, "") : line.replace(/<(https?:\/\/[^>\s]+)>/g, "$1");
247
+ line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
248
+ line = line.replace(/^\s{0,3}>\s?/, "");
249
+ if (/^\s{0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/.test(line)) return "";
250
+ line = line.replace(/`([^`]+)`/g, "$1");
251
+ line = line.replace(/\*\*([^*]+)\*\*/g, "$1");
252
+ line = line.replace(/__([^_]+)__/g, "$1");
253
+ line = line.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, "$1$2");
254
+ line = line.replace(/(^|[^_\w])_([^_\n]+)_(?![_\w])/g, "$1$2");
255
+ return line.replace(/[ \t]+$/, "");
256
+ }
257
+ function tableCells(row) {
258
+ const trimmed = row.trim();
259
+ const cells = [];
260
+ let cell = "";
261
+ for (let at = 0; at < trimmed.length; at += 1) {
262
+ const char = trimmed[at] ?? "";
263
+ if (char === "\\") {
264
+ cell += char + (trimmed[at + 1] ?? "");
265
+ at += 1;
266
+ continue;
267
+ }
268
+ if (char === "|") {
269
+ cells.push(cell.trim());
270
+ cell = "";
271
+ continue;
272
+ }
273
+ cell += char;
274
+ }
275
+ cells.push(cell.trim());
276
+ if (trimmed.startsWith("|") && cells[0] === "") cells.shift();
277
+ if (trimmed.endsWith("|") && cells[cells.length - 1] === "") cells.pop();
278
+ return cells;
279
+ }
280
+ function asSentence(text) {
281
+ return /[.!?]$/.test(text) ? text : `${text}.`;
282
+ }
283
+ function spokenTable(rows) {
284
+ if (rows.length < 2 || !TABLE_DELIMITER_ROW.test(rows[1] ?? "")) return rows;
285
+ const labels = tableCells(rows[0] ?? "");
286
+ const spoken = [];
287
+ for (const row of rows.slice(2)) {
288
+ const parts = [];
289
+ tableCells(row).forEach((value, column) => {
290
+ if (value === "") return;
291
+ const label = column === 0 ? "" : labels[column] ?? "";
292
+ parts.push(label === "" ? value : `${label}: ${value}`);
293
+ });
294
+ if (parts.length > 0) spoken.push(asSentence(parts.join(". ")));
295
+ }
296
+ return spoken;
297
+ }
298
+ function readableText(markdown, mode) {
299
+ const lines = [];
300
+ let inFence = false;
301
+ let table = [];
302
+ const flushTable = () => {
303
+ if (table.length === 0) return;
304
+ for (const line of spokenTable(table)) lines.push(readableLine(line, mode));
305
+ table = [];
306
+ };
307
+ for (const raw of markdown.split("\n")) {
308
+ if (CODE_FENCE.test(raw)) {
309
+ flushTable();
310
+ inFence = !inFence;
311
+ continue;
312
+ }
313
+ if (inFence) {
314
+ lines.push(raw);
315
+ continue;
316
+ }
317
+ if (mode === "speech" && TABLE_ROW_LINE.test(raw)) {
318
+ table.push(raw);
319
+ continue;
320
+ }
321
+ flushTable();
322
+ lines.push(readableLine(raw, mode));
323
+ }
324
+ flushTable();
325
+ return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
326
+ }
327
+ function answerPlainText(markdown) {
328
+ return readableText(markdown, "clipboard");
329
+ }
330
+ function answerSpeechText(markdown) {
331
+ return readableText(markdown, "speech");
332
+ }
333
+ var AnswerScopeContext = createContext(null);
334
+ var InsideLinkContext = createContext(false);
335
+ function AnswerLink(props) {
336
+ return /* @__PURE__ */ jsx(InsideLinkContext.Provider, { value: true, children: /* @__PURE__ */ jsx(MarkdownLink, { ...props }) });
337
+ }
338
+ var INLINE_IMAGE_FRAME = "relative inline-block max-w-full overflow-hidden rounded-xl border border-border bg-muted align-top shadow-sm";
339
+ function InlineConceptImage({ src, alt }) {
340
+ const scope = useContext(AnswerScopeContext);
341
+ const insideLink = useContext(InsideLinkContext);
342
+ if (!scope || !scope.enabled || !isInlineImageAllowed(src, scope.origins) || scope.failed.has(src)) {
343
+ return null;
344
+ }
345
+ const { strings: T, onFailed } = scope;
346
+ const picture = /* @__PURE__ */ jsxs(Fragment, { children: [
347
+ /* @__PURE__ */ jsx(
348
+ "img",
349
+ {
350
+ src,
351
+ alt: alt ?? "",
352
+ loading: "lazy",
353
+ decoding: "async",
354
+ onError: () => onFailed(src),
355
+ className: "block h-auto max-h-80 w-auto max-w-full object-contain"
356
+ }
357
+ ),
358
+ /* @__PURE__ */ jsx(
359
+ "span",
360
+ {
361
+ "aria-hidden": "true",
362
+ className: "pointer-events-none absolute left-2 top-2 rounded-full border border-border bg-background/90 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-foreground shadow-sm",
363
+ children: T.conceptIllustration
364
+ }
365
+ )
366
+ ] });
367
+ return /* @__PURE__ */ jsx("span", { "data-boff-explore": "inline-image", className: "my-3 block", children: insideLink ? /* @__PURE__ */ jsx("span", { className: INLINE_IMAGE_FRAME, children: picture }) : /* @__PURE__ */ jsxs(
368
+ "a",
369
+ {
370
+ href: src,
371
+ target: "_blank",
372
+ rel: "noopener noreferrer",
373
+ className: cn(
374
+ INLINE_IMAGE_FRAME,
375
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
376
+ ),
377
+ children: [
378
+ picture,
379
+ /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
380
+ "(",
381
+ T.opensInNewTab,
382
+ ")"
383
+ ] })
384
+ ]
385
+ }
386
+ ) });
387
+ }
388
+ function markdownText(node) {
389
+ if (!node) return "";
390
+ if (typeof node.value === "string") return node.value;
391
+ return (node.children ?? []).map(markdownText).join("");
392
+ }
393
+ function hasHeaderCells(node) {
394
+ return (node?.children ?? []).some((cell) => cell.tagName === "th");
395
+ }
396
+ function findHeaderRow(node) {
397
+ for (const child of node?.children ?? []) {
398
+ if (child.tagName === "tr" && hasHeaderCells(child)) return child;
399
+ const nested = findHeaderRow(child);
400
+ if (nested !== null) return nested;
401
+ }
402
+ return null;
403
+ }
404
+ function tableColumnLabels(node) {
405
+ const row = findHeaderRow(node);
406
+ if (row === null) return [];
407
+ return (row.children ?? []).filter((cell) => cell.tagName === "th").map((cell) => markdownText(cell).trim());
408
+ }
409
+ var TableColumnsContext = createContext([]);
410
+ function AnswerTable({ node, children }) {
411
+ const scope = useContext(AnswerScopeContext);
412
+ const table = /* @__PURE__ */ jsx(TableColumnsContext.Provider, { value: tableColumnLabels(node), children: /* @__PURE__ */ jsx("table", { children }) });
413
+ if (!scope) return table;
414
+ return /* @__PURE__ */ jsx(
415
+ "div",
416
+ {
417
+ "data-boff-explore": "table",
418
+ role: "region",
419
+ "aria-label": scope.strings.answerTable,
420
+ tabIndex: 0,
421
+ className: "boff-explore-table",
422
+ children: table
423
+ }
424
+ );
425
+ }
426
+ function AnswerTableRow({
427
+ node,
428
+ children,
429
+ ...props
430
+ }) {
431
+ const labels = useContext(TableColumnsContext);
432
+ if (labels.length === 0 || hasHeaderCells(node)) {
433
+ return /* @__PURE__ */ jsx("tr", { ...props, children });
434
+ }
435
+ let column = 0;
436
+ const cells = Children.map(children, (cell) => {
437
+ if (!isValidElement(cell)) return cell;
438
+ const label = labels[column] ?? "";
439
+ column += 1;
440
+ if (label === "") return cell;
441
+ return cloneElement(cell, {
442
+ "data-label": label
443
+ });
444
+ });
445
+ return /* @__PURE__ */ jsx("tr", { ...props, children: cells });
446
+ }
447
+ var ANSWER_MARKDOWN_COMPONENTS = {
448
+ a: AnswerLink,
449
+ img: InlineConceptImage,
450
+ table: AnswerTable,
451
+ tr: AnswerTableRow
452
+ };
453
+ function ExploreAnswerBody({
454
+ children,
455
+ strings,
456
+ imageOrigins,
457
+ enabled = true,
458
+ components,
459
+ className
460
+ }) {
461
+ const [failed, setFailed] = useState(() => /* @__PURE__ */ new Set());
462
+ const onFailed = useCallback((src) => {
463
+ setFailed(
464
+ (current) => current.has(src) ? current : new Set(current).add(src)
465
+ );
466
+ }, []);
467
+ const scope = useMemo(
468
+ () => ({
469
+ strings,
470
+ enabled,
471
+ origins: imageOrigins,
472
+ failed,
473
+ onFailed
474
+ }),
475
+ [strings, enabled, imageOrigins, failed, onFailed]
476
+ );
477
+ const overrides = useMemo(
478
+ () => components ? { ...ANSWER_MARKDOWN_COMPONENTS, ...components } : ANSWER_MARKDOWN_COMPONENTS,
479
+ [components]
480
+ );
481
+ return /* @__PURE__ */ jsx("div", { className: cn("boff-prose boff-explore-body min-w-0", className), children: /* @__PURE__ */ jsx(AnswerScopeContext.Provider, { value: scope, children: /* @__PURE__ */ jsx(Markdown, { components: overrides, children }) }) });
482
+ }
17
483
 
18
484
  // src/components/explore-i18n.ts
19
485
  var EXPLORE_LOCALES = ["en", "zh", "hi", "es", "ar", "ta"];
@@ -32,7 +498,7 @@ var EN = {
32
498
  showLess: "Show less",
33
499
  sources: "Sources",
34
500
  nextYouCouldAsk: "Next you could ask",
35
- placeholder: "Describe the problem you're facing\u2026",
501
+ placeholder: "Describe the problem\u2026",
36
502
  placeholderProduct: "Describe a problem, or ask about {product}\u2026",
37
503
  send: "Send",
38
504
  stop: "Stop",
@@ -73,6 +539,20 @@ var EN = {
73
539
  feedbackDismiss: "Dismiss",
74
540
  feedbackThanks: "Thanks \u2014 that helps.",
75
541
  feedbackNotSent: "That didn't send.",
542
+ escalateOpen: "Talk to a person",
543
+ escalateTitle: "Send this conversation to a person",
544
+ escalateBody: "We'll pass the whole conversation on, so you don't have to type any of it again.",
545
+ escalateEmailLabel: "Your email address",
546
+ escalateEmailPlaceholder: "you@example.com",
547
+ escalateNoteLabel: "Anything else a person should know? (optional)",
548
+ escalateSubmit: "Send to a person",
549
+ escalateSending: "Sending\u2026",
550
+ escalateCancel: "Cancel",
551
+ escalateInvalidEmail: "That doesn't look like an address we can reply to. Please check it.",
552
+ escalateSent: "A person now has this conversation. We'll reply to {email}.",
553
+ escalateRateLimited: "That's a lot of requests in a short time. Please try again in a few minutes.",
554
+ escalateCaptcha: "We couldn't verify that you're human. Please try that again.",
555
+ escalateFailed: "We couldn't pass that on just now. Please try again.",
76
556
  prevSuggestion: "Previous suggestion",
77
557
  nextSuggestion: "Next suggestion",
78
558
  conceptIllustration: "Concept illustration",
@@ -83,6 +563,9 @@ var EN = {
83
563
  conceptAskQuestion: "How does {title} work in {product}?",
84
564
  copyAnswer: "Copy answer",
85
565
  copyFailed: "Couldn't copy",
566
+ readAloud: "Read aloud",
567
+ stopReading: "Stop reading",
568
+ answerTable: "Table",
86
569
  groundedIn: "What this answer is based on",
87
570
  sourceCountOne: "1 source",
88
571
  sourceCountMany: "{count} sources",
@@ -117,7 +600,7 @@ var ZH = {
117
600
  showLess: "\u6536\u8D77",
118
601
  sources: "\u6765\u6E90",
119
602
  nextYouCouldAsk: "\u4F60\u8FD8\u53EF\u4EE5\u95EE",
120
- placeholder: "\u63CF\u8FF0\u4F60\u9047\u5230\u7684\u95EE\u9898\u2026",
603
+ placeholder: "\u63CF\u8FF0\u4F60\u7684\u95EE\u9898\u2026",
121
604
  placeholderProduct: "\u63CF\u8FF0\u95EE\u9898\uFF0C\u6216\u8BE2\u95EE {product}\u2026",
122
605
  send: "\u53D1\u9001",
123
606
  stop: "\u505C\u6B62",
@@ -155,6 +638,20 @@ var ZH = {
155
638
  feedbackDismiss: "\u5173\u95ED",
156
639
  feedbackThanks: "\u8C22\u8C22\uFF0C\u8FD9\u5BF9\u6211\u4EEC\u5F88\u6709\u5E2E\u52A9\u3002",
157
640
  feedbackNotSent: "\u6CA1\u6709\u63D0\u4EA4\u6210\u529F\u3002",
641
+ escalateOpen: "\u8054\u7CFB\u771F\u4EBA",
642
+ escalateTitle: "\u628A\u8FD9\u6BB5\u5BF9\u8BDD\u8F6C\u7ED9\u771F\u4EBA",
643
+ escalateBody: "\u6211\u4EEC\u4F1A\u628A\u6574\u6BB5\u5BF9\u8BDD\u4E00\u8D77\u8F6C\u8FC7\u53BB\uFF0C\u60A8\u4E0D\u7528\u518D\u91CD\u65B0\u6253\u4E00\u904D\u3002",
644
+ escalateEmailLabel: "\u60A8\u7684\u90AE\u7BB1\u5730\u5740",
645
+ escalateEmailPlaceholder: "you@example.com",
646
+ escalateNoteLabel: "\u8FD8\u6709\u4EC0\u4E48\u9700\u8981\u544A\u8BC9\u6211\u4EEC\u7684\u5417\uFF1F\uFF08\u9009\u586B\uFF09",
647
+ escalateSubmit: "\u8F6C\u7ED9\u771F\u4EBA",
648
+ escalateSending: "\u6B63\u5728\u53D1\u9001\u2026",
649
+ escalateCancel: "\u53D6\u6D88",
650
+ escalateInvalidEmail: "\u8FD9\u4F3C\u4E4E\u4E0D\u662F\u4E00\u4E2A\u6211\u4EEC\u53EF\u4EE5\u56DE\u590D\u7684\u90AE\u7BB1\u5730\u5740\uFF0C\u8BF7\u68C0\u67E5\u4E00\u4E0B\u3002",
651
+ escalateSent: "\u5DF2\u6709\u540C\u4E8B\u63A5\u624B\u8FD9\u6BB5\u5BF9\u8BDD\uFF0C\u6211\u4EEC\u4F1A\u56DE\u590D\u5230 {email}\u3002",
652
+ escalateRateLimited: "\u77ED\u65F6\u95F4\u5185\u8BF7\u6C42\u8FC7\u591A\uFF0C\u8BF7\u51E0\u5206\u949F\u540E\u518D\u8BD5\u3002",
653
+ escalateCaptcha: "\u6211\u4EEC\u65E0\u6CD5\u9A8C\u8BC1\u60A8\u662F\u771F\u4EBA\uFF0C\u8BF7\u518D\u8BD5\u4E00\u6B21\u3002",
654
+ escalateFailed: "\u6682\u65F6\u65E0\u6CD5\u8F6C\u4EA4\uFF0C\u8BF7\u518D\u8BD5\u4E00\u6B21\u3002",
158
655
  prevSuggestion: "\u4E0A\u4E00\u4E2A\u5EFA\u8BAE",
159
656
  nextSuggestion: "\u4E0B\u4E00\u4E2A\u5EFA\u8BAE",
160
657
  conceptIllustration: "\u6982\u5FF5\u793A\u610F\u56FE",
@@ -165,6 +662,9 @@ var ZH = {
165
662
  conceptAskQuestion: "{product} \u7684 {title} \u662F\u600E\u4E48\u8FD0\u4F5C\u7684\uFF1F",
166
663
  copyAnswer: "\u590D\u5236\u56DE\u7B54",
167
664
  copyFailed: "\u590D\u5236\u5931\u8D25",
665
+ readAloud: "\u6717\u8BFB",
666
+ stopReading: "\u505C\u6B62\u6717\u8BFB",
667
+ answerTable: "\u8868\u683C",
168
668
  groundedIn: "\u8FD9\u4E2A\u56DE\u7B54\u7684\u4F9D\u636E",
169
669
  sourceCountOne: "1 \u4E2A\u6765\u6E90",
170
670
  sourceCountMany: "{count} \u4E2A\u6765\u6E90",
@@ -195,7 +695,7 @@ var HI = {
195
695
  showLess: "\u0915\u092E \u0926\u093F\u0916\u093E\u090F\u0901",
196
696
  sources: "\u0938\u094D\u0930\u094B\u0924",
197
697
  nextYouCouldAsk: "\u0906\u0917\u0947 \u0906\u092A \u092A\u0942\u091B \u0938\u0915\u0924\u0947 \u0939\u0948\u0902",
198
- placeholder: "\u0905\u092A\u0928\u0940 \u0938\u092E\u0938\u094D\u092F\u093E \u092F\u0939\u093E\u0901 \u092C\u0924\u093E\u090F\u0902\u2026",
698
+ placeholder: "\u0905\u092A\u0928\u0940 \u0938\u092E\u0938\u094D\u092F\u093E \u092C\u0924\u093E\u090F\u0902\u2026",
199
699
  placeholderProduct: "\u0938\u092E\u0938\u094D\u092F\u093E \u092C\u0924\u093E\u090F\u0902, \u092F\u093E {product} \u0915\u0947 \u092C\u093E\u0930\u0947 \u092E\u0947\u0902 \u092A\u0942\u091B\u0947\u0902\u2026",
200
700
  send: "\u092D\u0947\u091C\u0947\u0902",
201
701
  stop: "\u0930\u094B\u0915\u0947\u0902",
@@ -233,6 +733,20 @@ var HI = {
233
733
  feedbackDismiss: "\u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
234
734
  feedbackThanks: "\u0927\u0928\u094D\u092F\u0935\u093E\u0926, \u0907\u0938\u0938\u0947 \u092E\u0926\u0926 \u092E\u093F\u0932\u0924\u0940 \u0939\u0948\u0964",
235
735
  feedbackNotSent: "\u092F\u0939 \u092D\u0947\u091C\u093E \u0928\u0939\u0940\u0902 \u091C\u093E \u0938\u0915\u093E\u0964",
736
+ escalateOpen: "\u0915\u093F\u0938\u0940 \u0935\u094D\u092F\u0915\u094D\u0924\u093F \u0938\u0947 \u092C\u093E\u0924 \u0915\u0930\u0947\u0902",
737
+ escalateTitle: "\u092F\u0939 \u092C\u093E\u0924\u091A\u0940\u0924 \u0915\u093F\u0938\u0940 \u0935\u094D\u092F\u0915\u094D\u0924\u093F \u0915\u094B \u092D\u0947\u091C\u0947\u0902",
738
+ escalateBody: "\u0939\u092E \u092A\u0942\u0930\u0940 \u092C\u093E\u0924\u091A\u0940\u0924 \u0906\u0917\u0947 \u092D\u0947\u091C \u0926\u0947\u0902\u0917\u0947, \u0924\u093E\u0915\u093F \u0906\u092A\u0915\u094B \u0926\u094B\u092C\u093E\u0930\u093E \u0915\u0941\u091B \u0932\u093F\u0916\u0928\u093E \u0928 \u092A\u0921\u093C\u0947\u0964",
739
+ escalateEmailLabel: "\u0906\u092A\u0915\u093E \u0908\u092E\u0947\u0932 \u092A\u0924\u093E",
740
+ escalateEmailPlaceholder: "you@example.com",
741
+ escalateNoteLabel: "\u0915\u094D\u092F\u093E \u0915\u0941\u091B \u0914\u0930 \u092C\u0924\u093E\u0928\u093E \u091A\u093E\u0939\u0947\u0902\u0917\u0947? (\u0935\u0948\u0915\u0932\u094D\u092A\u093F\u0915)",
742
+ escalateSubmit: "\u0915\u093F\u0938\u0940 \u0935\u094D\u092F\u0915\u094D\u0924\u093F \u0915\u094B \u092D\u0947\u091C\u0947\u0902",
743
+ escalateSending: "\u092D\u0947\u091C\u093E \u091C\u093E \u0930\u0939\u093E \u0939\u0948\u2026",
744
+ escalateCancel: "\u0930\u0926\u094D\u0926 \u0915\u0930\u0947\u0902",
745
+ escalateInvalidEmail: "\u092F\u0939 \u0910\u0938\u093E \u0908\u092E\u0947\u0932 \u092A\u0924\u093E \u0928\u0939\u0940\u0902 \u0932\u0917\u0924\u093E \u091C\u093F\u0938 \u092A\u0930 \u0939\u092E \u091C\u0935\u093E\u092C \u092D\u0947\u091C \u0938\u0915\u0947\u0902\u0964 \u0915\u0943\u092A\u092F\u093E \u091C\u093E\u0901\u091A \u0932\u0947\u0902\u0964",
746
+ escalateSent: "\u0905\u092C \u092F\u0939 \u092C\u093E\u0924\u091A\u0940\u0924 \u090F\u0915 \u0935\u094D\u092F\u0915\u094D\u0924\u093F \u0915\u0947 \u092A\u093E\u0938 \u0939\u0948\u0964 \u0939\u092E {email} \u092A\u0930 \u091C\u0935\u093E\u092C \u0926\u0947\u0902\u0917\u0947\u0964",
747
+ escalateRateLimited: "\u0925\u094B\u0921\u093C\u0947 \u0938\u092E\u092F \u092E\u0947\u0902 \u092C\u0939\u0941\u0924 \u0938\u093E\u0930\u0947 \u0905\u0928\u0941\u0930\u094B\u0927 \u0939\u094B \u0917\u090F\u0964 \u0915\u0943\u092A\u092F\u093E \u0915\u0941\u091B \u092E\u093F\u0928\u091F \u092C\u093E\u0926 \u0915\u094B\u0936\u093F\u0936 \u0915\u0930\u0947\u0902\u0964",
748
+ escalateCaptcha: "\u0939\u092E \u092F\u0939 \u0938\u0924\u094D\u092F\u093E\u092A\u093F\u0924 \u0928\u0939\u0940\u0902 \u0915\u0930 \u0938\u0915\u0947 \u0915\u093F \u0906\u092A \u090F\u0915 \u0935\u094D\u092F\u0915\u094D\u0924\u093F \u0939\u0948\u0902\u0964 \u0915\u0943\u092A\u092F\u093E \u092B\u093F\u0930 \u0915\u094B\u0936\u093F\u0936 \u0915\u0930\u0947\u0902\u0964",
749
+ escalateFailed: "\u0905\u092D\u0940 \u0907\u0938\u0947 \u0906\u0917\u0947 \u0928\u0939\u0940\u0902 \u092D\u0947\u091C\u093E \u091C\u093E \u0938\u0915\u093E\u0964 \u0915\u0943\u092A\u092F\u093E \u092B\u093F\u0930 \u0915\u094B\u0936\u093F\u0936 \u0915\u0930\u0947\u0902\u0964",
236
750
  prevSuggestion: "\u092A\u093F\u091B\u0932\u093E \u0938\u0941\u091D\u093E\u0935",
237
751
  nextSuggestion: "\u0905\u0917\u0932\u093E \u0938\u0941\u091D\u093E\u0935",
238
752
  conceptIllustration: "\u0905\u0935\u0927\u093E\u0930\u0923\u093E \u091A\u093F\u0924\u094D\u0930\u0923",
@@ -243,6 +757,9 @@ var HI = {
243
757
  conceptAskQuestion: "{product} \u092E\u0947\u0902 {title} \u0915\u0948\u0938\u0947 \u0915\u093E\u092E \u0915\u0930\u0924\u093E \u0939\u0948?",
244
758
  copyAnswer: "\u0909\u0924\u094D\u0924\u0930 \u0915\u0949\u092A\u0940 \u0915\u0930\u0947\u0902",
245
759
  copyFailed: "\u0915\u0949\u092A\u0940 \u0928\u0939\u0940\u0902 \u0939\u094B \u0938\u0915\u093E",
760
+ readAloud: "\u091C\u093C\u094B\u0930 \u0938\u0947 \u092A\u0922\u093C\u0947\u0902",
761
+ stopReading: "\u092A\u0922\u093C\u0928\u093E \u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
762
+ answerTable: "\u0924\u093E\u0932\u093F\u0915\u093E",
246
763
  groundedIn: "\u092F\u0939 \u0909\u0924\u094D\u0924\u0930 \u0915\u093F\u0938 \u092A\u0930 \u0906\u0927\u093E\u0930\u093F\u0924 \u0939\u0948",
247
764
  sourceCountOne: "1 \u0938\u094D\u0930\u094B\u0924",
248
765
  sourceCountMany: "{count} \u0938\u094D\u0930\u094B\u0924",
@@ -277,7 +794,7 @@ var ES = {
277
794
  showLess: "Ver menos",
278
795
  sources: "Fuentes",
279
796
  nextYouCouldAsk: "Tambi\xE9n puedes preguntar",
280
- placeholder: "Describe el problema que tienes\u2026",
797
+ placeholder: "Describe el problema\u2026",
281
798
  placeholderProduct: "Describe un problema o pregunta por {product}\u2026",
282
799
  send: "Enviar",
283
800
  stop: "Detener",
@@ -315,6 +832,20 @@ var ES = {
315
832
  feedbackDismiss: "Cerrar",
316
833
  feedbackThanks: "Gracias, nos sirve.",
317
834
  feedbackNotSent: "No se ha podido enviar.",
835
+ escalateOpen: "Hablar con una persona",
836
+ escalateTitle: "Env\xEDa esta conversaci\xF3n a una persona",
837
+ escalateBody: "Le pasaremos la conversaci\xF3n entera, as\xED no tienes que volver a escribirla.",
838
+ escalateEmailLabel: "Tu direcci\xF3n de correo",
839
+ escalateEmailPlaceholder: "tu@ejemplo.com",
840
+ escalateNoteLabel: "\xBFAlgo m\xE1s que deber\xEDamos saber? (opcional)",
841
+ escalateSubmit: "Enviar a una persona",
842
+ escalateSending: "Enviando\u2026",
843
+ escalateCancel: "Cancelar",
844
+ escalateInvalidEmail: "No parece una direcci\xF3n a la que podamos responder. Rev\xEDsala, por favor.",
845
+ escalateSent: "Una persona ya tiene esta conversaci\xF3n. Te responderemos a {email}.",
846
+ escalateRateLimited: "Han sido muchas peticiones en poco tiempo. Int\xE9ntalo de nuevo en unos minutos.",
847
+ escalateCaptcha: "No hemos podido verificar que eres una persona. Int\xE9ntalo de nuevo.",
848
+ escalateFailed: "No hemos podido pasarlo ahora mismo. Int\xE9ntalo de nuevo.",
318
849
  prevSuggestion: "Sugerencia anterior",
319
850
  nextSuggestion: "Sugerencia siguiente",
320
851
  conceptIllustration: "Ilustraci\xF3n del concepto",
@@ -325,6 +856,9 @@ var ES = {
325
856
  conceptAskQuestion: "\xBFC\xF3mo funciona {title} en {product}?",
326
857
  copyAnswer: "Copiar respuesta",
327
858
  copyFailed: "No se pudo copiar",
859
+ readAloud: "Leer en voz alta",
860
+ stopReading: "Dejar de leer",
861
+ answerTable: "Tabla",
328
862
  groundedIn: "En qu\xE9 se basa esta respuesta",
329
863
  sourceCountOne: "1 fuente",
330
864
  sourceCountMany: "{count} fuentes",
@@ -359,7 +893,7 @@ var AR = {
359
893
  showLess: "\u0639\u0631\u0636 \u0623\u0642\u0644",
360
894
  sources: "\u0627\u0644\u0645\u0635\u0627\u062F\u0631",
361
895
  nextYouCouldAsk: "\u064A\u0645\u0643\u0646\u0643 \u0623\u0646 \u062A\u0633\u0623\u0644 \u0628\u0639\u062F \u0630\u0644\u0643",
362
- placeholder: "\u0635\u0641 \u0627\u0644\u0645\u0634\u0643\u0644\u0629 \u0627\u0644\u062A\u064A \u062A\u0648\u0627\u062C\u0647\u0647\u0627\u2026",
896
+ placeholder: "\u0635\u0641 \u0627\u0644\u0645\u0634\u0643\u0644\u0629\u2026",
363
897
  placeholderProduct: "\u0635\u0641 \u0645\u0634\u0643\u0644\u062A\u0643 \u0623\u0648 \u0627\u0633\u0623\u0644 \u0639\u0646 {product}\u2026",
364
898
  send: "\u0625\u0631\u0633\u0627\u0644",
365
899
  stop: "\u0625\u064A\u0642\u0627\u0641",
@@ -397,6 +931,20 @@ var AR = {
397
931
  feedbackDismiss: "\u0625\u063A\u0644\u0627\u0642",
398
932
  feedbackThanks: "\u0634\u0643\u0631\u064B\u0627\u060C \u0647\u0630\u0627 \u064A\u0633\u0627\u0639\u062F\u0646\u0627.",
399
933
  feedbackNotSent: "\u0644\u0645 \u064A\u062A\u0645 \u0627\u0644\u0625\u0631\u0633\u0627\u0644.",
934
+ escalateOpen: "\u062A\u0648\u0627\u0635\u0644 \u0645\u0639 \u0634\u062E\u0635",
935
+ escalateTitle: "\u0623\u0631\u0633\u0644 \u0647\u0630\u0647 \u0627\u0644\u0645\u062D\u0627\u062F\u062B\u0629 \u0625\u0644\u0649 \u0634\u062E\u0635",
936
+ escalateBody: "\u0633\u0646\u0645\u0631\u0651\u0631 \u0627\u0644\u0645\u062D\u0627\u062F\u062B\u0629 \u0643\u0627\u0645\u0644\u0629\u060C \u062D\u062A\u0649 \u0644\u0627 \u062A\u0636\u0637\u0631 \u0625\u0644\u0649 \u0643\u062A\u0627\u0628\u062A\u0647\u0627 \u0645\u0646 \u062C\u062F\u064A\u062F.",
937
+ escalateEmailLabel: "\u0628\u0631\u064A\u062F\u0643 \u0627\u0644\u0625\u0644\u0643\u062A\u0631\u0648\u0646\u064A",
938
+ escalateEmailPlaceholder: "you@example.com",
939
+ escalateNoteLabel: "\u0647\u0644 \u0645\u0646 \u0634\u064A\u0621 \u0622\u062E\u0631 \u064A\u0646\u0628\u063A\u064A \u0623\u0646 \u0646\u0639\u0631\u0641\u0647\u061F (\u0627\u062E\u062A\u064A\u0627\u0631\u064A)",
940
+ escalateSubmit: "\u0625\u0631\u0633\u0627\u0644 \u0625\u0644\u0649 \u0634\u062E\u0635",
941
+ escalateSending: "\u062C\u0627\u0631\u064D \u0627\u0644\u0625\u0631\u0633\u0627\u0644\u2026",
942
+ escalateCancel: "\u0625\u0644\u063A\u0627\u0621",
943
+ escalateInvalidEmail: "\u0644\u0627 \u064A\u0628\u062F\u0648 \u0647\u0630\u0627 \u0639\u0646\u0648\u0627\u0646\u064B\u0627 \u064A\u0645\u0643\u0646\u0646\u0627 \u0627\u0644\u0631\u062F \u0639\u0644\u064A\u0647. \u064A\u0631\u062C\u0649 \u0627\u0644\u062A\u062D\u0642\u0642 \u0645\u0646\u0647.",
944
+ escalateSent: "\u0623\u0635\u0628\u062D\u062A \u0647\u0630\u0647 \u0627\u0644\u0645\u062D\u0627\u062F\u062B\u0629 \u0644\u062F\u0649 \u0623\u062D\u062F \u0627\u0644\u0632\u0645\u0644\u0627\u0621. \u0633\u0646\u0631\u062F \u0639\u0644\u0649 {email}.",
945
+ escalateRateLimited: "\u0637\u0644\u0628\u0627\u062A \u0643\u062B\u064A\u0631\u0629 \u062E\u0644\u0627\u0644 \u0648\u0642\u062A \u0642\u0635\u064A\u0631. \u064A\u0631\u062C\u0649 \u0627\u0644\u0645\u062D\u0627\u0648\u0644\u0629 \u0628\u0639\u062F \u0628\u0636\u0639 \u062F\u0642\u0627\u0626\u0642.",
946
+ escalateCaptcha: "\u062A\u0639\u0630\u0651\u0631 \u0627\u0644\u062A\u062D\u0642\u0642 \u0645\u0646 \u0623\u0646\u0643 \u0644\u0633\u062A \u0631\u0648\u0628\u0648\u062A\u064B\u0627. \u064A\u0631\u062C\u0649 \u0627\u0644\u0645\u062D\u0627\u0648\u0644\u0629 \u0645\u0631\u0629 \u0623\u062E\u0631\u0649.",
947
+ escalateFailed: "\u062A\u0639\u0630\u0651\u0631 \u062A\u0645\u0631\u064A\u0631 \u0627\u0644\u0645\u062D\u0627\u062F\u062B\u0629 \u0627\u0644\u0622\u0646. \u064A\u0631\u062C\u0649 \u0627\u0644\u0645\u062D\u0627\u0648\u0644\u0629 \u0645\u0631\u0629 \u0623\u062E\u0631\u0649.",
400
948
  prevSuggestion: "\u0627\u0644\u0627\u0642\u062A\u0631\u0627\u062D \u0627\u0644\u0633\u0627\u0628\u0642",
401
949
  nextSuggestion: "\u0627\u0644\u0627\u0642\u062A\u0631\u0627\u062D \u0627\u0644\u062A\u0627\u0644\u064A",
402
950
  conceptIllustration: "\u0631\u0633\u0645 \u062A\u0648\u0636\u064A\u062D\u064A \u0644\u0644\u0645\u0641\u0647\u0648\u0645",
@@ -407,6 +955,9 @@ var AR = {
407
955
  conceptAskQuestion: "\u0643\u064A\u0641 \u064A\u0639\u0645\u0644 {title} \u0641\u064A {product}\u061F",
408
956
  copyAnswer: "\u0646\u0633\u062E \u0627\u0644\u0625\u062C\u0627\u0628\u0629",
409
957
  copyFailed: "\u062A\u0639\u0630\u0651\u0631 \u0627\u0644\u0646\u0633\u062E",
958
+ readAloud: "\u0627\u0642\u0631\u0623 \u0628\u0635\u0648\u062A \u0639\u0627\u0644\u064D",
959
+ stopReading: "\u0625\u064A\u0642\u0627\u0641 \u0627\u0644\u0642\u0631\u0627\u0621\u0629",
960
+ answerTable: "\u062C\u062F\u0648\u0644",
410
961
  groundedIn: "\u0639\u0644\u0649 \u0645\u0627\u0630\u0627 \u062A\u0633\u062A\u0646\u062F \u0647\u0630\u0647 \u0627\u0644\u0625\u062C\u0627\u0628\u0629",
411
962
  sourceCountOne: "\u0645\u0635\u062F\u0631 \u0648\u0627\u062D\u062F",
412
963
  sourceCountMany: "{count} \u0645\u0635\u0627\u062F\u0631",
@@ -441,7 +992,7 @@ var TA = {
441
992
  showLess: "\u0B95\u0BC1\u0BB1\u0BC8\u0BB5\u0BBE\u0B95\u0B95\u0BCD \u0B95\u0BBE\u0B9F\u0BCD\u0B9F\u0BC1",
442
993
  sources: "\u0BAE\u0BC2\u0BB2\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
443
994
  nextYouCouldAsk: "\u0B85\u0B9F\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1 \u0BA8\u0BC0\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0B95\u0BC7\u0B9F\u0BCD\u0B95\u0BB2\u0BBE\u0BAE\u0BCD",
444
- placeholder: "\u0BA8\u0BC0\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0B8E\u0BA4\u0BBF\u0BB0\u0BCD\u0B95\u0BCA\u0BB3\u0BCD\u0BB3\u0BC1\u0BAE\u0BCD \u0B9A\u0BBF\u0B95\u0BCD\u0B95\u0BB2\u0BC8 \u0BB5\u0BBF\u0BB5\u0BB0\u0BBF\u0BAF\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD\u2026",
995
+ placeholder: "\u0B9A\u0BBF\u0B95\u0BCD\u0B95\u0BB2\u0BC8 \u0BB5\u0BBF\u0BB5\u0BB0\u0BBF\u0BAF\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD\u2026",
445
996
  placeholderProduct: "\u0B9A\u0BBF\u0B95\u0BCD\u0B95\u0BB2\u0BC8 \u0BB5\u0BBF\u0BB5\u0BB0\u0BBF\u0BAF\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD, \u0B85\u0BB2\u0BCD\u0BB2\u0BA4\u0BC1 {product} \u0BAA\u0BB1\u0BCD\u0BB1\u0BBF\u0B95\u0BCD \u0B95\u0BC7\u0BB3\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD\u2026",
446
997
  send: "\u0B85\u0BA9\u0BC1\u0BAA\u0BCD\u0BAA\u0BC1",
447
998
  stop: "\u0BA8\u0BBF\u0BB1\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1",
@@ -479,6 +1030,20 @@ var TA = {
479
1030
  feedbackDismiss: "\u0BAE\u0BC2\u0B9F\u0BC1",
480
1031
  feedbackThanks: "\u0BA8\u0BA9\u0BCD\u0BB1\u0BBF, \u0B87\u0BA4\u0BC1 \u0B8E\u0B99\u0BCD\u0B95\u0BB3\u0BC1\u0B95\u0BCD\u0B95\u0BC1 \u0B89\u0BA4\u0BB5\u0BC1\u0B95\u0BBF\u0BB1\u0BA4\u0BC1.",
481
1032
  feedbackNotSent: "\u0B85\u0BA9\u0BC1\u0BAA\u0BCD\u0BAA \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8.",
1033
+ escalateOpen: "\u0B92\u0BB0\u0BC1\u0BB5\u0BB0\u0BBF\u0B9F\u0BAE\u0BCD \u0BAA\u0BC7\u0B9A\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
1034
+ escalateTitle: "\u0B87\u0BA8\u0BCD\u0BA4 \u0B89\u0BB0\u0BC8\u0BAF\u0BBE\u0B9F\u0BB2\u0BC8 \u0B92\u0BB0\u0BC1\u0BB5\u0BB0\u0BC1\u0B95\u0BCD\u0B95\u0BC1 \u0B85\u0BA9\u0BC1\u0BAA\u0BCD\u0BAA\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
1035
+ escalateBody: "\u0BAE\u0BC1\u0BB4\u0BC1 \u0B89\u0BB0\u0BC8\u0BAF\u0BBE\u0B9F\u0BB2\u0BC8\u0BAF\u0BC1\u0BAE\u0BCD \u0BA8\u0BBE\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0B85\u0BA9\u0BC1\u0BAA\u0BCD\u0BAA\u0BBF\u0BB5\u0BBF\u0B9F\u0BC1\u0BB5\u0BCB\u0BAE\u0BCD; \u0BA8\u0BC0\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0BAE\u0BC0\u0BA3\u0BCD\u0B9F\u0BC1\u0BAE\u0BCD \u0BA4\u0B9F\u0BCD\u0B9F\u0B9A\u0BCD\u0B9A\u0BC1 \u0B9A\u0BC6\u0BAF\u0BCD\u0BAF \u0BB5\u0BC7\u0BA3\u0BCD\u0B9F\u0BBF\u0BAF\u0BA4\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8.",
1036
+ escalateEmailLabel: "\u0B89\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0BAE\u0BBF\u0BA9\u0BCD\u0BA9\u0B9E\u0BCD\u0B9A\u0BB2\u0BCD \u0BAE\u0BC1\u0B95\u0BB5\u0BB0\u0BBF",
1037
+ escalateEmailPlaceholder: "you@example.com",
1038
+ escalateNoteLabel: "\u0BB5\u0BC7\u0BB1\u0BC1 \u0B8F\u0BA4\u0BC7\u0BA9\u0BC1\u0BAE\u0BCD \u0B9A\u0BCA\u0BB2\u0BCD\u0BB2 \u0BB5\u0BBF\u0BB0\u0BC1\u0BAE\u0BCD\u0BAA\u0BC1\u0B95\u0BBF\u0BB1\u0BC0\u0BB0\u0BCD\u0B95\u0BB3\u0BBE? (\u0BB5\u0BBF\u0BB0\u0BC1\u0BAA\u0BCD\u0BAA\u0BAE\u0BCD)",
1039
+ escalateSubmit: "\u0B92\u0BB0\u0BC1\u0BB5\u0BB0\u0BC1\u0B95\u0BCD\u0B95\u0BC1 \u0B85\u0BA9\u0BC1\u0BAA\u0BCD\u0BAA\u0BC1",
1040
+ escalateSending: "\u0B85\u0BA9\u0BC1\u0BAA\u0BCD\u0BAA\u0BAA\u0BCD\u0BAA\u0B9F\u0BC1\u0B95\u0BBF\u0BB1\u0BA4\u0BC1\u2026",
1041
+ escalateCancel: "\u0BB0\u0BA4\u0BCD\u0BA4\u0BC1 \u0B9A\u0BC6\u0BAF\u0BCD",
1042
+ escalateInvalidEmail: "\u0B87\u0BA4\u0BC1 \u0BA8\u0BBE\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BB3\u0BBF\u0B95\u0BCD\u0B95\u0B95\u0BCD\u0B95\u0BC2\u0B9F\u0BBF\u0BAF \u0BAE\u0BBF\u0BA9\u0BCD\u0BA9\u0B9E\u0BCD\u0B9A\u0BB2\u0BCD \u0BAE\u0BC1\u0B95\u0BB5\u0BB0\u0BBF \u0BAA\u0BCB\u0BB2\u0BCD \u0BA4\u0BC6\u0BB0\u0BBF\u0BAF\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8. \u0B9A\u0BB0\u0BBF\u0BAA\u0BBE\u0BB0\u0BCD\u0B95\u0BCD\u0B95\u0BB5\u0BC1\u0BAE\u0BCD.",
1043
+ escalateSent: "\u0B87\u0BA8\u0BCD\u0BA4 \u0B89\u0BB0\u0BC8\u0BAF\u0BBE\u0B9F\u0BB2\u0BCD \u0B87\u0BAA\u0BCD\u0BAA\u0BCB\u0BA4\u0BC1 \u0B92\u0BB0\u0BC1\u0BB5\u0BB0\u0BBF\u0B9F\u0BAE\u0BCD \u0B89\u0BB3\u0BCD\u0BB3\u0BA4\u0BC1. {email} \u0BAE\u0BC1\u0B95\u0BB5\u0BB0\u0BBF\u0B95\u0BCD\u0B95\u0BC1\u0BAA\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BB3\u0BBF\u0BAA\u0BCD\u0BAA\u0BCB\u0BAE\u0BCD.",
1044
+ escalateRateLimited: "\u0B95\u0BC1\u0BB1\u0BC1\u0B95\u0BBF\u0BAF \u0BA8\u0BC7\u0BB0\u0BA4\u0BCD\u0BA4\u0BBF\u0BB2\u0BCD \u0B85\u0BA4\u0BBF\u0B95 \u0B95\u0BCB\u0BB0\u0BBF\u0B95\u0BCD\u0B95\u0BC8\u0B95\u0BB3\u0BCD. \u0B9A\u0BBF\u0BB2 \u0BA8\u0BBF\u0BAE\u0BBF\u0B9F\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0B95\u0BB4\u0BBF\u0BA4\u0BCD\u0BA4\u0BC1 \u0BAE\u0BC1\u0BAF\u0BB1\u0BCD\u0B9A\u0BBF\u0B95\u0BCD\u0B95\u0BB5\u0BC1\u0BAE\u0BCD.",
1045
+ escalateCaptcha: "\u0BA8\u0BC0\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0BAE\u0BA9\u0BBF\u0BA4\u0BB0\u0BCD \u0B8E\u0BA9\u0BCD\u0BAA\u0BA4\u0BC8 \u0B89\u0BB1\u0BC1\u0BA4\u0BBF\u0B9A\u0BC6\u0BAF\u0BCD\u0BAF \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8. \u0BAE\u0BC0\u0BA3\u0BCD\u0B9F\u0BC1\u0BAE\u0BCD \u0BAE\u0BC1\u0BAF\u0BB1\u0BCD\u0B9A\u0BBF\u0B95\u0BCD\u0B95\u0BB5\u0BC1\u0BAE\u0BCD.",
1046
+ escalateFailed: "\u0B87\u0BAA\u0BCD\u0BAA\u0BCB\u0BA4\u0BC1 \u0B85\u0BA9\u0BC1\u0BAA\u0BCD\u0BAA \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8. \u0BAE\u0BC0\u0BA3\u0BCD\u0B9F\u0BC1\u0BAE\u0BCD \u0BAE\u0BC1\u0BAF\u0BB1\u0BCD\u0B9A\u0BBF\u0B95\u0BCD\u0B95\u0BB5\u0BC1\u0BAE\u0BCD.",
482
1047
  prevSuggestion: "\u0BAE\u0BC1\u0BA8\u0BCD\u0BA4\u0BC8\u0BAF \u0BAA\u0BB0\u0BBF\u0BA8\u0BCD\u0BA4\u0BC1\u0BB0\u0BC8",
483
1048
  nextSuggestion: "\u0B85\u0B9F\u0BC1\u0BA4\u0BCD\u0BA4 \u0BAA\u0BB0\u0BBF\u0BA8\u0BCD\u0BA4\u0BC1\u0BB0\u0BC8",
484
1049
  conceptIllustration: "\u0B95\u0BB0\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1 \u0BB5\u0BBF\u0BB3\u0B95\u0BCD\u0B95\u0BAA\u0BCD\u0BAA\u0B9F\u0BAE\u0BCD",
@@ -489,6 +1054,9 @@ var TA = {
489
1054
  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
1055
  copyAnswer: "\u0BAA\u0BA4\u0BBF\u0BB2\u0BC8 \u0BA8\u0B95\u0BB2\u0BC6\u0B9F\u0BC1",
491
1056
  copyFailed: "\u0BA8\u0B95\u0BB2\u0BC6\u0B9F\u0BC1\u0B95\u0BCD\u0B95 \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8",
1057
+ readAloud: "\u0B9A\u0BA4\u0BCD\u0BA4\u0BAE\u0BBE\u0B95\u0BAA\u0BCD \u0BAA\u0B9F\u0BBF",
1058
+ stopReading: "\u0BAA\u0B9F\u0BBF\u0BAA\u0BCD\u0BAA\u0BA4\u0BC8 \u0BA8\u0BBF\u0BB1\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1",
1059
+ answerTable: "\u0B85\u0B9F\u0BCD\u0B9F\u0BB5\u0BA3\u0BC8",
492
1060
  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
1061
  sourceCountOne: "1 \u0BAE\u0BC2\u0BB2\u0BAE\u0BCD",
494
1062
  sourceCountMany: "{count} \u0BAE\u0BC2\u0BB2\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
@@ -520,6 +1088,17 @@ var STRINGS = {
520
1088
  ar: AR,
521
1089
  ta: TA
522
1090
  };
1091
+ var SPEECH_TAGS = {
1092
+ en: "en-US",
1093
+ zh: "zh-CN",
1094
+ hi: "hi-IN",
1095
+ es: "es-ES",
1096
+ ar: "ar-SA",
1097
+ ta: "ta-IN"
1098
+ };
1099
+ function speechLocaleOf(locale) {
1100
+ return SPEECH_TAGS[locale];
1101
+ }
523
1102
  function resolveExploreLocale(explicit) {
524
1103
  const candidates = [
525
1104
  explicit,
@@ -707,6 +1286,29 @@ var SUBMIT_EXPLORE_FEEDBACK_MUTATION = (
707
1286
  `
708
1287
  );
709
1288
  var EXPLORE_FEEDBACK_COMMENT_SOFT_MAX = 500;
1289
+ var ESCALATE_EXPLORE_CONVERSATION_MUTATION = (
1290
+ /* GraphQL */
1291
+ `
1292
+ mutation EscalateExploreConversation(
1293
+ $input: EscalateExploreConversationInput!
1294
+ ) {
1295
+ escalateExploreConversation(input: $input) {
1296
+ created
1297
+ escalation {
1298
+ email
1299
+ createdAt
1300
+ }
1301
+ }
1302
+ }
1303
+ `
1304
+ );
1305
+ var EXPLORE_ESCALATION_NOTE_SOFT_MAX = 2e3;
1306
+ var DELIVERABLE_EMAIL = /^[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\.)+[A-Za-z]{2,}$/;
1307
+ function looksLikeDeliverableEmail(value) {
1308
+ const trimmed = value.trim();
1309
+ if (trimmed.length === 0 || trimmed.length > 254) return false;
1310
+ return DELIVERABLE_EMAIL.test(trimmed);
1311
+ }
710
1312
  var CREATE_EXPLORE_SHARE_LINK_MUTATION = (
711
1313
  /* GraphQL */
712
1314
  `
@@ -727,48 +1329,6 @@ var EXPLORE_SHARED_CONVERSATION_QUERY = (
727
1329
  }
728
1330
  `
729
1331
  );
730
- function MarkdownLink({
731
- href,
732
- children,
733
- // react-markdown passes the hast node to every override; it is not a DOM attribute.
734
- node: _node,
735
- ...props
736
- }) {
737
- const isExternal = typeof href === "string" && (href.startsWith("http://") || href.startsWith("https://"));
738
- return /* @__PURE__ */ jsx(
739
- "a",
740
- {
741
- href,
742
- ...props,
743
- ...isExternal ? { target: "_blank", rel: "noopener noreferrer" } : { rel: "noopener noreferrer" },
744
- children
745
- }
746
- );
747
- }
748
- var defaultComponents = {
749
- a: MarkdownLink
750
- };
751
- function Markdown({
752
- children,
753
- className,
754
- inline,
755
- components
756
- }) {
757
- const content = /* @__PURE__ */ jsx(
758
- ReactMarkdown,
759
- {
760
- remarkPlugins: [remarkGfm],
761
- rehypePlugins: [rehypeSanitize],
762
- components: { ...defaultComponents, ...components },
763
- ...inline ? { disallowedElements: ["p"], unwrapDisallowed: true } : {},
764
- children
765
- }
766
- );
767
- if (inline) {
768
- return /* @__PURE__ */ jsx("span", { className, children: content });
769
- }
770
- return /* @__PURE__ */ jsx("div", { className, children: content });
771
- }
772
1332
  var PROSE_CSS = `
773
1333
  .boff-prose {
774
1334
  max-width: none;
@@ -1165,6 +1725,10 @@ function useWebSDK() {
1165
1725
  }
1166
1726
  return client;
1167
1727
  }
1728
+ function useWebSDKConfig() {
1729
+ const client = useWebSDK();
1730
+ return client.getConfig();
1731
+ }
1168
1732
 
1169
1733
  // src/data/products.ts
1170
1734
  var ECOSYSTEM_PRODUCTS = [
@@ -1511,6 +2075,25 @@ var VALIDATION_CODES = /* @__PURE__ */ new Set([
1511
2075
  "CONVERSATION_LIMIT",
1512
2076
  "INVALID_INPUT"
1513
2077
  ]);
2078
+ function escalateOutcomeForCode(code) {
2079
+ switch (code) {
2080
+ case "INVALID_EMAIL":
2081
+ return "invalid-email";
2082
+ case "RATE_LIMITED":
2083
+ case "HTTP_429":
2084
+ return "rate-limited";
2085
+ case "CAPTCHA_REQUIRED":
2086
+ case "CAPTCHA_FAILED":
2087
+ return "captcha";
2088
+ // The token owns nothing, or owns a different conversation. The server answers NOT_FOUND
2089
+ // rather than FORBIDDEN so the endpoint is not an oracle for whether a thread exists;
2090
+ // either way there is nothing this visitor can do about it, so it reads as a failure.
2091
+ case "NOT_FOUND":
2092
+ return "failed";
2093
+ default:
2094
+ return "failed";
2095
+ }
2096
+ }
1514
2097
  function classifyExploreError(code, serverMessage) {
1515
2098
  const c = code ?? "UNKNOWN";
1516
2099
  if (RATE_LIMIT_CODES.has(c)) {
@@ -1677,6 +2260,7 @@ function useExploreChat(options = {}) {
1677
2260
  onSend
1678
2261
  } = options;
1679
2262
  const client = useWebSDK();
2263
+ const { productId } = useWebSDKConfig();
1680
2264
  const promptKey = JSON.stringify(examplePrompts ?? []);
1681
2265
  const welcomeKey = JSON.stringify(fallbackWelcome ?? null);
1682
2266
  const fallbackCatalog = useMemo(
@@ -1700,6 +2284,7 @@ function useExploreChat(options = {}) {
1700
2284
  const pollGenerationRef = useRef(0);
1701
2285
  const pollTimerRef = useRef(null);
1702
2286
  const conversationTokenRef = useRef(null);
2287
+ const [hasConversation, setHasConversation] = useState(false);
1703
2288
  const shareTokenRef = useRef(void 0);
1704
2289
  if (shareTokenRef.current === void 0) {
1705
2290
  shareTokenRef.current = readShareToken();
@@ -1714,6 +2299,10 @@ function useExploreChat(options = {}) {
1714
2299
  );
1715
2300
  const inFlightRef = useRef(false);
1716
2301
  catalogRef.current = catalog;
2302
+ const rememberConversationToken = useCallback((token) => {
2303
+ conversationTokenRef.current = token;
2304
+ setHasConversation(token !== null);
2305
+ }, []);
1717
2306
  const clearPollTimer = useCallback(() => {
1718
2307
  if (pollTimerRef.current !== null) {
1719
2308
  clearTimeout(pollTimerRef.current);
@@ -1780,7 +2369,7 @@ function useExploreChat(options = {}) {
1780
2369
  setArchive(readArchive(storageKey));
1781
2370
  const stored = readPersisted(storageKey);
1782
2371
  if (stored) {
1783
- conversationTokenRef.current = stored.conversationToken;
2372
+ rememberConversationToken(stored.conversationToken);
1784
2373
  focusProductRef.current = stored.focusProduct;
1785
2374
  const settled = stored.messages.flatMap((m) => {
1786
2375
  if (m.role !== "ASSISTANT") return [m];
@@ -1796,7 +2385,7 @@ function useExploreChat(options = {}) {
1796
2385
  setTurnCount(turns);
1797
2386
  }
1798
2387
  setHydrated(true);
1799
- }, [persist, storageKey]);
2388
+ }, [persist, rememberConversationToken, storageKey]);
1800
2389
  useEffect(() => {
1801
2390
  if (!persist || !hydrated || typeof window === "undefined") return;
1802
2391
  try {
@@ -1989,7 +2578,7 @@ function useExploreChat(options = {}) {
1989
2578
  setPhase(classified.kind === "rate-limit" ? "limited" : "error");
1990
2579
  return;
1991
2580
  }
1992
- conversationTokenRef.current = result.conversation.token;
2581
+ rememberConversationToken(result.conversation.token);
1993
2582
  turnCountRef.current = result.conversation.turnCount;
1994
2583
  setTurnCount(result.conversation.turnCount);
1995
2584
  setRemainingToday(result.remainingToday ?? null);
@@ -2015,7 +2604,14 @@ function useExploreChat(options = {}) {
2015
2604
  setPhase("streaming");
2016
2605
  startPolling(result.conversation.token, assistantMessage.id);
2017
2606
  },
2018
- [client, locale, pageUrl, productSlug, startPolling]
2607
+ [
2608
+ client,
2609
+ locale,
2610
+ pageUrl,
2611
+ productSlug,
2612
+ rememberConversationToken,
2613
+ startPolling
2614
+ ]
2019
2615
  );
2020
2616
  const send = useCallback(
2021
2617
  async (message, attachments = []) => {
@@ -2132,7 +2728,7 @@ function useExploreChat(options = {}) {
2132
2728
  );
2133
2729
  cancelPolling();
2134
2730
  inFlightRef.current = false;
2135
- conversationTokenRef.current = null;
2731
+ rememberConversationToken(null);
2136
2732
  lastSentTextRef.current = null;
2137
2733
  lastSentAttachmentsRef.current = [];
2138
2734
  turnCountRef.current = 0;
@@ -2147,7 +2743,7 @@ function useExploreChat(options = {}) {
2147
2743
  } catch {
2148
2744
  }
2149
2745
  }
2150
- }, [cancelPolling, storageKey]);
2746
+ }, [cancelPolling, rememberConversationToken, storageKey]);
2151
2747
  const openConversation = useCallback(
2152
2748
  (token) => {
2153
2749
  const entry = readArchive(storageKey).find((e) => e.token === token);
@@ -2159,7 +2755,7 @@ function useExploreChat(options = {}) {
2159
2755
  });
2160
2756
  cancelPolling();
2161
2757
  inFlightRef.current = false;
2162
- conversationTokenRef.current = entry.token;
2758
+ rememberConversationToken(entry.token);
2163
2759
  turnCountRef.current = entry.messages.filter(
2164
2760
  (m) => m.role === "USER"
2165
2761
  ).length;
@@ -2171,7 +2767,7 @@ function useExploreChat(options = {}) {
2171
2767
  setPhase(catalogRef.current.enabled ? "ready" : "disabled");
2172
2768
  setArchive(readArchive(storageKey));
2173
2769
  },
2174
- [cancelPolling, storageKey]
2770
+ [cancelPolling, rememberConversationToken, storageKey]
2175
2771
  );
2176
2772
  const deleteConversation = useCallback(
2177
2773
  (token) => {
@@ -2260,6 +2856,69 @@ function useExploreChat(options = {}) {
2260
2856
  },
2261
2857
  [client]
2262
2858
  );
2859
+ const escalate = useCallback(
2860
+ async (email, note) => {
2861
+ const token = conversationTokenRef.current;
2862
+ if (!token) {
2863
+ return { outcome: "no-conversation", email: null, createdAt: null };
2864
+ }
2865
+ const attempt = async (captchaAttempt) => {
2866
+ const activeCatalog = catalogRef.current;
2867
+ let recaptchaToken;
2868
+ if (activeCatalog.captchaRequired && activeCatalog.recaptchaSiteKey) {
2869
+ try {
2870
+ recaptchaToken = await getRecaptchaV3Token(
2871
+ activeCatalog.recaptchaSiteKey
2872
+ );
2873
+ } catch (err) {
2874
+ console.warn(
2875
+ "[WebSDK] reCAPTCHA v3 token could not be minted",
2876
+ err
2877
+ );
2878
+ }
2879
+ }
2880
+ const input = {
2881
+ conversationToken: token,
2882
+ email: email.trim(),
2883
+ note: note?.trim() ? note.trim() : null,
2884
+ ...recaptchaToken ? { recaptchaToken } : {},
2885
+ ...productId ? { productId } : {}
2886
+ };
2887
+ try {
2888
+ const res = await client.mutate(
2889
+ ESCALATE_EXPLORE_CONVERSATION_MUTATION,
2890
+ { input }
2891
+ );
2892
+ const first = res?.errors?.[0];
2893
+ const result = res?.data?.escalateExploreConversation;
2894
+ if (first || !result) {
2895
+ const code = typeof first?.extensions?.code === "string" ? first.extensions.code : "ESCALATION_FAILED";
2896
+ if (code === "CAPTCHA_FAILED" && captchaAttempt === 0) {
2897
+ return attempt(1);
2898
+ }
2899
+ return {
2900
+ outcome: escalateOutcomeForCode(code),
2901
+ email: null,
2902
+ createdAt: null
2903
+ };
2904
+ }
2905
+ return {
2906
+ // `created: false` is the double-click, the retry, the reload. The handover
2907
+ // exists, so this is a success — and the address comes from the SERVER, because
2908
+ // on a repeat submit the reply goes to whatever the FIRST handover used.
2909
+ outcome: result.created ? "sent" : "already-handed-over",
2910
+ email: result.escalation?.email ?? null,
2911
+ createdAt: result.escalation?.createdAt ?? null
2912
+ };
2913
+ } catch {
2914
+ return { outcome: "failed", email: null, createdAt: null };
2915
+ }
2916
+ };
2917
+ return attempt(0);
2918
+ },
2919
+ [client, productId]
2920
+ );
2921
+ const canEscalate = hasConversation && !isSharedView;
2263
2922
  return {
2264
2923
  phase,
2265
2924
  catalog,
@@ -2280,6 +2939,8 @@ function useExploreChat(options = {}) {
2280
2939
  clearHistory,
2281
2940
  recordClick,
2282
2941
  submitFeedback,
2942
+ escalate,
2943
+ canEscalate,
2283
2944
  createShareLink,
2284
2945
  isSharedView
2285
2946
  };
@@ -2289,6 +2950,224 @@ function makeOptimisticId() {
2289
2950
  optimisticCounter += 1;
2290
2951
  return `boff-explore-local-${Date.now()}-${optimisticCounter}`;
2291
2952
  }
2953
+ var DRAIN_TARGET_MS = 1200;
2954
+ var MIN_CHARS_PER_SECOND = 90;
2955
+ var MAX_CHARS_PER_SECOND = 2200;
2956
+ var MAX_STEP_MS = 250;
2957
+ var COMMIT_INTERVAL_MS = 45;
2958
+ var FALLBACK_STEP_MS = 50;
2959
+ var FENCE = /^ {0,3}(?:```|~~~)/;
2960
+ var TABLE_ROW = /^ {0,3}\|/;
2961
+ var EMPTY_BLOCK_MARKER = /^ {0,3}(?:[-+>]|#{1,6}|\d{1,9}[.)])\s*$/;
2962
+ function isWordCharacter(char) {
2963
+ return char !== void 0 && /[\p{L}\p{N}]/u.test(char);
2964
+ }
2965
+ function clamp(value, min, max) {
2966
+ return Math.min(Math.max(value, min), max);
2967
+ }
2968
+ function now() {
2969
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
2970
+ }
2971
+ function isHighSurrogate(code) {
2972
+ return code >= 55296 && code <= 56319;
2973
+ }
2974
+ function safeInlineEnd(line) {
2975
+ let codeSpan = null;
2976
+ let link = null;
2977
+ const emphasis = [];
2978
+ let at = 0;
2979
+ while (at < line.length) {
2980
+ const char = line[at] ?? "";
2981
+ if (char === "\\") {
2982
+ at += 2;
2983
+ continue;
2984
+ }
2985
+ if (char === "`") {
2986
+ let run = 1;
2987
+ while (line[at + run] === "`") run += 1;
2988
+ codeSpan = codeSpan === null ? at : null;
2989
+ at += run;
2990
+ continue;
2991
+ }
2992
+ if (codeSpan !== null) {
2993
+ at += 1;
2994
+ continue;
2995
+ }
2996
+ if (char === "[") {
2997
+ if (link === null) link = line[at - 1] === "!" ? at - 1 : at;
2998
+ at += 1;
2999
+ continue;
3000
+ }
3001
+ if (char === "]") {
3002
+ if (link !== null && line[at + 1] !== "(") link = null;
3003
+ at += 1;
3004
+ continue;
3005
+ }
3006
+ if (char === ")") {
3007
+ link = null;
3008
+ at += 1;
3009
+ continue;
3010
+ }
3011
+ if (char === "*" || char === "_" || char === "~") {
3012
+ let run = 1;
3013
+ while (line[at + run] === char) run += 1;
3014
+ const start = at;
3015
+ at += run;
3016
+ const before = line[start - 1];
3017
+ const after = line[start + run];
3018
+ if (run < 2) continue;
3019
+ if (char === "_" && isWordCharacter(before) && isWordCharacter(after)) {
3020
+ continue;
3021
+ }
3022
+ const marker = char.repeat(run);
3023
+ const open2 = emphasis.findIndex((entry) => entry.marker === marker);
3024
+ if (open2 !== -1) {
3025
+ emphasis.splice(open2, 1);
3026
+ continue;
3027
+ }
3028
+ if (after !== void 0 && /\s/.test(after)) continue;
3029
+ emphasis.push({ marker, start });
3030
+ continue;
3031
+ }
3032
+ at += 1;
3033
+ }
3034
+ const open = [codeSpan, link, emphasis[0]?.start ?? null].filter(
3035
+ (value) => value !== null
3036
+ );
3037
+ return open.length === 0 ? line.length : Math.min(...open);
3038
+ }
3039
+ function revealBoundary(text, limit) {
3040
+ if (limit <= 0) return 0;
3041
+ const caughtUp = limit >= text.length;
3042
+ const head = caughtUp ? text : text.slice(0, limit);
3043
+ const lineStart = head.lastIndexOf("\n") + 1;
3044
+ const line = head.slice(lineStart);
3045
+ let fenceStart = null;
3046
+ let tableStart = null;
3047
+ let tableRows = 0;
3048
+ let at = 0;
3049
+ while (at < lineStart) {
3050
+ const breakAt = head.indexOf("\n", at);
3051
+ const end = breakAt === -1 ? lineStart : breakAt;
3052
+ const row = head.slice(at, end);
3053
+ if (fenceStart !== null) {
3054
+ if (FENCE.test(row)) fenceStart = null;
3055
+ } else if (FENCE.test(row)) {
3056
+ fenceStart = at;
3057
+ } else if (TABLE_ROW.test(row)) {
3058
+ if (tableStart === null) {
3059
+ tableStart = at;
3060
+ tableRows = 0;
3061
+ }
3062
+ tableRows += 1;
3063
+ } else if (tableStart !== null) {
3064
+ tableStart = null;
3065
+ tableRows = 0;
3066
+ }
3067
+ at = end + 1;
3068
+ }
3069
+ if (fenceStart === null) {
3070
+ const lastIsRow = TABLE_ROW.test(line);
3071
+ if (lastIsRow && tableStart === null) {
3072
+ tableStart = lineStart;
3073
+ tableRows = 0;
3074
+ } else if (!lastIsRow && line !== "") {
3075
+ tableStart = null;
3076
+ tableRows = 0;
3077
+ }
3078
+ if (tableStart !== null && tableRows < 2) return tableStart;
3079
+ }
3080
+ if (caughtUp) return text.length;
3081
+ if (fenceStart !== null) return fenceStart;
3082
+ if (TABLE_ROW.test(line)) return lineStart;
3083
+ if (EMPTY_BLOCK_MARKER.test(line)) return lineStart;
3084
+ const cut = lineStart + safeInlineEnd(line);
3085
+ return isHighSurrogate(text.charCodeAt(cut - 1)) ? cut - 1 : cut;
3086
+ }
3087
+ function prefersReducedMotion() {
3088
+ if (typeof window === "undefined") return true;
3089
+ try {
3090
+ return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
3091
+ } catch {
3092
+ return false;
3093
+ }
3094
+ }
3095
+ function useProgressiveReveal(text, settled) {
3096
+ const [, setFrame] = useState(0);
3097
+ const [reduced] = useState(prefersReducedMotion);
3098
+ const immediate = settled || reduced;
3099
+ const textRef = useRef(text);
3100
+ const cursorRef = useRef(immediate ? text.length : 0);
3101
+ const cutRef = useRef(immediate ? text.length : 0);
3102
+ const committedRef = useRef(0);
3103
+ if (text !== textRef.current) {
3104
+ const revealed = textRef.current.slice(0, Math.floor(cursorRef.current));
3105
+ if (revealed !== "" && !text.startsWith(revealed)) {
3106
+ cursorRef.current = text.length;
3107
+ }
3108
+ cursorRef.current = Math.min(cursorRef.current, text.length);
3109
+ cutRef.current = Math.min(cutRef.current, text.length);
3110
+ textRef.current = text;
3111
+ }
3112
+ if (immediate) cursorRef.current = text.length;
3113
+ const cut = immediate ? text.length : Math.max(
3114
+ cutRef.current,
3115
+ revealBoundary(text, Math.floor(cursorRef.current))
3116
+ );
3117
+ cutRef.current = cut;
3118
+ useEffect(() => {
3119
+ if (immediate) return;
3120
+ if (cursorRef.current >= text.length) return;
3121
+ let cancelled = false;
3122
+ let cancelStep = null;
3123
+ let previous = now();
3124
+ const commit = (at, force) => {
3125
+ if (!force && at - committedRef.current < COMMIT_INTERVAL_MS) return;
3126
+ committedRef.current = at;
3127
+ setFrame((value) => value + 1);
3128
+ };
3129
+ const step = () => {
3130
+ cancelStep = null;
3131
+ if (cancelled) return;
3132
+ const at = now();
3133
+ const elapsed = clamp(at - previous, 0, MAX_STEP_MS);
3134
+ previous = at;
3135
+ const length = textRef.current.length;
3136
+ const pending = length - cursorRef.current;
3137
+ if (pending <= 0) {
3138
+ commit(at, true);
3139
+ return;
3140
+ }
3141
+ const perSecond = clamp(
3142
+ pending / (DRAIN_TARGET_MS / 1e3),
3143
+ MIN_CHARS_PER_SECOND,
3144
+ MAX_CHARS_PER_SECOND
3145
+ );
3146
+ cursorRef.current = Math.min(
3147
+ length,
3148
+ cursorRef.current + perSecond * elapsed / 1e3
3149
+ );
3150
+ const done = cursorRef.current >= length;
3151
+ commit(at, done);
3152
+ if (!done) schedule();
3153
+ };
3154
+ const schedule = () => {
3155
+ if (typeof window.requestAnimationFrame === "function") {
3156
+ const id2 = window.requestAnimationFrame(step);
3157
+ cancelStep = () => window.cancelAnimationFrame(id2);
3158
+ return;
3159
+ }
3160
+ const id = setTimeout(step, FALLBACK_STEP_MS);
3161
+ cancelStep = () => clearTimeout(id);
3162
+ };
3163
+ schedule();
3164
+ return () => {
3165
+ cancelled = true;
3166
+ cancelStep?.();
3167
+ };
3168
+ }, [text, immediate]);
3169
+ return text.slice(0, cut);
3170
+ }
2292
3171
  function getRecognitionCtor() {
2293
3172
  if (typeof window === "undefined") return null;
2294
3173
  const w = window;
@@ -2507,6 +3386,141 @@ function useSpeechInput({
2507
3386
  );
2508
3387
  return { supported, listening, interim, error, start, stop, toggle };
2509
3388
  }
3389
+ var SILENT_FAILURE_MS = 1500;
3390
+ function speechPlatform() {
3391
+ if (typeof window === "undefined") return null;
3392
+ const scope = window;
3393
+ const synth = scope.speechSynthesis;
3394
+ const Utterance = scope.SpeechSynthesisUtterance;
3395
+ if (!synth || typeof synth.speak !== "function") return null;
3396
+ if (typeof Utterance !== "function") return null;
3397
+ return { synth, Utterance };
3398
+ }
3399
+ function normaliseTag(tag) {
3400
+ return tag.trim().toLowerCase().replace(/_/g, "-");
3401
+ }
3402
+ function primaryOf(tag) {
3403
+ return normaliseTag(tag).split("-")[0] ?? "";
3404
+ }
3405
+ function pickVoice(voices, lang) {
3406
+ const wanted = normaliseTag(lang);
3407
+ if (wanted === "") return null;
3408
+ const primary = primaryOf(wanted);
3409
+ const exact = voices.filter((voice) => normaliseTag(voice.lang) === wanted);
3410
+ const related = voices.filter((voice) => primaryOf(voice.lang) === primary);
3411
+ return exact.find((voice) => voice.default) ?? exact[0] ?? related.find((voice) => voice.default) ?? related[0] ?? null;
3412
+ }
3413
+ var activeOwner = null;
3414
+ var activeStop = null;
3415
+ function stopSpeaking() {
3416
+ activeStop?.();
3417
+ }
3418
+ function useSpeechOutput({
3419
+ lang
3420
+ } = {}) {
3421
+ const [available, setAvailable] = useState(() => speechPlatform() !== null);
3422
+ const [speaking, setSpeaking] = useState(false);
3423
+ const utteranceRef = useRef(null);
3424
+ const watchdogRef = useRef(null);
3425
+ const ownerRef = useRef({});
3426
+ const stopRef = useRef(() => void 0);
3427
+ const langRef = useRef(lang);
3428
+ langRef.current = lang;
3429
+ const clearWatchdog = useCallback(() => {
3430
+ if (watchdogRef.current === null) return;
3431
+ clearTimeout(watchdogRef.current);
3432
+ watchdogRef.current = null;
3433
+ }, []);
3434
+ const settle = useCallback(() => {
3435
+ clearWatchdog();
3436
+ utteranceRef.current = null;
3437
+ if (activeOwner === ownerRef.current) {
3438
+ activeOwner = null;
3439
+ activeStop = null;
3440
+ }
3441
+ setSpeaking(false);
3442
+ }, [clearWatchdog]);
3443
+ const stop = useCallback(() => {
3444
+ const owned = utteranceRef.current !== null;
3445
+ settle();
3446
+ if (owned) speechPlatform()?.synth.cancel();
3447
+ }, [settle]);
3448
+ stopRef.current = stop;
3449
+ const speak = useCallback(
3450
+ (text) => {
3451
+ const platform = speechPlatform();
3452
+ if (!platform) {
3453
+ setAvailable(false);
3454
+ return;
3455
+ }
3456
+ const spoken = text.trim();
3457
+ if (spoken === "") return;
3458
+ if (activeStop && activeOwner !== ownerRef.current) activeStop();
3459
+ platform.synth.cancel();
3460
+ const utterance = new platform.Utterance(spoken);
3461
+ const wanted = langRef.current;
3462
+ if (wanted !== void 0 && wanted !== "") {
3463
+ const voice = pickVoice(platform.synth.getVoices(), wanted);
3464
+ utterance.lang = voice?.lang ?? wanted;
3465
+ if (voice) utterance.voice = voice;
3466
+ }
3467
+ const mine = () => utteranceRef.current === utterance;
3468
+ utterance.onstart = () => {
3469
+ if (!mine()) return;
3470
+ clearWatchdog();
3471
+ setSpeaking(true);
3472
+ };
3473
+ utterance.onend = () => {
3474
+ if (!mine()) return;
3475
+ settle();
3476
+ };
3477
+ utterance.onerror = (event) => {
3478
+ if (!mine()) return;
3479
+ const code = typeof event?.error === "string" ? event.error : "";
3480
+ settle();
3481
+ if (code !== "interrupted" && code !== "canceled") setAvailable(false);
3482
+ };
3483
+ utteranceRef.current = utterance;
3484
+ activeOwner = ownerRef.current;
3485
+ activeStop = () => {
3486
+ stopRef.current();
3487
+ };
3488
+ setSpeaking(true);
3489
+ platform.synth.speak(utterance);
3490
+ clearWatchdog();
3491
+ watchdogRef.current = setTimeout(() => {
3492
+ watchdogRef.current = null;
3493
+ if (!mine()) return;
3494
+ if (platform.synth.speaking) return;
3495
+ settle();
3496
+ setAvailable(false);
3497
+ }, SILENT_FAILURE_MS);
3498
+ },
3499
+ [clearWatchdog, settle]
3500
+ );
3501
+ const toggle = useCallback(
3502
+ (text) => {
3503
+ if (speaking) stop();
3504
+ else speak(text);
3505
+ },
3506
+ [speak, speaking, stop]
3507
+ );
3508
+ useEffect(() => {
3509
+ if (!speaking || typeof window === "undefined") return;
3510
+ const onHide = () => {
3511
+ stopRef.current();
3512
+ };
3513
+ window.addEventListener("pagehide", onHide);
3514
+ return () => window.removeEventListener("pagehide", onHide);
3515
+ }, [speaking]);
3516
+ useEffect(
3517
+ () => () => {
3518
+ stopRef.current();
3519
+ },
3520
+ []
3521
+ );
3522
+ return { supported: available, speaking, speak, stop, toggle };
3523
+ }
2510
3524
  var SWIPE_THRESHOLD = 40;
2511
3525
  function useSwipe({
2512
3526
  onSwipeLeft,
@@ -2612,9 +3626,6 @@ function PageHead({
2612
3626
  ))
2613
3627
  ] });
2614
3628
  }
2615
- function cn(...inputs) {
2616
- return twMerge(clsx(inputs));
2617
- }
2618
3629
  var buttonVariants = cva(
2619
3630
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
2620
3631
  {
@@ -2872,34 +3883,7 @@ var EXPLORE_CSS = `
2872
3883
  }
2873
3884
  .boff-explore-fade { animation: boff-explore-fade-in 220ms ease-out; }
2874
3885
  @media (prefers-reduced-motion: reduce) {
2875
- .boff-explore-fade { animation: none; }
2876
- }
2877
-
2878
- /* Nothing inside an answer may widen the page. Model output carries long URLs, code and
2879
- tables \u2014 each of those overflows a phone by default, and one overflowing child makes the
2880
- WHOLE document horizontally scrollable, not just the bubble. */
2881
- .boff-explore-body,
2882
- .boff-explore-body p,
2883
- .boff-explore-body li,
2884
- .boff-explore-body a,
2885
- .boff-explore-body h1,
2886
- .boff-explore-body h2,
2887
- .boff-explore-body h3 {
2888
- overflow-wrap: anywhere;
2889
- word-break: break-word;
2890
- }
2891
- .boff-explore-body pre {
2892
- overflow-x: auto;
2893
- max-width: 100%;
2894
- }
2895
- .boff-explore-body table {
2896
- display: block;
2897
- overflow-x: auto;
2898
- max-width: 100%;
2899
- }
2900
- .boff-explore-body img {
2901
- max-width: 100%;
2902
- height: auto;
3886
+ .boff-explore-fade { animation: none; }
2903
3887
  }
2904
3888
 
2905
3889
  /* ---- Ambient backdrop -----------------------------------------------------------------
@@ -2958,11 +3942,40 @@ var EXPLORE_CSS = `
2958
3942
  animation: boff-explore-sheen 1.5s ease-in-out infinite;
2959
3943
  }
2960
3944
 
3945
+ /* ---- The composer, before anyone has typed -----------------------------------------
3946
+ On the welcome screen the box IS the page: everything above it is a way of explaining
3947
+ what to put in it. Unhighlighted it reads as a search bar at the bottom of a landing
3948
+ page, which people scroll past \u2014 so while the conversation is empty it gets a soft halo
3949
+ in the site's own --primary and one slow breath of it, and the moment there is a
3950
+ transcript, or the visitor focuses or types, it drops back to the ordinary card it was.
3951
+ An input that keeps pulsing while you are using it is nagging, not inviting.
3952
+
3953
+ Colour comes from the product's --primary token, so it is whatever that site's palette
3954
+ already is, never a colour chosen here. */
3955
+ .boff-explore-invite {
3956
+ border-color: hsl(var(--primary) / 0.45);
3957
+ box-shadow:
3958
+ 0 0 0 4px hsl(var(--primary) / 0.08),
3959
+ 0 8px 24px -12px hsl(var(--primary) / 0.45);
3960
+ animation: boff-explore-breathe 3.2s ease-in-out infinite;
3961
+ }
3962
+ /* Focus and typing both mean "I have found it", and the hint has done its job. */
3963
+ .boff-explore-invite:focus-within,
3964
+ .boff-explore-invite.boff-explore-invite-quiet {
3965
+ animation: none;
3966
+ box-shadow: 0 0 0 4px hsl(var(--primary) / 0.10);
3967
+ }
3968
+ @keyframes boff-explore-breathe {
3969
+ 0%, 100% { box-shadow: 0 0 0 4px hsl(var(--primary) / 0.07), 0 8px 24px -12px hsl(var(--primary) / 0.40); }
3970
+ 50% { box-shadow: 0 0 0 7px hsl(var(--primary) / 0.13), 0 8px 28px -12px hsl(var(--primary) / 0.55); }
3971
+ }
3972
+
2961
3973
  /* Every one of the above is decoration. Reduced motion removes the movement and keeps the
2962
3974
  final state, rather than leaving content mid-animation and invisible. */
2963
3975
  @media (prefers-reduced-motion: reduce) {
2964
3976
  .boff-explore-aura::before,
2965
- .boff-explore-busy::after { animation: none; }
3977
+ .boff-explore-busy::after,
3978
+ .boff-explore-invite { animation: none; }
2966
3979
  .boff-explore-rise { animation: none; opacity: 1; transform: none; }
2967
3980
  }
2968
3981
 
@@ -3073,7 +4086,7 @@ function isOwnElement(el, root) {
3073
4086
  function visualViewportHeight() {
3074
4087
  return window.visualViewport?.height ?? window.innerHeight;
3075
4088
  }
3076
- function clamp(value, min, max) {
4089
+ function clamp2(value, min, max) {
3077
4090
  return Math.min(Math.max(value, min), max);
3078
4091
  }
3079
4092
  function isRestoredNavigation() {
@@ -3110,34 +4123,6 @@ function logoUrlOf(url) {
3110
4123
  return null;
3111
4124
  }
3112
4125
  }
3113
- function inlineImageOrigins(products) {
3114
- const origins = /* @__PURE__ */ new Set();
3115
- for (const product of products) {
3116
- let url;
3117
- try {
3118
- url = new URL(product.website);
3119
- } catch {
3120
- continue;
3121
- }
3122
- if (url.protocol !== "https:") continue;
3123
- const apex = url.hostname.replace(/^www\./, "");
3124
- const port = url.port ? `:${url.port}` : "";
3125
- origins.add(`https://${apex}${port}`);
3126
- origins.add(`https://www.${apex}${port}`);
3127
- }
3128
- return origins;
3129
- }
3130
- function isInlineImageAllowed(src, origins) {
3131
- if (typeof src !== "string" || src === "") return false;
3132
- let url;
3133
- try {
3134
- url = new URL(src);
3135
- } catch {
3136
- return false;
3137
- }
3138
- if (url.protocol !== "https:" || url.username || url.password) return false;
3139
- return origins.has(url.origin);
3140
- }
3141
4126
  var REFERENCE_KIND_ORDER = [
3142
4127
  "CONCEPT",
3143
4128
  "DOCS",
@@ -3222,97 +4207,6 @@ function summariseReferences(references, origins, productNames) {
3222
4207
  products
3223
4208
  };
3224
4209
  }
3225
- var CODE_FENCE = /^\s*(?:```|~~~)/;
3226
- function answerPlainText(markdown) {
3227
- const lines = [];
3228
- let inFence = false;
3229
- for (const raw of markdown.split("\n")) {
3230
- if (CODE_FENCE.test(raw)) {
3231
- inFence = !inFence;
3232
- continue;
3233
- }
3234
- if (inFence) {
3235
- lines.push(raw);
3236
- continue;
3237
- }
3238
- let line = raw;
3239
- line = line.replace(/!\[[^\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g, "");
3240
- line = line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1 ($2)");
3241
- line = line.replace(/<(https?:\/\/[^>\s]+)>/g, "$1");
3242
- line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
3243
- line = line.replace(/^\s{0,3}>\s?/, "");
3244
- if (/^\s{0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/.test(line)) {
3245
- lines.push("");
3246
- continue;
3247
- }
3248
- line = line.replace(/`([^`]+)`/g, "$1");
3249
- line = line.replace(/\*\*([^*]+)\*\*/g, "$1");
3250
- line = line.replace(/__([^_]+)__/g, "$1");
3251
- line = line.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, "$1$2");
3252
- line = line.replace(/(^|[^_\w])_([^_\n]+)_(?![_\w])/g, "$1$2");
3253
- lines.push(line.replace(/[ \t]+$/, ""));
3254
- }
3255
- return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
3256
- }
3257
- var InlineImageScopeContext = createContext(null);
3258
- var InsideLinkContext = createContext(false);
3259
- function AnswerLink(props) {
3260
- return /* @__PURE__ */ jsx(InsideLinkContext.Provider, { value: true, children: /* @__PURE__ */ jsx(MarkdownLink, { ...props }) });
3261
- }
3262
- var INLINE_IMAGE_FRAME = "relative inline-block max-w-full overflow-hidden rounded-xl border border-border bg-muted align-top shadow-sm";
3263
- function InlineConceptImage({ src, alt }) {
3264
- const scope = useContext(InlineImageScopeContext);
3265
- const insideLink = useContext(InsideLinkContext);
3266
- if (!scope || !scope.enabled || !isInlineImageAllowed(src, scope.origins) || scope.failed.has(src)) {
3267
- return null;
3268
- }
3269
- const { strings: T, onFailed } = scope;
3270
- const picture = /* @__PURE__ */ jsxs(Fragment, { children: [
3271
- /* @__PURE__ */ jsx(
3272
- "img",
3273
- {
3274
- src,
3275
- alt: alt ?? "",
3276
- loading: "lazy",
3277
- decoding: "async",
3278
- onError: () => onFailed(src),
3279
- className: "block h-auto max-h-80 w-auto max-w-full object-contain"
3280
- }
3281
- ),
3282
- /* @__PURE__ */ jsx(
3283
- "span",
3284
- {
3285
- "aria-hidden": "true",
3286
- className: "pointer-events-none absolute left-2 top-2 rounded-full border border-border bg-background/90 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-foreground shadow-sm",
3287
- children: T.conceptIllustration
3288
- }
3289
- )
3290
- ] });
3291
- return /* @__PURE__ */ jsx("span", { "data-boff-explore": "inline-image", className: "my-3 block", children: insideLink ? /* @__PURE__ */ jsx("span", { className: INLINE_IMAGE_FRAME, children: picture }) : /* @__PURE__ */ jsxs(
3292
- "a",
3293
- {
3294
- href: src,
3295
- target: "_blank",
3296
- rel: "noopener noreferrer",
3297
- className: cn(
3298
- INLINE_IMAGE_FRAME,
3299
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
3300
- ),
3301
- children: [
3302
- picture,
3303
- /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
3304
- "(",
3305
- T.opensInNewTab,
3306
- ")"
3307
- ] })
3308
- ]
3309
- }
3310
- ) });
3311
- }
3312
- var ANSWER_MARKDOWN_COMPONENTS = {
3313
- a: AnswerLink,
3314
- img: InlineConceptImage
3315
- };
3316
4210
  function ReferenceCard({
3317
4211
  reference,
3318
4212
  onFollow
@@ -3651,6 +4545,195 @@ function AnswerFeedback({
3651
4545
  }
3652
4546
  );
3653
4547
  }
4548
+ function EscalateForm({
4549
+ strings: T,
4550
+ formId,
4551
+ onEscalate,
4552
+ onClose,
4553
+ onBusyChange
4554
+ }) {
4555
+ const [email, setEmail] = useState("");
4556
+ const [note, setNote] = useState("");
4557
+ const [error, setError] = useState(null);
4558
+ const [sending, setSending] = useState(false);
4559
+ const [sentTo, setSentTo] = useState(null);
4560
+ const emailRef = useRef(null);
4561
+ const inFlightRef = useRef(false);
4562
+ const titleId = `${formId}-title`;
4563
+ const emailId = `${formId}-email`;
4564
+ const noteId = `${formId}-note`;
4565
+ const errorId = `${formId}-error`;
4566
+ useEffect(() => {
4567
+ emailRef.current?.focus();
4568
+ }, []);
4569
+ useEffect(() => {
4570
+ onBusyChange?.(sending);
4571
+ return () => onBusyChange?.(false);
4572
+ }, [sending, onBusyChange]);
4573
+ const handleSubmit = useCallback(
4574
+ async (event) => {
4575
+ event.preventDefault();
4576
+ if (inFlightRef.current) return;
4577
+ const address = email.trim();
4578
+ if (!looksLikeDeliverableEmail(address)) {
4579
+ setError(T.escalateInvalidEmail);
4580
+ emailRef.current?.focus();
4581
+ return;
4582
+ }
4583
+ inFlightRef.current = true;
4584
+ setSending(true);
4585
+ setError(null);
4586
+ const result = await onEscalate(address, note);
4587
+ inFlightRef.current = false;
4588
+ setSending(false);
4589
+ switch (result.outcome) {
4590
+ case "sent":
4591
+ case "already-handed-over":
4592
+ setSentTo(result.email ?? address);
4593
+ return;
4594
+ case "invalid-email":
4595
+ setError(T.escalateInvalidEmail);
4596
+ emailRef.current?.focus();
4597
+ return;
4598
+ case "rate-limited":
4599
+ setError(T.escalateRateLimited);
4600
+ return;
4601
+ case "captcha":
4602
+ setError(T.escalateCaptcha);
4603
+ return;
4604
+ // `no-conversation` included: the form should not have been offered at all, and the
4605
+ // honest thing to show is that nothing was passed on.
4606
+ default:
4607
+ setError(T.escalateFailed);
4608
+ return;
4609
+ }
4610
+ },
4611
+ [T, email, note, onEscalate]
4612
+ );
4613
+ if (sentTo !== null) {
4614
+ return /* @__PURE__ */ jsx(
4615
+ "p",
4616
+ {
4617
+ id: formId,
4618
+ "data-boff-explore": "escalate-sent",
4619
+ role: "status",
4620
+ className: "boff-explore-fade mt-2 rounded-xl border border-border bg-card px-3 py-2 text-xs leading-relaxed text-card-foreground",
4621
+ children: fill(T.escalateSent, { email: sentTo })
4622
+ }
4623
+ );
4624
+ }
4625
+ return /* @__PURE__ */ jsxs(
4626
+ "form",
4627
+ {
4628
+ id: formId,
4629
+ "data-boff-explore": "escalate-form",
4630
+ "aria-labelledby": titleId,
4631
+ noValidate: true,
4632
+ onSubmit: handleSubmit,
4633
+ onKeyDown: (event) => {
4634
+ if (event.key !== "Escape") return;
4635
+ event.stopPropagation();
4636
+ if (sending) return;
4637
+ onClose();
4638
+ },
4639
+ className: "boff-explore-fade mt-2 space-y-2 rounded-xl border border-border bg-card p-3",
4640
+ children: [
4641
+ /* @__PURE__ */ jsx("p", { id: titleId, className: "text-xs font-medium text-card-foreground", children: T.escalateTitle }),
4642
+ /* @__PURE__ */ jsx("p", { className: "text-xs leading-relaxed text-muted-foreground", children: T.escalateBody }),
4643
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
4644
+ /* @__PURE__ */ jsx(
4645
+ "label",
4646
+ {
4647
+ htmlFor: emailId,
4648
+ className: "block text-xs font-medium text-card-foreground",
4649
+ children: T.escalateEmailLabel
4650
+ }
4651
+ ),
4652
+ /* @__PURE__ */ jsx(
4653
+ "input",
4654
+ {
4655
+ ref: emailRef,
4656
+ id: emailId,
4657
+ name: "email",
4658
+ "data-boff-explore": "escalate-email",
4659
+ type: "email",
4660
+ inputMode: "email",
4661
+ autoComplete: "email",
4662
+ required: true,
4663
+ value: email,
4664
+ onChange: (event) => {
4665
+ setEmail(event.target.value);
4666
+ if (error) setError(null);
4667
+ },
4668
+ "aria-invalid": error ? "true" : void 0,
4669
+ "aria-describedby": error ? errorId : void 0,
4670
+ placeholder: T.escalateEmailPlaceholder,
4671
+ className: "w-full rounded-md border border-border bg-background px-2 py-1 text-base text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring sm:text-sm"
4672
+ }
4673
+ )
4674
+ ] }),
4675
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
4676
+ /* @__PURE__ */ jsx(
4677
+ "label",
4678
+ {
4679
+ htmlFor: noteId,
4680
+ className: "block text-xs font-medium text-card-foreground",
4681
+ children: T.escalateNoteLabel
4682
+ }
4683
+ ),
4684
+ /* @__PURE__ */ jsx(
4685
+ Textarea,
4686
+ {
4687
+ id: noteId,
4688
+ name: "note",
4689
+ "data-boff-explore": "escalate-note",
4690
+ rows: 2,
4691
+ value: note,
4692
+ onChange: (event) => setNote(event.target.value),
4693
+ maxLength: EXPLORE_ESCALATION_NOTE_SOFT_MAX,
4694
+ className: "min-h-0 border-border bg-background"
4695
+ }
4696
+ )
4697
+ ] }),
4698
+ error ? /* @__PURE__ */ jsx(
4699
+ "p",
4700
+ {
4701
+ id: errorId,
4702
+ "data-boff-explore": "escalate-error",
4703
+ role: "alert",
4704
+ className: "text-xs leading-relaxed text-destructive",
4705
+ children: error
4706
+ }
4707
+ ) : null,
4708
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
4709
+ /* @__PURE__ */ jsx(
4710
+ Button,
4711
+ {
4712
+ "data-boff-explore": "escalate-send",
4713
+ type: "submit",
4714
+ size: "sm",
4715
+ disabled: sending,
4716
+ children: sending ? T.escalateSending : T.escalateSubmit
4717
+ }
4718
+ ),
4719
+ /* @__PURE__ */ jsx(
4720
+ Button,
4721
+ {
4722
+ "data-boff-explore": "escalate-cancel",
4723
+ type: "button",
4724
+ size: "sm",
4725
+ variant: "ghost",
4726
+ className: "text-muted-foreground",
4727
+ disabled: sending,
4728
+ onClick: onClose,
4729
+ children: T.escalateCancel
4730
+ }
4731
+ )
4732
+ ] })
4733
+ ]
4734
+ }
4735
+ );
4736
+ }
3654
4737
  var PROVENANCE_CHIP = "inline-flex max-w-full items-center gap-1 rounded-full border border-border bg-muted/60 px-2 py-0.5 text-[11px] leading-5 text-muted-foreground";
3655
4738
  function ProvenanceChips({
3656
4739
  strings: T,
@@ -3700,9 +4783,42 @@ function ProvenanceChips({
3700
4783
  );
3701
4784
  }
3702
4785
  var COPY_FEEDBACK_MS = 2500;
4786
+ 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";
4787
+ var ANSWER_ACTION_IDLE = "border-border bg-card text-muted-foreground hover:border-primary/40 hover:bg-accent hover:text-foreground";
4788
+ var ANSWER_ACTION_ACTIVE = "border-primary/40 bg-accent text-primary";
4789
+ function ReadAloudButton({
4790
+ strings: T,
4791
+ text,
4792
+ lang
4793
+ }) {
4794
+ const speech = useSpeechOutput({ lang });
4795
+ if (!speech.supported) return null;
4796
+ const label = speech.speaking ? T.stopReading : T.readAloud;
4797
+ return /* @__PURE__ */ jsxs(
4798
+ "button",
4799
+ {
4800
+ "data-boff-explore": "read-aloud",
4801
+ "data-speaking": speech.speaking ? "true" : "false",
4802
+ type: "button",
4803
+ title: label,
4804
+ "aria-pressed": speech.speaking,
4805
+ onClick: () => speech.toggle(text),
4806
+ className: cn(
4807
+ ANSWER_ACTION_PILL,
4808
+ speech.speaking ? ANSWER_ACTION_ACTIVE : ANSWER_ACTION_IDLE
4809
+ ),
4810
+ children: [
4811
+ /* @__PURE__ */ jsx(Volume2, { className: "h-3 w-3 shrink-0", "aria-hidden": "true" }),
4812
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: label })
4813
+ ]
4814
+ }
4815
+ );
4816
+ }
3703
4817
  function AnswerActions({
3704
4818
  strings: T,
3705
- text
4819
+ text,
4820
+ speechText,
4821
+ speechLang
3706
4822
  }) {
3707
4823
  const [outcome, setOutcome] = useState("idle");
3708
4824
  const resetRef = useRef(null);
@@ -3745,9 +4861,8 @@ function AnswerActions({
3745
4861
  void copy();
3746
4862
  },
3747
4863
  className: cn(
3748
- "inline-flex max-w-full items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs transition-colors",
3749
- "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
3750
- outcome === "failed" ? "border-destructive/40 bg-card text-destructive" : "border-border bg-card text-muted-foreground hover:border-primary/40 hover:bg-accent hover:text-foreground"
4864
+ ANSWER_ACTION_PILL,
4865
+ outcome === "failed" ? "border-destructive/40 bg-card text-destructive" : ANSWER_ACTION_IDLE
3751
4866
  ),
3752
4867
  children: [
3753
4868
  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 +4870,7 @@ function AnswerActions({
3755
4870
  ]
3756
4871
  }
3757
4872
  ),
4873
+ /* @__PURE__ */ jsx(ReadAloudButton, { strings: T, text: speechText, lang: speechLang }),
3758
4874
  /* @__PURE__ */ jsx("span", { "data-boff-explore": "copy-status", role: "status", className: "sr-only", children: outcome === "idle" ? "" : label })
3759
4875
  ]
3760
4876
  }
@@ -3806,34 +4922,20 @@ function AssistantBubble({
3806
4922
  activity,
3807
4923
  imageOrigins,
3808
4924
  productNames,
4925
+ speechLang,
3809
4926
  onNavigate,
3810
4927
  anchorRef,
3811
4928
  onFollow,
3812
4929
  onFollowUp,
3813
- onFeedback
4930
+ onFeedback,
4931
+ onEscalate,
4932
+ isLatestAnswer = false
3814
4933
  }) {
3815
4934
  const streaming = message.status === "PENDING" || message.status === "RUNNING";
3816
4935
  const body = message.content || message.partialContent || "";
3817
4936
  const failed = message.status === "FAILED";
3818
4937
  const completed = message.status === "COMPLETED";
3819
- const [failedImages, setFailedImages] = useState(
3820
- () => /* @__PURE__ */ new Set()
3821
- );
3822
- const markImageFailed = useCallback((src) => {
3823
- setFailedImages(
3824
- (current) => current.has(src) ? current : new Set(current).add(src)
3825
- );
3826
- }, []);
3827
- const imageScope = useMemo(
3828
- () => ({
3829
- strings: T,
3830
- enabled: completed,
3831
- origins: imageOrigins,
3832
- failed: failedImages,
3833
- onFailed: markImageFailed
3834
- }),
3835
- [T, completed, imageOrigins, failedImages, markImageFailed]
3836
- );
4938
+ const visible = useProgressiveReveal(body, !streaming);
3837
4939
  const provenance = useMemo(
3838
4940
  () => summariseReferences(message.references, imageOrigins, productNames),
3839
4941
  [message.references, imageOrigins, productNames]
@@ -3842,7 +4944,25 @@ function AssistantBubble({
3842
4944
  () => answerPlainText(message.content),
3843
4945
  [message.content]
3844
4946
  );
4947
+ const speechText = useMemo(
4948
+ () => answerSpeechText(message.content),
4949
+ [message.content]
4950
+ );
3845
4951
  const grounded = completed && message.answered !== false;
4952
+ const [escalateOpen, setEscalateOpen] = useState(false);
4953
+ const [escalateBusy, setEscalateBusy] = useState(false);
4954
+ const escalateTriggerRef = useRef(null);
4955
+ const escalateFormId = `boff-explore-escalate-${message.id}`;
4956
+ const unanswered = completed && message.answered === false;
4957
+ const offerEscalate = Boolean(onEscalate) && completed;
4958
+ const showQuietEscalate = offerEscalate && !unanswered && isLatestAnswer;
4959
+ const closeEscalate = useCallback(() => {
4960
+ setEscalateOpen(false);
4961
+ escalateTriggerRef.current?.focus();
4962
+ }, []);
4963
+ const handleEscalateBusy = useCallback((busy) => {
4964
+ setEscalateBusy(busy);
4965
+ }, []);
3846
4966
  return /* @__PURE__ */ jsxs(
3847
4967
  "div",
3848
4968
  {
@@ -3860,7 +4980,7 @@ function AssistantBubble({
3860
4980
  }
3861
4981
  ),
3862
4982
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-3", children: [
3863
- message.status === "COMPLETED" && message.answered === false ? /* @__PURE__ */ jsxs(
4983
+ unanswered ? /* @__PURE__ */ jsxs(
3864
4984
  "div",
3865
4985
  {
3866
4986
  "data-boff-explore": "no-answer",
@@ -3870,26 +4990,56 @@ function AssistantBubble({
3870
4990
  /* @__PURE__ */ jsxs("span", { children: [
3871
4991
  T.noAnswer,
3872
4992
  " ",
3873
- /* @__PURE__ */ jsx(
3874
- "a",
3875
- {
3876
- href: "/contact",
3877
- className: "underline underline-offset-2 hover:text-primary",
3878
- onClick: (event) => {
3879
- if (!onNavigate) return;
3880
- event.preventDefault();
3881
- onNavigate("/contact");
3882
- },
3883
- children: T.askHuman
3884
- }
4993
+ offerEscalate ? (
4994
+ /* The point of the wave: the same words, but they now open a form that
4995
+ sends the conversation we are both looking at, instead of a link to a
4996
+ blank contact page that asks the visitor to type it all again. */
4997
+ /* @__PURE__ */ jsx(
4998
+ "button",
4999
+ {
5000
+ ref: escalateTriggerRef,
5001
+ "data-boff-explore": "escalate-open",
5002
+ type: "button",
5003
+ "aria-expanded": escalateOpen,
5004
+ "aria-controls": escalateOpen ? escalateFormId : void 0,
5005
+ onClick: () => setEscalateOpen(true),
5006
+ className: "rounded-sm underline underline-offset-2 hover:text-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
5007
+ children: T.askHuman
5008
+ }
5009
+ )
5010
+ ) : (
5011
+ /* No conversation to hand over (a shared, read-only view). The old dead end
5012
+ is still better than a control whose submit could only fail. */
5013
+ /* @__PURE__ */ jsx(
5014
+ "a",
5015
+ {
5016
+ href: "/contact",
5017
+ className: "underline underline-offset-2 hover:text-primary",
5018
+ onClick: (event) => {
5019
+ if (!onNavigate) return;
5020
+ event.preventDefault();
5021
+ onNavigate("/contact");
5022
+ },
5023
+ children: T.askHuman
5024
+ }
5025
+ )
3885
5026
  ),
3886
5027
  "."
3887
5028
  ] })
3888
5029
  ]
3889
5030
  }
3890
5031
  ) : null,
3891
- body ? /* @__PURE__ */ jsxs("div", { className: "rounded-2xl rounded-tl-md border border-border bg-card px-4 py-3 shadow-sm", children: [
3892
- /* @__PURE__ */ jsx("div", { className: "boff-prose boff-prose-sm boff-explore-body min-w-0 text-card-foreground", children: /* @__PURE__ */ jsx(InlineImageScopeContext.Provider, { value: imageScope, children: /* @__PURE__ */ jsx(Markdown, { components: ANSWER_MARKDOWN_COMPONENTS, children: body }) }) }),
5032
+ visible ? /* @__PURE__ */ jsxs("div", { className: "rounded-2xl rounded-tl-md border border-border bg-card px-4 py-3 shadow-sm", children: [
5033
+ /* @__PURE__ */ jsx(
5034
+ ExploreAnswerBody,
5035
+ {
5036
+ strings: T,
5037
+ imageOrigins,
5038
+ enabled: completed,
5039
+ className: "boff-prose-sm text-card-foreground",
5040
+ children: visible
5041
+ }
5042
+ ),
3893
5043
  streaming ? /* @__PURE__ */ jsx("span", { className: "boff-explore-caret", "aria-hidden": "true" }) : null
3894
5044
  ] }) : null,
3895
5045
  grounded && provenance.total > 0 ? /* @__PURE__ */ jsx(ProvenanceChips, { strings: T, provenance }) : null,
@@ -3932,7 +5082,15 @@ function AssistantBubble({
3932
5082
  question
3933
5083
  )) })
3934
5084
  ] }) : null,
3935
- completed && copyText !== "" ? /* @__PURE__ */ jsx(AnswerActions, { strings: T, text: copyText }) : null,
5085
+ completed && copyText !== "" ? /* @__PURE__ */ jsx(
5086
+ AnswerActions,
5087
+ {
5088
+ strings: T,
5089
+ text: copyText,
5090
+ speechText,
5091
+ speechLang
5092
+ }
5093
+ ) : null,
3936
5094
  completed && onFeedback ? /* @__PURE__ */ jsx(
3937
5095
  AnswerFeedback,
3938
5096
  {
@@ -3941,6 +5099,30 @@ function AssistantBubble({
3941
5099
  feedback: message.feedback,
3942
5100
  onSubmit: onFeedback
3943
5101
  }
5102
+ ) : null,
5103
+ showQuietEscalate ? /* @__PURE__ */ jsx("div", { className: "pt-0.5", children: /* @__PURE__ */ jsx(
5104
+ "button",
5105
+ {
5106
+ ref: escalateTriggerRef,
5107
+ "data-boff-explore": "escalate-open",
5108
+ type: "button",
5109
+ "aria-expanded": escalateOpen,
5110
+ "aria-controls": escalateOpen ? escalateFormId : void 0,
5111
+ disabled: escalateBusy,
5112
+ onClick: () => setEscalateOpen((open) => !open),
5113
+ className: "rounded-sm text-xs text-muted-foreground underline underline-offset-2 hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
5114
+ children: T.escalateOpen
5115
+ }
5116
+ ) }) : null,
5117
+ escalateOpen && onEscalate ? /* @__PURE__ */ jsx(
5118
+ EscalateForm,
5119
+ {
5120
+ strings: T,
5121
+ formId: escalateFormId,
5122
+ onEscalate,
5123
+ onClose: closeEscalate,
5124
+ onBusyChange: handleEscalateBusy
5125
+ }
3944
5126
  ) : null
3945
5127
  ] })
3946
5128
  ]
@@ -4380,15 +5562,6 @@ function WelcomeState({
4380
5562
  }
4381
5563
  )
4382
5564
  ] }) : null,
4383
- /* @__PURE__ */ jsx(
4384
- ConceptStrip,
4385
- {
4386
- strings: T,
4387
- highlights: conceptHighlights,
4388
- onAsk: onPrompt,
4389
- disabled
4390
- }
4391
- ),
4392
5565
  products.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "boff-explore-rise boff-explore-rise-3 mt-6", children: [
4393
5566
  /* @__PURE__ */ jsx("p", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: T.focusProduct }),
4394
5567
  /* @__PURE__ */ jsx(
@@ -4400,7 +5573,16 @@ function WelcomeState({
4400
5573
  onFocus
4401
5574
  }
4402
5575
  )
4403
- ] }) : null
5576
+ ] }) : null,
5577
+ /* @__PURE__ */ jsx(
5578
+ ConceptStrip,
5579
+ {
5580
+ strings: T,
5581
+ highlights: conceptHighlights,
5582
+ onAsk: onPrompt,
5583
+ disabled
5584
+ }
5585
+ )
4404
5586
  ] });
4405
5587
  }
4406
5588
  function ExplorePage({
@@ -4426,6 +5608,7 @@ function ExplorePage({
4426
5608
  );
4427
5609
  const T = useMemo(() => exploreStrings(activeLocale), [activeLocale]);
4428
5610
  const rtl = isRtlLocale(activeLocale);
5611
+ const speechLang = speechLocaleOf(activeLocale);
4429
5612
  const {
4430
5613
  phase,
4431
5614
  catalog,
@@ -4446,6 +5629,8 @@ function ExplorePage({
4446
5629
  clearHistory,
4447
5630
  recordClick,
4448
5631
  submitFeedback,
5632
+ escalate,
5633
+ canEscalate,
4449
5634
  createShareLink,
4450
5635
  isSharedView
4451
5636
  } = useExploreChat({
@@ -4530,11 +5715,11 @@ function ExplorePage({
4530
5715
  const layoutHeight = window.innerHeight;
4531
5716
  if (layoutHeight === 0) return;
4532
5717
  const top = Math.round(
4533
- clamp(el.getBoundingClientRect().top, 0, layoutHeight)
5718
+ clamp2(el.getBoundingClientRect().top, 0, layoutHeight)
4534
5719
  );
4535
5720
  const viewport = window.visualViewport;
4536
5721
  const inset = viewport ? Math.round(
4537
- clamp(
5722
+ clamp2(
4538
5723
  layoutHeight - viewport.height - viewport.offsetTop,
4539
5724
  0,
4540
5725
  layoutHeight
@@ -4815,6 +6000,7 @@ function ExplorePage({
4815
6000
  const text = draft.trim();
4816
6001
  if (!text || overLimit || busy || !canSend) return;
4817
6002
  stopDictation();
6003
+ stopSpeaking();
4818
6004
  setDraft("");
4819
6005
  stickToBottomRef.current = true;
4820
6006
  void send(text, toPayload(stagedRef.current));
@@ -4825,6 +6011,7 @@ function ExplorePage({
4825
6011
  const sendPrompt = useCallback(
4826
6012
  (prompt) => {
4827
6013
  if (!canSend || busy) return;
6014
+ stopSpeaking();
4828
6015
  setDraft("");
4829
6016
  stickToBottomRef.current = true;
4830
6017
  void send(prompt, toPayload(stagedRef.current));
@@ -4887,6 +6074,7 @@ function ExplorePage({
4887
6074
  ),
4888
6075
  /* @__PURE__ */ jsx(ProseStyles, {}),
4889
6076
  /* @__PURE__ */ jsx(ExploreStyles, {}),
6077
+ /* @__PURE__ */ jsx(ExploreAnswerStyles, {}),
4890
6078
  !showWelcome ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 border-b border-border px-4 py-2.5", children: [
4891
6079
  /* @__PURE__ */ jsx(
4892
6080
  "span",
@@ -5060,11 +6248,14 @@ function ExplorePage({
5060
6248
  activity,
5061
6249
  imageOrigins,
5062
6250
  productNames,
6251
+ speechLang,
5063
6252
  onNavigate,
5064
6253
  anchorRef: message.id === latestAssistantId ? latestAssistantRef : void 0,
5065
6254
  onFollow: (kind, url) => recordClick(message.id, kind, url),
5066
6255
  onFollowUp: sendPrompt,
5067
- onFeedback: isSharedView ? void 0 : (rating, detail) => submitFeedback(message.id, rating, detail)
6256
+ onFeedback: isSharedView ? void 0 : (rating, detail) => submitFeedback(message.id, rating, detail),
6257
+ onEscalate: canEscalate ? escalate : void 0,
6258
+ isLatestAnswer: message.id === latestAssistantId
5068
6259
  },
5069
6260
  message.id
5070
6261
  )
@@ -5171,10 +6362,19 @@ function ExplorePage({
5171
6362
  /* @__PURE__ */ jsxs(
5172
6363
  "div",
5173
6364
  {
6365
+ "data-boff-explore": "composer-field",
5174
6366
  className: cn(
5175
6367
  "flex min-w-0 items-end gap-2 rounded-2xl border bg-card p-2 shadow-sm transition-all",
5176
6368
  busy && "boff-explore-busy",
5177
- overLimit ? "border-destructive/60" : "border-border focus-within:border-primary/40 focus-within:ring-1 focus-within:ring-ring"
6369
+ overLimit ? "border-destructive/60" : "border-border focus-within:border-primary/40 focus-within:ring-1 focus-within:ring-ring",
6370
+ // The halo is an invitation, and an invitation is only useful before it has
6371
+ // been accepted: while the conversation is empty, the composer is usable, and
6372
+ // nothing has been typed. Once any of those stops being true the class goes
6373
+ // and the box is an ordinary card again. `-quiet` keeps the ring but stops the
6374
+ // movement the moment there are characters in the field, so it does not pulse
6375
+ // at someone who is mid-sentence.
6376
+ showWelcome && !composerDisabled && !overLimit && "boff-explore-invite",
6377
+ showWelcome && draft.length > 0 && "boff-explore-invite-quiet"
5178
6378
  ),
5179
6379
  children: [
5180
6380
  /* @__PURE__ */ jsx(
@@ -5362,6 +6562,6 @@ function ExplorePage({
5362
6562
  );
5363
6563
  }
5364
6564
 
5365
- export { ExplorePage, answerPlainText, inlineImageOrigins, isInlineImageAllowed, logoUrlOf, productNamesByOrigin, referenceKindOf, summariseReferences };
6565
+ export { ExplorePage, answerPlainText, answerSpeechText, inlineImageOrigins, isInlineImageAllowed, logoUrlOf, productNamesByOrigin, referenceKindOf, summariseReferences };
5366
6566
  //# sourceMappingURL=explore.mjs.map
5367
6567
  //# sourceMappingURL=explore.mjs.map