@burdenoff/website-sdk 2026.829.5 → 2026.829.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { createContext, useMemo, useState, useRef, useCallback, useEffect, useContext } from 'react';
3
- import { Sparkles, RotateCcw, Link2, History, Trash2, Mic, Square, ArrowUp, BookOpenText, Layers, TriangleAlert, ChevronLeft, ChevronRight, ExternalLink } from 'lucide-react';
3
+ import { Sparkles, RotateCcw, Link2, History, Trash2, Paperclip, X, Mic, Square, ArrowUp, BookOpenText, Layers, TriangleAlert, ChevronLeft, ChevronRight, ExternalLink } from 'lucide-react';
4
4
  import ReactMarkdown from 'react-markdown';
5
5
  import rehypeSanitize from 'rehype-sanitize';
6
6
  import remarkGfm from 'remark-gfm';
@@ -47,6 +47,12 @@ var EN = {
47
47
  disclaimerShort: "Grounded in our docs \u2014 check anything important.",
48
48
  dictate: "Dictate your question",
49
49
  stopDictating: "Stop dictating",
50
+ attach: "Attach files",
51
+ attachHint: "Up to {{max}} files, {{size}} MB each",
52
+ removeAttachment: "Remove {{name}}",
53
+ attachmentNameOnly: "name and type only",
54
+ attachmentTooLarge: "{{name}} is larger than {{size}} MB and was not attached.",
55
+ attachmentTooMany: "Only {{max}} files can be attached, so {{name}} was skipped.",
50
56
  noAnswer: "I couldn't find this in our documentation, so the answer below is not grounded in a source. Try rephrasing, or",
51
57
  askHuman: "ask a human",
52
58
  prevSuggestion: "Previous suggestion",
@@ -89,6 +95,12 @@ var ZH = {
89
95
  disclaimerShort: "\u5185\u5BB9\u57FA\u4E8E\u6211\u4EEC\u7684\u6587\u6863\uFF0C\u91CD\u8981\u4FE1\u606F\u8BF7\u81EA\u884C\u6838\u5BF9\u3002",
90
96
  dictate: "\u8BED\u97F3\u8F93\u5165",
91
97
  stopDictating: "\u505C\u6B62\u8BED\u97F3\u8F93\u5165",
98
+ attach: "\u6DFB\u52A0\u6587\u4EF6",
99
+ attachHint: "\u6700\u591A {{max}} \u4E2A\u6587\u4EF6\uFF0C\u6BCF\u4E2A\u4E0D\u8D85\u8FC7 {{size}} MB",
100
+ removeAttachment: "\u79FB\u9664 {{name}}",
101
+ attachmentNameOnly: "\u4EC5\u6587\u4EF6\u540D\u548C\u7C7B\u578B",
102
+ attachmentTooLarge: "{{name}} \u8D85\u8FC7 {{size}} MB\uFF0C\u672A\u80FD\u6DFB\u52A0\u3002",
103
+ attachmentTooMany: "\u6700\u591A\u53EA\u80FD\u6DFB\u52A0 {{max}} \u4E2A\u6587\u4EF6\uFF0C\u5DF2\u8DF3\u8FC7 {{name}}\u3002",
92
104
  noAnswer: "\u6211\u5728\u6587\u6863\u4E2D\u6CA1\u6709\u627E\u5230\u76F8\u5173\u5185\u5BB9\uFF0C\u4E0B\u9762\u7684\u56DE\u7B54\u6CA1\u6709\u6765\u6E90\u652F\u6301\u3002\u8BF7\u6362\u4E2A\u8BF4\u6CD5\uFF0C\u6216",
93
105
  askHuman: "\u8054\u7CFB\u6211\u4EEC",
94
106
  prevSuggestion: "\u4E0A\u4E00\u4E2A\u5EFA\u8BAE",
@@ -127,6 +139,12 @@ var HI = {
127
139
  disclaimerShort: "\u092F\u0939 \u0939\u092E\u093E\u0930\u0947 \u0926\u0938\u094D\u0924\u093E\u0935\u0947\u091C\u093C\u094B\u0902 \u092A\u0930 \u0906\u0927\u093E\u0930\u093F\u0924 \u0939\u0948 \u2014 \u092E\u0939\u0924\u094D\u0935\u092A\u0942\u0930\u094D\u0923 \u092C\u093E\u0924\u0947\u0902 \u091C\u093C\u0930\u0942\u0930 \u091C\u093E\u0901\u091A \u0932\u0947\u0902\u0964",
128
140
  dictate: "\u092C\u094B\u0932\u0915\u0930 \u092A\u0942\u091B\u0947\u0902",
129
141
  stopDictating: "\u092C\u094B\u0932\u0928\u093E \u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
142
+ attach: "\u092B\u093C\u093E\u0907\u0932\u0947\u0902 \u091C\u094B\u0921\u093C\u0947\u0902",
143
+ attachHint: "\u0905\u0927\u093F\u0915\u0924\u092E {{max}} \u092B\u093C\u093E\u0907\u0932\u0947\u0902, \u092A\u094D\u0930\u0924\u094D\u092F\u0947\u0915 {{size}} MB \u0924\u0915",
144
+ removeAttachment: "{{name}} \u0939\u091F\u093E\u090F\u0901",
145
+ attachmentNameOnly: "\u0915\u0947\u0935\u0932 \u0928\u093E\u092E \u0914\u0930 \u092A\u094D\u0930\u0915\u093E\u0930",
146
+ attachmentTooLarge: "{{name}} {{size}} MB \u0938\u0947 \u092C\u0921\u093C\u0940 \u0939\u0948, \u0907\u0938\u0932\u093F\u090F \u091C\u094B\u0921\u093C\u0940 \u0928\u0939\u0940\u0902 \u0917\u0908\u0964",
147
+ attachmentTooMany: "\u0915\u0947\u0935\u0932 {{max}} \u092B\u093C\u093E\u0907\u0932\u0947\u0902 \u091C\u094B\u0921\u093C\u0940 \u091C\u093E \u0938\u0915\u0924\u0940 \u0939\u0948\u0902, \u0907\u0938\u0932\u093F\u090F {{name}} \u091B\u094B\u0921\u093C \u0926\u0940 \u0917\u0908\u0964",
130
148
  noAnswer: "\u092F\u0939 \u092E\u0941\u091D\u0947 \u0939\u092E\u093E\u0930\u0947 \u0926\u0938\u094D\u0924\u093E\u0935\u0947\u091C\u093C\u094B\u0902 \u092E\u0947\u0902 \u0928\u0939\u0940\u0902 \u092E\u093F\u0932\u093E, \u0907\u0938\u0932\u093F\u090F \u0928\u0940\u091A\u0947 \u0926\u093F\u092F\u093E \u0909\u0924\u094D\u0924\u0930 \u0915\u093F\u0938\u0940 \u0938\u094D\u0930\u094B\u0924 \u092A\u0930 \u0906\u0927\u093E\u0930\u093F\u0924 \u0928\u0939\u0940\u0902 \u0939\u0948\u0964 \u0926\u0942\u0938\u0930\u0947 \u0936\u092C\u094D\u0926\u094B\u0902 \u092E\u0947\u0902 \u092A\u0942\u091B\u0947\u0902, \u092F\u093E",
131
149
  askHuman: "\u0939\u092E\u0938\u0947 \u0938\u0902\u092A\u0930\u094D\u0915 \u0915\u0930\u0947\u0902",
132
150
  prevSuggestion: "\u092A\u093F\u091B\u0932\u093E \u0938\u0941\u091D\u093E\u0935",
@@ -169,6 +187,12 @@ var ES = {
169
187
  disclaimerShort: "Basado en nuestra documentaci\xF3n: comprueba cualquier dato importante.",
170
188
  dictate: "Dictar tu pregunta",
171
189
  stopDictating: "Dejar de dictar",
190
+ attach: "Adjuntar archivos",
191
+ attachHint: "Hasta {{max}} archivos, {{size}} MB cada uno",
192
+ removeAttachment: "Quitar {{name}}",
193
+ attachmentNameOnly: "solo nombre y tipo",
194
+ attachmentTooLarge: "{{name}} supera los {{size}} MB y no se adjunt\xF3.",
195
+ attachmentTooMany: "Solo se pueden adjuntar {{max}} archivos, as\xED que se omiti\xF3 {{name}}.",
172
196
  noAnswer: "No he encontrado esto en nuestra documentaci\xF3n, as\xED que la respuesta de abajo no se apoya en ninguna fuente. Prueba a reformularla, o",
173
197
  askHuman: "habla con una persona",
174
198
  prevSuggestion: "Sugerencia anterior",
@@ -211,6 +235,12 @@ var AR = {
211
235
  disclaimerShort: "\u0645\u0628\u0646\u064A \u0639\u0644\u0649 \u0648\u062B\u0627\u0626\u0642\u0646\u0627 \u2014 \u062A\u062D\u0642\u0651\u0642 \u0645\u0646 \u0623\u064A \u0645\u0639\u0644\u0648\u0645\u0629 \u0645\u0647\u0645\u0629.",
212
236
  dictate: "\u0623\u0645\u0644\u0650 \u0633\u0624\u0627\u0644\u0643",
213
237
  stopDictating: "\u0625\u064A\u0642\u0627\u0641 \u0627\u0644\u0625\u0645\u0644\u0627\u0621",
238
+ attach: "\u0625\u0631\u0641\u0627\u0642 \u0645\u0644\u0641\u0627\u062A",
239
+ attachHint: "\u062D\u062A\u0649 {{max}} \u0645\u0644\u0641\u0627\u062A\u060C \u0628\u062D\u062F \u0623\u0642\u0635\u0649 {{size}} \u0645\u064A\u063A\u0627\u0628\u0627\u064A\u062A \u0644\u0643\u0644 \u0645\u0644\u0641",
240
+ removeAttachment: "\u0625\u0632\u0627\u0644\u0629 {{name}}",
241
+ attachmentNameOnly: "\u0627\u0644\u0627\u0633\u0645 \u0648\u0627\u0644\u0646\u0648\u0639 \u0641\u0642\u0637",
242
+ attachmentTooLarge: "{{name}} \u0623\u0643\u0628\u0631 \u0645\u0646 {{size}} \u0645\u064A\u063A\u0627\u0628\u0627\u064A\u062A \u0648\u0644\u0645 \u064A\u062A\u0645 \u0625\u0631\u0641\u0627\u0642\u0647.",
243
+ attachmentTooMany: "\u064A\u0645\u0643\u0646 \u0625\u0631\u0641\u0627\u0642 {{max}} \u0645\u0644\u0641\u0627\u062A \u0641\u0642\u0637\u060C \u0644\u0630\u0627 \u062A\u0645 \u062A\u062E\u0637\u064A {{name}}.",
214
244
  noAnswer: "\u0644\u0645 \u0623\u062C\u062F \u0647\u0630\u0627 \u0641\u064A \u0648\u062B\u0627\u0626\u0642\u0646\u0627\u060C \u0644\u0630\u0627 \u0641\u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0623\u062F\u0646\u0627\u0647 \u063A\u064A\u0631 \u0645\u0633\u062A\u0646\u062F\u0629 \u0625\u0644\u0649 \u0645\u0635\u062F\u0631. \u062D\u0627\u0648\u0644 \u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u0635\u064A\u0627\u063A\u0629\u060C \u0623\u0648",
215
245
  askHuman: "\u062A\u062D\u062F\u0651\u062B \u0625\u0644\u0649 \u0634\u062E\u0635",
216
246
  prevSuggestion: "\u0627\u0644\u0627\u0642\u062A\u0631\u0627\u062D \u0627\u0644\u0633\u0627\u0628\u0642",
@@ -253,6 +283,12 @@ var TA = {
253
283
  disclaimerShort: "\u0B8E\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0B86\u0BB5\u0BA3\u0B99\u0BCD\u0B95\u0BB3\u0BC8 \u0B85\u0B9F\u0BBF\u0BAA\u0BCD\u0BAA\u0B9F\u0BC8\u0BAF\u0BBE\u0B95\u0B95\u0BCD \u0B95\u0BCA\u0BA3\u0BCD\u0B9F\u0BA4\u0BC1 \u2014 \u0BAE\u0BC1\u0B95\u0BCD\u0B95\u0BBF\u0BAF\u0BAE\u0BBE\u0BA9\u0BA4\u0BC8\u0B9A\u0BCD \u0B9A\u0BB0\u0BBF\u0BAA\u0BBE\u0BB0\u0BCD\u0B95\u0BCD\u0B95\u0BB5\u0BC1\u0BAE\u0BCD.",
254
284
  dictate: "\u0B89\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0B95\u0BC7\u0BB3\u0BCD\u0BB5\u0BBF\u0BAF\u0BC8\u0BAA\u0BCD \u0BAA\u0BC7\u0B9A\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
255
285
  stopDictating: "\u0BAA\u0BC7\u0B9A\u0BC1\u0BB5\u0BA4\u0BC8 \u0BA8\u0BBF\u0BB1\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1",
286
+ attach: "\u0B95\u0BCB\u0BAA\u0BCD\u0BAA\u0BC1\u0B95\u0BB3\u0BC8 \u0B87\u0BA3\u0BC8\u0B95\u0BCD\u0B95",
287
+ attachHint: "\u0B85\u0BA4\u0BBF\u0B95\u0BAA\u0B9F\u0BCD\u0B9A\u0BAE\u0BCD {{max}} \u0B95\u0BCB\u0BAA\u0BCD\u0BAA\u0BC1\u0B95\u0BB3\u0BCD, \u0B92\u0BB5\u0BCD\u0BB5\u0BCA\u0BA9\u0BCD\u0BB1\u0BC1\u0BAE\u0BCD {{size}} MB \u0BB5\u0BB0\u0BC8",
288
+ removeAttachment: "{{name}} \u0B85\u0B95\u0BB1\u0BCD\u0BB1\u0BC1",
289
+ attachmentNameOnly: "\u0BAA\u0BC6\u0BAF\u0BB0\u0BC1\u0BAE\u0BCD \u0BB5\u0B95\u0BC8\u0BAF\u0BC1\u0BAE\u0BCD \u0BAE\u0B9F\u0BCD\u0B9F\u0BC1\u0BAE\u0BCD",
290
+ attachmentTooLarge: "{{name}} {{size}} MB-\u0B90 \u0BB5\u0BBF\u0B9F \u0BAA\u0BC6\u0BB0\u0BBF\u0BAF\u0BA4\u0BC1, \u0B8E\u0BA9\u0BB5\u0BC7 \u0B87\u0BA3\u0BC8\u0B95\u0BCD\u0B95\u0BAA\u0BCD\u0BAA\u0B9F\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8.",
291
+ attachmentTooMany: "{{max}} \u0B95\u0BCB\u0BAA\u0BCD\u0BAA\u0BC1\u0B95\u0BB3\u0BCD \u0BAE\u0B9F\u0BCD\u0B9F\u0BC1\u0BAE\u0BC7 \u0B87\u0BA3\u0BC8\u0B95\u0BCD\u0B95 \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BC1\u0BAE\u0BCD, \u0B8E\u0BA9\u0BB5\u0BC7 {{name}} \u0BA4\u0BB5\u0BBF\u0BB0\u0BCD\u0B95\u0BCD\u0B95\u0BAA\u0BCD\u0BAA\u0B9F\u0BCD\u0B9F\u0BA4\u0BC1.",
256
292
  noAnswer: "\u0B87\u0BA4\u0BC1 \u0B8E\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0B86\u0BB5\u0BA3\u0B99\u0BCD\u0B95\u0BB3\u0BBF\u0BB2\u0BCD \u0B95\u0BBF\u0B9F\u0BC8\u0B95\u0BCD\u0B95\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8, \u0B8E\u0BA9\u0BB5\u0BC7 \u0B95\u0BC0\u0BB4\u0BC7 \u0B89\u0BB3\u0BCD\u0BB3 \u0BAA\u0BA4\u0BBF\u0BB2\u0BC1\u0B95\u0BCD\u0B95\u0BC1 \u0BAE\u0BC2\u0BB2\u0BAE\u0BCD \u0B87\u0BB2\u0BCD\u0BB2\u0BC8. \u0BB5\u0BC7\u0BB1\u0BC1 \u0BB5\u0B95\u0BC8\u0BAF\u0BBF\u0BB2\u0BCD \u0B95\u0BC7\u0BB3\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD, \u0B85\u0BB2\u0BCD\u0BB2\u0BA4\u0BC1",
257
293
  askHuman: "\u0B8E\u0B99\u0BCD\u0B95\u0BB3\u0BC8\u0BA4\u0BCD \u0BA4\u0BCA\u0B9F\u0BB0\u0BCD\u0BAA\u0BC1 \u0B95\u0BCA\u0BB3\u0BCD\u0BB3\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
258
294
  prevSuggestion: "\u0BAE\u0BC1\u0BA8\u0BCD\u0BA4\u0BC8\u0BAF \u0BAA\u0BB0\u0BBF\u0BA8\u0BCD\u0BA4\u0BC1\u0BB0\u0BC8",
@@ -756,6 +792,12 @@ var EXPLORE_MESSAGE_FIELDS = (
756
792
  }
757
793
  followUps
758
794
  answered
795
+ attachments {
796
+ name
797
+ mimeType
798
+ size
799
+ textExtracted
800
+ }
759
801
  errorCode
760
802
  createdAt
761
803
  completedAt
@@ -841,6 +883,16 @@ var CREATE_EXPLORE_SHARE_LINK_MUTATION = (
841
883
  }
842
884
  `
843
885
  );
886
+ var EXPLORE_SHARED_CONVERSATION_QUERY = (
887
+ /* GraphQL */
888
+ `
889
+ query ExploreSharedConversation($shareToken: String!) {
890
+ exploreSharedConversation(shareToken: $shareToken) {
891
+ ${EXPLORE_MESSAGE_FIELDS}
892
+ }
893
+ }
894
+ `
895
+ );
844
896
 
845
897
  // src/data/products.ts
846
898
  var ECOSYSTEM_PRODUCTS = [
@@ -1291,6 +1343,14 @@ function readPersisted(key) {
1291
1343
  return null;
1292
1344
  }
1293
1345
  }
1346
+ function readShareToken() {
1347
+ if (typeof window === "undefined") return null;
1348
+ try {
1349
+ return new URL(window.location.href).searchParams.get("c");
1350
+ } catch {
1351
+ return null;
1352
+ }
1353
+ }
1294
1354
  function visibleText(message) {
1295
1355
  return message.content || message.partialContent || "";
1296
1356
  }
@@ -1329,10 +1389,18 @@ function useExploreChat(options = {}) {
1329
1389
  const pollGenerationRef = useRef(0);
1330
1390
  const pollTimerRef = useRef(null);
1331
1391
  const conversationTokenRef = useRef(null);
1392
+ const shareTokenRef = useRef(void 0);
1393
+ if (shareTokenRef.current === void 0) {
1394
+ shareTokenRef.current = readShareToken();
1395
+ }
1396
+ const isSharedView = shareTokenRef.current !== null;
1332
1397
  const catalogRef = useRef(fallbackCatalog);
1333
1398
  const turnCountRef = useRef(0);
1334
1399
  const focusProductRef = useRef(null);
1335
1400
  const lastSentTextRef = useRef(null);
1401
+ const lastSentAttachmentsRef = useRef(
1402
+ []
1403
+ );
1336
1404
  const inFlightRef = useRef(false);
1337
1405
  catalogRef.current = catalog;
1338
1406
  const clearPollTimer = useCallback(() => {
@@ -1357,6 +1425,43 @@ function useExploreChat(options = {}) {
1357
1425
  };
1358
1426
  }, []);
1359
1427
  useEffect(() => {
1428
+ const shareToken = shareTokenRef.current;
1429
+ if (!shareToken) return;
1430
+ let cancelled = false;
1431
+ setHydrated(true);
1432
+ void client.query(
1433
+ EXPLORE_SHARED_CONVERSATION_QUERY,
1434
+ { shareToken },
1435
+ { cache: false }
1436
+ ).then((res) => {
1437
+ if (cancelled || !mountedRef.current) return;
1438
+ const shared = res?.data?.exploreSharedConversation;
1439
+ if (shared && shared.length > 0) {
1440
+ setMessages(shared);
1441
+ return;
1442
+ }
1443
+ setError({
1444
+ kind: "unknown",
1445
+ code: "SHARE_NOT_FOUND",
1446
+ message: "That shared conversation could not be found. The link may have expired.",
1447
+ // Nothing to retry: a bad token will stay bad.
1448
+ retryable: false
1449
+ });
1450
+ }).catch(() => {
1451
+ if (cancelled || !mountedRef.current) return;
1452
+ setError({
1453
+ kind: "unknown",
1454
+ code: "SHARE_NOT_FOUND",
1455
+ message: "That shared conversation could not be loaded.",
1456
+ retryable: true
1457
+ });
1458
+ });
1459
+ return () => {
1460
+ cancelled = true;
1461
+ };
1462
+ }, [client]);
1463
+ useEffect(() => {
1464
+ if (shareTokenRef.current !== null) return;
1360
1465
  if (!persist) {
1361
1466
  setHydrated(true);
1362
1467
  return;
@@ -1524,7 +1629,7 @@ function useExploreChat(options = {}) {
1524
1629
  [client, pollIntervalMs, pollTimeoutMs]
1525
1630
  );
1526
1631
  const submit = useCallback(
1527
- async (text, optimisticId, captchaAttempt) => {
1632
+ async (text, optimisticId, captchaAttempt, attachments) => {
1528
1633
  const activeCatalog = catalogRef.current;
1529
1634
  let recaptchaToken;
1530
1635
  if (activeCatalog.captchaRequired && activeCatalog.recaptchaSiteKey) {
@@ -1542,7 +1647,8 @@ function useExploreChat(options = {}) {
1542
1647
  ...focusProductRef.current || productSlug ? { productSlug: focusProductRef.current ?? productSlug } : {},
1543
1648
  ...recaptchaToken ? { recaptchaToken } : {},
1544
1649
  ...locale ? { locale } : {},
1545
- ...pageUrl ? { pageUrl } : {}
1650
+ ...pageUrl ? { pageUrl } : {},
1651
+ ...attachments.length > 0 ? { attachments: [...attachments] } : {}
1546
1652
  };
1547
1653
  const res = await client.mutate(
1548
1654
  SEND_EXPLORE_MESSAGE_MUTATION,
@@ -1556,7 +1662,7 @@ function useExploreChat(options = {}) {
1556
1662
  const code = typeof first?.extensions?.code === "string" ? first.extensions.code : void 0;
1557
1663
  const classified = classifyExploreError(code, first?.message);
1558
1664
  if (classified.kind === "captcha" && captchaAttempt === 0) {
1559
- await submit(text, optimisticId, 1);
1665
+ await submit(text, optimisticId, 1, attachments);
1560
1666
  return;
1561
1667
  }
1562
1668
  inFlightRef.current = false;
@@ -1593,7 +1699,7 @@ function useExploreChat(options = {}) {
1593
1699
  [client, locale, pageUrl, productSlug, startPolling]
1594
1700
  );
1595
1701
  const send = useCallback(
1596
- async (message) => {
1702
+ async (message, attachments = []) => {
1597
1703
  const text = message.trim();
1598
1704
  if (!text) return;
1599
1705
  if (inFlightRef.current) return;
@@ -1623,6 +1729,7 @@ function useExploreChat(options = {}) {
1623
1729
  }
1624
1730
  inFlightRef.current = true;
1625
1731
  lastSentTextRef.current = text;
1732
+ lastSentAttachmentsRef.current = attachments;
1626
1733
  setError(null);
1627
1734
  setPhase("sending");
1628
1735
  cancelPolling();
@@ -1645,7 +1752,7 @@ function useExploreChat(options = {}) {
1645
1752
  setTurnCount(turnCountRef.current);
1646
1753
  onSend?.(text);
1647
1754
  try {
1648
- await submit(text, optimistic.id, 0);
1755
+ await submit(text, optimistic.id, 0, attachments);
1649
1756
  } catch (err) {
1650
1757
  if (!mountedRef.current) return;
1651
1758
  console.error("[WebSDK] Explore send failed", err);
@@ -1694,7 +1801,7 @@ function useExploreChat(options = {}) {
1694
1801
  });
1695
1802
  turnCountRef.current = Math.max(0, turnCountRef.current - 1);
1696
1803
  setTurnCount(turnCountRef.current);
1697
- void send(text);
1804
+ void send(text, lastSentAttachmentsRef.current);
1698
1805
  }, [cancelPolling, send]);
1699
1806
  const reset = useCallback(() => {
1700
1807
  setArchive(
@@ -1708,6 +1815,7 @@ function useExploreChat(options = {}) {
1708
1815
  inFlightRef.current = false;
1709
1816
  conversationTokenRef.current = null;
1710
1817
  lastSentTextRef.current = null;
1818
+ lastSentAttachmentsRef.current = [];
1711
1819
  turnCountRef.current = 0;
1712
1820
  setTurnCount(0);
1713
1821
  setMessages([]);
@@ -1816,7 +1924,8 @@ function useExploreChat(options = {}) {
1816
1924
  deleteConversation,
1817
1925
  clearHistory,
1818
1926
  recordClick,
1819
- createShareLink
1927
+ createShareLink,
1928
+ isSharedView
1820
1929
  };
1821
1930
  }
1822
1931
  var optimisticCounter = 0;
@@ -2200,6 +2309,145 @@ var Textarea = React.forwardRef(({ className, ...props }, ref) => {
2200
2309
  );
2201
2310
  });
2202
2311
  Textarea.displayName = "Textarea";
2312
+
2313
+ // src/utils/explore-attachments.ts
2314
+ var MAX_ATTACHMENTS = 5;
2315
+ var MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
2316
+ var MAX_TEXT_PER_ATTACHMENT = 2e4;
2317
+ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
2318
+ "txt",
2319
+ "md",
2320
+ "markdown",
2321
+ "csv",
2322
+ "tsv",
2323
+ "json",
2324
+ "jsonl",
2325
+ "yaml",
2326
+ "yml",
2327
+ "toml",
2328
+ "ini",
2329
+ "cfg",
2330
+ "conf",
2331
+ "env",
2332
+ "log",
2333
+ "sql",
2334
+ "graphql",
2335
+ "gql",
2336
+ "html",
2337
+ "htm",
2338
+ "xml",
2339
+ "svg",
2340
+ "css",
2341
+ "scss",
2342
+ "js",
2343
+ "jsx",
2344
+ "mjs",
2345
+ "cjs",
2346
+ "ts",
2347
+ "tsx",
2348
+ "py",
2349
+ "rb",
2350
+ "go",
2351
+ "rs",
2352
+ "java",
2353
+ "kt",
2354
+ "c",
2355
+ "h",
2356
+ "cpp",
2357
+ "cs",
2358
+ "php",
2359
+ "sh",
2360
+ "bash",
2361
+ "zsh",
2362
+ "ps1",
2363
+ "dockerfile",
2364
+ "tf",
2365
+ "tfvars"
2366
+ ]);
2367
+ function isTextualMimeType(mimeType) {
2368
+ if (mimeType.startsWith("text/")) return true;
2369
+ return mimeType === "application/json" || mimeType === "application/xml" || mimeType === "application/yaml" || mimeType === "application/x-yaml" || mimeType === "application/javascript" || mimeType === "application/graphql" || mimeType.endsWith("+json") || mimeType.endsWith("+xml");
2370
+ }
2371
+ function extensionOf(name) {
2372
+ const dot = name.lastIndexOf(".");
2373
+ if (dot <= 0 || dot === name.length - 1) return "";
2374
+ return name.slice(dot + 1).toLowerCase();
2375
+ }
2376
+ function looksTextual(name, mimeType) {
2377
+ if (isTextualMimeType(mimeType)) return true;
2378
+ return TEXT_EXTENSIONS.has(extensionOf(name));
2379
+ }
2380
+ function isDecodedTextUsable(decoded) {
2381
+ if (decoded.length === 0) return false;
2382
+ if (decoded.includes("\0")) return false;
2383
+ let replacements = 0;
2384
+ for (const char of decoded) if (char === "\uFFFD") replacements += 1;
2385
+ return replacements / decoded.length < 0.1;
2386
+ }
2387
+ function formatBytes(bytes) {
2388
+ if (bytes < 1024) return `${bytes} B`;
2389
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
2390
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2391
+ }
2392
+ var counter = 0;
2393
+ function nextId() {
2394
+ counter += 1;
2395
+ return `att-${counter}`;
2396
+ }
2397
+ async function readArrayBuffer(blob) {
2398
+ if (typeof blob.arrayBuffer === "function") return blob.arrayBuffer();
2399
+ return new Promise((resolve, reject) => {
2400
+ const reader = new FileReader();
2401
+ reader.onload = () => resolve(reader.result);
2402
+ reader.onerror = () => reject(reader.error ?? new Error("file read failed"));
2403
+ reader.readAsArrayBuffer(blob);
2404
+ });
2405
+ }
2406
+ async function readTextPrefix(file) {
2407
+ try {
2408
+ const slice = file.slice(0, MAX_TEXT_PER_ATTACHMENT);
2409
+ const buffer = await readArrayBuffer(slice);
2410
+ const decoded = new TextDecoder("utf-8").decode(buffer);
2411
+ return isDecodedTextUsable(decoded) ? decoded : null;
2412
+ } catch {
2413
+ return null;
2414
+ }
2415
+ }
2416
+ async function stageFiles(files, alreadyStaged) {
2417
+ const accepted = [];
2418
+ const rejected = [];
2419
+ let slots = MAX_ATTACHMENTS - alreadyStaged.length;
2420
+ for (const file of files) {
2421
+ if (slots <= 0) {
2422
+ rejected.push({ name: file.name, reason: "too-many" });
2423
+ continue;
2424
+ }
2425
+ if (file.size > MAX_ATTACHMENT_BYTES) {
2426
+ rejected.push({ name: file.name, reason: "too-large" });
2427
+ continue;
2428
+ }
2429
+ const mimeType = file.type || "application/octet-stream";
2430
+ const textContent = looksTextual(file.name, mimeType) ? await readTextPrefix(file) : null;
2431
+ accepted.push({
2432
+ id: nextId(),
2433
+ name: file.name,
2434
+ mimeType,
2435
+ size: file.size,
2436
+ textContent,
2437
+ textExtracted: textContent !== null
2438
+ });
2439
+ slots -= 1;
2440
+ }
2441
+ return { accepted, rejected };
2442
+ }
2443
+ function toPayload(staged) {
2444
+ return staged.map(({ name, mimeType, size, textContent }) => ({
2445
+ name,
2446
+ mimeType,
2447
+ size,
2448
+ textContent
2449
+ }));
2450
+ }
2203
2451
  var EXPLORE_CSS = `
2204
2452
  @keyframes boff-explore-pulse {
2205
2453
  0%, 80%, 100% { opacity: 0.25; transform: translateY(0); }
@@ -2548,15 +2796,45 @@ function ThinkingIndicator({ activity }) {
2548
2796
  }
2549
2797
  );
2550
2798
  }
2551
- function UserBubble({ message }) {
2552
- return /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
2553
- "div",
2554
- {
2555
- "data-boff-explore": "user",
2556
- className: "max-w-[85%] whitespace-pre-wrap break-words rounded-2xl rounded-br-md bg-primary px-4 py-2.5 text-sm leading-relaxed text-primary-foreground shadow-sm",
2557
- children: message.content
2558
- }
2559
- ) });
2799
+ function UserBubble({
2800
+ strings: T,
2801
+ message
2802
+ }) {
2803
+ const attachments = message.attachments ?? [];
2804
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-end gap-1.5", children: [
2805
+ /* @__PURE__ */ jsx(
2806
+ "div",
2807
+ {
2808
+ "data-boff-explore": "user",
2809
+ className: "max-w-[85%] whitespace-pre-wrap break-words rounded-2xl rounded-br-md bg-primary px-4 py-2.5 text-sm leading-relaxed text-primary-foreground shadow-sm",
2810
+ children: message.content
2811
+ }
2812
+ ),
2813
+ attachments.length > 0 ? /* @__PURE__ */ jsx(
2814
+ "ul",
2815
+ {
2816
+ "data-boff-explore": "user-attachments",
2817
+ className: "flex max-w-[85%] flex-wrap justify-end gap-1.5",
2818
+ children: attachments.map((attachment) => /* @__PURE__ */ jsxs(
2819
+ "li",
2820
+ {
2821
+ "data-boff-explore": "user-attachment",
2822
+ "data-text-extracted": attachment.textExtracted ? "true" : "false",
2823
+ className: "flex max-w-full items-center gap-1.5 rounded-full border border-border bg-muted/60 px-2.5 py-1 text-xs text-muted-foreground",
2824
+ children: [
2825
+ /* @__PURE__ */ jsx(Paperclip, { className: "h-3 w-3 shrink-0" }),
2826
+ /* @__PURE__ */ jsx("span", { className: "truncate", title: attachment.name, children: attachment.name }),
2827
+ attachment.textExtracted ? null : /* @__PURE__ */ jsxs("span", { className: "shrink-0", children: [
2828
+ "\xB7 ",
2829
+ T.attachmentNameOnly
2830
+ ] })
2831
+ ]
2832
+ },
2833
+ `${attachment.name}-${attachment.size}`
2834
+ ))
2835
+ }
2836
+ ) : null
2837
+ ] });
2560
2838
  }
