@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.
@@ -6,11 +6,11 @@ var ReactMarkdown = require('react-markdown');
6
6
  var rehypeSanitize = require('rehype-sanitize');
7
7
  var remarkGfm = require('remark-gfm');
8
8
  var jsxRuntime = require('react/jsx-runtime');
9
+ var clsx = require('clsx');
10
+ var tailwindMerge = require('tailwind-merge');
9
11
  var reactHelmetAsync = require('react-helmet-async');
10
12
  var reactSlot = require('@radix-ui/react-slot');
11
13
  var classVarianceAuthority = require('class-variance-authority');
12
- var clsx = require('clsx');
13
- var tailwindMerge = require('tailwind-merge');
14
14
 
15
15
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
16
16
 
@@ -40,6 +40,472 @@ var remarkGfm__default = /*#__PURE__*/_interopDefault(remarkGfm);
40
40
  var __defProp = Object.defineProperty;
41
41
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
42
42
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
43
+ function MarkdownLink({
44
+ href,
45
+ children,
46
+ // react-markdown passes the hast node to every override; it is not a DOM attribute.
47
+ node: _node,
48
+ ...props
49
+ }) {
50
+ const isExternal = typeof href === "string" && (href.startsWith("http://") || href.startsWith("https://"));
51
+ return /* @__PURE__ */ jsxRuntime.jsx(
52
+ "a",
53
+ {
54
+ href,
55
+ ...props,
56
+ ...isExternal ? { target: "_blank", rel: "noopener noreferrer" } : { rel: "noopener noreferrer" },
57
+ children
58
+ }
59
+ );
60
+ }
61
+ var defaultComponents = {
62
+ a: MarkdownLink
63
+ };
64
+ function Markdown({
65
+ children,
66
+ className,
67
+ inline,
68
+ components
69
+ }) {
70
+ const content = /* @__PURE__ */ jsxRuntime.jsx(
71
+ ReactMarkdown__default.default,
72
+ {
73
+ remarkPlugins: [remarkGfm__default.default],
74
+ rehypePlugins: [rehypeSanitize__default.default],
75
+ components: { ...defaultComponents, ...components },
76
+ ...inline ? { disallowedElements: ["p"], unwrapDisallowed: true } : {},
77
+ children
78
+ }
79
+ );
80
+ if (inline) {
81
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className, children: content });
82
+ }
83
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: content });
84
+ }
85
+ function cn(...inputs) {
86
+ return tailwindMerge.twMerge(clsx.clsx(inputs));
87
+ }
88
+ var EXPLORE_ANSWER_CSS = `
89
+ /* Nothing inside an answer may widen the page. Model output carries long URLs, code and
90
+ tables \u2014 each of those overflows a phone by default, and one overflowing child makes the
91
+ WHOLE document horizontally scrollable, not just the bubble. */
92
+ .boff-explore-body,
93
+ .boff-explore-body p,
94
+ .boff-explore-body li,
95
+ .boff-explore-body a,
96
+ .boff-explore-body h1,
97
+ .boff-explore-body h2,
98
+ .boff-explore-body h3,
99
+ /* h4-h6 as well: a published answer page pushes the model's own headings down a level or
100
+ two so they nest under the page's, and a long unbroken word is no narrower for it. */
101
+ .boff-explore-body h4,
102
+ .boff-explore-body h5,
103
+ .boff-explore-body h6 {
104
+ overflow-wrap: anywhere;
105
+ word-break: break-word;
106
+ }
107
+ .boff-explore-body pre {
108
+ overflow-x: auto;
109
+ max-width: 100%;
110
+ }
111
+ .boff-explore-body table {
112
+ display: block;
113
+ overflow-x: auto;
114
+ max-width: 100%;
115
+ }
116
+ .boff-explore-body img {
117
+ max-width: 100%;
118
+ height: auto;
119
+ }
120
+
121
+ /* ---- Comparison and pricing tables ----------------------------------------------------
122
+ The harness emits a comparison, and a set of plans, as a GFM table whose FIRST column is
123
+ the subject \u2014 the product, or the plan (CONTRACT.md \xA77). Two things have to be true of it:
124
+ the page must not scroll sideways because of it, and it has to be readable on a 320px
125
+ phone, which a four-column grid of 1px borders is not.
126
+
127
+ Wide: a contained scroller. \`max-width: 100%\` keeps the box inside the bubble and
128
+ \`overflow-x: auto\` keeps the columns inside the box, so the widest possible table still
129
+ cannot widen the document. \`overscroll-behavior-x: contain\` stops a swipe that runs out
130
+ of table from chaining to the page \u2014 the same rule the transcript needed. */
131
+ .boff-explore-body .boff-explore-table {
132
+ max-width: 100%;
133
+ overflow-x: auto;
134
+ overscroll-behavior-x: contain;
135
+ margin: 0.75rem 0;
136
+ border: 1px solid hsl(var(--border));
137
+ border-radius: 0.75rem;
138
+ background-color: hsl(var(--card));
139
+ }
140
+ .boff-explore-body .boff-explore-table:focus-visible {
141
+ outline: 2px solid hsl(var(--ring));
142
+ outline-offset: 2px;
143
+ }
144
+ .boff-explore-body .boff-explore-table table {
145
+ /* Beats the blanket \`display: block\` above, which is the unwrapped fallback. */
146
+ display: table;
147
+ width: 100%;
148
+ max-width: none;
149
+ margin: 0;
150
+ border-collapse: collapse;
151
+ font-size: 0.8125rem;
152
+ }
153
+ .boff-explore-body .boff-explore-table th,
154
+ .boff-explore-body .boff-explore-table td {
155
+ border: 0;
156
+ border-top: 1px solid hsl(var(--border));
157
+ padding: 0.5rem 0.75rem;
158
+ /* \`start\`, never \`left\`: the page root carries dir="rtl" in Arabic, and a left-aligned
159
+ table there reads against the grain of every other line on the page. */
160
+ text-align: start;
161
+ vertical-align: top;
162
+ overflow-wrap: anywhere;
163
+ /* Prices in a column line up under each other instead of drifting by digit width. */
164
+ font-variant-numeric: tabular-nums;
165
+ min-width: 6rem;
166
+ }
167
+ .boff-explore-body .boff-explore-table thead th {
168
+ border-top: 0;
169
+ background-color: hsl(var(--muted));
170
+ color: hsl(var(--foreground));
171
+ font-weight: 600;
172
+ white-space: nowrap;
173
+ }
174
+ /* The first column is what the row is ABOUT \u2014 the product, or the plan name. Reading it as
175
+ the row's heading is what makes a price list scannable rather than a grid of strings. */
176
+ .boff-explore-body .boff-explore-table tbody td:first-child {
177
+ font-weight: 600;
178
+ color: hsl(var(--card-foreground));
179
+ }
180
+
181
+ /* Narrow: one card per row. Each cell prints its own column heading, so a value is never
182
+ stranded without the question it answers, and the subject cell becomes the card's title.
183
+ A media query rather than a measured width \u2014 these pages are serialised post-hydration,
184
+ so anything measured here would ship to every phone as static HTML. */
185
+ @media (max-width: 30rem) {
186
+ .boff-explore-body .boff-explore-table {
187
+ overflow-x: visible;
188
+ }
189
+ .boff-explore-body .boff-explore-table table,
190
+ .boff-explore-body .boff-explore-table tbody,
191
+ .boff-explore-body .boff-explore-table tr,
192
+ .boff-explore-body .boff-explore-table td {
193
+ display: block;
194
+ width: 100%;
195
+ min-width: 0;
196
+ }
197
+ .boff-explore-body .boff-explore-table thead {
198
+ display: none;
199
+ }
200
+ .boff-explore-body .boff-explore-table tr {
201
+ border-top: 1px solid hsl(var(--border));
202
+ padding: 0.5rem 0;
203
+ }
204
+ .boff-explore-body .boff-explore-table tr:first-child {
205
+ border-top: 0;
206
+ }
207
+ .boff-explore-body .boff-explore-table td {
208
+ border: 0;
209
+ padding: 0.125rem 0.75rem;
210
+ }
211
+ .boff-explore-body .boff-explore-table td[data-label]::before {
212
+ content: attr(data-label);
213
+ display: block;
214
+ font-size: 0.6875rem;
215
+ font-weight: 600;
216
+ text-transform: uppercase;
217
+ letter-spacing: 0.04em;
218
+ color: hsl(var(--muted-foreground));
219
+ }
220
+ .boff-explore-body .boff-explore-table tbody td:first-child {
221
+ font-size: 0.9375rem;
222
+ padding-bottom: 0.25rem;
223
+ }
224
+ .boff-explore-body .boff-explore-table tbody td:first-child::before {
225
+ /* The plan's own name is the heading here; repeating "Plan" over it says nothing. */
226
+ display: none;
227
+ }
228
+ }
229
+ `;
230
+ function ExploreAnswerStyles() {
231
+ return /* @__PURE__ */ jsxRuntime.jsx("style", { dangerouslySetInnerHTML: { __html: EXPLORE_ANSWER_CSS } });
232
+ }
233
+ function inlineImageOrigins(products) {
234
+ const origins = /* @__PURE__ */ new Set();
235
+ for (const product of products) {
236
+ let url;
237
+ try {
238
+ url = new URL(product.website);
239
+ } catch {
240
+ continue;
241
+ }
242
+ if (url.protocol !== "https:") continue;
243
+ const apex = url.hostname.replace(/^www\./, "");
244
+ const port = url.port ? `:${url.port}` : "";
245
+ origins.add(`https://${apex}${port}`);
246
+ origins.add(`https://www.${apex}${port}`);
247
+ }
248
+ return origins;
249
+ }
250
+ function isInlineImageAllowed(src, origins) {
251
+ if (typeof src !== "string" || src === "") return false;
252
+ let url;
253
+ try {
254
+ url = new URL(src);
255
+ } catch {
256
+ return false;
257
+ }
258
+ if (url.protocol !== "https:" || url.username || url.password) return false;
259
+ return origins.has(url.origin);
260
+ }
261
+ var CODE_FENCE = /^\s*(?:```|~~~)/;
262
+ var TABLE_ROW_LINE = /^\s{0,3}\|/;
263
+ var TABLE_DELIMITER_ROW = /^\s{0,3}\|[\s:|-]*-[\s:|-]*$/;
264
+ function readableLine(raw, mode) {
265
+ let line = raw;
266
+ line = line.replace(/!\[[^\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g, "");
267
+ line = mode === "speech" ? (
268
+ // A voice reading "h-t-t-p-s colon slash slash…" mid-sentence is noise, and the
269
+ // prompt asks for two to five links in every answer. The label IS the words.
270
+ line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1")
271
+ ) : line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1 ($2)");
272
+ line = mode === "speech" ? line.replace(/<(https?:\/\/[^>\s]+)>/g, "") : line.replace(/<(https?:\/\/[^>\s]+)>/g, "$1");
273
+ line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
274
+ line = line.replace(/^\s{0,3}>\s?/, "");
275
+ if (/^\s{0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/.test(line)) return "";
276
+ line = line.replace(/`([^`]+)`/g, "$1");
277
+ line = line.replace(/\*\*([^*]+)\*\*/g, "$1");
278
+ line = line.replace(/__([^_]+)__/g, "$1");
279
+ line = line.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, "$1$2");
280
+ line = line.replace(/(^|[^_\w])_([^_\n]+)_(?![_\w])/g, "$1$2");
281
+ return line.replace(/[ \t]+$/, "");
282
+ }
283
+ function tableCells(row) {
284
+ const trimmed = row.trim();
285
+ const cells = [];
286
+ let cell = "";
287
+ for (let at = 0; at < trimmed.length; at += 1) {
288
+ const char = trimmed[at] ?? "";
289
+ if (char === "\\") {
290
+ cell += char + (trimmed[at + 1] ?? "");
291
+ at += 1;
292
+ continue;
293
+ }
294
+ if (char === "|") {
295
+ cells.push(cell.trim());
296
+ cell = "";
297
+ continue;
298
+ }
299
+ cell += char;
300
+ }
301
+ cells.push(cell.trim());
302
+ if (trimmed.startsWith("|") && cells[0] === "") cells.shift();
303
+ if (trimmed.endsWith("|") && cells[cells.length - 1] === "") cells.pop();
304
+ return cells;
305
+ }
306
+ function asSentence(text) {
307
+ return /[.!?]$/.test(text) ? text : `${text}.`;
308
+ }
309
+ function spokenTable(rows) {
310
+ if (rows.length < 2 || !TABLE_DELIMITER_ROW.test(rows[1] ?? "")) return rows;
311
+ const labels = tableCells(rows[0] ?? "");
312
+ const spoken = [];
313
+ for (const row of rows.slice(2)) {
314
+ const parts = [];
315
+ tableCells(row).forEach((value, column) => {
316
+ if (value === "") return;
317
+ const label = column === 0 ? "" : labels[column] ?? "";
318
+ parts.push(label === "" ? value : `${label}: ${value}`);
319
+ });
320
+ if (parts.length > 0) spoken.push(asSentence(parts.join(". ")));
321
+ }
322
+ return spoken;
323
+ }
324
+ function readableText(markdown, mode) {
325
+ const lines = [];
326
+ let inFence = false;
327
+ let table = [];
328
+ const flushTable = () => {
329
+ if (table.length === 0) return;
330
+ for (const line of spokenTable(table)) lines.push(readableLine(line, mode));
331
+ table = [];
332
+ };
333
+ for (const raw of markdown.split("\n")) {
334
+ if (CODE_FENCE.test(raw)) {
335
+ flushTable();
336
+ inFence = !inFence;
337
+ continue;
338
+ }
339
+ if (inFence) {
340
+ lines.push(raw);
341
+ continue;
342
+ }
343
+ if (mode === "speech" && TABLE_ROW_LINE.test(raw)) {
344
+ table.push(raw);
345
+ continue;
346
+ }
347
+ flushTable();
348
+ lines.push(readableLine(raw, mode));
349
+ }
350
+ flushTable();
351
+ return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
352
+ }
353
+ function answerPlainText(markdown) {
354
+ return readableText(markdown, "clipboard");
355
+ }
356
+ function answerSpeechText(markdown) {
357
+ return readableText(markdown, "speech");
358
+ }
359
+ var AnswerScopeContext = React.createContext(null);
360
+ var InsideLinkContext = React.createContext(false);
361
+ function AnswerLink(props) {
362
+ return /* @__PURE__ */ jsxRuntime.jsx(InsideLinkContext.Provider, { value: true, children: /* @__PURE__ */ jsxRuntime.jsx(MarkdownLink, { ...props }) });
363
+ }
364
+ var INLINE_IMAGE_FRAME = "relative inline-block max-w-full overflow-hidden rounded-xl border border-border bg-muted align-top shadow-sm";
365
+ function InlineConceptImage({ src, alt }) {
366
+ const scope = React.useContext(AnswerScopeContext);
367
+ const insideLink = React.useContext(InsideLinkContext);
368
+ if (!scope || !scope.enabled || !isInlineImageAllowed(src, scope.origins) || scope.failed.has(src)) {
369
+ return null;
370
+ }
371
+ const { strings: T, onFailed } = scope;
372
+ const picture = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
373
+ /* @__PURE__ */ jsxRuntime.jsx(
374
+ "img",
375
+ {
376
+ src,
377
+ alt: alt ?? "",
378
+ loading: "lazy",
379
+ decoding: "async",
380
+ onError: () => onFailed(src),
381
+ className: "block h-auto max-h-80 w-auto max-w-full object-contain"
382
+ }
383
+ ),
384
+ /* @__PURE__ */ jsxRuntime.jsx(
385
+ "span",
386
+ {
387
+ "aria-hidden": "true",
388
+ 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",
389
+ children: T.conceptIllustration
390
+ }
391
+ )
392
+ ] });
393
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { "data-boff-explore": "inline-image", className: "my-3 block", children: insideLink ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: INLINE_IMAGE_FRAME, children: picture }) : /* @__PURE__ */ jsxRuntime.jsxs(
394
+ "a",
395
+ {
396
+ href: src,
397
+ target: "_blank",
398
+ rel: "noopener noreferrer",
399
+ className: cn(
400
+ INLINE_IMAGE_FRAME,
401
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
402
+ ),
403
+ children: [
404
+ picture,
405
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sr-only", children: [
406
+ "(",
407
+ T.opensInNewTab,
408
+ ")"
409
+ ] })
410
+ ]
411
+ }
412
+ ) });
413
+ }
414
+ function markdownText(node) {
415
+ if (!node) return "";
416
+ if (typeof node.value === "string") return node.value;
417
+ return (node.children ?? []).map(markdownText).join("");
418
+ }
419
+ function hasHeaderCells(node) {
420
+ return (node?.children ?? []).some((cell) => cell.tagName === "th");
421
+ }
422
+ function findHeaderRow(node) {
423
+ for (const child of node?.children ?? []) {
424
+ if (child.tagName === "tr" && hasHeaderCells(child)) return child;
425
+ const nested = findHeaderRow(child);
426
+ if (nested !== null) return nested;
427
+ }
428
+ return null;
429
+ }
430
+ function tableColumnLabels(node) {
431
+ const row = findHeaderRow(node);
432
+ if (row === null) return [];
433
+ return (row.children ?? []).filter((cell) => cell.tagName === "th").map((cell) => markdownText(cell).trim());
434
+ }
435
+ var TableColumnsContext = React.createContext([]);
436
+ function AnswerTable({ node, children }) {
437
+ const scope = React.useContext(AnswerScopeContext);
438
+ const table = /* @__PURE__ */ jsxRuntime.jsx(TableColumnsContext.Provider, { value: tableColumnLabels(node), children: /* @__PURE__ */ jsxRuntime.jsx("table", { children }) });
439
+ if (!scope) return table;
440
+ return /* @__PURE__ */ jsxRuntime.jsx(
441
+ "div",
442
+ {
443
+ "data-boff-explore": "table",
444
+ role: "region",
445
+ "aria-label": scope.strings.answerTable,
446
+ tabIndex: 0,
447
+ className: "boff-explore-table",
448
+ children: table
449
+ }
450
+ );
451
+ }
452
+ function AnswerTableRow({
453
+ node,
454
+ children,
455
+ ...props
456
+ }) {
457
+ const labels = React.useContext(TableColumnsContext);
458
+ if (labels.length === 0 || hasHeaderCells(node)) {
459
+ return /* @__PURE__ */ jsxRuntime.jsx("tr", { ...props, children });
460
+ }
461
+ let column = 0;
462
+ const cells = React.Children.map(children, (cell) => {
463
+ if (!React.isValidElement(cell)) return cell;
464
+ const label = labels[column] ?? "";
465
+ column += 1;
466
+ if (label === "") return cell;
467
+ return React.cloneElement(cell, {
468
+ "data-label": label
469
+ });
470
+ });
471
+ return /* @__PURE__ */ jsxRuntime.jsx("tr", { ...props, children: cells });
472
+ }
473
+ var ANSWER_MARKDOWN_COMPONENTS = {
474
+ a: AnswerLink,
475
+ img: InlineConceptImage,
476
+ table: AnswerTable,
477
+ tr: AnswerTableRow
478
+ };
479
+ function ExploreAnswerBody({
480
+ children,
481
+ strings,
482
+ imageOrigins,
483
+ enabled = true,
484
+ components,
485
+ className
486
+ }) {
487
+ const [failed, setFailed] = React.useState(() => /* @__PURE__ */ new Set());
488
+ const onFailed = React.useCallback((src) => {
489
+ setFailed(
490
+ (current) => current.has(src) ? current : new Set(current).add(src)
491
+ );
492
+ }, []);
493
+ const scope = React.useMemo(
494
+ () => ({
495
+ strings,
496
+ enabled,
497
+ origins: imageOrigins,
498
+ failed,
499
+ onFailed
500
+ }),
501
+ [strings, enabled, imageOrigins, failed, onFailed]
502
+ );
503
+ const overrides = React.useMemo(
504
+ () => components ? { ...ANSWER_MARKDOWN_COMPONENTS, ...components } : ANSWER_MARKDOWN_COMPONENTS,
505
+ [components]
506
+ );
507
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("boff-prose boff-explore-body min-w-0", className), children: /* @__PURE__ */ jsxRuntime.jsx(AnswerScopeContext.Provider, { value: scope, children: /* @__PURE__ */ jsxRuntime.jsx(Markdown, { components: overrides, children }) }) });
508
+ }
43
509
 
