@burdenoff/website-sdk 2026.922.5 → 2026.923.2

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.
@@ -5,15 +5,481 @@ import ReactMarkdown from 'react-markdown';
5
5
  import rehypeSanitize from 'rehype-sanitize';
6
6
  import remarkGfm from 'remark-gfm';
7
7
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
8
+ import { clsx } from 'clsx';
9
+ import { twMerge } from 'tailwind-merge';
8
10
  import { Helmet } from 'react-helmet-async';
9
11
  import { Slot } from '@radix-ui/react-slot';
10
12
  import { cva } from 'class-variance-authority';
11
- import { clsx } from 'clsx';
12
- import { twMerge } from 'tailwind-merge';
13
13
 
14
14
  var __defProp = Object.defineProperty;
15
15
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
16
16
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
17
+ function MarkdownLink({
18
+ href,
19
+ children,
20
+ // react-markdown passes the hast node to every override; it is not a DOM attribute.
21
+ node: _node,
22
+ ...props
23
+ }) {
24
+ const isExternal = typeof href === "string" && (href.startsWith("http://") || href.startsWith("https://"));
25
+ return /* @__PURE__ */ jsx(
26
+ "a",
27
+ {
28
+ href,
29
+ ...props,
30
+ ...isExternal ? { target: "_blank", rel: "noopener noreferrer" } : { rel: "noopener noreferrer" },
31
+ children
32
+ }
33
+ );
34
+ }
35
+ var defaultComponents = {
36
+ a: MarkdownLink
37
+ };
38
+ function Markdown({
39
+ children,
40
+ className,
41
+ inline,
42
+ components
43
+ }) {
44
+ const content = /* @__PURE__ */ jsx(
45
+ ReactMarkdown,
46
+ {
47
+ remarkPlugins: [remarkGfm],
48
+ rehypePlugins: [rehypeSanitize],
49
+ components: { ...defaultComponents, ...components },
50
+ ...inline ? { disallowedElements: ["p"], unwrapDisallowed: true } : {},
51
+ children
52
+ }
53
+ );
54
+ if (inline) {
55
+ return /* @__PURE__ */ jsx("span", { className, children: content });
56
+ }
57
+ return /* @__PURE__ */ jsx("div", { className, children: content });
58
+ }
59
+ function cn(...inputs) {
60
+ return twMerge(clsx(inputs));
61
+ }
62
+ var EXPLORE_ANSWER_CSS = `
63
+ /* Nothing inside an answer may widen the page. Model output carries long URLs, code and
64
+ tables \u2014 each of those overflows a phone by default, and one overflowing child makes the
65
+ WHOLE document horizontally scrollable, not just the bubble. */
66
+ .boff-explore-body,
67
+ .boff-explore-body p,
68
+ .boff-explore-body li,
69
+ .boff-explore-body a,
70
+ .boff-explore-body h1,
71
+ .boff-explore-body h2,
72
+ .boff-explore-body h3,
73
+ /* h4-h6 as well: a published answer page pushes the model's own headings down a level or
74
+ two so they nest under the page's, and a long unbroken word is no narrower for it. */
75
+ .boff-explore-body h4,
76
+ .boff-explore-body h5,
77
+ .boff-explore-body h6 {
78
+ overflow-wrap: anywhere;
79
+ word-break: break-word;
80
+ }
81
+ .boff-explore-body pre {
82
+ overflow-x: auto;
83
+ max-width: 100%;
84
+ }
85
+ .boff-explore-body table {
86
+ display: block;
87
+ overflow-x: auto;
88
+ max-width: 100%;
89
+ }
90
+ .boff-explore-body img {
91
+ max-width: 100%;
92
+ height: auto;
93
+ }
94
+
95
+ /* ---- Comparison and pricing tables ----------------------------------------------------
96
+ The harness emits a comparison, and a set of plans, as a GFM table whose FIRST column is
97
+ the subject \u2014 the product, or the plan (CONTRACT.md \xA77). Two things have to be true of it:
98
+ the page must not scroll sideways because of it, and it has to be readable on a 320px
99
+ phone, which a four-column grid of 1px borders is not.
100
+
101
+ Wide: a contained scroller. \`max-width: 100%\` keeps the box inside the bubble and
102
+ \`overflow-x: auto\` keeps the columns inside the box, so the widest possible table still
103
+ cannot widen the document. \`overscroll-behavior-x: contain\` stops a swipe that runs out
104
+ of table from chaining to the page \u2014 the same rule the transcript needed. */
105
+ .boff-explore-body .boff-explore-table {
106
+ max-width: 100%;
107
+ overflow-x: auto;
108
+ overscroll-behavior-x: contain;
109
+ margin: 0.75rem 0;
110
+ border: 1px solid hsl(var(--border));
111
+ border-radius: 0.75rem;
112
+ background-color: hsl(var(--card));
113
+ }
114
+ .boff-explore-body .boff-explore-table:focus-visible {
115
+ outline: 2px solid hsl(var(--ring));
116
+ outline-offset: 2px;
117
+ }
118
+ .boff-explore-body .boff-explore-table table {
119
+ /* Beats the blanket \`display: block\` above, which is the unwrapped fallback. */
120
+ display: table;
121
+ width: 100%;
122
+ max-width: none;
123
+ margin: 0;
124
+ border-collapse: collapse;
125
+ font-size: 0.8125rem;
126
+ }
127
+ .boff-explore-body .boff-explore-table th,
128
+ .boff-explore-body .boff-explore-table td {
129
+ border: 0;
130
+ border-top: 1px solid hsl(var(--border));
131
+ padding: 0.5rem 0.75rem;
132
+ /* \`start\`, never \`left\`: the page root carries dir="rtl" in Arabic, and a left-aligned
133
+ table there reads against the grain of every other line on the page. */
134
+ text-align: start;
135
+ vertical-align: top;
136
+ overflow-wrap: anywhere;
137
+ /* Prices in a column line up under each other instead of drifting by digit width. */
138
+ font-variant-numeric: tabular-nums;
139
+ min-width: 6rem;
140
+ }
141
+ .boff-explore-body .boff-explore-table thead th {
142
+ border-top: 0;
143
+ background-color: hsl(var(--muted));
144
+ color: hsl(var(--foreground));
145
+ font-weight: 600;
146
+ white-space: nowrap;
147
+ }
148
+ /* The first column is what the row is ABOUT \u2014 the product, or the plan name. Reading it as
149
+ the row's heading is what makes a price list scannable rather than a grid of strings. */
150
+ .boff-explore-body .boff-explore-table tbody td:first-child {
151
+ font-weight: 600;
152
+ color: hsl(var(--card-foreground));
153
+ }
154
+
155
+ /* Narrow: one card per row. Each cell prints its own column heading, so a value is never
156
+ stranded without the question it answers, and the subject cell becomes the card's title.
157
+ A media query rather than a measured width \u2014 these pages are serialised post-hydration,
158
+ so anything measured here would ship to every phone as static HTML. */
159
+ @media (max-width: 30rem) {
160
+ .boff-explore-body .boff-explore-table {
161
+ overflow-x: visible;
162
+ }
163
+ .boff-explore-body .boff-explore-table table,
164
+ .boff-explore-body .boff-explore-table tbody,
165
+ .boff-explore-body .boff-explore-table tr,
166
+ .boff-explore-body .boff-explore-table td {
167
+ display: block;
168
+ width: 100%;
169
+ min-width: 0;
170
+ }
171
+ .boff-explore-body .boff-explore-table thead {
172
+ display: none;
173
+ }
174
+ .boff-explore-body .boff-explore-table tr {
175
+ border-top: 1px solid hsl(var(--border));
176
+ padding: 0.5rem 0;
177
+ }
178
+ .boff-explore-body .boff-explore-table tr:first-child {
179
+ border-top: 0;
180
+ }
181
+ .boff-explore-body .boff-explore-table td {
182
+ border: 0;
183
+ padding: 0.125rem 0.75rem;
184
+ }
185
+ .boff-explore-body .boff-explore-table td[data-label]::before {
186
+ content: attr(data-label);
187
+ display: block;
188
+ font-size: 0.6875rem;
189
+ font-weight: 600;
190
+ text-transform: uppercase;
191
+ letter-spacing: 0.04em;
192
+ color: hsl(var(--muted-foreground));
193
+ }
194
+ .boff-explore-body .boff-explore-table tbody td:first-child {
195
+ font-size: 0.9375rem;
196
+ padding-bottom: 0.25rem;
197
+ }
198
+ .boff-explore-body .boff-explore-table tbody td:first-child::before {
199
+ /* The plan's own name is the heading here; repeating "Plan" over it says nothing. */
200
+ display: none;
201
+ }
202
+ }
203
+ `;
204
+ function ExploreAnswerStyles() {
205
+ return /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: EXPLORE_ANSWER_CSS } });
206
+ }
207
+ function inlineImageOrigins(products) {
208
+ const origins = /* @__PURE__ */ new Set();
209
+ for (const product of products) {
210
+ let url;
211
+ try {
212
+ url = new URL(product.website);
213
+ } catch {
214
+ continue;
215
+ }
216
+ if (url.protocol !== "https:") continue;
217
+ const apex = url.hostname.replace(/^www\./, "");
218
+ const port = url.port ? `:${url.port}` : "";
219
+ origins.add(`https://${apex}${port}`);
220
+ origins.add(`https://www.${apex}${port}`);
221
+ }
222
+ return origins;
223
+ }
224
+ function isInlineImageAllowed(src, origins) {
225
+ if (typeof src !== "string" || src === "") return false;
226
+ let url;
227
+ try {
228
+ url = new URL(src);
229
+ } catch {
230
+ return false;
231
+ }
232
+ if (url.protocol !== "https:" || url.username || url.password) return false;
233
+ return origins.has(url.origin);
234
+ }
235
+ var CODE_FENCE = /^\s*(?:```|~~~)/;
236
+ var TABLE_ROW_LINE = /^\s{0,3}\|/;
237
+ var TABLE_DELIMITER_ROW = /^\s{0,3}\|[\s:|-]*-[\s:|-]*$/;
238
+ function readableLine(raw, mode) {
239
+ let line = raw;
240
+ line = line.replace(/!\[[^\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g, "");
241
+ line = mode === "speech" ? (
242
+ // A voice reading "h-t-t-p-s colon slash slash…" mid-sentence is noise, and the
243
+ // prompt asks for two to five links in every answer. The label IS the words.
244
+ line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1")
245
+ ) : line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1 ($2)");
246
+ line = mode === "speech" ? line.replace(/<(https?:\/\/[^>\s]+)>/g, "") : line.replace(/<(https?:\/\/[^>\s]+)>/g, "$1");
247
+ line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
248
+ line = line.replace(/^\s{0,3}>\s?/, "");
249
+ if (/^\s{0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/.test(line)) return "";
250
+ line = line.replace(/`([^`]+)`/g, "$1");
251
+ line = line.replace(/\*\*([^*]+)\*\*/g, "$1");
252
+ line = line.replace(/__([^_]+)__/g, "$1");
253
+ line = line.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, "$1$2");
254
+ line = line.replace(/(^|[^_\w])_([^_\n]+)_(?![_\w])/g, "$1$2");
255
+ return line.replace(/[ \t]+$/, "");
256
+ }
257
+ function tableCells(row) {
258
+ const trimmed = row.trim();
259
+ const cells = [];
260
+ let cell = "";
261
+ for (let at = 0; at < trimmed.length; at += 1) {
262
+ const char = trimmed[at] ?? "";
263
+ if (char === "\\") {
264
+ cell += char + (trimmed[at + 1] ?? "");
265
+ at += 1;
266
+ continue;
267
+ }
268
+ if (char === "|") {
269
+ cells.push(cell.trim());
270
+ cell = "";
271
+ continue;
272
+ }
273
+ cell += char;
274
+ }
275
+ cells.push(cell.trim());
276
+ if (trimmed.startsWith("|") && cells[0] === "") cells.shift();
277
+ if (trimmed.endsWith("|") && cells[cells.length - 1] === "") cells.pop();
278
+ return cells;
279
+ }
280
+ function asSentence(text) {
281
+ return /[.!?]$/.test(text) ? text : `${text}.`;
282
+ }
283
+ function spokenTable(rows) {
284
+ if (rows.length < 2 || !TABLE_DELIMITER_ROW.test(rows[1] ?? "")) return rows;
285
+ const labels = tableCells(rows[0] ?? "");
286
+ const spoken = [];
287
+ for (const row of rows.slice(2)) {
288
+ const parts = [];
289
+ tableCells(row).forEach((value, column) => {
290
+ if (value === "") return;
291
+ const label = column === 0 ? "" : labels[column] ?? "";
292
+ parts.push(label === "" ? value : `${label}: ${value}`);
293
+ });
294
+ if (parts.length > 0) spoken.push(asSentence(parts.join(". ")));
295
+ }
296
+ return spoken;
297
+ }
298
+ function readableText(markdown, mode) {
299
+ const lines = [];
300
+ let inFence = false;
301
+ let table = [];
302
+ const flushTable = () => {
303
+ if (table.length === 0) return;
304
+ for (const line of spokenTable(table)) lines.push(readableLine(line, mode));
305
+ table = [];
306
+ };
307
+ for (const raw of markdown.split("\n")) {
308
+ if (CODE_FENCE.test(raw)) {
309
+ flushTable();
310
+ inFence = !inFence;
311
+ continue;
312
+ }
313
+ if (inFence) {
314
+ lines.push(raw);
315
+ continue;
316
+ }
317
+ if (mode === "speech" && TABLE_ROW_LINE.test(raw)) {
318
+ table.push(raw);
319
+ continue;
320
+ }
321
+ flushTable();
322
+ lines.push(readableLine(raw, mode));
323
+ }
324
+ flushTable();
325
+ return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
326
+ }
327
+ function answerPlainText(markdown) {
328
+ return readableText(markdown, "clipboard");
329
+ }
330
+ function answerSpeechText(markdown) {
331
+ return readableText(markdown, "speech");
332
+ }
333
+ var AnswerScopeContext = createContext(null);
334
+ var InsideLinkContext = createContext(false);
335
+ function AnswerLink(props) {
336
+ return /* @__PURE__ */ jsx(InsideLinkContext.Provider, { value: true, children: /* @__PURE__ */ jsx(MarkdownLink, { ...props }) });
337
+ }
338
+ var INLINE_IMAGE_FRAME = "relative inline-block max-w-full overflow-hidden rounded-xl border border-border bg-muted align-top shadow-sm";
339
+ function InlineConceptImage({ src, alt }) {
340
+ const scope = useContext(AnswerScopeContext);
341
+ const insideLink = useContext(InsideLinkContext);
342
+ if (!scope || !scope.enabled || !isInlineImageAllowed(src, scope.origins) || scope.failed.has(src)) {
343
+ return null;
344
+ }
345
+ const { strings: T, onFailed } = scope;
346
+ const picture = /* @__PURE__ */ jsxs(Fragment, { children: [
347
+ /* @__PURE__ */ jsx(
348
+ "img",
349
+ {
350
+ src,
351
+ alt: alt ?? "",
352
+ loading: "lazy",
353
+ decoding: "async",
354
+ onError: () => onFailed(src),
355
+ className: "block h-auto max-h-80 w-auto max-w-full object-contain"
356
+ }
357
+ ),
358
+ /* @__PURE__ */ jsx(
359
+ "span",
360
+ {
361
+ "aria-hidden": "true",
362
+ className: "pointer-events-none absolute left-2 top-2 rounded-full border border-border bg-background/90 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-foreground shadow-sm",
363
+ children: T.conceptIllustration
364
+ }
365
+ )
366
+ ] });
367
+ return /* @__PURE__ */ jsx("span", { "data-boff-explore": "inline-image", className: "my-3 block", children: insideLink ? /* @__PURE__ */ jsx("span", { className: INLINE_IMAGE_FRAME, children: picture }) : /* @__PURE__ */ jsxs(
368
+ "a",
369
+ {
370
+ href: src,
371
+ target: "_blank",
372
+ rel: "noopener noreferrer",
373
+ className: cn(
374
+ INLINE_IMAGE_FRAME,
375
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
376
+ ),
377
+ children: [
378
+ picture,
379
+ /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
380
+ "(",
381
+ T.opensInNewTab,
382
+ ")"
383
+ ] })
384
+ ]
385
+ }
386
+ ) });
387
+ }
388
+ function markdownText(node) {
389
+ if (!node) return "";
390
+ if (typeof node.value === "string") return node.value;
391
+ return (node.children ?? []).map(markdownText).join("");
392
+ }
393
+ function hasHeaderCells(node) {
394
+ return (node?.children ?? []).some((cell) => cell.tagName === "th");
395
+ }
396
+ function findHeaderRow(node) {
397
+ for (const child of node?.children ?? []) {
398
+ if (child.tagName === "tr" && hasHeaderCells(child)) return child;
399
+ const nested = findHeaderRow(child);
400
+ if (nested !== null) return nested;
401
+ }
402
+ return null;
403
+ }
404
+ function tableColumnLabels(node) {
405
+ const row = findHeaderRow(node);
406
+ if (row === null) return [];
407
+ return (row.children ?? []).filter((cell) => cell.tagName === "th").map((cell) => markdownText(cell).trim());
408
+ }
409
+ var TableColumnsContext = createContext([]);
410
+ function AnswerTable({ node, children }) {
411
+ const scope = useContext(AnswerScopeContext);
412
+ const table = /* @__PURE__ */ jsx(TableColumnsContext.Provider, { value: tableColumnLabels(node), children: /* @__PURE__ */ jsx("table", { children }) });
413
+ if (!scope) return table;
414
+ return /* @__PURE__ */ jsx(
415
+ "div",
416
+ {
417
+ "data-boff-explore": "table",
418
+ role: "region",
419
+ "aria-label": scope.strings.answerTable,
420
+ tabIndex: 0,
421
+ className: "boff-explore-table",
422
+ children: table
423
+ }
424
+ );
425
+ }
426
+ function AnswerTableRow({
427
+ node,
428
+ children,
429
+ ...props
430
+ }) {
431
+ const labels = useContext(TableColumnsContext);
432
+ if (labels.length === 0 || hasHeaderCells(node)) {
433
+ return /* @__PURE__ */ jsx("tr", { ...props, children });
434
+ }
435
+ let column = 0;
436
+ const cells = Children.map(children, (cell) => {
437
+ if (!isValidElement(cell)) return cell;
438
+ const label = labels[column] ?? "";
439
+ column += 1;
440
+ if (label === "") return cell;
441
+ return cloneElement(cell, {
442
+ "data-label": label
443
+ });
444
+ });
445
+ return /* @__PURE__ */ jsx("tr", { ...props, children: cells });
446
+ }
447
+ var ANSWER_MARKDOWN_COMPONENTS = {
448
+ a: AnswerLink,
449
+ img: InlineConceptImage,
450
+ table: AnswerTable,
451
+ tr: AnswerTableRow
452
+ };
453
+ function ExploreAnswerBody({
454
+ children,
455
+ strings,
456
+ imageOrigins,
457
+ enabled = true,
458
+ components,
459
+ className
460
+ }) {
461
+ const [failed, setFailed] = useState(() => /* @__PURE__ */ new Set());
462
+ const onFailed = useCallback((src) => {
463
+ setFailed(
464
+ (current) => current.has(src) ? current : new Set(current).add(src)
465
+ );
466
+ }, []);
467
+ const scope = useMemo(
468
+ () => ({
469
+ strings,
470
+ enabled,
471
+ origins: imageOrigins,
472
+ failed,
473
+ onFailed
474
+ }),
475
+ [strings, enabled, imageOrigins, failed, onFailed]
476
+ );
477
+ const overrides = useMemo(
478
+ () => components ? { ...ANSWER_MARKDOWN_COMPONENTS, ...components } : ANSWER_MARKDOWN_COMPONENTS,
479
+ [components]
480
+ );
481
+ return /* @__PURE__ */ jsx("div", { className: cn("boff-prose boff-explore-body min-w-0", className), children: /* @__PURE__ */ jsx(AnswerScopeContext.Provider, { value: scope, children: /* @__PURE__ */ jsx(Markdown, { components: overrides, children }) }) });
482
+ }
17
483
 