2561
2839
  function AssistantBubble({
2562
2840
  strings: T,
@@ -3055,7 +3333,8 @@ function ExplorePage({
3055
3333
  deleteConversation,
3056
3334
  clearHistory,
3057
3335
  recordClick,
3058
- createShareLink
3336
+ createShareLink,
3337
+ isSharedView
3059
3338
  } = useExploreChat({ productSlug, pageUrl, onSend, examplePrompts });
3060
3339
  const [draft, setDraft] = useState("");
3061
3340
  const textareaRef = useRef(null);
@@ -3081,6 +3360,12 @@ function ExplorePage({
3081
3360
  const T = useMemo(() => exploreStrings(activeLocale), [activeLocale]);
3082
3361
  const rtl = isRtlLocale(activeLocale);
3083
3362
  const [disclaimerOpen, setDisclaimerOpen] = useState(false);
3363
+ const [staged, setStaged] = useState([]);
3364
+ const [attachmentNotice, setAttachmentNotice] = useState(null);
3365
+ const fileInputRef = useRef(null);
3366
+ const stagedRef = useRef([]);
3367
+ stagedRef.current = staged;
3368
+ const pickQueueRef = useRef(Promise.resolve());
3084
3369
  const [shareLabel, setShareLabel] = useState("Share");
3085
3370
  const [shareUrl, setShareUrl] = useState(null);
3086
3371
  const scrollRef = useRef(null);
@@ -3157,13 +3442,44 @@ function ExplorePage({
3157
3442
  setShareLabel("Share");
3158
3443
  }
3159
3444
  }, [createShareLink, pageUrl]);
3445
+ const handleFilesPicked = useCallback(
3446
+ (picked) => {
3447
+ if (!picked || picked.length === 0) return pickQueueRef.current;
3448
+ const files = Array.from(picked);
3449
+ pickQueueRef.current = pickQueueRef.current.then(async () => {
3450
+ const { accepted, rejected } = await stageFiles(
3451
+ files,
3452
+ stagedRef.current
3453
+ );
3454
+ setStaged((prev) => [...prev, ...accepted]);
3455
+ const first = rejected[0];
3456
+ setAttachmentNotice(
3457
+ first ? first.reason === "too-large" ? fill(T.attachmentTooLarge, {
3458
+ name: first.name,
3459
+ size: String(MAX_ATTACHMENT_BYTES / (1024 * 1024))
3460
+ }) : fill(T.attachmentTooMany, {
3461
+ name: first.name,
3462
+ max: String(MAX_ATTACHMENTS)
3463
+ }) : null
3464
+ );
3465
+ });
3466
+ return pickQueueRef.current;
3467
+ },
3468
+ [T]
3469
+ );
3470
+ const removeAttachment = useCallback((id) => {
3471
+ setStaged((prev) => prev.filter((entry) => entry.id !== id));
3472
+ setAttachmentNotice(null);
3473
+ }, []);
3160
3474
  const submitDraft = useCallback(() => {
3161
3475
  const text = draft.trim();
3162
3476
  if (!text || overLimit || busy || !canSend) return;
3163
3477
  stopDictation();
3164
3478
  setDraft("");
3165
3479
  stickToBottomRef.current = true;
3166
- void send(text);
3480
+ void send(text, toPayload(stagedRef.current));
3481
+ setStaged([]);
3482
+ setAttachmentNotice(null);
3167
3483
  textareaRef.current?.focus();
3168
3484
  }, [busy, canSend, draft, overLimit, send, stopDictation]);
3169
3485
  const sendPrompt = useCallback(
@@ -3171,7 +3487,9 @@ function ExplorePage({
3171
3487
  if (!canSend || busy) return;
3172
3488
  setDraft("");
3173
3489
  stickToBottomRef.current = true;
3174
- void send(prompt);
3490
+ void send(prompt, toPayload(stagedRef.current));
3491
+ setStaged([]);
3492
+ setAttachmentNotice(null);
3175
3493
  },
3176
3494
  [busy, canSend, send]
3177
3495
  );
@@ -3184,7 +3502,9 @@ function ExplorePage({
3184
3502
  },
3185
3503
  [submitDraft]
3186
3504
  );
3187
- const composerDisabled = phase === "limited" || phase === "disabled" || !catalog.enabled;
3505
+ const composerDisabled = phase === "limited" || phase === "disabled" || !catalog.enabled || // A share token grants read access only — the server would reject a send anyway, so
3506
+ // disable it here rather than letting the visitor type into a dead composer.
3507
+ isSharedView;
3188
3508
  const captchaOn = Boolean(
3189
3509
  catalog.captchaRequired && catalog.recaptchaSiteKey
3190
3510
  );
@@ -3378,7 +3698,7 @@ function ExplorePage({
3378
3698
  disabled: !canSend || busy
3379
3699
  }
3380
3700
  ) : /* @__PURE__ */ jsx("div", { className: "mx-auto w-full min-w-0 max-w-3xl space-y-6 px-4 py-6", children: messages.map(
3381
- (message) => message.role === "USER" ? /* @__PURE__ */ jsx(UserBubble, { message }, message.id) : /* @__PURE__ */ jsx(
3701
+ (message) => message.role === "USER" ? /* @__PURE__ */ jsx(UserBubble, { strings: T, message }, message.id) : /* @__PURE__ */ jsx(
3382
3702
  AssistantBubble,
3383
3703
  {
3384
3704
  strings: T,
@@ -3402,6 +3722,33 @@ function ExplorePage({
3402
3722
  captchaOn
3403
3723
  }
3404
3724
  ),
3725
+ isSharedView ? /* @__PURE__ */ jsxs(
3726
+ "div",
3727
+ {
3728
+ "data-boff-explore": "shared-banner",
3729
+ className: "mx-auto flex w-full max-w-3xl items-center gap-2 px-4 pb-2 text-xs text-muted-foreground",
3730
+ children: [
3731
+ /* @__PURE__ */ jsx(Link2, { className: "h-3.5 w-3.5 shrink-0" }),
3732
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1", children: "You are viewing a shared conversation. Start your own to ask a question." }),
3733
+ /* @__PURE__ */ jsx(
3734
+ "button",
3735
+ {
3736
+ type: "button",
3737
+ "data-boff-explore": "shared-start-own",
3738
+ onClick: () => {
3739
+ if (typeof window !== "undefined") {
3740
+ const url = new URL(window.location.href);
3741
+ url.searchParams.delete("c");
3742
+ window.location.href = url.toString();
3743
+ }
3744
+ },
3745
+ className: "shrink-0 underline underline-offset-2 hover:text-foreground",
3746
+ children: "New chat"
3747
+ }
3748
+ )
3749
+ ]
3750
+ }
3751
+ ) : null,
3405
3752
  /* @__PURE__ */ jsx("div", { className: "border-t border-border bg-background/85 px-4 pb-[max(1rem,env(safe-area-inset-bottom))] pt-3 backdrop-blur-md", children: /* @__PURE__ */ jsxs(
3406
3753
  "form",
3407
3754
  {
@@ -3413,6 +3760,51 @@ function ExplorePage({
3413
3760
  },
3414
3761
  children: [
3415
3762
  error ? /* @__PURE__ */ jsx(ErrorBanner, { error, onRetry: retry, onReset: reset }) : null,
3763
+ staged.length > 0 ? /* @__PURE__ */ jsx(
3764
+ "ul",
3765
+ {
3766
+ "data-boff-explore": "attachments",
3767
+ className: "mb-2 flex flex-wrap gap-1.5",
3768
+ "aria-label": T.attach,
3769
+ children: staged.map((entry) => /* @__PURE__ */ jsxs(
3770
+ "li",
3771
+ {
3772
+ "data-boff-explore": "attachment",
3773
+ "data-text-extracted": entry.textExtracted ? "true" : "false",
3774
+ className: "flex max-w-full items-center gap-1.5 rounded-full border border-border bg-muted/60 py-1 pl-2.5 pr-1 text-xs",
3775
+ children: [
3776
+ /* @__PURE__ */ jsx(Paperclip, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
3777
+ /* @__PURE__ */ jsx("span", { className: "truncate font-medium", title: entry.name, children: entry.name }),
3778
+ /* @__PURE__ */ jsxs("span", { className: "shrink-0 text-muted-foreground", children: [
3779
+ formatBytes(entry.size),
3780
+ entry.textExtracted ? "" : ` \xB7 ${T.attachmentNameOnly}`
3781
+ ] }),
3782
+ /* @__PURE__ */ jsx(
3783
+ "button",
3784
+ {
3785
+ type: "button",
3786
+ "data-boff-explore": "attachment-remove",
3787
+ onClick: () => removeAttachment(entry.id),
3788
+ "aria-label": fill(T.removeAttachment, { name: entry.name }),
3789
+ className: "ml-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-background hover:text-foreground",
3790
+ children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
3791
+ }
3792
+ )
3793
+ ]
3794
+ },
3795
+ entry.id
3796
+ ))
3797
+ }
3798
+ ) : null,
3799
+ attachmentNotice !== null ? /* @__PURE__ */ jsx(
3800
+ "p",
3801
+ {
3802
+ "data-boff-explore": "attachment-notice",
3803
+ role: "status",
3804
+ className: "mb-2 text-xs text-destructive",
3805
+ children: attachmentNotice
3806
+ }
3807
+ ) : null,
3416
3808
  /* @__PURE__ */ jsxs(
3417
3809
  "div",
3418
3810
  {
@@ -3441,7 +3833,37 @@ function ExplorePage({
3441
3833
  placeholder: composerDisabled ? phase === "limited" ? "Message limit reached \u2014 please try again later" : "Explore is unavailable right now" : focusedProductName ? fill(T.placeholderProduct, {
3442
3834
  product: focusedProductName
3443
3835
  }) : T.placeholder,
3444
- className: "max-h-[200px] min-h-[44px] w-full min-w-0 flex-1 resize-none border-0 bg-transparent px-2 py-2.5 text-sm shadow-none focus-visible:ring-0 md:text-sm"
3836
+ className: "max-h-[200px] min-h-[44px] w-full min-w-0 flex-1 resize-none border-0 bg-transparent px-2 py-2.5 text-base shadow-none focus-visible:ring-0 sm:text-sm"
3837
+ }
3838
+ ),
3839
+ /* @__PURE__ */ jsx(
3840
+ "input",
3841
+ {
3842
+ ref: fileInputRef,
3843
+ type: "file",
3844
+ multiple: true,
3845
+ className: "hidden",
3846
+ onChange: (event) => {
3847
+ void handleFilesPicked(event.target.files);
3848
+ event.target.value = "";
3849
+ }
3850
+ }
3851
+ ),
3852
+ /* @__PURE__ */ jsx(
3853
+ Button,
3854
+ {
3855
+ "data-boff-explore": "attach",
3856
+ type: "button",
3857
+ size: "icon",
3858
+ variant: "ghost",
3859
+ disabled: composerDisabled || staged.length >= MAX_ATTACHMENTS,
3860
+ "aria-label": T.attach,
3861
+ title: `${T.attach} \u2014 ${fill(T.attachHint, {
3862
+ max: String(MAX_ATTACHMENTS),
3863
+ size: String(MAX_ATTACHMENT_BYTES / (1024 * 1024))
3864
+ })}`,
3865
+ onClick: () => fileInputRef.current?.click(),
3866
+ children: /* @__PURE__ */ jsx(Paperclip, { className: "h-4 w-4" })
3445
3867
  }
3446
3868
  ),
3447
3869
  speech.supported ? /* @__PURE__ */ jsx(