@burdenoff/website-sdk 2026.826.1 → 2026.828.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -0
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +13 -7
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +13 -7
- package/dist/client/index.mjs.map +1 -1
- package/dist/components/contact.js +13 -7
- package/dist/components/contact.js.map +1 -1
- package/dist/components/contact.mjs +13 -7
- package/dist/components/contact.mjs.map +1 -1
- package/dist/components/explore-cta.d.mts +26 -0
- package/dist/components/explore-cta.d.ts +26 -0
- package/dist/components/explore-cta.js +108 -0
- package/dist/components/explore-cta.js.map +1 -0
- package/dist/components/explore-cta.mjs +106 -0
- package/dist/components/explore-cta.mjs.map +1 -0
- package/dist/components/explore.d.mts +43 -0
- package/dist/components/explore.d.ts +43 -0
- package/dist/components/explore.js +2219 -0
- package/dist/components/explore.js.map +1 -0
- package/dist/components/explore.mjs +2193 -0
- package/dist/components/explore.mjs.map +1 -0
- package/dist/components/newsletter.js +13 -7
- package/dist/components/newsletter.js.map +1 -1
- package/dist/components/newsletter.mjs +13 -7
- package/dist/components/newsletter.mjs.map +1 -1
- package/dist/components/partners.js +13 -7
- package/dist/components/partners.js.map +1 -1
- package/dist/components/partners.mjs +13 -7
- package/dist/components/partners.mjs.map +1 -1
- package/dist/components/pricing.js +13 -7
- package/dist/components/pricing.js.map +1 -1
- package/dist/components/pricing.mjs +13 -7
- package/dist/components/pricing.mjs.map +1 -1
- package/dist/components/resource-links.js +13 -7
- package/dist/components/resource-links.js.map +1 -1
- package/dist/components/resource-links.mjs +13 -7
- package/dist/components/resource-links.mjs.map +1 -1
- package/dist/components/social-links.js +13 -7
- package/dist/components/social-links.js.map +1 -1
- package/dist/components/social-links.mjs +13 -7
- package/dist/components/social-links.mjs.map +1 -1
- package/dist/components/unsubscribe.js +13 -7
- package/dist/components/unsubscribe.js.map +1 -1
- package/dist/components/unsubscribe.mjs +13 -7
- package/dist/components/unsubscribe.mjs.map +1 -1
- package/dist/components/waitlist.js +13 -7
- package/dist/components/waitlist.js.map +1 -1
- package/dist/components/waitlist.mjs +13 -7
- package/dist/components/waitlist.mjs.map +1 -1
- package/dist/content/index.d.mts +2 -2
- package/dist/content/index.d.ts +2 -2
- package/dist/content/index.js +13 -7
- package/dist/content/index.js.map +1 -1
- package/dist/content/index.mjs +13 -7
- package/dist/content/index.mjs.map +1 -1
- package/dist/{graphql-client-Cv6TO6hn.d.mts → graphql-client-BXn6RYV0.d.mts} +31 -4
- package/dist/{graphql-client-Cv6TO6hn.d.ts → graphql-client-BXn6RYV0.d.ts} +31 -4
- package/dist/hooks/index.d.mts +1 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1145 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/index.mjs +1140 -0
- package/dist/hooks/index.mjs.map +1 -0
- package/dist/{index-Cts2CAMO.d.ts → index-BXg7cQLm.d.ts} +1 -1
- package/dist/index-CUJTTlYx.d.mts +244 -0
- package/dist/index-CUJTTlYx.d.ts +244 -0
- package/dist/{index-B36goDSY.d.mts → index-DYxQiEm5.d.mts} +1 -1
- package/dist/index.d.mts +9 -6
- package/dist/index.d.ts +9 -6
- package/dist/index.js +2000 -426
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1990 -428
- package/dist/index.mjs.map +1 -1
- package/dist/product/index.js +13 -7
- package/dist/product/index.js.map +1 -1
- package/dist/product/index.mjs +13 -7
- package/dist/product/index.mjs.map +1 -1
- package/dist/{provider-BZlnZDBb.d.mts → provider-CUfI8meE.d.mts} +1 -1
- package/dist/{provider-CRLmsW5h.d.ts → provider-D_uUM7_e.d.ts} +1 -1
- package/dist/store/index.js +13 -7
- package/dist/store/index.js.map +1 -1
- package/dist/store/index.mjs +13 -7
- package/dist/store/index.mjs.map +1 -1
- package/dist/utils/index.d.mts +49 -1
- package/dist/utils/index.d.ts +49 -1
- package/dist/utils/index.js +70 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/index.mjs +68 -1
- package/dist/utils/index.mjs.map +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,2219 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React = require('react');
|
|
4
|
+
var lucideReact = require('lucide-react');
|
|
5
|
+
var ReactMarkdown = require('react-markdown');
|
|
6
|
+
var rehypeSanitize = require('rehype-sanitize');
|
|
7
|
+
var remarkGfm = require('remark-gfm');
|
|
8
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
9
|
+
var reactHelmetAsync = require('react-helmet-async');
|
|
10
|
+
var reactSlot = require('@radix-ui/react-slot');
|
|
11
|
+
var classVarianceAuthority = require('class-variance-authority');
|
|
12
|
+
var clsx = require('clsx');
|
|
13
|
+
var tailwindMerge = require('tailwind-merge');
|
|
14
|
+
|
|
15
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
16
|
+
|
|
17
|
+
function _interopNamespace(e) {
|
|
18
|
+
if (e && e.__esModule) return e;
|
|
19
|
+
var n = Object.create(null);
|
|
20
|
+
if (e) {
|
|
21
|
+
Object.keys(e).forEach(function (k) {
|
|
22
|
+
if (k !== 'default') {
|
|
23
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
24
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
get: function () { return e[k]; }
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
n.default = e;
|
|
32
|
+
return Object.freeze(n);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
36
|
+
var ReactMarkdown__default = /*#__PURE__*/_interopDefault(ReactMarkdown);
|
|
37
|
+
var rehypeSanitize__default = /*#__PURE__*/_interopDefault(rehypeSanitize);
|
|
38
|
+
var remarkGfm__default = /*#__PURE__*/_interopDefault(remarkGfm);
|
|
39
|
+
|
|
40
|
+
var __defProp = Object.defineProperty;
|
|
41
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
42
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
43
|
+
var defaultComponents = {
|
|
44
|
+
a: ({ href, children, ...props }) => {
|
|
45
|
+
const isExternal = typeof href === "string" && (href.startsWith("http://") || href.startsWith("https://"));
|
|
46
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
47
|
+
"a",
|
|
48
|
+
{
|
|
49
|
+
href,
|
|
50
|
+
...props,
|
|
51
|
+
...isExternal ? { target: "_blank", rel: "noopener noreferrer" } : { rel: "noopener noreferrer" },
|
|
52
|
+
children
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function Markdown({
|
|
58
|
+
children,
|
|
59
|
+
className,
|
|
60
|
+
inline,
|
|
61
|
+
components
|
|
62
|
+
}) {
|
|
63
|
+
const content = /* @__PURE__ */ jsxRuntime.jsx(
|
|
64
|
+
ReactMarkdown__default.default,
|
|
65
|
+
{
|
|
66
|
+
remarkPlugins: [remarkGfm__default.default],
|
|
67
|
+
rehypePlugins: [rehypeSanitize__default.default],
|
|
68
|
+
components: { ...defaultComponents, ...components },
|
|
69
|
+
...inline ? { disallowedElements: ["p"], unwrapDisallowed: true } : {},
|
|
70
|
+
children
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
if (inline) {
|
|
74
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className, children: content });
|
|
75
|
+
}
|
|
76
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: content });
|
|
77
|
+
}
|
|
78
|
+
var PROSE_CSS = `
|
|
79
|
+
.boff-prose {
|
|
80
|
+
max-width: none;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.boff-prose h1 {
|
|
84
|
+
font-size: 2.25rem;
|
|
85
|
+
font-weight: 800;
|
|
86
|
+
line-height: 1.2;
|
|
87
|
+
margin-top: 0;
|
|
88
|
+
margin-bottom: 1rem;
|
|
89
|
+
color: hsl(var(--foreground));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.boff-prose h2 {
|
|
93
|
+
font-size: 1.75rem;
|
|
94
|
+
font-weight: 700;
|
|
95
|
+
line-height: 1.3;
|
|
96
|
+
margin-top: 2rem;
|
|
97
|
+
margin-bottom: 1rem;
|
|
98
|
+
color: hsl(var(--foreground));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.boff-prose h3 {
|
|
102
|
+
font-size: 1.5rem;
|
|
103
|
+
font-weight: 600;
|
|
104
|
+
line-height: 1.4;
|
|
105
|
+
margin-top: 1.5rem;
|
|
106
|
+
margin-bottom: 0.75rem;
|
|
107
|
+
color: hsl(var(--foreground));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.boff-prose h4 {
|
|
111
|
+
font-size: 1.25rem;
|
|
112
|
+
font-weight: 600;
|
|
113
|
+
line-height: 1.5;
|
|
114
|
+
margin-top: 1.25rem;
|
|
115
|
+
margin-bottom: 0.5rem;
|
|
116
|
+
color: hsl(var(--foreground));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.boff-prose p {
|
|
120
|
+
margin-bottom: 1rem;
|
|
121
|
+
line-height: 1.75;
|
|
122
|
+
color: hsl(var(--foreground));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.boff-prose ul,
|
|
126
|
+
.boff-prose ol {
|
|
127
|
+
margin-bottom: 1rem;
|
|
128
|
+
padding-left: 1.5rem;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.boff-prose ul {
|
|
132
|
+
list-style-type: disc;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.boff-prose ol {
|
|
136
|
+
list-style-type: decimal;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.boff-prose li {
|
|
140
|
+
margin-bottom: 0.25rem;
|
|
141
|
+
line-height: 1.75;
|
|
142
|
+
color: hsl(var(--foreground));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.boff-prose li > ul,
|
|
146
|
+
.boff-prose li > ol {
|
|
147
|
+
margin-top: 0.25rem;
|
|
148
|
+
margin-bottom: 0.25rem;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.boff-prose a {
|
|
152
|
+
color: hsl(var(--primary));
|
|
153
|
+
text-decoration: underline;
|
|
154
|
+
text-underline-offset: 2px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.boff-prose a:hover {
|
|
158
|
+
opacity: 0.8;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.boff-prose hr {
|
|
162
|
+
border: none;
|
|
163
|
+
height: 1px;
|
|
164
|
+
background-color: hsl(var(--border));
|
|
165
|
+
margin: 2rem 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.boff-prose blockquote {
|
|
169
|
+
border-left: 4px solid hsl(var(--border));
|
|
170
|
+
padding-left: 1rem;
|
|
171
|
+
margin: 1.5rem 0;
|
|
172
|
+
font-style: italic;
|
|
173
|
+
color: hsl(var(--muted-foreground));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.boff-prose strong {
|
|
177
|
+
font-weight: 700;
|
|
178
|
+
color: hsl(var(--foreground));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.boff-prose code {
|
|
182
|
+
background-color: hsl(var(--muted));
|
|
183
|
+
padding: 0.125rem 0.25rem;
|
|
184
|
+
border-radius: 0.25rem;
|
|
185
|
+
font-size: 0.875em;
|
|
186
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.boff-prose pre {
|
|
190
|
+
background-color: hsl(var(--muted));
|
|
191
|
+
padding: 1rem;
|
|
192
|
+
border-radius: 0.5rem;
|
|
193
|
+
overflow-x: auto;
|
|
194
|
+
margin-bottom: 1rem;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.boff-prose pre code {
|
|
198
|
+
background-color: transparent;
|
|
199
|
+
padding: 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.boff-prose img {
|
|
203
|
+
max-width: 100%;
|
|
204
|
+
height: auto;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.boff-prose table {
|
|
208
|
+
width: 100%;
|
|
209
|
+
border-collapse: collapse;
|
|
210
|
+
margin-bottom: 1rem;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.boff-prose th,
|
|
214
|
+
.boff-prose td {
|
|
215
|
+
border: 1px solid hsl(var(--border));
|
|
216
|
+
padding: 0.5rem;
|
|
217
|
+
text-align: left;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.boff-prose th {
|
|
221
|
+
font-weight: 600;
|
|
222
|
+
background-color: hsl(var(--muted));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* Smaller variant used inside cards and compact panels */
|
|
226
|
+
.boff-prose-sm {
|
|
227
|
+
font-size: 0.875rem;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.boff-prose-sm h1 {
|
|
231
|
+
font-size: 1.75rem;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.boff-prose-sm h2 {
|
|
235
|
+
font-size: 1.5rem;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.boff-prose-sm h3 {
|
|
239
|
+
font-size: 1.25rem;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.boff-prose-sm h4 {
|
|
243
|
+
font-size: 1.125rem;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.boff-prose-sm p,
|
|
247
|
+
.boff-prose-sm li {
|
|
248
|
+
line-height: 1.625;
|
|
249
|
+
}
|
|
250
|
+
`;
|
|
251
|
+
function ProseStyles() {
|
|
252
|
+
return /* @__PURE__ */ jsxRuntime.jsx("style", { dangerouslySetInnerHTML: { __html: PROSE_CSS } });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/client/security.ts
|
|
256
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
257
|
+
var RESERVED_HEADERS = /* @__PURE__ */ new Set([
|
|
258
|
+
"authorization",
|
|
259
|
+
"cookie",
|
|
260
|
+
"x-actor-id",
|
|
261
|
+
"x-actor-type",
|
|
262
|
+
"x-consent-granted-by",
|
|
263
|
+
"x-consent-scope",
|
|
264
|
+
"x-consent-granted-at",
|
|
265
|
+
"x-consent-expires-at"
|
|
266
|
+
]);
|
|
267
|
+
function validateGatewayUrl(value) {
|
|
268
|
+
let url;
|
|
269
|
+
try {
|
|
270
|
+
url = new URL(value);
|
|
271
|
+
} catch {
|
|
272
|
+
throw new Error("Gateway URL must be a valid absolute URL");
|
|
273
|
+
}
|
|
274
|
+
if (url.protocol === "https:") {
|
|
275
|
+
return url.toString();
|
|
276
|
+
}
|
|
277
|
+
if (url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname.toLowerCase())) {
|
|
278
|
+
return url.toString();
|
|
279
|
+
}
|
|
280
|
+
throw new Error("Gateway URL must use HTTPS unless it targets localhost");
|
|
281
|
+
}
|
|
282
|
+
function sanitizeHeaders(headers) {
|
|
283
|
+
if (!headers) return void 0;
|
|
284
|
+
return Object.fromEntries(
|
|
285
|
+
Object.entries(headers).filter(
|
|
286
|
+
([key]) => !RESERVED_HEADERS.has(key.toLowerCase())
|
|
287
|
+
)
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/client/graphql-client.ts
|
|
292
|
+
var _WebSDKClient = class _WebSDKClient {
|
|
293
|
+
constructor(config) {
|
|
294
|
+
__publicField(this, "config");
|
|
295
|
+
__publicField(this, "queryCache", /* @__PURE__ */ new Map());
|
|
296
|
+
__publicField(this, "inflight", /* @__PURE__ */ new Map());
|
|
297
|
+
this.config = {
|
|
298
|
+
...config,
|
|
299
|
+
gatewayUrl: validateGatewayUrl(config.gatewayUrl),
|
|
300
|
+
headers: sanitizeHeaders(config.headers)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
getConfig() {
|
|
304
|
+
return this.config;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Execute a GraphQL query (with deduplication and caching).
|
|
308
|
+
*
|
|
309
|
+
* Pass `{ cache: false }` to bypass the cache entirely — required for poll
|
|
310
|
+
* loops, where every tick must reach the network.
|
|
311
|
+
*/
|
|
312
|
+
async query(query, variables, options) {
|
|
313
|
+
if (options?.cache === false) {
|
|
314
|
+
return this.request(query, variables, true, options.timeoutMs);
|
|
315
|
+
}
|
|
316
|
+
const cacheKey = JSON.stringify({ query, variables });
|
|
317
|
+
const cached = this.queryCache.get(cacheKey);
|
|
318
|
+
if (cached) {
|
|
319
|
+
if (Date.now() - cached.ts < _WebSDKClient.CACHE_TTL) {
|
|
320
|
+
return cached.data;
|
|
321
|
+
}
|
|
322
|
+
this.queryCache.delete(cacheKey);
|
|
323
|
+
}
|
|
324
|
+
const existing = this.inflight.get(cacheKey);
|
|
325
|
+
if (existing) {
|
|
326
|
+
return existing;
|
|
327
|
+
}
|
|
328
|
+
const promise = this.request(query, variables, true, options?.timeoutMs).then((result) => {
|
|
329
|
+
if (result.data && !result.errors?.length) {
|
|
330
|
+
if (this.queryCache.size >= _WebSDKClient.MAX_CACHE_ENTRIES) {
|
|
331
|
+
const oldestEntry = this.queryCache.keys().next().value;
|
|
332
|
+
if (oldestEntry) {
|
|
333
|
+
this.queryCache.delete(oldestEntry);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
this.queryCache.set(cacheKey, { data: result, ts: Date.now() });
|
|
337
|
+
}
|
|
338
|
+
return result;
|
|
339
|
+
}).finally(() => {
|
|
340
|
+
this.inflight.delete(cacheKey);
|
|
341
|
+
});
|
|
342
|
+
this.inflight.set(cacheKey, promise);
|
|
343
|
+
return promise;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Execute a GraphQL mutation
|
|
347
|
+
*/
|
|
348
|
+
async mutate(mutation, variables, options) {
|
|
349
|
+
return this.request(mutation, variables, false, options?.timeoutMs);
|
|
350
|
+
}
|
|
351
|
+
/** HTTP statuses worth another attempt — transient server/edge conditions. */
|
|
352
|
+
static isRetriableStatus(status) {
|
|
353
|
+
return status === 429 || status === 408 || status >= 500 && status < 600;
|
|
354
|
+
}
|
|
355
|
+
static sleep(ms) {
|
|
356
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Perform the HTTP call with a hard timeout. Every outcome resolves to a
|
|
360
|
+
* GraphQLResponse — callers never hang, so a UI's `loading` state always
|
|
361
|
+
* settles into data or a renderable error.
|
|
362
|
+
*/
|
|
363
|
+
async attempt(query, variables, timeoutMs) {
|
|
364
|
+
const controller = new AbortController();
|
|
365
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
366
|
+
try {
|
|
367
|
+
const response = await fetch(this.config.gatewayUrl, {
|
|
368
|
+
method: "POST",
|
|
369
|
+
headers: {
|
|
370
|
+
"Content-Type": "application/json",
|
|
371
|
+
"x-product-id": this.config.productId,
|
|
372
|
+
...this.config.headers
|
|
373
|
+
},
|
|
374
|
+
body: JSON.stringify({ query, variables }),
|
|
375
|
+
signal: controller.signal
|
|
376
|
+
});
|
|
377
|
+
if (!response.ok) {
|
|
378
|
+
console.error(
|
|
379
|
+
"[WebSDK] GraphQL request failed",
|
|
380
|
+
response.status,
|
|
381
|
+
response.statusText
|
|
382
|
+
);
|
|
383
|
+
return {
|
|
384
|
+
res: {
|
|
385
|
+
errors: [
|
|
386
|
+
{
|
|
387
|
+
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
388
|
+
extensions: { code: `HTTP_${response.status}` }
|
|
389
|
+
}
|
|
390
|
+
]
|
|
391
|
+
},
|
|
392
|
+
retriable: _WebSDKClient.isRetriableStatus(response.status)
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
res: await response.json(),
|
|
397
|
+
retriable: false
|
|
398
|
+
};
|
|
399
|
+
} catch (error) {
|
|
400
|
+
const aborted = error instanceof Error && (error.name === "AbortError" || controller.signal.aborted);
|
|
401
|
+
const message = aborted ? `Request timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : "Network error";
|
|
402
|
+
console.error("[WebSDK] Network error", message);
|
|
403
|
+
return {
|
|
404
|
+
res: {
|
|
405
|
+
errors: [
|
|
406
|
+
{
|
|
407
|
+
message,
|
|
408
|
+
extensions: { code: aborted ? "TIMEOUT" : "NETWORK_ERROR" }
|
|
409
|
+
}
|
|
410
|
+
]
|
|
411
|
+
},
|
|
412
|
+
// Transport-level failures are worth one more try; a genuine outage
|
|
413
|
+
// just fails twice quickly and still renders an error state.
|
|
414
|
+
retriable: true
|
|
415
|
+
};
|
|
416
|
+
} finally {
|
|
417
|
+
clearTimeout(timer);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async request(query, variables, retry = true, timeoutOverrideMs) {
|
|
421
|
+
const timeoutMs = timeoutOverrideMs ?? this.config.timeoutMs ?? _WebSDKClient.DEFAULT_TIMEOUT_MS;
|
|
422
|
+
const maxRetries = retry ? this.config.maxRetries ?? _WebSDKClient.DEFAULT_MAX_RETRIES : 0;
|
|
423
|
+
let last;
|
|
424
|
+
for (let i = 0; i <= maxRetries; i++) {
|
|
425
|
+
const { res, retriable } = await this.attempt(
|
|
426
|
+
query,
|
|
427
|
+
variables,
|
|
428
|
+
timeoutMs
|
|
429
|
+
);
|
|
430
|
+
if (!retriable || i === maxRetries) return res;
|
|
431
|
+
last = res;
|
|
432
|
+
await _WebSDKClient.sleep(
|
|
433
|
+
_WebSDKClient.RETRY_BASE_DELAY_MS * (i + 1) + Math.random() * 200
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
return last;
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
__publicField(_WebSDKClient, "CACHE_TTL", 5 * 60 * 1e3);
|
|
440
|
+
// 5 minutes
|
|
441
|
+
__publicField(_WebSDKClient, "MAX_CACHE_ENTRIES", 100);
|
|
442
|
+
__publicField(_WebSDKClient, "DEFAULT_TIMEOUT_MS", 12e3);
|
|
443
|
+
__publicField(_WebSDKClient, "DEFAULT_MAX_RETRIES", 1);
|
|
444
|
+
__publicField(_WebSDKClient, "RETRY_BASE_DELAY_MS", 400);
|
|
445
|
+
var WebSDKContext = React.createContext(null);
|
|
446
|
+
function useWebSDK() {
|
|
447
|
+
const client = React.useContext(WebSDKContext);
|
|
448
|
+
if (!client) {
|
|
449
|
+
throw new Error("useWebSDK must be used within a WebSDKProvider");
|
|
450
|
+
}
|
|
451
|
+
return client;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/components/explore-types.ts
|
|
455
|
+
var EXPLORE_MESSAGE_FIELDS = (
|
|
456
|
+
/* GraphQL */
|
|
457
|
+
`
|
|
458
|
+
id
|
|
459
|
+
conversationId
|
|
460
|
+
role
|
|
461
|
+
status
|
|
462
|
+
content
|
|
463
|
+
partialContent
|
|
464
|
+
activity
|
|
465
|
+
references {
|
|
466
|
+
url
|
|
467
|
+
title
|
|
468
|
+
description
|
|
469
|
+
product
|
|
470
|
+
imageUrl
|
|
471
|
+
}
|
|
472
|
+
ctas {
|
|
473
|
+
kind
|
|
474
|
+
label
|
|
475
|
+
url
|
|
476
|
+
product
|
|
477
|
+
}
|
|
478
|
+
errorCode
|
|
479
|
+
createdAt
|
|
480
|
+
completedAt
|
|
481
|
+
`
|
|
482
|
+
);
|
|
483
|
+
var EXPLORE_CATALOG_QUERY = (
|
|
484
|
+
/* GraphQL */
|
|
485
|
+
`
|
|
486
|
+
query ExploreCatalog($productSlug: String) {
|
|
487
|
+
exploreCatalog(productSlug: $productSlug) {
|
|
488
|
+
enabled
|
|
489
|
+
products {
|
|
490
|
+
slug
|
|
491
|
+
name
|
|
492
|
+
tagline
|
|
493
|
+
website
|
|
494
|
+
}
|
|
495
|
+
examplePrompts
|
|
496
|
+
welcomeTitle
|
|
497
|
+
welcomeBody
|
|
498
|
+
limits {
|
|
499
|
+
maxMessageLength
|
|
500
|
+
maxTurnsPerConversation
|
|
501
|
+
messagesPerDay
|
|
502
|
+
historyTurns
|
|
503
|
+
}
|
|
504
|
+
captchaRequired
|
|
505
|
+
recaptchaSiteKey
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
`
|
|
509
|
+
);
|
|
510
|
+
var EXPLORE_MESSAGE_QUERY = (
|
|
511
|
+
/* GraphQL */
|
|
512
|
+
`
|
|
513
|
+
query ExploreMessage($conversationToken: String!, $id: ID!) {
|
|
514
|
+
exploreMessage(conversationToken: $conversationToken, id: $id) {
|
|
515
|
+
${EXPLORE_MESSAGE_FIELDS}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
`
|
|
519
|
+
);
|
|
520
|
+
var SEND_EXPLORE_MESSAGE_MUTATION = (
|
|
521
|
+
/* GraphQL */
|
|
522
|
+
`
|
|
523
|
+
mutation SendExploreMessage($input: SendExploreMessageInput!) {
|
|
524
|
+
sendExploreMessage(input: $input) {
|
|
525
|
+
conversation {
|
|
526
|
+
id
|
|
527
|
+
token
|
|
528
|
+
productSlug
|
|
529
|
+
turnCount
|
|
530
|
+
createdAt
|
|
531
|
+
}
|
|
532
|
+
userMessage {
|
|
533
|
+
${EXPLORE_MESSAGE_FIELDS}
|
|
534
|
+
}
|
|
535
|
+
assistantMessage {
|
|
536
|
+
${EXPLORE_MESSAGE_FIELDS}
|
|
537
|
+
}
|
|
538
|
+
remainingToday
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
`
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
// src/data/products.ts
|
|
545
|
+
var ECOSYSTEM_PRODUCTS = [
|
|
546
|
+
{
|
|
547
|
+
slug: "workspaces",
|
|
548
|
+
name: "Workspaces",
|
|
549
|
+
websiteUrl: "https://burdenoff.com"
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
slug: "vibecontrols",
|
|
553
|
+
name: "VibeControls",
|
|
554
|
+
websiteUrl: "https://vibecontrols.com"
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
slug: "fluidgrids",
|
|
558
|
+
name: "FluidGrids",
|
|
559
|
+
websiteUrl: "https://fluidgrids.com"
|
|
560
|
+
},
|
|
561
|
+
// Botlit ships on botlit.ai — botlit.com does not resolve.
|
|
562
|
+
{ slug: "botlit", name: "Botlit", websiteUrl: "https://www.botlit.ai" },
|
|
563
|
+
{
|
|
564
|
+
slug: "bigconsole",
|
|
565
|
+
name: "BigConsole",
|
|
566
|
+
websiteUrl: "https://bigconsole.com"
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
slug: "semanticfed",
|
|
570
|
+
name: "SemanticFed",
|
|
571
|
+
websiteUrl: "https://semanticfed.com"
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
slug: "intelwatchtower",
|
|
575
|
+
name: "IntelWatchtower",
|
|
576
|
+
websiteUrl: "https://intelwatchtower.com"
|
|
577
|
+
},
|
|
578
|
+
{ slug: "buildmyiq", name: "BuildMyIQ", websiteUrl: "https://buildmyiq.com" },
|
|
579
|
+
{
|
|
580
|
+
slug: "assethandler",
|
|
581
|
+
name: "AssetHandler",
|
|
582
|
+
websiteUrl: "https://assethandler.com"
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
slug: "crewfoundry",
|
|
586
|
+
name: "CrewFoundry",
|
|
587
|
+
websiteUrl: "https://crewfoundry.com"
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
slug: "planmagnet",
|
|
591
|
+
name: "PlanMagnet",
|
|
592
|
+
websiteUrl: "https://planmagnet.com"
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
slug: "eventfullymanaged",
|
|
596
|
+
name: "EventfullyManaged",
|
|
597
|
+
websiteUrl: "https://eventfullymanaged.com"
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
slug: "headshotmarketing",
|
|
601
|
+
name: "HeadshotMarketing",
|
|
602
|
+
websiteUrl: "https://headshotmarketing.com"
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
slug: "technospam",
|
|
606
|
+
name: "TechnoSpam",
|
|
607
|
+
websiteUrl: "https://technospam.com"
|
|
608
|
+
},
|
|
609
|
+
{ slug: "kadaikodi", name: "Kadaikodi", websiteUrl: "https://kadaikodi.com" },
|
|
610
|
+
{
|
|
611
|
+
slug: "movethewheels",
|
|
612
|
+
name: "MoveTheWheels",
|
|
613
|
+
websiteUrl: "https://movethewheels.com"
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
slug: "healthybowl",
|
|
617
|
+
name: "Healthy Bowl",
|
|
618
|
+
websiteUrl: "https://gethealthybowl.com"
|
|
619
|
+
},
|
|
620
|
+
{ slug: "ggnomad", name: "GGNomad", websiteUrl: "https://ggnomad.com" },
|
|
621
|
+
{
|
|
622
|
+
slug: "timecampus",
|
|
623
|
+
name: "TimeCampus",
|
|
624
|
+
websiteUrl: "https://timecampus.com"
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
slug: "manufacturedops",
|
|
628
|
+
name: "ManufacturedOps",
|
|
629
|
+
websiteUrl: "https://manufacturedops.com"
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
slug: "theglobalfuel",
|
|
633
|
+
name: "TheGlobalFuel",
|
|
634
|
+
websiteUrl: "https://theglobalfuel.com"
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
slug: "housingvista",
|
|
638
|
+
name: "HousingVista",
|
|
639
|
+
websiteUrl: "https://housingvista.com"
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
slug: "proserviceworld",
|
|
643
|
+
name: "ProServiceWorld",
|
|
644
|
+
websiteUrl: "https://proserviceworld.com"
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
slug: "mybodyshield",
|
|
648
|
+
name: "MyBodyShield",
|
|
649
|
+
websiteUrl: "https://mybodyshield.com"
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
slug: "artistrybase",
|
|
653
|
+
name: "ArtistryBase",
|
|
654
|
+
websiteUrl: "https://artistrybase.com"
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
slug: "theculturepost",
|
|
658
|
+
name: "TheCulturePost",
|
|
659
|
+
websiteUrl: "https://theculturepost.com"
|
|
660
|
+
},
|
|
661
|
+
{ slug: "collabkin", name: "CollabKin", websiteUrl: "https://collabkin.com" },
|
|
662
|
+
{
|
|
663
|
+
slug: "comradecircle",
|
|
664
|
+
name: "ComradeCircle",
|
|
665
|
+
websiteUrl: "https://comradecircle.com"
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
slug: "brainyrich",
|
|
669
|
+
name: "BrainyRich",
|
|
670
|
+
websiteUrl: "https://brainyrich.com"
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
slug: "cosmicintersection",
|
|
674
|
+
name: "CosmicIntersection",
|
|
675
|
+
websiteUrl: "https://cosmicintersection.com"
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
slug: "govcitizenhub",
|
|
679
|
+
name: "GovCitizenHub",
|
|
680
|
+
websiteUrl: "https://govcitizenhub.com"
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
slug: "adaptercloud",
|
|
684
|
+
name: "AdapterCloud",
|
|
685
|
+
websiteUrl: "https://adaptercloud.com"
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
slug: "subscriberbot",
|
|
689
|
+
name: "Subscriber Bot",
|
|
690
|
+
websiteUrl: "https://subscriberbot.com"
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
slug: "hookmovies",
|
|
694
|
+
name: "HookMovies",
|
|
695
|
+
websiteUrl: "https://hookmovies.com"
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
slug: "coolandlovely",
|
|
699
|
+
name: "CoolAndLovely",
|
|
700
|
+
websiteUrl: "https://coolandlovely.com"
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
slug: "atheletebridge",
|
|
704
|
+
name: "AthleteBridge",
|
|
705
|
+
websiteUrl: "https://atheletebridge.com"
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
slug: "labsofscience",
|
|
709
|
+
name: "LabsOfScience",
|
|
710
|
+
websiteUrl: "https://labsofscience.com"
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
slug: "ecoimpacthub",
|
|
714
|
+
name: "EcoImpactHub",
|
|
715
|
+
websiteUrl: "https://ecoimpacthub.com"
|
|
716
|
+
}
|
|
717
|
+
];
|
|
718
|
+
|
|
719
|
+
// src/utils/recaptcha-v3.ts
|
|
720
|
+
var SCRIPT_ID = "boff-recaptcha-v3";
|
|
721
|
+
var SCRIPT_SRC = "https://www.google.com/recaptcha/api.js?render=";
|
|
722
|
+
var EXPLORE_RECAPTCHA_ACTION = "explore_chat";
|
|
723
|
+
var loaders = /* @__PURE__ */ new Map();
|
|
724
|
+
function isBrowser() {
|
|
725
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
726
|
+
}
|
|
727
|
+
function loadRecaptchaV3(siteKey) {
|
|
728
|
+
if (!siteKey) {
|
|
729
|
+
return Promise.reject(new Error("reCAPTCHA site key is required"));
|
|
730
|
+
}
|
|
731
|
+
if (!isBrowser()) {
|
|
732
|
+
return Promise.reject(
|
|
733
|
+
new Error("reCAPTCHA v3 can only be loaded in a browser")
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
const existing = loaders.get(siteKey);
|
|
737
|
+
if (existing) return existing;
|
|
738
|
+
const promise = new Promise((resolve, reject) => {
|
|
739
|
+
const ready = () => {
|
|
740
|
+
const api = window.grecaptcha;
|
|
741
|
+
if (!api) {
|
|
742
|
+
reject(new Error("reCAPTCHA loaded but grecaptcha is unavailable"));
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
api.ready(() => resolve(api));
|
|
746
|
+
};
|
|
747
|
+
if (window.grecaptcha) {
|
|
748
|
+
ready();
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const scriptId = `${SCRIPT_ID}-${siteKey}`;
|
|
752
|
+
const found = document.getElementById(scriptId);
|
|
753
|
+
if (found) {
|
|
754
|
+
found.addEventListener("load", ready, { once: true });
|
|
755
|
+
found.addEventListener(
|
|
756
|
+
"error",
|
|
757
|
+
() => reject(new Error("Failed to load reCAPTCHA v3")),
|
|
758
|
+
{ once: true }
|
|
759
|
+
);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
const script = document.createElement("script");
|
|
763
|
+
script.id = scriptId;
|
|
764
|
+
script.src = `${SCRIPT_SRC}${encodeURIComponent(siteKey)}`;
|
|
765
|
+
script.async = true;
|
|
766
|
+
script.defer = true;
|
|
767
|
+
script.addEventListener("load", ready, { once: true });
|
|
768
|
+
script.addEventListener(
|
|
769
|
+
"error",
|
|
770
|
+
() => {
|
|
771
|
+
loaders.delete(siteKey);
|
|
772
|
+
reject(new Error("Failed to load reCAPTCHA v3"));
|
|
773
|
+
},
|
|
774
|
+
{ once: true }
|
|
775
|
+
);
|
|
776
|
+
document.head.appendChild(script);
|
|
777
|
+
});
|
|
778
|
+
loaders.set(siteKey, promise);
|
|
779
|
+
return promise;
|
|
780
|
+
}
|
|
781
|
+
async function getRecaptchaV3Token(siteKey, action = EXPLORE_RECAPTCHA_ACTION) {
|
|
782
|
+
const api = await loadRecaptchaV3(siteKey);
|
|
783
|
+
return api.execute(siteKey, { action });
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/hooks/use-explore-chat.ts
|
|
787
|
+
var DEFAULT_STORAGE_KEY = "boff.explore.v1";
|
|
788
|
+
var DEFAULT_POLL_INTERVAL_MS = 1500;
|
|
789
|
+
var DEFAULT_POLL_TIMEOUT_MS = 9e4;
|
|
790
|
+
var POLL_REQUEST_TIMEOUT_MS = 1e4;
|
|
791
|
+
var SEND_REQUEST_TIMEOUT_MS = 18e3;
|
|
792
|
+
var MAX_CONSECUTIVE_POLL_FAILURES = 4;
|
|
793
|
+
var MAX_PERSISTED_MESSAGES = 40;
|
|
794
|
+
var FALLBACK_EXPLORE_LIMITS = {
|
|
795
|
+
maxMessageLength: 2e3,
|
|
796
|
+
maxTurnsPerConversation: 20,
|
|
797
|
+
messagesPerDay: 100,
|
|
798
|
+
historyTurns: 6
|
|
799
|
+
};
|
|
800
|
+
var DEFAULT_EXPLORE_EXAMPLE_PROMPTS = [
|
|
801
|
+
"What does this product actually do?",
|
|
802
|
+
"Which Burdenoff product fits my team?",
|
|
803
|
+
"How do I get started, step by step?",
|
|
804
|
+
"What does it integrate with?"
|
|
805
|
+
];
|
|
806
|
+
var FALLBACK_WELCOME_TITLE = "Ask anything about our products";
|
|
807
|
+
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.";
|
|
808
|
+
function buildFallbackCatalog(examplePrompts) {
|
|
809
|
+
return {
|
|
810
|
+
enabled: true,
|
|
811
|
+
products: ECOSYSTEM_PRODUCTS.map((p) => ({
|
|
812
|
+
slug: p.slug,
|
|
813
|
+
name: p.name,
|
|
814
|
+
tagline: null,
|
|
815
|
+
website: p.websiteUrl
|
|
816
|
+
})),
|
|
817
|
+
examplePrompts: examplePrompts?.length ? examplePrompts : DEFAULT_EXPLORE_EXAMPLE_PROMPTS,
|
|
818
|
+
welcomeTitle: FALLBACK_WELCOME_TITLE,
|
|
819
|
+
welcomeBody: FALLBACK_WELCOME_BODY,
|
|
820
|
+
limits: FALLBACK_EXPLORE_LIMITS,
|
|
821
|
+
captchaRequired: false,
|
|
822
|
+
recaptchaSiteKey: null
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
function normalizeCatalog(raw, fallback) {
|
|
826
|
+
return {
|
|
827
|
+
enabled: raw.enabled !== false,
|
|
828
|
+
products: raw.products?.length ? raw.products : fallback.products,
|
|
829
|
+
examplePrompts: raw.examplePrompts?.length ? raw.examplePrompts : fallback.examplePrompts,
|
|
830
|
+
welcomeTitle: raw.welcomeTitle || fallback.welcomeTitle,
|
|
831
|
+
welcomeBody: raw.welcomeBody || fallback.welcomeBody,
|
|
832
|
+
limits: {
|
|
833
|
+
maxMessageLength: raw.limits?.maxMessageLength || fallback.limits.maxMessageLength,
|
|
834
|
+
maxTurnsPerConversation: raw.limits?.maxTurnsPerConversation || fallback.limits.maxTurnsPerConversation,
|
|
835
|
+
messagesPerDay: raw.limits?.messagesPerDay || fallback.limits.messagesPerDay,
|
|
836
|
+
historyTurns: raw.limits?.historyTurns || fallback.limits.historyTurns
|
|
837
|
+
},
|
|
838
|
+
captchaRequired: raw.captchaRequired === true,
|
|
839
|
+
recaptchaSiteKey: raw.recaptchaSiteKey ?? null
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
var RATE_LIMIT_CODES = /* @__PURE__ */ new Set([
|
|
843
|
+
"RATE_LIMITED",
|
|
844
|
+
"HTTP_429",
|
|
845
|
+
"EXPLORE_GLOBAL_LIMIT"
|
|
846
|
+
]);
|
|
847
|
+
var CAPTCHA_CODES = /* @__PURE__ */ new Set(["CAPTCHA_FAILED", "CAPTCHA_REQUIRED"]);
|
|
848
|
+
var NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
849
|
+
"NETWORK_ERROR",
|
|
850
|
+
"UPSTREAM_UNAVAILABLE",
|
|
851
|
+
"CONVERSATION_BUSY",
|
|
852
|
+
"PROVIDER_ERROR",
|
|
853
|
+
"QUEUE_FULL"
|
|
854
|
+
]);
|
|
855
|
+
var VALIDATION_CODES = /* @__PURE__ */ new Set([
|
|
856
|
+
"MESSAGE_TOO_LONG",
|
|
857
|
+
"MESSAGE_EMPTY",
|
|
858
|
+
"CONVERSATION_LIMIT",
|
|
859
|
+
"INVALID_INPUT"
|
|
860
|
+
]);
|
|
861
|
+
function classifyExploreError(code, serverMessage) {
|
|
862
|
+
const c = code ?? "UNKNOWN";
|
|
863
|
+
if (RATE_LIMIT_CODES.has(c)) {
|
|
864
|
+
return {
|
|
865
|
+
kind: "rate-limit",
|
|
866
|
+
code: c,
|
|
867
|
+
message: "You've reached the message limit for now. Please try again later.",
|
|
868
|
+
retryable: false
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
if (CAPTCHA_CODES.has(c)) {
|
|
872
|
+
return {
|
|
873
|
+
kind: "captcha",
|
|
874
|
+
code: c,
|
|
875
|
+
message: "We couldn't verify that you're human. Please try sending that again.",
|
|
876
|
+
retryable: true
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
if (c === "TIMEOUT" || c === "TOOL_BUDGET") {
|
|
880
|
+
return {
|
|
881
|
+
kind: "timeout",
|
|
882
|
+
code: c,
|
|
883
|
+
message: "That answer took too long to come back. Try asking again.",
|
|
884
|
+
retryable: true
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
if (NETWORK_CODES.has(c)) {
|
|
888
|
+
return {
|
|
889
|
+
kind: "network",
|
|
890
|
+
code: c,
|
|
891
|
+
message: "We couldn't reach the assistant just now. Check your connection and try again.",
|
|
892
|
+
retryable: true
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
if (c === "EXPLORE_DISABLED") {
|
|
896
|
+
return {
|
|
897
|
+
kind: "disabled",
|
|
898
|
+
code: c,
|
|
899
|
+
message: "Explore is unavailable at the moment. Please check back soon.",
|
|
900
|
+
retryable: false
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
if (VALIDATION_CODES.has(c)) {
|
|
904
|
+
return {
|
|
905
|
+
kind: "validation",
|
|
906
|
+
code: c,
|
|
907
|
+
message: serverMessage || "That message couldn't be sent.",
|
|
908
|
+
retryable: false
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
if (c === "EMPTY_ANSWER") {
|
|
912
|
+
return {
|
|
913
|
+
kind: "unknown",
|
|
914
|
+
code: c,
|
|
915
|
+
message: "No answer came back for that one. Try rephrasing, or ask something more specific.",
|
|
916
|
+
retryable: true
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
return {
|
|
920
|
+
kind: "unknown",
|
|
921
|
+
code: c,
|
|
922
|
+
message: serverMessage || "Something went wrong. Please try again.",
|
|
923
|
+
retryable: true
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
function readPersisted(key) {
|
|
927
|
+
if (typeof window === "undefined") return null;
|
|
928
|
+
try {
|
|
929
|
+
const raw = window.sessionStorage.getItem(key);
|
|
930
|
+
if (!raw) return null;
|
|
931
|
+
const parsed = JSON.parse(raw);
|
|
932
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
933
|
+
const record = parsed;
|
|
934
|
+
return {
|
|
935
|
+
conversationToken: typeof record.conversationToken === "string" ? record.conversationToken : null,
|
|
936
|
+
messages: Array.isArray(record.messages) ? record.messages.slice(-MAX_PERSISTED_MESSAGES) : [],
|
|
937
|
+
focusProduct: typeof record.focusProduct === "string" ? record.focusProduct : null
|
|
938
|
+
};
|
|
939
|
+
} catch {
|
|
940
|
+
return null;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
function visibleText(message) {
|
|
944
|
+
return message.content || message.partialContent || "";
|
|
945
|
+
}
|
|
946
|
+
function countUserTurns(messages) {
|
|
947
|
+
return messages.filter((m) => m.role === "USER").length;
|
|
948
|
+
}
|
|
949
|
+
function useExploreChat(options = {}) {
|
|
950
|
+
const {
|
|
951
|
+
productSlug,
|
|
952
|
+
pageUrl,
|
|
953
|
+
locale,
|
|
954
|
+
examplePrompts,
|
|
955
|
+
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
|
|
956
|
+
pollTimeoutMs = DEFAULT_POLL_TIMEOUT_MS,
|
|
957
|
+
storageKey = DEFAULT_STORAGE_KEY,
|
|
958
|
+
persist = true,
|
|
959
|
+
onSend
|
|
960
|
+
} = options;
|
|
961
|
+
const client = useWebSDK();
|
|
962
|
+
const promptKey = JSON.stringify(examplePrompts ?? []);
|
|
963
|
+
const fallbackCatalog = React.useMemo(
|
|
964
|
+
() => buildFallbackCatalog(JSON.parse(promptKey)),
|
|
965
|
+
[promptKey]
|
|
966
|
+
);
|
|
967
|
+
const [catalog, setCatalog] = React.useState(fallbackCatalog);
|
|
968
|
+
const [messages, setMessages] = React.useState([]);
|
|
969
|
+
const [phase, setPhase] = React.useState("loading");
|
|
970
|
+
const [error, setError] = React.useState(null);
|
|
971
|
+
const [remainingToday, setRemainingToday] = React.useState(null);
|
|
972
|
+
const [focusProduct, setFocusProductState] = React.useState(null);
|
|
973
|
+
const [turnCount, setTurnCount] = React.useState(0);
|
|
974
|
+
const [hydrated, setHydrated] = React.useState(false);
|
|
975
|
+
const mountedRef = React.useRef(true);
|
|
976
|
+
const pollGenerationRef = React.useRef(0);
|
|
977
|
+
const pollTimerRef = React.useRef(null);
|
|
978
|
+
const conversationTokenRef = React.useRef(null);
|
|
979
|
+
const catalogRef = React.useRef(fallbackCatalog);
|
|
980
|
+
const turnCountRef = React.useRef(0);
|
|
981
|
+
const focusProductRef = React.useRef(null);
|
|
982
|
+
const lastSentTextRef = React.useRef(null);
|
|
983
|
+
const inFlightRef = React.useRef(false);
|
|
984
|
+
catalogRef.current = catalog;
|
|
985
|
+
const clearPollTimer = React.useCallback(() => {
|
|
986
|
+
if (pollTimerRef.current !== null) {
|
|
987
|
+
clearTimeout(pollTimerRef.current);
|
|
988
|
+
pollTimerRef.current = null;
|
|
989
|
+
}
|
|
990
|
+
}, []);
|
|
991
|
+
const cancelPolling = React.useCallback(() => {
|
|
992
|
+
pollGenerationRef.current += 1;
|
|
993
|
+
clearPollTimer();
|
|
994
|
+
}, [clearPollTimer]);
|
|
995
|
+
React.useEffect(() => {
|
|
996
|
+
mountedRef.current = true;
|
|
997
|
+
return () => {
|
|
998
|
+
mountedRef.current = false;
|
|
999
|
+
pollGenerationRef.current += 1;
|
|
1000
|
+
if (pollTimerRef.current !== null) {
|
|
1001
|
+
clearTimeout(pollTimerRef.current);
|
|
1002
|
+
pollTimerRef.current = null;
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
}, []);
|
|
1006
|
+
React.useEffect(() => {
|
|
1007
|
+
if (!persist) {
|
|
1008
|
+
setHydrated(true);
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
const stored = readPersisted(storageKey);
|
|
1012
|
+
if (stored) {
|
|
1013
|
+
conversationTokenRef.current = stored.conversationToken;
|
|
1014
|
+
focusProductRef.current = stored.focusProduct;
|
|
1015
|
+
const settled = stored.messages.flatMap((m) => {
|
|
1016
|
+
if (m.role !== "ASSISTANT") return [m];
|
|
1017
|
+
if (m.status !== "PENDING" && m.status !== "RUNNING") return [m];
|
|
1018
|
+
const text = visibleText(m);
|
|
1019
|
+
if (!text) return [];
|
|
1020
|
+
return [{ ...m, status: "COMPLETED", content: text }];
|
|
1021
|
+
});
|
|
1022
|
+
setMessages(settled);
|
|
1023
|
+
setFocusProductState(stored.focusProduct);
|
|
1024
|
+
const turns = countUserTurns(settled);
|
|
1025
|
+
turnCountRef.current = turns;
|
|
1026
|
+
setTurnCount(turns);
|
|
1027
|
+
}
|
|
1028
|
+
setHydrated(true);
|
|
1029
|
+
}, [persist, storageKey]);
|
|
1030
|
+
React.useEffect(() => {
|
|
1031
|
+
if (!persist || !hydrated || typeof window === "undefined") return;
|
|
1032
|
+
try {
|
|
1033
|
+
const payload = {
|
|
1034
|
+
conversationToken: conversationTokenRef.current,
|
|
1035
|
+
messages: messages.slice(-MAX_PERSISTED_MESSAGES),
|
|
1036
|
+
focusProduct
|
|
1037
|
+
};
|
|
1038
|
+
if (!payload.conversationToken && payload.messages.length === 0 && !payload.focusProduct) {
|
|
1039
|
+
window.sessionStorage.removeItem(storageKey);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
window.sessionStorage.setItem(storageKey, JSON.stringify(payload));
|
|
1043
|
+
} catch {
|
|
1044
|
+
}
|
|
1045
|
+
}, [persist, hydrated, storageKey, messages, focusProduct]);
|
|
1046
|
+
React.useEffect(() => {
|
|
1047
|
+
let cancelled = false;
|
|
1048
|
+
const load = async () => {
|
|
1049
|
+
const res = await client.query(
|
|
1050
|
+
EXPLORE_CATALOG_QUERY,
|
|
1051
|
+
{ productSlug: productSlug ?? null }
|
|
1052
|
+
);
|
|
1053
|
+
if (cancelled || !mountedRef.current) return;
|
|
1054
|
+
const raw = res.data?.exploreCatalog;
|
|
1055
|
+
if (res.errors?.length || !raw) {
|
|
1056
|
+
console.warn(
|
|
1057
|
+
"[WebSDK] exploreCatalog unavailable \u2014 using the built-in fallback catalog",
|
|
1058
|
+
res.errors?.[0]?.message
|
|
1059
|
+
);
|
|
1060
|
+
setPhase((prev) => prev === "loading" ? "ready" : prev);
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
const next = normalizeCatalog(raw, fallbackCatalog);
|
|
1064
|
+
setCatalog(next);
|
|
1065
|
+
catalogRef.current = next;
|
|
1066
|
+
setPhase((prev) => {
|
|
1067
|
+
if (!next.enabled) return "disabled";
|
|
1068
|
+
return prev === "loading" ? "ready" : prev;
|
|
1069
|
+
});
|
|
1070
|
+
};
|
|
1071
|
+
void load();
|
|
1072
|
+
return () => {
|
|
1073
|
+
cancelled = true;
|
|
1074
|
+
};
|
|
1075
|
+
}, [client, productSlug, fallbackCatalog]);
|
|
1076
|
+
const startPolling = React.useCallback(
|
|
1077
|
+
(conversationToken, messageId) => {
|
|
1078
|
+
pollGenerationRef.current += 1;
|
|
1079
|
+
const generation = pollGenerationRef.current;
|
|
1080
|
+
const startedAt = Date.now();
|
|
1081
|
+
let consecutiveFailures = 0;
|
|
1082
|
+
const isStale = () => !mountedRef.current || generation !== pollGenerationRef.current;
|
|
1083
|
+
const finishWithError = (chatError) => {
|
|
1084
|
+
inFlightRef.current = false;
|
|
1085
|
+
setMessages(
|
|
1086
|
+
(prev) => prev.flatMap((m) => {
|
|
1087
|
+
if (m.id !== messageId) return [m];
|
|
1088
|
+
const text = visibleText(m);
|
|
1089
|
+
if (!text) return [];
|
|
1090
|
+
return [{ ...m, status: "FAILED", content: text }];
|
|
1091
|
+
})
|
|
1092
|
+
);
|
|
1093
|
+
setError(chatError);
|
|
1094
|
+
setPhase(chatError.kind === "rate-limit" ? "limited" : "error");
|
|
1095
|
+
};
|
|
1096
|
+
const tick = async () => {
|
|
1097
|
+
if (isStale()) return;
|
|
1098
|
+
if (Date.now() - startedAt > pollTimeoutMs) {
|
|
1099
|
+
finishWithError(classifyExploreError("TIMEOUT"));
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
const res = await client.query(
|
|
1103
|
+
EXPLORE_MESSAGE_QUERY,
|
|
1104
|
+
{ conversationToken, id: messageId },
|
|
1105
|
+
{ cache: false, timeoutMs: POLL_REQUEST_TIMEOUT_MS }
|
|
1106
|
+
);
|
|
1107
|
+
if (isStale()) return;
|
|
1108
|
+
const first = res.errors?.[0];
|
|
1109
|
+
if (first) {
|
|
1110
|
+
const code = typeof first.extensions?.code === "string" ? first.extensions.code : void 0;
|
|
1111
|
+
const classified = classifyExploreError(code, first.message);
|
|
1112
|
+
if (!classified.retryable) {
|
|
1113
|
+
finishWithError(classified);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
consecutiveFailures += 1;
|
|
1117
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_POLL_FAILURES) {
|
|
1118
|
+
finishWithError(classified);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
pollTimerRef.current = setTimeout(() => void tick(), pollIntervalMs);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const message = res.data?.exploreMessage;
|
|
1125
|
+
if (!message) {
|
|
1126
|
+
consecutiveFailures += 1;
|
|
1127
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_POLL_FAILURES) {
|
|
1128
|
+
finishWithError(classifyExploreError("NOT_FOUND"));
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
pollTimerRef.current = setTimeout(() => void tick(), pollIntervalMs);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
consecutiveFailures = 0;
|
|
1135
|
+
setMessages(
|
|
1136
|
+
(prev) => prev.map((m) => m.id === messageId ? message : m)
|
|
1137
|
+
);
|
|
1138
|
+
if (message.status === "COMPLETED") {
|
|
1139
|
+
inFlightRef.current = false;
|
|
1140
|
+
setError(null);
|
|
1141
|
+
setPhase("ready");
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
if (message.status === "FAILED") {
|
|
1145
|
+
finishWithError(
|
|
1146
|
+
classifyExploreError(message.errorCode ?? "UPSTREAM_UNAVAILABLE")
|
|
1147
|
+
);
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
setPhase("streaming");
|
|
1151
|
+
pollTimerRef.current = setTimeout(() => void tick(), pollIntervalMs);
|
|
1152
|
+
};
|
|
1153
|
+
pollTimerRef.current = setTimeout(() => void tick(), pollIntervalMs);
|
|
1154
|
+
},
|
|
1155
|
+
[client, pollIntervalMs, pollTimeoutMs]
|
|
1156
|
+
);
|
|
1157
|
+
const submit = React.useCallback(
|
|
1158
|
+
async (text, optimisticId, captchaAttempt) => {
|
|
1159
|
+
const activeCatalog = catalogRef.current;
|
|
1160
|
+
let recaptchaToken;
|
|
1161
|
+
if (activeCatalog.captchaRequired && activeCatalog.recaptchaSiteKey) {
|
|
1162
|
+
try {
|
|
1163
|
+
recaptchaToken = await getRecaptchaV3Token(
|
|
1164
|
+
activeCatalog.recaptchaSiteKey
|
|
1165
|
+
);
|
|
1166
|
+
} catch (err) {
|
|
1167
|
+
console.warn("[WebSDK] reCAPTCHA v3 token could not be minted", err);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
const input = {
|
|
1171
|
+
message: text,
|
|
1172
|
+
...conversationTokenRef.current ? { conversationToken: conversationTokenRef.current } : {},
|
|
1173
|
+
...focusProductRef.current || productSlug ? { productSlug: focusProductRef.current ?? productSlug } : {},
|
|
1174
|
+
...recaptchaToken ? { recaptchaToken } : {},
|
|
1175
|
+
...locale ? { locale } : {},
|
|
1176
|
+
...pageUrl ? { pageUrl } : {}
|
|
1177
|
+
};
|
|
1178
|
+
const res = await client.mutate(
|
|
1179
|
+
SEND_EXPLORE_MESSAGE_MUTATION,
|
|
1180
|
+
{ input },
|
|
1181
|
+
{ timeoutMs: SEND_REQUEST_TIMEOUT_MS }
|
|
1182
|
+
);
|
|
1183
|
+
if (!mountedRef.current) return;
|
|
1184
|
+
const first = res.errors?.[0];
|
|
1185
|
+
const result = res.data?.sendExploreMessage;
|
|
1186
|
+
if (first || !result) {
|
|
1187
|
+
const code = typeof first?.extensions?.code === "string" ? first.extensions.code : void 0;
|
|
1188
|
+
const classified = classifyExploreError(code, first?.message);
|
|
1189
|
+
if (classified.kind === "captcha" && captchaAttempt === 0) {
|
|
1190
|
+
await submit(text, optimisticId, 1);
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
inFlightRef.current = false;
|
|
1194
|
+
setError(classified);
|
|
1195
|
+
setPhase(classified.kind === "rate-limit" ? "limited" : "error");
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
conversationTokenRef.current = result.conversation.token;
|
|
1199
|
+
turnCountRef.current = result.conversation.turnCount;
|
|
1200
|
+
setTurnCount(result.conversation.turnCount);
|
|
1201
|
+
setRemainingToday(result.remainingToday ?? null);
|
|
1202
|
+
const userMessage = result.userMessage;
|
|
1203
|
+
const assistantMessage = result.assistantMessage;
|
|
1204
|
+
setMessages((prev) => {
|
|
1205
|
+
const replaced = prev.some((m) => m.id === optimisticId) ? prev.map((m) => m.id === optimisticId ? userMessage : m) : [...prev, userMessage];
|
|
1206
|
+
return [...replaced, assistantMessage];
|
|
1207
|
+
});
|
|
1208
|
+
if (assistantMessage.status === "COMPLETED" || assistantMessage.status === "FAILED") {
|
|
1209
|
+
inFlightRef.current = false;
|
|
1210
|
+
if (assistantMessage.status === "FAILED") {
|
|
1211
|
+
const classified = classifyExploreError(
|
|
1212
|
+
assistantMessage.errorCode ?? "UPSTREAM_UNAVAILABLE"
|
|
1213
|
+
);
|
|
1214
|
+
setError(classified);
|
|
1215
|
+
setPhase("error");
|
|
1216
|
+
} else {
|
|
1217
|
+
setPhase("ready");
|
|
1218
|
+
}
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
setPhase("streaming");
|
|
1222
|
+
startPolling(result.conversation.token, assistantMessage.id);
|
|
1223
|
+
},
|
|
1224
|
+
[client, locale, pageUrl, productSlug, startPolling]
|
|
1225
|
+
);
|
|
1226
|
+
const send = React.useCallback(
|
|
1227
|
+
async (message) => {
|
|
1228
|
+
const text = message.trim();
|
|
1229
|
+
if (!text) return;
|
|
1230
|
+
if (inFlightRef.current) return;
|
|
1231
|
+
const activeCatalog = catalogRef.current;
|
|
1232
|
+
if (!activeCatalog.enabled) {
|
|
1233
|
+
setError(classifyExploreError("EXPLORE_DISABLED"));
|
|
1234
|
+
setPhase("disabled");
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
if (text.length > activeCatalog.limits.maxMessageLength) {
|
|
1238
|
+
setError({
|
|
1239
|
+
kind: "validation",
|
|
1240
|
+
code: "MESSAGE_TOO_LONG",
|
|
1241
|
+
message: `That message is ${text.length} characters \u2014 the limit is ${activeCatalog.limits.maxMessageLength}.`,
|
|
1242
|
+
retryable: false
|
|
1243
|
+
});
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
if (turnCountRef.current >= activeCatalog.limits.maxTurnsPerConversation) {
|
|
1247
|
+
setError({
|
|
1248
|
+
kind: "validation",
|
|
1249
|
+
code: "CONVERSATION_LIMIT",
|
|
1250
|
+
message: `This conversation has reached its ${activeCatalog.limits.maxTurnsPerConversation}-message limit. Start a new chat to keep going.`,
|
|
1251
|
+
retryable: false
|
|
1252
|
+
});
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
inFlightRef.current = true;
|
|
1256
|
+
lastSentTextRef.current = text;
|
|
1257
|
+
setError(null);
|
|
1258
|
+
setPhase("sending");
|
|
1259
|
+
cancelPolling();
|
|
1260
|
+
const optimistic = {
|
|
1261
|
+
id: makeOptimisticId(),
|
|
1262
|
+
conversationId: conversationTokenRef.current ?? "pending",
|
|
1263
|
+
role: "USER",
|
|
1264
|
+
status: "COMPLETED",
|
|
1265
|
+
content: text,
|
|
1266
|
+
partialContent: null,
|
|
1267
|
+
activity: [],
|
|
1268
|
+
references: [],
|
|
1269
|
+
ctas: [],
|
|
1270
|
+
errorCode: null,
|
|
1271
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1272
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1273
|
+
};
|
|
1274
|
+
setMessages((prev) => [...prev, optimistic]);
|
|
1275
|
+
turnCountRef.current += 1;
|
|
1276
|
+
setTurnCount(turnCountRef.current);
|
|
1277
|
+
onSend?.(text);
|
|
1278
|
+
try {
|
|
1279
|
+
await submit(text, optimistic.id, 0);
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
if (!mountedRef.current) return;
|
|
1282
|
+
console.error("[WebSDK] Explore send failed", err);
|
|
1283
|
+
inFlightRef.current = false;
|
|
1284
|
+
setError(classifyExploreError("NETWORK_ERROR"));
|
|
1285
|
+
setPhase("error");
|
|
1286
|
+
}
|
|
1287
|
+
},
|
|
1288
|
+
[cancelPolling, onSend, submit]
|
|
1289
|
+
);
|
|
1290
|
+
const stop = React.useCallback(() => {
|
|
1291
|
+
cancelPolling();
|
|
1292
|
+
inFlightRef.current = false;
|
|
1293
|
+
setMessages(
|
|
1294
|
+
(prev) => prev.flatMap((m) => {
|
|
1295
|
+
if (m.role !== "ASSISTANT") return [m];
|
|
1296
|
+
if (m.status !== "PENDING" && m.status !== "RUNNING") return [m];
|
|
1297
|
+
const text = visibleText(m);
|
|
1298
|
+
if (!text) return [];
|
|
1299
|
+
return [
|
|
1300
|
+
{
|
|
1301
|
+
...m,
|
|
1302
|
+
status: "COMPLETED",
|
|
1303
|
+
content: text,
|
|
1304
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1305
|
+
}
|
|
1306
|
+
];
|
|
1307
|
+
})
|
|
1308
|
+
);
|
|
1309
|
+
setPhase(
|
|
1310
|
+
(prev) => prev === "sending" || prev === "streaming" ? "ready" : prev
|
|
1311
|
+
);
|
|
1312
|
+
}, [cancelPolling]);
|
|
1313
|
+
const retry = React.useCallback(() => {
|
|
1314
|
+
const text = lastSentTextRef.current;
|
|
1315
|
+
if (!text) return;
|
|
1316
|
+
cancelPolling();
|
|
1317
|
+
inFlightRef.current = false;
|
|
1318
|
+
setMessages((prev) => {
|
|
1319
|
+
const next = [...prev];
|
|
1320
|
+
const last = next[next.length - 1];
|
|
1321
|
+
if (last?.role === "ASSISTANT" && last.status === "FAILED") next.pop();
|
|
1322
|
+
const prior = next[next.length - 1];
|
|
1323
|
+
if (prior?.role === "USER" && prior.content === text) next.pop();
|
|
1324
|
+
return next;
|
|
1325
|
+
});
|
|
1326
|
+
turnCountRef.current = Math.max(0, turnCountRef.current - 1);
|
|
1327
|
+
setTurnCount(turnCountRef.current);
|
|
1328
|
+
void send(text);
|
|
1329
|
+
}, [cancelPolling, send]);
|
|
1330
|
+
const reset = React.useCallback(() => {
|
|
1331
|
+
cancelPolling();
|
|
1332
|
+
inFlightRef.current = false;
|
|
1333
|
+
conversationTokenRef.current = null;
|
|
1334
|
+
lastSentTextRef.current = null;
|
|
1335
|
+
turnCountRef.current = 0;
|
|
1336
|
+
setTurnCount(0);
|
|
1337
|
+
setMessages([]);
|
|
1338
|
+
setError(null);
|
|
1339
|
+
setRemainingToday(null);
|
|
1340
|
+
setPhase(catalogRef.current.enabled ? "ready" : "disabled");
|
|
1341
|
+
if (typeof window !== "undefined") {
|
|
1342
|
+
try {
|
|
1343
|
+
window.sessionStorage.removeItem(storageKey);
|
|
1344
|
+
} catch {
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}, [cancelPolling, storageKey]);
|
|
1348
|
+
const setFocusProduct = React.useCallback((slug) => {
|
|
1349
|
+
focusProductRef.current = slug;
|
|
1350
|
+
setFocusProductState(slug);
|
|
1351
|
+
}, []);
|
|
1352
|
+
const activity = React.useMemo(() => {
|
|
1353
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
1354
|
+
const m = messages[i];
|
|
1355
|
+
if (!m || m.role !== "ASSISTANT") continue;
|
|
1356
|
+
if (m.status !== "PENDING" && m.status !== "RUNNING") return null;
|
|
1357
|
+
const lines = m.activity ?? [];
|
|
1358
|
+
return lines.length ? lines[lines.length - 1] ?? null : null;
|
|
1359
|
+
}
|
|
1360
|
+
return null;
|
|
1361
|
+
}, [messages]);
|
|
1362
|
+
const canSend = (phase === "ready" || phase === "error") && catalog.enabled && turnCount < catalog.limits.maxTurnsPerConversation;
|
|
1363
|
+
return {
|
|
1364
|
+
phase,
|
|
1365
|
+
catalog,
|
|
1366
|
+
messages,
|
|
1367
|
+
activity,
|
|
1368
|
+
remainingToday,
|
|
1369
|
+
error,
|
|
1370
|
+
focusProduct,
|
|
1371
|
+
setFocusProduct,
|
|
1372
|
+
send,
|
|
1373
|
+
stop,
|
|
1374
|
+
retry,
|
|
1375
|
+
reset,
|
|
1376
|
+
canSend
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
var optimisticCounter = 0;
|
|
1380
|
+
function makeOptimisticId() {
|
|
1381
|
+
optimisticCounter += 1;
|
|
1382
|
+
return `boff-explore-local-${Date.now()}-${optimisticCounter}`;
|
|
1383
|
+
}
|
|
1384
|
+
function canonicalFromLocation() {
|
|
1385
|
+
if (typeof window === "undefined" || !window.location) return void 0;
|
|
1386
|
+
const { origin, pathname } = window.location;
|
|
1387
|
+
if (!origin || origin === "null") return void 0;
|
|
1388
|
+
const path = pathname === "/" ? "/" : pathname.replace(/\/+$/, "");
|
|
1389
|
+
return `${origin}${path}`;
|
|
1390
|
+
}
|
|
1391
|
+
function resolveImageUrl(image, base) {
|
|
1392
|
+
if (!image || /^https?:\/\//i.test(image)) return image;
|
|
1393
|
+
if (!base) return image;
|
|
1394
|
+
try {
|
|
1395
|
+
return new URL(image, base).toString();
|
|
1396
|
+
} catch {
|
|
1397
|
+
return image;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
function PageHead({
|
|
1401
|
+
title,
|
|
1402
|
+
description,
|
|
1403
|
+
productName,
|
|
1404
|
+
keywords,
|
|
1405
|
+
image,
|
|
1406
|
+
url,
|
|
1407
|
+
favicon,
|
|
1408
|
+
robots = "index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1",
|
|
1409
|
+
locale = "en_US",
|
|
1410
|
+
twitterHandle,
|
|
1411
|
+
themeColor = "#000000",
|
|
1412
|
+
structuredData,
|
|
1413
|
+
additionalMeta
|
|
1414
|
+
}) {
|
|
1415
|
+
const fullTitle = productName && !title.includes(productName) ? `${title} | ${productName}` : title;
|
|
1416
|
+
const resolvedUrl = url ?? canonicalFromLocation();
|
|
1417
|
+
const fullImageUrl = resolveImageUrl(image, resolvedUrl);
|
|
1418
|
+
const structuredDataItems = structuredData ? Array.isArray(structuredData) ? structuredData : [structuredData] : [];
|
|
1419
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(reactHelmetAsync.Helmet, { children: [
|
|
1420
|
+
/* @__PURE__ */ jsxRuntime.jsx("title", { children: fullTitle }),
|
|
1421
|
+
/* @__PURE__ */ jsxRuntime.jsx("meta", { name: "title", content: fullTitle }),
|
|
1422
|
+
description && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "description", content: description }),
|
|
1423
|
+
keywords && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "keywords", content: keywords }),
|
|
1424
|
+
/* @__PURE__ */ jsxRuntime.jsx("meta", { name: "author", content: "Burdenoff Consultancy Services Pvt Ltd" }),
|
|
1425
|
+
/* @__PURE__ */ jsxRuntime.jsx("meta", { name: "robots", content: robots }),
|
|
1426
|
+
resolvedUrl && /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "canonical", href: resolvedUrl }),
|
|
1427
|
+
favicon && /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "icon", href: favicon }),
|
|
1428
|
+
/* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:type", content: "website" }),
|
|
1429
|
+
/* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:title", content: fullTitle }),
|
|
1430
|
+
description && /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:description", content: description }),
|
|
1431
|
+
fullImageUrl && /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:image", content: fullImageUrl }),
|
|
1432
|
+
resolvedUrl && /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:url", content: resolvedUrl }),
|
|
1433
|
+
/* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:locale", content: locale }),
|
|
1434
|
+
productName && /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:site_name", content: productName }),
|
|
1435
|
+
/* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:card", content: "summary_large_image" }),
|
|
1436
|
+
/* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:title", content: fullTitle }),
|
|
1437
|
+
description && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:description", content: description }),
|
|
1438
|
+
fullImageUrl && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:image", content: fullImageUrl }),
|
|
1439
|
+
twitterHandle && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:creator", content: twitterHandle }),
|
|
1440
|
+
/* @__PURE__ */ jsxRuntime.jsx("meta", { name: "theme-color", content: themeColor }),
|
|
1441
|
+
structuredDataItems.map((data, index) => /* @__PURE__ */ jsxRuntime.jsx("script", { type: "application/ld+json", children: JSON.stringify(data) }, `ld-${index}`)),
|
|
1442
|
+
additionalMeta?.map((meta, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1443
|
+
"meta",
|
|
1444
|
+
{
|
|
1445
|
+
...meta.name ? { name: meta.name } : {},
|
|
1446
|
+
...meta.property ? { property: meta.property } : {},
|
|
1447
|
+
content: meta.content
|
|
1448
|
+
},
|
|
1449
|
+
`meta-${index}`
|
|
1450
|
+
))
|
|
1451
|
+
] });
|
|
1452
|
+
}
|
|
1453
|
+
function cn(...inputs) {
|
|
1454
|
+
return tailwindMerge.twMerge(clsx.clsx(inputs));
|
|
1455
|
+
}
|
|
1456
|
+
var buttonVariants = classVarianceAuthority.cva(
|
|
1457
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
1458
|
+
{
|
|
1459
|
+
variants: {
|
|
1460
|
+
variant: {
|
|
1461
|
+
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
1462
|
+
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
|
1463
|
+
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
1464
|
+
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
|
1465
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
1466
|
+
link: "text-primary underline-offset-4 hover:underline"
|
|
1467
|
+
},
|
|
1468
|
+
size: {
|
|
1469
|
+
default: "h-9 px-4 py-2",
|
|
1470
|
+
sm: "h-8 rounded-md px-3 text-xs",
|
|
1471
|
+
lg: "h-10 rounded-md px-8",
|
|
1472
|
+
icon: "h-9 w-9"
|
|
1473
|
+
}
|
|
1474
|
+
},
|
|
1475
|
+
defaultVariants: {
|
|
1476
|
+
variant: "default",
|
|
1477
|
+
size: "default"
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
);
|
|
1481
|
+
var Button = React__namespace.forwardRef(
|
|
1482
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
1483
|
+
const Comp = asChild ? reactSlot.Slot : "button";
|
|
1484
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1485
|
+
Comp,
|
|
1486
|
+
{
|
|
1487
|
+
className: cn(buttonVariants({ variant, size, className })),
|
|
1488
|
+
ref,
|
|
1489
|
+
...props
|
|
1490
|
+
}
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
);
|
|
1494
|
+
Button.displayName = "Button";
|
|
1495
|
+
var Textarea = React__namespace.forwardRef(({ className, ...props }, ref) => {
|
|
1496
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1497
|
+
"textarea",
|
|
1498
|
+
{
|
|
1499
|
+
className: cn(
|
|
1500
|
+
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
1501
|
+
className
|
|
1502
|
+
),
|
|
1503
|
+
ref,
|
|
1504
|
+
...props
|
|
1505
|
+
}
|
|
1506
|
+
);
|
|
1507
|
+
});
|
|
1508
|
+
Textarea.displayName = "Textarea";
|
|
1509
|
+
var EXPLORE_CSS = `
|
|
1510
|
+
@keyframes boff-explore-pulse {
|
|
1511
|
+
0%, 80%, 100% { opacity: 0.25; transform: translateY(0); }
|
|
1512
|
+
40% { opacity: 1; transform: translateY(-2px); }
|
|
1513
|
+
}
|
|
1514
|
+
.boff-explore-dot {
|
|
1515
|
+
display: inline-block;
|
|
1516
|
+
width: 5px;
|
|
1517
|
+
height: 5px;
|
|
1518
|
+
border-radius: 9999px;
|
|
1519
|
+
background-color: hsl(var(--muted-foreground));
|
|
1520
|
+
animation: boff-explore-pulse 1.2s ease-in-out infinite;
|
|
1521
|
+
}
|
|
1522
|
+
@keyframes boff-explore-caret {
|
|
1523
|
+
0%, 45% { opacity: 1; }
|
|
1524
|
+
50%, 100% { opacity: 0; }
|
|
1525
|
+
}
|
|
1526
|
+
.boff-explore-caret {
|
|
1527
|
+
display: inline-block;
|
|
1528
|
+
width: 2px;
|
|
1529
|
+
height: 1em;
|
|
1530
|
+
margin-left: 2px;
|
|
1531
|
+
vertical-align: text-bottom;
|
|
1532
|
+
background-color: hsl(var(--primary));
|
|
1533
|
+
animation: boff-explore-caret 1s step-end infinite;
|
|
1534
|
+
}
|
|
1535
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1536
|
+
.boff-explore-dot,
|
|
1537
|
+
.boff-explore-caret { animation: none; }
|
|
1538
|
+
.boff-explore-dot { opacity: 0.6; }
|
|
1539
|
+
}
|
|
1540
|
+
.boff-explore-scroll {
|
|
1541
|
+
scrollbar-width: thin;
|
|
1542
|
+
scrollbar-color: hsl(var(--border)) transparent;
|
|
1543
|
+
}
|
|
1544
|
+
.boff-explore-scroll::-webkit-scrollbar { width: 10px; }
|
|
1545
|
+
.boff-explore-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
1546
|
+
.boff-explore-scroll::-webkit-scrollbar-thumb {
|
|
1547
|
+
background-color: hsl(var(--border));
|
|
1548
|
+
border: 3px solid transparent;
|
|
1549
|
+
border-radius: 9999px;
|
|
1550
|
+
background-clip: content-box;
|
|
1551
|
+
}
|
|
1552
|
+
`;
|
|
1553
|
+
function ExploreStyles() {
|
|
1554
|
+
return /* @__PURE__ */ jsxRuntime.jsx("style", { dangerouslySetInnerHTML: { __html: EXPLORE_CSS } });
|
|
1555
|
+
}
|
|
1556
|
+
var CAPABILITIES = [
|
|
1557
|
+
{
|
|
1558
|
+
icon: lucideReact.BookOpenText,
|
|
1559
|
+
title: "Grounded in our own docs",
|
|
1560
|
+
body: "Every answer comes from published product pages and documentation \u2014 never invented."
|
|
1561
|
+
},
|
|
1562
|
+
{
|
|
1563
|
+
icon: lucideReact.Layers,
|
|
1564
|
+
title: "Compare across the ecosystem",
|
|
1565
|
+
body: "Ask which product fits your team, or how two of them work together."
|
|
1566
|
+
},
|
|
1567
|
+
{
|
|
1568
|
+
icon: lucideReact.Link2,
|
|
1569
|
+
title: "Straight to the right page",
|
|
1570
|
+
body: "Get the sources and next steps \u2014 pricing, docs, a demo \u2014 without hunting."
|
|
1571
|
+
}
|
|
1572
|
+
];
|
|
1573
|
+
var PRODUCT_CHIP_PREVIEW = 12;
|
|
1574
|
+
var CTA_VARIANTS = {
|
|
1575
|
+
CONTACT: "default",
|
|
1576
|
+
WAITLIST: "default",
|
|
1577
|
+
APP: "default",
|
|
1578
|
+
PRICING: "secondary",
|
|
1579
|
+
STORE: "secondary",
|
|
1580
|
+
PARTNERS: "secondary",
|
|
1581
|
+
DOCS: "outline",
|
|
1582
|
+
SERVICES: "outline",
|
|
1583
|
+
OTHER: "outline"
|
|
1584
|
+
};
|
|
1585
|
+
function normalizeOffset(offset) {
|
|
1586
|
+
if (offset === void 0) return "0px";
|
|
1587
|
+
return typeof offset === "number" ? `${offset}px` : offset;
|
|
1588
|
+
}
|
|
1589
|
+
function isInternalPath(url) {
|
|
1590
|
+
return url.startsWith("/") && !url.startsWith("//");
|
|
1591
|
+
}
|
|
1592
|
+
function hostnameOf(url) {
|
|
1593
|
+
try {
|
|
1594
|
+
return new URL(url).hostname.replace(/^www\./, "");
|
|
1595
|
+
} catch {
|
|
1596
|
+
return "";
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
function initialOf(value, fallback) {
|
|
1600
|
+
const source = (value || fallback).trim();
|
|
1601
|
+
return source ? source.slice(0, 1).toUpperCase() : "?";
|
|
1602
|
+
}
|
|
1603
|
+
function ReferenceCard({ reference }) {
|
|
1604
|
+
const [imageFailed, setImageFailed] = React.useState(false);
|
|
1605
|
+
const host = hostnameOf(reference.url);
|
|
1606
|
+
const showImage = Boolean(reference.imageUrl) && !imageFailed;
|
|
1607
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1608
|
+
"a",
|
|
1609
|
+
{
|
|
1610
|
+
"data-boff-explore": "reference",
|
|
1611
|
+
href: reference.url,
|
|
1612
|
+
target: "_blank",
|
|
1613
|
+
rel: "noopener noreferrer",
|
|
1614
|
+
className: "group flex flex-col overflow-hidden rounded-xl border border-border bg-card transition-colors hover:border-primary/40 hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
1615
|
+
children: [
|
|
1616
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative aspect-[16/9] w-full overflow-hidden bg-muted", children: showImage ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1617
|
+
"img",
|
|
1618
|
+
{
|
|
1619
|
+
src: reference.imageUrl ?? "",
|
|
1620
|
+
alt: "",
|
|
1621
|
+
loading: "lazy",
|
|
1622
|
+
onError: () => setImageFailed(true),
|
|
1623
|
+
className: "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
1624
|
+
}
|
|
1625
|
+
) : (
|
|
1626
|
+
/* Graceful fallback: a token-derived gradient plate, so a missing or
|
|
1627
|
+
broken preview still reads as a deliberate card, not a hole. */
|
|
1628
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full w-full items-center justify-center bg-gradient-to-br from-primary/25 via-primary/5 to-secondary", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-2xl font-semibold text-primary/70", children: initialOf(reference.product, reference.title || host) }) })
|
|
1629
|
+
) }),
|
|
1630
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col gap-1.5 p-3", children: [
|
|
1631
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "line-clamp-2 text-sm font-semibold leading-snug text-card-foreground", children: reference.title || host || reference.url }),
|
|
1632
|
+
reference.description ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "line-clamp-2 text-xs leading-relaxed text-muted-foreground", children: reference.description }) : null,
|
|
1633
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-auto flex items-center gap-2 pt-2", children: [
|
|
1634
|
+
reference.product ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rounded-full bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground", children: reference.product }) : null,
|
|
1635
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-auto inline-flex min-w-0 items-center gap-1 text-xs text-muted-foreground", children: [
|
|
1636
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ExternalLink, { className: "h-3 w-3 shrink-0" }),
|
|
1637
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: host })
|
|
1638
|
+
] })
|
|
1639
|
+
] })
|
|
1640
|
+
] })
|
|
1641
|
+
]
|
|
1642
|
+
}
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
function CtaButton({
|
|
1646
|
+
cta,
|
|
1647
|
+
onNavigate
|
|
1648
|
+
}) {
|
|
1649
|
+
const variant = CTA_VARIANTS[cta.kind] ?? "outline";
|
|
1650
|
+
const internal = isInternalPath(cta.url);
|
|
1651
|
+
if (internal && onNavigate) {
|
|
1652
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1653
|
+
Button,
|
|
1654
|
+
{
|
|
1655
|
+
"data-boff-explore": "cta",
|
|
1656
|
+
"data-kind": cta.kind,
|
|
1657
|
+
type: "button",
|
|
1658
|
+
size: "sm",
|
|
1659
|
+
variant,
|
|
1660
|
+
onClick: () => onNavigate(cta.url),
|
|
1661
|
+
children: cta.label
|
|
1662
|
+
}
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Button, { asChild: true, size: "sm", variant, children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1666
|
+
"a",
|
|
1667
|
+
{
|
|
1668
|
+
"data-boff-explore": "cta",
|
|
1669
|
+
"data-kind": cta.kind,
|
|
1670
|
+
href: cta.url,
|
|
1671
|
+
...internal ? {} : { target: "_blank", rel: "noopener noreferrer" },
|
|
1672
|
+
children: [
|
|
1673
|
+
cta.label,
|
|
1674
|
+
internal ? null : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ExternalLink, { className: "ml-1 h-3 w-3" })
|
|
1675
|
+
]
|
|
1676
|
+
}
|
|
1677
|
+
) });
|
|
1678
|
+
}
|
|
1679
|
+
function ThinkingIndicator({ activity }) {
|
|
1680
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1681
|
+
"div",
|
|
1682
|
+
{
|
|
1683
|
+
"data-boff-explore": "thinking",
|
|
1684
|
+
className: "flex items-center gap-2.5 rounded-2xl border border-border bg-muted/60 px-3.5 py-2.5 text-sm text-muted-foreground",
|
|
1685
|
+
children: [
|
|
1686
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1", "aria-hidden": "true", children: [
|
|
1687
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "boff-explore-dot" }),
|
|
1688
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1689
|
+
"span",
|
|
1690
|
+
{
|
|
1691
|
+
className: "boff-explore-dot",
|
|
1692
|
+
style: { animationDelay: "150ms" }
|
|
1693
|
+
}
|
|
1694
|
+
),
|
|
1695
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1696
|
+
"span",
|
|
1697
|
+
{
|
|
1698
|
+
className: "boff-explore-dot",
|
|
1699
|
+
style: { animationDelay: "300ms" }
|
|
1700
|
+
}
|
|
1701
|
+
)
|
|
1702
|
+
] }),
|
|
1703
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: activity || "Thinking\u2026" })
|
|
1704
|
+
]
|
|
1705
|
+
}
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
function UserBubble({ message }) {
|
|
1709
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1710
|
+
"div",
|
|
1711
|
+
{
|
|
1712
|
+
"data-boff-explore": "user",
|
|
1713
|
+
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",
|
|
1714
|
+
children: message.content
|
|
1715
|
+
}
|
|
1716
|
+
) });
|
|
1717
|
+
}
|
|
1718
|
+
function AssistantBubble({
|
|
1719
|
+
message,
|
|
1720
|
+
activity,
|
|
1721
|
+
onNavigate
|
|
1722
|
+
}) {
|
|
1723
|
+
const streaming = message.status === "PENDING" || message.status === "RUNNING";
|
|
1724
|
+
const body = message.content || message.partialContent || "";
|
|
1725
|
+
const failed = message.status === "FAILED";
|
|
1726
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1727
|
+
"div",
|
|
1728
|
+
{
|
|
1729
|
+
"data-boff-explore": "assistant",
|
|
1730
|
+
"data-status": message.status,
|
|
1731
|
+
className: "flex gap-3",
|
|
1732
|
+
children: [
|
|
1733
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1734
|
+
"span",
|
|
1735
|
+
{
|
|
1736
|
+
"aria-hidden": "true",
|
|
1737
|
+
className: "mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary",
|
|
1738
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "h-4 w-4" })
|
|
1739
|
+
}
|
|
1740
|
+
),
|
|
1741
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1 space-y-3", children: [
|
|
1742
|
+
body ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-2xl rounded-tl-md border border-border bg-card px-4 py-3 shadow-sm", children: [
|
|
1743
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "boff-prose boff-prose-sm text-card-foreground", children: /* @__PURE__ */ jsxRuntime.jsx(Markdown, { children: body }) }),
|
|
1744
|
+
streaming ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "boff-explore-caret", "aria-hidden": "true" }) : null
|
|
1745
|
+
] }) : null,
|
|
1746
|
+
streaming ? /* @__PURE__ */ jsxRuntime.jsx(ThinkingIndicator, { activity }) : null,
|
|
1747
|
+
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: "That answer didn't come through. Try asking again." }) : null,
|
|
1748
|
+
message.references.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1749
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Sources" }),
|
|
1750
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3", children: message.references.map((reference) => /* @__PURE__ */ jsxRuntime.jsx(ReferenceCard, { reference }, reference.url)) })
|
|
1751
|
+
] }) : null,
|
|
1752
|
+
message.ctas.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2 pt-0.5", children: message.ctas.map((cta) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1753
|
+
CtaButton,
|
|
1754
|
+
{
|
|
1755
|
+
cta,
|
|
1756
|
+
onNavigate
|
|
1757
|
+
},
|
|
1758
|
+
`${cta.kind}-${cta.url}`
|
|
1759
|
+
)) }) : null
|
|
1760
|
+
] })
|
|
1761
|
+
]
|
|
1762
|
+
}
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
function ErrorBanner({
|
|
1766
|
+
error,
|
|
1767
|
+
onRetry,
|
|
1768
|
+
onReset
|
|
1769
|
+
}) {
|
|
1770
|
+
const showRetry = error.retryable;
|
|
1771
|
+
const showReset = error.code === "CONVERSATION_LIMIT";
|
|
1772
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1773
|
+
"div",
|
|
1774
|
+
{
|
|
1775
|
+
"data-boff-explore": "error",
|
|
1776
|
+
"data-error-kind": error.kind,
|
|
1777
|
+
role: "alert",
|
|
1778
|
+
className: "mb-3 flex items-start gap-3 rounded-xl border border-destructive/40 bg-destructive/10 px-3 py-2.5",
|
|
1779
|
+
children: [
|
|
1780
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1781
|
+
"span",
|
|
1782
|
+
{
|
|
1783
|
+
"aria-hidden": "true",
|
|
1784
|
+
className: "mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-destructive text-destructive-foreground",
|
|
1785
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.TriangleAlert, { className: "h-3 w-3" })
|
|
1786
|
+
}
|
|
1787
|
+
),
|
|
1788
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "flex-1 text-sm leading-relaxed text-foreground", children: error.message }),
|
|
1789
|
+
showRetry ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1790
|
+
Button,
|
|
1791
|
+
{
|
|
1792
|
+
"data-boff-explore": "retry",
|
|
1793
|
+
type: "button",
|
|
1794
|
+
size: "sm",
|
|
1795
|
+
variant: "outline",
|
|
1796
|
+
onClick: onRetry,
|
|
1797
|
+
children: "Retry"
|
|
1798
|
+
}
|
|
1799
|
+
) : null,
|
|
1800
|
+
showReset ? /* @__PURE__ */ jsxRuntime.jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: onReset, children: "New chat" }) : null
|
|
1801
|
+
]
|
|
1802
|
+
}
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
function WelcomeState({
|
|
1806
|
+
title,
|
|
1807
|
+
body,
|
|
1808
|
+
prompts,
|
|
1809
|
+
products,
|
|
1810
|
+
focusProduct,
|
|
1811
|
+
onPrompt,
|
|
1812
|
+
onFocus,
|
|
1813
|
+
disabled
|
|
1814
|
+
}) {
|
|
1815
|
+
const [showAllProducts, setShowAllProducts] = React.useState(false);
|
|
1816
|
+
const visibleProducts = showAllProducts ? products : products.slice(0, PRODUCT_CHIP_PREVIEW);
|
|
1817
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto w-full max-w-3xl px-4 py-10 sm:py-14", children: [
|
|
1818
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
|
|
1819
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-1.5 rounded-full border border-border bg-muted px-3 py-1 text-xs font-medium text-muted-foreground", children: [
|
|
1820
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "h-3.5 w-3.5" }),
|
|
1821
|
+
"AI answers, grounded in our documentation"
|
|
1822
|
+
] }),
|
|
1823
|
+
/* @__PURE__ */ jsxRuntime.jsx("h1", { className: "mt-5 text-balance text-3xl font-bold tracking-tight sm:text-4xl", children: title }),
|
|
1824
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mx-auto mt-3 max-w-2xl text-pretty text-base leading-relaxed text-muted-foreground sm:text-lg", children: body })
|
|
1825
|
+
] }),
|
|
1826
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { className: "mt-9 grid gap-3 sm:grid-cols-3", children: CAPABILITIES.map(({ icon: Icon, title: capTitle, body: capBody }) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1827
|
+
"li",
|
|
1828
|
+
{
|
|
1829
|
+
className: "rounded-xl border border-border bg-card p-4 text-left",
|
|
1830
|
+
children: [
|
|
1831
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "mb-2.5 flex h-8 w-8 items-center justify-center rounded-lg bg-primary/10 text-primary", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "h-4 w-4" }) }),
|
|
1832
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-semibold text-card-foreground", children: capTitle }),
|
|
1833
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-xs leading-relaxed text-muted-foreground", children: capBody })
|
|
1834
|
+
]
|
|
1835
|
+
},
|
|
1836
|
+
capTitle
|
|
1837
|
+
)) }),
|
|
1838
|
+
prompts.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-9", children: [
|
|
1839
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Try asking" }),
|
|
1840
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: prompts.map((prompt) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1841
|
+
"button",
|
|
1842
|
+
{
|
|
1843
|
+
"data-boff-explore": "chip",
|
|
1844
|
+
"data-chip-kind": "prompt",
|
|
1845
|
+
type: "button",
|
|
1846
|
+
disabled,
|
|
1847
|
+
onClick: () => onPrompt(prompt),
|
|
1848
|
+
className: "group inline-flex max-w-full items-center gap-1.5 rounded-full border border-border bg-card px-3.5 py-1.5 text-left text-sm text-card-foreground transition-colors hover:border-primary/40 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
1849
|
+
children: [
|
|
1850
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: prompt }),
|
|
1851
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowUp, { className: "h-3 w-3 shrink-0 rotate-45 text-muted-foreground transition-colors group-hover:text-primary" })
|
|
1852
|
+
]
|
|
1853
|
+
},
|
|
1854
|
+
prompt
|
|
1855
|
+
)) })
|
|
1856
|
+
] }) : null,
|
|
1857
|
+
products.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-8", children: [
|
|
1858
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Focus on a product" }),
|
|
1859
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
1860
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1861
|
+
"button",
|
|
1862
|
+
{
|
|
1863
|
+
"data-boff-explore": "chip",
|
|
1864
|
+
"data-chip-kind": "product",
|
|
1865
|
+
"data-product": "all",
|
|
1866
|
+
type: "button",
|
|
1867
|
+
"aria-pressed": focusProduct === null,
|
|
1868
|
+
onClick: () => onFocus(null),
|
|
1869
|
+
className: cn(
|
|
1870
|
+
"rounded-full border px-3 py-1 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
1871
|
+
focusProduct === null ? "border-primary bg-primary text-primary-foreground" : "border-border bg-card text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
1872
|
+
),
|
|
1873
|
+
children: "All products"
|
|
1874
|
+
}
|
|
1875
|
+
),
|
|
1876
|
+
visibleProducts.map((product) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1877
|
+
"button",
|
|
1878
|
+
{
|
|
1879
|
+
"data-boff-explore": "chip",
|
|
1880
|
+
"data-chip-kind": "product",
|
|
1881
|
+
"data-product": product.slug,
|
|
1882
|
+
type: "button",
|
|
1883
|
+
"aria-pressed": focusProduct === product.slug,
|
|
1884
|
+
title: product.tagline ?? product.name,
|
|
1885
|
+
onClick: () => onFocus(focusProduct === product.slug ? null : product.slug),
|
|
1886
|
+
className: cn(
|
|
1887
|
+
"rounded-full border px-3 py-1 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
1888
|
+
focusProduct === product.slug ? "border-primary bg-primary text-primary-foreground" : "border-border bg-card text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
1889
|
+
),
|
|
1890
|
+
children: product.name
|
|
1891
|
+
},
|
|
1892
|
+
product.slug
|
|
1893
|
+
)),
|
|
1894
|
+
products.length > PRODUCT_CHIP_PREVIEW ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1895
|
+
"button",
|
|
1896
|
+
{
|
|
1897
|
+
type: "button",
|
|
1898
|
+
onClick: () => setShowAllProducts((v) => !v),
|
|
1899
|
+
className: "rounded-full px-3 py-1 text-xs font-medium text-primary underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
1900
|
+
children: showAllProducts ? "Show fewer" : `Show all ${products.length}`
|
|
1901
|
+
}
|
|
1902
|
+
) : null
|
|
1903
|
+
] })
|
|
1904
|
+
] }) : null
|
|
1905
|
+
] });
|
|
1906
|
+
}
|
|
1907
|
+
function ExplorePage({
|
|
1908
|
+
seo,
|
|
1909
|
+
productSlug,
|
|
1910
|
+
examplePrompts,
|
|
1911
|
+
className,
|
|
1912
|
+
heightMode = "viewport",
|
|
1913
|
+
viewportOffset,
|
|
1914
|
+
onNavigate,
|
|
1915
|
+
onSend,
|
|
1916
|
+
welcomeTitle,
|
|
1917
|
+
welcomeBody
|
|
1918
|
+
}) {
|
|
1919
|
+
const pageUrl = React.useMemo(
|
|
1920
|
+
() => typeof window === "undefined" ? void 0 : window.location.href,
|
|
1921
|
+
[]
|
|
1922
|
+
);
|
|
1923
|
+
const {
|
|
1924
|
+
phase,
|
|
1925
|
+
catalog,
|
|
1926
|
+
messages,
|
|
1927
|
+
activity,
|
|
1928
|
+
remainingToday,
|
|
1929
|
+
error,
|
|
1930
|
+
focusProduct,
|
|
1931
|
+
setFocusProduct,
|
|
1932
|
+
send,
|
|
1933
|
+
stop,
|
|
1934
|
+
retry,
|
|
1935
|
+
reset,
|
|
1936
|
+
canSend
|
|
1937
|
+
} = useExploreChat({ productSlug, pageUrl, onSend, examplePrompts });
|
|
1938
|
+
const [draft, setDraft] = React.useState("");
|
|
1939
|
+
const textareaRef = React.useRef(null);
|
|
1940
|
+
const scrollRef = React.useRef(null);
|
|
1941
|
+
const stickToBottomRef = React.useRef(true);
|
|
1942
|
+
const composingRef = React.useRef(false);
|
|
1943
|
+
const busy = phase === "sending" || phase === "streaming";
|
|
1944
|
+
const showWelcome = messages.length === 0;
|
|
1945
|
+
const maxLength = catalog.limits.maxMessageLength;
|
|
1946
|
+
const overLimit = draft.length > maxLength;
|
|
1947
|
+
const canSubmit = canSend && draft.trim().length > 0 && !overLimit && !busy;
|
|
1948
|
+
const resolvedTitle = welcomeTitle ?? catalog.welcomeTitle;
|
|
1949
|
+
const resolvedBody = welcomeBody ?? catalog.welcomeBody;
|
|
1950
|
+
const prompts = catalog.examplePrompts;
|
|
1951
|
+
const focusedProductName = React.useMemo(
|
|
1952
|
+
() => catalog.products.find((p) => p.slug === focusProduct)?.name ?? null,
|
|
1953
|
+
[catalog.products, focusProduct]
|
|
1954
|
+
);
|
|
1955
|
+
React.useEffect(() => {
|
|
1956
|
+
const el = textareaRef.current;
|
|
1957
|
+
if (!el) return;
|
|
1958
|
+
el.style.height = "auto";
|
|
1959
|
+
el.style.height = `${Math.min(el.scrollHeight, 200)}px`;
|
|
1960
|
+
}, [draft]);
|
|
1961
|
+
const handleScroll = React.useCallback(() => {
|
|
1962
|
+
const el = scrollRef.current;
|
|
1963
|
+
if (!el) return;
|
|
1964
|
+
stickToBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
|
|
1965
|
+
}, []);
|
|
1966
|
+
React.useEffect(() => {
|
|
1967
|
+
if (!stickToBottomRef.current) return;
|
|
1968
|
+
const el = scrollRef.current;
|
|
1969
|
+
if (!el) return;
|
|
1970
|
+
el.scrollTop = el.scrollHeight;
|
|
1971
|
+
}, [messages, activity]);
|
|
1972
|
+
const submitDraft = React.useCallback(() => {
|
|
1973
|
+
const text = draft.trim();
|
|
1974
|
+
if (!text || overLimit || busy || !canSend) return;
|
|
1975
|
+
setDraft("");
|
|
1976
|
+
stickToBottomRef.current = true;
|
|
1977
|
+
void send(text);
|
|
1978
|
+
textareaRef.current?.focus();
|
|
1979
|
+
}, [busy, canSend, draft, overLimit, send]);
|
|
1980
|
+
const sendPrompt = React.useCallback(
|
|
1981
|
+
(prompt) => {
|
|
1982
|
+
if (!canSend || busy) return;
|
|
1983
|
+
setDraft("");
|
|
1984
|
+
stickToBottomRef.current = true;
|
|
1985
|
+
void send(prompt);
|
|
1986
|
+
},
|
|
1987
|
+
[busy, canSend, send]
|
|
1988
|
+
);
|
|
1989
|
+
const handleKeyDown = React.useCallback(
|
|
1990
|
+
(event) => {
|
|
1991
|
+
if (event.key !== "Enter" || event.shiftKey) return;
|
|
1992
|
+
if (composingRef.current || event.nativeEvent.isComposing) return;
|
|
1993
|
+
event.preventDefault();
|
|
1994
|
+
submitDraft();
|
|
1995
|
+
},
|
|
1996
|
+
[submitDraft]
|
|
1997
|
+
);
|
|
1998
|
+
const composerDisabled = phase === "limited" || phase === "disabled" || !catalog.enabled;
|
|
1999
|
+
const captchaOn = Boolean(
|
|
2000
|
+
catalog.captchaRequired && catalog.recaptchaSiteKey
|
|
2001
|
+
);
|
|
2002
|
+
const rootStyle = heightMode === "viewport" ? { height: `calc(100dvh - ${normalizeOffset(viewportOffset)})` } : void 0;
|
|
2003
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2004
|
+
"div",
|
|
2005
|
+
{
|
|
2006
|
+
"data-boff-explore": "page",
|
|
2007
|
+
"data-phase": phase,
|
|
2008
|
+
className: cn(
|
|
2009
|
+
"flex w-full flex-col bg-background text-foreground",
|
|
2010
|
+
heightMode === "auto" && "min-h-[70vh]",
|
|
2011
|
+
className
|
|
2012
|
+
),
|
|
2013
|
+
style: rootStyle,
|
|
2014
|
+
children: [
|
|
2015
|
+
seo ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
2016
|
+
PageHead,
|
|
2017
|
+
{
|
|
2018
|
+
title: seo.title ?? "Explore \u2014 ask anything about our products",
|
|
2019
|
+
description: seo.description ?? "Ask questions in plain English and get answers drawn from our product documentation, with links to the exact pages they came from.",
|
|
2020
|
+
keywords: seo.keywords,
|
|
2021
|
+
image: seo.image,
|
|
2022
|
+
url: seo.url
|
|
2023
|
+
}
|
|
2024
|
+
) : null,
|
|
2025
|
+
/* @__PURE__ */ jsxRuntime.jsx(ProseStyles, {}),
|
|
2026
|
+
/* @__PURE__ */ jsxRuntime.jsx(ExploreStyles, {}),
|
|
2027
|
+
!showWelcome ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2.5 border-b border-border px-4 py-2.5", children: [
|
|
2028
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2029
|
+
"span",
|
|
2030
|
+
{
|
|
2031
|
+
"aria-hidden": "true",
|
|
2032
|
+
className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary",
|
|
2033
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "h-4 w-4" })
|
|
2034
|
+
}
|
|
2035
|
+
),
|
|
2036
|
+
/* @__PURE__ */ jsxRuntime.jsx("h1", { className: "truncate text-sm font-semibold", children: resolvedTitle }),
|
|
2037
|
+
focusedProductName ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden shrink-0 rounded-full bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground sm:inline", children: focusedProductName }) : null,
|
|
2038
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2039
|
+
Button,
|
|
2040
|
+
{
|
|
2041
|
+
"data-boff-explore": "new-chat",
|
|
2042
|
+
type: "button",
|
|
2043
|
+
size: "sm",
|
|
2044
|
+
variant: "ghost",
|
|
2045
|
+
className: "ml-auto shrink-0 text-muted-foreground",
|
|
2046
|
+
onClick: reset,
|
|
2047
|
+
children: [
|
|
2048
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { className: "h-3.5 w-3.5" }),
|
|
2049
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "New chat" })
|
|
2050
|
+
]
|
|
2051
|
+
}
|
|
2052
|
+
)
|
|
2053
|
+
] }) : null,
|
|
2054
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2055
|
+
"div",
|
|
2056
|
+
{
|
|
2057
|
+
ref: scrollRef,
|
|
2058
|
+
onScroll: handleScroll,
|
|
2059
|
+
"data-boff-explore": "messages",
|
|
2060
|
+
role: "log",
|
|
2061
|
+
"aria-live": "polite",
|
|
2062
|
+
"aria-label": "Explore conversation",
|
|
2063
|
+
className: "boff-explore-scroll min-h-0 flex-1 overflow-y-auto",
|
|
2064
|
+
children: showWelcome ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
2065
|
+
WelcomeState,
|
|
2066
|
+
{
|
|
2067
|
+
title: resolvedTitle,
|
|
2068
|
+
body: resolvedBody,
|
|
2069
|
+
prompts,
|
|
2070
|
+
products: catalog.products,
|
|
2071
|
+
focusProduct,
|
|
2072
|
+
onPrompt: sendPrompt,
|
|
2073
|
+
onFocus: setFocusProduct,
|
|
2074
|
+
disabled: !canSend || busy
|
|
2075
|
+
}
|
|
2076
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-auto w-full max-w-3xl space-y-6 px-4 py-6", children: messages.map(
|
|
2077
|
+
(message) => message.role === "USER" ? /* @__PURE__ */ jsxRuntime.jsx(UserBubble, { message }, message.id) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
2078
|
+
AssistantBubble,
|
|
2079
|
+
{
|
|
2080
|
+
message,
|
|
2081
|
+
activity,
|
|
2082
|
+
onNavigate
|
|
2083
|
+
},
|
|
2084
|
+
message.id
|
|
2085
|
+
)
|
|
2086
|
+
) })
|
|
2087
|
+
}
|
|
2088
|
+
),
|
|
2089
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t border-border bg-background px-4 pb-4 pt-3", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2090
|
+
"form",
|
|
2091
|
+
{
|
|
2092
|
+
"data-boff-explore": "composer",
|
|
2093
|
+
className: "mx-auto w-full max-w-3xl",
|
|
2094
|
+
onSubmit: (event) => {
|
|
2095
|
+
event.preventDefault();
|
|
2096
|
+
submitDraft();
|
|
2097
|
+
},
|
|
2098
|
+
children: [
|
|
2099
|
+
error ? /* @__PURE__ */ jsxRuntime.jsx(ErrorBanner, { error, onRetry: retry, onReset: reset }) : null,
|
|
2100
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2101
|
+
"div",
|
|
2102
|
+
{
|
|
2103
|
+
className: cn(
|
|
2104
|
+
"flex items-end gap-2 rounded-2xl border bg-card p-2 shadow-sm transition-colors",
|
|
2105
|
+
overLimit ? "border-destructive/60" : "border-border focus-within:border-primary/40 focus-within:ring-1 focus-within:ring-ring"
|
|
2106
|
+
),
|
|
2107
|
+
children: [
|
|
2108
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2109
|
+
Textarea,
|
|
2110
|
+
{
|
|
2111
|
+
ref: textareaRef,
|
|
2112
|
+
value: draft,
|
|
2113
|
+
rows: 1,
|
|
2114
|
+
disabled: composerDisabled,
|
|
2115
|
+
onChange: (event) => setDraft(event.target.value),
|
|
2116
|
+
onKeyDown: handleKeyDown,
|
|
2117
|
+
onCompositionStart: () => {
|
|
2118
|
+
composingRef.current = true;
|
|
2119
|
+
},
|
|
2120
|
+
onCompositionEnd: () => {
|
|
2121
|
+
composingRef.current = false;
|
|
2122
|
+
},
|
|
2123
|
+
"aria-label": "Ask a question",
|
|
2124
|
+
placeholder: composerDisabled ? phase === "limited" ? "Message limit reached \u2014 please try again later" : "Explore is unavailable right now" : focusedProductName ? `Ask about ${focusedProductName}\u2026` : "Ask anything about our products\u2026",
|
|
2125
|
+
className: "max-h-[200px] min-h-[44px] flex-1 resize-none border-0 bg-transparent px-2 py-2.5 text-sm shadow-none focus-visible:ring-0 md:text-sm"
|
|
2126
|
+
}
|
|
2127
|
+
),
|
|
2128
|
+
busy ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
2129
|
+
Button,
|
|
2130
|
+
{
|
|
2131
|
+
"data-boff-explore": "stop",
|
|
2132
|
+
type: "button",
|
|
2133
|
+
size: "icon",
|
|
2134
|
+
variant: "secondary",
|
|
2135
|
+
"aria-label": "Stop generating",
|
|
2136
|
+
onClick: stop,
|
|
2137
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Square, { className: "h-4 w-4" })
|
|
2138
|
+
}
|
|
2139
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
2140
|
+
Button,
|
|
2141
|
+
{
|
|
2142
|
+
"data-boff-explore": "send",
|
|
2143
|
+
type: "submit",
|
|
2144
|
+
size: "icon",
|
|
2145
|
+
disabled: !canSubmit,
|
|
2146
|
+
"aria-label": "Send message",
|
|
2147
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowUp, { className: "h-4 w-4" })
|
|
2148
|
+
}
|
|
2149
|
+
)
|
|
2150
|
+
]
|
|
2151
|
+
}
|
|
2152
|
+
),
|
|
2153
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [
|
|
2154
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "Enter to send \xB7 Shift + Enter for a new line" }),
|
|
2155
|
+
remainingToday !== null ? /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-boff-explore": "remaining", children: [
|
|
2156
|
+
remainingToday,
|
|
2157
|
+
" message",
|
|
2158
|
+
remainingToday === 1 ? "" : "s",
|
|
2159
|
+
" left today"
|
|
2160
|
+
] }) : null,
|
|
2161
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2162
|
+
"span",
|
|
2163
|
+
{
|
|
2164
|
+
className: cn(
|
|
2165
|
+
"ml-auto tabular-nums",
|
|
2166
|
+
overLimit && "font-medium text-destructive"
|
|
2167
|
+
),
|
|
2168
|
+
children: [
|
|
2169
|
+
draft.length.toLocaleString(),
|
|
2170
|
+
" / ",
|
|
2171
|
+
maxLength.toLocaleString()
|
|
2172
|
+
]
|
|
2173
|
+
}
|
|
2174
|
+
)
|
|
2175
|
+
] }),
|
|
2176
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "mt-2 text-xs leading-relaxed text-muted-foreground", children: [
|
|
2177
|
+
"Answers are generated from our public documentation and can be imperfect \u2014 check anything important against the linked sources.",
|
|
2178
|
+
captchaOn ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2179
|
+
" ",
|
|
2180
|
+
"This site is protected by reCAPTCHA; the Google",
|
|
2181
|
+
" ",
|
|
2182
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2183
|
+
"a",
|
|
2184
|
+
{
|
|
2185
|
+
href: "https://policies.google.com/privacy",
|
|
2186
|
+
target: "_blank",
|
|
2187
|
+
rel: "noopener noreferrer",
|
|
2188
|
+
className: "underline underline-offset-2 hover:text-foreground",
|
|
2189
|
+
children: "Privacy Policy"
|
|
2190
|
+
}
|
|
2191
|
+
),
|
|
2192
|
+
" ",
|
|
2193
|
+
"and",
|
|
2194
|
+
" ",
|
|
2195
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2196
|
+
"a",
|
|
2197
|
+
{
|
|
2198
|
+
href: "https://policies.google.com/terms",
|
|
2199
|
+
target: "_blank",
|
|
2200
|
+
rel: "noopener noreferrer",
|
|
2201
|
+
className: "underline underline-offset-2 hover:text-foreground",
|
|
2202
|
+
children: "Terms of Service"
|
|
2203
|
+
}
|
|
2204
|
+
),
|
|
2205
|
+
" ",
|
|
2206
|
+
"apply."
|
|
2207
|
+
] }) : null
|
|
2208
|
+
] })
|
|
2209
|
+
]
|
|
2210
|
+
}
|
|
2211
|
+
) })
|
|
2212
|
+
]
|
|
2213
|
+
}
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
exports.ExplorePage = ExplorePage;
|
|
2218
|
+
//# sourceMappingURL=explore.js.map
|
|
2219
|
+
//# sourceMappingURL=explore.js.map
|