@burdenoff/website-sdk 2026.824.2 → 2026.828.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +88 -0
  2. package/dist/client/index.d.mts +2 -3
  3. package/dist/client/index.d.ts +2 -3
  4. package/dist/client/index.js +13 -7
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/client/index.mjs +13 -7
  7. package/dist/client/index.mjs.map +1 -1
  8. package/dist/components/contact.d.mts +2 -2
  9. package/dist/components/contact.d.ts +2 -2
  10. package/dist/components/contact.js +13 -7
  11. package/dist/components/contact.js.map +1 -1
  12. package/dist/components/contact.mjs +13 -7
  13. package/dist/components/contact.mjs.map +1 -1
  14. package/dist/components/electron-download.d.mts +2 -2
  15. package/dist/components/electron-download.d.ts +2 -2
  16. package/dist/components/explore-cta.d.mts +26 -0
  17. package/dist/components/explore-cta.d.ts +26 -0
  18. package/dist/components/explore-cta.js +95 -0
  19. package/dist/components/explore-cta.js.map +1 -0
  20. package/dist/components/explore-cta.mjs +93 -0
  21. package/dist/components/explore-cta.mjs.map +1 -0
  22. package/dist/components/explore.d.mts +43 -0
  23. package/dist/components/explore.d.ts +43 -0
  24. package/dist/components/explore.js +2219 -0
  25. package/dist/components/explore.js.map +1 -0
  26. package/dist/components/explore.mjs +2193 -0
  27. package/dist/components/explore.mjs.map +1 -0
  28. package/dist/components/launch-countdown.d.mts +2 -2
  29. package/dist/components/launch-countdown.d.ts +2 -2
  30. package/dist/components/newsletter.d.mts +3 -3
  31. package/dist/components/newsletter.d.ts +3 -3
  32. package/dist/components/newsletter.js +13 -7
  33. package/dist/components/newsletter.js.map +1 -1
  34. package/dist/components/newsletter.mjs +13 -7
  35. package/dist/components/newsletter.mjs.map +1 -1
  36. package/dist/components/partners.d.mts +2 -2
  37. package/dist/components/partners.d.ts +2 -2
  38. package/dist/components/partners.js +13 -7
  39. package/dist/components/partners.js.map +1 -1
  40. package/dist/components/partners.mjs +13 -7
  41. package/dist/components/partners.mjs.map +1 -1
  42. package/dist/components/pricing.d.mts +3 -3
  43. package/dist/components/pricing.d.ts +3 -3
  44. package/dist/components/pricing.js +13 -7
  45. package/dist/components/pricing.js.map +1 -1
  46. package/dist/components/pricing.mjs +13 -7
  47. package/dist/components/pricing.mjs.map +1 -1
  48. package/dist/components/problems-solved.d.mts +2 -2
  49. package/dist/components/problems-solved.d.ts +2 -2
  50. package/dist/components/resource-links.d.mts +2 -2
  51. package/dist/components/resource-links.d.ts +2 -2
  52. package/dist/components/resource-links.js +13 -7
  53. package/dist/components/resource-links.js.map +1 -1
  54. package/dist/components/resource-links.mjs +13 -7
  55. package/dist/components/resource-links.mjs.map +1 -1
  56. package/dist/components/social-links.d.mts +2 -2
  57. package/dist/components/social-links.d.ts +2 -2
  58. package/dist/components/social-links.js +13 -7
  59. package/dist/components/social-links.js.map +1 -1
  60. package/dist/components/social-links.mjs +13 -7
  61. package/dist/components/social-links.mjs.map +1 -1
  62. package/dist/components/unsubscribe.d.mts +2 -2
  63. package/dist/components/unsubscribe.d.ts +2 -2
  64. package/dist/components/unsubscribe.js +13 -7
  65. package/dist/components/unsubscribe.js.map +1 -1
  66. package/dist/components/unsubscribe.mjs +13 -7
  67. package/dist/components/unsubscribe.mjs.map +1 -1
  68. package/dist/components/waitlist.d.mts +3 -3
  69. package/dist/components/waitlist.d.ts +3 -3
  70. package/dist/components/waitlist.js +13 -7
  71. package/dist/components/waitlist.js.map +1 -1
  72. package/dist/components/waitlist.mjs +13 -7
  73. package/dist/components/waitlist.mjs.map +1 -1
  74. package/dist/components/website-switcher.d.mts +2 -2
  75. package/dist/components/website-switcher.d.ts +2 -2
  76. package/dist/content/index.d.mts +2 -3
  77. package/dist/content/index.d.ts +2 -3
  78. package/dist/content/index.js +13 -7
  79. package/dist/content/index.js.map +1 -1
  80. package/dist/content/index.mjs +13 -7
  81. package/dist/content/index.mjs.map +1 -1
  82. package/dist/{graphql-client-Cv6TO6hn.d.mts → graphql-client-BXn6RYV0.d.mts} +31 -4
  83. package/dist/{graphql-client-Cv6TO6hn.d.ts → graphql-client-BXn6RYV0.d.ts} +31 -4
  84. package/dist/hooks/index.d.mts +1 -0
  85. package/dist/hooks/index.d.ts +1 -0
  86. package/dist/hooks/index.js +1145 -0
  87. package/dist/hooks/index.js.map +1 -0
  88. package/dist/hooks/index.mjs +1140 -0
  89. package/dist/hooks/index.mjs.map +1 -0
  90. package/dist/{index-CFsUFxYo.d.ts → index-BXg7cQLm.d.ts} +11 -11
  91. package/dist/index-CUJTTlYx.d.mts +244 -0
  92. package/dist/index-CUJTTlYx.d.ts +244 -0
  93. package/dist/{index-CuMRiKGB.d.mts → index-DYxQiEm5.d.mts} +11 -11
  94. package/dist/index.d.mts +17 -15
  95. package/dist/index.d.ts +17 -15
  96. package/dist/index.js +1987 -426
  97. package/dist/index.js.map +1 -1
  98. package/dist/index.mjs +1977 -428
  99. package/dist/index.mjs.map +1 -1
  100. package/dist/product/index.d.mts +2 -2
  101. package/dist/product/index.d.ts +2 -2
  102. package/dist/product/index.js +13 -7
  103. package/dist/product/index.js.map +1 -1
  104. package/dist/product/index.mjs +13 -7
  105. package/dist/product/index.mjs.map +1 -1
  106. package/dist/{product-card-DL9O3Ez9.d.mts → product-card-CGxETsFa.d.mts} +7 -7
  107. package/dist/{product-card-DL9O3Ez9.d.ts → product-card-CGxETsFa.d.ts} +7 -7
  108. package/dist/{provider-D-Do5PmD.d.ts → provider-CUfI8meE.d.mts} +3 -3
  109. package/dist/{provider-BhsbxVPo.d.mts → provider-D_uUM7_e.d.ts} +3 -3
  110. package/dist/seo/index.d.mts +2 -2
  111. package/dist/seo/index.d.ts +2 -2
  112. package/dist/store/index.d.mts +3 -3
  113. package/dist/store/index.d.ts +3 -3
  114. package/dist/store/index.js +13 -7
  115. package/dist/store/index.js.map +1 -1
  116. package/dist/store/index.mjs +13 -7
  117. package/dist/store/index.mjs.map +1 -1
  118. package/dist/utils/index.d.mts +49 -1
  119. package/dist/utils/index.d.ts +49 -1
  120. package/dist/utils/index.js +70 -0
  121. package/dist/utils/index.js.map +1 -1
  122. package/dist/utils/index.mjs +68 -1
  123. package/dist/utils/index.mjs.map +1 -1
  124. package/package.json +40 -30