44
510
  // src/components/explore-i18n.ts
45
511
  var EXPLORE_LOCALES = ["en", "zh", "hi", "es", "ar", "ta"];
@@ -58,7 +524,7 @@ var EN = {
58
524
  showLess: "Show less",
59
525
  sources: "Sources",
60
526
  nextYouCouldAsk: "Next you could ask",
61
- placeholder: "Describe the problem you're facing\u2026",
527
+ placeholder: "Describe the problem\u2026",
62
528
  placeholderProduct: "Describe a problem, or ask about {product}\u2026",
63
529
  send: "Send",
64
530
  stop: "Stop",
@@ -99,6 +565,20 @@ var EN = {
99
565
  feedbackDismiss: "Dismiss",
100
566
  feedbackThanks: "Thanks \u2014 that helps.",
101
567
  feedbackNotSent: "That didn't send.",
568
+ escalateOpen: "Talk to a person",
569
+ escalateTitle: "Send this conversation to a person",
570
+ escalateBody: "We'll pass the whole conversation on, so you don't have to type any of it again.",
571
+ escalateEmailLabel: "Your email address",
572
+ escalateEmailPlaceholder: "you@example.com",
573
+ escalateNoteLabel: "Anything else a person should know? (optional)",
574
+ escalateSubmit: "Send to a person",
575
+ escalateSending: "Sending\u2026",
576
+ escalateCancel: "Cancel",
577
+ escalateInvalidEmail: "That doesn't look like an address we can reply to. Please check it.",
578
+ escalateSent: "A person now has this conversation. We'll reply to {email}.",
579
+ escalateRateLimited: "That's a lot of requests in a short time. Please try again in a few minutes.",
580
+ escalateCaptcha: "We couldn't verify that you're human. Please try that again.",
581
+ escalateFailed: "We couldn't pass that on just now. Please try again.",
102
582
  prevSuggestion: "Previous suggestion",
103
583
  nextSuggestion: "Next suggestion",
104
584
  conceptIllustration: "Concept illustration",
@@ -109,6 +589,9 @@ var EN = {
109
589
  conceptAskQuestion: "How does {title} work in {product}?",
110
590
  copyAnswer: "Copy answer",
111
591
  copyFailed: "Couldn't copy",
592
+ readAloud: "Read aloud",
593
+ stopReading: "Stop reading",
594
+ answerTable: "Table",
112
595
  groundedIn: "What this answer is based on",
113
596
  sourceCountOne: "1 source",
114
597
  sourceCountMany: "{count} sources",
@@ -143,7 +626,7 @@ var ZH = {
143
626
  showLess: "\u6536\u8D77",
144
627
  sources: "\u6765\u6E90",
145
628
  nextYouCouldAsk: "\u4F60\u8FD8\u53EF\u4EE5\u95EE",
146
- placeholder: "\u63CF\u8FF0\u4F60\u9047\u5230\u7684\u95EE\u9898\u2026",
629
+ placeholder: "\u63CF\u8FF0\u4F60\u7684\u95EE\u9898\u2026",
147
630
  placeholderProduct: "\u63CF\u8FF0\u95EE\u9898\uFF0C\u6216\u8BE2\u95EE {product}\u2026",
148
631
  send: "\u53D1\u9001",
149
632
  stop: "\u505C\u6B62",
@@ -181,6 +664,20 @@ var ZH = {
181
664
  feedbackDismiss: "\u5173\u95ED",
182
665
  feedbackThanks: "\u8C22\u8C22\uFF0C\u8FD9\u5BF9\u6211\u4EEC\u5F88\u6709\u5E2E\u52A9\u3002",
183
666
  feedbackNotSent: "\u6CA1\u6709\u63D0\u4EA4\u6210\u529F\u3002",
667
+ escalateOpen: "\u8054\u7CFB\u771F\u4EBA",
668
+ escalateTitle: "\u628A\u8FD9\u6BB5\u5BF9\u8BDD\u8F6C\u7ED9\u771F\u4EBA",
669
+ 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",
670
+ escalateEmailLabel: "\u60A8\u7684\u90AE\u7BB1\u5730\u5740",
671
+ escalateEmailPlaceholder: "you@example.com",
672
+ escalateNoteLabel: "\u8FD8\u6709\u4EC0\u4E48\u9700\u8981\u544A\u8BC9\u6211\u4EEC\u7684\u5417\uFF1F\uFF08\u9009\u586B\uFF09",
673
+ escalateSubmit: "\u8F6C\u7ED9\u771F\u4EBA",
674
+ escalateSending: "\u6B63\u5728\u53D1\u9001\u2026",
675
+ escalateCancel: "\u53D6\u6D88",
676
+ 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",
677
+ escalateSent: "\u5DF2\u6709\u540C\u4E8B\u63A5\u624B\u8FD9\u6BB5\u5BF9\u8BDD\uFF0C\u6211\u4EEC\u4F1A\u56DE\u590D\u5230 {email}\u3002",
678
+ escalateRateLimited: "\u77ED\u65F6\u95F4\u5185\u8BF7\u6C42\u8FC7\u591A\uFF0C\u8BF7\u51E0\u5206\u949F\u540E\u518D\u8BD5\u3002",
679
+ escalateCaptcha: "\u6211\u4EEC\u65E0\u6CD5\u9A8C\u8BC1\u60A8\u662F\u771F\u4EBA\uFF0C\u8BF7\u518D\u8BD5\u4E00\u6B21\u3002",
680
+ escalateFailed: "\u6682\u65F6\u65E0\u6CD5\u8F6C\u4EA4\uFF0C\u8BF7\u518D\u8BD5\u4E00\u6B21\u3002",
184
681
  prevSuggestion: "\u4E0A\u4E00\u4E2A\u5EFA\u8BAE",
185
682
  nextSuggestion: "\u4E0B\u4E00\u4E2A\u5EFA\u8BAE",
186
683
  conceptIllustration: "\u6982\u5FF5\u793A\u610F\u56FE",
@@ -191,6 +688,9 @@ var ZH = {
191
688
  conceptAskQuestion: "{product} \u7684 {title} \u662F\u600E\u4E48\u8FD0\u4F5C\u7684\uFF1F",
192
689
  copyAnswer: "\u590D\u5236\u56DE\u7B54",
193
690
  copyFailed: "\u590D\u5236\u5931\u8D25",
691
+ readAloud: "\u6717\u8BFB",
692
+ stopReading: "\u505C\u6B62\u6717\u8BFB",
693
+ answerTable: "\u8868\u683C",
194
694
  groundedIn: "\u8FD9\u4E2A\u56DE\u7B54\u7684\u4F9D\u636E",
195
695
  sourceCountOne: "1 \u4E2A\u6765\u6E90",
196
696
  sourceCountMany: "{count} \u4E2A\u6765\u6E90",
@@ -221,7 +721,7 @@ var HI = {
221
721
  showLess: "\u0915\u092E \u0926\u093F\u0916\u093E\u090F\u0901",
222
722
  sources: "\u0938\u094D\u0930\u094B\u0924",
223
723
  nextYouCouldAsk: "\u0906\u0917\u0947 \u0906\u092A \u092A\u0942\u091B \u0938\u0915\u0924\u0947 \u0939\u0948\u0902",
224
- placeholder: "\u0905\u092A\u0928\u0940 \u0938\u092E\u0938\u094D\u092F\u093E \u092F\u0939\u093E\u0901 \u092C\u0924\u093E\u090F\u0902\u2026",
724
+ placeholder: "\u0905\u092A\u0928\u0940 \u0938\u092E\u0938\u094D\u092F\u093E \u092C\u0924\u093E\u090F\u0902\u2026",
225
725
  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",
226
726
  send: "\u092D\u0947\u091C\u0947\u0902",
227
727
  stop: "\u0930\u094B\u0915\u0947\u0902",
@@ -259,6 +759,20 @@ var HI = {
259
759
  feedbackDismiss: "\u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
260
760
  feedbackThanks: "\u0927\u0928\u094D\u092F\u0935\u093E\u0926, \u0907\u0938\u0938\u0947 \u092E\u0926\u0926 \u092E\u093F\u0932\u0924\u0940 \u0939\u0948\u0964",
261
761
  feedbackNotSent: "\u092F\u0939 \u092D\u0947\u091C\u093E \u0928\u0939\u0940\u0902 \u091C\u093E \u0938\u0915\u093E\u0964",
762
+ escalateOpen: "\u0915\u093F\u0938\u0940 \u0935\u094D\u092F\u0915\u094D\u0924\u093F \u0938\u0947 \u092C\u093E\u0924 \u0915\u0930\u0947\u0902",
763
+ 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",
764
+ 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",
765
+ escalateEmailLabel: "\u0906\u092A\u0915\u093E \u0908\u092E\u0947\u0932 \u092A\u0924\u093E",
766
+ escalateEmailPlaceholder: "you@example.com",
767
+ 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)",
768
+ escalateSubmit: "\u0915\u093F\u0938\u0940 \u0935\u094D\u092F\u0915\u094D\u0924\u093F \u0915\u094B \u092D\u0947\u091C\u0947\u0902",
769
+ escalateSending: "\u092D\u0947\u091C\u093E \u091C\u093E \u0930\u0939\u093E \u0939\u0948\u2026",
770
+ escalateCancel: "\u0930\u0926\u094D\u0926 \u0915\u0930\u0947\u0902",
771
+ 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",
772
+ 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",
773
+ 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",
774
+ 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",
775
+ 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",
262
776
  prevSuggestion: "\u092A\u093F\u091B\u0932\u093E \u0938\u0941\u091D\u093E\u0935",
263
777
  nextSuggestion: "\u0905\u0917\u0932\u093E \u0938\u0941\u091D\u093E\u0935",
264
778
  conceptIllustration: "\u0905\u0935\u0927\u093E\u0930\u0923\u093E \u091A\u093F\u0924\u094D\u0930\u0923",
@@ -269,6 +783,9 @@ var HI = {
269
783
  conceptAskQuestion: "{product} \u092E\u0947\u0902 {title} \u0915\u0948\u0938\u0947 \u0915\u093E\u092E \u0915\u0930\u0924\u093E \u0939\u0948?",
270
784
  copyAnswer: "\u0909\u0924\u094D\u0924\u0930 \u0915\u0949\u092A\u0940 \u0915\u0930\u0947\u0902",
271
785
  copyFailed: "\u0915\u0949\u092A\u0940 \u0928\u0939\u0940\u0902 \u0939\u094B \u0938\u0915\u093E",
786
+ readAloud: "\u091C\u093C\u094B\u0930 \u0938\u0947 \u092A\u0922\u093C\u0947\u0902",
787
+ stopReading: "\u092A\u0922\u093C\u0928\u093E \u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
788
+ answerTable: "\u0924\u093E\u0932\u093F\u0915\u093E",
272
789
  groundedIn: "\u092F\u0939 \u0909\u0924\u094D\u0924\u0930 \u0915\u093F\u0938 \u092A\u0930 \u0906\u0927\u093E\u0930\u093F\u0924 \u0939\u0948",
273
790
  sourceCountOne: "1 \u0938\u094D\u0930\u094B\u0924",
274
791
  sourceCountMany: "{count} \u0938\u094D\u0930\u094B\u0924",
@@ -303,7 +820,7 @@ var ES = {
303
820
  showLess: "Ver menos",
304
821
  sources: "Fuentes",
305
822
  nextYouCouldAsk: "Tambi\xE9n puedes preguntar",
306
- placeholder: "Describe el problema que tienes\u2026",
823
+ placeholder: "Describe el problema\u2026",
307
824
  placeholderProduct: "Describe un problema o pregunta por {product}\u2026",
308
825
  send: "Enviar",
309
826
  stop: "Detener",
@@ -341,6 +858,20 @@ var ES = {
341
858
  feedbackDismiss: "Cerrar",
342
859
  feedbackThanks: "Gracias, nos sirve.",
343
860
  feedbackNotSent: "No se ha podido enviar.",
861
+ escalateOpen: "Hablar con una persona",
862
+ escalateTitle: "Env\xEDa esta conversaci\xF3n a una persona",
863
+ escalateBody: "Le pasaremos la conversaci\xF3n entera, as\xED no tienes que volver a escribirla.",
864
+ escalateEmailLabel: "Tu direcci\xF3n de correo",
865
+ escalateEmailPlaceholder: "tu@ejemplo.com",
866
+ escalateNoteLabel: "\xBFAlgo m\xE1s que deber\xEDamos saber? (opcional)",
867
+ escalateSubmit: "Enviar a una persona",
868
+ escalateSending: "Enviando\u2026",
869
+ escalateCancel: "Cancelar",
870
+ escalateInvalidEmail: "No parece una direcci\xF3n a la que podamos responder. Rev\xEDsala, por favor.",
871
+ escalateSent: "Una persona ya tiene esta conversaci\xF3n. Te responderemos a {email}.",
872
+ escalateRateLimited: "Han sido muchas peticiones en poco tiempo. Int\xE9ntalo de nuevo en unos minutos.",
873
+ escalateCaptcha: "No hemos podido verificar que eres una persona. Int\xE9ntalo de nuevo.",
874
+ escalateFailed: "No hemos podido pasarlo ahora mismo. Int\xE9ntalo de nuevo.",
344
875
  prevSuggestion: "Sugerencia anterior",
345
876
  nextSuggestion: "Sugerencia siguiente",
346
877
  conceptIllustration: "Ilustraci\xF3n del concepto",
@@ -351,6 +882,9 @@ var ES = {
351
882
  conceptAskQuestion: "\xBFC\xF3mo funciona {title} en {product}?",
352
883
  copyAnswer: "Copiar respuesta",
353
884
  copyFailed: "No se pudo copiar",
885
+ readAloud: "Leer en voz alta",
886
+ stopReading: "Dejar de leer",
887
+ answerTable: "Tabla",
354
888
  groundedIn: "En qu\xE9 se basa esta respuesta",
355
889
  sourceCountOne: "1 fuente",
356
890
  sourceCountMany: "{count} fuentes",
@@ -385,7 +919,7 @@ var AR = {
385
919
  showLess: "\u0639\u0631\u0636 \u0623\u0642\u0644",
386
920
  sources: "\u0627\u0644\u0645\u0635\u0627\u062F\u0631",
387
921
  nextYouCouldAsk: "\u064A\u0645\u0643\u0646\u0643 \u0623\u0646 \u062A\u0633\u0623\u0644 \u0628\u0639\u062F \u0630\u0644\u0643",
388
- placeholder: "\u0635\u0641 \u0627\u0644\u0645\u0634\u0643\u0644\u0629 \u0627\u0644\u062A\u064A \u062A\u0648\u0627\u062C\u0647\u0647\u0627\u2026",
922
+ placeholder: "\u0635\u0641 \u0627\u0644\u0645\u0634\u0643\u0644\u0629\u2026",
389
923
  placeholderProduct: "\u0635\u0641 \u0645\u0634\u0643\u0644\u062A\u0643 \u0623\u0648 \u0627\u0633\u0623\u0644 \u0639\u0646 {product}\u2026",
390
924
  send: "\u0625\u0631\u0633\u0627\u0644",
391
925
  stop: "\u0625\u064A\u0642\u0627\u0641",
@@ -423,6 +957,20 @@ var AR = {
423
957
  feedbackDismiss: "\u0625\u063A\u0644\u0627\u0642",
424
958
  feedbackThanks: "\u0634\u0643\u0631\u064B\u0627\u060C \u0647\u0630\u0627 \u064A\u0633\u0627\u0639\u062F\u0646\u0627.",
425
959
  feedbackNotSent: "\u0644\u0645 \u064A\u062A\u0645 \u0627\u0644\u0625\u0631\u0633\u0627\u0644.",
960
+ escalateOpen: "\u062A\u0648\u0627\u0635\u0644 \u0645\u0639 \u0634\u062E\u0635",
961
+ escalateTitle: "\u0623\u0631\u0633\u0644 \u0647\u0630\u0647 \u0627\u0644\u0645\u062D\u0627\u062F\u062B\u0629 \u0625\u0644\u0649 \u0634\u062E\u0635",
962
+ 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.",
963
+ escalateEmailLabel: "\u0628\u0631\u064A\u062F\u0643 \u0627\u0644\u0625\u0644\u0643\u062A\u0631\u0648\u0646\u064A",
964
+ escalateEmailPlaceholder: "you@example.com",
965
+ 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)",
966
+ escalateSubmit: "\u0625\u0631\u0633\u0627\u0644 \u0625\u0644\u0649 \u0634\u062E\u0635",
967
+ escalateSending: "\u062C\u0627\u0631\u064D \u0627\u0644\u0625\u0631\u0633\u0627\u0644\u2026",
968
+ escalateCancel: "\u0625\u0644\u063A\u0627\u0621",
969
+ 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.",
970
+ 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}.",
971
+ 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.",
972
+ 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.",
973
+ 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.",
426
974
  prevSuggestion: "\u0627\u0644\u0627\u0642\u062A\u0631\u0627\u062D \u0627\u0644\u0633\u0627\u0628\u0642",
427
975
  nextSuggestion: "\u0627\u0644\u0627\u0642\u062A\u0631\u0627\u062D \u0627\u0644\u062A\u0627\u0644\u064A",
428
976
  conceptIllustration: "\u0631\u0633\u0645 \u062A\u0648\u0636\u064A\u062D\u064A \u0644\u0644\u0645\u0641\u0647\u0648\u0645",
@@ -433,6 +981,9 @@ var AR = {
433
981
  conceptAskQuestion: "\u0643\u064A\u0641 \u064A\u0639\u0645\u0644 {title} \u0641\u064A {product}\u061F",
434
982
  copyAnswer: "\u0646\u0633\u062E \u0627\u0644\u0625\u062C\u0627\u0628\u0629",
435
983
  copyFailed: "\u062A\u0639\u0630\u0651\u0631 \u0627\u0644\u0646\u0633\u062E",
984
+ readAloud: "\u0627\u0642\u0631\u0623 \u0628\u0635\u0648\u062A \u0639\u0627\u0644\u064D",
985
+ stopReading: "\u0625\u064A\u0642\u0627\u0641 \u0627\u0644\u0642\u0631\u0627\u0621\u0629",
986
+ answerTable: "\u062C\u062F\u0648\u0644",
436
987
  groundedIn: "\u0639\u0644\u0649 \u0645\u0627\u0630\u0627 \u062A\u0633\u062A\u0646\u062F \u0647\u0630\u0647 \u0627\u0644\u0625\u062C\u0627\u0628\u0629",
437
988
  sourceCountOne: "\u0645\u0635\u062F\u0631 \u0648\u0627\u062D\u062F",
438
989
  sourceCountMany: "{count} \u0645\u0635\u0627\u062F\u0631",
@@ -467,7 +1018,7 @@ var TA = {
467
1018
  showLess: "\u0B95\u0BC1\u0BB1\u0BC8\u0BB5\u0BBE\u0B95\u0B95\u0BCD \u0B95\u0BBE\u0B9F\u0BCD\u0B9F\u0BC1",
468
1019
  sources: "\u0BAE\u0BC2\u0BB2\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
469
1020
  nextYouCouldAsk: "\u0B85\u0B9F\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1 \u0BA8\u0BC0\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0B95\u0BC7\u0B9F\u0BCD\u0B95\u0BB2\u0BBE\u0BAE\u0BCD",
470
- 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",
1021
+ placeholder: "\u0B9A\u0BBF\u0B95\u0BCD\u0B95\u0BB2\u0BC8 \u0BB5\u0BBF\u0BB5\u0BB0\u0BBF\u0BAF\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD\u2026",
471
1022
  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",
472
1023
  send: "\u0B85\u0BA9\u0BC1\u0BAA\u0BCD\u0BAA\u0BC1",
473
1024
  stop: "\u0BA8\u0BBF\u0BB1\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1",
@@ -505,6 +1056,20 @@ var TA = {
505
1056
  feedbackDismiss: "\u0BAE\u0BC2\u0B9F\u0BC1",
506
1057
  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.",
507
1058
  feedbackNotSent: "\u0B85\u0BA9\u0BC1\u0BAA\u0BCD\u0BAA \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8.",
1059
+ escalateOpen: "\u0B92\u0BB0\u0BC1\u0BB5\u0BB0\u0BBF\u0B9F\u0BAE\u0BCD \u0BAA\u0BC7\u0B9A\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
1060
+ 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",
1061
+ 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.",
1062
+ escalateEmailLabel: "\u0B89\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0BAE\u0BBF\u0BA9\u0BCD\u0BA9\u0B9E\u0BCD\u0B9A\u0BB2\u0BCD \u0BAE\u0BC1\u0B95\u0BB5\u0BB0\u0BBF",
1063
+ escalateEmailPlaceholder: "you@example.com",
1064
+ 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)",
1065
+ escalateSubmit: "\u0B92\u0BB0\u0BC1\u0BB5\u0BB0\u0BC1\u0B95\u0BCD\u0B95\u0BC1 \u0B85\u0BA9\u0BC1\u0BAA\u0BCD\u0BAA\u0BC1",
1066
+ escalateSending: "\u0B85\u0BA9\u0BC1\u0BAA\u0BCD\u0BAA\u0BAA\u0BCD\u0BAA\u0B9F\u0BC1\u0B95\u0BBF\u0BB1\u0BA4\u0BC1\u2026",
1067
+ escalateCancel: "\u0BB0\u0BA4\u0BCD\u0BA4\u0BC1 \u0B9A\u0BC6\u0BAF\u0BCD",
1068
+ 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.",
1069
+ 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.",
1070
+ 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.",
1071
+ 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.",
1072
+ 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.",
508
1073
  prevSuggestion: "\u0BAE\u0BC1\u0BA8\u0BCD\u0BA4\u0BC8\u0BAF \u0BAA\u0BB0\u0BBF\u0BA8\u0BCD\u0BA4\u0BC1\u0BB0\u0BC8",
509
1074
  nextSuggestion: "\u0B85\u0B9F\u0BC1\u0BA4\u0BCD\u0BA4 \u0BAA\u0BB0\u0BBF\u0BA8\u0BCD\u0BA4\u0BC1\u0BB0\u0BC8",
510
1075
  conceptIllustration: "\u0B95\u0BB0\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1 \u0BB5\u0BBF\u0BB3\u0B95\u0BCD\u0B95\u0BAA\u0BCD\u0BAA\u0B9F\u0BAE\u0BCD",
@@ -515,6 +1080,9 @@ var TA = {
515
1080
  conceptAskQuestion: "{product} \u0B87\u0BB2\u0BCD {title} \u0B8E\u0BAA\u0BCD\u0BAA\u0B9F\u0BBF \u0BB5\u0BC7\u0BB2\u0BC8 \u0B9A\u0BC6\u0BAF\u0BCD\u0B95\u0BBF\u0BB1\u0BA4\u0BC1?",
516
1081
  copyAnswer: "\u0BAA\u0BA4\u0BBF\u0BB2\u0BC8 \u0BA8\u0B95\u0BB2\u0BC6\u0B9F\u0BC1",
517
1082
  copyFailed: "\u0BA8\u0B95\u0BB2\u0BC6\u0B9F\u0BC1\u0B95\u0BCD\u0B95 \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8",
1083
+ readAloud: "\u0B9A\u0BA4\u0BCD\u0BA4\u0BAE\u0BBE\u0B95\u0BAA\u0BCD \u0BAA\u0B9F\u0BBF",
1084
+ stopReading: "\u0BAA\u0B9F\u0BBF\u0BAA\u0BCD\u0BAA\u0BA4\u0BC8 \u0BA8\u0BBF\u0BB1\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1",
1085
+ answerTable: "\u0B85\u0B9F\u0BCD\u0B9F\u0BB5\u0BA3\u0BC8",
518
1086
  groundedIn: "\u0B87\u0BA8\u0BCD\u0BA4\u0BAA\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BCD \u0B8E\u0BA4\u0BC8 \u0B85\u0B9F\u0BBF\u0BAA\u0BCD\u0BAA\u0B9F\u0BC8\u0BAF\u0BBE\u0B95\u0B95\u0BCD \u0B95\u0BCA\u0BA3\u0BCD\u0B9F\u0BA4\u0BC1",
519
1087
  sourceCountOne: "1 \u0BAE\u0BC2\u0BB2\u0BAE\u0BCD",
520
1088
  sourceCountMany: "{count} \u0BAE\u0BC2\u0BB2\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
@@ -546,6 +1114,17 @@ var STRINGS = {
546
1114
  ar: AR,
547
1115
  ta: TA
548
1116
  };
1117
+ var SPEECH_TAGS = {
1118
+ en: "en-US",
1119
+ zh: "zh-CN",
1120
+ hi: "hi-IN",
1121
+ es: "es-ES",
1122
+ ar: "ar-SA",
1123
+ ta: "ta-IN"
1124
+ };
1125
+ function speechLocaleOf(locale) {
1126
+ return SPEECH_TAGS[locale];
1127
+ }
549
1128
  function resolveExploreLocale(explicit) {
550
1129
  const candidates = [
551
1130
  explicit,
@@ -733,6 +1312,29 @@ var SUBMIT_EXPLORE_FEEDBACK_MUTATION = (
733
1312
  `
734
1313
  );
735
1314
  var EXPLORE_FEEDBACK_COMMENT_SOFT_MAX = 500;
1315
+ var ESCALATE_EXPLORE_CONVERSATION_MUTATION = (
1316
+ /* GraphQL */
1317
+ `
1318
+ mutation EscalateExploreConversation(
1319
+ $input: EscalateExploreConversationInput!
1320
+ ) {
1321
+ escalateExploreConversation(input: $input) {
1322
+ created
1323
+ escalation {
1324
+ email
1325
+ createdAt
1326
+ }
1327
+ }
1328
+ }
1329
+ `
1330
+ );
1331
+ var EXPLORE_ESCALATION_NOTE_SOFT_MAX = 2e3;
1332
+ 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,}$/;
1333
+ function looksLikeDeliverableEmail(value) {
1334
+ const trimmed = value.trim();
1335
+ if (trimmed.length === 0 || trimmed.length > 254) return false;
1336
+ return DELIVERABLE_EMAIL.test(trimmed);
1337
+ }
736
1338
  var CREATE_EXPLORE_SHARE_LINK_MUTATION = (
737
1339
  /* GraphQL */
738
1340
  `
@@ -753,48 +1355,6 @@ var EXPLORE_SHARED_CONVERSATION_QUERY = (
753
1355
  }
754
1356
  `
755
1357
  );
756
- function MarkdownLink({
757
- href,
758
- children,
759
- // react-markdown passes the hast node to every override; it is not a DOM attribute.
760
- node: _node,
761
- ...props
762
- }) {
763
- const isExternal = typeof href === "string" && (href.startsWith("http://") || href.startsWith("https://"));
764
- return /* @__PURE__ */ jsxRuntime.jsx(
765
- "a",
766
- {
767
- href,
768
- ...props,
769
- ...isExternal ? { target: "_blank", rel: "noopener noreferrer" } : { rel: "noopener noreferrer" },
770
- children
771
- }
772
- );
773
- }
774
- var defaultComponents = {
775
- a: MarkdownLink
776
- };
777
- function Markdown({
778
- children,
779
- className,
780
- inline,
781
- components
782
- }) {
783
- const content = /* @__PURE__ */ jsxRuntime.jsx(
784
- ReactMarkdown__default.default,
785
- {
786
- remarkPlugins: [remarkGfm__default.default],
787
- rehypePlugins: [rehypeSanitize__default.default],
788
- components: { ...defaultComponents, ...components },
789
- ...inline ? { disallowedElements: ["p"], unwrapDisallowed: true } : {},
790
- children
791
- }
792
- );
793
- if (inline) {
794
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className, children: content });
795
- }
796
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: content });
797
- }
798
1358
  var PROSE_CSS = `
799
1359
  .boff-prose {
800
1360
  max-width: none;
@@ -1191,6 +1751,10 @@ function useWebSDK() {
1191
1751
  }
1192
1752
  return client;
1193
1753
  }
1754
+ function useWebSDKConfig() {
1755
+ const client = useWebSDK();
1756
+ return client.getConfig();
1757
+ }
1194
1758
 
1195
1759
  // src/data/products.ts
1196
1760
  var ECOSYSTEM_PRODUCTS = [
@@ -1537,6 +2101,25 @@ var VALIDATION_CODES = /* @__PURE__ */ new Set([
1537
2101
  "CONVERSATION_LIMIT",
1538
2102
  "INVALID_INPUT"
1539
2103
  ]);
2104
+ function escalateOutcomeForCode(code) {
2105
+ switch (code) {
2106
+ case "INVALID_EMAIL":
2107
+ return "invalid-email";
2108
+ case "RATE_LIMITED":
2109
+ case "HTTP_429":
2110
+ return "rate-limited";
2111
+ case "CAPTCHA_REQUIRED":
2112
+ case "CAPTCHA_FAILED":
2113
+ return "captcha";
2114
+ // The token owns nothing, or owns a different conversation. The server answers NOT_FOUND
2115
+ // rather than FORBIDDEN so the endpoint is not an oracle for whether a thread exists;
2116
+ // either way there is nothing this visitor can do about it, so it reads as a failure.
2117
+ case "NOT_FOUND":
2118
+ return "failed";
2119
+ default:
2120
+ return "failed";
2121
+ }
2122
+ }
1540
2123
  function classifyExploreError(code, serverMessage) {
1541
2124
  const c = code ?? "UNKNOWN";
1542
2125
  if (RATE_LIMIT_CODES.has(c)) {
@@ -1703,6 +2286,7 @@ function useExploreChat(options = {}) {
1703
2286
  onSend
1704
2287
  } = options;
1705
2288
  const client = useWebSDK();
2289
+ const { productId } = useWebSDKConfig();
1706
2290
  const promptKey = JSON.stringify(examplePrompts ?? []);
1707
2291
  const welcomeKey = JSON.stringify(fallbackWelcome ?? null);
1708
2292
  const fallbackCatalog = React.useMemo(
@@ -1726,6 +2310,7 @@ function useExploreChat(options = {}) {
1726
2310
  const pollGenerationRef = React.useRef(0);
1727
2311
  const pollTimerRef = React.useRef(null);
1728
2312
  const conversationTokenRef = React.useRef(null);
2313
+ const [hasConversation, setHasConversation] = React.useState(false);
1729
2314
  const shareTokenRef = React.useRef(void 0);
1730
2315
  if (shareTokenRef.current === void 0) {
1731
2316
  shareTokenRef.current = readShareToken();
@@ -1740,6 +2325,10 @@ function useExploreChat(options = {}) {
1740
2325
  );
1741
2326
  const inFlightRef = React.useRef(false);
1742
2327
  catalogRef.current = catalog;
2328
+ const rememberConversationToken = React.useCallback((token) => {
2329
+ conversationTokenRef.current = token;
2330
+ setHasConversation(token !== null);
2331
+ }, []);
1743
2332
  const clearPollTimer = React.useCallback(() => {
1744
2333
  if (pollTimerRef.current !== null) {
1745
2334
  clearTimeout(pollTimerRef.current);
@@ -1806,7 +2395,7 @@ function useExploreChat(options = {}) {
1806
2395
  setArchive(readArchive(storageKey));
1807
2396
  const stored = readPersisted(storageKey);
1808
2397
  if (stored) {
1809
- conversationTokenRef.current = stored.conversationToken;
2398
+ rememberConversationToken(stored.conversationToken);
1810
2399
  focusProductRef.current = stored.focusProduct;
1811
2400
  const settled = stored.messages.flatMap((m) => {
1812
2401
  if (m.role !== "ASSISTANT") return [m];
@@ -1822,7 +2411,7 @@ function useExploreChat(options = {}) {
1822
2411
  setTurnCount(turns);
1823
2412
  }
1824
2413
  setHydrated(true);
1825
- }, [persist, storageKey]);
2414
+ }, [persist, rememberConversationToken, storageKey]);
1826
2415
  React.useEffect(() => {
1827
2416
  if (!persist || !hydrated || typeof window === "undefined") return;
1828
2417
  try {
@@ -2015,7 +2604,7 @@ function useExploreChat(options = {}) {
2015
2604
  setPhase(classified.kind === "rate-limit" ? "limited" : "error");
2016
2605
  return;
2017
2606
  }
2018
- conversationTokenRef.current = result.conversation.token;
2607
+ rememberConversationToken(result.conversation.token);
2019
2608
  turnCountRef.current = result.conversation.turnCount;
2020
2609
  setTurnCount(result.conversation.turnCount);
2021
2610
  setRemainingToday(result.remainingToday ?? null);
@@ -2041,7 +2630,14 @@ function useExploreChat(options = {}) {
2041
2630
  setPhase("streaming");
2042
2631
  startPolling(result.conversation.token, assistantMessage.id);
2043
2632
  },
2044
- [client, locale, pageUrl, productSlug, startPolling]
2633
+ [
2634
+ client,
2635
+ locale,
2636
+ pageUrl,
2637
+ productSlug,
2638
+ rememberConversationToken,
2639
+ startPolling
2640
+ ]
2045
2641
  );
2046
2642
  const send = React.useCallback(
2047
2643
  async (message, attachments = []) => {
@@ -2158,7 +2754,7 @@ function useExploreChat(options = {}) {
2158
2754
  );
2159
2755
  cancelPolling();
2160
2756
  inFlightRef.current = false;
2161
- conversationTokenRef.current = null;
2757
+ rememberConversationToken(null);
2162
2758
  lastSentTextRef.current = null;
2163
2759
  lastSentAttachmentsRef.current = [];
2164
2760
  turnCountRef.current = 0;
@@ -2173,7 +2769,7 @@ function useExploreChat(options = {}) {
2173
2769
  } catch {
2174
2770
  }
2175
2771
  }
2176
- }, [cancelPolling, storageKey]);
2772
+ }, [cancelPolling, rememberConversationToken, storageKey]);
2177
2773
  const openConversation = React.useCallback(
2178
2774
  (token) => {
2179
2775
  const entry = readArchive(storageKey).find((e) => e.token === token);
@@ -2185,7 +2781,7 @@ function useExploreChat(options = {}) {
2185
2781
  });
2186
2782
  cancelPolling();
2187
2783
  inFlightRef.current = false;
2188
- conversationTokenRef.current = entry.token;
2784
+ rememberConversationToken(entry.token);
2189
2785
  turnCountRef.current = entry.messages.filter(
2190
2786
  (m) => m.role === "USER"
2191
2787
  ).length;
@@ -2197,7 +2793,7 @@ function useExploreChat(options = {}) {
2197
2793
  setPhase(catalogRef.current.enabled ? "ready" : "disabled");
2198
2794
  setArchive(readArchive(storageKey));
2199
2795
  },
2200
- [cancelPolling, storageKey]
2796
+ [cancelPolling, rememberConversationToken, storageKey]
2201
2797
  );
2202
2798
  const deleteConversation = React.useCallback(
2203
2799
  (token) => {
@@ -2286,6 +2882,69 @@ function useExploreChat(options = {}) {
2286
2882
  },
2287
2883
  [client]
2288
2884
  );
2885
+ const escalate = React.useCallback(
2886
+ async (email, note) => {
2887
+ const token = conversationTokenRef.current;
2888
+ if (!token) {
2889
+ return { outcome: "no-conversation", email: null, createdAt: null };
2890
+ }
2891
+ const attempt = async (captchaAttempt) => {
2892
+ const activeCatalog = catalogRef.current;
2893
+ let recaptchaToken;
2894
+ if (activeCatalog.captchaRequired && activeCatalog.recaptchaSiteKey) {
2895
+ try {
2896
+ recaptchaToken = await getRecaptchaV3Token(
2897
+ activeCatalog.recaptchaSiteKey
2898
+ );
2899
+ } catch (err) {
2900
+ console.warn(
2901
+ "[WebSDK] reCAPTCHA v3 token could not be minted",
2902
+ err
2903
+ );
2904
+ }
2905
+ }
2906
+ const input = {
2907
+ conversationToken: token,
2908
+ email: email.trim(),
2909
+ note: note?.trim() ? note.trim() : null,
2910
+ ...recaptchaToken ? { recaptchaToken } : {},
2911
+ ...productId ? { productId } : {}
2912
+ };
2913
+ try {
2914
+ const res = await client.mutate(
2915
+ ESCALATE_EXPLORE_CONVERSATION_MUTATION,
2916
+ { input }
2917
+ );
2918
+ const first = res?.errors?.[0];
2919
+ const result = res?.data?.escalateExploreConversation;
2920
+ if (first || !result) {
2921
+ const code = typeof first?.extensions?.code === "string" ? first.extensions.code : "ESCALATION_FAILED";
2922
+ if (code === "CAPTCHA_FAILED" && captchaAttempt === 0) {
2923
+ return attempt(1);
2924
+ }
2925
+ return {
2926
+ outcome: escalateOutcomeForCode(code),
2927
+ email: null,
2928
+ createdAt: null
2929
+ };
2930
+ }
2931
+ return {
2932
+ // `created: false` is the double-click, the retry, the reload. The handover
2933
+ // exists, so this is a success — and the address comes from the SERVER, because
2934
+ // on a repeat submit the reply goes to whatever the FIRST handover used.
2935
+ outcome: result.created ? "sent" : "already-handed-over",
2936
+ email: result.escalation?.email ?? null,
2937
+ createdAt: result.escalation?.createdAt ?? null
2938
+ };
2939
+ } catch {
2940
+ return { outcome: "failed", email: null, createdAt: null };
2941
+ }
2942
+ };
2943
+ return attempt(0);
2944
+ },
2945
+ [client, productId]
2946
+ );
2947
+ const canEscalate = hasConversation && !isSharedView;
2289
2948
  return {
2290
2949
  phase,
2291
2950
  catalog,
@@ -2306,6 +2965,8 @@ function useExploreChat(options = {}) {
2306
2965
  clearHistory,
2307
2966
  recordClick,
2308
2967
  submitFeedback,
2968
+ escalate,
2969
+ canEscalate,
2309
2970
  createShareLink,
2310
2971
  isSharedView
2311
2972
  };
@@ -2315,6 +2976,224 @@ function makeOptimisticId() {
2315
2976
  optimisticCounter += 1;
2316
2977
  return `boff-explore-local-${Date.now()}-${optimisticCounter}`;
2317
2978
  }
2979
+ var DRAIN_TARGET_MS = 1200;
2980
+ var MIN_CHARS_PER_SECOND = 90;
2981
+ var MAX_CHARS_PER_SECOND = 2200;
2982
+ var MAX_STEP_MS = 250;
2983
+ var COMMIT_INTERVAL_MS = 45;
2984
+ var FALLBACK_STEP_MS = 50;
2985
+ var FENCE = /^ {0,3}(?:```|~~~)/;
2986
+ var TABLE_ROW = /^ {0,3}\|/;
2987
+ var EMPTY_BLOCK_MARKER = /^ {0,3}(?:[-+>]|#{1,6}|\d{1,9}[.)])\s*$/;
2988
+ function isWordCharacter(char) {
2989
+ return char !== void 0 && /[\p{L}\p{N}]/u.test(char);
2990
+ }
2991
+ function clamp(value, min, max) {
2992
+ return Math.min(Math.max(value, min), max);
2993
+ }
2994
+ function now() {
2995
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
2996
+ }
2997
+ function isHighSurrogate(code) {
2998
+ return code >= 55296 && code <= 56319;
2999
+ }
3000
+ function safeInlineEnd(line) {
3001
+ let codeSpan = null;
3002
+ let link = null;
3003
+ const emphasis = [];
3004
+ let at = 0;
3005
+ while (at < line.length) {
3006
+ const char = line[at] ?? "";
3007
+ if (char === "\\") {
3008
+ at += 2;
3009
+ continue;
3010
+ }
3011
+ if (char === "`") {
3012
+ let run = 1;
3013
+ while (line[at + run] === "`") run += 1;
3014
+ codeSpan = codeSpan === null ? at : null;
3015
+ at += run;
3016
+ continue;
3017
+ }
3018
+ if (codeSpan !== null) {
3019
+ at += 1;
3020
+ continue;
3021
+ }
3022
+ if (char === "[") {
3023
+ if (link === null) link = line[at - 1] === "!" ? at - 1 : at;
3024
+ at += 1;
3025
+ continue;
3026
+ }
3027
+ if (char === "]") {
3028
+ if (link !== null && line[at + 1] !== "(") link = null;
3029
+ at += 1;
3030
+ continue;
3031
+ }
3032
+ if (char === ")") {
3033
+ link = null;
3034
+ at += 1;
3035
+ continue;
3036
+ }
3037
+ if (char === "*" || char === "_" || char === "~") {
3038
+ let run = 1;
3039
+ while (line[at + run] === char) run += 1;
3040
+ const start = at;
3041
+ at += run;
3042
+ const before = line[start - 1];
3043
+ const after = line[start + run];
3044
+ if (run < 2) continue;
3045
+ if (char === "_" && isWordCharacter(before) && isWordCharacter(after)) {
3046
+ continue;
3047
+ }
3048
+ const marker = char.repeat(run);
3049
+ const open2 = emphasis.findIndex((entry) => entry.marker === marker);
3050
+ if (open2 !== -1) {
3051
+ emphasis.splice(open2, 1);
3052
+ continue;
3053
+ }
3054
+ if (after !== void 0 && /\s/.test(after)) continue;
3055
+ emphasis.push({ marker, start });
3056
+ continue;
3057
+ }
3058
+ at += 1;
3059
+ }
3060
+ const open = [codeSpan, link, emphasis[0]?.start ?? null].filter(
3061
+ (value) => value !== null
3062
+ );
3063
+ return open.length === 0 ? line.length : Math.min(...open);
3064
+ }
3065
+ function revealBoundary(text, limit) {
3066
+ if (limit <= 0) return 0;
3067
+ const caughtUp = limit >= text.length;
3068
+ const head = caughtUp ? text : text.slice(0, limit);
3069
+ const lineStart = head.lastIndexOf("\n") + 1;
3070
+ const line = head.slice(lineStart);
3071
+ let fenceStart = null;
3072
+ let tableStart = null;
3073
+ let tableRows = 0;
3074
+ let at = 0;
3075
+ while (at < lineStart) {
3076
+ const breakAt = head.indexOf("\n", at);
3077
+ const end = breakAt === -1 ? lineStart : breakAt;
3078
+ const row = head.slice(at, end);
3079
+ if (fenceStart !== null) {
3080
+ if (FENCE.test(row)) fenceStart = null;
3081
+ } else if (FENCE.test(row)) {
3082
+ fenceStart = at;
3083
+ } else if (TABLE_ROW.test(row)) {
3084
+ if (tableStart === null) {
3085
+ tableStart = at;
3086
+ tableRows = 0;
3087
+ }
3088
+ tableRows += 1;
3089
+ } else if (tableStart !== null) {
3090
+ tableStart = null;
3091
+ tableRows = 0;
3092
+ }
3093
+ at = end + 1;
3094
+ }
3095
+ if (fenceStart === null) {
3096
+ const lastIsRow = TABLE_ROW.test(line);
3097
+ if (lastIsRow && tableStart === null) {
3098
+ tableStart = lineStart;
3099
+ tableRows = 0;
3100
+ } else if (!lastIsRow && line !== "") {
3101
+ tableStart = null;
3102
+ tableRows = 0;
3103
+ }
3104
+ if (tableStart !== null && tableRows < 2) return tableStart;
3105
+ }
3106
+ if (caughtUp) return text.length;
3107
+ if (fenceStart !== null) return fenceStart;
3108
+ if (TABLE_ROW.test(line)) return lineStart;
3109
+ if (EMPTY_BLOCK_MARKER.test(line)) return lineStart;
3110
+ const cut = lineStart + safeInlineEnd(line);
3111
+ return isHighSurrogate(text.charCodeAt(cut - 1)) ? cut - 1 : cut;
3112
+ }
3113
+ function prefersReducedMotion() {
3114
+ if (typeof window === "undefined") return true;
3115
+ try {
3116
+ return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
3117
+ } catch {
3118
+ return false;
3119
+ }
3120
+ }
3121
+ function useProgressiveReveal(text, settled) {
3122
+ const [, setFrame] = React.useState(0);
3123
+ const [reduced] = React.useState(prefersReducedMotion);
3124
+ const immediate = settled || reduced;
3125
+ const textRef = React.useRef(text);
3126
+ const cursorRef = React.useRef(immediate ? text.length : 0);
3127
+ const cutRef = React.useRef(immediate ? text.length : 0);
3128
+ const committedRef = React.useRef(0);
3129
+ if (text !== textRef.current) {
3130
+ const revealed = textRef.current.slice(0, Math.floor(cursorRef.current));
3131
+ if (revealed !== "" && !text.startsWith(revealed)) {
3132
+ cursorRef.current = text.length;
3133
+ }
3134
+ cursorRef.current = Math.min(cursorRef.current, text.length);
3135
+ cutRef.current = Math.min(cutRef.current, text.length);
3136
+ textRef.current = text;
3137
+ }
3138
+ if (immediate) cursorRef.current = text.length;
3139
+ const cut = immediate ? text.length : Math.max(
3140
+ cutRef.current,
3141
+ revealBoundary(text, Math.floor(cursorRef.current))
3142
+ );
3143
+ cutRef.current = cut;
3144
+ React.useEffect(() => {
3145
+ if (immediate) return;
3146
+ if (cursorRef.current >= text.length) return;
3147
+ let cancelled = false;
3148
+ let cancelStep = null;
3149
+ let previous = now();
3150
+ const commit = (at, force) => {
3151
+ if (!force && at - committedRef.current < COMMIT_INTERVAL_MS) return;
3152
+ committedRef.current = at;
3153
+ setFrame((value) => value + 1);
3154
+ };
3155
+ const step = () => {
3156
+ cancelStep = null;
3157
+ if (cancelled) return;
3158
+ const at = now();
3159
+ const elapsed = clamp(at - previous, 0, MAX_STEP_MS);
3160
+ previous = at;
3161
+ const length = textRef.current.length;
3162
+ const pending = length - cursorRef.current;
3163
+ if (pending <= 0) {
3164
+ commit(at, true);
3165
+ return;
3166
+ }
3167
+ const perSecond = clamp(
3168
+ pending / (DRAIN_TARGET_MS / 1e3),
3169
+ MIN_CHARS_PER_SECOND,
3170
+ MAX_CHARS_PER_SECOND
3171
+ );
3172
+ cursorRef.current = Math.min(
3173
+ length,
3174
+ cursorRef.current + perSecond * elapsed / 1e3
3175
+ );
3176
+ const done = cursorRef.current >= length;
3177
+ commit(at, done);
3178
+ if (!done) schedule();
3179
+ };
3180
+ const schedule = () => {
3181
+ if (typeof window.requestAnimationFrame === "function") {
3182
+ const id2 = window.requestAnimationFrame(step);
3183
+ cancelStep = () => window.cancelAnimationFrame(id2);
3184
+ return;
3185
+ }
3186
+ const id = setTimeout(step, FALLBACK_STEP_MS);
3187
+ cancelStep = () => clearTimeout(id);
3188
+ };
3189
+ schedule();
3190
+ return () => {
3191
+ cancelled = true;
3192
+ cancelStep?.();
3193
+ };
3194
+ }, [text, immediate]);
3195
+ return text.slice(0, cut);
3196
+ }
2318
3197
  function getRecognitionCtor() {
2319
3198
  if (typeof window === "undefined") return null;
2320
3199
  const w = window;
@@ -2533,6 +3412,141 @@ function useSpeechInput({
2533
3412
  );
2534
3413
  return { supported, listening, interim, error, start, stop, toggle };
2535
3414
  }
3415
+ var SILENT_FAILURE_MS = 1500;
3416
+ function speechPlatform() {
3417
+ if (typeof window === "undefined") return null;
3418
+ const scope = window;
3419
+ const synth = scope.speechSynthesis;
3420
+ const Utterance = scope.SpeechSynthesisUtterance;
3421
+ if (!synth || typeof synth.speak !== "function") return null;
3422
+ if (typeof Utterance !== "function") return null;
3423
+ return { synth, Utterance };
3424
+ }
3425
+ function normaliseTag(tag) {
3426
+ return tag.trim().toLowerCase().replace(/_/g, "-");
3427
+ }
3428
+ function primaryOf(tag) {
3429
+ return normaliseTag(tag).split("-")[0] ?? "";
3430
+ }
3431
+ function pickVoice(voices, lang) {
3432
+ const wanted = normaliseTag(lang);
3433
+ if (wanted === "") return null;
3434
+ const primary = primaryOf(wanted);
3435
+ const exact = voices.filter((voice) => normaliseTag(voice.lang) === wanted);
3436
+ const related = voices.filter((voice) => primaryOf(voice.lang) === primary);
3437
+ return exact.find((voice) => voice.default) ?? exact[0] ?? related.find((voice) => voice.default) ?? related[0] ?? null;
3438
+ }
3439
+ var activeOwner = null;
3440
+ var activeStop = null;
3441
+ function stopSpeaking() {
3442
+ activeStop?.();
3443
+ }
3444
+ function useSpeechOutput({
3445
+ lang
3446
+ } = {}) {
3447
+ const [available, setAvailable] = React.useState(() => speechPlatform() !== null);
3448
+ const [speaking, setSpeaking] = React.useState(false);
3449
+ const utteranceRef = React.useRef(null);
3450
+ const watchdogRef = React.useRef(null);
3451
+ const ownerRef = React.useRef({});
3452
+ const stopRef = React.useRef(() => void 0);
3453
+ const langRef = React.useRef(lang);
3454
+ langRef.current = lang;
3455
+ const clearWatchdog = React.useCallback(() => {
3456
+ if (watchdogRef.current === null) return;
3457
+ clearTimeout(watchdogRef.current);
3458
+ watchdogRef.current = null;
3459
+ }, []);
3460
+ const settle = React.useCallback(() => {
3461
+ clearWatchdog();
3462
+ utteranceRef.current = null;
3463
+ if (activeOwner === ownerRef.current) {
3464
+ activeOwner = null;
3465
+ activeStop = null;
3466
+ }
3467
+ setSpeaking(false);
3468
+ }, [clearWatchdog]);
3469
+ const stop = React.useCallback(() => {
3470
+ const owned = utteranceRef.current !== null;
3471
+ settle();
3472
+ if (owned) speechPlatform()?.synth.cancel();
3473
+ }, [settle]);
3474
+ stopRef.current = stop;
3475
+ const speak = React.useCallback(
3476
+ (text) => {
3477
+ const platform = speechPlatform();
3478
+ if (!platform) {
3479
+ setAvailable(false);
3480
+ return;
3481
+ }
3482
+ const spoken = text.trim();
3483
+ if (spoken === "") return;
3484
+ if (activeStop && activeOwner !== ownerRef.current) activeStop();
3485
+ platform.synth.cancel();
3486
+ const utterance = new platform.Utterance(spoken);
3487
+ const wanted = langRef.current;
3488
+ if (wanted !== void 0 && wanted !== "") {
3489
+ const voice = pickVoice(platform.synth.getVoices(), wanted);
3490
+ utterance.lang = voice?.lang ?? wanted;
3491
+ if (voice) utterance.voice = voice;
3492
+ }
3493
+ const mine = () => utteranceRef.current === utterance;
3494
+ utterance.onstart = () => {
3495
+ if (!mine()) return;
3496
+ clearWatchdog();
3497
+ setSpeaking(true);
3498
+ };
3499
+ utterance.onend = () => {
3500
+ if (!mine()) return;
3501
+ settle();
3502
+ };
3503
+ utterance.onerror = (event) => {
3504
+ if (!mine()) return;
3505
+ const code = typeof event?.error === "string" ? event.error : "";
3506
+ settle();
3507
+ if (code !== "interrupted" && code !== "canceled") setAvailable(false);
3508
+ };
3509
+ utteranceRef.current = utterance;
3510
+ activeOwner = ownerRef.current;
3511
+ activeStop = () => {
3512
+ stopRef.current();
3513
+ };
3514
+ setSpeaking(true);
3515
+ platform.synth.speak(utterance);
3516
+ clearWatchdog();
3517
+ watchdogRef.current = setTimeout(() => {
3518
+ watchdogRef.current = null;
3519
+ if (!mine()) return;
3520
+ if (platform.synth.speaking) return;
3521
+ settle();
3522
+ setAvailable(false);
3523
+ }, SILENT_FAILURE_MS);
3524
+ },
3525
+ [clearWatchdog, settle]
3526
+ );
3527
+ const toggle = React.useCallback(
3528
+ (text) => {
3529
+ if (speaking) stop();
3530
+ else speak(text);
3531
+ },
3532
+ [speak, speaking, stop]
3533
+ );
3534
+ React.useEffect(() => {
3535
+ if (!speaking || typeof window === "undefined") return;
3536
+ const onHide = () => {
3537
+ stopRef.current();
3538
+ };
3539
+ window.addEventListener("pagehide", onHide);
3540
+ return () => window.removeEventListener("pagehide", onHide);
3541
+ }, [speaking]);
3542
+ React.useEffect(
3543
+ () => () => {
3544
+ stopRef.current();
3545
+ },
3546
+ []
3547
+ );
3548
+ return { supported: available, speaking, speak, stop, toggle };
3549
+ }
2536
3550
  var SWIPE_THRESHOLD = 40;
2537
3551
  function useSwipe({
2538
3552
  onSwipeLeft,
@@ -2638,9 +3652,6 @@ function PageHead({
2638
3652
  ))
2639
3653
  ] });
2640
3654
  }
2641
- function cn(...inputs) {
2642
- return tailwindMerge.twMerge(clsx.clsx(inputs));
2643
- }
2644
3655
  var buttonVariants = classVarianceAuthority.cva(
2645
3656
  "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",
2646
3657
  {
@@ -2898,34 +3909,7 @@ var EXPLORE_CSS = `
2898
3909
  }
2899
3910
  .boff-explore-fade { animation: boff-explore-fade-in 220ms ease-out; }
2900
3911
  @media (prefers-reduced-motion: reduce) {
2901
- .boff-explore-fade { animation: none; }
2902
- }
2903
-
2904
- /* Nothing inside an answer may widen the page. Model output carries long URLs, code and
2905
- tables \u2014 each of those overflows a phone by default, and one overflowing child makes the
2906
- WHOLE document horizontally scrollable, not just the bubble. */
2907
- .boff-explore-body,
2908
- .boff-explore-body p,
2909
- .boff-explore-body li,
2910
- .boff-explore-body a,
2911
- .boff-explore-body h1,
2912
- .boff-explore-body h2,
2913
- .boff-explore-body h3 {
2914
- overflow-wrap: anywhere;
2915
- word-break: break-word;
2916
- }
2917
- .boff-explore-body pre {
2918
- overflow-x: auto;
2919
- max-width: 100%;
2920
- }
2921
- .boff-explore-body table {
2922
- display: block;
2923
- overflow-x: auto;
2924
- max-width: 100%;
2925
- }
2926
- .boff-explore-body img {
2927
- max-width: 100%;
2928
- height: auto;
3912
+ .boff-explore-fade { animation: none; }
2929
3913
  }
2930
3914
 
2931
3915
  /* ---- Ambient backdrop -----------------------------------------------------------------
@@ -2984,11 +3968,40 @@ var EXPLORE_CSS = `
2984
3968
  animation: boff-explore-sheen 1.5s ease-in-out infinite;
2985
3969
  }
2986
3970
 
3971
+ /* ---- The composer, before anyone has typed -----------------------------------------
3972
+ On the welcome screen the box IS the page: everything above it is a way of explaining
3973
+ what to put in it. Unhighlighted it reads as a search bar at the bottom of a landing
3974
+ page, which people scroll past \u2014 so while the conversation is empty it gets a soft halo
3975
+ in the site's own --primary and one slow breath of it, and the moment there is a
3976
+ transcript, or the visitor focuses or types, it drops back to the ordinary card it was.
3977
+ An input that keeps pulsing while you are using it is nagging, not inviting.
3978
+
3979
+ Colour comes from the product's --primary token, so it is whatever that site's palette
3980
+ already is, never a colour chosen here. */
3981
+ .boff-explore-invite {
3982
+ border-color: hsl(var(--primary) / 0.45);
3983
+ box-shadow:
3984
+ 0 0 0 4px hsl(var(--primary) / 0.08),
3985
+ 0 8px 24px -12px hsl(var(--primary) / 0.45);
3986
+ animation: boff-explore-breathe 3.2s ease-in-out infinite;
3987
+ }
3988
+ /* Focus and typing both mean "I have found it", and the hint has done its job. */
3989
+ .boff-explore-invite:focus-within,
3990
+ .boff-explore-invite.boff-explore-invite-quiet {
3991
+ animation: none;
3992
+ box-shadow: 0 0 0 4px hsl(var(--primary) / 0.10);
3993
+ }
3994
+ @keyframes boff-explore-breathe {
3995
+ 0%, 100% { box-shadow: 0 0 0 4px hsl(var(--primary) / 0.07), 0 8px 24px -12px hsl(var(--primary) / 0.40); }
3996
+ 50% { box-shadow: 0 0 0 7px hsl(var(--primary) / 0.13), 0 8px 28px -12px hsl(var(--primary) / 0.55); }
3997
+ }
3998
+
2987
3999
  /* Every one of the above is decoration. Reduced motion removes the movement and keeps the
2988
4000
  final state, rather than leaving content mid-animation and invisible. */
2989
4001
  @media (prefers-reduced-motion: reduce) {
2990
4002
  .boff-explore-aura::before,
2991
- .boff-explore-busy::after { animation: none; }
4003
+ .boff-explore-busy::after,
4004
+ .boff-explore-invite { animation: none; }
2992
4005
  .boff-explore-rise { animation: none; opacity: 1; transform: none; }
2993
4006
  }
2994
4007
 
@@ -3099,7 +4112,7 @@ function isOwnElement(el, root) {
3099
4112
  function visualViewportHeight() {
3100
4113
  return window.visualViewport?.height ?? window.innerHeight;
3101
4114
  }
3102
- function clamp(value, min, max) {
4115
+ function clamp2(value, min, max) {
3103
4116
  return Math.min(Math.max(value, min), max);
3104
4117
  }
3105
4118
  function isRestoredNavigation() {
@@ -3136,34 +4149,6 @@ function logoUrlOf(url) {
3136
4149
  return null;
3137
4150
  }
3138
4151
  }
3139
- function inlineImageOrigins(products) {
3140
- const origins = /* @__PURE__ */ new Set();
3141
- for (const product of products) {
3142
- let url;
3143
- try {
3144
- url = new URL(product.website);
3145
- } catch {
3146
- continue;
3147
- }
3148
- if (url.protocol !== "https:") continue;
3149
- const apex = url.hostname.replace(/^www\./, "");
3150
- const port = url.port ? `:${url.port}` : "";
3151
- origins.add(`https://${apex}${port}`);
3152
- origins.add(`https://www.${apex}${port}`);
3153
- }
3154
- return origins;
3155
- }
3156
- function isInlineImageAllowed(src, origins) {
3157
- if (typeof src !== "string" || src === "") return false;
3158
- let url;
3159
- try {
3160
- url = new URL(src);
3161
- } catch {
3162
- return false;
3163
- }
3164
- if (url.protocol !== "https:" || url.username || url.password) return false;
3165
- return origins.has(url.origin);
3166
- }
3167
4152
  var REFERENCE_KIND_ORDER = [
3168
4153
  "CONCEPT",
3169
4154
  "DOCS",
@@ -3248,97 +4233,6 @@ function summariseReferences(references, origins, productNames) {
3248
4233
  products
3249
4234
  };
3250
4235
  }
3251
- var CODE_FENCE = /^\s*(?:```|~~~)/;
3252
- function answerPlainText(markdown) {
3253
- const lines = [];
3254
- let inFence = false;
3255
- for (const raw of markdown.split("\n")) {
3256
- if (CODE_FENCE.test(raw)) {
3257
- inFence = !inFence;
3258
- continue;
3259
- }
3260
- if (inFence) {
3261
- lines.push(raw);
3262
- continue;
3263
- }
3264
- let line = raw;
3265
- line = line.replace(/!\[[^\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g, "");
3266
- line = line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1 ($2)");
3267
- line = line.replace(/<(https?:\/\/[^>\s]+)>/g, "$1");
3268
- line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
3269
- line = line.replace(/^\s{0,3}>\s?/, "");
3270
- if (/^\s{0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/.test(line)) {
3271
- lines.push("");
3272
- continue;
3273
- }
3274
- line = line.replace(/`([^`]+)`/g, "$1");
3275
- line = line.replace(/\*\*([^*]+)\*\*/g, "$1");
3276
- line = line.replace(/__([^_]+)__/g, "$1");
3277
- line = line.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, "$1$2");
3278
- line = line.replace(/(^|[^_\w])_([^_\n]+)_(?![_\w])/g, "$1$2");
3279
- lines.push(line.replace(/[ \t]+$/, ""));
3280
- }
3281
- return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
3282
- }
3283
- var InlineImageScopeContext = React.createContext(null);
3284
- var InsideLinkContext = React.createContext(false);
3285
- function AnswerLink(props) {
3286
- return /* @__PURE__ */ jsxRuntime.jsx(InsideLinkContext.Provider, { value: true, children: /* @__PURE__ */ jsxRuntime.jsx(MarkdownLink, { ...props }) });
3287
- }
3288
- var INLINE_IMAGE_FRAME = "relative inline-block max-w-full overflow-hidden rounded-xl border border-border bg-muted align-top shadow-sm";
3289
- function InlineConceptImage({ src, alt }) {
3290
- const scope = React.useContext(InlineImageScopeContext);
3291
- const insideLink = React.useContext(InsideLinkContext);
3292
- if (!scope || !scope.enabled || !isInlineImageAllowed(src, scope.origins) || scope.failed.has(src)) {
3293
- return null;
3294
- }
3295
- const { strings: T, onFailed } = scope;
3296
- const picture = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3297
- /* @__PURE__ */ jsxRuntime.jsx(
3298
- "img",
3299
- {
3300
- src,
3301
- alt: alt ?? "",
3302
- loading: "lazy",
3303
- decoding: "async",
3304
- onError: () => onFailed(src),
3305
- className: "block h-auto max-h-80 w-auto max-w-full object-contain"
3306
- }
3307
- ),
3308
- /* @__PURE__ */ jsxRuntime.jsx(
3309
- "span",
3310
- {
3311
- "aria-hidden": "true",
3312
- 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",
3313
- children: T.conceptIllustration
3314
- }
3315
- )
3316
- ] });
3317
- return /* @__PURE__ */ jsxRuntime.jsx("span", { "data-boff-explore": "inline-image", className: "my-3 block", children: insideLink ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: INLINE_IMAGE_FRAME, children: picture }) : /* @__PURE__ */ jsxRuntime.jsxs(
3318
- "a",
3319
- {
3320
- href: src,
3321
- target: "_blank",
3322
- rel: "noopener noreferrer",
3323
- className: cn(
3324
- INLINE_IMAGE_FRAME,
3325
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
3326
- ),
3327
- children: [
3328
- picture,
3329
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sr-only", children: [
3330
- "(",
3331
- T.opensInNewTab,
3332
- ")"
3333
- ] })
3334
- ]
3335
- }
3336
- ) });
3337
- }
3338
- var ANSWER_MARKDOWN_COMPONENTS = {
3339
- a: AnswerLink,
3340
- img: InlineConceptImage
3341
- };
3342
4236
  function ReferenceCard({
3343
4237
  reference,
3344
4238
  onFollow
@@ -3677,6 +4571,195 @@ function AnswerFeedback({
3677
4571
  }
3678
4572
  );
3679
4573
  }
4574
+ function EscalateForm({
4575
+ strings: T,
4576
+ formId,
4577
+ onEscalate,
4578
+ onClose,
4579
+ onBusyChange
4580
+ }) {
4581
+ const [email, setEmail] = React.useState("");
4582
+ const [note, setNote] = React.useState("");
4583
+ const [error, setError] = React.useState(null);
4584
+ const [sending, setSending] = React.useState(false);
4585
+ const [sentTo, setSentTo] = React.useState(null);
4586
+ const emailRef = React.useRef(null);
4587
+ const inFlightRef = React.useRef(false);
4588
+ const titleId = `${formId}-title`;
4589
+ const emailId = `${formId}-email`;
4590
+ const noteId = `${formId}-note`;
4591
+ const errorId = `${formId}-error`;
4592
+ React.useEffect(() => {
4593
+ emailRef.current?.focus();
4594
+ }, []);
4595
+ React.useEffect(() => {
4596
+ onBusyChange?.(sending);
4597
+ return () => onBusyChange?.(false);
4598
+ }, [sending, onBusyChange]);
4599
+ const handleSubmit = React.useCallback(
4600
+ async (event) => {
4601
+ event.preventDefault();
4602
+ if (inFlightRef.current) return;
4603
+ const address = email.trim();
4604
+ if (!looksLikeDeliverableEmail(address)) {
4605
+ setError(T.escalateInvalidEmail);
4606
+ emailRef.current?.focus();
4607
+ return;
4608
+ }
4609
+ inFlightRef.current = true;
4610
+ setSending(true);
4611
+ setError(null);
4612
+ const result = await onEscalate(address, note);
4613
+ inFlightRef.current = false;
4614
+ setSending(false);
4615
+ switch (result.outcome) {
4616
+ case "sent":
4617
+ case "already-handed-over":
4618
+ setSentTo(result.email ?? address);
4619
+ return;
4620
+ case "invalid-email":
4621
+ setError(T.escalateInvalidEmail);
4622
+ emailRef.current?.focus();
4623
+ return;
4624
+ case "rate-limited":
4625
+ setError(T.escalateRateLimited);
4626
+ return;
4627
+ case "captcha":
4628
+ setError(T.escalateCaptcha);
4629
+ return;
4630
+ // `no-conversation` included: the form should not have been offered at all, and the
4631
+ // honest thing to show is that nothing was passed on.
4632
+ default:
4633
+ setError(T.escalateFailed);
4634
+ return;
4635
+ }
4636
+ },
4637
+ [T, email, note, onEscalate]
4638
+ );
4639
+ if (sentTo !== null) {
4640
+ return /* @__PURE__ */ jsxRuntime.jsx(
4641
+ "p",
4642
+ {
4643
+ id: formId,
4644
+ "data-boff-explore": "escalate-sent",
4645
+ role: "status",
4646
+ className: "boff-explore-fade mt-2 rounded-xl border border-border bg-card px-3 py-2 text-xs leading-relaxed text-card-foreground",
4647
+ children: fill(T.escalateSent, { email: sentTo })
4648
+ }
4649
+ );
4650
+ }
4651
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4652
+ "form",
4653
+ {
4654
+ id: formId,
4655
+ "data-boff-explore": "escalate-form",
4656
+ "aria-labelledby": titleId,
4657
+ noValidate: true,
4658
+ onSubmit: handleSubmit,
4659
+ onKeyDown: (event) => {
4660
+ if (event.key !== "Escape") return;
4661
+ event.stopPropagation();
4662
+ if (sending) return;
4663
+ onClose();
4664
+ },
4665
+ className: "boff-explore-fade mt-2 space-y-2 rounded-xl border border-border bg-card p-3",
4666
+ children: [
4667
+ /* @__PURE__ */ jsxRuntime.jsx("p", { id: titleId, className: "text-xs font-medium text-card-foreground", children: T.escalateTitle }),
4668
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs leading-relaxed text-muted-foreground", children: T.escalateBody }),
4669
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
4670
+ /* @__PURE__ */ jsxRuntime.jsx(
4671
+ "label",
4672
+ {
4673
+ htmlFor: emailId,
4674
+ className: "block text-xs font-medium text-card-foreground",
4675
+ children: T.escalateEmailLabel
4676
+ }
4677
+ ),
4678
+ /* @__PURE__ */ jsxRuntime.jsx(
4679
+ "input",
4680
+ {
4681
+ ref: emailRef,
4682
+ id: emailId,
4683
+ name: "email",
4684
+ "data-boff-explore": "escalate-email",
4685
+ type: "email",
4686
+ inputMode: "email",
4687
+ autoComplete: "email",
4688
+ required: true,
4689
+ value: email,
4690
+ onChange: (event) => {
4691
+ setEmail(event.target.value);
4692
+ if (error) setError(null);
4693
+ },
4694
+ "aria-invalid": error ? "true" : void 0,
4695
+ "aria-describedby": error ? errorId : void 0,
4696
+ placeholder: T.escalateEmailPlaceholder,
4697
+ 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"
4698
+ }
4699
+ )
4700
+ ] }),
4701
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
4702
+ /* @__PURE__ */ jsxRuntime.jsx(
4703
+ "label",
4704
+ {
4705
+ htmlFor: noteId,
4706
+ className: "block text-xs font-medium text-card-foreground",
4707
+ children: T.escalateNoteLabel
4708
+ }
4709
+ ),
4710
+ /* @__PURE__ */ jsxRuntime.jsx(
4711
+ Textarea,
4712
+ {
4713
+ id: noteId,
4714
+ name: "note",
4715
+ "data-boff-explore": "escalate-note",
4716
+ rows: 2,
4717
+ value: note,
4718
+ onChange: (event) => setNote(event.target.value),
4719
+ maxLength: EXPLORE_ESCALATION_NOTE_SOFT_MAX,
4720
+ className: "min-h-0 border-border bg-background"
4721
+ }
4722
+ )
4723
+ ] }),
4724
+ error ? /* @__PURE__ */ jsxRuntime.jsx(
4725
+ "p",
4726
+ {
4727
+ id: errorId,
4728
+ "data-boff-explore": "escalate-error",
4729
+ role: "alert",
4730
+ className: "text-xs leading-relaxed text-destructive",
4731
+ children: error
4732
+ }
4733
+ ) : null,
4734
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
4735
+ /* @__PURE__ */ jsxRuntime.jsx(
4736
+ Button,
4737
+ {
4738
+ "data-boff-explore": "escalate-send",
4739
+ type: "submit",
4740
+ size: "sm",
4741
+ disabled: sending,
4742
+ children: sending ? T.escalateSending : T.escalateSubmit
4743
+ }
4744
+ ),
4745
+ /* @__PURE__ */ jsxRuntime.jsx(
4746
+ Button,
4747
+ {
4748
+ "data-boff-explore": "escalate-cancel",
4749
+ type: "button",
4750
+ size: "sm",
4751
+ variant: "ghost",
4752
+ className: "text-muted-foreground",
4753
+ disabled: sending,
4754
+ onClick: onClose,
4755
+ children: T.escalateCancel
4756
+ }
4757
+ )
4758
+ ] })
4759
+ ]
4760
+ }
4761
+ );
4762
+ }
3680
4763
  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";
3681
4764
  function ProvenanceChips({
3682
4765
  strings: T,
@@ -3726,9 +4809,42 @@ function ProvenanceChips({
3726
4809
  );
3727
4810
  }
3728
4811
  var COPY_FEEDBACK_MS = 2500;
4812
+ 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";
4813
+ var ANSWER_ACTION_IDLE = "border-border bg-card text-muted-foreground hover:border-primary/40 hover:bg-accent hover:text-foreground";
4814
+ var ANSWER_ACTION_ACTIVE = "border-primary/40 bg-accent text-primary";
4815
+ function ReadAloudButton({
4816
+ strings: T,
4817
+ text,
4818
+ lang
4819
+ }) {
4820
+ const speech = useSpeechOutput({ lang });
4821
+ if (!speech.supported) return null;
4822
+ const label = speech.speaking ? T.stopReading : T.readAloud;
4823
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4824
+ "button",
4825
+ {
4826
+ "data-boff-explore": "read-aloud",
4827
+ "data-speaking": speech.speaking ? "true" : "false",
4828
+ type: "button",
4829
+ title: label,
4830
+ "aria-pressed": speech.speaking,
4831
+ onClick: () => speech.toggle(text),
4832
+ className: cn(
4833
+ ANSWER_ACTION_PILL,
4834
+ speech.speaking ? ANSWER_ACTION_ACTIVE : ANSWER_ACTION_IDLE
4835
+ ),
4836
+ children: [
4837
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Volume2, { className: "h-3 w-3 shrink-0", "aria-hidden": "true" }),
4838
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: label })
4839
+ ]
4840
+ }
4841
+ );
4842
+ }
3729
4843
  function AnswerActions({
3730
4844
  strings: T,
3731
- text
4845
+ text,
4846
+ speechText,
4847
+ speechLang
3732
4848
  }) {
3733
4849
  const [outcome, setOutcome] = React.useState("idle");
3734
4850
  const resetRef = React.useRef(null);
@@ -3771,9 +4887,8 @@ function AnswerActions({
3771
4887
  void copy();
3772
4888
  },
3773
4889
  className: cn(
3774
- "inline-flex max-w-full items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs transition-colors",
3775
- "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
3776
- outcome === "failed" ? "border-destructive/40 bg-card text-destructive" : "border-border bg-card text-muted-foreground hover:border-primary/40 hover:bg-accent hover:text-foreground"
4890
+ ANSWER_ACTION_PILL,
4891
+ outcome === "failed" ? "border-destructive/40 bg-card text-destructive" : ANSWER_ACTION_IDLE
3777
4892
  ),
3778
4893
  children: [
3779
4894
  outcome === "copied" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-3 w-3 shrink-0", "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { className: "h-3 w-3 shrink-0", "aria-hidden": "true" }),
@@ -3781,6 +4896,7 @@ function AnswerActions({
3781
4896
  ]
3782
4897
  }
3783
4898
  ),
4899
+ /* @__PURE__ */ jsxRuntime.jsx(ReadAloudButton, { strings: T, text: speechText, lang: speechLang }),
3784
4900
  /* @__PURE__ */ jsxRuntime.jsx("span", { "data-boff-explore": "copy-status", role: "status", className: "sr-only", children: outcome === "idle" ? "" : label })
3785
4901
  ]
3786
4902
  }
@@ -3832,34 +4948,20 @@ function AssistantBubble({
3832
4948
  activity,
3833
4949
  imageOrigins,
3834
4950
  productNames,
4951
+ speechLang,
3835
4952
  onNavigate,
3836
4953
  anchorRef,
3837
4954
  onFollow,
3838
4955
  onFollowUp,
3839
- onFeedback
4956
+ onFeedback,
4957
+ onEscalate,
4958
+ isLatestAnswer = false
3840
4959
  }) {
3841
4960
  const streaming = message.status === "PENDING" || message.status === "RUNNING";
3842
4961
  const body = message.content || message.partialContent || "";
3843
4962
  const failed = message.status === "FAILED";
3844
4963
  const completed = message.status === "COMPLETED";
3845
- const [failedImages, setFailedImages] = React.useState(
3846
- () => /* @__PURE__ */ new Set()
3847
- );
3848
- const markImageFailed = React.useCallback((src) => {
3849
- setFailedImages(
3850
- (current) => current.has(src) ? current : new Set(current).add(src)
3851
- );
3852
- }, []);
3853
- const imageScope = React.useMemo(
3854
- () => ({
3855
- strings: T,
3856
- enabled: completed,
3857
- origins: imageOrigins,
3858
- failed: failedImages,
3859
- onFailed: markImageFailed
3860
- }),
3861
- [T, completed, imageOrigins, failedImages, markImageFailed]
3862
- );
4964
+ const visible = useProgressiveReveal(body, !streaming);
3863
4965
  const provenance = React.useMemo(
3864
4966
  () => summariseReferences(message.references, imageOrigins, productNames),
3865
4967
  [message.references, imageOrigins, productNames]
@@ -3868,7 +4970,25 @@ function AssistantBubble({
3868
4970
  () => answerPlainText(message.content),
3869
4971
  [message.content]
3870
4972
  );
4973
+ const speechText = React.useMemo(
4974
+ () => answerSpeechText(message.content),
4975
+ [message.content]
4976
+ );
3871
4977
  const grounded = completed && message.answered !== false;
4978
+ const [escalateOpen, setEscalateOpen] = React.useState(false);
4979
+ const [escalateBusy, setEscalateBusy] = React.useState(false);
4980
+ const escalateTriggerRef = React.useRef(null);
4981
+ const escalateFormId = `boff-explore-escalate-${message.id}`;
4982
+ const unanswered = completed && message.answered === false;
4983
+ const offerEscalate = Boolean(onEscalate) && completed;
4984
+ const showQuietEscalate = offerEscalate && !unanswered && isLatestAnswer;
4985
+ const closeEscalate = React.useCallback(() => {
4986
+ setEscalateOpen(false);
4987
+ escalateTriggerRef.current?.focus();
4988
+ }, []);
4989
+ const handleEscalateBusy = React.useCallback((busy) => {
4990
+ setEscalateBusy(busy);
4991
+ }, []);
3872
4992
  return /* @__PURE__ */ jsxRuntime.jsxs(
3873
4993
  "div",
3874
4994
  {
@@ -3886,7 +5006,7 @@ function AssistantBubble({
3886
5006
  }
3887
5007
  ),
3888
5008
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1 space-y-3", children: [
3889
- message.status === "COMPLETED" && message.answered === false ? /* @__PURE__ */ jsxRuntime.jsxs(
5009
+ unanswered ? /* @__PURE__ */ jsxRuntime.jsxs(
3890
5010
  "div",
3891
5011
  {
3892
5012
  "data-boff-explore": "no-answer",
@@ -3896,26 +5016,56 @@ function AssistantBubble({
3896
5016
  /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
3897
5017
  T.noAnswer,
3898
5018
  " ",
3899
- /* @__PURE__ */ jsxRuntime.jsx(
3900
- "a",
3901
- {
3902
- href: "/contact",
3903
- className: "underline underline-offset-2 hover:text-primary",
3904
- onClick: (event) => {
3905
- if (!onNavigate) return;
3906
- event.preventDefault();
3907
- onNavigate("/contact");
3908
- },
3909
- children: T.askHuman
3910
- }
5019
+ offerEscalate ? (
5020
+ /* The point of the wave: the same words, but they now open a form that
5021
+ sends the conversation we are both looking at, instead of a link to a
5022
+ blank contact page that asks the visitor to type it all again. */
5023
+ /* @__PURE__ */ jsxRuntime.jsx(
5024
+ "button",
5025
+ {
5026
+ ref: escalateTriggerRef,
5027
+ "data-boff-explore": "escalate-open",
5028
+ type: "button",
5029
+ "aria-expanded": escalateOpen,
5030
+ "aria-controls": escalateOpen ? escalateFormId : void 0,
5031
+ onClick: () => setEscalateOpen(true),
5032
+ className: "rounded-sm underline underline-offset-2 hover:text-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
5033
+ children: T.askHuman
5034
+ }
5035
+ )
5036
+ ) : (
5037
+ /* No conversation to hand over (a shared, read-only view). The old dead end
5038
+ is still better than a control whose submit could only fail. */
5039
+ /* @__PURE__ */ jsxRuntime.jsx(
5040
+ "a",
5041
+ {
5042
+ href: "/contact",
5043
+ className: "underline underline-offset-2 hover:text-primary",
5044
+ onClick: (event) => {
5045
+ if (!onNavigate) return;
5046
+ event.preventDefault();
5047
+ onNavigate("/contact");
5048
+ },
5049
+ children: T.askHuman
5050
+ }
5051
+ )
3911
5052
  ),
3912
5053
  "."
3913
5054
  ] })
3914
5055
  ]
3915
5056
  }
3916
5057
  ) : null,
3917
- body ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-2xl rounded-tl-md border border-border bg-card px-4 py-3 shadow-sm", children: [
3918
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "boff-prose boff-prose-sm boff-explore-body min-w-0 text-card-foreground", children: /* @__PURE__ */ jsxRuntime.jsx(InlineImageScopeContext.Provider, { value: imageScope, children: /* @__PURE__ */ jsxRuntime.jsx(Markdown, { components: ANSWER_MARKDOWN_COMPONENTS, children: body }) }) }),
5058
+ visible ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-2xl rounded-tl-md border border-border bg-card px-4 py-3 shadow-sm", children: [
5059
+ /* @__PURE__ */ jsxRuntime.jsx(
5060
+ ExploreAnswerBody,
5061
+ {
5062
+ strings: T,
5063
+ imageOrigins,
5064
+ enabled: completed,
5065
+ className: "boff-prose-sm text-card-foreground",
5066
+ children: visible
5067
+ }
5068
+ ),
3919
5069
  streaming ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "boff-explore-caret", "aria-hidden": "true" }) : null
3920
5070
  ] }) : null,
3921
5071
  grounded && provenance.total > 0 ? /* @__PURE__ */ jsxRuntime.jsx(ProvenanceChips, { strings: T, provenance }) : null,
@@ -3958,7 +5108,15 @@ function AssistantBubble({
3958
5108
  question
3959
5109
  )) })
3960
5110
  ] }) : null,