18
484
  // src/components/explore-i18n.ts
19
485
  var EXPLORE_LOCALES = ["en", "zh", "hi", "es", "ar", "ta"];
@@ -32,7 +498,7 @@ var EN = {
32
498
  showLess: "Show less",
33
499
  sources: "Sources",
34
500
  nextYouCouldAsk: "Next you could ask",
35
- placeholder: "Describe the problem you're facing\u2026",
501
+ placeholder: "Describe the problem\u2026",
36
502
  placeholderProduct: "Describe a problem, or ask about {product}\u2026",
37
503
  send: "Send",
38
504
  stop: "Stop",
@@ -73,6 +539,20 @@ var EN = {
73
539
  feedbackDismiss: "Dismiss",
74
540
  feedbackThanks: "Thanks \u2014 that helps.",
75
541
  feedbackNotSent: "That didn't send.",
542
+ escalateOpen: "Talk to a person",
543
+ escalateTitle: "Send this conversation to a person",
544
+ escalateBody: "We'll pass the whole conversation on, so you don't have to type any of it again.",
545
+ escalateEmailLabel: "Your email address",
546
+ escalateEmailPlaceholder: "you@example.com",
547
+ escalateNoteLabel: "Anything else a person should know? (optional)",
548
+ escalateSubmit: "Send to a person",
549
+ escalateSending: "Sending\u2026",
550
+ escalateCancel: "Cancel",
551
+ escalateInvalidEmail: "That doesn't look like an address we can reply to. Please check it.",
552
+ escalateSent: "A person now has this conversation. We'll reply to {email}.",
553
+ escalateRateLimited: "That's a lot of requests in a short time. Please try again in a few minutes.",
554
+ escalateCaptcha: "We couldn't verify that you're human. Please try that again.",
555
+ escalateFailed: "We couldn't pass that on just now. Please try again.",
76
556
  prevSuggestion: "Previous suggestion",
77
557
  nextSuggestion: "Next suggestion",
78
558
  conceptIllustration: "Concept illustration",
@@ -120,7 +600,7 @@ var ZH = {
120
600
  showLess: "\u6536\u8D77",
121
601
  sources: "\u6765\u6E90",
122
602
  nextYouCouldAsk: "\u4F60\u8FD8\u53EF\u4EE5\u95EE",
123
- placeholder: "\u63CF\u8FF0\u4F60\u9047\u5230\u7684\u95EE\u9898\u2026",
603
+ placeholder: "\u63CF\u8FF0\u4F60\u7684\u95EE\u9898\u2026",
124
604
  placeholderProduct: "\u63CF\u8FF0\u95EE\u9898\uFF0C\u6216\u8BE2\u95EE {product}\u2026",
125
605
  send: "\u53D1\u9001",
126
606
  stop: "\u505C\u6B62",
@@ -158,6 +638,20 @@ var ZH = {
158
638
  feedbackDismiss: "\u5173\u95ED",
159
639
  feedbackThanks: "\u8C22\u8C22\uFF0C\u8FD9\u5BF9\u6211\u4EEC\u5F88\u6709\u5E2E\u52A9\u3002",
160
640
  feedbackNotSent: "\u6CA1\u6709\u63D0\u4EA4\u6210\u529F\u3002",
641
+ escalateOpen: "\u8054\u7CFB\u771F\u4EBA",
642
+ escalateTitle: "\u628A\u8FD9\u6BB5\u5BF9\u8BDD\u8F6C\u7ED9\u771F\u4EBA",
643
+ escalateBody: "\u6211\u4EEC\u4F1A\u628A\u6574\u6BB5\u5BF9\u8BDD\u4E00\u8D77\u8F6C\u8FC7\u53BB\uFF0C\u60A8\u4E0D\u7528\u518D\u91CD\u65B0\u6253\u4E00\u904D\u3002",
644
+ escalateEmailLabel: "\u60A8\u7684\u90AE\u7BB1\u5730\u5740",
645
+ escalateEmailPlaceholder: "you@example.com",
646
+ escalateNoteLabel: "\u8FD8\u6709\u4EC0\u4E48\u9700\u8981\u544A\u8BC9\u6211\u4EEC\u7684\u5417\uFF1F\uFF08\u9009\u586B\uFF09",
647
+ escalateSubmit: "\u8F6C\u7ED9\u771F\u4EBA",
648
+ escalateSending: "\u6B63\u5728\u53D1\u9001\u2026",
649
+ escalateCancel: "\u53D6\u6D88",
650
+ escalateInvalidEmail: "\u8FD9\u4F3C\u4E4E\u4E0D\u662F\u4E00\u4E2A\u6211\u4EEC\u53EF\u4EE5\u56DE\u590D\u7684\u90AE\u7BB1\u5730\u5740\uFF0C\u8BF7\u68C0\u67E5\u4E00\u4E0B\u3002",
651
+ escalateSent: "\u5DF2\u6709\u540C\u4E8B\u63A5\u624B\u8FD9\u6BB5\u5BF9\u8BDD\uFF0C\u6211\u4EEC\u4F1A\u56DE\u590D\u5230 {email}\u3002",
652
+ escalateRateLimited: "\u77ED\u65F6\u95F4\u5185\u8BF7\u6C42\u8FC7\u591A\uFF0C\u8BF7\u51E0\u5206\u949F\u540E\u518D\u8BD5\u3002",
653
+ escalateCaptcha: "\u6211\u4EEC\u65E0\u6CD5\u9A8C\u8BC1\u60A8\u662F\u771F\u4EBA\uFF0C\u8BF7\u518D\u8BD5\u4E00\u6B21\u3002",
654
+ escalateFailed: "\u6682\u65F6\u65E0\u6CD5\u8F6C\u4EA4\uFF0C\u8BF7\u518D\u8BD5\u4E00\u6B21\u3002",
161
655
  prevSuggestion: "\u4E0A\u4E00\u4E2A\u5EFA\u8BAE",
162
656
  nextSuggestion: "\u4E0B\u4E00\u4E2A\u5EFA\u8BAE",
163
657
  conceptIllustration: "\u6982\u5FF5\u793A\u610F\u56FE",
@@ -201,7 +695,7 @@ var HI = {
201
695
  showLess: "\u0915\u092E \u0926\u093F\u0916\u093E\u090F\u0901",
202
696
  sources: "\u0938\u094D\u0930\u094B\u0924",
203
697
  nextYouCouldAsk: "\u0906\u0917\u0947 \u0906\u092A \u092A\u0942\u091B \u0938\u0915\u0924\u0947 \u0939\u0948\u0902",
204
- placeholder: "\u0905\u092A\u0928\u0940 \u0938\u092E\u0938\u094D\u092F\u093E \u092F\u0939\u093E\u0901 \u092C\u0924\u093E\u090F\u0902\u2026",
698
+ placeholder: "\u0905\u092A\u0928\u0940 \u0938\u092E\u0938\u094D\u092F\u093E \u092C\u0924\u093E\u090F\u0902\u2026",
205
699
  placeholderProduct: "\u0938\u092E\u0938\u094D\u092F\u093E \u092C\u0924\u093E\u090F\u0902, \u092F\u093E {product} \u0915\u0947 \u092C\u093E\u0930\u0947 \u092E\u0947\u0902 \u092A\u0942\u091B\u0947\u0902\u2026",
206
700
  send: "\u092D\u0947\u091C\u0947\u0902",
207
701
  stop: "\u0930\u094B\u0915\u0947\u0902",
@@ -239,6 +733,20 @@ var HI = {
239
733
  feedbackDismiss: "\u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
240
734
  feedbackThanks: "\u0927\u0928\u094D\u092F\u0935\u093E\u0926, \u0907\u0938\u0938\u0947 \u092E\u0926\u0926 \u092E\u093F\u0932\u0924\u0940 \u0939\u0948\u0964",
241
735
  feedbackNotSent: "\u092F\u0939 \u092D\u0947\u091C\u093E \u0928\u0939\u0940\u0902 \u091C\u093E \u0938\u0915\u093E\u0964",
736
+ escalateOpen: "\u0915\u093F\u0938\u0940 \u0935\u094D\u092F\u0915\u094D\u0924\u093F \u0938\u0947 \u092C\u093E\u0924 \u0915\u0930\u0947\u0902",
737
+ escalateTitle: "\u092F\u0939 \u092C\u093E\u0924\u091A\u0940\u0924 \u0915\u093F\u0938\u0940 \u0935\u094D\u092F\u0915\u094D\u0924\u093F \u0915\u094B \u092D\u0947\u091C\u0947\u0902",
738
+ escalateBody: "\u0939\u092E \u092A\u0942\u0930\u0940 \u092C\u093E\u0924\u091A\u0940\u0924 \u0906\u0917\u0947 \u092D\u0947\u091C \u0926\u0947\u0902\u0917\u0947, \u0924\u093E\u0915\u093F \u0906\u092A\u0915\u094B \u0926\u094B\u092C\u093E\u0930\u093E \u0915\u0941\u091B \u0932\u093F\u0916\u0928\u093E \u0928 \u092A\u0921\u093C\u0947\u0964",
739
+ escalateEmailLabel: "\u0906\u092A\u0915\u093E \u0908\u092E\u0947\u0932 \u092A\u0924\u093E",
740
+ escalateEmailPlaceholder: "you@example.com",
741
+ escalateNoteLabel: "\u0915\u094D\u092F\u093E \u0915\u0941\u091B \u0914\u0930 \u092C\u0924\u093E\u0928\u093E \u091A\u093E\u0939\u0947\u0902\u0917\u0947? (\u0935\u0948\u0915\u0932\u094D\u092A\u093F\u0915)",
742
+ escalateSubmit: "\u0915\u093F\u0938\u0940 \u0935\u094D\u092F\u0915\u094D\u0924\u093F \u0915\u094B \u092D\u0947\u091C\u0947\u0902",
743
+ escalateSending: "\u092D\u0947\u091C\u093E \u091C\u093E \u0930\u0939\u093E \u0939\u0948\u2026",
744
+ escalateCancel: "\u0930\u0926\u094D\u0926 \u0915\u0930\u0947\u0902",
745
+ escalateInvalidEmail: "\u092F\u0939 \u0910\u0938\u093E \u0908\u092E\u0947\u0932 \u092A\u0924\u093E \u0928\u0939\u0940\u0902 \u0932\u0917\u0924\u093E \u091C\u093F\u0938 \u092A\u0930 \u0939\u092E \u091C\u0935\u093E\u092C \u092D\u0947\u091C \u0938\u0915\u0947\u0902\u0964 \u0915\u0943\u092A\u092F\u093E \u091C\u093E\u0901\u091A \u0932\u0947\u0902\u0964",
746
+ escalateSent: "\u0905\u092C \u092F\u0939 \u092C\u093E\u0924\u091A\u0940\u0924 \u090F\u0915 \u0935\u094D\u092F\u0915\u094D\u0924\u093F \u0915\u0947 \u092A\u093E\u0938 \u0939\u0948\u0964 \u0939\u092E {email} \u092A\u0930 \u091C\u0935\u093E\u092C \u0926\u0947\u0902\u0917\u0947\u0964",
747
+ escalateRateLimited: "\u0925\u094B\u0921\u093C\u0947 \u0938\u092E\u092F \u092E\u0947\u0902 \u092C\u0939\u0941\u0924 \u0938\u093E\u0930\u0947 \u0905\u0928\u0941\u0930\u094B\u0927 \u0939\u094B \u0917\u090F\u0964 \u0915\u0943\u092A\u092F\u093E \u0915\u0941\u091B \u092E\u093F\u0928\u091F \u092C\u093E\u0926 \u0915\u094B\u0936\u093F\u0936 \u0915\u0930\u0947\u0902\u0964",
748
+ escalateCaptcha: "\u0939\u092E \u092F\u0939 \u0938\u0924\u094D\u092F\u093E\u092A\u093F\u0924 \u0928\u0939\u0940\u0902 \u0915\u0930 \u0938\u0915\u0947 \u0915\u093F \u0906\u092A \u090F\u0915 \u0935\u094D\u092F\u0915\u094D\u0924\u093F \u0939\u0948\u0902\u0964 \u0915\u0943\u092A\u092F\u093E \u092B\u093F\u0930 \u0915\u094B\u0936\u093F\u0936 \u0915\u0930\u0947\u0902\u0964",
749
+ escalateFailed: "\u0905\u092D\u0940 \u0907\u0938\u0947 \u0906\u0917\u0947 \u0928\u0939\u0940\u0902 \u092D\u0947\u091C\u093E \u091C\u093E \u0938\u0915\u093E\u0964 \u0915\u0943\u092A\u092F\u093E \u092B\u093F\u0930 \u0915\u094B\u0936\u093F\u0936 \u0915\u0930\u0947\u0902\u0964",
242
750
  prevSuggestion: "\u092A\u093F\u091B\u0932\u093E \u0938\u0941\u091D\u093E\u0935",
243
751
  nextSuggestion: "\u0905\u0917\u0932\u093E \u0938\u0941\u091D\u093E\u0935",
244
752
  conceptIllustration: "\u0905\u0935\u0927\u093E\u0930\u0923\u093E \u091A\u093F\u0924\u094D\u0930\u0923",
@@ -286,7 +794,7 @@ var ES = {
286
794
  showLess: "Ver menos",
287
795
  sources: "Fuentes",
288
796
  nextYouCouldAsk: "Tambi\xE9n puedes preguntar",
289
- placeholder: "Describe el problema que tienes\u2026",
797
+ placeholder: "Describe el problema\u2026",
290
798
  placeholderProduct: "Describe un problema o pregunta por {product}\u2026",
291
799
  send: "Enviar",
292
800
  stop: "Detener",
@@ -324,6 +832,20 @@ var ES = {
324
832
  feedbackDismiss: "Cerrar",
325
833
  feedbackThanks: "Gracias, nos sirve.",
326
834
  feedbackNotSent: "No se ha podido enviar.",
835
+ escalateOpen: "Hablar con una persona",
836
+ escalateTitle: "Env\xEDa esta conversaci\xF3n a una persona",
837
+ escalateBody: "Le pasaremos la conversaci\xF3n entera, as\xED no tienes que volver a escribirla.",
838
+ escalateEmailLabel: "Tu direcci\xF3n de correo",
839
+ escalateEmailPlaceholder: "tu@ejemplo.com",
840
+ escalateNoteLabel: "\xBFAlgo m\xE1s que deber\xEDamos saber? (opcional)",
841
+ escalateSubmit: "Enviar a una persona",
842
+ escalateSending: "Enviando\u2026",
843
+ escalateCancel: "Cancelar",
844
+ escalateInvalidEmail: "No parece una direcci\xF3n a la que podamos responder. Rev\xEDsala, por favor.",
845
+ escalateSent: "Una persona ya tiene esta conversaci\xF3n. Te responderemos a {email}.",
846
+ escalateRateLimited: "Han sido muchas peticiones en poco tiempo. Int\xE9ntalo de nuevo en unos minutos.",
847
+ escalateCaptcha: "No hemos podido verificar que eres una persona. Int\xE9ntalo de nuevo.",
848
+ escalateFailed: "No hemos podido pasarlo ahora mismo. Int\xE9ntalo de nuevo.",
327
849
  prevSuggestion: "Sugerencia anterior",
328
850
  nextSuggestion: "Sugerencia siguiente",
329
851
  conceptIllustration: "Ilustraci\xF3n del concepto",
@@ -371,7 +893,7 @@ var AR = {
371
893
  showLess: "\u0639\u0631\u0636 \u0623\u0642\u0644",
372
894
  sources: "\u0627\u0644\u0645\u0635\u0627\u062F\u0631",
373
895
  nextYouCouldAsk: "\u064A\u0645\u0643\u0646\u0643 \u0623\u0646 \u062A\u0633\u0623\u0644 \u0628\u0639\u062F \u0630\u0644\u0643",
374
- placeholder: "\u0635\u0641 \u0627\u0644\u0645\u0634\u0643\u0644\u0629 \u0627\u0644\u062A\u064A \u062A\u0648\u0627\u062C\u0647\u0647\u0627\u2026",
896
+ placeholder: "\u0635\u0641 \u0627\u0644\u0645\u0634\u0643\u0644\u0629\u2026",
375
897
  placeholderProduct: "\u0635\u0641 \u0645\u0634\u0643\u0644\u062A\u0643 \u0623\u0648 \u0627\u0633\u0623\u0644 \u0639\u0646 {product}\u2026",
376
898
  send: "\u0625\u0631\u0633\u0627\u0644",
377
899
  stop: "\u0625\u064A\u0642\u0627\u0641",
@@ -409,6 +931,20 @@ var AR = {
409
931
  feedbackDismiss: "\u0625\u063A\u0644\u0627\u0642",
410
932
  feedbackThanks: "\u0634\u0643\u0631\u064B\u0627\u060C \u0647\u0630\u0627 \u064A\u0633\u0627\u0639\u062F\u0646\u0627.",
411
933
  feedbackNotSent: "\u0644\u0645 \u064A\u062A\u0645 \u0627\u0644\u0625\u0631\u0633\u0627\u0644.",
934
+ escalateOpen: "\u062A\u0648\u0627\u0635\u0644 \u0645\u0639 \u0634\u062E\u0635",
935
+ escalateTitle: "\u0623\u0631\u0633\u0644 \u0647\u0630\u0647 \u0627\u0644\u0645\u062D\u0627\u062F\u062B\u0629 \u0625\u0644\u0649 \u0634\u062E\u0635",
936
+ escalateBody: "\u0633\u0646\u0645\u0631\u0651\u0631 \u0627\u0644\u0645\u062D\u0627\u062F\u062B\u0629 \u0643\u0627\u0645\u0644\u0629\u060C \u062D\u062A\u0649 \u0644\u0627 \u062A\u0636\u0637\u0631 \u0625\u0644\u0649 \u0643\u062A\u0627\u0628\u062A\u0647\u0627 \u0645\u0646 \u062C\u062F\u064A\u062F.",
937
+ escalateEmailLabel: "\u0628\u0631\u064A\u062F\u0643 \u0627\u0644\u0625\u0644\u0643\u062A\u0631\u0648\u0646\u064A",
938
+ escalateEmailPlaceholder: "you@example.com",
939
+ escalateNoteLabel: "\u0647\u0644 \u0645\u0646 \u0634\u064A\u0621 \u0622\u062E\u0631 \u064A\u0646\u0628\u063A\u064A \u0623\u0646 \u0646\u0639\u0631\u0641\u0647\u061F (\u0627\u062E\u062A\u064A\u0627\u0631\u064A)",
940
+ escalateSubmit: "\u0625\u0631\u0633\u0627\u0644 \u0625\u0644\u0649 \u0634\u062E\u0635",
941
+ escalateSending: "\u062C\u0627\u0631\u064D \u0627\u0644\u0625\u0631\u0633\u0627\u0644\u2026",
942
+ escalateCancel: "\u0625\u0644\u063A\u0627\u0621",
943
+ escalateInvalidEmail: "\u0644\u0627 \u064A\u0628\u062F\u0648 \u0647\u0630\u0627 \u0639\u0646\u0648\u0627\u0646\u064B\u0627 \u064A\u0645\u0643\u0646\u0646\u0627 \u0627\u0644\u0631\u062F \u0639\u0644\u064A\u0647. \u064A\u0631\u062C\u0649 \u0627\u0644\u062A\u062D\u0642\u0642 \u0645\u0646\u0647.",
944
+ escalateSent: "\u0623\u0635\u0628\u062D\u062A \u0647\u0630\u0647 \u0627\u0644\u0645\u062D\u0627\u062F\u062B\u0629 \u0644\u062F\u0649 \u0623\u062D\u062F \u0627\u0644\u0632\u0645\u0644\u0627\u0621. \u0633\u0646\u0631\u062F \u0639\u0644\u0649 {email}.",
945
+ escalateRateLimited: "\u0637\u0644\u0628\u0627\u062A \u0643\u062B\u064A\u0631\u0629 \u062E\u0644\u0627\u0644 \u0648\u0642\u062A \u0642\u0635\u064A\u0631. \u064A\u0631\u062C\u0649 \u0627\u0644\u0645\u062D\u0627\u0648\u0644\u0629 \u0628\u0639\u062F \u0628\u0636\u0639 \u062F\u0642\u0627\u0626\u0642.",
946
+ escalateCaptcha: "\u062A\u0639\u0630\u0651\u0631 \u0627\u0644\u062A\u062D\u0642\u0642 \u0645\u0646 \u0623\u0646\u0643 \u0644\u0633\u062A \u0631\u0648\u0628\u0648\u062A\u064B\u0627. \u064A\u0631\u062C\u0649 \u0627\u0644\u0645\u062D\u0627\u0648\u0644\u0629 \u0645\u0631\u0629 \u0623\u062E\u0631\u0649.",
947
+ escalateFailed: "\u062A\u0639\u0630\u0651\u0631 \u062A\u0645\u0631\u064A\u0631 \u0627\u0644\u0645\u062D\u0627\u062F\u062B\u0629 \u0627\u0644\u0622\u0646. \u064A\u0631\u062C\u0649 \u0627\u0644\u0645\u062D\u0627\u0648\u0644\u0629 \u0645\u0631\u0629 \u0623\u062E\u0631\u0649.",
412
948
  prevSuggestion: "\u0627\u0644\u0627\u0642\u062A\u0631\u0627\u062D \u0627\u0644\u0633\u0627\u0628\u0642",
413
949
  nextSuggestion: "\u0627\u0644\u0627\u0642\u062A\u0631\u0627\u062D \u0627\u0644\u062A\u0627\u0644\u064A",
414
950
  conceptIllustration: "\u0631\u0633\u0645 \u062A\u0648\u0636\u064A\u062D\u064A \u0644\u0644\u0645\u0641\u0647\u0648\u0645",
@@ -456,7 +992,7 @@ var TA = {
456
992
  showLess: "\u0B95\u0BC1\u0BB1\u0BC8\u0BB5\u0BBE\u0B95\u0B95\u0BCD \u0B95\u0BBE\u0B9F\u0BCD\u0B9F\u0BC1",
457
993
  sources: "\u0BAE\u0BC2\u0BB2\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
458
994
  nextYouCouldAsk: "\u0B85\u0B9F\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1 \u0BA8\u0BC0\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0B95\u0BC7\u0B9F\u0BCD\u0B95\u0BB2\u0BBE\u0BAE\u0BCD",
459
- placeholder: "\u0BA8\u0BC0\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0B8E\u0BA4\u0BBF\u0BB0\u0BCD\u0B95\u0BCA\u0BB3\u0BCD\u0BB3\u0BC1\u0BAE\u0BCD \u0B9A\u0BBF\u0B95\u0BCD\u0B95\u0BB2\u0BC8 \u0BB5\u0BBF\u0BB5\u0BB0\u0BBF\u0BAF\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD\u2026",
995
+ placeholder: "\u0B9A\u0BBF\u0B95\u0BCD\u0B95\u0BB2\u0BC8 \u0BB5\u0BBF\u0BB5\u0BB0\u0BBF\u0BAF\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD\u2026",
460
996
  placeholderProduct: "\u0B9A\u0BBF\u0B95\u0BCD\u0B95\u0BB2\u0BC8 \u0BB5\u0BBF\u0BB5\u0BB0\u0BBF\u0BAF\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD, \u0B85\u0BB2\u0BCD\u0BB2\u0BA4\u0BC1 {product} \u0BAA\u0BB1\u0BCD\u0BB1\u0BBF\u0B95\u0BCD \u0B95\u0BC7\u0BB3\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD\u2026",
461
997
  send: "\u0B85\u0BA9\u0BC1\u0BAA\u0BCD\u0BAA\u0BC1",
462
998
  stop: "\u0BA8\u0BBF\u0BB1\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1",
@@ -494,6 +1030,20 @@ var TA = {
494
1030
  feedbackDismiss: "\u0BAE\u0BC2\u0B9F\u0BC1",
495
1031
  feedbackThanks: "\u0BA8\u0BA9\u0BCD\u0BB1\u0BBF, \u0B87\u0BA4\u0BC1 \u0B8E\u0B99\u0BCD\u0B95\u0BB3\u0BC1\u0B95\u0BCD\u0B95\u0BC1 \u0B89\u0BA4\u0BB5\u0BC1\u0B95\u0BBF\u0BB1\u0BA4\u0BC1.",
496
1032
  feedbackNotSent: "\u0B85\u0BA9\u0BC1\u0BAA\u0BCD\u0BAA \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8.",
1033
+ escalateOpen: "\u0B92\u0BB0\u0BC1\u0BB5\u0BB0\u0BBF\u0B9F\u0BAE\u0BCD \u0BAA\u0BC7\u0B9A\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
1034
+ escalateTitle: "\u0B87\u0BA8\u0BCD\u0BA4 \u0B89\u0BB0\u0BC8\u0BAF\u0BBE\u0B9F\u0BB2\u0BC8 \u0B92\u0BB0\u0BC1\u0BB5\u0BB0\u0BC1\u0B95\u0BCD\u0B95\u0BC1 \u0B85\u0BA9\u0BC1\u0BAA\u0BCD\u0BAA\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
1035
+ escalateBody: "\u0BAE\u0BC1\u0BB4\u0BC1 \u0B89\u0BB0\u0BC8\u0BAF\u0BBE\u0B9F\u0BB2\u0BC8\u0BAF\u0BC1\u0BAE\u0BCD \u0BA8\u0BBE\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0B85\u0BA9\u0BC1\u0BAA\u0BCD\u0BAA\u0BBF\u0BB5\u0BBF\u0B9F\u0BC1\u0BB5\u0BCB\u0BAE\u0BCD; \u0BA8\u0BC0\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0BAE\u0BC0\u0BA3\u0BCD\u0B9F\u0BC1\u0BAE\u0BCD \u0BA4\u0B9F\u0BCD\u0B9F\u0B9A\u0BCD\u0B9A\u0BC1 \u0B9A\u0BC6\u0BAF\u0BCD\u0BAF \u0BB5\u0BC7\u0BA3\u0BCD\u0B9F\u0BBF\u0BAF\u0BA4\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8.",
1036
+ escalateEmailLabel: "\u0B89\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0BAE\u0BBF\u0BA9\u0BCD\u0BA9\u0B9E\u0BCD\u0B9A\u0BB2\u0BCD \u0BAE\u0BC1\u0B95\u0BB5\u0BB0\u0BBF",
1037
+ escalateEmailPlaceholder: "you@example.com",
1038
+ escalateNoteLabel: "\u0BB5\u0BC7\u0BB1\u0BC1 \u0B8F\u0BA4\u0BC7\u0BA9\u0BC1\u0BAE\u0BCD \u0B9A\u0BCA\u0BB2\u0BCD\u0BB2 \u0BB5\u0BBF\u0BB0\u0BC1\u0BAE\u0BCD\u0BAA\u0BC1\u0B95\u0BBF\u0BB1\u0BC0\u0BB0\u0BCD\u0B95\u0BB3\u0BBE? (\u0BB5\u0BBF\u0BB0\u0BC1\u0BAA\u0BCD\u0BAA\u0BAE\u0BCD)",
1039
+ escalateSubmit: "\u0B92\u0BB0\u0BC1\u0BB5\u0BB0\u0BC1\u0B95\u0BCD\u0B95\u0BC1 \u0B85\u0BA9\u0BC1\u0BAA\u0BCD\u0BAA\u0BC1",
1040
+ escalateSending: "\u0B85\u0BA9\u0BC1\u0BAA\u0BCD\u0BAA\u0BAA\u0BCD\u0BAA\u0B9F\u0BC1\u0B95\u0BBF\u0BB1\u0BA4\u0BC1\u2026",
1041
+ escalateCancel: "\u0BB0\u0BA4\u0BCD\u0BA4\u0BC1 \u0B9A\u0BC6\u0BAF\u0BCD",
1042
+ escalateInvalidEmail: "\u0B87\u0BA4\u0BC1 \u0BA8\u0BBE\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BB3\u0BBF\u0B95\u0BCD\u0B95\u0B95\u0BCD\u0B95\u0BC2\u0B9F\u0BBF\u0BAF \u0BAE\u0BBF\u0BA9\u0BCD\u0BA9\u0B9E\u0BCD\u0B9A\u0BB2\u0BCD \u0BAE\u0BC1\u0B95\u0BB5\u0BB0\u0BBF \u0BAA\u0BCB\u0BB2\u0BCD \u0BA4\u0BC6\u0BB0\u0BBF\u0BAF\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8. \u0B9A\u0BB0\u0BBF\u0BAA\u0BBE\u0BB0\u0BCD\u0B95\u0BCD\u0B95\u0BB5\u0BC1\u0BAE\u0BCD.",
1043
+ escalateSent: "\u0B87\u0BA8\u0BCD\u0BA4 \u0B89\u0BB0\u0BC8\u0BAF\u0BBE\u0B9F\u0BB2\u0BCD \u0B87\u0BAA\u0BCD\u0BAA\u0BCB\u0BA4\u0BC1 \u0B92\u0BB0\u0BC1\u0BB5\u0BB0\u0BBF\u0B9F\u0BAE\u0BCD \u0B89\u0BB3\u0BCD\u0BB3\u0BA4\u0BC1. {email} \u0BAE\u0BC1\u0B95\u0BB5\u0BB0\u0BBF\u0B95\u0BCD\u0B95\u0BC1\u0BAA\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BB3\u0BBF\u0BAA\u0BCD\u0BAA\u0BCB\u0BAE\u0BCD.",
1044
+ escalateRateLimited: "\u0B95\u0BC1\u0BB1\u0BC1\u0B95\u0BBF\u0BAF \u0BA8\u0BC7\u0BB0\u0BA4\u0BCD\u0BA4\u0BBF\u0BB2\u0BCD \u0B85\u0BA4\u0BBF\u0B95 \u0B95\u0BCB\u0BB0\u0BBF\u0B95\u0BCD\u0B95\u0BC8\u0B95\u0BB3\u0BCD. \u0B9A\u0BBF\u0BB2 \u0BA8\u0BBF\u0BAE\u0BBF\u0B9F\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0B95\u0BB4\u0BBF\u0BA4\u0BCD\u0BA4\u0BC1 \u0BAE\u0BC1\u0BAF\u0BB1\u0BCD\u0B9A\u0BBF\u0B95\u0BCD\u0B95\u0BB5\u0BC1\u0BAE\u0BCD.",
1045
+ escalateCaptcha: "\u0BA8\u0BC0\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0BAE\u0BA9\u0BBF\u0BA4\u0BB0\u0BCD \u0B8E\u0BA9\u0BCD\u0BAA\u0BA4\u0BC8 \u0B89\u0BB1\u0BC1\u0BA4\u0BBF\u0B9A\u0BC6\u0BAF\u0BCD\u0BAF \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8. \u0BAE\u0BC0\u0BA3\u0BCD\u0B9F\u0BC1\u0BAE\u0BCD \u0BAE\u0BC1\u0BAF\u0BB1\u0BCD\u0B9A\u0BBF\u0B95\u0BCD\u0B95\u0BB5\u0BC1\u0BAE\u0BCD.",
1046
+ escalateFailed: "\u0B87\u0BAA\u0BCD\u0BAA\u0BCB\u0BA4\u0BC1 \u0B85\u0BA9\u0BC1\u0BAA\u0BCD\u0BAA \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8. \u0BAE\u0BC0\u0BA3\u0BCD\u0B9F\u0BC1\u0BAE\u0BCD \u0BAE\u0BC1\u0BAF\u0BB1\u0BCD\u0B9A\u0BBF\u0B95\u0BCD\u0B95\u0BB5\u0BC1\u0BAE\u0BCD.",
497
1047
  prevSuggestion: "\u0BAE\u0BC1\u0BA8\u0BCD\u0BA4\u0BC8\u0BAF \u0BAA\u0BB0\u0BBF\u0BA8\u0BCD\u0BA4\u0BC1\u0BB0\u0BC8",
498
1048
  nextSuggestion: "\u0B85\u0B9F\u0BC1\u0BA4\u0BCD\u0BA4 \u0BAA\u0BB0\u0BBF\u0BA8\u0BCD\u0BA4\u0BC1\u0BB0\u0BC8",
499
1049
  conceptIllustration: "\u0B95\u0BB0\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1 \u0BB5\u0BBF\u0BB3\u0B95\u0BCD\u0B95\u0BAA\u0BCD\u0BAA\u0B9F\u0BAE\u0BCD",
@@ -736,6 +1286,29 @@ var SUBMIT_EXPLORE_FEEDBACK_MUTATION = (
736
1286
  `
737
1287
  );
738
1288
  var EXPLORE_FEEDBACK_COMMENT_SOFT_MAX = 500;
1289
+ var ESCALATE_EXPLORE_CONVERSATION_MUTATION = (
1290
+ /* GraphQL */
1291
+ `
1292
+ mutation EscalateExploreConversation(
1293
+ $input: EscalateExploreConversationInput!
1294
+ ) {
1295
+ escalateExploreConversation(input: $input) {
1296
+ created
1297
+ escalation {
1298
+ email
1299
+ createdAt
1300
+ }
1301
+ }
1302
+ }
1303
+ `
1304
+ );
1305
+ var EXPLORE_ESCALATION_NOTE_SOFT_MAX = 2e3;
1306
+ var DELIVERABLE_EMAIL = /^[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\.)+[A-Za-z]{2,}$/;
1307
+ function looksLikeDeliverableEmail(value) {
1308
+ const trimmed = value.trim();
1309
+ if (trimmed.length === 0 || trimmed.length > 254) return false;
1310
+ return DELIVERABLE_EMAIL.test(trimmed);
1311
+ }
739
1312
  var CREATE_EXPLORE_SHARE_LINK_MUTATION = (
740
1313
  /* GraphQL */
741
1314
  `
@@ -756,48 +1329,6 @@ var EXPLORE_SHARED_CONVERSATION_QUERY = (
756
1329
  }
757
1330
  `
758
1331
  );
759
- function MarkdownLink({
760
- href,
761
- children,
762
- // react-markdown passes the hast node to every override; it is not a DOM attribute.
763
- node: _node,
764
- ...props
765
- }) {
766
- const isExternal = typeof href === "string" && (href.startsWith("http://") || href.startsWith("https://"));
767
- return /* @__PURE__ */ jsx(
768
- "a",
769
- {
770
- href,
771
- ...props,
772
- ...isExternal ? { target: "_blank", rel: "noopener noreferrer" } : { rel: "noopener noreferrer" },
773
- children
774
- }
775
- );
776
- }
777
- var defaultComponents = {
778
- a: MarkdownLink
779
- };
780
- function Markdown({
781
- children,
782
- className,
783
- inline,
784
- components
785
- }) {
786
- const content = /* @__PURE__ */ jsx(
787
- ReactMarkdown,
788
- {
789
- remarkPlugins: [remarkGfm],
790
- rehypePlugins: [rehypeSanitize],
791
- components: { ...defaultComponents, ...components },
792
- ...inline ? { disallowedElements: ["p"], unwrapDisallowed: true } : {},
793
- children
794
- }
795
- );
796
- if (inline) {
797
- return /* @__PURE__ */ jsx("span", { className, children: content });
798
- }
799
- return /* @__PURE__ */ jsx("div", { className, children: content });
800
- }
801
1332
  var PROSE_CSS = `
802
1333
  .boff-prose {
803
1334
  max-width: none;
@@ -1194,6 +1725,10 @@ function useWebSDK() {
1194
1725
  }
1195
1726
  return client;
1196
1727
  }
1728
+ function useWebSDKConfig() {
1729
+ const client = useWebSDK();
1730
+ return client.getConfig();
1731
+ }
1197
1732
 
1198
1733
  // src/data/products.ts
1199
1734
  var ECOSYSTEM_PRODUCTS = [
@@ -1540,6 +2075,25 @@ var VALIDATION_CODES = /* @__PURE__ */ new Set([
1540
2075
  "CONVERSATION_LIMIT",
1541
2076
  "INVALID_INPUT"
1542
2077
  ]);
2078
+ function escalateOutcomeForCode(code) {
2079
+ switch (code) {
2080
+ case "INVALID_EMAIL":
2081
+ return "invalid-email";
2082
+ case "RATE_LIMITED":
2083
+ case "HTTP_429":
2084
+ return "rate-limited";
2085
+ case "CAPTCHA_REQUIRED":
2086
+ case "CAPTCHA_FAILED":
2087
+ return "captcha";
2088
+ // The token owns nothing, or owns a different conversation. The server answers NOT_FOUND
2089
+ // rather than FORBIDDEN so the endpoint is not an oracle for whether a thread exists;
2090
+ // either way there is nothing this visitor can do about it, so it reads as a failure.
2091
+ case "NOT_FOUND":
2092
+ return "failed";
2093
+ default:
2094
+ return "failed";
2095
+ }
2096
+ }
1543
2097
  function classifyExploreError(code, serverMessage) {
1544
2098
  const c = code ?? "UNKNOWN";
1545
2099
  if (RATE_LIMIT_CODES.has(c)) {
@@ -1706,6 +2260,7 @@ function useExploreChat(options = {}) {
1706
2260
  onSend
1707
2261
  } = options;
1708
2262
  const client = useWebSDK();
2263
+ const { productId } = useWebSDKConfig();
1709
2264
  const promptKey = JSON.stringify(examplePrompts ?? []);
1710
2265
  const welcomeKey = JSON.stringify(fallbackWelcome ?? null);
1711
2266
  const fallbackCatalog = useMemo(
@@ -1729,6 +2284,7 @@ function useExploreChat(options = {}) {
1729
2284
  const pollGenerationRef = useRef(0);
1730
2285
  const pollTimerRef = useRef(null);
1731
2286
  const conversationTokenRef = useRef(null);
2287
+ const [hasConversation, setHasConversation] = useState(false);
1732
2288
  const shareTokenRef = useRef(void 0);
1733
2289
  if (shareTokenRef.current === void 0) {
1734
2290
  shareTokenRef.current = readShareToken();
@@ -1743,6 +2299,10 @@ function useExploreChat(options = {}) {
1743
2299
  );
1744
2300
  const inFlightRef = useRef(false);
1745
2301
  catalogRef.current = catalog;
2302
+ const rememberConversationToken = useCallback((token) => {
2303
+ conversationTokenRef.current = token;
2304
+ setHasConversation(token !== null);
2305
+ }, []);
1746
2306
  const clearPollTimer = useCallback(() => {
1747
2307
  if (pollTimerRef.current !== null) {
1748
2308
  clearTimeout(pollTimerRef.current);
@@ -1809,7 +2369,7 @@ function useExploreChat(options = {}) {
1809
2369
  setArchive(readArchive(storageKey));
1810
2370
  const stored = readPersisted(storageKey);
1811
2371
  if (stored) {
1812
- conversationTokenRef.current = stored.conversationToken;
2372
+ rememberConversationToken(stored.conversationToken);
1813
2373
  focusProductRef.current = stored.focusProduct;
1814
2374
  const settled = stored.messages.flatMap((m) => {
1815
2375
  if (m.role !== "ASSISTANT") return [m];
@@ -1825,7 +2385,7 @@ function useExploreChat(options = {}) {
1825
2385
  setTurnCount(turns);
1826
2386
  }
1827
2387
  setHydrated(true);
1828
- }, [persist, storageKey]);
2388
+ }, [persist, rememberConversationToken, storageKey]);
1829
2389
  useEffect(() => {
1830
2390
  if (!persist || !hydrated || typeof window === "undefined") return;
1831
2391
  try {
@@ -2018,7 +2578,7 @@ function useExploreChat(options = {}) {
2018
2578
  setPhase(classified.kind === "rate-limit" ? "limited" : "error");
2019
2579
  return;
2020
2580
  }
2021
- conversationTokenRef.current = result.conversation.token;
2581
+ rememberConversationToken(result.conversation.token);
2022
2582
  turnCountRef.current = result.conversation.turnCount;
2023
2583
  setTurnCount(result.conversation.turnCount);
2024
2584
  setRemainingToday(result.remainingToday ?? null);
@@ -2044,7 +2604,14 @@ function useExploreChat(options = {}) {
2044
2604
  setPhase("streaming");
2045
2605
  startPolling(result.conversation.token, assistantMessage.id);
2046
2606
  },
2047
- [client, locale, pageUrl, productSlug, startPolling]
2607
+ [
2608
+ client,
2609
+ locale,
2610
+ pageUrl,
2611
+ productSlug,
2612
+ rememberConversationToken,
2613
+ startPolling
2614
+ ]
2048
2615
  );
2049
2616
  const send = useCallback(
2050
2617
  async (message, attachments = []) => {
@@ -2161,7 +2728,7 @@ function useExploreChat(options = {}) {
2161
2728
  );
2162
2729
  cancelPolling();
2163
2730
  inFlightRef.current = false;
2164
- conversationTokenRef.current = null;
2731
+ rememberConversationToken(null);
2165
2732
  lastSentTextRef.current = null;
2166
2733
  lastSentAttachmentsRef.current = [];
2167
2734
  turnCountRef.current = 0;
@@ -2176,7 +2743,7 @@ function useExploreChat(options = {}) {
2176
2743
  } catch {
2177
2744
  }
2178
2745
  }
2179
- }, [cancelPolling, storageKey]);
2746
+ }, [cancelPolling, rememberConversationToken, storageKey]);
2180
2747
  const openConversation = useCallback(
2181
2748
  (token) => {
2182
2749
  const entry = readArchive(storageKey).find((e) => e.token === token);
@@ -2188,7 +2755,7 @@ function useExploreChat(options = {}) {
2188
2755
  });
2189
2756
  cancelPolling();
2190
2757
  inFlightRef.current = false;
2191
- conversationTokenRef.current = entry.token;
2758
+ rememberConversationToken(entry.token);
2192
2759
  turnCountRef.current = entry.messages.filter(
2193
2760
  (m) => m.role === "USER"
2194
2761
  ).length;
@@ -2200,7 +2767,7 @@ function useExploreChat(options = {}) {
2200
2767
  setPhase(catalogRef.current.enabled ? "ready" : "disabled");
2201
2768
  setArchive(readArchive(storageKey));
2202
2769
  },
2203
- [cancelPolling, storageKey]
2770
+ [cancelPolling, rememberConversationToken, storageKey]
2204
2771
  );
2205
2772
  const deleteConversation = useCallback(
2206
2773
  (token) => {
@@ -2289,6 +2856,69 @@ function useExploreChat(options = {}) {
2289
2856
  },
2290
2857
  [client]
2291
2858
  );
2859
+ const escalate = useCallback(
2860
+ async (email, note) => {
2861
+ const token = conversationTokenRef.current;
2862
+ if (!token) {
2863
+ return { outcome: "no-conversation", email: null, createdAt: null };
2864
+ }
2865
+ const attempt = async (captchaAttempt) => {
2866
+ const activeCatalog = catalogRef.current;
2867
+ let recaptchaToken;
2868
+ if (activeCatalog.captchaRequired && activeCatalog.recaptchaSiteKey) {
2869
+ try {
2870
+ recaptchaToken = await getRecaptchaV3Token(
2871
+ activeCatalog.recaptchaSiteKey
2872
+ );
2873
+ } catch (err) {
2874
+ console.warn(
2875
+ "[WebSDK] reCAPTCHA v3 token could not be minted",
2876
+ err
2877
+ );
2878
+ }
2879
+ }
2880
+ const input = {
2881
+ conversationToken: token,
2882
+ email: email.trim(),
2883
+ note: note?.trim() ? note.trim() : null,
2884
+ ...recaptchaToken ? { recaptchaToken } : {},
2885
+ ...productId ? { productId } : {}
2886
+ };
2887
+ try {
2888
+ const res = await client.mutate(
2889
+ ESCALATE_EXPLORE_CONVERSATION_MUTATION,
2890
+ { input }
2891
+ );
2892
+ const first = res?.errors?.[0];
2893
+ const result = res?.data?.escalateExploreConversation;
2894
+ if (first || !result) {
2895
+ const code = typeof first?.extensions?.code === "string" ? first.extensions.code : "ESCALATION_FAILED";
2896
+ if (code === "CAPTCHA_FAILED" && captchaAttempt === 0) {
2897
+ return attempt(1);
2898
+ }
2899
+ return {
2900
+ outcome: escalateOutcomeForCode(code),
2901
+ email: null,
2902
+ createdAt: null
2903
+ };
2904
+ }
2905
+ return {
2906
+ // `created: false` is the double-click, the retry, the reload. The handover
2907
+ // exists, so this is a success — and the address comes from the SERVER, because
2908
+ // on a repeat submit the reply goes to whatever the FIRST handover used.
2909
+ outcome: result.created ? "sent" : "already-handed-over",
2910
+ email: result.escalation?.email ?? null,
2911
+ createdAt: result.escalation?.createdAt ?? null
2912
+ };
2913
+ } catch {
2914
+ return { outcome: "failed", email: null, createdAt: null };
2915
+ }
2916
+ };
2917
+ return attempt(0);
2918
+ },
2919
+ [client, productId]
2920
+ );
2921
+ const canEscalate = hasConversation && !isSharedView;
2292
2922
  return {
2293
2923
  phase,
2294
2924
  catalog,
@@ -2309,6 +2939,8 @@ function useExploreChat(options = {}) {
2309
2939
  clearHistory,
2310
2940
  recordClick,
2311
2941
  submitFeedback,
2942
+ escalate,
2943
+ canEscalate,
2312
2944
  createShareLink,
2313
2945
  isSharedView
2314
2946
  };
@@ -2994,9 +3626,6 @@ function PageHead({
2994
3626
  ))
2995
3627
  ] });
2996
3628
  }
2997
- function cn(...inputs) {
2998
- return twMerge(clsx(inputs));
2999
- }
3000
3629
  var buttonVariants = cva(
3001
3630
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
3002
3631
  {
@@ -3234,163 +3863,27 @@ var EXPLORE_CSS = `
3234
3863
  .boff-explore-scroll::-webkit-scrollbar { width: 10px; }
3235
3864
  .boff-explore-scroll::-webkit-scrollbar-track { background: transparent; }
3236
3865
  .boff-explore-scroll::-webkit-scrollbar-thumb {
3237
- background-color: hsl(var(--border));
3238
- border: 3px solid transparent;
3239
- border-radius: 9999px;
3240
- background-clip: content-box;
3241
- }
3242
-
3243
- /* The product strip scrolls inside itself; a visible scrollbar would read as a broken
3244
- layout rather than an affordance, and reserving its gutter shifts the chips. */
3245
- .boff-explore-strip {
3246
- scrollbar-width: none;
3247
- -ms-overflow-style: none;
3248
- }
3249
- .boff-explore-strip::-webkit-scrollbar { display: none; }
3250
-
3251
- @keyframes boff-explore-fade-in {
3252
- from { opacity: 0; transform: translateY(2px); }
3253
- to { opacity: 1; transform: none; }
3254
- }
3255
- .boff-explore-fade { animation: boff-explore-fade-in 220ms ease-out; }
3256
- @media (prefers-reduced-motion: reduce) {
3257
- .boff-explore-fade { animation: none; }
3258
- }
3259
-
3260
- /* Nothing inside an answer may widen the page. Model output carries long URLs, code and
3261
- tables \u2014 each of those overflows a phone by default, and one overflowing child makes the
3262
- WHOLE document horizontally scrollable, not just the bubble. */
3263
- .boff-explore-body,
3264
- .boff-explore-body p,
3265
- .boff-explore-body li,
3266
- .boff-explore-body a,
3267
- .boff-explore-body h1,
3268
- .boff-explore-body h2,
3269
- .boff-explore-body h3 {
3270
- overflow-wrap: anywhere;
3271
- word-break: break-word;
3272
- }
3273
- .boff-explore-body pre {
3274
- overflow-x: auto;
3275
- max-width: 100%;
3276
- }
3277
- .boff-explore-body table {
3278
- display: block;
3279
- overflow-x: auto;
3280
- max-width: 100%;
3281
- }
3282
- .boff-explore-body img {
3283
- max-width: 100%;
3284
- height: auto;
3285
- }
3286
-
3287
- /* ---- Comparison and pricing tables ----------------------------------------------------
3288
- The harness emits a comparison, and a set of plans, as a GFM table whose FIRST column is
3289
- the subject \u2014 the product, or the plan (CONTRACT.md \xA77). Two things have to be true of it:
3290
- the page must not scroll sideways because of it, and it has to be readable on a 320px
3291
- phone, which a four-column grid of 1px borders is not.
3292
-
3293
- Wide: a contained scroller. \`max-width: 100%\` keeps the box inside the bubble and
3294
- \`overflow-x: auto\` keeps the columns inside the box, so the widest possible table still
3295
- cannot widen the document. \`overscroll-behavior-x: contain\` stops a swipe that runs out
3296
- of table from chaining to the page \u2014 the same rule the transcript needed. */
3297
- .boff-explore-body .boff-explore-table {
3298
- max-width: 100%;
3299
- overflow-x: auto;
3300
- overscroll-behavior-x: contain;
3301
- margin: 0.75rem 0;
3302
- border: 1px solid hsl(var(--border));
3303
- border-radius: 0.75rem;
3304
- background-color: hsl(var(--card));
3305
- }
3306
- .boff-explore-body .boff-explore-table:focus-visible {
3307
- outline: 2px solid hsl(var(--ring));
3308
- outline-offset: 2px;
3309
- }
3310
- .boff-explore-body .boff-explore-table table {
3311
- /* Beats the blanket \`display: block\` above, which is the unwrapped fallback. */
3312
- display: table;
3313
- width: 100%;
3314
- max-width: none;
3315
- margin: 0;
3316
- border-collapse: collapse;
3317
- font-size: 0.8125rem;
3318
- }
3319
- .boff-explore-body .boff-explore-table th,
3320
- .boff-explore-body .boff-explore-table td {
3321
- border: 0;
3322
- border-top: 1px solid hsl(var(--border));
3323
- padding: 0.5rem 0.75rem;
3324
- /* \`start\`, never \`left\`: the page root carries dir="rtl" in Arabic, and a left-aligned
3325
- table there reads against the grain of every other line on the page. */
3326
- text-align: start;
3327
- vertical-align: top;
3328
- overflow-wrap: anywhere;
3329
- /* Prices in a column line up under each other instead of drifting by digit width. */
3330
- font-variant-numeric: tabular-nums;
3331
- min-width: 6rem;
3332
- }
3333
- .boff-explore-body .boff-explore-table thead th {
3334
- border-top: 0;
3335
- background-color: hsl(var(--muted));
3336
- color: hsl(var(--foreground));
3337
- font-weight: 600;
3338
- white-space: nowrap;
3339
- }
3340
- /* The first column is what the row is ABOUT \u2014 the product, or the plan name. Reading it as
3341
- the row's heading is what makes a price list scannable rather than a grid of strings. */
3342
- .boff-explore-body .boff-explore-table tbody td:first-child {
3343
- font-weight: 600;
3344
- color: hsl(var(--card-foreground));
3345
- }
3346
-
3347
- /* Narrow: one card per row. Each cell prints its own column heading, so a value is never
3348
- stranded without the question it answers, and the subject cell becomes the card's title.
3349
- A media query rather than a measured width \u2014 these pages are serialised post-hydration,
3350
- so anything measured here would ship to every phone as static HTML. */
3351
- @media (max-width: 30rem) {
3352
- .boff-explore-body .boff-explore-table {
3353
- overflow-x: visible;
3354
- }
3355
- .boff-explore-body .boff-explore-table table,
3356
- .boff-explore-body .boff-explore-table tbody,
3357
- .boff-explore-body .boff-explore-table tr,
3358
- .boff-explore-body .boff-explore-table td {
3359
- display: block;
3360
- width: 100%;
3361
- min-width: 0;
3362
- }
3363
- .boff-explore-body .boff-explore-table thead {
3364
- display: none;
3365
- }
3366
- .boff-explore-body .boff-explore-table tr {
3367
- border-top: 1px solid hsl(var(--border));
3368
- padding: 0.5rem 0;
3369
- }
3370
- .boff-explore-body .boff-explore-table tr:first-child {
3371
- border-top: 0;
3372
- }
3373
- .boff-explore-body .boff-explore-table td {
3374
- border: 0;
3375
- padding: 0.125rem 0.75rem;
3376
- }
3377
- .boff-explore-body .boff-explore-table td[data-label]::before {
3378
- content: attr(data-label);
3379
- display: block;
3380
- font-size: 0.6875rem;
3381
- font-weight: 600;
3382
- text-transform: uppercase;
3383
- letter-spacing: 0.04em;
3384
- color: hsl(var(--muted-foreground));
3385
- }
3386
- .boff-explore-body .boff-explore-table tbody td:first-child {
3387
- font-size: 0.9375rem;
3388
- padding-bottom: 0.25rem;
3389
- }
3390
- .boff-explore-body .boff-explore-table tbody td:first-child::before {
3391
- /* The plan's own name is the heading here; repeating "Plan" over it says nothing. */
3392
- display: none;
3393
- }
3866
+ background-color: hsl(var(--border));
3867
+ border: 3px solid transparent;
3868
+ border-radius: 9999px;
3869
+ background-clip: content-box;
3870
+ }
3871
+
3872
+ /* The product strip scrolls inside itself; a visible scrollbar would read as a broken
3873
+ layout rather than an affordance, and reserving its gutter shifts the chips. */
3874
+ .boff-explore-strip {
3875
+ scrollbar-width: none;
3876
+ -ms-overflow-style: none;
3877
+ }
3878
+ .boff-explore-strip::-webkit-scrollbar { display: none; }
3879
+
3880
+ @keyframes boff-explore-fade-in {
3881
+ from { opacity: 0; transform: translateY(2px); }
3882
+ to { opacity: 1; transform: none; }
3883
+ }
3884
+ .boff-explore-fade { animation: boff-explore-fade-in 220ms ease-out; }
3885
+ @media (prefers-reduced-motion: reduce) {
3886
+ .boff-explore-fade { animation: none; }
3394
3887
  }
3395
3888
 
3396
3889
  /* ---- Ambient backdrop -----------------------------------------------------------------
@@ -3449,11 +3942,40 @@ var EXPLORE_CSS = `
3449
3942
  animation: boff-explore-sheen 1.5s ease-in-out infinite;
3450
3943
  }
3451
3944
 
3945
+ /* ---- The composer, before anyone has typed -----------------------------------------
3946
+ On the welcome screen the box IS the page: everything above it is a way of explaining
3947
+ what to put in it. Unhighlighted it reads as a search bar at the bottom of a landing
3948
+ page, which people scroll past \u2014 so while the conversation is empty it gets a soft halo
3949
+ in the site's own --primary and one slow breath of it, and the moment there is a
3950
+ transcript, or the visitor focuses or types, it drops back to the ordinary card it was.
3951
+ An input that keeps pulsing while you are using it is nagging, not inviting.
3952
+
3953
+ Colour comes from the product's --primary token, so it is whatever that site's palette
3954
+ already is, never a colour chosen here. */
3955
+ .boff-explore-invite {
3956
+ border-color: hsl(var(--primary) / 0.45);
3957
+ box-shadow:
3958
+ 0 0 0 4px hsl(var(--primary) / 0.08),
3959
+ 0 8px 24px -12px hsl(var(--primary) / 0.45);
3960
+ animation: boff-explore-breathe 3.2s ease-in-out infinite;
3961
+ }
3962
+ /* Focus and typing both mean "I have found it", and the hint has done its job. */
3963
+ .boff-explore-invite:focus-within,
3964
+ .boff-explore-invite.boff-explore-invite-quiet {
3965
+ animation: none;
3966
+ box-shadow: 0 0 0 4px hsl(var(--primary) / 0.10);
3967
+ }
3968
+ @keyframes boff-explore-breathe {
3969
+ 0%, 100% { box-shadow: 0 0 0 4px hsl(var(--primary) / 0.07), 0 8px 24px -12px hsl(var(--primary) / 0.40); }
3970
+ 50% { box-shadow: 0 0 0 7px hsl(var(--primary) / 0.13), 0 8px 28px -12px hsl(var(--primary) / 0.55); }
3971
+ }
3972
+
3452
3973
  /* Every one of the above is decoration. Reduced motion removes the movement and keeps the
3453
3974
  final state, rather than leaving content mid-animation and invisible. */
3454
3975
  @media (prefers-reduced-motion: reduce) {
3455
3976
  .boff-explore-aura::before,
3456
- .boff-explore-busy::after { animation: none; }
3977
+ .boff-explore-busy::after,
3978
+ .boff-explore-invite { animation: none; }
3457
3979
  .boff-explore-rise { animation: none; opacity: 1; transform: none; }
3458
3980
  }
3459
3981
 
@@ -3480,457 +4002,211 @@ var EXPLORE_CSS = `
3480
4002
  something is actually covering the bottom, so a snapshot taken
3481
4003
  with no keyboard open never carries it. */
3482
4004
  .boff-explore-viewport {
3483
- height: calc(100vh - var(--boff-explore-offset, 0px) - var(--boff-explore-inset, 0px));
3484
- }
3485
- @supports (height: 100dvh) {
3486
- .boff-explore-viewport {
3487
- height: calc(100dvh - var(--boff-explore-offset, 0px) - var(--boff-explore-inset, 0px));
3488
- }
3489
- }
3490
-
3491
- /* Landscape phones and other short viewports: a one-screen pane leaves a ~124px transcript
3492
- sliver under ~610px of chrome and content. At or below the threshold the pane stops
3493
- pretending to be a window and lets the page's own scrolling carry the transcript. The
3494
- min-height is the available height rather than a fixed rem value, so the fallback can
3495
- never be taller than the screen it is rescuing. */
3496
- @media (max-height: ${SHORT_VIEWPORT_PX}px) {
3497
- .boff-explore-viewport {
3498
- height: auto;
3499
- min-height: calc(100vh - var(--boff-explore-offset, 0px));
3500
- }
3501
- }
3502
- @supports (height: 100dvh) {
3503
- @media (max-height: ${SHORT_VIEWPORT_PX}px) {
3504
- .boff-explore-viewport {
3505
- min-height: calc(100dvh - var(--boff-explore-offset, 0px));
3506
- }
3507
- }
3508
- }
3509
-
3510
- /* ---- Composer dock --------------------------------------------------------------------
3511
- Three declarations, each the fallback for the one after it: a plain 1rem where \`max()\`
3512
- is unknown, then the measured overlap of any FOREIGN fixed chrome sitting over the
3513
- composer (the host sites' cookie banner is 229px tall and lands exactly there), then the
3514
- same plus the home-indicator inset. \`--boff-explore-chrome\` is set on the root only once
3515
- an overlap has actually been measured, so everywhere else this collapses to 1rem. */
3516
- .boff-explore-composer {
3517
- padding-bottom: 1rem;
3518
- padding-bottom: max(1rem, var(--boff-explore-chrome, 0px));
3519
- padding-bottom: max(1rem, env(safe-area-inset-bottom), var(--boff-explore-chrome, 0px));
3520
- }
3521
- `;
3522
- function ExploreStyles() {
3523
- return /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: EXPLORE_CSS } });
3524
- }
3525
- var CAPABILITIES = [
3526
- {
3527
- icon: BookOpenText,
3528
- title: "Grounded in our own docs",
3529
- body: "Every answer comes from published product pages and documentation \u2014 never invented."
3530
- },
3531
- {
3532
- icon: Layers,
3533
- title: "Compare across the ecosystem",
3534
- body: "Ask which product fits your team, or how two of them work together."
3535
- },
3536
- {
3537
- icon: Link2,
3538
- title: "Straight to the right page",
3539
- body: "Get the sources and next steps \u2014 pricing, docs, a demo \u2014 without hunting."
3540
- }
3541
- ];
3542
- var MIN_VISIBLE_FRACTION = 0.25;
3543
- var SDK_OVERLAY_SELECTOR = "[data-boff-explore]";
3544
- var CTA_VARIANTS = {
3545
- CONTACT: "default",
3546
- WAITLIST: "default",
3547
- APP: "default",
3548
- PRICING: "secondary",
3549
- STORE: "secondary",
3550
- PARTNERS: "secondary",
3551
- DOCS: "outline",
3552
- SERVICES: "outline",
3553
- OTHER: "outline"
3554
- };
3555
- function normalizeOffset(offset) {
3556
- if (offset === void 0) return "0px";
3557
- return typeof offset === "number" ? `${offset}px` : offset;
3558
- }
3559
- function isOwnElement(el, root) {
3560
- if (el === document.body || el === document.documentElement) return true;
3561
- if (root.contains(el)) return true;
3562
- return el.closest(SDK_OVERLAY_SELECTOR) !== null;
3563
- }
3564
- function visualViewportHeight() {
3565
- return window.visualViewport?.height ?? window.innerHeight;
3566
- }
3567
- function clamp2(value, min, max) {
3568
- return Math.min(Math.max(value, min), max);
3569
- }
3570
- function isRestoredNavigation() {
3571
- const entries = window.performance?.getEntriesByType?.("navigation");
3572
- const entry = entries?.[0];
3573
- if (!entry) return true;
3574
- const navigation = entry;
3575
- return navigation.type === "reload" || navigation.type === "back_forward";
3576
- }
3577
- function pageOwnsScrolling(el) {
3578
- if (window.innerHeight > SHORT_VIEWPORT_PX) return false;
3579
- return el.scrollHeight - el.clientHeight <= 1;
3580
- }
3581
- function isInternalPath(url) {
3582
- return url.startsWith("/") && !url.startsWith("//");
3583
- }
3584
- function hostnameOf(url) {
3585
- try {
3586
- return new URL(url).hostname.replace(/^www\./, "");
3587
- } catch {
3588
- return "";
3589
- }
3590
- }
3591
- function initialOf(value, fallback) {
3592
- const source = (value || fallback).trim();
3593
- return source ? source.slice(0, 1).toUpperCase() : "?";
3594
- }
3595
- function logoUrlOf(url) {
3596
- try {
3597
- const { origin, protocol } = new URL(url);
3598
- if (protocol !== "https:" && protocol !== "http:") return null;
3599
- return `${origin}/favicon.svg`;
3600
- } catch {
3601
- return null;
3602
- }
3603
- }
3604
- function inlineImageOrigins(products) {
3605
- const origins = /* @__PURE__ */ new Set();
3606
- for (const product of products) {
3607
- let url;
3608
- try {
3609
- url = new URL(product.website);
3610
- } catch {
3611
- continue;
3612
- }
3613
- if (url.protocol !== "https:") continue;
3614
- const apex = url.hostname.replace(/^www\./, "");
3615
- const port = url.port ? `:${url.port}` : "";
3616
- origins.add(`https://${apex}${port}`);
3617
- origins.add(`https://www.${apex}${port}`);
3618
- }
3619
- return origins;
3620
- }
3621
- function isInlineImageAllowed(src, origins) {
3622
- if (typeof src !== "string" || src === "") return false;
3623
- let url;
3624
- try {
3625
- url = new URL(src);
3626
- } catch {
3627
- return false;
3628
- }
3629
- if (url.protocol !== "https:" || url.username || url.password) return false;
3630
- return origins.has(url.origin);
3631
- }
3632
- var REFERENCE_KIND_ORDER = [
3633
- "CONCEPT",
3634
- "DOCS",
3635
- "PRICING",
3636
- "USE_CASE",
3637
- "PAGE"
3638
- ];
3639
- var PRODUCT_SUBDOMAINS = [
3640
- "alphadocs.",
3641
- "alphaapp.",
3642
- "docs.",
3643
- "alpha.",
3644
- "app.",
3645
- "www."
3646
- ];
3647
- function readCatalogUrl(raw) {
3648
- let parsed;
3649
- try {
3650
- parsed = new URL(raw);
3651
- } catch {
3652
- return null;
3653
- }
3654
- if (parsed.protocol !== "https:") return null;
3655
- const host = parsed.hostname.toLowerCase();
3656
- const prefix = PRODUCT_SUBDOMAINS.find(
3657
- (candidate) => host.startsWith(candidate)
3658
- );
3659
- const apex = prefix === void 0 ? host : host.slice(prefix.length);
3660
- const port = parsed.port ? `:${parsed.port}` : "";
3661
- return {
3662
- origin: `https://${apex}${port}`,
3663
- prefix,
3664
- pathname: parsed.pathname
3665
- };
3666
- }
3667
- function referenceKindOf(url, origins) {
3668
- const parsed = readCatalogUrl(url);
3669
- if (parsed === null || !origins.has(parsed.origin)) return null;
3670
- if (parsed.prefix === "docs." || parsed.prefix === "alphadocs.")
3671
- return "DOCS";
3672
- const segment = parsed.pathname.toLowerCase().split("/").filter(Boolean)[0] ?? "";
3673
- if (segment === "concept" || segment === "concepts") return "CONCEPT";
3674
- if (segment === "docs") return "DOCS";
3675
- if (segment === "pricing" || segment === "plans") return "PRICING";
3676
- if (segment === "use-case" || segment === "use-cases" || segment === "solutions") {
3677
- return "USE_CASE";
3678
- }
3679
- return "PAGE";
3680
- }
3681
- function productNamesByOrigin(products) {
3682
- const names = /* @__PURE__ */ new Map();
3683
- for (const product of products) {
3684
- let url;
3685
- try {
3686
- url = new URL(product.website);
3687
- } catch {
3688
- continue;
3689
- }
3690
- if (url.protocol !== "https:") continue;
3691
- const apex = url.hostname.replace(/^www\./, "");
3692
- const port = url.port ? `:${url.port}` : "";
3693
- const key = `https://${apex}${port}`;
3694
- if (!names.has(key)) names.set(key, product.name);
3695
- }
3696
- return names;
3697
- }
3698
- function summariseReferences(references, origins, productNames) {
3699
- const counts = /* @__PURE__ */ new Map();
3700
- const products = [];
3701
- for (const reference of references) {
3702
- const kind = referenceKindOf(reference.url, origins);
3703
- if (kind !== null) counts.set(kind, (counts.get(kind) ?? 0) + 1);
3704
- const parsed = readCatalogUrl(reference.url);
3705
- const name = (parsed === null ? void 0 : productNames.get(parsed.origin)) ?? (reference.product || null);
3706
- if (name != null && !products.includes(name)) products.push(name);
3707
- }
3708
- return {
3709
- total: references.length,
3710
- kinds: REFERENCE_KIND_ORDER.filter((kind) => counts.has(kind)).map(
3711
- (kind) => ({ kind, count: counts.get(kind) ?? 0 })
3712
- ),
3713
- products
3714
- };
4005
+ height: calc(100vh - var(--boff-explore-offset, 0px) - var(--boff-explore-inset, 0px));
3715
4006
  }
3716
- var CODE_FENCE = /^\s*(?:```|~~~)/;
3717
- var TABLE_ROW_LINE = /^\s{0,3}\|/;
3718
- var TABLE_DELIMITER_ROW = /^\s{0,3}\|[\s:|-]*-[\s:|-]*$/;
3719
- function readableLine(raw, mode) {
3720
- let line = raw;
3721
- line = line.replace(/!\[[^\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g, "");
3722
- line = mode === "speech" ? (
3723
- // A voice reading "h-t-t-p-s colon slash slash…" mid-sentence is noise, and the
3724
- // prompt asks for two to five links in every answer. The label IS the words.
3725
- line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1")
3726
- ) : line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1 ($2)");
3727
- line = mode === "speech" ? line.replace(/<(https?:\/\/[^>\s]+)>/g, "") : line.replace(/<(https?:\/\/[^>\s]+)>/g, "$1");
3728
- line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
3729
- line = line.replace(/^\s{0,3}>\s?/, "");
3730
- if (/^\s{0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/.test(line)) return "";
3731
- line = line.replace(/`([^`]+)`/g, "$1");
3732
- line = line.replace(/\*\*([^*]+)\*\*/g, "$1");
3733
- line = line.replace(/__([^_]+)__/g, "$1");
3734
- line = line.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, "$1$2");
3735
- line = line.replace(/(^|[^_\w])_([^_\n]+)_(?![_\w])/g, "$1$2");
3736
- return line.replace(/[ \t]+$/, "");
4007
+ @supports (height: 100dvh) {
4008
+ .boff-explore-viewport {
4009
+ height: calc(100dvh - var(--boff-explore-offset, 0px) - var(--boff-explore-inset, 0px));
4010
+ }
3737
4011
  }
3738
- function tableCells(row) {
3739
- const trimmed = row.trim();
3740
- const cells = [];
3741
- let cell = "";
3742
- for (let at = 0; at < trimmed.length; at += 1) {
3743
- const char = trimmed[at] ?? "";
3744
- if (char === "\\") {
3745
- cell += char + (trimmed[at + 1] ?? "");
3746
- at += 1;
3747
- continue;
3748
- }
3749
- if (char === "|") {
3750
- cells.push(cell.trim());
3751
- cell = "";
3752
- continue;
4012
+
4013
+ /* Landscape phones and other short viewports: a one-screen pane leaves a ~124px transcript
4014
+ sliver under ~610px of chrome and content. At or below the threshold the pane stops
4015
+ pretending to be a window and lets the page's own scrolling carry the transcript. The
4016
+ min-height is the available height rather than a fixed rem value, so the fallback can
4017
+ never be taller than the screen it is rescuing. */
4018
+ @media (max-height: ${SHORT_VIEWPORT_PX}px) {
4019
+ .boff-explore-viewport {
4020
+ height: auto;
4021
+ min-height: calc(100vh - var(--boff-explore-offset, 0px));
4022
+ }
4023
+ }
4024
+ @supports (height: 100dvh) {
4025
+ @media (max-height: ${SHORT_VIEWPORT_PX}px) {
4026
+ .boff-explore-viewport {
4027
+ min-height: calc(100dvh - var(--boff-explore-offset, 0px));
3753
4028
  }
3754
- cell += char;
3755
4029
  }
3756
- cells.push(cell.trim());
3757
- if (trimmed.startsWith("|") && cells[0] === "") cells.shift();
3758
- if (trimmed.endsWith("|") && cells[cells.length - 1] === "") cells.pop();
3759
- return cells;
3760
4030
  }
3761
- function asSentence(text) {
3762
- return /[.!?]$/.test(text) ? text : `${text}.`;
4031
+
4032
+ /* ---- Composer dock --------------------------------------------------------------------
4033
+ Three declarations, each the fallback for the one after it: a plain 1rem where \`max()\`
4034
+ is unknown, then the measured overlap of any FOREIGN fixed chrome sitting over the
4035
+ composer (the host sites' cookie banner is 229px tall and lands exactly there), then the
4036
+ same plus the home-indicator inset. \`--boff-explore-chrome\` is set on the root only once
4037
+ an overlap has actually been measured, so everywhere else this collapses to 1rem. */
4038
+ .boff-explore-composer {
4039
+ padding-bottom: 1rem;
4040
+ padding-bottom: max(1rem, var(--boff-explore-chrome, 0px));
4041
+ padding-bottom: max(1rem, env(safe-area-inset-bottom), var(--boff-explore-chrome, 0px));
3763
4042
  }
3764
- function spokenTable(rows) {
3765
- if (rows.length < 2 || !TABLE_DELIMITER_ROW.test(rows[1] ?? "")) return rows;
3766
- const labels = tableCells(rows[0] ?? "");
3767
- const spoken = [];
3768
- for (const row of rows.slice(2)) {
3769
- const parts = [];
3770
- tableCells(row).forEach((value, column) => {
3771
- if (value === "") return;
3772
- const label = column === 0 ? "" : labels[column] ?? "";
3773
- parts.push(label === "" ? value : `${label}: ${value}`);
3774
- });
3775
- if (parts.length > 0) spoken.push(asSentence(parts.join(". ")));
3776
- }
3777
- return spoken;
4043
+ `;
4044
+ function ExploreStyles() {
4045
+ return /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: EXPLORE_CSS } });
3778
4046
  }
3779
- function readableText(markdown, mode) {
3780
- const lines = [];
3781
- let inFence = false;
3782
- let table = [];
3783
- const flushTable = () => {
3784
- if (table.length === 0) return;
3785
- for (const line of spokenTable(table)) lines.push(readableLine(line, mode));
3786
- table = [];
3787
- };
3788
- for (const raw of markdown.split("\n")) {
3789
- if (CODE_FENCE.test(raw)) {
3790
- flushTable();
3791
- inFence = !inFence;
3792
- continue;
3793
- }
3794
- if (inFence) {
3795
- lines.push(raw);
3796
- continue;
3797
- }
3798
- if (mode === "speech" && TABLE_ROW_LINE.test(raw)) {
3799
- table.push(raw);
3800
- continue;
3801
- }
3802
- flushTable();
3803
- lines.push(readableLine(raw, mode));
4047
+ var CAPABILITIES = [
4048
+ {
4049
+ icon: BookOpenText,
4050
+ title: "Grounded in our own docs",
4051
+ body: "Every answer comes from published product pages and documentation \u2014 never invented."
4052
+ },
4053
+ {
4054
+ icon: Layers,
4055
+ title: "Compare across the ecosystem",
4056
+ body: "Ask which product fits your team, or how two of them work together."
4057
+ },
4058
+ {
4059
+ icon: Link2,
4060
+ title: "Straight to the right page",
4061
+ body: "Get the sources and next steps \u2014 pricing, docs, a demo \u2014 without hunting."
3804
4062
  }
3805
- flushTable();
3806
- return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
4063
+ ];
4064
+ var MIN_VISIBLE_FRACTION = 0.25;
4065
+ var SDK_OVERLAY_SELECTOR = "[data-boff-explore]";
4066
+ var CTA_VARIANTS = {
4067
+ CONTACT: "default",
4068
+ WAITLIST: "default",
4069
+ APP: "default",
4070
+ PRICING: "secondary",
4071
+ STORE: "secondary",
4072
+ PARTNERS: "secondary",
4073
+ DOCS: "outline",
4074
+ SERVICES: "outline",
4075
+ OTHER: "outline"
4076
+ };
4077
+ function normalizeOffset(offset) {
4078
+ if (offset === void 0) return "0px";
4079
+ return typeof offset === "number" ? `${offset}px` : offset;
3807
4080
  }
3808
- function answerPlainText(markdown) {
3809
- return readableText(markdown, "clipboard");
4081
+ function isOwnElement(el, root) {
4082
+ if (el === document.body || el === document.documentElement) return true;
4083
+ if (root.contains(el)) return true;
4084
+ return el.closest(SDK_OVERLAY_SELECTOR) !== null;
3810
4085
  }
3811
- function answerSpeechText(markdown) {
3812
- return readableText(markdown, "speech");
4086
+ function visualViewportHeight() {
4087
+ return window.visualViewport?.height ?? window.innerHeight;
3813
4088
  }
3814
- var AnswerScopeContext = createContext(null);
3815
- var InsideLinkContext = createContext(false);
3816
- function AnswerLink(props) {
3817
- return /* @__PURE__ */ jsx(InsideLinkContext.Provider, { value: true, children: /* @__PURE__ */ jsx(MarkdownLink, { ...props }) });
4089
+ function clamp2(value, min, max) {
4090
+ return Math.min(Math.max(value, min), max);
3818
4091
  }
3819
- var INLINE_IMAGE_FRAME = "relative inline-block max-w-full overflow-hidden rounded-xl border border-border bg-muted align-top shadow-sm";
3820
- function InlineConceptImage({ src, alt }) {
3821
- const scope = useContext(AnswerScopeContext);
3822
- const insideLink = useContext(InsideLinkContext);
3823
- if (!scope || !scope.enabled || !isInlineImageAllowed(src, scope.origins) || scope.failed.has(src)) {
3824
- return null;
3825
- }
3826
- const { strings: T, onFailed } = scope;
3827
- const picture = /* @__PURE__ */ jsxs(Fragment, { children: [
3828
- /* @__PURE__ */ jsx(
3829
- "img",
3830
- {
3831
- src,
3832
- alt: alt ?? "",
3833
- loading: "lazy",
3834
- decoding: "async",
3835
- onError: () => onFailed(src),
3836
- className: "block h-auto max-h-80 w-auto max-w-full object-contain"
3837
- }
3838
- ),
3839
- /* @__PURE__ */ jsx(
3840
- "span",
3841
- {
3842
- "aria-hidden": "true",
3843
- 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",
3844
- children: T.conceptIllustration
3845
- }
3846
- )
3847
- ] });
3848
- return /* @__PURE__ */ jsx("span", { "data-boff-explore": "inline-image", className: "my-3 block", children: insideLink ? /* @__PURE__ */ jsx("span", { className: INLINE_IMAGE_FRAME, children: picture }) : /* @__PURE__ */ jsxs(
3849
- "a",
3850
- {
3851
- href: src,
3852
- target: "_blank",
3853
- rel: "noopener noreferrer",
3854
- className: cn(
3855
- INLINE_IMAGE_FRAME,
3856
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
3857
- ),
3858
- children: [
3859
- picture,
3860
- /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
3861
- "(",
3862
- T.opensInNewTab,
3863
- ")"
3864
- ] })
3865
- ]
3866
- }
3867
- ) });
4092
+ function isRestoredNavigation() {
4093
+ const entries = window.performance?.getEntriesByType?.("navigation");
4094
+ const entry = entries?.[0];
4095
+ if (!entry) return true;
4096
+ const navigation = entry;
4097
+ return navigation.type === "reload" || navigation.type === "back_forward";
3868
4098
  }
3869
- function markdownText(node) {
3870
- if (!node) return "";
3871
- if (typeof node.value === "string") return node.value;
3872
- return (node.children ?? []).map(markdownText).join("");
4099
+ function pageOwnsScrolling(el) {
4100
+ if (window.innerHeight > SHORT_VIEWPORT_PX) return false;
4101
+ return el.scrollHeight - el.clientHeight <= 1;
3873
4102
  }
3874
- function hasHeaderCells(node) {
3875
- return (node?.children ?? []).some((cell) => cell.tagName === "th");
4103
+ function isInternalPath(url) {
4104
+ return url.startsWith("/") && !url.startsWith("//");
3876
4105
  }
3877
- function findHeaderRow(node) {
3878
- for (const child of node?.children ?? []) {
3879
- if (child.tagName === "tr" && hasHeaderCells(child)) return child;
3880
- const nested = findHeaderRow(child);
3881
- if (nested !== null) return nested;
4106
+ function hostnameOf(url) {
4107
+ try {
4108
+ return new URL(url).hostname.replace(/^www\./, "");
4109
+ } catch {
4110
+ return "";
4111
+ }
4112
+ }
4113
+ function initialOf(value, fallback) {
4114
+ const source = (value || fallback).trim();
4115
+ return source ? source.slice(0, 1).toUpperCase() : "?";
4116
+ }
4117
+ function logoUrlOf(url) {
4118
+ try {
4119
+ const { origin, protocol } = new URL(url);
4120
+ if (protocol !== "https:" && protocol !== "http:") return null;
4121
+ return `${origin}/favicon.svg`;
4122
+ } catch {
4123
+ return null;
3882
4124
  }
3883
- return null;
3884
4125
  }
3885
- function tableColumnLabels(node) {
3886
- const row = findHeaderRow(node);
3887
- if (row === null) return [];
3888
- return (row.children ?? []).filter((cell) => cell.tagName === "th").map((cell) => markdownText(cell).trim());
4126
+ var REFERENCE_KIND_ORDER = [
4127
+ "CONCEPT",
4128
+ "DOCS",
4129
+ "PRICING",
4130
+ "USE_CASE",
4131
+ "PAGE"
4132
+ ];
4133
+ var PRODUCT_SUBDOMAINS = [
4134
+ "alphadocs.",
4135
+ "alphaapp.",
4136
+ "docs.",
4137
+ "alpha.",
4138
+ "app.",
4139
+ "www."
4140
+ ];
4141
+ function readCatalogUrl(raw) {
4142
+ let parsed;
4143
+ try {
4144
+ parsed = new URL(raw);
4145
+ } catch {
4146
+ return null;
4147
+ }
4148
+ if (parsed.protocol !== "https:") return null;
4149
+ const host = parsed.hostname.toLowerCase();
4150
+ const prefix = PRODUCT_SUBDOMAINS.find(
4151
+ (candidate) => host.startsWith(candidate)
4152
+ );
4153
+ const apex = prefix === void 0 ? host : host.slice(prefix.length);
4154
+ const port = parsed.port ? `:${parsed.port}` : "";
4155
+ return {
4156
+ origin: `https://${apex}${port}`,
4157
+ prefix,
4158
+ pathname: parsed.pathname
4159
+ };
3889
4160
  }
3890
- var TableColumnsContext = createContext([]);
3891
- function AnswerTable({ node, children }) {
3892
- const scope = useContext(AnswerScopeContext);
3893
- const table = /* @__PURE__ */ jsx(TableColumnsContext.Provider, { value: tableColumnLabels(node), children: /* @__PURE__ */ jsx("table", { children }) });
3894
- if (!scope) return table;
3895
- return /* @__PURE__ */ jsx(
3896
- "div",
3897
- {
3898
- "data-boff-explore": "table",
3899
- role: "region",
3900
- "aria-label": scope.strings.answerTable,
3901
- tabIndex: 0,
3902
- className: "boff-explore-table",
3903
- children: table
4161
+ function referenceKindOf(url, origins) {
4162
+ const parsed = readCatalogUrl(url);
4163
+ if (parsed === null || !origins.has(parsed.origin)) return null;
4164
+ if (parsed.prefix === "docs." || parsed.prefix === "alphadocs.")
4165
+ return "DOCS";
4166
+ const segment = parsed.pathname.toLowerCase().split("/").filter(Boolean)[0] ?? "";
4167
+ if (segment === "concept" || segment === "concepts") return "CONCEPT";
4168
+ if (segment === "docs") return "DOCS";
4169
+ if (segment === "pricing" || segment === "plans") return "PRICING";
4170
+ if (segment === "use-case" || segment === "use-cases" || segment === "solutions") {
4171
+ return "USE_CASE";
4172
+ }
4173
+ return "PAGE";
4174
+ }
4175
+ function productNamesByOrigin(products) {
4176
+ const names = /* @__PURE__ */ new Map();
4177
+ for (const product of products) {
4178
+ let url;
4179
+ try {
4180
+ url = new URL(product.website);
4181
+ } catch {
4182
+ continue;
3904
4183
  }
3905
- );
4184
+ if (url.protocol !== "https:") continue;
4185
+ const apex = url.hostname.replace(/^www\./, "");
4186
+ const port = url.port ? `:${url.port}` : "";
4187
+ const key = `https://${apex}${port}`;
4188
+ if (!names.has(key)) names.set(key, product.name);
4189
+ }
4190
+ return names;
3906
4191
  }
3907
- function AnswerTableRow({
3908
- node,
3909
- children,
3910
- ...props
3911
- }) {
3912
- const labels = useContext(TableColumnsContext);
3913
- if (labels.length === 0 || hasHeaderCells(node)) {
3914
- return /* @__PURE__ */ jsx("tr", { ...props, children });
4192
+ function summariseReferences(references, origins, productNames) {
4193
+ const counts = /* @__PURE__ */ new Map();
4194
+ const products = [];
4195
+ for (const reference of references) {
4196
+ const kind = referenceKindOf(reference.url, origins);
4197
+ if (kind !== null) counts.set(kind, (counts.get(kind) ?? 0) + 1);
4198
+ const parsed = readCatalogUrl(reference.url);
4199
+ const name = (parsed === null ? void 0 : productNames.get(parsed.origin)) ?? (reference.product || null);
4200
+ if (name != null && !products.includes(name)) products.push(name);
3915
4201
  }
3916
- let column = 0;
3917
- const cells = Children.map(children, (cell) => {
3918
- if (!isValidElement(cell)) return cell;
3919
- const label = labels[column] ?? "";
3920
- column += 1;
3921
- if (label === "") return cell;
3922
- return cloneElement(cell, {
3923
- "data-label": label
3924
- });
3925
- });
3926
- return /* @__PURE__ */ jsx("tr", { ...props, children: cells });
4202
+ return {
4203
+ total: references.length,
4204
+ kinds: REFERENCE_KIND_ORDER.filter((kind) => counts.has(kind)).map(
4205
+ (kind) => ({ kind, count: counts.get(kind) ?? 0 })
4206
+ ),
4207
+ products
4208
+ };
3927
4209
  }
3928
- var ANSWER_MARKDOWN_COMPONENTS = {
3929
- a: AnswerLink,
3930
- img: InlineConceptImage,
3931
- table: AnswerTable,
3932
- tr: AnswerTableRow
3933
- };
3934
4210
  function ReferenceCard({
3935
4211
  reference,
3936
4212
  onFollow
@@ -4269,6 +4545,195 @@ function AnswerFeedback({
4269
4545
  }
4270
4546
  );
4271
4547
  }
4548
+ function EscalateForm({
4549
+ strings: T,
4550
+ formId,
4551
+ onEscalate,
4552
+ onClose,
4553
+ onBusyChange
4554
+ }) {
4555
+ const [email, setEmail] = useState("");
4556
+ const [note, setNote] = useState("");
4557
+ const [error, setError] = useState(null);
4558
+ const [sending, setSending] = useState(false);
4559
+ const [sentTo, setSentTo] = useState(null);
4560
+ const emailRef = useRef(null);
4561
+ const inFlightRef = useRef(false);
4562
+ const titleId = `${formId}-title`;
4563
+ const emailId = `${formId}-email`;
4564
+ const noteId = `${formId}-note`;
4565
+ const errorId = `${formId}-error`;
4566
+ useEffect(() => {
4567
+ emailRef.current?.focus();
4568
+ }, []);
4569
+ useEffect(() => {
4570
+ onBusyChange?.(sending);
4571
+ return () => onBusyChange?.(false);
4572
+ }, [sending, onBusyChange]);
4573
+ const handleSubmit = useCallback(
4574
+ async (event) => {
4575
+ event.preventDefault();
4576
+ if (inFlightRef.current) return;
4577
+ const address = email.trim();
4578
+ if (!looksLikeDeliverableEmail(address)) {
4579
+ setError(T.escalateInvalidEmail);
4580
+ emailRef.current?.focus();
4581
+ return;
4582
+ }
4583
+ inFlightRef.current = true;
4584
+ setSending(true);
4585
+ setError(null);
4586
+ const result = await onEscalate(address, note);
4587
+ inFlightRef.current = false;
4588
+ setSending(false);
4589
+ switch (result.outcome) {
4590
+ case "sent":
4591
+ case "already-handed-over":
4592
+ setSentTo(result.email ?? address);
4593
+ return;
4594
+ case "invalid-email":
4595
+ setError(T.escalateInvalidEmail);
4596
+ emailRef.current?.focus();
4597
+ return;
4598
+ case "rate-limited":
4599
+ setError(T.escalateRateLimited);
4600
+ return;
4601
+ case "captcha":
4602
+ setError(T.escalateCaptcha);
4603
+ return;
4604
+ // `no-conversation` included: the form should not have been offered at all, and the
4605
+ // honest thing to show is that nothing was passed on.
4606
+ default:
4607
+ setError(T.escalateFailed);
4608
+ return;
4609
+ }
4610
+ },
4611
+ [T, email, note, onEscalate]
4612
+ );
4613
+ if (sentTo !== null) {
4614
+ return /* @__PURE__ */ jsx(
4615
+ "p",
4616
+ {
4617
+ id: formId,
4618
+ "data-boff-explore": "escalate-sent",
4619
+ role: "status",
4620
+ className: "boff-explore-fade mt-2 rounded-xl border border-border bg-card px-3 py-2 text-xs leading-relaxed text-card-foreground",
4621
+ children: fill(T.escalateSent, { email: sentTo })
4622
+ }
4623
+ );
4624
+ }
4625
+ return /* @__PURE__ */ jsxs(
4626
+ "form",
4627
+ {
4628
+ id: formId,
4629
+ "data-boff-explore": "escalate-form",
4630
+ "aria-labelledby": titleId,
4631
+ noValidate: true,
4632
+ onSubmit: handleSubmit,
4633
+ onKeyDown: (event) => {
4634
+ if (event.key !== "Escape") return;
4635
+ event.stopPropagation();
4636
+ if (sending) return;
4637
+ onClose();
4638
+ },
4639
+ className: "boff-explore-fade mt-2 space-y-2 rounded-xl border border-border bg-card p-3",
4640
+ children: [
4641
+ /* @__PURE__ */ jsx("p", { id: titleId, className: "text-xs font-medium text-card-foreground", children: T.escalateTitle }),
4642
+ /* @__PURE__ */ jsx("p", { className: "text-xs leading-relaxed text-muted-foreground", children: T.escalateBody }),
4643
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
4644
+ /* @__PURE__ */ jsx(
4645
+ "label",
4646
+ {
4647
+ htmlFor: emailId,
4648
+ className: "block text-xs font-medium text-card-foreground",
4649
+ children: T.escalateEmailLabel
4650
+ }
4651
+ ),
4652
+ /* @__PURE__ */ jsx(
4653
+ "input",
4654
+ {
4655
+ ref: emailRef,
4656
+ id: emailId,
4657
+ name: "email",
4658
+ "data-boff-explore": "escalate-email",
4659
+ type: "email",
4660
+ inputMode: "email",
4661
+ autoComplete: "email",
4662
+ required: true,
4663
+ value: email,
4664
+ onChange: (event) => {
4665
+ setEmail(event.target.value);
4666
+ if (error) setError(null);
4667
+ },
4668
+ "aria-invalid": error ? "true" : void 0,
4669
+ "aria-describedby": error ? errorId : void 0,
4670
+ placeholder: T.escalateEmailPlaceholder,
4671
+ className: "w-full rounded-md border border-border bg-background px-2 py-1 text-base text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring sm:text-sm"
4672
+ }
4673
+ )
4674
+ ] }),
4675
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
4676
+ /* @__PURE__ */ jsx(
4677
+ "label",
4678
+ {
4679
+ htmlFor: noteId,
4680
+ className: "block text-xs font-medium text-card-foreground",
4681
+ children: T.escalateNoteLabel
4682
+ }
4683
+ ),
4684
+ /* @__PURE__ */ jsx(
4685
+ Textarea,
4686
+ {
4687
+ id: noteId,
4688
+ name: "note",
4689
+ "data-boff-explore": "escalate-note",
4690
+ rows: 2,
4691
+ value: note,
4692
+ onChange: (event) => setNote(event.target.value),
4693
+ maxLength: EXPLORE_ESCALATION_NOTE_SOFT_MAX,
4694
+ className: "min-h-0 border-border bg-background"
4695
+ }
4696
+ )
4697
+ ] }),
4698
+ error ? /* @__PURE__ */ jsx(
4699
+ "p",
4700
+ {
4701
+ id: errorId,
4702
+ "data-boff-explore": "escalate-error",
4703
+ role: "alert",
4704
+ className: "text-xs leading-relaxed text-destructive",
4705
+ children: error
4706
+ }
4707
+ ) : null,
4708
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
4709
+ /* @__PURE__ */ jsx(
4710
+ Button,
4711
+ {
4712
+ "data-boff-explore": "escalate-send",
4713
+ type: "submit",
4714
+ size: "sm",
4715
+ disabled: sending,
4716
+ children: sending ? T.escalateSending : T.escalateSubmit
4717
+ }
4718
+ ),
4719
+ /* @__PURE__ */ jsx(
4720
+ Button,
4721
+ {
4722
+ "data-boff-explore": "escalate-cancel",
4723
+ type: "button",
4724
+ size: "sm",
4725
+ variant: "ghost",
4726
+ className: "text-muted-foreground",
4727
+ disabled: sending,
4728
+ onClick: onClose,
4729
+ children: T.escalateCancel
4730
+ }
4731
+ )
4732
+ ] })
4733
+ ]
4734
+ }
4735
+ );
4736
+ }
4272
4737
  var PROVENANCE_CHIP = "inline-flex max-w-full items-center gap-1 rounded-full border border-border bg-muted/60 px-2 py-0.5 text-[11px] leading-5 text-muted-foreground";
4273
4738
  function ProvenanceChips({
4274
4739
  strings: T,
@@ -4462,31 +4927,15 @@ function AssistantBubble({
4462
4927
  anchorRef,
4463
4928
  onFollow,
4464
4929
  onFollowUp,
4465
- onFeedback
4930
+ onFeedback,
4931
+ onEscalate,
4932
+ isLatestAnswer = false
4466
4933
  }) {
4467
4934
  const streaming = message.status === "PENDING" || message.status === "RUNNING";
4468
4935
  const body = message.content || message.partialContent || "";
4469
4936
  const failed = message.status === "FAILED";
4470
4937
  const completed = message.status === "COMPLETED";
4471
4938
  const visible = useProgressiveReveal(body, !streaming);
4472
- const [failedImages, setFailedImages] = useState(
4473
- () => /* @__PURE__ */ new Set()
4474
- );
4475
- const markImageFailed = useCallback((src) => {
4476
- setFailedImages(
4477
- (current) => current.has(src) ? current : new Set(current).add(src)
4478
- );
4479
- }, []);
4480
- const answerScope = useMemo(
4481
- () => ({
4482
- strings: T,
4483
- enabled: completed,
4484
- origins: imageOrigins,
4485
- failed: failedImages,
4486
- onFailed: markImageFailed
4487
- }),
4488
- [T, completed, imageOrigins, failedImages, markImageFailed]
4489
- );
4490
4939
  const provenance = useMemo(
4491
4940
  () => summariseReferences(message.references, imageOrigins, productNames),
4492
4941
  [message.references, imageOrigins, productNames]
@@ -4500,6 +4949,20 @@ function AssistantBubble({
4500
4949
  [message.content]
4501
4950
  );
4502
4951
  const grounded = completed && message.answered !== false;
4952
+ const [escalateOpen, setEscalateOpen] = useState(false);
4953
+ const [escalateBusy, setEscalateBusy] = useState(false);
4954
+ const escalateTriggerRef = useRef(null);
4955
+ const escalateFormId = `boff-explore-escalate-${message.id}`;
4956
+ const unanswered = completed && message.answered === false;
4957
+ const offerEscalate = Boolean(onEscalate) && completed;
4958
+ const showQuietEscalate = offerEscalate && !unanswered && isLatestAnswer;
4959
+ const closeEscalate = useCallback(() => {
4960
+ setEscalateOpen(false);
4961
+ escalateTriggerRef.current?.focus();
4962
+ }, []);
4963
+ const handleEscalateBusy = useCallback((busy) => {
4964
+ setEscalateBusy(busy);
4965
+ }, []);
4503
4966
  return /* @__PURE__ */ jsxs(
4504
4967
  "div",
4505
4968
  {
@@ -4517,7 +4980,7 @@ function AssistantBubble({
4517
4980
  }
4518
4981
  ),
4519
4982
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-3", children: [
4520
- message.status === "COMPLETED" && message.answered === false ? /* @__PURE__ */ jsxs(
4983
+ unanswered ? /* @__PURE__ */ jsxs(
4521
4984
  "div",
4522
4985
  {
4523
4986
  "data-boff-explore": "no-answer",
@@ -4527,18 +4990,39 @@ function AssistantBubble({
4527
4990
  /* @__PURE__ */ jsxs("span", { children: [
4528
4991
  T.noAnswer,
4529
4992
  " ",
4530
- /* @__PURE__ */ jsx(
4531
- "a",
4532
- {
4533
- href: "/contact",
4534
- className: "underline underline-offset-2 hover:text-primary",
4535
- onClick: (event) => {
4536
- if (!onNavigate) return;
4537
- event.preventDefault();
4538
- onNavigate("/contact");
4539
- },
4540
- children: T.askHuman
4541
- }
4993
+ offerEscalate ? (
4994
+ /* The point of the wave: the same words, but they now open a form that
4995
+ sends the conversation we are both looking at, instead of a link to a
4996
+ blank contact page that asks the visitor to type it all again. */
4997
+ /* @__PURE__ */ jsx(
4998
+ "button",
4999
+ {
5000
+ ref: escalateTriggerRef,
5001
+ "data-boff-explore": "escalate-open",
5002
+ type: "button",
5003
+ "aria-expanded": escalateOpen,
5004
+ "aria-controls": escalateOpen ? escalateFormId : void 0,
5005
+ onClick: () => setEscalateOpen(true),
5006
+ className: "rounded-sm underline underline-offset-2 hover:text-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
5007
+ children: T.askHuman
5008
+ }
5009
+ )
5010
+ ) : (
5011
+ /* No conversation to hand over (a shared, read-only view). The old dead end
5012
+ is still better than a control whose submit could only fail. */
5013
+ /* @__PURE__ */ jsx(
5014
+ "a",
5015
+ {
5016
+ href: "/contact",
5017
+ className: "underline underline-offset-2 hover:text-primary",
5018
+ onClick: (event) => {
5019
+ if (!onNavigate) return;
5020
+ event.preventDefault();
5021
+ onNavigate("/contact");
5022
+ },
5023
+ children: T.askHuman
5024
+ }
5025
+ )
4542
5026
  ),
4543
5027
  "."
4544
5028
  ] })
@@ -4546,7 +5030,16 @@ function AssistantBubble({
4546
5030
  }
4547
5031
  ) : null,
4548
5032
  visible ? /* @__PURE__ */ jsxs("div", { className: "rounded-2xl rounded-tl-md border border-border bg-card px-4 py-3 shadow-sm", children: [
4549
- /* @__PURE__ */ jsx("div", { className: "boff-prose boff-prose-sm boff-explore-body min-w-0 text-card-foreground", children: /* @__PURE__ */ jsx(AnswerScopeContext.Provider, { value: answerScope, children: /* @__PURE__ */ jsx(Markdown, { components: ANSWER_MARKDOWN_COMPONENTS, children: visible }) }) }),
5033
+ /* @__PURE__ */ jsx(
5034
+ ExploreAnswerBody,
5035
+ {
5036
+ strings: T,
5037
+ imageOrigins,
5038
+ enabled: completed,
5039
+ className: "boff-prose-sm text-card-foreground",
5040
+ children: visible
5041
+ }
5042
+ ),
4550
5043
  streaming ? /* @__PURE__ */ jsx("span", { className: "boff-explore-caret", "aria-hidden": "true" }) : null
4551
5044
  ] }) : null,
4552
5045
  grounded && provenance.total > 0 ? /* @__PURE__ */ jsx(ProvenanceChips, { strings: T, provenance }) : null,
@@ -4606,6 +5099,30 @@ function AssistantBubble({
4606
5099
  feedback: message.feedback,
4607
5100
  onSubmit: onFeedback
4608
5101
  }
5102
+ ) : null,
5103
+ showQuietEscalate ? /* @__PURE__ */ jsx("div", { className: "pt-0.5", children: /* @__PURE__ */ jsx(
5104
+ "button",
5105
+ {
5106
+ ref: escalateTriggerRef,
5107
+ "data-boff-explore": "escalate-open",
5108
+ type: "button",
5109
+ "aria-expanded": escalateOpen,
5110
+ "aria-controls": escalateOpen ? escalateFormId : void 0,
5111
+ disabled: escalateBusy,
5112
+ onClick: () => setEscalateOpen((open) => !open),
5113
+ className: "rounded-sm text-xs text-muted-foreground underline underline-offset-2 hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
5114
+ children: T.escalateOpen
5115
+ }
5116
+ ) }) : null,
5117
+ escalateOpen && onEscalate ? /* @__PURE__ */ jsx(
5118
+ EscalateForm,
5119
+ {
5120
+ strings: T,
5121
+ formId: escalateFormId,
5122
+ onEscalate,
5123
+ onClose: closeEscalate,
5124
+ onBusyChange: handleEscalateBusy
5125
+ }
4609
5126
  ) : null
4610
5127
  ] })
4611
5128
  ]
@@ -4831,7 +5348,7 @@ function ConceptStrip({
4831
5348
  "div",
4832
5349
  {
4833
5350
  "data-boff-explore": "concept-strip-block",
4834
- className: "boff-explore-rise boff-explore-rise-1 mt-6",
5351
+ className: "boff-explore-rise boff-explore-rise-3 mt-6",
4835
5352
  children: [
4836
5353
  /* @__PURE__ */ jsx("p", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: T.conceptStripTitle }),
4837
5354
  /* @__PURE__ */ jsx(
@@ -5021,15 +5538,6 @@ function WelcomeState({
5021
5538
  /* @__PURE__ */ jsx("h1", { className: "mt-4 text-balance break-words text-2xl font-bold tracking-tight sm:text-4xl", children: title }),
5022
5539
  /* @__PURE__ */ jsx("p", { className: "mx-auto mt-2.5 max-w-2xl text-pretty text-sm leading-relaxed text-muted-foreground sm:text-lg", children: body })
5023
5540
  ] }),
5024
- /* @__PURE__ */ jsx(
5025
- ConceptStrip,
5026
- {
5027
- strings: T,
5028
- highlights: conceptHighlights,
5029
- onAsk: onPrompt,
5030
- disabled
5031
- }
5032
- ),
5033
5541
  /* @__PURE__ */ jsx("ul", { className: "boff-explore-rise boff-explore-rise-1 mt-6 grid grid-cols-3 gap-2 sm:mt-7 sm:gap-2.5", children: CAPABILITIES.map(({ icon: Icon, body: capBody }, i) => /* @__PURE__ */ jsxs(
5034
5542
  "li",
5035
5543
  {
@@ -5065,7 +5573,16 @@ function WelcomeState({
5065
5573
  onFocus
5066
5574
  }
5067
5575
  )
5068
- ] }) : null
5576
+ ] }) : null,
5577
+ /* @__PURE__ */ jsx(
5578
+ ConceptStrip,
5579
+ {
5580
+ strings: T,
5581
+ highlights: conceptHighlights,
5582
+ onAsk: onPrompt,
5583
+ disabled
5584
+ }
5585
+ )
5069
5586
  ] });
5070
5587
  }
5071
5588
  function ExplorePage({
@@ -5112,6 +5629,8 @@ function ExplorePage({
5112
5629
  clearHistory,
5113
5630
  recordClick,
5114
5631
  submitFeedback,
5632
+ escalate,
5633
+ canEscalate,
5115
5634
  createShareLink,
5116
5635
  isSharedView
5117
5636
  } = useExploreChat({
@@ -5555,6 +6074,7 @@ function ExplorePage({
5555
6074
  ),
5556
6075
  /* @__PURE__ */ jsx(ProseStyles, {}),
5557
6076
  /* @__PURE__ */ jsx(ExploreStyles, {}),
6077
+ /* @__PURE__ */ jsx(ExploreAnswerStyles, {}),
5558
6078
  !showWelcome ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 border-b border-border px-4 py-2.5", children: [
5559
6079
  /* @__PURE__ */ jsx(
5560
6080
  "span",
@@ -5733,7 +6253,9 @@ function ExplorePage({
5733
6253
  anchorRef: message.id === latestAssistantId ? latestAssistantRef : void 0,
5734
6254
  onFollow: (kind, url) => recordClick(message.id, kind, url),
5735
6255
  onFollowUp: sendPrompt,
5736
- onFeedback: isSharedView ? void 0 : (rating, detail) => submitFeedback(message.id, rating, detail)
6256
+ onFeedback: isSharedView ? void 0 : (rating, detail) => submitFeedback(message.id, rating, detail),
6257
+ onEscalate: canEscalate ? escalate : void 0,
6258
+ isLatestAnswer: message.id === latestAssistantId
5737
6259
  },
5738
6260
  message.id
5739
6261
  )
@@ -5840,10 +6362,19 @@ function ExplorePage({
5840
6362
  /* @__PURE__ */ jsxs(
5841
6363
  "div",
5842
6364
  {
6365
+ "data-boff-explore": "composer-field",
5843
6366
  className: cn(
5844
6367
  "flex min-w-0 items-end gap-2 rounded-2xl border bg-card p-2 shadow-sm transition-all",
5845
6368
  busy && "boff-explore-busy",
5846
- overLimit ? "border-destructive/60" : "border-border focus-within:border-primary/40 focus-within:ring-1 focus-within:ring-ring"
6369
+ overLimit ? "border-destructive/60" : "border-border focus-within:border-primary/40 focus-within:ring-1 focus-within:ring-ring",
6370
+ // The halo is an invitation, and an invitation is only useful before it has
6371
+ // been accepted: while the conversation is empty, the composer is usable, and
6372
+ // nothing has been typed. Once any of those stops being true the class goes
6373
+ // and the box is an ordinary card again. `-quiet` keeps the ring but stops the
6374
+ // movement the moment there are characters in the field, so it does not pulse
6375
+ // at someone who is mid-sentence.
6376
+ showWelcome && !composerDisabled && !overLimit && "boff-explore-invite",
6377
+ showWelcome && draft.length > 0 && "boff-explore-invite-quiet"
5847
6378
  ),
5848
6379
  children: [
5849
6380
  /* @__PURE__ */ jsx(