@burdenoff/website-sdk 2026.922.3 → 2026.922.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -103,6 +103,23 @@ var EN = {
103
103
  nextSuggestion: "Next suggestion",
104
104
  conceptIllustration: "Concept illustration",
105
105
  opensInNewTab: "opens in a new tab",
106
+ conceptStripTitle: "Or start from a concept",
107
+ conceptStripLabel: "Concepts you can ask about",
108
+ conceptAsk: "Ask about {title} in {product}",
109
+ conceptAskQuestion: "How does {title} work in {product}?",
110
+ copyAnswer: "Copy answer",
111
+ copyFailed: "Couldn't copy",
112
+ readAloud: "Read aloud",
113
+ stopReading: "Stop reading",
114
+ answerTable: "Table",
115
+ groundedIn: "What this answer is based on",
116
+ sourceCountOne: "1 source",
117
+ sourceCountMany: "{count} sources",
118
+ sourceKindConcept: "Concept",
119
+ sourceKindDocs: "Documentation",
120
+ sourceKindPricing: "Pricing",
121
+ sourceKindUseCase: "Use case",
122
+ sourceKindPage: "Product page",
106
123
  seoTitle: "Describe a problem, get an answer",
107
124
  seoDescription: "Describe the problem you're facing in your own words and get an answer drawn from our product documentation, with links to the exact pages it came from.",
108
125
  seoDescriptionProduct: "Describe the problem you're facing in your own words and get an answer drawn from {product}'s documentation \u2014 and every other product we build \u2014 with links to the exact pages it came from.",
@@ -171,6 +188,23 @@ var ZH = {
171
188
  nextSuggestion: "\u4E0B\u4E00\u4E2A\u5EFA\u8BAE",
172
189
  conceptIllustration: "\u6982\u5FF5\u793A\u610F\u56FE",
173
190
  opensInNewTab: "\u5728\u65B0\u6807\u7B7E\u9875\u4E2D\u6253\u5F00",
191
+ conceptStripTitle: "\u6216\u8005\u4ECE\u4E00\u4E2A\u6982\u5FF5\u5F00\u59CB",
192
+ conceptStripLabel: "\u4F60\u53EF\u4EE5\u8BE2\u95EE\u7684\u6982\u5FF5",
193
+ conceptAsk: "\u8BE2\u95EE {product} \u4E2D\u7684 {title}",
194
+ conceptAskQuestion: "{product} \u7684 {title} \u662F\u600E\u4E48\u8FD0\u4F5C\u7684\uFF1F",
195
+ copyAnswer: "\u590D\u5236\u56DE\u7B54",
196
+ copyFailed: "\u590D\u5236\u5931\u8D25",
197
+ readAloud: "\u6717\u8BFB",
198
+ stopReading: "\u505C\u6B62\u6717\u8BFB",
199
+ answerTable: "\u8868\u683C",
200
+ groundedIn: "\u8FD9\u4E2A\u56DE\u7B54\u7684\u4F9D\u636E",
201
+ sourceCountOne: "1 \u4E2A\u6765\u6E90",
202
+ sourceCountMany: "{count} \u4E2A\u6765\u6E90",
203
+ sourceKindConcept: "\u6982\u5FF5",
204
+ sourceKindDocs: "\u6587\u6863",
205
+ sourceKindPricing: "\u4EF7\u683C",
206
+ sourceKindUseCase: "\u5E94\u7528\u573A\u666F",
207
+ sourceKindPage: "\u4EA7\u54C1\u9875\u9762",
174
208
  seoTitle: "\u63CF\u8FF0\u95EE\u9898\uFF0C\u83B7\u5F97\u7B54\u6848",
175
209
  seoDescription: "\u7528\u4F60\u81EA\u5DF1\u7684\u8BDD\u63CF\u8FF0\u9047\u5230\u7684\u95EE\u9898\uFF0C\u5373\u53EF\u83B7\u5F97\u6765\u81EA\u6211\u4EEC\u4EA7\u54C1\u6587\u6863\u7684\u56DE\u7B54\uFF0C\u5E76\u9644\u4E0A\u7B54\u6848\u51FA\u5904\u7684\u9875\u9762\u94FE\u63A5\u3002",
176
210
  seoDescriptionProduct: "\u7528\u4F60\u81EA\u5DF1\u7684\u8BDD\u63CF\u8FF0\u9047\u5230\u7684\u95EE\u9898\uFF0C\u5373\u53EF\u83B7\u5F97\u6765\u81EA {product} \u53CA\u6211\u4EEC\u5176\u4ED6\u4EA7\u54C1\u6587\u6863\u7684\u56DE\u7B54\uFF0C\u5E76\u9644\u4E0A\u7B54\u6848\u51FA\u5904\u7684\u9875\u9762\u94FE\u63A5\u3002",
@@ -235,6 +269,23 @@ var HI = {
235
269
  nextSuggestion: "\u0905\u0917\u0932\u093E \u0938\u0941\u091D\u093E\u0935",
236
270
  conceptIllustration: "\u0905\u0935\u0927\u093E\u0930\u0923\u093E \u091A\u093F\u0924\u094D\u0930\u0923",
237
271
  opensInNewTab: "\u0928\u090F \u091F\u0948\u092C \u092E\u0947\u0902 \u0916\u0941\u0932\u0924\u093E \u0939\u0948",
272
+ conceptStripTitle: "\u092F\u093E \u0915\u093F\u0938\u0940 \u0905\u0935\u0927\u093E\u0930\u0923\u093E \u0938\u0947 \u0936\u0941\u0930\u0942 \u0915\u0930\u0947\u0902",
273
+ conceptStripLabel: "\u091C\u093F\u0928 \u0905\u0935\u0927\u093E\u0930\u0923\u093E\u0913\u0902 \u0915\u0947 \u092C\u093E\u0930\u0947 \u092E\u0947\u0902 \u0906\u092A \u092A\u0942\u091B \u0938\u0915\u0924\u0947 \u0939\u0948\u0902",
274
+ conceptAsk: "{product} \u092E\u0947\u0902 {title} \u0915\u0947 \u092C\u093E\u0930\u0947 \u092E\u0947\u0902 \u092A\u0942\u091B\u0947\u0902",
275
+ conceptAskQuestion: "{product} \u092E\u0947\u0902 {title} \u0915\u0948\u0938\u0947 \u0915\u093E\u092E \u0915\u0930\u0924\u093E \u0939\u0948?",
276
+ copyAnswer: "\u0909\u0924\u094D\u0924\u0930 \u0915\u0949\u092A\u0940 \u0915\u0930\u0947\u0902",
277
+ copyFailed: "\u0915\u0949\u092A\u0940 \u0928\u0939\u0940\u0902 \u0939\u094B \u0938\u0915\u093E",
278
+ readAloud: "\u091C\u093C\u094B\u0930 \u0938\u0947 \u092A\u0922\u093C\u0947\u0902",
279
+ stopReading: "\u092A\u0922\u093C\u0928\u093E \u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
280
+ answerTable: "\u0924\u093E\u0932\u093F\u0915\u093E",
281
+ groundedIn: "\u092F\u0939 \u0909\u0924\u094D\u0924\u0930 \u0915\u093F\u0938 \u092A\u0930 \u0906\u0927\u093E\u0930\u093F\u0924 \u0939\u0948",
282
+ sourceCountOne: "1 \u0938\u094D\u0930\u094B\u0924",
283
+ sourceCountMany: "{count} \u0938\u094D\u0930\u094B\u0924",
284
+ sourceKindConcept: "\u0905\u0935\u0927\u093E\u0930\u0923\u093E",
285
+ sourceKindDocs: "\u0926\u0938\u094D\u0924\u093E\u0935\u0947\u091C\u093C",
286
+ sourceKindPricing: "\u092E\u0942\u0932\u094D\u092F",
287
+ sourceKindUseCase: "\u0909\u092A\u092F\u094B\u0917 \u0915\u093E \u0909\u0926\u093E\u0939\u0930\u0923",
288
+ sourceKindPage: "\u0909\u0924\u094D\u092A\u093E\u0926 \u092A\u0947\u091C",
238
289
  seoTitle: "\u0938\u092E\u0938\u094D\u092F\u093E \u092C\u0924\u093E\u090F\u0902, \u091C\u0935\u093E\u092C \u092A\u093E\u090F\u0902",
239
290
  seoDescription: "\u0905\u092A\u0928\u0940 \u0938\u092E\u0938\u094D\u092F\u093E \u0905\u092A\u0928\u0947 \u0936\u092C\u094D\u0926\u094B\u0902 \u092E\u0947\u0902 \u092C\u0924\u093E\u090F\u0902 \u0914\u0930 \u0939\u092E\u093E\u0930\u0947 \u0909\u0924\u094D\u092A\u093E\u0926 \u0926\u0938\u094D\u0924\u093E\u0935\u0947\u091C\u093C\u094B\u0902 \u0938\u0947 \u0932\u093F\u092F\u093E \u0917\u092F\u093E \u091C\u0935\u093E\u092C \u092A\u093E\u090F\u0902, \u0938\u093E\u0925 \u092E\u0947\u0902 \u0909\u0928\u094D\u0939\u0940\u0902 \u092A\u0928\u094D\u0928\u094B\u0902 \u0915\u0947 \u0932\u093F\u0902\u0915 \u091C\u0939\u093E\u0901 \u0938\u0947 \u0935\u0939 \u0906\u092F\u093E \u0939\u0948\u0964",
240
291
  seoDescriptionProduct: "\u0905\u092A\u0928\u0940 \u0938\u092E\u0938\u094D\u092F\u093E \u0905\u092A\u0928\u0947 \u0936\u092C\u094D\u0926\u094B\u0902 \u092E\u0947\u0902 \u092C\u0924\u093E\u090F\u0902 \u0914\u0930 {product} \u0924\u0925\u093E \u0939\u092E\u093E\u0930\u0947 \u092C\u093E\u0915\u0940 \u0909\u0924\u094D\u092A\u093E\u0926\u094B\u0902 \u0915\u0947 \u0926\u0938\u094D\u0924\u093E\u0935\u0947\u091C\u093C\u094B\u0902 \u0938\u0947 \u0932\u093F\u092F\u093E \u0917\u092F\u093E \u091C\u0935\u093E\u092C \u092A\u093E\u090F\u0902, \u0938\u093E\u0925 \u092E\u0947\u0902 \u0909\u0928\u094D\u0939\u0940\u0902 \u092A\u0928\u094D\u0928\u094B\u0902 \u0915\u0947 \u0932\u093F\u0902\u0915 \u091C\u0939\u093E\u0901 \u0938\u0947 \u0935\u0939 \u0906\u092F\u093E \u0939\u0948\u0964",
@@ -303,6 +354,23 @@ var ES = {
303
354
  nextSuggestion: "Sugerencia siguiente",
304
355
  conceptIllustration: "Ilustraci\xF3n del concepto",
305
356
  opensInNewTab: "se abre en una pesta\xF1a nueva",
357
+ conceptStripTitle: "O empieza por un concepto",
358
+ conceptStripLabel: "Conceptos sobre los que puedes preguntar",
359
+ conceptAsk: "Preguntar por {title} en {product}",
360
+ conceptAskQuestion: "\xBFC\xF3mo funciona {title} en {product}?",
361
+ copyAnswer: "Copiar respuesta",
362
+ copyFailed: "No se pudo copiar",
363
+ readAloud: "Leer en voz alta",
364
+ stopReading: "Dejar de leer",
365
+ answerTable: "Tabla",
366
+ groundedIn: "En qu\xE9 se basa esta respuesta",
367
+ sourceCountOne: "1 fuente",
368
+ sourceCountMany: "{count} fuentes",
369
+ sourceKindConcept: "Concepto",
370
+ sourceKindDocs: "Documentaci\xF3n",
371
+ sourceKindPricing: "Precios",
372
+ sourceKindUseCase: "Caso de uso",
373
+ sourceKindPage: "P\xE1gina de producto",
306
374
  seoTitle: "Describe un problema y obt\xE9n una respuesta",
307
375
  seoDescription: "Describe con tus palabras el problema que tienes y recibe una respuesta extra\xEDda de la documentaci\xF3n de nuestros productos, con enlaces a las p\xE1ginas exactas de las que procede.",
308
376
  seoDescriptionProduct: "Describe con tus palabras el problema que tienes y recibe una respuesta extra\xEDda de la documentaci\xF3n de {product} y del resto de nuestros productos, con enlaces a las p\xE1ginas exactas de las que procede.",
@@ -371,6 +439,23 @@ var AR = {
371
439
  nextSuggestion: "\u0627\u0644\u0627\u0642\u062A\u0631\u0627\u062D \u0627\u0644\u062A\u0627\u0644\u064A",
372
440
  conceptIllustration: "\u0631\u0633\u0645 \u062A\u0648\u0636\u064A\u062D\u064A \u0644\u0644\u0645\u0641\u0647\u0648\u0645",
373
441
  opensInNewTab: "\u064A\u064F\u0641\u062A\u062D \u0641\u064A \u0639\u0644\u0627\u0645\u0629 \u062A\u0628\u0648\u064A\u0628 \u062C\u062F\u064A\u062F\u0629",
442
+ conceptStripTitle: "\u0623\u0648 \u0627\u0628\u062F\u0623 \u0645\u0646 \u0645\u0641\u0647\u0648\u0645",
443
+ conceptStripLabel: "\u0645\u0641\u0627\u0647\u064A\u0645 \u064A\u0645\u0643\u0646\u0643 \u0627\u0644\u0633\u0624\u0627\u0644 \u0639\u0646\u0647\u0627",
444
+ conceptAsk: "\u0627\u0633\u0623\u0644 \u0639\u0646 {title} \u0641\u064A {product}",
445
+ conceptAskQuestion: "\u0643\u064A\u0641 \u064A\u0639\u0645\u0644 {title} \u0641\u064A {product}\u061F",
446
+ copyAnswer: "\u0646\u0633\u062E \u0627\u0644\u0625\u062C\u0627\u0628\u0629",
447
+ copyFailed: "\u062A\u0639\u0630\u0651\u0631 \u0627\u0644\u0646\u0633\u062E",
448
+ readAloud: "\u0627\u0642\u0631\u0623 \u0628\u0635\u0648\u062A \u0639\u0627\u0644\u064D",
449
+ stopReading: "\u0625\u064A\u0642\u0627\u0641 \u0627\u0644\u0642\u0631\u0627\u0621\u0629",
450
+ answerTable: "\u062C\u062F\u0648\u0644",
451
+ groundedIn: "\u0639\u0644\u0649 \u0645\u0627\u0630\u0627 \u062A\u0633\u062A\u0646\u062F \u0647\u0630\u0647 \u0627\u0644\u0625\u062C\u0627\u0628\u0629",
452
+ sourceCountOne: "\u0645\u0635\u062F\u0631 \u0648\u0627\u062D\u062F",
453
+ sourceCountMany: "{count} \u0645\u0635\u0627\u062F\u0631",
454
+ sourceKindConcept: "\u0645\u0641\u0647\u0648\u0645",
455
+ sourceKindDocs: "\u0627\u0644\u062A\u0648\u062B\u064A\u0642",
456
+ sourceKindPricing: "\u0627\u0644\u0623\u0633\u0639\u0627\u0631",
457
+ sourceKindUseCase: "\u062D\u0627\u0644\u0629 \u0627\u0633\u062A\u062E\u062F\u0627\u0645",
458
+ sourceKindPage: "\u0635\u0641\u062D\u0629 \u0627\u0644\u0645\u0646\u062A\u062C",
374
459
  seoTitle: "\u0635\u0641 \u0645\u0634\u0643\u0644\u062A\u0643 \u0648\u0627\u062D\u0635\u0644 \u0639\u0644\u0649 \u0625\u062C\u0627\u0628\u0629",
375
460
  seoDescription: "\u0635\u0641 \u0627\u0644\u0645\u0634\u0643\u0644\u0629 \u0627\u0644\u062A\u064A \u062A\u0648\u0627\u062C\u0647\u0647\u0627 \u0628\u0643\u0644\u0645\u0627\u062A\u0643 \u0648\u0627\u062D\u0635\u0644 \u0639\u0644\u0649 \u0625\u062C\u0627\u0628\u0629 \u0645\u0633\u062A\u0645\u062F\u0629 \u0645\u0646 \u0648\u062B\u0627\u0626\u0642 \u0645\u0646\u062A\u062C\u0627\u062A\u0646\u0627\u060C \u0645\u0639 \u0631\u0648\u0627\u0628\u0637 \u0625\u0644\u0649 \u0627\u0644\u0635\u0641\u062D\u0627\u062A \u0627\u0644\u062A\u064A \u062C\u0627\u0621\u062A \u0645\u0646\u0647\u0627.",
376
461
  seoDescriptionProduct: "\u0635\u0641 \u0627\u0644\u0645\u0634\u0643\u0644\u0629 \u0627\u0644\u062A\u064A \u062A\u0648\u0627\u062C\u0647\u0647\u0627 \u0628\u0643\u0644\u0645\u0627\u062A\u0643 \u0648\u0627\u062D\u0635\u0644 \u0639\u0644\u0649 \u0625\u062C\u0627\u0628\u0629 \u0645\u0633\u062A\u0645\u062F\u0629 \u0645\u0646 \u0648\u062B\u0627\u0626\u0642 {product} \u0648\u0628\u0642\u064A\u0629 \u0645\u0646\u062A\u062C\u0627\u062A\u0646\u0627\u060C \u0645\u0639 \u0631\u0648\u0627\u0628\u0637 \u0625\u0644\u0649 \u0627\u0644\u0635\u0641\u062D\u0627\u062A \u0627\u0644\u062A\u064A \u062C\u0627\u0621\u062A \u0645\u0646\u0647\u0627.",
@@ -439,6 +524,23 @@ var TA = {
439
524
  nextSuggestion: "\u0B85\u0B9F\u0BC1\u0BA4\u0BCD\u0BA4 \u0BAA\u0BB0\u0BBF\u0BA8\u0BCD\u0BA4\u0BC1\u0BB0\u0BC8",
440
525
  conceptIllustration: "\u0B95\u0BB0\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1 \u0BB5\u0BBF\u0BB3\u0B95\u0BCD\u0B95\u0BAA\u0BCD\u0BAA\u0B9F\u0BAE\u0BCD",
441
526
  opensInNewTab: "\u0BAA\u0BC1\u0BA4\u0BBF\u0BAF \u0BA4\u0BBE\u0BB5\u0BB2\u0BBF\u0BB2\u0BCD \u0BA4\u0BBF\u0BB1\u0B95\u0BCD\u0B95\u0BC1\u0BAE\u0BCD",
527
+ conceptStripTitle: "\u0B85\u0BB2\u0BCD\u0BB2\u0BA4\u0BC1 \u0B92\u0BB0\u0BC1 \u0B95\u0BB0\u0BC1\u0BA4\u0BCD\u0BA4\u0BBF\u0BB2\u0BBF\u0BB0\u0BC1\u0BA8\u0BCD\u0BA4\u0BC1 \u0BA4\u0BCA\u0B9F\u0B99\u0BCD\u0B95\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
528
+ conceptStripLabel: "\u0BA8\u0BC0\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0B95\u0BC7\u0B9F\u0BCD\u0B95\u0B95\u0BCD\u0B95\u0BC2\u0B9F\u0BBF\u0BAF \u0B95\u0BB0\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1\u0B95\u0BB3\u0BCD",
529
+ conceptAsk: "{product} \u0B87\u0BB2\u0BCD {title} \u0BAA\u0BB1\u0BCD\u0BB1\u0BBF\u0B95\u0BCD \u0B95\u0BC7\u0BB3\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
530
+ conceptAskQuestion: "{product} \u0B87\u0BB2\u0BCD {title} \u0B8E\u0BAA\u0BCD\u0BAA\u0B9F\u0BBF \u0BB5\u0BC7\u0BB2\u0BC8 \u0B9A\u0BC6\u0BAF\u0BCD\u0B95\u0BBF\u0BB1\u0BA4\u0BC1?",
531
+ copyAnswer: "\u0BAA\u0BA4\u0BBF\u0BB2\u0BC8 \u0BA8\u0B95\u0BB2\u0BC6\u0B9F\u0BC1",
532
+ copyFailed: "\u0BA8\u0B95\u0BB2\u0BC6\u0B9F\u0BC1\u0B95\u0BCD\u0B95 \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8",
533
+ readAloud: "\u0B9A\u0BA4\u0BCD\u0BA4\u0BAE\u0BBE\u0B95\u0BAA\u0BCD \u0BAA\u0B9F\u0BBF",
534
+ stopReading: "\u0BAA\u0B9F\u0BBF\u0BAA\u0BCD\u0BAA\u0BA4\u0BC8 \u0BA8\u0BBF\u0BB1\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1",
535
+ answerTable: "\u0B85\u0B9F\u0BCD\u0B9F\u0BB5\u0BA3\u0BC8",
536
+ groundedIn: "\u0B87\u0BA8\u0BCD\u0BA4\u0BAA\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BCD \u0B8E\u0BA4\u0BC8 \u0B85\u0B9F\u0BBF\u0BAA\u0BCD\u0BAA\u0B9F\u0BC8\u0BAF\u0BBE\u0B95\u0B95\u0BCD \u0B95\u0BCA\u0BA3\u0BCD\u0B9F\u0BA4\u0BC1",
537
+ sourceCountOne: "1 \u0BAE\u0BC2\u0BB2\u0BAE\u0BCD",
538
+ sourceCountMany: "{count} \u0BAE\u0BC2\u0BB2\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
539
+ sourceKindConcept: "\u0B95\u0BB0\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1",
540
+ sourceKindDocs: "\u0B86\u0BB5\u0BA3\u0BAE\u0BCD",
541
+ sourceKindPricing: "\u0BB5\u0BBF\u0BB2\u0BC8",
542
+ sourceKindUseCase: "\u0BAA\u0BAF\u0BA9\u0BCD\u0BAA\u0BBE\u0B9F\u0BCD\u0B9F\u0BC1 \u0BA8\u0BBF\u0BB2\u0BC8",
543
+ sourceKindPage: "\u0BA4\u0BAF\u0BBE\u0BB0\u0BBF\u0BAA\u0BCD\u0BAA\u0BC1\u0BAA\u0BCD \u0BAA\u0B95\u0BCD\u0B95\u0BAE\u0BCD",
442
544
  seoTitle: "\u0B9A\u0BBF\u0B95\u0BCD\u0B95\u0BB2\u0BC8 \u0BB5\u0BBF\u0BB5\u0BB0\u0BBF\u0BAF\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD, \u0BAA\u0BA4\u0BBF\u0BB2\u0BCD \u0BAA\u0BC6\u0BB1\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
443
545
  seoDescription: "\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 \u0B89\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0B9A\u0BCA\u0BA8\u0BCD\u0BA4 \u0BB5\u0BBE\u0BB0\u0BCD\u0BA4\u0BCD\u0BA4\u0BC8\u0B95\u0BB3\u0BBF\u0BB2\u0BCD \u0BB5\u0BBF\u0BB5\u0BB0\u0BBF\u0BAF\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD; \u0B8E\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0BA4\u0BAF\u0BBE\u0BB0\u0BBF\u0BAA\u0BCD\u0BAA\u0BC1 \u0B86\u0BB5\u0BA3\u0B99\u0BCD\u0B95\u0BB3\u0BBF\u0BB2\u0BBF\u0BB0\u0BC1\u0BA8\u0BCD\u0BA4\u0BC1 \u0BAA\u0BA4\u0BBF\u0BB2\u0BCD \u0B95\u0BBF\u0B9F\u0BC8\u0B95\u0BCD\u0B95\u0BC1\u0BAE\u0BCD, \u0B85\u0BA4\u0BC1 \u0BB5\u0BA8\u0BCD\u0BA4 \u0BAA\u0B95\u0BCD\u0B95\u0B99\u0BCD\u0B95\u0BB3\u0BC1\u0B95\u0BCD\u0B95\u0BBE\u0BA9 \u0B87\u0BA3\u0BC8\u0BAA\u0BCD\u0BAA\u0BC1\u0B95\u0BB3\u0BC1\u0B9F\u0BA9\u0BCD.",
444
546
  seoDescriptionProduct: "{product} \u0BAE\u0BB1\u0BCD\u0BB1\u0BC1\u0BAE\u0BCD \u0B8E\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0BAA\u0BBF\u0BB1 \u0BA4\u0BAF\u0BBE\u0BB0\u0BBF\u0BAA\u0BCD\u0BAA\u0BC1\u0B95\u0BB3\u0BBF\u0BA9\u0BCD \u0B86\u0BB5\u0BA3\u0B99\u0BCD\u0B95\u0BB3\u0BBF\u0BB2\u0BBF\u0BB0\u0BC1\u0BA8\u0BCD\u0BA4\u0BC1 \u0BAA\u0BA4\u0BBF\u0BB2\u0BCD \u0BAA\u0BC6\u0BB1, \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 \u0B89\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0B9A\u0BCA\u0BA8\u0BCD\u0BA4 \u0BB5\u0BBE\u0BB0\u0BCD\u0BA4\u0BCD\u0BA4\u0BC8\u0B95\u0BB3\u0BBF\u0BB2\u0BCD \u0BB5\u0BBF\u0BB5\u0BB0\u0BBF\u0BAF\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u2014 \u0B85\u0BA4\u0BC1 \u0BB5\u0BA8\u0BCD\u0BA4 \u0BAA\u0B95\u0BCD\u0B95\u0B99\u0BCD\u0B95\u0BB3\u0BC1\u0B95\u0BCD\u0B95\u0BBE\u0BA9 \u0B87\u0BA3\u0BC8\u0BAA\u0BCD\u0BAA\u0BC1\u0B95\u0BB3\u0BC1\u0B9F\u0BA9\u0BCD.",
@@ -462,6 +564,17 @@ var STRINGS = {
462
564
  ar: AR,
463
565
  ta: TA
464
566
  };
567
+ var SPEECH_TAGS = {
568
+ en: "en-US",
569
+ zh: "zh-CN",
570
+ hi: "hi-IN",
571
+ es: "es-ES",
572
+ ar: "ar-SA",
573
+ ta: "ta-IN"
574
+ };
575
+ function speechLocaleOf(locale) {
576
+ return SPEECH_TAGS[locale];
577
+ }
465
578
  function resolveExploreLocale(explicit) {
466
579
  const candidates = [
467
580
  explicit,
@@ -540,11 +653,9 @@ var EXPLORE_OWN_MESSAGE_FIELDS = (
540
653
  }
541
654
  `
542
655
  );
543
- var EXPLORE_CATALOG_QUERY = (
656
+ var EXPLORE_CATALOG_ESTABLISHED_FIELDS = (
544
657
  /* GraphQL */
545
658
  `
546
- query ExploreCatalog($productSlug: String) {
547
- exploreCatalog(productSlug: $productSlug) {
548
659
  enabled
549
660
  products {
550
661
  slug
@@ -562,11 +673,35 @@ var EXPLORE_CATALOG_QUERY = (
562
673
  historyTurns
563
674
  }
564
675
  captchaRequired
565
- recaptchaSiteKey
676
+ recaptchaSiteKey`
677
+ );
678
+ var EXPLORE_CATALOG_HIGHLIGHT_FIELD = (
679
+ /* GraphQL */
680
+ `
681
+ conceptHighlights {
682
+ product
683
+ productName
684
+ title
685
+ url
686
+ imageUrl
687
+ alt
688
+ }`
689
+ );
690
+ function exploreCatalogDocument(extraFields) {
691
+ return (
692
+ /* GraphQL */
693
+ `
694
+ query ExploreCatalog($productSlug: String) {
695
+ exploreCatalog(productSlug: $productSlug) {${EXPLORE_CATALOG_ESTABLISHED_FIELDS}${extraFields}
566
696
  }
567
697
  }
568
698
  `
699
+ );
700
+ }
701
+ var EXPLORE_CATALOG_QUERY = exploreCatalogDocument(
702
+ EXPLORE_CATALOG_HIGHLIGHT_FIELD
569
703
  );
704
+ var EXPLORE_CATALOG_QUERY_WITHOUT_HIGHLIGHTS = exploreCatalogDocument("");
570
705
  var EXPLORE_MESSAGE_QUERY = (
571
706
  /* GraphQL */
572
707
  `
@@ -1369,7 +1504,12 @@ function buildFallbackCatalog(examplePrompts, welcome) {
1369
1504
  welcomeBody: welcome?.body ?? FALLBACK_WELCOME_BODY,
1370
1505
  limits: FALLBACK_EXPLORE_LIMITS,
1371
1506
  captchaRequired: false,
1372
- recaptchaSiteKey: null
1507
+ recaptchaSiteKey: null,
1508
+ // Empty, and deliberately not synthesised from `ECOSYSTEM_PRODUCTS`: a concept highlight
1509
+ // needs a real published illustration URL, and this SDK has no registry of them. An
1510
+ // invented path would render 38 broken frames on the one screen every prerender captures.
1511
+ // The strip renders nothing at all when the list is empty, which is the honest fallback.
1512
+ conceptHighlights: []
1373
1513
  };
1374
1514
  }
1375
1515
  function normalizeCatalog(raw, fallback) {
@@ -1386,9 +1526,27 @@ function normalizeCatalog(raw, fallback) {
1386
1526
  historyTurns: raw.limits?.historyTurns || fallback.limits.historyTurns
1387
1527
  },
1388
1528
  captchaRequired: raw.captchaRequired === true,
1389
- recaptchaSiteKey: raw.recaptchaSiteKey ?? null
1529
+ recaptchaSiteKey: raw.recaptchaSiteKey ?? null,
1530
+ // `[ExploreConceptHighlight!]!` on the wire, so a gateway that KNOWS the field always
1531
+ // sends an array. The one response that arrives without it is the retry against an older
1532
+ // supergraph (`EXPLORE_CATALOG_QUERY_WITHOUT_HIGHLIGHTS`) — that gateway does not answer
1533
+ // the full document at all, it refuses it. Either way
1534
+ // `catalog.conceptHighlights.filter(...)` on `undefined` would throw inside render,
1535
+ // blanking the welcome screen over a decorative strip.
1536
+ conceptHighlights: raw.conceptHighlights?.length ? raw.conceptHighlights : fallback.conceptHighlights
1390
1537
  };
1391
1538
  }
1539
+ var CATALOG_DOCUMENT_REJECTED_CODES = /* @__PURE__ */ new Set([
1540
+ "GRAPHQL_VALIDATION_FAILED",
1541
+ "HTTP_400"
1542
+ ]);
1543
+ function catalogRejectedTheHighlightField(res) {
1544
+ if (res.data?.exploreCatalog) return false;
1545
+ return (res.errors ?? []).some((error) => {
1546
+ const code = error.extensions?.code;
1547
+ return typeof code === "string" && CATALOG_DOCUMENT_REJECTED_CODES.has(code) || error.message.includes("conceptHighlights");
1548
+ });
1549
+ }
1392
1550
  var RATE_LIMIT_CODES = /* @__PURE__ */ new Set([
1393
1551
  "RATE_LIMITED",
1394
1552
  "HTTP_429",
@@ -1728,11 +1886,19 @@ function useExploreChat(options = {}) {
1728
1886
  React.useEffect(() => {
1729
1887
  let cancelled = false;
1730
1888
  const load = async () => {
1731
- const res = await client.query(
1889
+ const variables = { productSlug: productSlug ?? null };
1890
+ let res = await client.query(
1732
1891
  EXPLORE_CATALOG_QUERY,
1733
- { productSlug: productSlug ?? null }
1892
+ variables
1734
1893
  );
1735
1894
  if (cancelled || !mountedRef.current) return;
1895
+ if (catalogRejectedTheHighlightField(res)) {
1896
+ res = await client.query(
1897
+ EXPLORE_CATALOG_QUERY_WITHOUT_HIGHLIGHTS,
1898
+ variables
1899
+ );
1900
+ if (cancelled || !mountedRef.current) return;
1901
+ }
1736
1902
  const raw = res.data?.exploreCatalog;
1737
1903
  if (res.errors?.length || !raw) {
1738
1904
  console.warn(
@@ -2178,6 +2344,224 @@ function makeOptimisticId() {
2178
2344
  optimisticCounter += 1;
2179
2345
  return `boff-explore-local-${Date.now()}-${optimisticCounter}`;
2180
2346
  }
2347
+ var DRAIN_TARGET_MS = 1200;
2348
+ var MIN_CHARS_PER_SECOND = 90;
2349
+ var MAX_CHARS_PER_SECOND = 2200;
2350
+ var MAX_STEP_MS = 250;
2351
+ var COMMIT_INTERVAL_MS = 45;
2352
+ var FALLBACK_STEP_MS = 50;
2353
+ var FENCE = /^ {0,3}(?:```|~~~)/;
2354
+ var TABLE_ROW = /^ {0,3}\|/;
2355
+ var EMPTY_BLOCK_MARKER = /^ {0,3}(?:[-+>]|#{1,6}|\d{1,9}[.)])\s*$/;
2356
+ function isWordCharacter(char) {
2357
+ return char !== void 0 && /[\p{L}\p{N}]/u.test(char);
2358
+ }
2359
+ function clamp(value, min, max) {
2360
+ return Math.min(Math.max(value, min), max);
2361
+ }
2362
+ function now() {
2363
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
2364
+ }
2365
+ function isHighSurrogate(code) {
2366
+ return code >= 55296 && code <= 56319;
2367
+ }
2368
+ function safeInlineEnd(line) {
2369
+ let codeSpan = null;
2370
+ let link = null;
2371
+ const emphasis = [];
2372
+ let at = 0;
2373
+ while (at < line.length) {
2374
+ const char = line[at] ?? "";
2375
+ if (char === "\\") {
2376
+ at += 2;
2377
+ continue;
2378
+ }
2379
+ if (char === "`") {
2380
+ let run = 1;
2381
+ while (line[at + run] === "`") run += 1;
2382
+ codeSpan = codeSpan === null ? at : null;
2383
+ at += run;
2384
+ continue;
2385
+ }
2386
+ if (codeSpan !== null) {
2387
+ at += 1;
2388
+ continue;
2389
+ }
2390
+ if (char === "[") {
2391
+ if (link === null) link = line[at - 1] === "!" ? at - 1 : at;
2392
+ at += 1;
2393
+ continue;
2394
+ }
2395
+ if (char === "]") {
2396
+ if (link !== null && line[at + 1] !== "(") link = null;
2397
+ at += 1;
2398
+ continue;
2399
+ }
2400
+ if (char === ")") {
2401
+ link = null;
2402
+ at += 1;
2403
+ continue;
2404
+ }
2405
+ if (char === "*" || char === "_" || char === "~") {
2406
+ let run = 1;
2407
+ while (line[at + run] === char) run += 1;
2408
+ const start = at;
2409
+ at += run;
2410
+ const before = line[start - 1];
2411
+ const after = line[start + run];
2412
+ if (run < 2) continue;
2413
+ if (char === "_" && isWordCharacter(before) && isWordCharacter(after)) {
2414
+ continue;
2415
+ }
2416
+ const marker = char.repeat(run);
2417
+ const open2 = emphasis.findIndex((entry) => entry.marker === marker);
2418
+ if (open2 !== -1) {
2419
+ emphasis.splice(open2, 1);
2420
+ continue;
2421
+ }
2422
+ if (after !== void 0 && /\s/.test(after)) continue;
2423
+ emphasis.push({ marker, start });
2424
+ continue;
2425
+ }
2426
+ at += 1;
2427
+ }
2428
+ const open = [codeSpan, link, emphasis[0]?.start ?? null].filter(
2429
+ (value) => value !== null
2430
+ );
2431
+ return open.length === 0 ? line.length : Math.min(...open);
2432
+ }
2433
+ function revealBoundary(text, limit) {
2434
+ if (limit <= 0) return 0;
2435
+ const caughtUp = limit >= text.length;
2436
+ const head = caughtUp ? text : text.slice(0, limit);
2437
+ const lineStart = head.lastIndexOf("\n") + 1;
2438
+ const line = head.slice(lineStart);
2439
+ let fenceStart = null;
2440
+ let tableStart = null;
2441
+ let tableRows = 0;
2442
+ let at = 0;
2443
+ while (at < lineStart) {
2444
+ const breakAt = head.indexOf("\n", at);
2445
+ const end = breakAt === -1 ? lineStart : breakAt;
2446
+ const row = head.slice(at, end);
2447
+ if (fenceStart !== null) {
2448
+ if (FENCE.test(row)) fenceStart = null;
2449
+ } else if (FENCE.test(row)) {
2450
+ fenceStart = at;
2451
+ } else if (TABLE_ROW.test(row)) {
2452
+ if (tableStart === null) {
2453
+ tableStart = at;
2454
+ tableRows = 0;
2455
+ }
2456
+ tableRows += 1;
2457
+ } else if (tableStart !== null) {
2458
+ tableStart = null;
2459
+ tableRows = 0;
2460
+ }
2461
+ at = end + 1;
2462
+ }
2463
+ if (fenceStart === null) {
2464
+ const lastIsRow = TABLE_ROW.test(line);
2465
+ if (lastIsRow && tableStart === null) {
2466
+ tableStart = lineStart;
2467
+ tableRows = 0;
2468
+ } else if (!lastIsRow && line !== "") {
2469
+ tableStart = null;
2470
+ tableRows = 0;
2471
+ }
2472
+ if (tableStart !== null && tableRows < 2) return tableStart;
2473
+ }
2474
+ if (caughtUp) return text.length;
2475
+ if (fenceStart !== null) return fenceStart;
2476
+ if (TABLE_ROW.test(line)) return lineStart;
2477
+ if (EMPTY_BLOCK_MARKER.test(line)) return lineStart;
2478
+ const cut = lineStart + safeInlineEnd(line);
2479
+ return isHighSurrogate(text.charCodeAt(cut - 1)) ? cut - 1 : cut;
2480
+ }
2481
+ function prefersReducedMotion() {
2482
+ if (typeof window === "undefined") return true;
2483
+ try {
2484
+ return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
2485
+ } catch {
2486
+ return false;
2487
+ }
2488
+ }
2489
+ function useProgressiveReveal(text, settled) {
2490
+ const [, setFrame] = React.useState(0);
2491
+ const [reduced] = React.useState(prefersReducedMotion);
2492
+ const immediate = settled || reduced;
2493
+ const textRef = React.useRef(text);
2494
+ const cursorRef = React.useRef(immediate ? text.length : 0);
2495
+ const cutRef = React.useRef(immediate ? text.length : 0);
2496
+ const committedRef = React.useRef(0);
2497
+ if (text !== textRef.current) {
2498
+ const revealed = textRef.current.slice(0, Math.floor(cursorRef.current));
2499
+ if (revealed !== "" && !text.startsWith(revealed)) {
2500
+ cursorRef.current = text.length;
2501
+ }
2502
+ cursorRef.current = Math.min(cursorRef.current, text.length);
2503
+ cutRef.current = Math.min(cutRef.current, text.length);
2504
+ textRef.current = text;
2505
+ }
2506
+ if (immediate) cursorRef.current = text.length;
2507
+ const cut = immediate ? text.length : Math.max(
2508
+ cutRef.current,
2509
+ revealBoundary(text, Math.floor(cursorRef.current))
2510
+ );
2511
+ cutRef.current = cut;
2512
+ React.useEffect(() => {
2513
+ if (immediate) return;
2514
+ if (cursorRef.current >= text.length) return;
2515
+ let cancelled = false;
2516
+ let cancelStep = null;
2517
+ let previous = now();
2518
+ const commit = (at, force) => {
2519
+ if (!force && at - committedRef.current < COMMIT_INTERVAL_MS) return;
2520
+ committedRef.current = at;
2521
+ setFrame((value) => value + 1);
2522
+ };
2523
+ const step = () => {
2524
+ cancelStep = null;
2525
+ if (cancelled) return;
2526
+ const at = now();
2527
+ const elapsed = clamp(at - previous, 0, MAX_STEP_MS);
2528
+ previous = at;
2529
+ const length = textRef.current.length;
2530
+ const pending = length - cursorRef.current;
2531
+ if (pending <= 0) {
2532
+ commit(at, true);
2533
+ return;
2534
+ }
2535
+ const perSecond = clamp(
2536
+ pending / (DRAIN_TARGET_MS / 1e3),
2537
+ MIN_CHARS_PER_SECOND,
2538
+ MAX_CHARS_PER_SECOND
2539
+ );
2540
+ cursorRef.current = Math.min(
2541
+ length,
2542
+ cursorRef.current + perSecond * elapsed / 1e3
2543
+ );
2544
+ const done = cursorRef.current >= length;
2545
+ commit(at, done);
2546
+ if (!done) schedule();
2547
+ };
2548
+ const schedule = () => {
2549
+ if (typeof window.requestAnimationFrame === "function") {
2550
+ const id2 = window.requestAnimationFrame(step);
2551
+ cancelStep = () => window.cancelAnimationFrame(id2);
2552
+ return;
2553
+ }
2554
+ const id = setTimeout(step, FALLBACK_STEP_MS);
2555
+ cancelStep = () => clearTimeout(id);
2556
+ };
2557
+ schedule();
2558
+ return () => {
2559
+ cancelled = true;
2560
+ cancelStep?.();
2561
+ };
2562
+ }, [text, immediate]);
2563
+ return text.slice(0, cut);
2564
+ }
2181
2565
  function getRecognitionCtor() {
2182
2566
  if (typeof window === "undefined") return null;
2183
2567
  const w = window;
@@ -2396,6 +2780,141 @@ function useSpeechInput({
2396
2780
  );
2397
2781
  return { supported, listening, interim, error, start, stop, toggle };
2398
2782
  }
2783
+ var SILENT_FAILURE_MS = 1500;
2784
+ function speechPlatform() {
2785
+ if (typeof window === "undefined") return null;
2786
+ const scope = window;
2787
+ const synth = scope.speechSynthesis;
2788
+ const Utterance = scope.SpeechSynthesisUtterance;
2789
+ if (!synth || typeof synth.speak !== "function") return null;
2790
+ if (typeof Utterance !== "function") return null;
2791
+ return { synth, Utterance };
2792
+ }
2793
+ function normaliseTag(tag) {
2794
+ return tag.trim().toLowerCase().replace(/_/g, "-");
2795
+ }
2796
+ function primaryOf(tag) {
2797
+ return normaliseTag(tag).split("-")[0] ?? "";
2798
+ }
2799
+ function pickVoice(voices, lang) {
2800
+ const wanted = normaliseTag(lang);
2801
+ if (wanted === "") return null;
2802
+ const primary = primaryOf(wanted);
2803
+ const exact = voices.filter((voice) => normaliseTag(voice.lang) === wanted);
2804
+ const related = voices.filter((voice) => primaryOf(voice.lang) === primary);
2805
+ return exact.find((voice) => voice.default) ?? exact[0] ?? related.find((voice) => voice.default) ?? related[0] ?? null;
2806
+ }
2807
+ var activeOwner = null;
2808
+ var activeStop = null;
2809
+ function stopSpeaking() {
2810
+ activeStop?.();
2811
+ }
2812
+ function useSpeechOutput({
2813
+ lang
2814
+ } = {}) {
2815
+ const [available, setAvailable] = React.useState(() => speechPlatform() !== null);
2816
+ const [speaking, setSpeaking] = React.useState(false);
2817
+ const utteranceRef = React.useRef(null);
2818
+ const watchdogRef = React.useRef(null);
2819
+ const ownerRef = React.useRef({});
2820
+ const stopRef = React.useRef(() => void 0);
2821
+ const langRef = React.useRef(lang);
2822
+ langRef.current = lang;
2823
+ const clearWatchdog = React.useCallback(() => {
2824
+ if (watchdogRef.current === null) return;
2825
+ clearTimeout(watchdogRef.current);
2826
+ watchdogRef.current = null;
2827
+ }, []);
2828
+ const settle = React.useCallback(() => {
2829
+ clearWatchdog();
2830
+ utteranceRef.current = null;
2831
+ if (activeOwner === ownerRef.current) {
2832
+ activeOwner = null;
2833
+ activeStop = null;
2834
+ }
2835
+ setSpeaking(false);
2836
+ }, [clearWatchdog]);
2837
+ const stop = React.useCallback(() => {
2838
+ const owned = utteranceRef.current !== null;
2839
+ settle();
2840
+ if (owned) speechPlatform()?.synth.cancel();
2841
+ }, [settle]);
2842
+ stopRef.current = stop;
2843
+ const speak = React.useCallback(
2844
+ (text) => {
2845
+ const platform = speechPlatform();
2846
+ if (!platform) {
2847
+ setAvailable(false);
2848
+ return;
2849
+ }
2850
+ const spoken = text.trim();
2851
+ if (spoken === "") return;
2852
+ if (activeStop && activeOwner !== ownerRef.current) activeStop();
2853
+ platform.synth.cancel();
2854
+ const utterance = new platform.Utterance(spoken);
2855
+ const wanted = langRef.current;
2856
+ if (wanted !== void 0 && wanted !== "") {
2857
+ const voice = pickVoice(platform.synth.getVoices(), wanted);
2858
+ utterance.lang = voice?.lang ?? wanted;
2859
+ if (voice) utterance.voice = voice;
2860
+ }
2861
+ const mine = () => utteranceRef.current === utterance;
2862
+ utterance.onstart = () => {
2863
+ if (!mine()) return;
2864
+ clearWatchdog();
2865
+ setSpeaking(true);
2866
+ };
2867
+ utterance.onend = () => {
2868
+ if (!mine()) return;
2869
+ settle();
2870
+ };
2871
+ utterance.onerror = (event) => {
2872
+ if (!mine()) return;
2873
+ const code = typeof event?.error === "string" ? event.error : "";
2874
+ settle();
2875
+ if (code !== "interrupted" && code !== "canceled") setAvailable(false);
2876
+ };
2877
+ utteranceRef.current = utterance;
2878
+ activeOwner = ownerRef.current;
2879
+ activeStop = () => {
2880
+ stopRef.current();
2881
+ };
2882
+ setSpeaking(true);
2883
+ platform.synth.speak(utterance);
2884
+ clearWatchdog();
2885
+ watchdogRef.current = setTimeout(() => {
2886
+ watchdogRef.current = null;
2887
+ if (!mine()) return;
2888
+ if (platform.synth.speaking) return;
2889
+ settle();
2890
+ setAvailable(false);
2891
+ }, SILENT_FAILURE_MS);
2892
+ },
2893
+ [clearWatchdog, settle]
2894
+ );
2895
+ const toggle = React.useCallback(
2896
+ (text) => {
2897
+ if (speaking) stop();
2898
+ else speak(text);
2899
+ },
2900
+ [speak, speaking, stop]
2901
+ );
2902
+ React.useEffect(() => {
2903
+ if (!speaking || typeof window === "undefined") return;
2904
+ const onHide = () => {
2905
+ stopRef.current();
2906
+ };
2907
+ window.addEventListener("pagehide", onHide);
2908
+ return () => window.removeEventListener("pagehide", onHide);
2909
+ }, [speaking]);
2910
+ React.useEffect(
2911
+ () => () => {
2912
+ stopRef.current();
2913
+ },
2914
+ []
2915
+ );
2916
+ return { supported: available, speaking, speak, stop, toggle };
2917
+ }
2399
2918
  var SWIPE_THRESHOLD = 40;
2400
2919
  function useSwipe({
2401
2920
  onSwipeLeft,
@@ -2791,6 +3310,115 @@ var EXPLORE_CSS = `
2791
3310
  height: auto;
2792
3311
  }
2793
3312
 
3313
+ /* ---- Comparison and pricing tables ----------------------------------------------------
3314
+ The harness emits a comparison, and a set of plans, as a GFM table whose FIRST column is
3315
+ the subject \u2014 the product, or the plan (CONTRACT.md \xA77). Two things have to be true of it:
3316
+ the page must not scroll sideways because of it, and it has to be readable on a 320px
3317
+ phone, which a four-column grid of 1px borders is not.
3318
+
3319
+ Wide: a contained scroller. \`max-width: 100%\` keeps the box inside the bubble and
3320
+ \`overflow-x: auto\` keeps the columns inside the box, so the widest possible table still
3321
+ cannot widen the document. \`overscroll-behavior-x: contain\` stops a swipe that runs out
3322
+ of table from chaining to the page \u2014 the same rule the transcript needed. */
3323
+ .boff-explore-body .boff-explore-table {
3324
+ max-width: 100%;
3325
+ overflow-x: auto;
3326
+ overscroll-behavior-x: contain;
3327
+ margin: 0.75rem 0;
3328
+ border: 1px solid hsl(var(--border));
3329
+ border-radius: 0.75rem;
3330
+ background-color: hsl(var(--card));
3331
+ }
3332
+ .boff-explore-body .boff-explore-table:focus-visible {
3333
+ outline: 2px solid hsl(var(--ring));
3334
+ outline-offset: 2px;
3335
+ }
3336
+ .boff-explore-body .boff-explore-table table {
3337
+ /* Beats the blanket \`display: block\` above, which is the unwrapped fallback. */
3338
+ display: table;
3339
+ width: 100%;
3340
+ max-width: none;
3341
+ margin: 0;
3342
+ border-collapse: collapse;
3343
+ font-size: 0.8125rem;
3344
+ }
3345
+ .boff-explore-body .boff-explore-table th,
3346
+ .boff-explore-body .boff-explore-table td {
3347
+ border: 0;
3348
+ border-top: 1px solid hsl(var(--border));
3349
+ padding: 0.5rem 0.75rem;
3350
+ /* \`start\`, never \`left\`: the page root carries dir="rtl" in Arabic, and a left-aligned
3351
+ table there reads against the grain of every other line on the page. */
3352
+ text-align: start;
3353
+ vertical-align: top;
3354
+ overflow-wrap: anywhere;
3355
+ /* Prices in a column line up under each other instead of drifting by digit width. */
3356
+ font-variant-numeric: tabular-nums;
3357
+ min-width: 6rem;
3358
+ }
3359
+ .boff-explore-body .boff-explore-table thead th {
3360
+ border-top: 0;
3361
+ background-color: hsl(var(--muted));
3362
+ color: hsl(var(--foreground));
3363
+ font-weight: 600;
3364
+ white-space: nowrap;
3365
+ }
3366
+ /* The first column is what the row is ABOUT \u2014 the product, or the plan name. Reading it as
3367
+ the row's heading is what makes a price list scannable rather than a grid of strings. */
3368
+ .boff-explore-body .boff-explore-table tbody td:first-child {
3369
+ font-weight: 600;
3370
+ color: hsl(var(--card-foreground));
3371
+ }
3372
+
3373
+ /* Narrow: one card per row. Each cell prints its own column heading, so a value is never
3374
+ stranded without the question it answers, and the subject cell becomes the card's title.
3375
+ A media query rather than a measured width \u2014 these pages are serialised post-hydration,
3376
+ so anything measured here would ship to every phone as static HTML. */
3377
+ @media (max-width: 30rem) {
3378
+ .boff-explore-body .boff-explore-table {
3379
+ overflow-x: visible;
3380
+ }
3381
+ .boff-explore-body .boff-explore-table table,
3382
+ .boff-explore-body .boff-explore-table tbody,
3383
+ .boff-explore-body .boff-explore-table tr,
3384
+ .boff-explore-body .boff-explore-table td {
3385
+ display: block;
3386
+ width: 100%;
3387
+ min-width: 0;
3388
+ }
3389
+ .boff-explore-body .boff-explore-table thead {
3390
+ display: none;
3391
+ }
3392
+ .boff-explore-body .boff-explore-table tr {
3393
+ border-top: 1px solid hsl(var(--border));
3394
+ padding: 0.5rem 0;
3395
+ }
3396
+ .boff-explore-body .boff-explore-table tr:first-child {
3397
+ border-top: 0;
3398
+ }
3399
+ .boff-explore-body .boff-explore-table td {
3400
+ border: 0;
3401
+ padding: 0.125rem 0.75rem;
3402
+ }
3403
+ .boff-explore-body .boff-explore-table td[data-label]::before {
3404
+ content: attr(data-label);
3405
+ display: block;
3406
+ font-size: 0.6875rem;
3407
+ font-weight: 600;
3408
+ text-transform: uppercase;
3409
+ letter-spacing: 0.04em;
3410
+ color: hsl(var(--muted-foreground));
3411
+ }
3412
+ .boff-explore-body .boff-explore-table tbody td:first-child {
3413
+ font-size: 0.9375rem;
3414
+ padding-bottom: 0.25rem;
3415
+ }
3416
+ .boff-explore-body .boff-explore-table tbody td:first-child::before {
3417
+ /* The plan's own name is the heading here; repeating "Plan" over it says nothing. */
3418
+ display: none;
3419
+ }
3420
+ }
3421
+
2794
3422
  /* ---- Ambient backdrop -----------------------------------------------------------------
2795
3423
  Two very low-opacity radial washes behind the welcome state, drifting slowly. Painted on
2796
3424
  a pseudo-element with pointer-events:none so it can never intercept a click \u2014 a
@@ -2962,7 +3590,7 @@ function isOwnElement(el, root) {
2962
3590
  function visualViewportHeight() {
2963
3591
  return window.visualViewport?.height ?? window.innerHeight;
2964
3592
  }
2965
- function clamp(value, min, max) {
3593
+ function clamp2(value, min, max) {
2966
3594
  return Math.min(Math.max(value, min), max);
2967
3595
  }
2968
3596
  function isRestoredNavigation() {
@@ -3027,14 +3655,196 @@ function isInlineImageAllowed(src, origins) {
3027
3655
  if (url.protocol !== "https:" || url.username || url.password) return false;
3028
3656
  return origins.has(url.origin);
3029
3657
  }
3030
- var InlineImageScopeContext = React.createContext(null);
3658
+ var REFERENCE_KIND_ORDER = [
3659
+ "CONCEPT",
3660
+ "DOCS",
3661
+ "PRICING",
3662
+ "USE_CASE",
3663
+ "PAGE"
3664
+ ];
3665
+ var PRODUCT_SUBDOMAINS = [
3666
+ "alphadocs.",
3667
+ "alphaapp.",
3668
+ "docs.",
3669
+ "alpha.",
3670
+ "app.",
3671
+ "www."
3672
+ ];
3673
+ function readCatalogUrl(raw) {
3674
+ let parsed;
3675
+ try {
3676
+ parsed = new URL(raw);
3677
+ } catch {
3678
+ return null;
3679
+ }
3680
+ if (parsed.protocol !== "https:") return null;
3681
+ const host = parsed.hostname.toLowerCase();
3682
+ const prefix = PRODUCT_SUBDOMAINS.find(
3683
+ (candidate) => host.startsWith(candidate)
3684
+ );
3685
+ const apex = prefix === void 0 ? host : host.slice(prefix.length);
3686
+ const port = parsed.port ? `:${parsed.port}` : "";
3687
+ return {
3688
+ origin: `https://${apex}${port}`,
3689
+ prefix,
3690
+ pathname: parsed.pathname
3691
+ };
3692
+ }
3693
+ function referenceKindOf(url, origins) {
3694
+ const parsed = readCatalogUrl(url);
3695
+ if (parsed === null || !origins.has(parsed.origin)) return null;
3696
+ if (parsed.prefix === "docs." || parsed.prefix === "alphadocs.")
3697
+ return "DOCS";
3698
+ const segment = parsed.pathname.toLowerCase().split("/").filter(Boolean)[0] ?? "";
3699
+ if (segment === "concept" || segment === "concepts") return "CONCEPT";
3700
+ if (segment === "docs") return "DOCS";
3701
+ if (segment === "pricing" || segment === "plans") return "PRICING";
3702
+ if (segment === "use-case" || segment === "use-cases" || segment === "solutions") {
3703
+ return "USE_CASE";
3704
+ }
3705
+ return "PAGE";
3706
+ }
3707
+ function productNamesByOrigin(products) {
3708
+ const names = /* @__PURE__ */ new Map();
3709
+ for (const product of products) {
3710
+ let url;
3711
+ try {
3712
+ url = new URL(product.website);
3713
+ } catch {
3714
+ continue;
3715
+ }
3716
+ if (url.protocol !== "https:") continue;
3717
+ const apex = url.hostname.replace(/^www\./, "");
3718
+ const port = url.port ? `:${url.port}` : "";
3719
+ const key = `https://${apex}${port}`;
3720
+ if (!names.has(key)) names.set(key, product.name);
3721
+ }
3722
+ return names;
3723
+ }
3724
+ function summariseReferences(references, origins, productNames) {
3725
+ const counts = /* @__PURE__ */ new Map();
3726
+ const products = [];
3727
+ for (const reference of references) {
3728
+ const kind = referenceKindOf(reference.url, origins);
3729
+ if (kind !== null) counts.set(kind, (counts.get(kind) ?? 0) + 1);
3730
+ const parsed = readCatalogUrl(reference.url);
3731
+ const name = (parsed === null ? void 0 : productNames.get(parsed.origin)) ?? (reference.product || null);
3732
+ if (name != null && !products.includes(name)) products.push(name);
3733
+ }
3734
+ return {
3735
+ total: references.length,
3736
+ kinds: REFERENCE_KIND_ORDER.filter((kind) => counts.has(kind)).map(
3737
+ (kind) => ({ kind, count: counts.get(kind) ?? 0 })
3738
+ ),
3739
+ products
3740
+ };
3741
+ }
3742
+ var CODE_FENCE = /^\s*(?:```|~~~)/;
3743
+ var TABLE_ROW_LINE = /^\s{0,3}\|/;
3744
+ var TABLE_DELIMITER_ROW = /^\s{0,3}\|[\s:|-]*-[\s:|-]*$/;
3745
+ function readableLine(raw, mode) {
3746
+ let line = raw;
3747
+ line = line.replace(/!\[[^\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g, "");
3748
+ line = mode === "speech" ? (
3749
+ // A voice reading "h-t-t-p-s colon slash slash…" mid-sentence is noise, and the
3750
+ // prompt asks for two to five links in every answer. The label IS the words.
3751
+ line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1")
3752
+ ) : line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1 ($2)");
3753
+ line = mode === "speech" ? line.replace(/<(https?:\/\/[^>\s]+)>/g, "") : line.replace(/<(https?:\/\/[^>\s]+)>/g, "$1");
3754
+ line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
3755
+ line = line.replace(/^\s{0,3}>\s?/, "");
3756
+ if (/^\s{0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/.test(line)) return "";
3757
+ line = line.replace(/`([^`]+)`/g, "$1");
3758
+ line = line.replace(/\*\*([^*]+)\*\*/g, "$1");
3759
+ line = line.replace(/__([^_]+)__/g, "$1");
3760
+ line = line.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, "$1$2");
3761
+ line = line.replace(/(^|[^_\w])_([^_\n]+)_(?![_\w])/g, "$1$2");
3762
+ return line.replace(/[ \t]+$/, "");
3763
+ }
3764
+ function tableCells(row) {
3765
+ const trimmed = row.trim();
3766
+ const cells = [];
3767
+ let cell = "";
3768
+ for (let at = 0; at < trimmed.length; at += 1) {
3769
+ const char = trimmed[at] ?? "";
3770
+ if (char === "\\") {
3771
+ cell += char + (trimmed[at + 1] ?? "");
3772
+ at += 1;
3773
+ continue;
3774
+ }
3775
+ if (char === "|") {
3776
+ cells.push(cell.trim());
3777
+ cell = "";
3778
+ continue;
3779
+ }
3780
+ cell += char;
3781
+ }
3782
+ cells.push(cell.trim());
3783
+ if (trimmed.startsWith("|") && cells[0] === "") cells.shift();
3784
+ if (trimmed.endsWith("|") && cells[cells.length - 1] === "") cells.pop();
3785
+ return cells;
3786
+ }
3787
+ function asSentence(text) {
3788
+ return /[.!?]$/.test(text) ? text : `${text}.`;
3789
+ }
3790
+ function spokenTable(rows) {
3791
+ if (rows.length < 2 || !TABLE_DELIMITER_ROW.test(rows[1] ?? "")) return rows;
3792
+ const labels = tableCells(rows[0] ?? "");
3793
+ const spoken = [];
3794
+ for (const row of rows.slice(2)) {
3795
+ const parts = [];
3796
+ tableCells(row).forEach((value, column) => {
3797
+ if (value === "") return;
3798
+ const label = column === 0 ? "" : labels[column] ?? "";
3799
+ parts.push(label === "" ? value : `${label}: ${value}`);
3800
+ });
3801
+ if (parts.length > 0) spoken.push(asSentence(parts.join(". ")));
3802
+ }
3803
+ return spoken;
3804
+ }
3805
+ function readableText(markdown, mode) {
3806
+ const lines = [];
3807
+ let inFence = false;
3808
+ let table = [];
3809
+ const flushTable = () => {
3810
+ if (table.length === 0) return;
3811
+ for (const line of spokenTable(table)) lines.push(readableLine(line, mode));
3812
+ table = [];
3813
+ };
3814
+ for (const raw of markdown.split("\n")) {
3815
+ if (CODE_FENCE.test(raw)) {
3816
+ flushTable();
3817
+ inFence = !inFence;
3818
+ continue;
3819
+ }
3820
+ if (inFence) {
3821
+ lines.push(raw);
3822
+ continue;
3823
+ }
3824
+ if (mode === "speech" && TABLE_ROW_LINE.test(raw)) {
3825
+ table.push(raw);
3826
+ continue;
3827
+ }
3828
+ flushTable();
3829
+ lines.push(readableLine(raw, mode));
3830
+ }
3831
+ flushTable();
3832
+ return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
3833
+ }
3834
+ function answerPlainText(markdown) {
3835
+ return readableText(markdown, "clipboard");
3836
+ }
3837
+ function answerSpeechText(markdown) {
3838
+ return readableText(markdown, "speech");
3839
+ }
3840
+ var AnswerScopeContext = React.createContext(null);
3031
3841
  var InsideLinkContext = React.createContext(false);
3032
3842
  function AnswerLink(props) {
3033
3843
  return /* @__PURE__ */ jsxRuntime.jsx(InsideLinkContext.Provider, { value: true, children: /* @__PURE__ */ jsxRuntime.jsx(MarkdownLink, { ...props }) });
3034
3844
  }
3035
3845
  var INLINE_IMAGE_FRAME = "relative inline-block max-w-full overflow-hidden rounded-xl border border-border bg-muted align-top shadow-sm";
3036
3846
  function InlineConceptImage({ src, alt }) {
3037
- const scope = React.useContext(InlineImageScopeContext);
3847
+ const scope = React.useContext(AnswerScopeContext);
3038
3848
  const insideLink = React.useContext(InsideLinkContext);
3039
3849
  if (!scope || !scope.enabled || !isInlineImageAllowed(src, scope.origins) || scope.failed.has(src)) {
3040
3850
  return null;
@@ -3082,9 +3892,70 @@ function InlineConceptImage({ src, alt }) {
3082
3892
  }
3083
3893
  ) });
3084
3894
  }
3895
+ function markdownText(node) {
3896
+ if (!node) return "";
3897
+ if (typeof node.value === "string") return node.value;
3898
+ return (node.children ?? []).map(markdownText).join("");
3899
+ }
3900
+ function hasHeaderCells(node) {
3901
+ return (node?.children ?? []).some((cell) => cell.tagName === "th");
3902
+ }
3903
+ function findHeaderRow(node) {
3904
+ for (const child of node?.children ?? []) {
3905
+ if (child.tagName === "tr" && hasHeaderCells(child)) return child;
3906
+ const nested = findHeaderRow(child);
3907
+ if (nested !== null) return nested;
3908
+ }
3909
+ return null;
3910
+ }
3911
+ function tableColumnLabels(node) {
3912
+ const row = findHeaderRow(node);
3913
+ if (row === null) return [];
3914
+ return (row.children ?? []).filter((cell) => cell.tagName === "th").map((cell) => markdownText(cell).trim());
3915
+ }
3916
+ var TableColumnsContext = React.createContext([]);
3917
+ function AnswerTable({ node, children }) {
3918
+ const scope = React.useContext(AnswerScopeContext);
3919
+ const table = /* @__PURE__ */ jsxRuntime.jsx(TableColumnsContext.Provider, { value: tableColumnLabels(node), children: /* @__PURE__ */ jsxRuntime.jsx("table", { children }) });
3920
+ if (!scope) return table;
3921
+ return /* @__PURE__ */ jsxRuntime.jsx(
3922
+ "div",
3923
+ {
3924
+ "data-boff-explore": "table",
3925
+ role: "region",
3926
+ "aria-label": scope.strings.answerTable,
3927
+ tabIndex: 0,
3928
+ className: "boff-explore-table",
3929
+ children: table
3930
+ }
3931
+ );
3932
+ }
3933
+ function AnswerTableRow({
3934
+ node,
3935
+ children,
3936
+ ...props
3937
+ }) {
3938
+ const labels = React.useContext(TableColumnsContext);
3939
+ if (labels.length === 0 || hasHeaderCells(node)) {
3940
+ return /* @__PURE__ */ jsxRuntime.jsx("tr", { ...props, children });
3941
+ }
3942
+ let column = 0;
3943
+ const cells = React.Children.map(children, (cell) => {
3944
+ if (!React.isValidElement(cell)) return cell;
3945
+ const label = labels[column] ?? "";
3946
+ column += 1;
3947
+ if (label === "") return cell;
3948
+ return React.cloneElement(cell, {
3949
+ "data-label": label
3950
+ });
3951
+ });
3952
+ return /* @__PURE__ */ jsxRuntime.jsx("tr", { ...props, children: cells });
3953
+ }
3085
3954
  var ANSWER_MARKDOWN_COMPONENTS = {
3086
3955
  a: AnswerLink,
3087
- img: InlineConceptImage
3956
+ img: InlineConceptImage,
3957
+ table: AnswerTable,
3958
+ tr: AnswerTableRow
3088
3959
  };
3089
3960
  function ReferenceCard({
3090
3961
  reference,
@@ -3424,6 +4295,148 @@ function AnswerFeedback({
3424
4295
  }
3425
4296
  );
3426
4297
  }
4298
+ 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";
4299
+ function ProvenanceChips({
4300
+ strings: T,
4301
+ provenance
4302
+ }) {
4303
+ if (provenance.total === 0) return null;
4304
+ const kindLabels = {
4305
+ CONCEPT: T.sourceKindConcept,
4306
+ DOCS: T.sourceKindDocs,
4307
+ PRICING: T.sourceKindPricing,
4308
+ USE_CASE: T.sourceKindUseCase,
4309
+ PAGE: T.sourceKindPage
4310
+ };
4311
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4312
+ "ul",
4313
+ {
4314
+ "data-boff-explore": "provenance",
4315
+ "aria-label": T.groundedIn,
4316
+ className: "flex flex-wrap items-center gap-1.5",
4317
+ children: [
4318
+ /* @__PURE__ */ jsxRuntime.jsxs("li", { "data-boff-explore": "provenance-count", className: PROVENANCE_CHIP, children: [
4319
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.BookOpenText, { className: "h-3 w-3 shrink-0", "aria-hidden": "true" }),
4320
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: provenance.total === 1 ? T.sourceCountOne : fill(T.sourceCountMany, { count: String(provenance.total) }) })
4321
+ ] }),
4322
+ provenance.kinds.map(({ kind, count }) => /* @__PURE__ */ jsxRuntime.jsx(
4323
+ "li",
4324
+ {
4325
+ "data-boff-explore": "provenance-kind",
4326
+ "data-kind": kind,
4327
+ "data-count": count,
4328
+ className: PROVENANCE_CHIP,
4329
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: count > 1 ? `${kindLabels[kind]} \xB7 ${count}` : kindLabels[kind] })
4330
+ },
4331
+ kind
4332
+ )),
4333
+ provenance.products.map((name) => /* @__PURE__ */ jsxRuntime.jsx(
4334
+ "li",
4335
+ {
4336
+ "data-boff-explore": "provenance-product",
4337
+ className: PROVENANCE_CHIP,
4338
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: name })
4339
+ },
4340
+ name
4341
+ ))
4342
+ ]
4343
+ }
4344
+ );
4345
+ }
4346
+ var COPY_FEEDBACK_MS = 2500;
4347
+ var ANSWER_ACTION_PILL = "inline-flex max-w-full items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring";
4348
+ var ANSWER_ACTION_IDLE = "border-border bg-card text-muted-foreground hover:border-primary/40 hover:bg-accent hover:text-foreground";
4349
+ var ANSWER_ACTION_ACTIVE = "border-primary/40 bg-accent text-primary";
4350
+ function ReadAloudButton({
4351
+ strings: T,
4352
+ text,
4353
+ lang
4354
+ }) {
4355
+ const speech = useSpeechOutput({ lang });
4356
+ if (!speech.supported) return null;
4357
+ const label = speech.speaking ? T.stopReading : T.readAloud;
4358
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4359
+ "button",
4360
+ {
4361
+ "data-boff-explore": "read-aloud",
4362
+ "data-speaking": speech.speaking ? "true" : "false",
4363
+ type: "button",
4364
+ title: label,
4365
+ "aria-pressed": speech.speaking,
4366
+ onClick: () => speech.toggle(text),
4367
+ className: cn(
4368
+ ANSWER_ACTION_PILL,
4369
+ speech.speaking ? ANSWER_ACTION_ACTIVE : ANSWER_ACTION_IDLE
4370
+ ),
4371
+ children: [
4372
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Volume2, { className: "h-3 w-3 shrink-0", "aria-hidden": "true" }),
4373
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: label })
4374
+ ]
4375
+ }
4376
+ );
4377
+ }
4378
+ function AnswerActions({
4379
+ strings: T,
4380
+ text,
4381
+ speechText,
4382
+ speechLang
4383
+ }) {
4384
+ const [outcome, setOutcome] = React.useState("idle");
4385
+ const resetRef = React.useRef(null);
4386
+ React.useEffect(
4387
+ () => () => {
4388
+ if (resetRef.current !== null) clearTimeout(resetRef.current);
4389
+ },
4390
+ []
4391
+ );
4392
+ const copy = React.useCallback(async () => {
4393
+ let copied = false;
4394
+ try {
4395
+ const clipboard = typeof navigator === "undefined" ? void 0 : navigator.clipboard;
4396
+ if (typeof clipboard?.writeText === "function") {
4397
+ await clipboard.writeText(text);
4398
+ copied = true;
4399
+ }
4400
+ } catch {
4401
+ copied = false;
4402
+ }
4403
+ setOutcome(copied ? "copied" : "failed");
4404
+ if (resetRef.current !== null) clearTimeout(resetRef.current);
4405
+ resetRef.current = setTimeout(() => setOutcome("idle"), COPY_FEEDBACK_MS);
4406
+ }, [text]);
4407
+ const label = outcome === "copied" ? T.copied : outcome === "failed" ? T.copyFailed : T.copyAnswer;
4408
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4409
+ "div",
4410
+ {
4411
+ "data-boff-explore": "answer-actions",
4412
+ className: "flex flex-wrap items-center gap-2 pt-0.5",
4413
+ children: [
4414
+ /* @__PURE__ */ jsxRuntime.jsxs(
4415
+ "button",
4416
+ {
4417
+ "data-boff-explore": "copy-answer",
4418
+ "data-outcome": outcome,
4419
+ type: "button",
4420
+ title: label,
4421
+ onClick: () => {
4422
+ void copy();
4423
+ },
4424
+ className: cn(
4425
+ ANSWER_ACTION_PILL,
4426
+ outcome === "failed" ? "border-destructive/40 bg-card text-destructive" : ANSWER_ACTION_IDLE
4427
+ ),
4428
+ children: [
4429
+ outcome === "copied" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-3 w-3 shrink-0", "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { className: "h-3 w-3 shrink-0", "aria-hidden": "true" }),
4430
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: label })
4431
+ ]
4432
+ }
4433
+ ),
4434
+ /* @__PURE__ */ jsxRuntime.jsx(ReadAloudButton, { strings: T, text: speechText, lang: speechLang }),
4435
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "data-boff-explore": "copy-status", role: "status", className: "sr-only", children: outcome === "idle" ? "" : label })
4436
+ ]
4437
+ }
4438
+ );
4439
+ }
3427
4440
  function UserBubble({
3428
4441
  strings: T,
3429
4442
  message
@@ -3469,6 +4482,8 @@ function AssistantBubble({
3469
4482
  message,
3470
4483
  activity,
3471
4484
  imageOrigins,
4485
+ productNames,
4486
+ speechLang,
3472
4487
  onNavigate,
3473
4488
  anchorRef,
3474
4489
  onFollow,
@@ -3479,6 +4494,7 @@ function AssistantBubble({
3479
4494
  const body = message.content || message.partialContent || "";
3480
4495
  const failed = message.status === "FAILED";
3481
4496
  const completed = message.status === "COMPLETED";
4497
+ const visible = useProgressiveReveal(body, !streaming);
3482
4498
  const [failedImages, setFailedImages] = React.useState(
3483
4499
  () => /* @__PURE__ */ new Set()
3484
4500
  );
@@ -3487,7 +4503,7 @@ function AssistantBubble({
3487
4503
  (current) => current.has(src) ? current : new Set(current).add(src)
3488
4504
  );
3489
4505
  }, []);
3490
- const imageScope = React.useMemo(
4506
+ const answerScope = React.useMemo(
3491
4507
  () => ({
3492
4508
  strings: T,
3493
4509
  enabled: completed,
@@ -3497,6 +4513,19 @@ function AssistantBubble({
3497
4513
  }),
3498
4514
  [T, completed, imageOrigins, failedImages, markImageFailed]
3499
4515
  );
4516
+ const provenance = React.useMemo(
4517
+ () => summariseReferences(message.references, imageOrigins, productNames),
4518
+ [message.references, imageOrigins, productNames]
4519
+ );
4520
+ const copyText = React.useMemo(
4521
+ () => answerPlainText(message.content),
4522
+ [message.content]
4523
+ );
4524
+ const speechText = React.useMemo(
4525
+ () => answerSpeechText(message.content),
4526
+ [message.content]
4527
+ );
4528
+ const grounded = completed && message.answered !== false;
3500
4529
  return /* @__PURE__ */ jsxRuntime.jsxs(
3501
4530
  "div",
3502
4531
  {
@@ -3542,10 +4571,11 @@ function AssistantBubble({
3542
4571
  ]
3543
4572
  }
3544
4573
  ) : null,
3545
- body ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-2xl rounded-tl-md border border-border bg-card px-4 py-3 shadow-sm", children: [
3546
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "boff-prose boff-prose-sm boff-explore-body min-w-0 text-card-foreground", children: /* @__PURE__ */ jsxRuntime.jsx(InlineImageScopeContext.Provider, { value: imageScope, children: /* @__PURE__ */ jsxRuntime.jsx(Markdown, { components: ANSWER_MARKDOWN_COMPONENTS, children: body }) }) }),
4574
+ visible ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-2xl rounded-tl-md border border-border bg-card px-4 py-3 shadow-sm", children: [
4575
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "boff-prose boff-prose-sm boff-explore-body min-w-0 text-card-foreground", children: /* @__PURE__ */ jsxRuntime.jsx(AnswerScopeContext.Provider, { value: answerScope, children: /* @__PURE__ */ jsxRuntime.jsx(Markdown, { components: ANSWER_MARKDOWN_COMPONENTS, children: visible }) }) }),
3547
4576
  streaming ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "boff-explore-caret", "aria-hidden": "true" }) : null
3548
4577
  ] }) : null,