3961
- completed && copyText !== "" ? /* @__PURE__ */ jsxRuntime.jsx(AnswerActions, { strings: T, text: copyText }) : null,
5111
+ completed && copyText !== "" ? /* @__PURE__ */ jsxRuntime.jsx(
5112
+ AnswerActions,
5113
+ {
5114
+ strings: T,
5115
+ text: copyText,
5116
+ speechText,
5117
+ speechLang
5118
+ }
5119
+ ) : null,
3962
5120
  completed && onFeedback ? /* @__PURE__ */ jsxRuntime.jsx(
3963
5121
  AnswerFeedback,
3964
5122
  {
@@ -3967,6 +5125,30 @@ function AssistantBubble({
3967
5125
  feedback: message.feedback,
3968
5126
  onSubmit: onFeedback
3969
5127
  }
5128
+ ) : null,
5129
+ showQuietEscalate ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pt-0.5", children: /* @__PURE__ */ jsxRuntime.jsx(
5130
+ "button",
5131
+ {
5132
+ ref: escalateTriggerRef,
5133
+ "data-boff-explore": "escalate-open",
5134
+ type: "button",
5135
+ "aria-expanded": escalateOpen,
5136
+ "aria-controls": escalateOpen ? escalateFormId : void 0,
5137
+ disabled: escalateBusy,
5138
+ onClick: () => setEscalateOpen((open) => !open),
5139
+ 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",
5140
+ children: T.escalateOpen
5141
+ }
5142
+ ) }) : null,
5143
+ escalateOpen && onEscalate ? /* @__PURE__ */ jsxRuntime.jsx(
5144
+ EscalateForm,
5145
+ {
5146
+ strings: T,
5147
+ formId: escalateFormId,
5148
+ onEscalate,
5149
+ onClose: closeEscalate,
5150
+ onBusyChange: handleEscalateBusy
5151
+ }
3970
5152
  ) : null