@@ -0,0 +1,1145 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ require('react/jsx-runtime');
5
+
6
+ var __defProp = Object.defineProperty;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
9
+
10
+ // src/client/security.ts
11
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
12
+ var RESERVED_HEADERS = /* @__PURE__ */ new Set([
13
+ "authorization",
14
+ "cookie",
15
+ "x-actor-id",
16
+ "x-actor-type",
17
+ "x-consent-granted-by",
18
+ "x-consent-scope",
19
+ "x-consent-granted-at",
20
+ "x-consent-expires-at"
21
+ ]);
22
+ function validateGatewayUrl(value) {
23
+ let url;
24
+ try {
25
+ url = new URL(value);
26
+ } catch {
27
+ throw new Error("Gateway URL must be a valid absolute URL");
28
+ }
29
+ if (url.protocol === "https:") {
30
+ return url.toString();
31
+ }
32
+ if (url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname.toLowerCase())) {
33
+ return url.toString();
34
+ }
35
+ throw new Error("Gateway URL must use HTTPS unless it targets localhost");
36
+ }
37
+ function sanitizeHeaders(headers) {
38
+ if (!headers) return void 0;
39
+ return Object.fromEntries(
40
+ Object.entries(headers).filter(
41
+ ([key]) => !RESERVED_HEADERS.has(key.toLowerCase())
42
+ )
43
+ );
44
+ }
45
+
46
+ // src/client/graphql-client.ts
47
+ var _WebSDKClient = class _WebSDKClient {
48
+ constructor(config) {
49
+ __publicField(this, "config");
50
+ __publicField(this, "queryCache", /* @__PURE__ */ new Map());
51
+ __publicField(this, "inflight", /* @__PURE__ */ new Map());
52
+ this.config = {
53
+ ...config,
54
+ gatewayUrl: validateGatewayUrl(config.gatewayUrl),
55
+ headers: sanitizeHeaders(config.headers)
56
+ };
57
+ }
58
+ getConfig() {
59
+ return this.config;
60
+ }
61
+ /**
62
+ * Execute a GraphQL query (with deduplication and caching).
63
+ *
64
+ * Pass `{ cache: false }` to bypass the cache entirely — required for poll
65
+ * loops, where every tick must reach the network.
66
+ */
67
+ async query(query, variables, options) {
68
+ if (options?.cache === false) {
69
+ return this.request(query, variables, true, options.timeoutMs);
70
+ }
71
+ const cacheKey = JSON.stringify({ query, variables });
72
+ const cached = this.queryCache.get(cacheKey);
73
+ if (cached) {
74
+ if (Date.now() - cached.ts < _WebSDKClient.CACHE_TTL) {
75
+ return cached.data;
76
+ }
77
+ this.queryCache.delete(cacheKey);
78
+ }
79
+ const existing = this.inflight.get(cacheKey);
80
+ if (existing) {
81
+ return existing;
82
+ }
83
+ const promise = this.request(query, variables, true, options?.timeoutMs).then((result) => {
84
+ if (result.data && !result.errors?.length) {
85
+ if (this.queryCache.size >= _WebSDKClient.MAX_CACHE_ENTRIES) {
86
+ const oldestEntry = this.queryCache.keys().next().value;
87
+ if (oldestEntry) {
88
+ this.queryCache.delete(oldestEntry);
89
+ }
90
+ }
91
+ this.queryCache.set(cacheKey, { data: result, ts: Date.now() });
92
+ }
93
+ return result;
94
+ }).finally(() => {
95
+ this.inflight.delete(cacheKey);
96
+ });
97
+ this.inflight.set(cacheKey, promise);
98
+ return promise;
99
+ }
100
+ /**
101
+ * Execute a GraphQL mutation
102
+ */
103
+ async mutate(mutation, variables, options) {
104
+ return this.request(mutation, variables, false, options?.timeoutMs);
105
+ }
106
+ /** HTTP statuses worth another attempt — transient server/edge conditions. */
107
+ static isRetriableStatus(status) {
108
+ return status === 429 || status === 408 || status >= 500 && status < 600;
109
+ }
110
+ static sleep(ms) {
111
+ return new Promise((resolve) => setTimeout(resolve, ms));
112
+ }
113
+ /**
114
+ * Perform the HTTP call with a hard timeout. Every outcome resolves to a
115
+ * GraphQLResponse — callers never hang, so a UI's `loading` state always
116
+ * settles into data or a renderable error.
117
+ */
118
+ async attempt(query, variables, timeoutMs) {
119
+ const controller = new AbortController();
120
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
121
+ try {
122
+ const response = await fetch(this.config.gatewayUrl, {
123
+ method: "POST",
124
+ headers: {
125
+ "Content-Type": "application/json",
126
+ "x-product-id": this.config.productId,
127
+ ...this.config.headers
128
+ },
129
+ body: JSON.stringify({ query, variables }),
130
+ signal: controller.signal
131
+ });
132
+ if (!response.ok) {
133
+ console.error(
134
+ "[WebSDK] GraphQL request failed",
135
+ response.status,
136
+ response.statusText
137
+ );
138
+ return {
139
+ res: {
140
+ errors: [
141
+ {
142
+ message: `HTTP ${response.status}: ${response.statusText}`,
143
+ extensions: { code: `HTTP_${response.status}` }
144
+ }
145
+ ]
146
+ },
147
+ retriable: _WebSDKClient.isRetriableStatus(response.status)
148
+ };
149
+ }
150
+ return {
151
+ res: await response.json(),
152
+ retriable: false
153
+ };
154
+ } catch (error) {
155
+ const aborted = error instanceof Error && (error.name === "AbortError" || controller.signal.aborted);
156
+ const message = aborted ? `Request timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : "Network error";
157
+ console.error("[WebSDK] Network error", message);
158
+ return {
159
+ res: {
160
+ errors: [
161
+ {
162
+ message,
163
+ extensions: { code: aborted ? "TIMEOUT" : "NETWORK_ERROR" }
164
+ }
165
+ ]
166
+ },
167
+ // Transport-level failures are worth one more try; a genuine outage
168
+ // just fails twice quickly and still renders an error state.
169
+ retriable: true
170
+ };
171
+ } finally {
172
+ clearTimeout(timer);
173
+ }
174
+ }
175
+ async request(query, variables, retry = true, timeoutOverrideMs) {
176
+ const timeoutMs = timeoutOverrideMs ?? this.config.timeoutMs ?? _WebSDKClient.DEFAULT_TIMEOUT_MS;
177
+ const maxRetries = retry ? this.config.maxRetries ?? _WebSDKClient.DEFAULT_MAX_RETRIES : 0;
178
+ let last;
179
+ for (let i = 0; i <= maxRetries; i++) {
180
+ const { res, retriable } = await this.attempt(
181
+ query,
182
+ variables,
183
+ timeoutMs
184
+ );
185
+ if (!retriable || i === maxRetries) return res;
186
+ last = res;
187
+ await _WebSDKClient.sleep(
188
+ _WebSDKClient.RETRY_BASE_DELAY_MS * (i + 1) + Math.random() * 200
189
+ );
190
+ }
191
+ return last;
192
+ }
193
+ };
194
+ __publicField(_WebSDKClient, "CACHE_TTL", 5 * 60 * 1e3);
195
+ // 5 minutes
196
+ __publicField(_WebSDKClient, "MAX_CACHE_ENTRIES", 100);
197
+ __publicField(_WebSDKClient, "DEFAULT_TIMEOUT_MS", 12e3);
198
+ __publicField(_WebSDKClient, "DEFAULT_MAX_RETRIES", 1);
199
+ __publicField(_WebSDKClient, "RETRY_BASE_DELAY_MS", 400);
200
+ var WebSDKContext = react.createContext(null);
201
+ function useWebSDK() {
202
+ const client = react.useContext(WebSDKContext);
203
+ if (!client) {
204
+ throw new Error("useWebSDK must be used within a WebSDKProvider");
205
+ }
206
+ return client;
207
+ }
208
+
209
+ // src/components/explore-types.ts
210
+ var EXPLORE_MESSAGE_FIELDS = (
211
+ /* GraphQL */
212
+ `
213
+ id
214
+ conversationId
215
+ role
216
+ status
217
+ content
218
+ partialContent
219
+ activity
220
+ references {
221
+ url
222
+ title
223
+ description
224
+ product
225
+ imageUrl
226
+ }
227
+ ctas {
228
+ kind
229
+ label
230
+ url
231
+ product
232
+ }
233
+ errorCode
234
+ createdAt
235
+ completedAt
236
+ `
237
+ );
238
+ var EXPLORE_CATALOG_QUERY = (
239
+ /* GraphQL */
240
+ `
241
+ query ExploreCatalog($productSlug: String) {
242
+ exploreCatalog(productSlug: $productSlug) {
243
+ enabled
244
+ products {
245
+ slug
246
+ name
247
+ tagline
248
+ website
249
+ }
250
+ examplePrompts
251
+ welcomeTitle
252
+ welcomeBody
253
+ limits {
254
+ maxMessageLength
255
+ maxTurnsPerConversation
256
+ messagesPerDay
257
+ historyTurns
258
+ }
259
+ captchaRequired
260
+ recaptchaSiteKey
261
+ }
262
+ }
263
+ `
264
+ );
265
+ var EXPLORE_MESSAGE_QUERY = (
266
+ /* GraphQL */
267
+ `
268
+ query ExploreMessage($conversationToken: String!, $id: ID!) {
269
+ exploreMessage(conversationToken: $conversationToken, id: $id) {
270
+ ${EXPLORE_MESSAGE_FIELDS}
271
+ }
272
+ }
273
+ `
274
+ );
275
+ var SEND_EXPLORE_MESSAGE_MUTATION = (
276
+ /* GraphQL */
277
+ `
278
+ mutation SendExploreMessage($input: SendExploreMessageInput!) {
279
+ sendExploreMessage(input: $input) {
280
+ conversation {
281
+ id
282
+ token
283
+ productSlug
284
+ turnCount
285
+ createdAt
286
+ }
287
+ userMessage {
288
+ ${EXPLORE_MESSAGE_FIELDS}
289
+ }
290
+ assistantMessage {
291
+ ${EXPLORE_MESSAGE_FIELDS}
292
+ }
293
+ remainingToday
294
+ }
295
+ }
296
+ `
297
+ );
298
+
299
+ // src/data/products.ts
300
+ var ECOSYSTEM_PRODUCTS = [
301
+ {
302
+ slug: "workspaces",
303
+ name: "Workspaces",
304
+ websiteUrl: "https://burdenoff.com"
305
+ },
306
+ {
307
+ slug: "vibecontrols",
308
+ name: "VibeControls",
309
+ websiteUrl: "https://vibecontrols.com"
310
+ },
311
+ {
312
+ slug: "fluidgrids",
313
+ name: "FluidGrids",
314
+ websiteUrl: "https://fluidgrids.com"
315
+ },
316
+ // Botlit ships on botlit.ai — botlit.com does not resolve.
317
+ { slug: "botlit", name: "Botlit", websiteUrl: "https://www.botlit.ai" },
318
+ {
319
+ slug: "bigconsole",
320
+ name: "BigConsole",
321
+ websiteUrl: "https://bigconsole.com"
322
+ },
323
+ {
324
+ slug: "semanticfed",
325
+ name: "SemanticFed",
326
+ websiteUrl: "https://semanticfed.com"
327
+ },
328
+ {
329
+ slug: "intelwatchtower",
330
+ name: "IntelWatchtower",
331
+ websiteUrl: "https://intelwatchtower.com"
332
+ },
333
+ { slug: "buildmyiq", name: "BuildMyIQ", websiteUrl: "https://buildmyiq.com" },
334
+ {
335
+ slug: "assethandler",
336
+ name: "AssetHandler",
337
+ websiteUrl: "https://assethandler.com"
338
+ },
339
+ {
340
+ slug: "crewfoundry",
341
+ name: "CrewFoundry",
342
+ websiteUrl: "https://crewfoundry.com"
343
+ },
344
+ {
345
+ slug: "planmagnet",
346
+ name: "PlanMagnet",
347
+ websiteUrl: "https://planmagnet.com"
348
+ },
349
+ {
350
+ slug: "eventfullymanaged",
351
+ name: "EventfullyManaged",
352
+ websiteUrl: "https://eventfullymanaged.com"
353
+ },
354
+ {
355
+ slug: "headshotmarketing",
356
+ name: "HeadshotMarketing",
357
+ websiteUrl: "https://headshotmarketing.com"
358
+ },
359
+ {
360
+ slug: "technospam",
361
+ name: "TechnoSpam",
362
+ websiteUrl: "https://technospam.com"
363
+ },
364
+ { slug: "kadaikodi", name: "Kadaikodi", websiteUrl: "https://kadaikodi.com" },
365
+ {
366
+ slug: "movethewheels",
367
+ name: "MoveTheWheels",
368
+ websiteUrl: "https://movethewheels.com"
369
+ },
370
+ {
371
+ slug: "healthybowl",
372
+ name: "Healthy Bowl",
373
+ websiteUrl: "https://gethealthybowl.com"
374
+ },
375
+ { slug: "ggnomad", name: "GGNomad", websiteUrl: "https://ggnomad.com" },
376
+ {
377
+ slug: "timecampus",
378
+ name: "TimeCampus",
379
+ websiteUrl: "https://timecampus.com"
380
+ },
381
+ {
382
+ slug: "manufacturedops",
383
+ name: "ManufacturedOps",
384
+ websiteUrl: "https://manufacturedops.com"
385
+ },
386
+ {
387
+ slug: "theglobalfuel",
388
+ name: "TheGlobalFuel",
389
+ websiteUrl: "https://theglobalfuel.com"
390
+ },
391
+ {
392
+ slug: "housingvista",
393
+ name: "HousingVista",
394
+ websiteUrl: "https://housingvista.com"
395
+ },
396
+ {
397
+ slug: "proserviceworld",
398
+ name: "ProServiceWorld",
399
+ websiteUrl: "https://proserviceworld.com"
400
+ },
401
+ {
402
+ slug: "mybodyshield",
403
+ name: "MyBodyShield",
404
+ websiteUrl: "https://mybodyshield.com"
405
+ },
406
+ {
407
+ slug: "artistrybase",
408
+ name: "ArtistryBase",
409
+ websiteUrl: "https://artistrybase.com"
410
+ },
411
+ {
412
+ slug: "theculturepost",
413
+ name: "TheCulturePost",
414
+ websiteUrl: "https://theculturepost.com"
415
+ },
416
+ { slug: "collabkin", name: "CollabKin", websiteUrl: "https://collabkin.com" },
417
+ {
418
+ slug: "comradecircle",
419
+ name: "ComradeCircle",
420
+ websiteUrl: "https://comradecircle.com"
421
+ },
422
+ {
423
+ slug: "brainyrich",
424
+ name: "BrainyRich",
425
+ websiteUrl: "https://brainyrich.com"
426
+ },
427
+ {
428
+ slug: "cosmicintersection",
429
+ name: "CosmicIntersection",
430
+ websiteUrl: "https://cosmicintersection.com"
431
+ },
432
+ {
433
+ slug: "govcitizenhub",
434
+ name: "GovCitizenHub",
435
+ websiteUrl: "https://govcitizenhub.com"
436
+ },
437
+ {
438
+ slug: "adaptercloud",
439
+ name: "AdapterCloud",
440
+ websiteUrl: "https://adaptercloud.com"
441
+ },
442
+ {
443
+ slug: "subscriberbot",
444
+ name: "Subscriber Bot",
445
+ websiteUrl: "https://subscriberbot.com"
446
+ },
447
+ {
448
+ slug: "hookmovies",
449
+ name: "HookMovies",
450
+ websiteUrl: "https://hookmovies.com"
451
+ },
452
+ {
453
+ slug: "coolandlovely",
454
+ name: "CoolAndLovely",
455
+ websiteUrl: "https://coolandlovely.com"
456
+ },
457
+ {
458
+ slug: "atheletebridge",
459
+ name: "AthleteBridge",
460
+ websiteUrl: "https://atheletebridge.com"
461
+ },
462
+ {
463
+ slug: "labsofscience",
464
+ name: "LabsOfScience",
465
+ websiteUrl: "https://labsofscience.com"
466
+ },
467
+ {
468
+ slug: "ecoimpacthub",
469
+ name: "EcoImpactHub",
470
+ websiteUrl: "https://ecoimpacthub.com"
471
+ }
472
+ ];
473
+
474
+ // src/utils/recaptcha-v3.ts
475
+ var SCRIPT_ID = "boff-recaptcha-v3";
476
+ var SCRIPT_SRC = "https://www.google.com/recaptcha/api.js?render=";
477
+ var EXPLORE_RECAPTCHA_ACTION = "explore_chat";
478
+ var loaders = /* @__PURE__ */ new Map();
479
+ function isBrowser() {
480
+ return typeof window !== "undefined" && typeof document !== "undefined";
481
+ }
482
+ function loadRecaptchaV3(siteKey) {
483
+ if (!siteKey) {
484
+ return Promise.reject(new Error("reCAPTCHA site key is required"));
485
+ }
486
+ if (!isBrowser()) {
487
+ return Promise.reject(
488
+ new Error("reCAPTCHA v3 can only be loaded in a browser")
489
+ );
490
+ }
491
+ const existing = loaders.get(siteKey);
492
+ if (existing) return existing;
493
+ const promise = new Promise((resolve, reject) => {
494
+ const ready = () => {
495
+ const api = window.grecaptcha;
496
+ if (!api) {
497
+ reject(new Error("reCAPTCHA loaded but grecaptcha is unavailable"));
498
+ return;
499
+ }
500
+ api.ready(() => resolve(api));
501
+ };
502
+ if (window.grecaptcha) {
503
+ ready();
504
+ return;
505
+ }
506
+ const scriptId = `${SCRIPT_ID}-${siteKey}`;
507
+ const found = document.getElementById(scriptId);
508
+ if (found) {
509
+ found.addEventListener("load", ready, { once: true });
510
+ found.addEventListener(
511
+ "error",
512
+ () => reject(new Error("Failed to load reCAPTCHA v3")),
513
+ { once: true }
514
+ );
515
+ return;
516
+ }
517
+ const script = document.createElement("script");
518
+ script.id = scriptId;
519
+ script.src = `${SCRIPT_SRC}${encodeURIComponent(siteKey)}`;
520
+ script.async = true;
521
+ script.defer = true;
522
+ script.addEventListener("load", ready, { once: true });
523
+ script.addEventListener(
524
+ "error",
525
+ () => {
526
+ loaders.delete(siteKey);
527
+ reject(new Error("Failed to load reCAPTCHA v3"));
528
+ },
529
+ { once: true }
530
+ );
531
+ document.head.appendChild(script);
532
+ });
533
+ loaders.set(siteKey, promise);
534
+ return promise;
535
+ }
536
+ async function getRecaptchaV3Token(siteKey, action = EXPLORE_RECAPTCHA_ACTION) {
537
+ const api = await loadRecaptchaV3(siteKey);
538
+ return api.execute(siteKey, { action });
539
+ }
540
+
541
+ // src/hooks/use-explore-chat.ts
542
+ var DEFAULT_STORAGE_KEY = "boff.explore.v1";
543
+ var DEFAULT_POLL_INTERVAL_MS = 1500;
544
+ var DEFAULT_POLL_TIMEOUT_MS = 9e4;
545
+ var POLL_REQUEST_TIMEOUT_MS = 1e4;
546
+ var SEND_REQUEST_TIMEOUT_MS = 18e3;
547
+ var MAX_CONSECUTIVE_POLL_FAILURES = 4;
548
+ var MAX_PERSISTED_MESSAGES = 40;
549
+ var FALLBACK_EXPLORE_LIMITS = {
550
+ maxMessageLength: 2e3,
551
+ maxTurnsPerConversation: 20,
552
+ messagesPerDay: 100,
553
+ historyTurns: 6
554
+ };
555
+ var DEFAULT_EXPLORE_EXAMPLE_PROMPTS = [
556
+ "What does this product actually do?",
557
+ "Which Burdenoff product fits my team?",
558
+ "How do I get started, step by step?",
559
+ "What does it integrate with?"
560
+ ];
561
+ var FALLBACK_WELCOME_TITLE = "Ask anything about our products";
562
+ var FALLBACK_WELCOME_BODY = "Get straight answers drawn from our product documentation and public pages \u2014 with links back to the exact page each answer came from.";
563
+ function buildFallbackCatalog(examplePrompts) {
564
+ return {
565
+ enabled: true,
566
+ products: ECOSYSTEM_PRODUCTS.map((p) => ({
567
+ slug: p.slug,
568
+ name: p.name,
569
+ tagline: null,
570
+ website: p.websiteUrl
571
+ })),
572
+ examplePrompts: examplePrompts?.length ? examplePrompts : DEFAULT_EXPLORE_EXAMPLE_PROMPTS,
573
+ welcomeTitle: FALLBACK_WELCOME_TITLE,
574
+ welcomeBody: FALLBACK_WELCOME_BODY,
575
+ limits: FALLBACK_EXPLORE_LIMITS,
576
+ captchaRequired: false,
577
+ recaptchaSiteKey: null
578
+ };
579
+ }
580
+ function normalizeCatalog(raw, fallback) {
581
+ return {
582
+ enabled: raw.enabled !== false,
583
+ products: raw.products?.length ? raw.products : fallback.products,
584
+ examplePrompts: raw.examplePrompts?.length ? raw.examplePrompts : fallback.examplePrompts,
585
+ welcomeTitle: raw.welcomeTitle || fallback.welcomeTitle,
586
+ welcomeBody: raw.welcomeBody || fallback.welcomeBody,
587
+ limits: {
588
+ maxMessageLength: raw.limits?.maxMessageLength || fallback.limits.maxMessageLength,
589
+ maxTurnsPerConversation: raw.limits?.maxTurnsPerConversation || fallback.limits.maxTurnsPerConversation,
590
+ messagesPerDay: raw.limits?.messagesPerDay || fallback.limits.messagesPerDay,
591
+ historyTurns: raw.limits?.historyTurns || fallback.limits.historyTurns
592
+ },
593
+ captchaRequired: raw.captchaRequired === true,
594
+ recaptchaSiteKey: raw.recaptchaSiteKey ?? null
595
+ };
596
+ }
597
+ var RATE_LIMIT_CODES = /* @__PURE__ */ new Set([
598
+ "RATE_LIMITED",
599
+ "HTTP_429",
600
+ "EXPLORE_GLOBAL_LIMIT"
601
+ ]);
602
+ var CAPTCHA_CODES = /* @__PURE__ */ new Set(["CAPTCHA_FAILED", "CAPTCHA_REQUIRED"]);
603
+ var NETWORK_CODES = /* @__PURE__ */ new Set([
604
+ "NETWORK_ERROR",
605
+ "UPSTREAM_UNAVAILABLE",
606
+ "CONVERSATION_BUSY",
607
+ "PROVIDER_ERROR",
608
+ "QUEUE_FULL"
609
+ ]);
610
+ var VALIDATION_CODES = /* @__PURE__ */ new Set([
611
+ "MESSAGE_TOO_LONG",
612
+ "MESSAGE_EMPTY",
613
+ "CONVERSATION_LIMIT",
614
+ "INVALID_INPUT"
615
+ ]);
616
+ function classifyExploreError(code, serverMessage) {
617
+ const c = code ?? "UNKNOWN";
618
+ if (RATE_LIMIT_CODES.has(c)) {
619
+ return {
620
+ kind: "rate-limit",
621
+ code: c,
622
+ message: "You've reached the message limit for now. Please try again later.",
623
+ retryable: false
624
+ };
625
+ }
626
+ if (CAPTCHA_CODES.has(c)) {
627
+ return {
628
+ kind: "captcha",
629
+ code: c,
630
+ message: "We couldn't verify that you're human. Please try sending that again.",
631
+ retryable: true
632
+ };
633
+ }
634
+ if (c === "TIMEOUT" || c === "TOOL_BUDGET") {
635
+ return {
636
+ kind: "timeout",
637
+ code: c,
638
+ message: "That answer took too long to come back. Try asking again.",
639
+ retryable: true
640
+ };
641
+ }
642
+ if (NETWORK_CODES.has(c)) {
643
+ return {
644
+ kind: "network",
645
+ code: c,
646
+ message: "We couldn't reach the assistant just now. Check your connection and try again.",
647
+ retryable: true
648
+ };
649
+ }
650
+ if (c === "EXPLORE_DISABLED") {
651
+ return {
652
+ kind: "disabled",
653
+ code: c,
654
+ message: "Explore is unavailable at the moment. Please check back soon.",
655
+ retryable: false
656
+ };
657
+ }
658
+ if (VALIDATION_CODES.has(c)) {
659
+ return {
660
+ kind: "validation",
661
+ code: c,
662
+ message: serverMessage || "That message couldn't be sent.",
663
+ retryable: false
664
+ };
665
+ }
666
+ if (c === "EMPTY_ANSWER") {
667
+ return {
668
+ kind: "unknown",
669
+ code: c,
670
+ message: "No answer came back for that one. Try rephrasing, or ask something more specific.",
671
+ retryable: true
672
+ };
673
+ }
674
+ return {
675
+ kind: "unknown",
676
+ code: c,
677
+ message: serverMessage || "Something went wrong. Please try again.",
678
+ retryable: true
679
+ };
680
+ }
681
+ function readPersisted(key) {
682
+ if (typeof window === "undefined") return null;
683
+ try {
684
+ const raw = window.sessionStorage.getItem(key);
685
+ if (!raw) return null;
686
+ const parsed = JSON.parse(raw);
687
+ if (!parsed || typeof parsed !== "object") return null;
688
+ const record = parsed;
689
+ return {
690
+ conversationToken: typeof record.conversationToken === "string" ? record.conversationToken : null,
691
+ messages: Array.isArray(record.messages) ? record.messages.slice(-MAX_PERSISTED_MESSAGES) : [],
692
+ focusProduct: typeof record.focusProduct === "string" ? record.focusProduct : null
693
+ };
694
+ } catch {
695
+ return null;
696
+ }
697
+ }
698
+ function visibleText(message) {
699
+ return message.content || message.partialContent || "";
700
+ }
701
+ function countUserTurns(messages) {
702
+ return messages.filter((m) => m.role === "USER").length;
703
+ }
704
+ function useExploreChat(options = {}) {
705
+ const {
706
+ productSlug,
707
+ pageUrl,
708
+ locale,
709
+ examplePrompts,
710
+ pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
711
+ pollTimeoutMs = DEFAULT_POLL_TIMEOUT_MS,
712
+ storageKey = DEFAULT_STORAGE_KEY,
713
+ persist = true,
714
+ onSend
715
+ } = options;
716
+ const client = useWebSDK();
717
+ const promptKey = JSON.stringify(examplePrompts ?? []);
718
+ const fallbackCatalog = react.useMemo(
719
+ () => buildFallbackCatalog(JSON.parse(promptKey)),
720
+ [promptKey]
721
+ );
722
+ const [catalog, setCatalog] = react.useState(fallbackCatalog);
723
+ const [messages, setMessages] = react.useState([]);
724
+ const [phase, setPhase] = react.useState("loading");
725
+ const [error, setError] = react.useState(null);
726
+ const [remainingToday, setRemainingToday] = react.useState(null);
727
+ const [focusProduct, setFocusProductState] = react.useState(null);
728
+ const [turnCount, setTurnCount] = react.useState(0);
729
+ const [hydrated, setHydrated] = react.useState(false);
730
+ const mountedRef = react.useRef(true);
731
+ const pollGenerationRef = react.useRef(0);
732
+ const pollTimerRef = react.useRef(null);
733
+ const conversationTokenRef = react.useRef(null);
734
+ const catalogRef = react.useRef(fallbackCatalog);
735
+ const turnCountRef = react.useRef(0);
736
+ const focusProductRef = react.useRef(null);
737
+ const lastSentTextRef = react.useRef(null);
738
+ const inFlightRef = react.useRef(false);
739
+ catalogRef.current = catalog;
740
+ const clearPollTimer = react.useCallback(() => {
741
+ if (pollTimerRef.current !== null) {
742
+ clearTimeout(pollTimerRef.current);
743
+ pollTimerRef.current = null;
744
+ }
745
+ }, []);
746
+ const cancelPolling = react.useCallback(() => {
747
+ pollGenerationRef.current += 1;
748
+ clearPollTimer();
749
+ }, [clearPollTimer]);
750
+ react.useEffect(() => {
751
+ mountedRef.current = true;
752
+ return () => {
753
+ mountedRef.current = false;
754
+ pollGenerationRef.current += 1;
755
+ if (pollTimerRef.current !== null) {
756
+ clearTimeout(pollTimerRef.current);
757
+ pollTimerRef.current = null;
758
+ }
759
+ };
760
+ }, []);
761
+ react.useEffect(() => {
762
+ if (!persist) {
763
+ setHydrated(true);
764
+ return;
765
+ }
766
+ const stored = readPersisted(storageKey);
767
+ if (stored) {
768
+ conversationTokenRef.current = stored.conversationToken;
769
+ focusProductRef.current = stored.focusProduct;
770
+ const settled = stored.messages.flatMap((m) => {
771
+ if (m.role !== "ASSISTANT") return [m];
772
+ if (m.status !== "PENDING" && m.status !== "RUNNING") return [m];
773
+ const text = visibleText(m);
774
+ if (!text) return [];
775
+ return [{ ...m, status: "COMPLETED", content: text }];
776
+ });
777
+ setMessages(settled);
778
+ setFocusProductState(stored.focusProduct);
779
+ const turns = countUserTurns(settled);
780
+ turnCountRef.current = turns;
781
+ setTurnCount(turns);
782
+ }
783
+ setHydrated(true);
784
+ }, [persist, storageKey]);
785
+ react.useEffect(() => {
786
+ if (!persist || !hydrated || typeof window === "undefined") return;
787
+ try {
788
+ const payload = {
789
+ conversationToken: conversationTokenRef.current,
790
+ messages: messages.slice(-MAX_PERSISTED_MESSAGES),
791
+ focusProduct
792
+ };
793
+ if (!payload.conversationToken && payload.messages.length === 0 && !payload.focusProduct) {
794
+ window.sessionStorage.removeItem(storageKey);
795
+ return;
796
+ }
797
+ window.sessionStorage.setItem(storageKey, JSON.stringify(payload));
798
+ } catch {
799
+ }
800
+ }, [persist, hydrated, storageKey, messages, focusProduct]);
801
+ react.useEffect(() => {
802
+ let cancelled = false;
803
+ const load = async () => {
804
+ const res = await client.query(
805
+ EXPLORE_CATALOG_QUERY,
806
+ { productSlug: productSlug ?? null }
807
+ );
808
+ if (cancelled || !mountedRef.current) return;
809
+ const raw = res.data?.exploreCatalog;
810
+ if (res.errors?.length || !raw) {
811
+ console.warn(
812
+ "[WebSDK] exploreCatalog unavailable \u2014 using the built-in fallback catalog",
813
+ res.errors?.[0]?.message
814
+ );
815
+ setPhase((prev) => prev === "loading" ? "ready" : prev);
816
+ return;
817
+ }
818
+ const next = normalizeCatalog(raw, fallbackCatalog);
819
+ setCatalog(next);
820
+ catalogRef.current = next;
821
+ setPhase((prev) => {
822
+ if (!next.enabled) return "disabled";
823
+ return prev === "loading" ? "ready" : prev;
824
+ });
825
+ };
826
+ void load();
827
+ return () => {
828
+ cancelled = true;
829
+ };
830
+ }, [client, productSlug, fallbackCatalog]);
831
+ const startPolling = react.useCallback(
832
+ (conversationToken, messageId) => {
833
+ pollGenerationRef.current += 1;
834
+ const generation = pollGenerationRef.current;
835
+ const startedAt = Date.now();
836
+ let consecutiveFailures = 0;
837
+ const isStale = () => !mountedRef.current || generation !== pollGenerationRef.current;
838
+ const finishWithError = (chatError) => {
839
+ inFlightRef.current = false;
840
+ setMessages(
841
+ (prev) => prev.flatMap((m) => {
842
+ if (m.id !== messageId) return [m];
843
+ const text = visibleText(m);
844
+ if (!text) return [];
845
+ return [{ ...m, status: "FAILED", content: text }];
846
+ })
847
+ );
848
+ setError(chatError);
849
+ setPhase(chatError.kind === "rate-limit" ? "limited" : "error");
850
+ };
851
+ const tick = async () => {
852
+ if (isStale()) return;
853
+ if (Date.now() - startedAt > pollTimeoutMs) {
854
+ finishWithError(classifyExploreError("TIMEOUT"));
855
+ return;
856
+ }
857
+ const res = await client.query(
858
+ EXPLORE_MESSAGE_QUERY,
859
+ { conversationToken, id: messageId },
860
+ { cache: false, timeoutMs: POLL_REQUEST_TIMEOUT_MS }
861
+ );
862
+ if (isStale()) return;
863
+ const first = res.errors?.[0];
864
+ if (first) {
865
+ const code = typeof first.extensions?.code === "string" ? first.extensions.code : void 0;
866
+ const classified = classifyExploreError(code, first.message);
867
+ if (!classified.retryable) {
868
+ finishWithError(classified);
869
+ return;
870
+ }
871
+ consecutiveFailures += 1;
872
+ if (consecutiveFailures >= MAX_CONSECUTIVE_POLL_FAILURES) {
873
+ finishWithError(classified);
874
+ return;
875
+ }
876
+ pollTimerRef.current = setTimeout(() => void tick(), pollIntervalMs);
877
+ return;
878
+ }
879
+ const message = res.data?.exploreMessage;
880
+ if (!message) {
881
+ consecutiveFailures += 1;
882
+ if (consecutiveFailures >= MAX_CONSECUTIVE_POLL_FAILURES) {
883
+ finishWithError(classifyExploreError("NOT_FOUND"));
884
+ return;
885
+ }
886
+ pollTimerRef.current = setTimeout(() => void tick(), pollIntervalMs);
887
+ return;
888
+ }
889
+ consecutiveFailures = 0;
890
+ setMessages(
891
+ (prev) => prev.map((m) => m.id === messageId ? message : m)
892
+ );
893
+ if (message.status === "COMPLETED") {
894
+ inFlightRef.current = false;
895
+ setError(null);
896
+ setPhase("ready");
897
+ return;
898
+ }
899
+ if (message.status === "FAILED") {
900
+ finishWithError(
901
+ classifyExploreError(message.errorCode ?? "UPSTREAM_UNAVAILABLE")
902
+ );
903
+ return;
904
+ }
905
+ setPhase("streaming");
906
+ pollTimerRef.current = setTimeout(() => void tick(), pollIntervalMs);
907
+ };
908
+ pollTimerRef.current = setTimeout(() => void tick(), pollIntervalMs);
909
+ },
910
+ [client, pollIntervalMs, pollTimeoutMs]
911
+ );
912
+ const submit = react.useCallback(
913
+ async (text, optimisticId, captchaAttempt) => {
914
+ const activeCatalog = catalogRef.current;
915
+ let recaptchaToken;
916
+ if (activeCatalog.captchaRequired && activeCatalog.recaptchaSiteKey) {
917
+ try {
918
+ recaptchaToken = await getRecaptchaV3Token(
919
+ activeCatalog.recaptchaSiteKey
920
+ );
921
+ } catch (err) {
922
+ console.warn("[WebSDK] reCAPTCHA v3 token could not be minted", err);
923
+ }
924
+ }
925
+ const input = {
926
+ message: text,
927
+ ...conversationTokenRef.current ? { conversationToken: conversationTokenRef.current } : {},
928
+ ...focusProductRef.current || productSlug ? { productSlug: focusProductRef.current ?? productSlug } : {},
929
+ ...recaptchaToken ? { recaptchaToken } : {},
930
+ ...locale ? { locale } : {},
931
+ ...pageUrl ? { pageUrl } : {}
932
+ };
933
+ const res = await client.mutate(
934
+ SEND_EXPLORE_MESSAGE_MUTATION,
935
+ { input },
936
+ { timeoutMs: SEND_REQUEST_TIMEOUT_MS }
937
+ );
938
+ if (!mountedRef.current) return;
939
+ const first = res.errors?.[0];
940
+ const result = res.data?.sendExploreMessage;
941
+ if (first || !result) {
942
+ const code = typeof first?.extensions?.code === "string" ? first.extensions.code : void 0;
943
+ const classified = classifyExploreError(code, first?.message);
944
+ if (classified.kind === "captcha" && captchaAttempt === 0) {
945
+ await submit(text, optimisticId, 1);
946
+ return;
947
+ }
948
+ inFlightRef.current = false;
949
+ setError(classified);
950
+ setPhase(classified.kind === "rate-limit" ? "limited" : "error");
951
+ return;
952
+ }
953
+ conversationTokenRef.current = result.conversation.token;
954
+ turnCountRef.current = result.conversation.turnCount;
955
+ setTurnCount(result.conversation.turnCount);
956
+ setRemainingToday(result.remainingToday ?? null);
957
+ const userMessage = result.userMessage;
958
+ const assistantMessage = result.assistantMessage;
959
+ setMessages((prev) => {
960
+ const replaced = prev.some((m) => m.id === optimisticId) ? prev.map((m) => m.id === optimisticId ? userMessage : m) : [...prev, userMessage];
961
+ return [...replaced, assistantMessage];
962
+ });
963
+ if (assistantMessage.status === "COMPLETED" || assistantMessage.status === "FAILED") {
964
+ inFlightRef.current = false;
965
+ if (assistantMessage.status === "FAILED") {
966
+ const classified = classifyExploreError(
967
+ assistantMessage.errorCode ?? "UPSTREAM_UNAVAILABLE"
968
+ );
969
+ setError(classified);
970
+ setPhase("error");
971
+ } else {
972
+ setPhase("ready");
973
+ }
974
+ return;
975
+ }
976
+ setPhase("streaming");
977
+ startPolling(result.conversation.token, assistantMessage.id);
978
+ },
979
+ [client, locale, pageUrl, productSlug, startPolling]
980
+ );
981
+ const send = react.useCallback(
982
+ async (message) => {
983
+ const text = message.trim();
984
+ if (!text) return;
985
+ if (inFlightRef.current) return;
986
+ const activeCatalog = catalogRef.current;
987
+ if (!activeCatalog.enabled) {
988
+ setError(classifyExploreError("EXPLORE_DISABLED"));
989
+ setPhase("disabled");
990
+ return;
991
+ }
992
+ if (text.length > activeCatalog.limits.maxMessageLength) {
993
+ setError({
994
+ kind: "validation",
995
+ code: "MESSAGE_TOO_LONG",
996
+ message: `That message is ${text.length} characters \u2014 the limit is ${activeCatalog.limits.maxMessageLength}.`,
997
+ retryable: false
998
+ });
999
+ return;
1000
+ }
1001
+ if (turnCountRef.current >= activeCatalog.limits.maxTurnsPerConversation) {
1002
+ setError({
1003
+ kind: "validation",
1004
+ code: "CONVERSATION_LIMIT",
1005
+ message: `This conversation has reached its ${activeCatalog.limits.maxTurnsPerConversation}-message limit. Start a new chat to keep going.`,
1006
+ retryable: false
1007
+ });
1008
+ return;
1009
+ }
1010
+ inFlightRef.current = true;
1011
+ lastSentTextRef.current = text;
1012
+ setError(null);
1013
+ setPhase("sending");
1014
+ cancelPolling();
1015
+ const optimistic = {
1016
+ id: makeOptimisticId(),
1017
+ conversationId: conversationTokenRef.current ?? "pending",
1018
+ role: "USER",
1019
+ status: "COMPLETED",
1020
+ content: text,
1021
+ partialContent: null,
1022
+ activity: [],
1023
+ references: [],
1024
+ ctas: [],
1025
+ errorCode: null,
1026
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1027
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
1028
+ };
1029
+ setMessages((prev) => [...prev, optimistic]);
1030
+ turnCountRef.current += 1;
1031
+ setTurnCount(turnCountRef.current);
1032
+ onSend?.(text);
1033
+ try {
1034
+ await submit(text, optimistic.id, 0);
1035
+ } catch (err) {
1036
+ if (!mountedRef.current) return;
1037
+ console.error("[WebSDK] Explore send failed", err);
1038
+ inFlightRef.current = false;
1039
+ setError(classifyExploreError("NETWORK_ERROR"));
1040
+ setPhase("error");
1041
+ }
1042
+ },
1043
+ [cancelPolling, onSend, submit]
1044
+ );
1045
+ const stop = react.useCallback(() => {
1046
+ cancelPolling();
1047
+ inFlightRef.current = false;
1048
+ setMessages(
1049
+ (prev) => prev.flatMap((m) => {
1050
+ if (m.role !== "ASSISTANT") return [m];
1051
+ if (m.status !== "PENDING" && m.status !== "RUNNING") return [m];
1052
+ const text = visibleText(m);
1053
+ if (!text) return [];
1054
+ return [
1055
+ {
1056
+ ...m,
1057
+ status: "COMPLETED",
1058
+ content: text,
1059
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
1060
+ }
1061
+ ];
1062
+ })
1063
+ );
1064
+ setPhase(
1065
+ (prev) => prev === "sending" || prev === "streaming" ? "ready" : prev
1066
+ );
1067
+ }, [cancelPolling]);
1068
+ const retry = react.useCallback(() => {
1069
+ const text = lastSentTextRef.current;
1070
+ if (!text) return;
1071
+ cancelPolling();
1072
+ inFlightRef.current = false;
1073
+ setMessages((prev) => {
1074
+ const next = [...prev];
1075
+ const last = next[next.length - 1];
1076
+ if (last?.role === "ASSISTANT" && last.status === "FAILED") next.pop();
1077
+ const prior = next[next.length - 1];
1078
+ if (prior?.role === "USER" && prior.content === text) next.pop();
1079
+ return next;
1080
+ });
1081
+ turnCountRef.current = Math.max(0, turnCountRef.current - 1);
1082
+ setTurnCount(turnCountRef.current);
1083
+ void send(text);
1084
+ }, [cancelPolling, send]);
1085
+ const reset = react.useCallback(() => {
1086
+ cancelPolling();
1087
+ inFlightRef.current = false;
1088
+ conversationTokenRef.current = null;
1089
+ lastSentTextRef.current = null;
1090
+ turnCountRef.current = 0;
1091
+ setTurnCount(0);
1092
+ setMessages([]);
1093
+ setError(null);
1094
+ setRemainingToday(null);
1095
+ setPhase(catalogRef.current.enabled ? "ready" : "disabled");
1096
+ if (typeof window !== "undefined") {
1097
+ try {
1098
+ window.sessionStorage.removeItem(storageKey);
1099
+ } catch {
1100
+ }
1101
+ }
1102
+ }, [cancelPolling, storageKey]);
1103
+ const setFocusProduct = react.useCallback((slug) => {
1104
+ focusProductRef.current = slug;
1105
+ setFocusProductState(slug);
1106
+ }, []);
1107
+ const activity = react.useMemo(() => {
1108
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
1109
+ const m = messages[i];
1110
+ if (!m || m.role !== "ASSISTANT") continue;
1111
+ if (m.status !== "PENDING" && m.status !== "RUNNING") return null;
1112
+ const lines = m.activity ?? [];
1113
+ return lines.length ? lines[lines.length - 1] ?? null : null;
1114
+ }
1115
+ return null;
1116
+ }, [messages]);
1117
+ const canSend = (phase === "ready" || phase === "error") && catalog.enabled && turnCount < catalog.limits.maxTurnsPerConversation;
1118
+ return {
1119
+ phase,
1120
+ catalog,
1121
+ messages,
1122
+ activity,
1123
+ remainingToday,
1124
+ error,
1125
+ focusProduct,
1126
+ setFocusProduct,
1127
+ send,
1128
+ stop,
1129
+ retry,
1130
+ reset,
1131
+ canSend
1132
+ };
1133
+ }
1134
+ var optimisticCounter = 0;
1135
+ function makeOptimisticId() {
1136
+ optimisticCounter += 1;
1137
+ return `boff-explore-local-${Date.now()}-${optimisticCounter}`;
1138
+ }
1139
+
1140
+ exports.DEFAULT_EXPLORE_EXAMPLE_PROMPTS = DEFAULT_EXPLORE_EXAMPLE_PROMPTS;
1141
+ exports.FALLBACK_EXPLORE_LIMITS = FALLBACK_EXPLORE_LIMITS;
1142
+ exports.classifyExploreError = classifyExploreError;
1143
+ exports.useExploreChat = useExploreChat;
1144
+ //# sourceMappingURL=index.js.map
1145
+ //# sourceMappingURL=index.js.map