4578
+ grounded && provenance.total > 0 ? /* @__PURE__ */ jsxRuntime.jsx(ProvenanceChips, { strings: T, provenance }) : null,
3549
4579
  streaming ? /* @__PURE__ */ jsxRuntime.jsx(ThinkingIndicator, { activity }) : null,
3550
4580
  failed && !body ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-2xl rounded-tl-md border border-border bg-muted px-4 py-3 text-sm text-muted-foreground", children: T.answerDidNotArrive }) : null,
3551
4581
  message.references.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
@@ -3585,6 +4615,15 @@ function AssistantBubble({
3585
4615
  question
3586
4616
  )) })
3587
4617
  ] }) : null,
4618
+ completed && copyText !== "" ? /* @__PURE__ */ jsxRuntime.jsx(
4619
+ AnswerActions,
4620
+ {
4621
+ strings: T,
4622
+ text: copyText,
4623
+ speechText,
4624
+ speechLang
4625
+ }
4626
+ ) : null,
3588
4627
  completed && onFeedback ? /* @__PURE__ */ jsxRuntime.jsx(
3589
4628
  AnswerFeedback,
3590
4629
  {
@@ -3797,6 +4836,96 @@ function ProductStrip({
3797
4836
  }
3798
4837
  );
3799
4838
  }
4839
+ var MAX_CONCEPT_CARDS = 6;
4840
+ function ConceptStrip({
4841
+ strings: T,
4842
+ highlights,
4843
+ onAsk,
4844
+ disabled
4845
+ }) {
4846
+ const [failed, setFailed] = React.useState(() => /* @__PURE__ */ new Set());
4847
+ const markFailed = React.useCallback((src) => {
4848
+ setFailed(
4849
+ (current) => current.has(src) ? current : new Set(current).add(src)
4850
+ );
4851
+ }, []);
4852
+ const visible = highlights.filter(
4853
+ (highlight) => highlight.imageUrl !== "" && !failed.has(highlight.imageUrl)
4854
+ ).slice(0, MAX_CONCEPT_CARDS);
4855
+ if (visible.length === 0) return null;
4856
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4857
+ "div",
4858
+ {
4859
+ "data-boff-explore": "concept-strip-block",
4860
+ className: "boff-explore-rise boff-explore-rise-1 mt-6",
4861
+ children: [
4862
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: T.conceptStripTitle }),
4863
+ /* @__PURE__ */ jsxRuntime.jsx(
4864
+ "ul",
4865
+ {
4866
+ "data-boff-explore": "concept-strip",
4867
+ "aria-label": T.conceptStripLabel,
4868
+ className: "boff-explore-strip flex min-w-0 snap-x gap-3 overflow-x-auto pb-1",
4869
+ children: visible.map((highlight) => /* @__PURE__ */ jsxRuntime.jsx(
4870
+ "li",
4871
+ {
4872
+ className: "shrink-0 snap-start",
4873
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
4874
+ "button",
4875
+ {
4876
+ "data-boff-explore": "concept-card",
4877
+ "data-product": highlight.product,
4878
+ type: "button",
4879
+ disabled,
4880
+ "aria-label": fill(T.conceptAsk, {
4881
+ title: highlight.title,
4882
+ product: highlight.productName
4883
+ }),
4884
+ onClick: () => onAsk(
4885
+ fill(T.conceptAskQuestion, {
4886
+ title: highlight.title,
4887
+ product: highlight.productName
4888
+ })
4889
+ ),
4890
+ className: "group flex w-40 flex-col overflow-hidden rounded-xl border border-border bg-card text-left shadow-sm transition-colors hover:border-primary/50 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring sm:w-48",
4891
+ children: [
4892
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "relative block aspect-[16/9] w-full overflow-hidden bg-muted", children: [
4893
+ /* @__PURE__ */ jsxRuntime.jsx(
4894
+ "img",
4895
+ {
4896
+ src: highlight.imageUrl,
4897
+ alt: highlight.alt,
4898
+ loading: "lazy",
4899
+ decoding: "async",
4900
+ onError: () => markFailed(highlight.imageUrl),
4901
+ className: "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
4902
+ }
4903
+ ),
4904
+ /* @__PURE__ */ jsxRuntime.jsx(
4905
+ "span",
4906
+ {
4907
+ "aria-hidden": "true",
4908
+ className: "pointer-events-none absolute left-1.5 top-1.5 rounded-full border border-border bg-background/90 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-foreground",
4909
+ children: T.conceptIllustration
4910
+ }
4911
+ )
4912
+ ] }),
4913
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-col gap-0.5 p-2.5", children: [
4914
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "line-clamp-2 text-xs font-semibold leading-snug text-card-foreground", children: highlight.title }),
4915
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: highlight.productName })
4916
+ ] })
4917
+ ]
4918
+ }
4919
+ )
4920
+ },
4921
+ `${highlight.product}:${highlight.url}`
4922
+ ))
4923
+ }
4924
+ )
4925
+ ]
4926
+ }
4927
+ );
4928
+ }
3800
4929
  function DisclaimerDialog({
3801
4930
  open,
3802
4931
  onClose,
@@ -3903,6 +5032,7 @@ function WelcomeState({
3903
5032
  body,
3904
5033
  prompts,
3905
5034
  products,
5035
+ conceptHighlights,
3906
5036
  focusProduct,
3907
5037
  onPrompt,
3908
5038
  onFocus,
@@ -3917,6 +5047,15 @@ function WelcomeState({
3917
5047
  /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "mt-4 text-balance break-words text-2xl font-bold tracking-tight sm:text-4xl", children: title }),
3918
5048
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mx-auto mt-2.5 max-w-2xl text-pretty text-sm leading-relaxed text-muted-foreground sm:text-lg", children: body })
3919
5049
  ] }),
5050
+ /* @__PURE__ */ jsxRuntime.jsx(
5051
+ ConceptStrip,
5052
+ {
5053
+ strings: T,
5054
+ highlights: conceptHighlights,
5055
+ onAsk: onPrompt,
5056
+ disabled
5057
+ }
5058
+ ),
3920
5059
  /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "boff-explore-rise boff-explore-rise-1 mt-6 grid grid-cols-3 gap-2 sm:mt-7 sm:gap-2.5", children: CAPABILITIES.map(({ icon: Icon, body: capBody }, i) => /* @__PURE__ */ jsxRuntime.jsxs(
3921
5060
  "li",
3922
5061
  {
@@ -3978,6 +5117,7 @@ function ExplorePage({
3978
5117
  );
3979
5118
  const T = React.useMemo(() => exploreStrings(activeLocale), [activeLocale]);
3980
5119
  const rtl = isRtlLocale(activeLocale);
5120
+ const speechLang = speechLocaleOf(activeLocale);
3981
5121
  const {
3982
5122
  phase,
3983
5123
  catalog,
@@ -4062,6 +5202,10 @@ function ExplorePage({
4062
5202
  () => inlineImageOrigins(catalog.products),
4063
5203
  [catalog.products]
4064
5204
  );
5205
+ const productNames = React.useMemo(
5206
+ () => productNamesByOrigin(catalog.products),
5207
+ [catalog.products]
5208
+ );
4065
5209
  React.useEffect(() => {
4066
5210
  const el = textareaRef.current;
4067
5211
  if (!el) return;
@@ -4078,11 +5222,11 @@ function ExplorePage({
4078
5222
  const layoutHeight = window.innerHeight;
4079
5223
  if (layoutHeight === 0) return;
4080
5224
  const top = Math.round(
4081
- clamp(el.getBoundingClientRect().top, 0, layoutHeight)
5225
+ clamp2(el.getBoundingClientRect().top, 0, layoutHeight)
4082
5226
  );
4083
5227
  const viewport = window.visualViewport;
4084
5228
  const inset = viewport ? Math.round(
4085
- clamp(
5229
+ clamp2(
4086
5230
  layoutHeight - viewport.height - viewport.offsetTop,
4087
5231
  0,
4088
5232
  layoutHeight
@@ -4363,6 +5507,7 @@ function ExplorePage({
4363
5507
  const text = draft.trim();
4364
5508
  if (!text || overLimit || busy || !canSend) return;
4365
5509
  stopDictation();
5510
+ stopSpeaking();
4366
5511
  setDraft("");
4367
5512
  stickToBottomRef.current = true;
4368
5513
  void send(text, toPayload(stagedRef.current));
@@ -4373,6 +5518,7 @@ function ExplorePage({
4373
5518
  const sendPrompt = React.useCallback(
4374
5519
  (prompt) => {
4375
5520
  if (!canSend || busy) return;
5521
+ stopSpeaking();
4376
5522
  setDraft("");
4377
5523
  stickToBottomRef.current = true;
4378
5524
  void send(prompt, toPayload(stagedRef.current));
@@ -4593,6 +5739,7 @@ function ExplorePage({
4593
5739
  body: resolvedBody,
4594
5740
  prompts,
4595
5741
  products: catalog.products,
5742
+ conceptHighlights: catalog.conceptHighlights,
4596
5743
  focusProduct,
4597
5744
  onPrompt: sendPrompt,
4598
5745
  onFocus: setFocusProduct,
@@ -4606,6 +5753,8 @@ function ExplorePage({
4606
5753
  message,
4607
5754
  activity,
4608
5755
  imageOrigins,
5756
+ productNames,
5757
+ speechLang,
4609
5758
  onNavigate,
4610
5759
  anchorRef: message.id === latestAssistantId ? latestAssistantRef : void 0,
4611
5760
  onFollow: (kind, url) => recordClick(message.id, kind, url),
@@ -4909,8 +6058,13 @@ function ExplorePage({
4909
6058
  }
4910
6059
 
4911
6060
  exports.ExplorePage = ExplorePage;
6061
+ exports.answerPlainText = answerPlainText;
6062
+ exports.answerSpeechText = answerSpeechText;
4912
6063
  exports.inlineImageOrigins = inlineImageOrigins;
4913
6064
  exports.isInlineImageAllowed = isInlineImageAllowed;
4914
6065
  exports.logoUrlOf = logoUrlOf;
6066
+ exports.productNamesByOrigin = productNamesByOrigin;
6067
+ exports.referenceKindOf = referenceKindOf;
6068
+ exports.summariseReferences = summariseReferences;
4915
6069
  //# sourceMappingURL=explore.js.map
4916
6070
  //# sourceMappingURL=explore.js.map