3971
5153
  ] })
3972
5154
  ]
@@ -4406,15 +5588,6 @@ function WelcomeState({
4406
5588
  }
4407
5589
  )
4408
5590
  ] }) : null,
4409
- /* @__PURE__ */ jsxRuntime.jsx(
4410
- ConceptStrip,
4411
- {
4412
- strings: T,
4413
- highlights: conceptHighlights,
4414
- onAsk: onPrompt,
4415
- disabled
4416
- }
4417
- ),
4418
5591
  products.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "boff-explore-rise boff-explore-rise-3 mt-6", children: [
4419
5592
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: T.focusProduct }),
4420
5593
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4426,7 +5599,16 @@ function WelcomeState({
4426
5599
  onFocus
4427
5600
  }
4428
5601
  )
4429
- ] }) : null
5602
+ ] }) : null,
5603
+ /* @__PURE__ */ jsxRuntime.jsx(
5604
+ ConceptStrip,
5605
+ {
5606
+ strings: T,
5607
+ highlights: conceptHighlights,
5608
+ onAsk: onPrompt,
5609
+ disabled
5610
+ }
5611
+ )
4430
5612
  ] });
4431
5613
  }
4432
5614
  function ExplorePage({
@@ -4452,6 +5634,7 @@ function ExplorePage({
4452
5634
  );
4453
5635
  const T = React.useMemo(() => exploreStrings(activeLocale), [activeLocale]);
4454
5636
  const rtl = isRtlLocale(activeLocale);
5637
+ const speechLang = speechLocaleOf(activeLocale);
4455
5638
  const {
4456
5639
  phase,
4457
5640
  catalog,
@@ -4472,6 +5655,8 @@ function ExplorePage({
4472
5655
  clearHistory,
4473
5656
  recordClick,
4474
5657
  submitFeedback,
5658
+ escalate,
5659
+ canEscalate,
4475
5660
  createShareLink,
4476
5661
  isSharedView
4477
5662
  } = useExploreChat({
@@ -4556,11 +5741,11 @@ function ExplorePage({
4556
5741
  const layoutHeight = window.innerHeight;
4557
5742
  if (layoutHeight === 0) return;
4558
5743
  const top = Math.round(
4559
- clamp(el.getBoundingClientRect().top, 0, layoutHeight)
5744
+ clamp2(el.getBoundingClientRect().top, 0, layoutHeight)
4560
5745
  );
4561
5746
  const viewport = window.visualViewport;
4562
5747
  const inset = viewport ? Math.round(
4563
- clamp(
5748
+ clamp2(
4564
5749
  layoutHeight - viewport.height - viewport.offsetTop,
4565
5750
  0,
4566
5751
  layoutHeight
@@ -4841,6 +6026,7 @@ function ExplorePage({
4841
6026
  const text = draft.trim();
4842
6027
  if (!text || overLimit || busy || !canSend) return;
4843
6028
  stopDictation();
6029
+ stopSpeaking();
4844
6030
  setDraft("");
4845
6031
  stickToBottomRef.current = true;
4846
6032
  void send(text, toPayload(stagedRef.current));
@@ -4851,6 +6037,7 @@ function ExplorePage({
4851
6037
  const sendPrompt = React.useCallback(
4852
6038
  (prompt) => {
4853
6039
  if (!canSend || busy) return;
6040
+ stopSpeaking();
4854
6041
  setDraft("");
4855
6042
  stickToBottomRef.current = true;
4856
6043
  void send(prompt, toPayload(stagedRef.current));
@@ -4913,6 +6100,7 @@ function ExplorePage({
4913
6100
  ),
4914
6101
  /* @__PURE__ */ jsxRuntime.jsx(ProseStyles, {}),
4915
6102
  /* @__PURE__ */ jsxRuntime.jsx(ExploreStyles, {}),
6103
+ /* @__PURE__ */ jsxRuntime.jsx(ExploreAnswerStyles, {}),
4916
6104
  !showWelcome ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2.5 border-b border-border px-4 py-2.5", children: [
4917
6105
  /* @__PURE__ */ jsxRuntime.jsx(
4918
6106
  "span",
@@ -5086,11 +6274,14 @@ function ExplorePage({
5086
6274
  activity,
5087
6275
  imageOrigins,
5088
6276
  productNames,
6277
+ speechLang,
5089
6278
  onNavigate,
5090
6279
  anchorRef: message.id === latestAssistantId ? latestAssistantRef : void 0,
5091
6280
  onFollow: (kind, url) => recordClick(message.id, kind, url),
5092
6281
  onFollowUp: sendPrompt,
5093
- onFeedback: isSharedView ? void 0 : (rating, detail) => submitFeedback(message.id, rating, detail)
6282
+ onFeedback: isSharedView ? void 0 : (rating, detail) => submitFeedback(message.id, rating, detail),
6283
+ onEscalate: canEscalate ? escalate : void 0,
6284
+ isLatestAnswer: message.id === latestAssistantId
5094
6285
  },
5095
6286
  message.id
5096
6287
  )
@@ -5197,10 +6388,19 @@ function ExplorePage({
5197
6388
  /* @__PURE__ */ jsxRuntime.jsxs(
5198
6389
  "div",
5199
6390
  {
6391
+ "data-boff-explore": "composer-field",
5200
6392
  className: cn(
5201
6393
  "flex min-w-0 items-end gap-2 rounded-2xl border bg-card p-2 shadow-sm transition-all",
5202
6394
  busy && "boff-explore-busy",
5203
- overLimit ? "border-destructive/60" : "border-border focus-within:border-primary/40 focus-within:ring-1 focus-within:ring-ring"
6395
+ overLimit ? "border-destructive/60" : "border-border focus-within:border-primary/40 focus-within:ring-1 focus-within:ring-ring",
6396
+ // The halo is an invitation, and an invitation is only useful before it has
6397
+ // been accepted: while the conversation is empty, the composer is usable, and
6398
+ // nothing has been typed. Once any of those stops being true the class goes
6399
+ // and the box is an ordinary card again. `-quiet` keeps the ring but stops the
6400
+ // movement the moment there are characters in the field, so it does not pulse
6401
+ // at someone who is mid-sentence.
6402
+ showWelcome && !composerDisabled && !overLimit && "boff-explore-invite",
6403
+ showWelcome && draft.length > 0 && "boff-explore-invite-quiet"
5204
6404
  ),
5205
6405
  children: [
5206
6406
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -5390,6 +6590,7 @@ function ExplorePage({
5390
6590
 
5391
6591
  exports.ExplorePage = ExplorePage;
5392
6592
  exports.answerPlainText = answerPlainText;
6593
+ exports.answerSpeechText = answerSpeechText;
5393
6594
  exports.inlineImageOrigins = inlineImageOrigins;
5394
6595
  exports.isInlineImageAllowed = isInlineImageAllowed;
5395
6596
  exports.logoUrlOf = logoUrlOf;