@burdenoff/website-sdk 2026.1101.1 → 2026.1101.3
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 +52 -11
- package/dist/analytics/index.js +19 -69
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +3 -69
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/avatar-live/index.js +1 -0
- package/dist/avatar-live/index.js.map +1 -1
- package/dist/avatar-live/index.mjs +1 -0
- package/dist/avatar-live/index.mjs.map +1 -1
- package/dist/avatar-live-view/index.js +25 -30
- package/dist/avatar-live-view/index.js.map +1 -1
- package/dist/avatar-live-view/index.mjs +20 -25
- package/dist/avatar-live-view/index.mjs.map +1 -1
- package/dist/chunk-23H4JUL4.js +41 -0
- package/dist/chunk-23H4JUL4.js.map +1 -0
- package/dist/chunk-34RX5P75.mjs +25 -0
- package/dist/chunk-34RX5P75.mjs.map +1 -0
- package/dist/chunk-3LFFY7AZ.js +184 -0
- package/dist/chunk-3LFFY7AZ.js.map +1 -0
- package/dist/chunk-3X26GE4S.js +2794 -0
- package/dist/chunk-3X26GE4S.js.map +1 -0
- package/dist/chunk-45GCA4GG.js +85 -0
- package/dist/chunk-45GCA4GG.js.map +1 -0
- package/dist/chunk-4SAGVLHN.js +876 -0
- package/dist/chunk-4SAGVLHN.js.map +1 -0
- package/dist/chunk-4U6YAII5.js +218 -0
- package/dist/chunk-4U6YAII5.js.map +1 -0
- package/dist/chunk-5GMTBOO6.js +21 -0
- package/dist/chunk-5GMTBOO6.js.map +1 -0
- package/dist/chunk-5JRJ57CL.mjs +329 -0
- package/dist/chunk-5JRJ57CL.mjs.map +1 -0
- package/dist/chunk-6C2DKIC7.js +4 -0
- package/dist/chunk-6C2DKIC7.js.map +1 -0
- package/dist/chunk-6IBFBWC5.mjs +17 -0
- package/dist/chunk-6IBFBWC5.mjs.map +1 -0
- package/dist/chunk-6KY2ZRVI.js +116 -0
- package/dist/chunk-6KY2ZRVI.js.map +1 -0
- package/dist/chunk-6VJT3SM4.mjs +182 -0
- package/dist/chunk-6VJT3SM4.mjs.map +1 -0
- package/dist/chunk-6YZUFMK2.mjs +40 -0
- package/dist/chunk-6YZUFMK2.mjs.map +1 -0
- package/dist/chunk-7A6R7CGG.js +47 -0
- package/dist/chunk-7A6R7CGG.js.map +1 -0
- package/dist/chunk-7PWL4V2R.js +4 -0
- package/dist/chunk-7PWL4V2R.js.map +1 -0
- package/dist/chunk-AA33WDYS.js +161 -0
- package/dist/chunk-AA33WDYS.js.map +1 -0
- package/dist/chunk-AHRGFXV2.js +525 -0
- package/dist/chunk-AHRGFXV2.js.map +1 -0
- package/dist/chunk-B63XLDZH.mjs +109 -0
- package/dist/chunk-B63XLDZH.mjs.map +1 -0
- package/dist/chunk-BX2USEWF.mjs +35 -0
- package/dist/chunk-BX2USEWF.mjs.map +1 -0
- package/dist/chunk-BYSZT35R.js +150 -0
- package/dist/chunk-BYSZT35R.js.map +1 -0
- package/dist/chunk-C6OJIHNO.js +12 -0
- package/dist/chunk-C6OJIHNO.js.map +1 -0
- package/dist/chunk-COOELBX7.mjs +63 -0
- package/dist/chunk-COOELBX7.mjs.map +1 -0
- package/dist/chunk-D2TW7MNC.mjs +2771 -0
- package/dist/chunk-D2TW7MNC.mjs.map +1 -0
- package/dist/chunk-D5KEZ6V6.mjs +3 -0
- package/dist/chunk-D5KEZ6V6.mjs.map +1 -0
- package/dist/chunk-E3GUGJMY.js +46 -0
- package/dist/chunk-E3GUGJMY.js.map +1 -0
- package/dist/chunk-EBZ2HGJ2.mjs +52 -0
- package/dist/chunk-EBZ2HGJ2.mjs.map +1 -0
- package/dist/chunk-EDLTUOIA.mjs +210 -0
- package/dist/chunk-EDLTUOIA.mjs.map +1 -0
- package/dist/chunk-EJQSOT4X.js +4 -0
- package/dist/chunk-EJQSOT4X.js.map +1 -0
- package/dist/chunk-EUPFS6MZ.mjs +2019 -0
- package/dist/chunk-EUPFS6MZ.mjs.map +1 -0
- package/dist/chunk-F3XWDC3B.js +77 -0
- package/dist/chunk-F3XWDC3B.js.map +1 -0
- package/dist/chunk-FFADMVIK.mjs +5127 -0
- package/dist/chunk-FFADMVIK.mjs.map +1 -0
- package/dist/chunk-FJWS7BKT.mjs +3 -0
- package/dist/chunk-FJWS7BKT.mjs.map +1 -0
- package/dist/chunk-FJZJDUOR.mjs +3 -0
- package/dist/chunk-FJZJDUOR.mjs.map +1 -0
- package/dist/chunk-GCFLCO2Z.mjs +145 -0
- package/dist/chunk-GCFLCO2Z.mjs.map +1 -0
- package/dist/chunk-GEXC4LRP.js +61 -0
- package/dist/chunk-GEXC4LRP.js.map +1 -0
- package/dist/chunk-GTRAXQGC.mjs +3 -0
- package/dist/chunk-GTRAXQGC.mjs.map +1 -0
- package/dist/chunk-IKOIRUKW.js +270 -0
- package/dist/chunk-IKOIRUKW.js.map +1 -0
- package/dist/chunk-IXTWOPRN.js +45 -0
- package/dist/chunk-IXTWOPRN.js.map +1 -0
- package/dist/chunk-JDXOXGOK.js +547 -0
- package/dist/chunk-JDXOXGOK.js.map +1 -0
- package/dist/chunk-JSLDWYV7.mjs +558 -0
- package/dist/chunk-JSLDWYV7.mjs.map +1 -0
- package/dist/chunk-JXLSTVBK.js +336 -0
- package/dist/chunk-JXLSTVBK.js.map +1 -0
- package/dist/chunk-KHYXK7FG.mjs +49 -0
- package/dist/chunk-KHYXK7FG.mjs.map +1 -0
- package/dist/chunk-KSMBOWQQ.js +2034 -0
- package/dist/chunk-KSMBOWQQ.js.map +1 -0
- package/dist/chunk-KU2PXHGB.mjs +83 -0
- package/dist/chunk-KU2PXHGB.mjs.map +1 -0
- package/dist/chunk-KZANIV3S.mjs +37 -0
- package/dist/chunk-KZANIV3S.mjs.map +1 -0
- package/dist/chunk-L4BP6YOB.js +20 -0
- package/dist/chunk-L4BP6YOB.js.map +1 -0
- package/dist/chunk-MRKA2OAF.mjs +75 -0
- package/dist/chunk-MRKA2OAF.mjs.map +1 -0
- package/dist/chunk-N5EC53MH.mjs +80 -0
- package/dist/chunk-N5EC53MH.mjs.map +1 -0
- package/dist/chunk-NZANQGDM.mjs +34 -0
- package/dist/chunk-NZANQGDM.mjs.map +1 -0
- package/dist/chunk-O4GS3XPZ.js +10 -0
- package/dist/chunk-O4GS3XPZ.js.map +1 -0
- package/dist/chunk-O76QDGRM.js +5154 -0
- package/dist/chunk-O76QDGRM.js.map +1 -0
- package/dist/chunk-OYEUNZJY.js +114 -0
- package/dist/chunk-OYEUNZJY.js.map +1 -0
- package/dist/chunk-PHXAGQNB.js +74 -0
- package/dist/chunk-PHXAGQNB.js.map +1 -0
- package/dist/chunk-PIHHPP7J.mjs +16 -0
- package/dist/chunk-PIHHPP7J.mjs.map +1 -0
- package/dist/chunk-PXRHVVOU.mjs +499 -0
- package/dist/chunk-PXRHVVOU.mjs.map +1 -0
- package/dist/chunk-QMA6U6AU.js +36 -0
- package/dist/chunk-QMA6U6AU.js.map +1 -0
- package/dist/chunk-R6J7IULF.js +44 -0
- package/dist/chunk-R6J7IULF.js.map +1 -0
- package/dist/chunk-R7G6TWIS.mjs +10 -0
- package/dist/chunk-R7G6TWIS.mjs.map +1 -0
- package/dist/chunk-RUMGFI6L.mjs +544 -0
- package/dist/chunk-RUMGFI6L.mjs.map +1 -0
- package/dist/chunk-S54JWSJF.js +565 -0
- package/dist/chunk-S54JWSJF.js.map +1 -0
- package/dist/chunk-SK5UGG6A.js +2041 -0
- package/dist/chunk-SK5UGG6A.js.map +1 -0
- package/dist/chunk-SKIYDKIS.mjs +70 -0
- package/dist/chunk-SKIYDKIS.mjs.map +1 -0
- package/dist/chunk-SZNS5IK6.mjs +322 -0
- package/dist/chunk-SZNS5IK6.mjs.map +1 -0
- package/dist/chunk-T7JIPZAC.js +66 -0
- package/dist/chunk-T7JIPZAC.js.map +1 -0
- package/dist/chunk-TGYQHO2E.mjs +864 -0
- package/dist/chunk-TGYQHO2E.mjs.map +1 -0
- package/dist/chunk-TT3MHN42.mjs +8 -0
- package/dist/chunk-TT3MHN42.mjs.map +1 -0
- package/dist/chunk-UM55U65W.mjs +268 -0
- package/dist/chunk-UM55U65W.mjs.map +1 -0
- package/dist/chunk-UQ2HALUD.js +72 -0
- package/dist/chunk-UQ2HALUD.js.map +1 -0
- package/dist/chunk-UXWT7G77.js +82 -0
- package/dist/chunk-UXWT7G77.js.map +1 -0
- package/dist/chunk-VNS5WDWO.mjs +7 -0
- package/dist/chunk-VNS5WDWO.mjs.map +1 -0
- package/dist/chunk-VRWZLGR7.js +4 -0
- package/dist/chunk-VRWZLGR7.js.map +1 -0
- package/dist/chunk-VZLDSNH4.mjs +22 -0
- package/dist/chunk-VZLDSNH4.mjs.map +1 -0
- package/dist/chunk-W6P3YETJ.js +92 -0
- package/dist/chunk-W6P3YETJ.js.map +1 -0
- package/dist/chunk-WPKFFQCR.mjs +90 -0
- package/dist/chunk-WPKFFQCR.mjs.map +1 -0
- package/dist/chunk-WTVTI5TO.mjs +22 -0
- package/dist/chunk-WTVTI5TO.mjs.map +1 -0
- package/dist/chunk-XJOBZ4RJ.js +44 -0
- package/dist/chunk-XJOBZ4RJ.js.map +1 -0
- package/dist/chunk-XP2JDNDU.js +325 -0
- package/dist/chunk-XP2JDNDU.js.map +1 -0
- package/dist/chunk-Y4V5CEWD.mjs +112 -0
- package/dist/chunk-Y4V5CEWD.mjs.map +1 -0
- package/dist/chunk-YERN2UWM.js +116 -0
- package/dist/chunk-YERN2UWM.js.map +1 -0
- package/dist/chunk-ZKJQWFAA.mjs +114 -0
- package/dist/chunk-ZKJQWFAA.mjs.map +1 -0
- package/dist/chunk-ZOON3S3Y.mjs +138 -0
- package/dist/chunk-ZOON3S3Y.mjs.map +1 -0
- package/dist/chunk-ZP6QRAYC.mjs +2024 -0
- package/dist/chunk-ZP6QRAYC.mjs.map +1 -0
- package/dist/chunk-ZSTFX7FV.js +9 -0
- package/dist/chunk-ZSTFX7FV.js.map +1 -0
- package/dist/client/index.js +23 -331
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +3 -330
- package/dist/client/index.mjs.map +1 -1
- package/dist/comparison/index.js +28 -111
- package/dist/comparison/index.js.map +1 -1
- package/dist/comparison/index.mjs +2 -107
- package/dist/comparison/index.mjs.map +1 -1
- package/dist/components/contact.js +21 -931
- package/dist/components/contact.js.map +1 -1
- package/dist/components/contact.mjs +12 -909
- package/dist/components/contact.mjs.map +1 -1
- package/dist/components/electron-download.js +12 -124
- package/dist/components/electron-download.js.map +1 -1
- package/dist/components/electron-download.mjs +4 -106
- package/dist/components/electron-download.mjs.map +1 -1
- package/dist/components/explore-cta.js +8 -112
- package/dist/components/explore-cta.js.map +1 -1
- package/dist/components/explore-cta.mjs +3 -114
- package/dist/components/explore-cta.mjs.map +1 -1
- package/dist/components/explore.js +62 -7833
- package/dist/components/explore.js.map +1 -1
- package/dist/components/explore.mjs +13 -7800
- package/dist/components/explore.mjs.map +1 -1
- package/dist/components/launch-countdown.js +8 -83
- package/dist/components/launch-countdown.js.map +1 -1
- package/dist/components/launch-countdown.mjs +3 -85
- package/dist/components/launch-countdown.mjs.map +1 -1
- package/dist/components/newsletter.js +20 -993
- package/dist/components/newsletter.js.map +1 -1
- package/dist/components/newsletter.mjs +11 -971
- package/dist/components/newsletter.mjs.map +1 -1
- package/dist/components/partners.js +24 -1302
- package/dist/components/partners.js.map +1 -1
- package/dist/components/partners.mjs +15 -1276
- package/dist/components/partners.mjs.map +1 -1
- package/dist/components/pricing.js +49 -1249
- package/dist/components/pricing.js.map +1 -1
- package/dist/components/pricing.mjs +8 -1226
- package/dist/components/pricing.mjs.map +1 -1
- package/dist/components/problems-solved.js +8 -150
- package/dist/components/problems-solved.js.map +1 -1
- package/dist/components/problems-solved.mjs +4 -133
- package/dist/components/problems-solved.mjs.map +1 -1
- package/dist/components/resource-links.js +8 -304
- package/dist/components/resource-links.js.map +1 -1
- package/dist/components/resource-links.mjs +5 -308
- package/dist/components/resource-links.mjs.map +1 -1
- package/dist/components/social-links.js +8 -343
- package/dist/components/social-links.js.map +1 -1
- package/dist/components/social-links.mjs +5 -347
- package/dist/components/social-links.mjs.map +1 -1
- package/dist/components/unsubscribe.js +10 -627
- package/dist/components/unsubscribe.js.map +1 -1
- package/dist/components/unsubscribe.mjs +6 -611
- package/dist/components/unsubscribe.mjs.map +1 -1
- package/dist/components/waitlist.js +21 -1001
- package/dist/components/waitlist.js.map +1 -1
- package/dist/components/waitlist.mjs +12 -982
- package/dist/components/waitlist.mjs.map +1 -1
- package/dist/components/website-switcher.js +12 -175
- package/dist/components/website-switcher.js.map +1 -1
- package/dist/components/website-switcher.mjs +4 -157
- package/dist/components/website-switcher.mjs.map +1 -1
- package/dist/content/index.js +118 -3616
- package/dist/content/index.js.map +1 -1
- package/dist/content/index.mjs +12 -3567
- package/dist/content/index.mjs.map +1 -1
- package/dist/hooks/index.js +37 -2321
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/index.mjs +6 -2318
- package/dist/hooks/index.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4145 -20599
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3766 -20567
- package/dist/index.mjs.map +1 -1
- package/dist/product/index.js +20 -393
- package/dist/product/index.js.map +1 -1
- package/dist/product/index.mjs +5 -394
- package/dist/product/index.mjs.map +1 -1
- package/dist/seo/index.js +8 -76
- package/dist/seo/index.js.map +1 -1
- package/dist/seo/index.mjs +3 -78
- package/dist/seo/index.mjs.map +1 -1
- package/dist/store/index.js +51 -2650
- package/dist/store/index.js.map +1 -1
- package/dist/store/index.mjs +13 -2619
- package/dist/store/index.mjs.map +1 -1
- package/dist/utils/index.js +69 -134
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/index.mjs +5 -122
- package/dist/utils/index.mjs.map +1 -1
- package/package.json +2 -1
package/dist/hooks/index.js
CHANGED
|
@@ -1,2329 +1,45 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require('
|
|
3
|
+
require('../chunk-VRWZLGR7.js');
|
|
4
|
+
var chunkSK5UGG6A_js = require('../chunk-SK5UGG6A.js');
|
|
5
|
+
require('../chunk-PHXAGQNB.js');
|
|
6
|
+
require('../chunk-E3GUGJMY.js');
|
|
7
|
+
require('../chunk-JXLSTVBK.js');
|
|
8
|
+
require('../chunk-ZSTFX7FV.js');
|
|
5
9
|
|
|
6
|
-
var __defProp = Object.defineProperty;
|
|
7
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
8
|
-
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
9
10
|
|
|
10
|
-
// src/client/security.ts
|
|
11
|
-
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
12
|
-
var RESERVED_HEADERS = /* @__PURE__ */ new Set([
|
|
13
|
-
"authorization",
|
|
14
|
-
"cookie",
|
|
15
|
-
"x-actor-id",
|
|
16
|
-
"x-actor-type",
|
|
17
|
-
"x-consent-granted-by",
|
|
18
|
-
"x-consent-scope",
|
|
19
|
-
"x-consent-granted-at",
|
|
20
|
-
"x-consent-expires-at"
|
|
21
|
-
]);
|
|
22
|
-
function validateGatewayUrl(value) {
|
|
23
|
-
let url;
|
|
24
|
-
try {
|
|
25
|
-
url = new URL(value);
|
|
26
|
-
} catch {
|
|
27
|
-
throw new Error("Gateway URL must be a valid absolute URL");
|
|
28
|
-
}
|
|
29
|
-
if (url.protocol === "https:") {
|
|
30
|
-
return url.toString();
|
|
31
|
-
}
|
|
32
|
-
if (url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname.toLowerCase())) {
|
|
33
|
-
return url.toString();
|
|
34
|
-
}
|
|
35
|
-
throw new Error("Gateway URL must use HTTPS unless it targets localhost");
|
|
36
|
-
}
|
|
37
|
-
function sanitizeHeaders(headers) {
|
|
38
|
-
if (!headers) return void 0;
|
|
39
|
-
return Object.fromEntries(
|
|
40
|
-
Object.entries(headers).filter(
|
|
41
|
-
([key]) => !RESERVED_HEADERS.has(key.toLowerCase())
|
|
42
|
-
)
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
var CF_ACCESS_GATED_HOSTS = /* @__PURE__ */ new Set([
|
|
46
|
-
"alphagraphql.burdenoff.com",
|
|
47
|
-
"alphagraphqlworkspaces.burdenoff.com"
|
|
48
|
-
]);
|
|
49
|
-
function cfAccessHeaders(gatewayUrl, cfAccess) {
|
|
50
|
-
const clientId = cfAccess?.clientId?.trim();
|
|
51
|
-
const clientSecret = cfAccess?.clientSecret?.trim();
|
|
52
|
-
if (!clientId || !clientSecret) return {};
|
|
53
|
-
let host;
|
|
54
|
-
try {
|
|
55
|
-
host = new URL(gatewayUrl).hostname.toLowerCase();
|
|
56
|
-
} catch {
|
|
57
|
-
return {};
|
|
58
|
-
}
|
|
59
|
-
if (!CF_ACCESS_GATED_HOSTS.has(host)) return {};
|
|
60
|
-
return {
|
|
61
|
-
"CF-Access-Client-Id": clientId,
|
|
62
|
-
"CF-Access-Client-Secret": clientSecret
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
11
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
this.queryCache.delete(cacheKey);
|
|
98
|
-
}
|
|
99
|
-
const existing = this.inflight.get(cacheKey);
|
|
100
|
-
if (existing) {
|
|
101
|
-
return existing;
|
|
102
|
-
}
|
|
103
|
-
const promise = this.request(query, variables, true, options?.timeoutMs).then((result) => {
|
|
104
|
-
if (result.data && !result.errors?.length) {
|
|
105
|
-
if (this.queryCache.size >= _WebSDKClient.MAX_CACHE_ENTRIES) {
|
|
106
|
-
const oldestEntry = this.queryCache.keys().next().value;
|
|
107
|
-
if (oldestEntry) {
|
|
108
|
-
this.queryCache.delete(oldestEntry);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
this.queryCache.set(cacheKey, { data: result, ts: Date.now() });
|
|
112
|
-
}
|
|
113
|
-
return result;
|
|
114
|
-
}).finally(() => {
|
|
115
|
-
this.inflight.delete(cacheKey);
|
|
116
|
-
});
|
|
117
|
-
this.inflight.set(cacheKey, promise);
|
|
118
|
-
return promise;
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Execute a GraphQL mutation
|
|
122
|
-
*/
|
|
123
|
-
async mutate(mutation, variables, options) {
|
|
124
|
-
return this.request(mutation, variables, false, options?.timeoutMs);
|
|
125
|
-
}
|
|
126
|
-
/** HTTP statuses worth another attempt — transient server/edge conditions. */
|
|
127
|
-
static isRetriableStatus(status) {
|
|
128
|
-
return status === 429 || status === 408 || status >= 500 && status < 600;
|
|
129
|
-
}
|
|
130
|
-
static sleep(ms) {
|
|
131
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Perform the HTTP call with a hard timeout. Every outcome resolves to a
|
|
135
|
-
* GraphQLResponse — callers never hang, so a UI's `loading` state always
|
|
136
|
-
* settles into data or a renderable error.
|
|
137
|
-
*/
|
|
138
|
-
async attempt(query, variables, timeoutMs) {
|
|
139
|
-
const controller = new AbortController();
|
|
140
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
141
|
-
try {
|
|
142
|
-
const response = await fetch(this.config.gatewayUrl, {
|
|
143
|
-
method: "POST",
|
|
144
|
-
headers: {
|
|
145
|
-
"Content-Type": "application/json",
|
|
146
|
-
"x-product-id": this.config.productId,
|
|
147
|
-
...this.config.headers,
|
|
148
|
-
...cfAccessHeaders(this.config.gatewayUrl, this.config.cfAccess)
|
|
149
|
-
},
|
|
150
|
-
body: JSON.stringify({ query, variables }),
|
|
151
|
-
signal: controller.signal
|
|
152
|
-
});
|
|
153
|
-
if (!response.ok) {
|
|
154
|
-
console.error(
|
|
155
|
-
"[WebSDK] GraphQL request failed",
|
|
156
|
-
response.status,
|
|
157
|
-
response.statusText
|
|
158
|
-
);
|
|
159
|
-
return {
|
|
160
|
-
res: {
|
|
161
|
-
errors: [
|
|
162
|
-
{
|
|
163
|
-
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
164
|
-
extensions: { code: `HTTP_${response.status}` }
|
|
165
|
-
}
|
|
166
|
-
]
|
|
167
|
-
},
|
|
168
|
-
retriable: _WebSDKClient.isRetriableStatus(response.status)
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
return {
|
|
172
|
-
res: await response.json(),
|
|
173
|
-
retriable: false
|
|
174
|
-
};
|
|
175
|
-
} catch (error) {
|
|
176
|
-
const aborted = error instanceof Error && (error.name === "AbortError" || controller.signal.aborted);
|
|
177
|
-
const message = aborted ? `Request timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : "Network error";
|
|
178
|
-
console.error("[WebSDK] Network error", message);
|
|
179
|
-
return {
|
|
180
|
-
res: {
|
|
181
|
-
errors: [
|
|
182
|
-
{
|
|
183
|
-
message,
|
|
184
|
-
extensions: { code: aborted ? "TIMEOUT" : "NETWORK_ERROR" }
|
|
185
|
-
}
|
|
186
|
-
]
|
|
187
|
-
},
|
|
188
|
-
// Transport-level failures are worth one more try; a genuine outage
|
|
189
|
-
// just fails twice quickly and still renders an error state.
|
|
190
|
-
retriable: true
|
|
191
|
-
};
|
|
192
|
-
} finally {
|
|
193
|
-
clearTimeout(timer);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
async request(query, variables, retry = true, timeoutOverrideMs) {
|
|
197
|
-
const timeoutMs = timeoutOverrideMs ?? this.config.timeoutMs ?? _WebSDKClient.DEFAULT_TIMEOUT_MS;
|
|
198
|
-
const maxRetries = retry ? this.config.maxRetries ?? _WebSDKClient.DEFAULT_MAX_RETRIES : 0;
|
|
199
|
-
let last;
|
|
200
|
-
for (let i = 0; i <= maxRetries; i++) {
|
|
201
|
-
const { res, retriable } = await this.attempt(
|
|
202
|
-
query,
|
|
203
|
-
variables,
|
|
204
|
-
timeoutMs
|
|
205
|
-
);
|
|
206
|
-
if (!retriable || i === maxRetries) return res;
|
|
207
|
-
last = res;
|
|
208
|
-
await _WebSDKClient.sleep(
|
|
209
|
-
_WebSDKClient.RETRY_BASE_DELAY_MS * (i + 1) + Math.random() * 200
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
return last;
|
|
213
|
-
}
|
|
214
|
-
};
|
|
215
|
-
__publicField(_WebSDKClient, "CACHE_TTL", 5 * 60 * 1e3);
|
|
216
|
-
// 5 minutes
|
|
217
|
-
__publicField(_WebSDKClient, "MAX_CACHE_ENTRIES", 100);
|
|
218
|
-
__publicField(_WebSDKClient, "DEFAULT_TIMEOUT_MS", 12e3);
|
|
219
|
-
__publicField(_WebSDKClient, "DEFAULT_MAX_RETRIES", 1);
|
|
220
|
-
__publicField(_WebSDKClient, "RETRY_BASE_DELAY_MS", 400);
|
|
221
|
-
var WebSDKContext = react.createContext(null);
|
|
222
|
-
function useWebSDK() {
|
|
223
|
-
const client = react.useContext(WebSDKContext);
|
|
224
|
-
if (!client) {
|
|
225
|
-
throw new Error("useWebSDK must be used within a WebSDKProvider");
|
|
226
|
-
}
|
|
227
|
-
return client;
|
|
228
|
-
}
|
|
229
|
-
function useWebSDKConfig() {
|
|
230
|
-
const client = useWebSDK();
|
|
231
|
-
return client.getConfig();
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// src/components/explore-types.ts
|
|
235
|
-
var EXPLORE_MESSAGE_FIELDS = (
|
|
236
|
-
/* GraphQL */
|
|
237
|
-
`
|
|
238
|
-
id
|
|
239
|
-
conversationId
|
|
240
|
-
role
|
|
241
|
-
status
|
|
242
|
-
content
|
|
243
|
-
partialContent
|
|
244
|
-
activity
|
|
245
|
-
references {
|
|
246
|
-
url
|
|
247
|
-
title
|
|
248
|
-
description
|
|
249
|
-
product
|
|
250
|
-
imageUrl
|
|
251
|
-
index
|
|
252
|
-
}
|
|
253
|
-
ctas {
|
|
254
|
-
kind
|
|
255
|
-
label
|
|
256
|
-
url
|
|
257
|
-
product
|
|
258
|
-
}
|
|
259
|
-
followUps
|
|
260
|
-
answered
|
|
261
|
-
attachments {
|
|
262
|
-
name
|
|
263
|
-
mimeType
|
|
264
|
-
size
|
|
265
|
-
textExtracted
|
|
266
|
-
}
|
|
267
|
-
errorCode
|
|
268
|
-
createdAt
|
|
269
|
-
completedAt
|
|
270
|
-
`
|
|
271
|
-
);
|
|
272
|
-
var EXPLORE_OWN_MESSAGE_FIELDS = (
|
|
273
|
-
/* GraphQL */
|
|
274
|
-
`
|
|
275
|
-
${EXPLORE_MESSAGE_FIELDS}
|
|
276
|
-
feedback {
|
|
277
|
-
rating
|
|
278
|
-
reason
|
|
279
|
-
comment
|
|
280
|
-
createdAt
|
|
281
|
-
}
|
|
282
|
-
`
|
|
283
|
-
);
|
|
284
|
-
var EXPLORE_CATALOG_ESTABLISHED_FIELDS = (
|
|
285
|
-
/* GraphQL */
|
|
286
|
-
`
|
|
287
|
-
enabled
|
|
288
|
-
products {
|
|
289
|
-
slug
|
|
290
|
-
name
|
|
291
|
-
tagline
|
|
292
|
-
website
|
|
293
|
-
}
|
|
294
|
-
examplePrompts
|
|
295
|
-
welcomeTitle
|
|
296
|
-
welcomeBody
|
|
297
|
-
limits {
|
|
298
|
-
maxMessageLength
|
|
299
|
-
maxTurnsPerConversation
|
|
300
|
-
messagesPerDay
|
|
301
|
-
historyTurns
|
|
302
|
-
}
|
|
303
|
-
captchaRequired
|
|
304
|
-
recaptchaSiteKey`
|
|
305
|
-
);
|
|
306
|
-
var EXPLORE_CATALOG_HIGHLIGHT_FIELD = (
|
|
307
|
-
/* GraphQL */
|
|
308
|
-
`
|
|
309
|
-
conceptHighlights {
|
|
310
|
-
product
|
|
311
|
-
productName
|
|
312
|
-
title
|
|
313
|
-
url
|
|
314
|
-
imageUrl
|
|
315
|
-
alt
|
|
316
|
-
}`
|
|
317
|
-
);
|
|
318
|
-
function exploreCatalogDocument(extraFields) {
|
|
319
|
-
return (
|
|
320
|
-
/* GraphQL */
|
|
321
|
-
`
|
|
322
|
-
query ExploreCatalog($productSlug: String) {
|
|
323
|
-
exploreCatalog(productSlug: $productSlug) {${EXPLORE_CATALOG_ESTABLISHED_FIELDS}${extraFields}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
`
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
var EXPLORE_CATALOG_QUERY = exploreCatalogDocument(
|
|
330
|
-
EXPLORE_CATALOG_HIGHLIGHT_FIELD
|
|
331
|
-
);
|
|
332
|
-
var EXPLORE_CATALOG_QUERY_WITHOUT_HIGHLIGHTS = exploreCatalogDocument("");
|
|
333
|
-
var EXPLORE_MESSAGE_QUERY = (
|
|
334
|
-
/* GraphQL */
|
|
335
|
-
`
|
|
336
|
-
query ExploreMessage($conversationToken: String!, $id: ID!) {
|
|
337
|
-
exploreMessage(conversationToken: $conversationToken, id: $id) {
|
|
338
|
-
${EXPLORE_OWN_MESSAGE_FIELDS}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
`
|
|
342
|
-
);
|
|
343
|
-
var SEND_EXPLORE_MESSAGE_MUTATION = (
|
|
344
|
-
/* GraphQL */
|
|
345
|
-
`
|
|
346
|
-
mutation SendExploreMessage($input: SendExploreMessageInput!) {
|
|
347
|
-
sendExploreMessage(input: $input) {
|
|
348
|
-
conversation {
|
|
349
|
-
id
|
|
350
|
-
token
|
|
351
|
-
productSlug
|
|
352
|
-
turnCount
|
|
353
|
-
createdAt
|
|
354
|
-
}
|
|
355
|
-
userMessage {
|
|
356
|
-
${EXPLORE_OWN_MESSAGE_FIELDS}
|
|
357
|
-
}
|
|
358
|
-
assistantMessage {
|
|
359
|
-
${EXPLORE_OWN_MESSAGE_FIELDS}
|
|
360
|
-
}
|
|
361
|
-
remainingToday
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
`
|
|
365
|
-
);
|
|
366
|
-
var RECORD_EXPLORE_CLICK_MUTATION = (
|
|
367
|
-
/* GraphQL */
|
|
368
|
-
`
|
|
369
|
-
mutation RecordExploreClick($input: RecordExploreClickInput!) {
|
|
370
|
-
recordExploreClick(input: $input) {
|
|
371
|
-
recorded
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
`
|
|
375
|
-
);
|
|
376
|
-
var SUBMIT_EXPLORE_FEEDBACK_MUTATION = (
|
|
377
|
-
/* GraphQL */
|
|
378
|
-
`
|
|
379
|
-
mutation SubmitExploreFeedback($input: SubmitExploreFeedbackInput!) {
|
|
380
|
-
submitExploreFeedback(input: $input) {
|
|
381
|
-
recorded
|
|
382
|
-
feedback {
|
|
383
|
-
rating
|
|
384
|
-
reason
|
|
385
|
-
comment
|
|
386
|
-
createdAt
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
`
|
|
391
|
-
);
|
|
392
|
-
var ESCALATE_EXPLORE_CONVERSATION_MUTATION = (
|
|
393
|
-
/* GraphQL */
|
|
394
|
-
`
|
|
395
|
-
mutation EscalateExploreConversation(
|
|
396
|
-
$input: EscalateExploreConversationInput!
|
|
397
|
-
) {
|
|
398
|
-
escalateExploreConversation(input: $input) {
|
|
399
|
-
created
|
|
400
|
-
escalation {
|
|
401
|
-
email
|
|
402
|
-
createdAt
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
`
|
|
407
|
-
);
|
|
408
|
-
var CREATE_EXPLORE_SHARE_LINK_MUTATION = (
|
|
409
|
-
/* GraphQL */
|
|
410
|
-
`
|
|
411
|
-
mutation CreateExploreShareLink($conversationToken: String!) {
|
|
412
|
-
createExploreShareLink(conversationToken: $conversationToken) {
|
|
413
|
-
shareToken
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
`
|
|
417
|
-
);
|
|
418
|
-
var EXPLORE_SHARED_CONVERSATION_QUERY = (
|
|
419
|
-
/* GraphQL */
|
|
420
|
-
`
|
|
421
|
-
query ExploreSharedConversation($shareToken: String!) {
|
|
422
|
-
exploreSharedConversation(shareToken: $shareToken) {
|
|
423
|
-
${EXPLORE_MESSAGE_FIELDS}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
`
|
|
427
|
-
);
|
|
428
|
-
|
|
429
|
-
// src/utils/product-slug.ts
|
|
430
|
-
var PRODUCT_SLUG_RENAMES = Object.freeze(
|
|
431
|
-
[Object.freeze({ legacy: "botlit", current: "botmanor" })]
|
|
432
|
-
);
|
|
433
|
-
Object.freeze(
|
|
434
|
-
Object.fromEntries(PRODUCT_SLUG_RENAMES.map((r) => [r.legacy, r.current]))
|
|
435
|
-
);
|
|
436
|
-
var PRODUCT_SLUG_EMIT = Object.freeze({ productSlug: "current" });
|
|
437
|
-
function findRename(slug) {
|
|
438
|
-
const wanted = slug.toLowerCase();
|
|
439
|
-
return PRODUCT_SLUG_RENAMES.find(
|
|
440
|
-
(r) => r.legacy === wanted || r.current === wanted
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
function normalizeProductSlug(slug) {
|
|
444
|
-
return findRename(slug)?.current ?? slug;
|
|
445
|
-
}
|
|
446
|
-
function legacyProductSlug(slug) {
|
|
447
|
-
return findRename(slug)?.legacy ?? slug;
|
|
448
|
-
}
|
|
449
|
-
function emitProductSlug(slug) {
|
|
450
|
-
return PRODUCT_SLUG_EMIT.productSlug === "current" ? normalizeProductSlug(slug) : legacyProductSlug(slug);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// src/data/products.ts
|
|
454
|
-
var ECOSYSTEM_PRODUCTS = [
|
|
455
|
-
{
|
|
456
|
-
slug: "workspaces",
|
|
457
|
-
name: "Workspaces",
|
|
458
|
-
websiteUrl: "https://burdenoff.com"
|
|
459
|
-
},
|
|
460
|
-
{
|
|
461
|
-
slug: "vibecontrols",
|
|
462
|
-
name: "VibeControls",
|
|
463
|
-
websiteUrl: "https://vibecontrols.com"
|
|
464
|
-
},
|
|
465
|
-
// FluidGrids ships on www.fluidgrids.ai — fluidgrids.com only redirects there. The explore
|
|
466
|
-
// catalog and the site's own sitemap use this origin, and so must the fallback catalog
|
|
467
|
-
// built from this list, or /explore rejects FluidGrids concept images until it loads.
|
|
468
|
-
{
|
|
469
|
-
slug: "fluidgrids",
|
|
470
|
-
name: "FluidGrids",
|
|
471
|
-
websiteUrl: "https://www.fluidgrids.ai"
|
|
472
|
-
},
|
|
473
|
-
// BotManor ships on www.botmanor.com (the apex redirects there; the old botlit.ai domain
|
|
474
|
-
// still serves the same site but is no longer the name we link to).
|
|
475
|
-
// The slug follows the botlit → botmanor rename switch (utils/product-slug.ts).
|
|
476
|
-
{
|
|
477
|
-
slug: emitProductSlug("botmanor"),
|
|
478
|
-
name: "BotManor",
|
|
479
|
-
websiteUrl: "https://www.botmanor.com"
|
|
480
|
-
},
|
|
481
|
-
{
|
|
482
|
-
slug: "bigconsole",
|
|
483
|
-
name: "BigConsole",
|
|
484
|
-
websiteUrl: "https://bigconsole.com"
|
|
485
|
-
},
|
|
486
|
-
{
|
|
487
|
-
slug: "semanticfed",
|
|
488
|
-
name: "SemanticFed",
|
|
489
|
-
websiteUrl: "https://semanticfed.com"
|
|
490
|
-
},
|
|
491
|
-
{
|
|
492
|
-
slug: "intelwatchtower",
|
|
493
|
-
name: "IntelWatchtower",
|
|
494
|
-
websiteUrl: "https://intelwatchtower.com"
|
|
495
|
-
},
|
|
496
|
-
{ slug: "buildmyiq", name: "BuildMyIQ", websiteUrl: "https://buildmyiq.com" },
|
|
497
|
-
{
|
|
498
|
-
slug: "assethandler",
|
|
499
|
-
name: "AssetHandler",
|
|
500
|
-
websiteUrl: "https://assethandler.com"
|
|
501
|
-
},
|
|
502
|
-
{
|
|
503
|
-
slug: "crewfoundry",
|
|
504
|
-
name: "CrewFoundry",
|
|
505
|
-
websiteUrl: "https://crewfoundry.com"
|
|
506
|
-
},
|
|
507
|
-
{
|
|
508
|
-
slug: "planmagnet",
|
|
509
|
-
name: "PlanMagnet",
|
|
510
|
-
websiteUrl: "https://planmagnet.com"
|
|
511
|
-
},
|
|
512
|
-
{
|
|
513
|
-
slug: "eventfullymanaged",
|
|
514
|
-
name: "EventfullyManaged",
|
|
515
|
-
websiteUrl: "https://eventfullymanaged.com"
|
|
516
|
-
},
|
|
517
|
-
{
|
|
518
|
-
slug: "headshotmarketing",
|
|
519
|
-
name: "HeadshotMarketing",
|
|
520
|
-
websiteUrl: "https://headshotmarketing.com"
|
|
521
|
-
},
|
|
522
|
-
{
|
|
523
|
-
slug: "technospam",
|
|
524
|
-
name: "TechnoSpam",
|
|
525
|
-
websiteUrl: "https://technospam.com"
|
|
526
|
-
},
|
|
527
|
-
{ slug: "kadaikodi", name: "Kadaikodi", websiteUrl: "https://kadaikodi.com" },
|
|
528
|
-
{
|
|
529
|
-
slug: "movethewheels",
|
|
530
|
-
name: "MoveTheWheels",
|
|
531
|
-
websiteUrl: "https://movethewheels.com"
|
|
532
|
-
},
|
|
533
|
-
{
|
|
534
|
-
slug: "healthybowl",
|
|
535
|
-
name: "Healthy Bowl",
|
|
536
|
-
websiteUrl: "https://gethealthybowl.com"
|
|
537
|
-
},
|
|
538
|
-
{ slug: "ggnomad", name: "GGNomad", websiteUrl: "https://ggnomad.com" },
|
|
539
|
-
{
|
|
540
|
-
slug: "timecampus",
|
|
541
|
-
name: "TimeCampus",
|
|
542
|
-
websiteUrl: "https://timecampus.com"
|
|
543
|
-
},
|
|
544
|
-
{
|
|
545
|
-
slug: "manufacturedops",
|
|
546
|
-
name: "ManufacturedOps",
|
|
547
|
-
websiteUrl: "https://manufacturedops.com"
|
|
548
|
-
},
|
|
549
|
-
{
|
|
550
|
-
slug: "theglobalfuel",
|
|
551
|
-
name: "TheGlobalFuel",
|
|
552
|
-
websiteUrl: "https://theglobalfuel.com"
|
|
553
|
-
},
|
|
554
|
-
{
|
|
555
|
-
slug: "housingvista",
|
|
556
|
-
name: "HousingVista",
|
|
557
|
-
websiteUrl: "https://housingvista.com"
|
|
558
|
-
},
|
|
559
|
-
{
|
|
560
|
-
slug: "proserviceworld",
|
|
561
|
-
name: "ProServiceWorld",
|
|
562
|
-
websiteUrl: "https://proserviceworld.com"
|
|
563
|
-
},
|
|
564
|
-
{
|
|
565
|
-
slug: "mybodyshield",
|
|
566
|
-
name: "MyBodyShield",
|
|
567
|
-
websiteUrl: "https://mybodyshield.com"
|
|
568
|
-
},
|
|
569
|
-
{
|
|
570
|
-
slug: "artistrybase",
|
|
571
|
-
name: "ArtistryBase",
|
|
572
|
-
websiteUrl: "https://artistrybase.com"
|
|
573
|
-
},
|
|
574
|
-
{
|
|
575
|
-
slug: "theculturepost",
|
|
576
|
-
name: "TheCulturePost",
|
|
577
|
-
websiteUrl: "https://theculturepost.com"
|
|
578
|
-
},
|
|
579
|
-
{ slug: "collabkin", name: "CollabKin", websiteUrl: "https://collabkin.com" },
|
|
580
|
-
{
|
|
581
|
-
slug: "comradecircle",
|
|
582
|
-
name: "ComradeCircle",
|
|
583
|
-
websiteUrl: "https://comradecircle.com"
|
|
584
|
-
},
|
|
585
|
-
{
|
|
586
|
-
slug: "brainyrich",
|
|
587
|
-
name: "BrainyRich",
|
|
588
|
-
websiteUrl: "https://brainyrich.com"
|
|
589
|
-
},
|
|
590
|
-
{
|
|
591
|
-
slug: "cosmicintersection",
|
|
592
|
-
name: "CosmicIntersection",
|
|
593
|
-
websiteUrl: "https://cosmicintersection.com"
|
|
594
|
-
},
|
|
595
|
-
{
|
|
596
|
-
slug: "govcitizenhub",
|
|
597
|
-
name: "GovCitizenHub",
|
|
598
|
-
websiteUrl: "https://govcitizenhub.com"
|
|
599
|
-
},
|
|
600
|
-
{
|
|
601
|
-
slug: "adaptercloud",
|
|
602
|
-
name: "AdapterCloud",
|
|
603
|
-
websiteUrl: "https://adaptercloud.com"
|
|
604
|
-
},
|
|
605
|
-
{
|
|
606
|
-
slug: "subscriberbot",
|
|
607
|
-
name: "Subscriber Bot",
|
|
608
|
-
websiteUrl: "https://subscriberbot.com"
|
|
609
|
-
},
|
|
610
|
-
{
|
|
611
|
-
slug: "hookmovies",
|
|
612
|
-
name: "HookMovies",
|
|
613
|
-
websiteUrl: "https://hookmovies.com"
|
|
614
|
-
},
|
|
615
|
-
{
|
|
616
|
-
slug: "coolandlovely",
|
|
617
|
-
name: "CoolAndLovely",
|
|
618
|
-
websiteUrl: "https://coolandlovely.com"
|
|
619
|
-
},
|
|
620
|
-
{
|
|
621
|
-
slug: "atheletebridge",
|
|
622
|
-
name: "AthleteBridge",
|
|
623
|
-
websiteUrl: "https://atheletebridge.com"
|
|
624
|
-
},
|
|
625
|
-
{
|
|
626
|
-
slug: "labsofscience",
|
|
627
|
-
name: "LabsOfScience",
|
|
628
|
-
websiteUrl: "https://labsofscience.com"
|
|
629
|
-
},
|
|
630
|
-
{
|
|
631
|
-
slug: "ecoimpacthub",
|
|
632
|
-
name: "EcoImpactHub",
|
|
633
|
-
websiteUrl: "https://ecoimpacthub.com"
|
|
634
|
-
}
|
|
635
|
-
];
|
|
636
|
-
|
|
637
|
-
// src/utils/recaptcha-v3.ts
|
|
638
|
-
var SCRIPT_ID = "boff-recaptcha-v3";
|
|
639
|
-
var SCRIPT_SRC = "https://www.google.com/recaptcha/api.js?render=";
|
|
640
|
-
var EXPLORE_RECAPTCHA_ACTION = "explore_chat";
|
|
641
|
-
var loaders = /* @__PURE__ */ new Map();
|
|
642
|
-
function isBrowser() {
|
|
643
|
-
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
644
|
-
}
|
|
645
|
-
function loadRecaptchaV3(siteKey) {
|
|
646
|
-
if (!siteKey) {
|
|
647
|
-
return Promise.reject(new Error("reCAPTCHA site key is required"));
|
|
648
|
-
}
|
|
649
|
-
if (!isBrowser()) {
|
|
650
|
-
return Promise.reject(
|
|
651
|
-
new Error("reCAPTCHA v3 can only be loaded in a browser")
|
|
652
|
-
);
|
|
653
|
-
}
|
|
654
|
-
const existing = loaders.get(siteKey);
|
|
655
|
-
if (existing) return existing;
|
|
656
|
-
const promise = new Promise((resolve, reject) => {
|
|
657
|
-
const ready = () => {
|
|
658
|
-
const api = window.grecaptcha;
|
|
659
|
-
if (!api) {
|
|
660
|
-
reject(new Error("reCAPTCHA loaded but grecaptcha is unavailable"));
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
api.ready(() => resolve(api));
|
|
664
|
-
};
|
|
665
|
-
if (window.grecaptcha) {
|
|
666
|
-
ready();
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
const scriptId = `${SCRIPT_ID}-${siteKey}`;
|
|
670
|
-
const found = document.getElementById(scriptId);
|
|
671
|
-
if (found) {
|
|
672
|
-
found.addEventListener("load", ready, { once: true });
|
|
673
|
-
found.addEventListener(
|
|
674
|
-
"error",
|
|
675
|
-
() => reject(new Error("Failed to load reCAPTCHA v3")),
|
|
676
|
-
{ once: true }
|
|
677
|
-
);
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
const script = document.createElement("script");
|
|
681
|
-
script.id = scriptId;
|
|
682
|
-
script.src = `${SCRIPT_SRC}${encodeURIComponent(siteKey)}`;
|
|
683
|
-
script.async = true;
|
|
684
|
-
script.defer = true;
|
|
685
|
-
script.addEventListener("load", ready, { once: true });
|
|
686
|
-
script.addEventListener(
|
|
687
|
-
"error",
|
|
688
|
-
() => {
|
|
689
|
-
loaders.delete(siteKey);
|
|
690
|
-
reject(new Error("Failed to load reCAPTCHA v3"));
|
|
691
|
-
},
|
|
692
|
-
{ once: true }
|
|
693
|
-
);
|
|
694
|
-
document.head.appendChild(script);
|
|
695
|
-
});
|
|
696
|
-
loaders.set(siteKey, promise);
|
|
697
|
-
return promise;
|
|
698
|
-
}
|
|
699
|
-
async function getRecaptchaV3Token(siteKey, action = EXPLORE_RECAPTCHA_ACTION) {
|
|
700
|
-
const api = await loadRecaptchaV3(siteKey);
|
|
701
|
-
return api.execute(siteKey, { action });
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// src/hooks/use-explore-chat.ts
|
|
705
|
-
var DEFAULT_STORAGE_KEY = "boff.explore.v1";
|
|
706
|
-
var HISTORY_SUFFIX = ".history";
|
|
707
|
-
var MAX_ARCHIVED_CONVERSATIONS = 15;
|
|
708
|
-
var DEFAULT_POLL_INTERVAL_MS = 1500;
|
|
709
|
-
var DEFAULT_POLL_TIMEOUT_MS = 9e4;
|
|
710
|
-
var POLL_REQUEST_TIMEOUT_MS = 1e4;
|
|
711
|
-
var SEND_REQUEST_TIMEOUT_MS = 18e3;
|
|
712
|
-
var MAX_CONSECUTIVE_POLL_FAILURES = 4;
|
|
713
|
-
var MAX_PERSISTED_MESSAGES = 40;
|
|
714
|
-
var FALLBACK_EXPLORE_LIMITS = {
|
|
715
|
-
maxMessageLength: 2e3,
|
|
716
|
-
maxTurnsPerConversation: 20,
|
|
717
|
-
messagesPerDay: 100,
|
|
718
|
-
historyTurns: 6
|
|
719
|
-
};
|
|
720
|
-
var DEFAULT_EXPLORE_EXAMPLE_PROMPTS = [
|
|
721
|
-
"Every course we run lives in a different spreadsheet.",
|
|
722
|
-
"We only hear a machine is down after the line has stopped.",
|
|
723
|
-
"Members quietly stop showing up and we never know why.",
|
|
724
|
-
"Which of your products fits a problem like mine?"
|
|
725
|
-
];
|
|
726
|
-
var FALLBACK_WELCOME_TITLE = "What problem are you trying to solve?";
|
|
727
|
-
var FALLBACK_WELCOME_BODY = "Describe the problem, in your own words. You can also ask about a specific product or compare a few. Every answer comes straight from our documentation.";
|
|
728
|
-
function buildFallbackCatalog(examplePrompts, welcome) {
|
|
729
|
-
return {
|
|
730
|
-
enabled: true,
|
|
731
|
-
products: ECOSYSTEM_PRODUCTS.map((p) => ({
|
|
732
|
-
slug: p.slug,
|
|
733
|
-
name: p.name,
|
|
734
|
-
tagline: null,
|
|
735
|
-
website: p.websiteUrl
|
|
736
|
-
})),
|
|
737
|
-
examplePrompts: examplePrompts?.length ? examplePrompts : DEFAULT_EXPLORE_EXAMPLE_PROMPTS,
|
|
738
|
-
welcomeTitle: welcome?.title ?? FALLBACK_WELCOME_TITLE,
|
|
739
|
-
welcomeBody: welcome?.body ?? FALLBACK_WELCOME_BODY,
|
|
740
|
-
limits: FALLBACK_EXPLORE_LIMITS,
|
|
741
|
-
captchaRequired: false,
|
|
742
|
-
recaptchaSiteKey: null,
|
|
743
|
-
// Empty, and deliberately not synthesised from `ECOSYSTEM_PRODUCTS`: a concept highlight
|
|
744
|
-
// needs a real published illustration URL, and this SDK has no registry of them. An
|
|
745
|
-
// invented path would render 38 broken frames on the one screen every prerender captures.
|
|
746
|
-
// The strip renders nothing at all when the list is empty, which is the honest fallback.
|
|
747
|
-
conceptHighlights: []
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
|
-
var BAKED_CATALOG_ELEMENT_ID = "boff-explore-catalog";
|
|
751
|
-
function bakedCatalogLocale() {
|
|
752
|
-
if (typeof navigator === "undefined") return "";
|
|
753
|
-
return (navigator.language || "").toLowerCase().split("-")[0] ?? "";
|
|
754
|
-
}
|
|
755
|
-
function snapshotBakedElement() {
|
|
756
|
-
if (typeof document === "undefined") return null;
|
|
757
|
-
const el = document.getElementById(BAKED_CATALOG_ELEMENT_ID);
|
|
758
|
-
if (!el || el.tagName !== "SCRIPT" || el.getAttribute("type") !== "application/json")
|
|
759
|
-
return null;
|
|
760
|
-
return {
|
|
761
|
-
product: el.getAttribute("data-product") ?? "",
|
|
762
|
-
locale: el.getAttribute("data-locale") ?? "",
|
|
763
|
-
text: el.textContent ?? ""
|
|
764
|
-
};
|
|
765
|
-
}
|
|
766
|
-
var BAKED_AT_LOAD = snapshotBakedElement();
|
|
767
|
-
function isCatalogShape(value) {
|
|
768
|
-
if (!value || typeof value !== "object") return false;
|
|
769
|
-
const c = value;
|
|
770
|
-
const listOrAbsent = (v) => v === void 0 || v === null || Array.isArray(v);
|
|
771
|
-
const textOrAbsent = (v) => v === void 0 || v === null || typeof v === "string";
|
|
772
|
-
return (c["enabled"] === void 0 || typeof c["enabled"] === "boolean") && listOrAbsent(c["products"]) && listOrAbsent(c["examplePrompts"]) && listOrAbsent(c["conceptHighlights"]) && textOrAbsent(c["welcomeTitle"]) && textOrAbsent(c["welcomeBody"]);
|
|
773
|
-
}
|
|
774
|
-
function readBakedCatalog(productSlug) {
|
|
775
|
-
const baked = snapshotBakedElement() ?? BAKED_AT_LOAD;
|
|
776
|
-
if (!baked) return null;
|
|
777
|
-
if (baked.product !== (productSlug ?? "")) return null;
|
|
778
|
-
if (baked.locale && baked.locale !== bakedCatalogLocale()) return null;
|
|
779
|
-
try {
|
|
780
|
-
const parsed = JSON.parse(baked.text);
|
|
781
|
-
return isCatalogShape(parsed) ? parsed : null;
|
|
782
|
-
} catch {
|
|
783
|
-
return null;
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
function normalizeCatalog(raw, fallback) {
|
|
787
|
-
return {
|
|
788
|
-
enabled: raw.enabled !== false,
|
|
789
|
-
products: raw.products?.length ? raw.products : fallback.products,
|
|
790
|
-
examplePrompts: raw.examplePrompts?.length ? raw.examplePrompts : fallback.examplePrompts,
|
|
791
|
-
welcomeTitle: raw.welcomeTitle || fallback.welcomeTitle,
|
|
792
|
-
welcomeBody: raw.welcomeBody || fallback.welcomeBody,
|
|
793
|
-
limits: {
|
|
794
|
-
maxMessageLength: raw.limits?.maxMessageLength || fallback.limits.maxMessageLength,
|
|
795
|
-
maxTurnsPerConversation: raw.limits?.maxTurnsPerConversation || fallback.limits.maxTurnsPerConversation,
|
|
796
|
-
messagesPerDay: raw.limits?.messagesPerDay || fallback.limits.messagesPerDay,
|
|
797
|
-
historyTurns: raw.limits?.historyTurns || fallback.limits.historyTurns
|
|
798
|
-
},
|
|
799
|
-
captchaRequired: raw.captchaRequired === true,
|
|
800
|
-
recaptchaSiteKey: raw.recaptchaSiteKey ?? null,
|
|
801
|
-
// `[ExploreConceptHighlight!]!` on the wire, so a gateway that KNOWS the field always
|
|
802
|
-
// sends an array. The one response that arrives without it is the retry against an older
|
|
803
|
-
// supergraph (`EXPLORE_CATALOG_QUERY_WITHOUT_HIGHLIGHTS`) — that gateway does not answer
|
|
804
|
-
// the full document at all, it refuses it. Either way
|
|
805
|
-
// `catalog.conceptHighlights.filter(...)` on `undefined` would throw inside render,
|
|
806
|
-
// blanking the welcome screen over a decorative strip.
|
|
807
|
-
conceptHighlights: raw.conceptHighlights?.length ? raw.conceptHighlights : fallback.conceptHighlights
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
var CATALOG_DOCUMENT_REJECTED_CODES = /* @__PURE__ */ new Set([
|
|
811
|
-
"GRAPHQL_VALIDATION_FAILED",
|
|
812
|
-
"HTTP_400"
|
|
813
|
-
]);
|
|
814
|
-
function catalogRejectedTheHighlightField(res) {
|
|
815
|
-
if (res.data?.exploreCatalog) return false;
|
|
816
|
-
return (res.errors ?? []).some((error) => {
|
|
817
|
-
const code = error.extensions?.code;
|
|
818
|
-
return typeof code === "string" && CATALOG_DOCUMENT_REJECTED_CODES.has(code) || error.message.includes("conceptHighlights");
|
|
819
|
-
});
|
|
820
|
-
}
|
|
821
|
-
var RATE_LIMIT_CODES = /* @__PURE__ */ new Set([
|
|
822
|
-
"RATE_LIMITED",
|
|
823
|
-
"HTTP_429",
|
|
824
|
-
"EXPLORE_GLOBAL_LIMIT"
|
|
825
|
-
]);
|
|
826
|
-
var CAPTCHA_CODES = /* @__PURE__ */ new Set(["CAPTCHA_FAILED", "CAPTCHA_REQUIRED"]);
|
|
827
|
-
var NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
828
|
-
"NETWORK_ERROR",
|
|
829
|
-
"UPSTREAM_UNAVAILABLE",
|
|
830
|
-
"CONVERSATION_BUSY",
|
|
831
|
-
"PROVIDER_ERROR",
|
|
832
|
-
"QUEUE_FULL"
|
|
833
|
-
]);
|
|
834
|
-
var VALIDATION_CODES = /* @__PURE__ */ new Set([
|
|
835
|
-
"MESSAGE_TOO_LONG",
|
|
836
|
-
"MESSAGE_EMPTY",
|
|
837
|
-
"CONVERSATION_LIMIT",
|
|
838
|
-
"INVALID_INPUT"
|
|
839
|
-
]);
|
|
840
|
-
function escalateOutcomeForCode(code) {
|
|
841
|
-
switch (code) {
|
|
842
|
-
case "INVALID_EMAIL":
|
|
843
|
-
return "invalid-email";
|
|
844
|
-
case "RATE_LIMITED":
|
|
845
|
-
case "HTTP_429":
|
|
846
|
-
return "rate-limited";
|
|
847
|
-
case "CAPTCHA_REQUIRED":
|
|
848
|
-
case "CAPTCHA_FAILED":
|
|
849
|
-
return "captcha";
|
|
850
|
-
// The token owns nothing, or owns a different conversation. The server answers NOT_FOUND
|
|
851
|
-
// rather than FORBIDDEN so the endpoint is not an oracle for whether a thread exists;
|
|
852
|
-
// either way there is nothing this visitor can do about it, so it reads as a failure.
|
|
853
|
-
case "NOT_FOUND":
|
|
854
|
-
return "failed";
|
|
855
|
-
default:
|
|
856
|
-
return "failed";
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
function classifyExploreError(code, serverMessage) {
|
|
860
|
-
const c = code ?? "UNKNOWN";
|
|
861
|
-
if (RATE_LIMIT_CODES.has(c)) {
|
|
862
|
-
return {
|
|
863
|
-
kind: "rate-limit",
|
|
864
|
-
code: c,
|
|
865
|
-
message: "You've reached the message limit for now. Please try again later.",
|
|
866
|
-
retryable: false
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
if (CAPTCHA_CODES.has(c)) {
|
|
870
|
-
return {
|
|
871
|
-
kind: "captcha",
|
|
872
|
-
code: c,
|
|
873
|
-
message: "We couldn't verify that you're human. Please try sending that again.",
|
|
874
|
-
retryable: true
|
|
875
|
-
};
|
|
876
|
-
}
|
|
877
|
-
if (c === "TOOL_BUDGET") {
|
|
878
|
-
return {
|
|
879
|
-
kind: "scope",
|
|
880
|
-
code: c,
|
|
881
|
-
message: "That question spans more of our products than I can look through in one go. Try asking about two or three specific products.",
|
|
882
|
-
retryable: false
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
if (c === "TIMEOUT") {
|
|
886
|
-
return {
|
|
887
|
-
kind: "timeout",
|
|
888
|
-
code: c,
|
|
889
|
-
message: "That answer took too long to come back. Try asking again.",
|
|
890
|
-
retryable: true
|
|
891
|
-
};
|
|
892
|
-
}
|
|
893
|
-
if (NETWORK_CODES.has(c)) {
|
|
894
|
-
return {
|
|
895
|
-
kind: "network",
|
|
896
|
-
code: c,
|
|
897
|
-
message: "We couldn't reach the assistant just now. Check your connection and try again.",
|
|
898
|
-
retryable: true
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
if (c === "EXPLORE_DISABLED") {
|
|
902
|
-
return {
|
|
903
|
-
kind: "disabled",
|
|
904
|
-
code: c,
|
|
905
|
-
message: "Explore is unavailable at the moment. Please check back soon.",
|
|
906
|
-
retryable: false
|
|
907
|
-
};
|
|
908
|
-
}
|
|
909
|
-
if (VALIDATION_CODES.has(c)) {
|
|
910
|
-
return {
|
|
911
|
-
kind: "validation",
|
|
912
|
-
code: c,
|
|
913
|
-
message: serverMessage || "That message couldn't be sent.",
|
|
914
|
-
retryable: false
|
|
915
|
-
};
|
|
916
|
-
}
|
|
917
|
-
if (c === "EMPTY_ANSWER") {
|
|
918
|
-
return {
|
|
919
|
-
kind: "unknown",
|
|
920
|
-
code: c,
|
|
921
|
-
message: "No answer came back for that one. Try rephrasing, or ask something more specific.",
|
|
922
|
-
retryable: true
|
|
923
|
-
};
|
|
924
|
-
}
|
|
925
|
-
return {
|
|
926
|
-
kind: "unknown",
|
|
927
|
-
code: c,
|
|
928
|
-
message: serverMessage || "Something went wrong. Please try again.",
|
|
929
|
-
retryable: true
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
function historyKey(key) {
|
|
933
|
-
return `${key}${HISTORY_SUFFIX}`;
|
|
934
|
-
}
|
|
935
|
-
function readArchive(key) {
|
|
936
|
-
if (typeof window === "undefined") return [];
|
|
937
|
-
try {
|
|
938
|
-
const raw = window.localStorage.getItem(historyKey(key));
|
|
939
|
-
if (!raw) return [];
|
|
940
|
-
const parsed = JSON.parse(raw);
|
|
941
|
-
if (!Array.isArray(parsed)) return [];
|
|
942
|
-
return parsed.filter(
|
|
943
|
-
(entry) => typeof entry === "object" && entry !== null && typeof entry.token === "string" && Array.isArray(entry.messages)
|
|
944
|
-
);
|
|
945
|
-
} catch {
|
|
946
|
-
return [];
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
function writeArchive(key, entries) {
|
|
950
|
-
if (typeof window === "undefined") return;
|
|
951
|
-
try {
|
|
952
|
-
if (entries.length === 0) {
|
|
953
|
-
window.localStorage.removeItem(historyKey(key));
|
|
954
|
-
return;
|
|
955
|
-
}
|
|
956
|
-
window.localStorage.setItem(historyKey(key), JSON.stringify(entries));
|
|
957
|
-
} catch {
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
function upsertArchive(key, thread) {
|
|
961
|
-
if (!thread.token) return readArchive(key);
|
|
962
|
-
const real = thread.messages.filter(
|
|
963
|
-
(m) => m.content.trim() !== "" || m.role === "USER"
|
|
964
|
-
);
|
|
965
|
-
if (real.length === 0) return readArchive(key);
|
|
966
|
-
const firstUser = real.find((m) => m.role === "USER");
|
|
967
|
-
const entry = {
|
|
968
|
-
token: thread.token,
|
|
969
|
-
title: (firstUser?.content ?? "Conversation").trim().slice(0, 80),
|
|
970
|
-
updatedAt: real[real.length - 1]?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
971
|
-
messageCount: real.length,
|
|
972
|
-
focusProduct: thread.focusProduct,
|
|
973
|
-
messages: real.slice(-20)
|
|
974
|
-
};
|
|
975
|
-
const rest = readArchive(key).filter((e) => e.token !== entry.token);
|
|
976
|
-
const next = [entry, ...rest].slice(0, MAX_ARCHIVED_CONVERSATIONS);
|
|
977
|
-
writeArchive(key, next);
|
|
978
|
-
return next;
|
|
979
|
-
}
|
|
980
|
-
function readPersisted(key) {
|
|
981
|
-
if (typeof window === "undefined") return null;
|
|
982
|
-
try {
|
|
983
|
-
const raw = window.localStorage.getItem(key);
|
|
984
|
-
if (!raw) return null;
|
|
985
|
-
const parsed = JSON.parse(raw);
|
|
986
|
-
if (!parsed || typeof parsed !== "object") return null;
|
|
987
|
-
const record = parsed;
|
|
988
|
-
return {
|
|
989
|
-
conversationToken: typeof record.conversationToken === "string" ? record.conversationToken : null,
|
|
990
|
-
messages: Array.isArray(record.messages) ? record.messages.slice(-MAX_PERSISTED_MESSAGES) : [],
|
|
991
|
-
focusProduct: typeof record.focusProduct === "string" ? record.focusProduct : null
|
|
992
|
-
};
|
|
993
|
-
} catch {
|
|
994
|
-
return null;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
function readShareToken() {
|
|
998
|
-
if (typeof window === "undefined") return null;
|
|
999
|
-
try {
|
|
1000
|
-
return new URL(window.location.href).searchParams.get("c");
|
|
1001
|
-
} catch {
|
|
1002
|
-
return null;
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
function visibleText(message) {
|
|
1006
|
-
return message.content || message.partialContent || "";
|
|
1007
|
-
}
|
|
1008
|
-
function countUserTurns(messages) {
|
|
1009
|
-
return messages.filter((m) => m.role === "USER").length;
|
|
1010
|
-
}
|
|
1011
|
-
function useExploreChat(options = {}) {
|
|
1012
|
-
const {
|
|
1013
|
-
productSlug,
|
|
1014
|
-
pageUrl,
|
|
1015
|
-
locale,
|
|
1016
|
-
examplePrompts,
|
|
1017
|
-
fallbackWelcome,
|
|
1018
|
-
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
|
|
1019
|
-
pollTimeoutMs = DEFAULT_POLL_TIMEOUT_MS,
|
|
1020
|
-
storageKey = DEFAULT_STORAGE_KEY,
|
|
1021
|
-
persist = true,
|
|
1022
|
-
onSend,
|
|
1023
|
-
enabled = true,
|
|
1024
|
-
shareToken: shareTokenOption
|
|
1025
|
-
} = options;
|
|
1026
|
-
const client = useWebSDK();
|
|
1027
|
-
const { productId } = useWebSDKConfig();
|
|
1028
|
-
const promptKey = JSON.stringify(examplePrompts ?? []);
|
|
1029
|
-
const welcomeKey = JSON.stringify(fallbackWelcome ?? null);
|
|
1030
|
-
const fallbackCatalog = react.useMemo(
|
|
1031
|
-
() => buildFallbackCatalog(
|
|
1032
|
-
JSON.parse(promptKey),
|
|
1033
|
-
JSON.parse(welcomeKey) ?? void 0
|
|
1034
|
-
),
|
|
1035
|
-
[promptKey, welcomeKey]
|
|
1036
|
-
);
|
|
1037
|
-
const [bakedRaw] = react.useState(
|
|
1038
|
-
() => readBakedCatalog(productSlug)
|
|
1039
|
-
);
|
|
1040
|
-
const [catalog, setCatalog] = react.useState(
|
|
1041
|
-
() => bakedRaw ? normalizeCatalog(bakedRaw, fallbackCatalog) : fallbackCatalog
|
|
1042
|
-
);
|
|
1043
|
-
const [serverCatalogRaw, setServerCatalogRaw] = react.useState(bakedRaw);
|
|
1044
|
-
const [messages, setMessages] = react.useState([]);
|
|
1045
|
-
const [phase, setPhase] = react.useState(
|
|
1046
|
-
() => bakedRaw ? bakedRaw.enabled === false ? "disabled" : "ready" : "loading"
|
|
1047
|
-
);
|
|
1048
|
-
const [error, setError] = react.useState(null);
|
|
1049
|
-
const [remainingToday, setRemainingToday] = react.useState(null);
|
|
1050
|
-
const [focusProduct, setFocusProductState] = react.useState(null);
|
|
1051
|
-
const [turnCount, setTurnCount] = react.useState(0);
|
|
1052
|
-
const messagesRef = react.useRef([]);
|
|
1053
|
-
const [archive, setArchive] = react.useState([]);
|
|
1054
|
-
const [hydrated, setHydrated] = react.useState(false);
|
|
1055
|
-
const mountedRef = react.useRef(true);
|
|
1056
|
-
const pollGenerationRef = react.useRef(0);
|
|
1057
|
-
const pollTimerRef = react.useRef(null);
|
|
1058
|
-
const conversationTokenRef = react.useRef(null);
|
|
1059
|
-
const [hasConversation, setHasConversation] = react.useState(false);
|
|
1060
|
-
const shareTokenRef = react.useRef(void 0);
|
|
1061
|
-
if (shareTokenRef.current === void 0) {
|
|
1062
|
-
shareTokenRef.current = shareTokenOption === void 0 ? readShareToken() : shareTokenOption;
|
|
1063
|
-
}
|
|
1064
|
-
const isSharedView = shareTokenRef.current !== null;
|
|
1065
|
-
const catalogRef = react.useRef(catalog);
|
|
1066
|
-
const turnCountRef = react.useRef(0);
|
|
1067
|
-
const focusProductRef = react.useRef(null);
|
|
1068
|
-
const lastSentTextRef = react.useRef(null);
|
|
1069
|
-
const lastSentAttachmentsRef = react.useRef(
|
|
1070
|
-
[]
|
|
1071
|
-
);
|
|
1072
|
-
const inFlightRef = react.useRef(false);
|
|
1073
|
-
catalogRef.current = catalog;
|
|
1074
|
-
const rememberConversationToken = react.useCallback((token) => {
|
|
1075
|
-
conversationTokenRef.current = token;
|
|
1076
|
-
setHasConversation(token !== null);
|
|
1077
|
-
}, []);
|
|
1078
|
-
const clearPollTimer = react.useCallback(() => {
|
|
1079
|
-
if (pollTimerRef.current !== null) {
|
|
1080
|
-
clearTimeout(pollTimerRef.current);
|
|
1081
|
-
pollTimerRef.current = null;
|
|
1082
|
-
}
|
|
1083
|
-
}, []);
|
|
1084
|
-
const cancelPolling = react.useCallback(() => {
|
|
1085
|
-
pollGenerationRef.current += 1;
|
|
1086
|
-
clearPollTimer();
|
|
1087
|
-
}, [clearPollTimer]);
|
|
1088
|
-
react.useEffect(() => {
|
|
1089
|
-
mountedRef.current = true;
|
|
1090
|
-
return () => {
|
|
1091
|
-
mountedRef.current = false;
|
|
1092
|
-
pollGenerationRef.current += 1;
|
|
1093
|
-
if (pollTimerRef.current !== null) {
|
|
1094
|
-
clearTimeout(pollTimerRef.current);
|
|
1095
|
-
pollTimerRef.current = null;
|
|
1096
|
-
}
|
|
1097
|
-
};
|
|
1098
|
-
}, []);
|
|
1099
|
-
react.useEffect(() => {
|
|
1100
|
-
if (!enabled) return;
|
|
1101
|
-
const shareToken = shareTokenRef.current;
|
|
1102
|
-
if (!shareToken) return;
|
|
1103
|
-
let cancelled = false;
|
|
1104
|
-
setHydrated(true);
|
|
1105
|
-
void client.query(
|
|
1106
|
-
EXPLORE_SHARED_CONVERSATION_QUERY,
|
|
1107
|
-
{ shareToken },
|
|
1108
|
-
{ cache: false }
|
|
1109
|
-
).then((res) => {
|
|
1110
|
-
if (cancelled || !mountedRef.current) return;
|
|
1111
|
-
const shared = res?.data?.exploreSharedConversation;
|
|
1112
|
-
if (shared && shared.length > 0) {
|
|
1113
|
-
setMessages(shared);
|
|
1114
|
-
return;
|
|
1115
|
-
}
|
|
1116
|
-
setError({
|
|
1117
|
-
kind: "unknown",
|
|
1118
|
-
code: "SHARE_NOT_FOUND",
|
|
1119
|
-
message: "That shared conversation could not be found. The link may have expired.",
|
|
1120
|
-
// Nothing to retry: a bad token will stay bad.
|
|
1121
|
-
retryable: false
|
|
1122
|
-
});
|
|
1123
|
-
}).catch(() => {
|
|
1124
|
-
if (cancelled || !mountedRef.current) return;
|
|
1125
|
-
setError({
|
|
1126
|
-
kind: "unknown",
|
|
1127
|
-
code: "SHARE_NOT_FOUND",
|
|
1128
|
-
message: "That shared conversation could not be loaded.",
|
|
1129
|
-
retryable: true
|
|
1130
|
-
});
|
|
1131
|
-
});
|
|
1132
|
-
return () => {
|
|
1133
|
-
cancelled = true;
|
|
1134
|
-
};
|
|
1135
|
-
}, [client, enabled]);
|
|
1136
|
-
react.useEffect(() => {
|
|
1137
|
-
if (!enabled) return;
|
|
1138
|
-
if (shareTokenRef.current !== null) return;
|
|
1139
|
-
if (!persist) {
|
|
1140
|
-
setHydrated(true);
|
|
1141
|
-
return;
|
|
1142
|
-
}
|
|
1143
|
-
setArchive(readArchive(storageKey));
|
|
1144
|
-
const stored = readPersisted(storageKey);
|
|
1145
|
-
if (stored) {
|
|
1146
|
-
rememberConversationToken(stored.conversationToken);
|
|
1147
|
-
focusProductRef.current = stored.focusProduct;
|
|
1148
|
-
const settled = stored.messages.flatMap((m) => {
|
|
1149
|
-
if (m.role !== "ASSISTANT") return [m];
|
|
1150
|
-
if (m.status !== "PENDING" && m.status !== "RUNNING") return [m];
|
|
1151
|
-
const text = visibleText(m);
|
|
1152
|
-
if (!text) return [];
|
|
1153
|
-
return [{ ...m, status: "COMPLETED", content: text }];
|
|
1154
|
-
});
|
|
1155
|
-
setMessages(settled);
|
|
1156
|
-
setFocusProductState(stored.focusProduct);
|
|
1157
|
-
const turns = countUserTurns(settled);
|
|
1158
|
-
turnCountRef.current = turns;
|
|
1159
|
-
setTurnCount(turns);
|
|
1160
|
-
}
|
|
1161
|
-
setHydrated(true);
|
|
1162
|
-
}, [persist, rememberConversationToken, storageKey, enabled]);
|
|
1163
|
-
react.useEffect(() => {
|
|
1164
|
-
if (!persist || !hydrated || isSharedView || typeof window === "undefined")
|
|
1165
|
-
return;
|
|
1166
|
-
try {
|
|
1167
|
-
const payload = {
|
|
1168
|
-
conversationToken: conversationTokenRef.current,
|
|
1169
|
-
messages: messages.slice(-MAX_PERSISTED_MESSAGES),
|
|
1170
|
-
focusProduct
|
|
1171
|
-
};
|
|
1172
|
-
if (!payload.conversationToken && payload.messages.length === 0 && !payload.focusProduct) {
|
|
1173
|
-
window.localStorage.removeItem(storageKey);
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1176
|
-
window.localStorage.setItem(storageKey, JSON.stringify(payload));
|
|
1177
|
-
} catch {
|
|
1178
|
-
}
|
|
1179
|
-
}, [persist, hydrated, isSharedView, storageKey, messages, focusProduct]);
|
|
1180
|
-
react.useEffect(() => {
|
|
1181
|
-
messagesRef.current = messages;
|
|
1182
|
-
if (!persist || !hydrated || isSharedView) return;
|
|
1183
|
-
const settled = messages.some(
|
|
1184
|
-
(m) => m.role === "ASSISTANT" && (m.status === "COMPLETED" || m.status === "FAILED")
|
|
1185
|
-
);
|
|
1186
|
-
if (!settled) return;
|
|
1187
|
-
setArchive(
|
|
1188
|
-
upsertArchive(storageKey, {
|
|
1189
|
-
token: conversationTokenRef.current,
|
|
1190
|
-
messages,
|
|
1191
|
-
focusProduct: focusProductRef.current
|
|
1192
|
-
})
|
|
1193
|
-
);
|
|
1194
|
-
}, [messages, persist, hydrated, isSharedView, storageKey]);
|
|
1195
|
-
react.useEffect(() => {
|
|
1196
|
-
if (!enabled) return;
|
|
1197
|
-
let cancelled = false;
|
|
1198
|
-
const load = async () => {
|
|
1199
|
-
const variables = { productSlug: productSlug ?? null };
|
|
1200
|
-
let res = await client.query(
|
|
1201
|
-
EXPLORE_CATALOG_QUERY,
|
|
1202
|
-
variables
|
|
1203
|
-
);
|
|
1204
|
-
if (cancelled || !mountedRef.current) return;
|
|
1205
|
-
if (catalogRejectedTheHighlightField(res)) {
|
|
1206
|
-
res = await client.query(
|
|
1207
|
-
EXPLORE_CATALOG_QUERY_WITHOUT_HIGHLIGHTS,
|
|
1208
|
-
variables
|
|
1209
|
-
);
|
|
1210
|
-
if (cancelled || !mountedRef.current) return;
|
|
1211
|
-
}
|
|
1212
|
-
const raw = res.data?.exploreCatalog;
|
|
1213
|
-
if (res.errors?.length || !raw) {
|
|
1214
|
-
console.warn(
|
|
1215
|
-
"[WebSDK] exploreCatalog unavailable \u2014 keeping the catalog the page started with",
|
|
1216
|
-
res.errors?.[0]?.message
|
|
1217
|
-
);
|
|
1218
|
-
setPhase((prev) => prev === "loading" ? "ready" : prev);
|
|
1219
|
-
return;
|
|
1220
|
-
}
|
|
1221
|
-
const next = normalizeCatalog(
|
|
1222
|
-
raw,
|
|
1223
|
-
bakedRaw ? normalizeCatalog(bakedRaw, fallbackCatalog) : fallbackCatalog
|
|
1224
|
-
);
|
|
1225
|
-
setCatalog(next);
|
|
1226
|
-
setServerCatalogRaw(raw);
|
|
1227
|
-
catalogRef.current = next;
|
|
1228
|
-
setPhase((prev) => {
|
|
1229
|
-
if (!next.enabled) return "disabled";
|
|
1230
|
-
return prev === "loading" || prev === "disabled" ? "ready" : prev;
|
|
1231
|
-
});
|
|
1232
|
-
};
|
|
1233
|
-
void load();
|
|
1234
|
-
return () => {
|
|
1235
|
-
cancelled = true;
|
|
1236
|
-
};
|
|
1237
|
-
}, [client, productSlug, fallbackCatalog, bakedRaw, enabled]);
|
|
1238
|
-
const startPolling = react.useCallback(
|
|
1239
|
-
(conversationToken, messageId) => {
|
|
1240
|
-
pollGenerationRef.current += 1;
|
|
1241
|
-
const generation = pollGenerationRef.current;
|
|
1242
|
-
const startedAt = Date.now();
|
|
1243
|
-
let consecutiveFailures = 0;
|
|
1244
|
-
const isStale = () => !mountedRef.current || generation !== pollGenerationRef.current;
|
|
1245
|
-
const finishWithError = (chatError) => {
|
|
1246
|
-
inFlightRef.current = false;
|
|
1247
|
-
setMessages(
|
|
1248
|
-
(prev) => prev.flatMap((m) => {
|
|
1249
|
-
if (m.id !== messageId) return [m];
|
|
1250
|
-
const text = visibleText(m);
|
|
1251
|
-
if (!text) return [];
|
|
1252
|
-
return [{ ...m, status: "FAILED", content: text }];
|
|
1253
|
-
})
|
|
1254
|
-
);
|
|
1255
|
-
setError(chatError);
|
|
1256
|
-
setPhase(chatError.kind === "rate-limit" ? "limited" : "error");
|
|
1257
|
-
};
|
|
1258
|
-
const tick = async () => {
|
|
1259
|
-
if (isStale()) return;
|
|
1260
|
-
if (Date.now() - startedAt > pollTimeoutMs) {
|
|
1261
|
-
finishWithError(classifyExploreError("TIMEOUT"));
|
|
1262
|
-
return;
|
|
1263
|
-
}
|
|
1264
|
-
const res = await client.query(
|
|
1265
|
-
EXPLORE_MESSAGE_QUERY,
|
|
1266
|
-
{ conversationToken, id: messageId },
|
|
1267
|
-
{ cache: false, timeoutMs: POLL_REQUEST_TIMEOUT_MS }
|
|
1268
|
-
);
|
|
1269
|
-
if (isStale()) return;
|
|
1270
|
-
const first = res.errors?.[0];
|
|
1271
|
-
if (first) {
|
|
1272
|
-
const code = typeof first.extensions?.code === "string" ? first.extensions.code : void 0;
|
|
1273
|
-
const classified = classifyExploreError(code, first.message);
|
|
1274
|
-
if (!classified.retryable) {
|
|
1275
|
-
finishWithError(classified);
|
|
1276
|
-
return;
|
|
1277
|
-
}
|
|
1278
|
-
consecutiveFailures += 1;
|
|
1279
|
-
if (consecutiveFailures >= MAX_CONSECUTIVE_POLL_FAILURES) {
|
|
1280
|
-
finishWithError(classified);
|
|
1281
|
-
return;
|
|
1282
|
-
}
|
|
1283
|
-
pollTimerRef.current = setTimeout(() => void tick(), pollIntervalMs);
|
|
1284
|
-
return;
|
|
1285
|
-
}
|
|
1286
|
-
const message = res.data?.exploreMessage;
|
|
1287
|
-
if (!message) {
|
|
1288
|
-
consecutiveFailures += 1;
|
|
1289
|
-
if (consecutiveFailures >= MAX_CONSECUTIVE_POLL_FAILURES) {
|
|
1290
|
-
finishWithError(classifyExploreError("NOT_FOUND"));
|
|
1291
|
-
return;
|
|
1292
|
-
}
|
|
1293
|
-
pollTimerRef.current = setTimeout(() => void tick(), pollIntervalMs);
|
|
1294
|
-
return;
|
|
1295
|
-
}
|
|
1296
|
-
consecutiveFailures = 0;
|
|
1297
|
-
setMessages(
|
|
1298
|
-
(prev) => prev.map((m) => m.id === messageId ? message : m)
|
|
1299
|
-
);
|
|
1300
|
-
if (message.status === "COMPLETED") {
|
|
1301
|
-
inFlightRef.current = false;
|
|
1302
|
-
setError(null);
|
|
1303
|
-
setPhase("ready");
|
|
1304
|
-
return;
|
|
1305
|
-
}
|
|
1306
|
-
if (message.status === "FAILED") {
|
|
1307
|
-
finishWithError(
|
|
1308
|
-
classifyExploreError(message.errorCode ?? "UPSTREAM_UNAVAILABLE")
|
|
1309
|
-
);
|
|
1310
|
-
return;
|
|
1311
|
-
}
|
|
1312
|
-
setPhase("streaming");
|
|
1313
|
-
pollTimerRef.current = setTimeout(() => void tick(), pollIntervalMs);
|
|
1314
|
-
};
|
|
1315
|
-
pollTimerRef.current = setTimeout(() => void tick(), pollIntervalMs);
|
|
1316
|
-
},
|
|
1317
|
-
[client, pollIntervalMs, pollTimeoutMs]
|
|
1318
|
-
);
|
|
1319
|
-
const submit = react.useCallback(
|
|
1320
|
-
async (text, optimisticId, captchaAttempt, attachments) => {
|
|
1321
|
-
const activeCatalog = catalogRef.current;
|
|
1322
|
-
let recaptchaToken;
|
|
1323
|
-
if (activeCatalog.captchaRequired && activeCatalog.recaptchaSiteKey) {
|
|
1324
|
-
try {
|
|
1325
|
-
recaptchaToken = await getRecaptchaV3Token(
|
|
1326
|
-
activeCatalog.recaptchaSiteKey
|
|
1327
|
-
);
|
|
1328
|
-
} catch (err) {
|
|
1329
|
-
console.warn("[WebSDK] reCAPTCHA v3 token could not be minted", err);
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
const input = {
|
|
1333
|
-
message: text,
|
|
1334
|
-
...conversationTokenRef.current ? { conversationToken: conversationTokenRef.current } : {},
|
|
1335
|
-
...focusProductRef.current || productSlug ? { productSlug: focusProductRef.current ?? productSlug } : {},
|
|
1336
|
-
...recaptchaToken ? { recaptchaToken } : {},
|
|
1337
|
-
...locale ? { locale } : {},
|
|
1338
|
-
...pageUrl ? { pageUrl } : {},
|
|
1339
|
-
...attachments.length > 0 ? { attachments: [...attachments] } : {}
|
|
1340
|
-
};
|
|
1341
|
-
const res = await client.mutate(
|
|
1342
|
-
SEND_EXPLORE_MESSAGE_MUTATION,
|
|
1343
|
-
{ input },
|
|
1344
|
-
{ timeoutMs: SEND_REQUEST_TIMEOUT_MS }
|
|
1345
|
-
);
|
|
1346
|
-
if (!mountedRef.current) return;
|
|
1347
|
-
const first = res.errors?.[0];
|
|
1348
|
-
const result = res.data?.sendExploreMessage;
|
|
1349
|
-
if (first || !result) {
|
|
1350
|
-
const code = typeof first?.extensions?.code === "string" ? first.extensions.code : void 0;
|
|
1351
|
-
const classified = classifyExploreError(code, first?.message);
|
|
1352
|
-
if (classified.kind === "captcha" && captchaAttempt === 0) {
|
|
1353
|
-
await submit(text, optimisticId, 1, attachments);
|
|
1354
|
-
return;
|
|
1355
|
-
}
|
|
1356
|
-
inFlightRef.current = false;
|
|
1357
|
-
setError(classified);
|
|
1358
|
-
setPhase(classified.kind === "rate-limit" ? "limited" : "error");
|
|
1359
|
-
return;
|
|
1360
|
-
}
|
|
1361
|
-
rememberConversationToken(result.conversation.token);
|
|
1362
|
-
turnCountRef.current = result.conversation.turnCount;
|
|
1363
|
-
setTurnCount(result.conversation.turnCount);
|
|
1364
|
-
setRemainingToday(result.remainingToday ?? null);
|
|
1365
|
-
const userMessage = result.userMessage;
|
|
1366
|
-
const assistantMessage = result.assistantMessage;
|
|
1367
|
-
setMessages((prev) => {
|
|
1368
|
-
const replaced = prev.some((m) => m.id === optimisticId) ? prev.map((m) => m.id === optimisticId ? userMessage : m) : [...prev, userMessage];
|
|
1369
|
-
return [...replaced, assistantMessage];
|
|
1370
|
-
});
|
|
1371
|
-
if (assistantMessage.status === "COMPLETED" || assistantMessage.status === "FAILED") {
|
|
1372
|
-
inFlightRef.current = false;
|
|
1373
|
-
if (assistantMessage.status === "FAILED") {
|
|
1374
|
-
const classified = classifyExploreError(
|
|
1375
|
-
assistantMessage.errorCode ?? "UPSTREAM_UNAVAILABLE"
|
|
1376
|
-
);
|
|
1377
|
-
setError(classified);
|
|
1378
|
-
setPhase("error");
|
|
1379
|
-
} else {
|
|
1380
|
-
setPhase("ready");
|
|
1381
|
-
}
|
|
1382
|
-
return;
|
|
1383
|
-
}
|
|
1384
|
-
setPhase("streaming");
|
|
1385
|
-
startPolling(result.conversation.token, assistantMessage.id);
|
|
1386
|
-
},
|
|
1387
|
-
[
|
|
1388
|
-
client,
|
|
1389
|
-
locale,
|
|
1390
|
-
pageUrl,
|
|
1391
|
-
productSlug,
|
|
1392
|
-
rememberConversationToken,
|
|
1393
|
-
startPolling
|
|
1394
|
-
]
|
|
1395
|
-
);
|
|
1396
|
-
const send = react.useCallback(
|
|
1397
|
-
async (message, attachments = []) => {
|
|
1398
|
-
const text = message.trim();
|
|
1399
|
-
if (!text) return;
|
|
1400
|
-
if (inFlightRef.current) return;
|
|
1401
|
-
const activeCatalog = catalogRef.current;
|
|
1402
|
-
if (!activeCatalog.enabled) {
|
|
1403
|
-
setError(classifyExploreError("EXPLORE_DISABLED"));
|
|
1404
|
-
setPhase("disabled");
|
|
1405
|
-
return;
|
|
1406
|
-
}
|
|
1407
|
-
if (text.length > activeCatalog.limits.maxMessageLength) {
|
|
1408
|
-
setError({
|
|
1409
|
-
kind: "validation",
|
|
1410
|
-
code: "MESSAGE_TOO_LONG",
|
|
1411
|
-
message: `That message is ${text.length} characters \u2014 the limit is ${activeCatalog.limits.maxMessageLength}.`,
|
|
1412
|
-
retryable: false
|
|
1413
|
-
});
|
|
1414
|
-
return;
|
|
1415
|
-
}
|
|
1416
|
-
if (turnCountRef.current >= activeCatalog.limits.maxTurnsPerConversation) {
|
|
1417
|
-
setError({
|
|
1418
|
-
kind: "validation",
|
|
1419
|
-
code: "CONVERSATION_LIMIT",
|
|
1420
|
-
message: `This conversation has reached its ${activeCatalog.limits.maxTurnsPerConversation}-message limit. Start a new chat to keep going.`,
|
|
1421
|
-
retryable: false
|
|
1422
|
-
});
|
|
1423
|
-
return;
|
|
1424
|
-
}
|
|
1425
|
-
inFlightRef.current = true;
|
|
1426
|
-
lastSentTextRef.current = text;
|
|
1427
|
-
lastSentAttachmentsRef.current = attachments;
|
|
1428
|
-
setError(null);
|
|
1429
|
-
setPhase("sending");
|
|
1430
|
-
cancelPolling();
|
|
1431
|
-
const optimistic = {
|
|
1432
|
-
id: makeOptimisticId(),
|
|
1433
|
-
conversationId: conversationTokenRef.current ?? "pending",
|
|
1434
|
-
role: "USER",
|
|
1435
|
-
status: "COMPLETED",
|
|
1436
|
-
content: text,
|
|
1437
|
-
partialContent: null,
|
|
1438
|
-
activity: [],
|
|
1439
|
-
references: [],
|
|
1440
|
-
ctas: [],
|
|
1441
|
-
errorCode: null,
|
|
1442
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1443
|
-
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1444
|
-
};
|
|
1445
|
-
setMessages((prev) => [...prev, optimistic]);
|
|
1446
|
-
turnCountRef.current += 1;
|
|
1447
|
-
setTurnCount(turnCountRef.current);
|
|
1448
|
-
onSend?.(text);
|
|
1449
|
-
try {
|
|
1450
|
-
await submit(text, optimistic.id, 0, attachments);
|
|
1451
|
-
} catch (err) {
|
|
1452
|
-
if (!mountedRef.current) return;
|
|
1453
|
-
console.error("[WebSDK] Explore send failed", err);
|
|
1454
|
-
inFlightRef.current = false;
|
|
1455
|
-
setError(classifyExploreError("NETWORK_ERROR"));
|
|
1456
|
-
setPhase("error");
|
|
1457
|
-
}
|
|
1458
|
-
},
|
|
1459
|
-
[cancelPolling, onSend, submit]
|
|
1460
|
-
);
|
|
1461
|
-
const stop = react.useCallback(() => {
|
|
1462
|
-
cancelPolling();
|
|
1463
|
-
inFlightRef.current = false;
|
|
1464
|
-
setMessages(
|
|
1465
|
-
(prev) => prev.flatMap((m) => {
|
|
1466
|
-
if (m.role !== "ASSISTANT") return [m];
|
|
1467
|
-
if (m.status !== "PENDING" && m.status !== "RUNNING") return [m];
|
|
1468
|
-
const text = visibleText(m);
|
|
1469
|
-
if (!text) return [];
|
|
1470
|
-
return [
|
|
1471
|
-
{
|
|
1472
|
-
...m,
|
|
1473
|
-
status: "COMPLETED",
|
|
1474
|
-
content: text,
|
|
1475
|
-
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1476
|
-
}
|
|
1477
|
-
];
|
|
1478
|
-
})
|
|
1479
|
-
);
|
|
1480
|
-
setPhase(
|
|
1481
|
-
(prev) => prev === "sending" || prev === "streaming" ? "ready" : prev
|
|
1482
|
-
);
|
|
1483
|
-
}, [cancelPolling]);
|
|
1484
|
-
const retry = react.useCallback(() => {
|
|
1485
|
-
const text = lastSentTextRef.current;
|
|
1486
|
-
if (!text) return;
|
|
1487
|
-
cancelPolling();
|
|
1488
|
-
inFlightRef.current = false;
|
|
1489
|
-
setMessages((prev) => {
|
|
1490
|
-
const next = [...prev];
|
|
1491
|
-
const last = next[next.length - 1];
|
|
1492
|
-
if (last?.role === "ASSISTANT" && last.status === "FAILED") next.pop();
|
|
1493
|
-
const prior = next[next.length - 1];
|
|
1494
|
-
if (prior?.role === "USER" && prior.content === text) next.pop();
|
|
1495
|
-
return next;
|
|
1496
|
-
});
|
|
1497
|
-
turnCountRef.current = Math.max(0, turnCountRef.current - 1);
|
|
1498
|
-
setTurnCount(turnCountRef.current);
|
|
1499
|
-
void send(text, lastSentAttachmentsRef.current);
|
|
1500
|
-
}, [cancelPolling, send]);
|
|
1501
|
-
const reset = react.useCallback(() => {
|
|
1502
|
-
setArchive(
|
|
1503
|
-
upsertArchive(storageKey, {
|
|
1504
|
-
token: conversationTokenRef.current,
|
|
1505
|
-
messages: messagesRef.current,
|
|
1506
|
-
focusProduct: focusProductRef.current
|
|
1507
|
-
})
|
|
1508
|
-
);
|
|
1509
|
-
cancelPolling();
|
|
1510
|
-
inFlightRef.current = false;
|
|
1511
|
-
rememberConversationToken(null);
|
|
1512
|
-
lastSentTextRef.current = null;
|
|
1513
|
-
lastSentAttachmentsRef.current = [];
|
|
1514
|
-
turnCountRef.current = 0;
|
|
1515
|
-
setTurnCount(0);
|
|
1516
|
-
setMessages([]);
|
|
1517
|
-
setError(null);
|
|
1518
|
-
setRemainingToday(null);
|
|
1519
|
-
setPhase(catalogRef.current.enabled ? "ready" : "disabled");
|
|
1520
|
-
if (typeof window !== "undefined") {
|
|
1521
|
-
try {
|
|
1522
|
-
window.localStorage.removeItem(storageKey);
|
|
1523
|
-
} catch {
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
}, [cancelPolling, rememberConversationToken, storageKey]);
|
|
1527
|
-
const openConversation = react.useCallback(
|
|
1528
|
-
(token) => {
|
|
1529
|
-
const entry = readArchive(storageKey).find((e) => e.token === token);
|
|
1530
|
-
if (!entry) return;
|
|
1531
|
-
upsertArchive(storageKey, {
|
|
1532
|
-
token: conversationTokenRef.current,
|
|
1533
|
-
messages: messagesRef.current,
|
|
1534
|
-
focusProduct: focusProductRef.current
|
|
1535
|
-
});
|
|
1536
|
-
cancelPolling();
|
|
1537
|
-
inFlightRef.current = false;
|
|
1538
|
-
rememberConversationToken(entry.token);
|
|
1539
|
-
turnCountRef.current = entry.messages.filter(
|
|
1540
|
-
(m) => m.role === "USER"
|
|
1541
|
-
).length;
|
|
1542
|
-
setTurnCount(turnCountRef.current);
|
|
1543
|
-
setMessages(entry.messages);
|
|
1544
|
-
focusProductRef.current = entry.focusProduct;
|
|
1545
|
-
setFocusProductState(entry.focusProduct);
|
|
1546
|
-
setError(null);
|
|
1547
|
-
setPhase(catalogRef.current.enabled ? "ready" : "disabled");
|
|
1548
|
-
setArchive(readArchive(storageKey));
|
|
1549
|
-
},
|
|
1550
|
-
[cancelPolling, rememberConversationToken, storageKey]
|
|
1551
|
-
);
|
|
1552
|
-
const deleteConversation = react.useCallback(
|
|
1553
|
-
(token) => {
|
|
1554
|
-
const next = readArchive(storageKey).filter((e) => e.token !== token);
|
|
1555
|
-
writeArchive(storageKey, next);
|
|
1556
|
-
setArchive(next);
|
|
1557
|
-
},
|
|
1558
|
-
[storageKey]
|
|
1559
|
-
);
|
|
1560
|
-
const clearHistory = react.useCallback(() => {
|
|
1561
|
-
writeArchive(storageKey, []);
|
|
1562
|
-
setArchive([]);
|
|
1563
|
-
}, [storageKey]);
|
|
1564
|
-
const setFocusProduct = react.useCallback((slug) => {
|
|
1565
|
-
focusProductRef.current = slug;
|
|
1566
|
-
setFocusProductState(slug);
|
|
1567
|
-
}, []);
|
|
1568
|
-
const activity = react.useMemo(() => {
|
|
1569
|
-
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
1570
|
-
const m = messages[i];
|
|
1571
|
-
if (!m || m.role !== "ASSISTANT") continue;
|
|
1572
|
-
if (m.status !== "PENDING" && m.status !== "RUNNING") return null;
|
|
1573
|
-
const lines = m.activity ?? [];
|
|
1574
|
-
return lines.length ? lines[lines.length - 1] ?? null : null;
|
|
1575
|
-
}
|
|
1576
|
-
return null;
|
|
1577
|
-
}, [messages]);
|
|
1578
|
-
const canSend = (phase === "ready" || phase === "error") && catalog.enabled && turnCount < catalog.limits.maxTurnsPerConversation;
|
|
1579
|
-
const createShareLink = react.useCallback(async () => {
|
|
1580
|
-
const token = conversationTokenRef.current;
|
|
1581
|
-
if (!token) return null;
|
|
1582
|
-
try {
|
|
1583
|
-
const result = await client.mutate(CREATE_EXPLORE_SHARE_LINK_MUTATION, { conversationToken: token });
|
|
1584
|
-
return result?.data?.createExploreShareLink?.shareToken ?? null;
|
|
1585
|
-
} catch {
|
|
1586
|
-
return null;
|
|
1587
|
-
}
|
|
1588
|
-
}, [client]);
|
|
1589
|
-
const recordClick = react.useCallback(
|
|
1590
|
-
(messageId, kind, url) => {
|
|
1591
|
-
const token = conversationTokenRef.current;
|
|
1592
|
-
if (!token) return;
|
|
1593
|
-
try {
|
|
1594
|
-
void client.mutate(RECORD_EXPLORE_CLICK_MUTATION, {
|
|
1595
|
-
input: { conversationToken: token, messageId, kind, url }
|
|
1596
|
-
}).catch(() => {
|
|
1597
|
-
});
|
|
1598
|
-
} catch {
|
|
1599
|
-
}
|
|
1600
|
-
},
|
|
1601
|
-
[client]
|
|
1602
|
-
);
|
|
1603
|
-
const submitFeedback = react.useCallback(
|
|
1604
|
-
async (messageId, rating, detail) => {
|
|
1605
|
-
const token = conversationTokenRef.current;
|
|
1606
|
-
if (!token) return false;
|
|
1607
|
-
const comment = detail?.comment?.trim();
|
|
1608
|
-
const input = {
|
|
1609
|
-
conversationToken: token,
|
|
1610
|
-
messageId,
|
|
1611
|
-
rating,
|
|
1612
|
-
// Only meaningful with NOT_HELPFUL. The server stores null for HELPFUL either
|
|
1613
|
-
// way; sending it would just be noise the backend has to defend against.
|
|
1614
|
-
reason: rating === "NOT_HELPFUL" ? detail?.reason ?? null : null,
|
|
1615
|
-
comment: comment ? comment : null
|
|
1616
|
-
};
|
|
1617
|
-
try {
|
|
1618
|
-
const res = await client.mutate(
|
|
1619
|
-
SUBMIT_EXPLORE_FEEDBACK_MUTATION,
|
|
1620
|
-
{ input }
|
|
1621
|
-
);
|
|
1622
|
-
const result = res?.data?.submitExploreFeedback;
|
|
1623
|
-
if (!result?.recorded) return false;
|
|
1624
|
-
const recorded = result.feedback;
|
|
1625
|
-
if (recorded && mountedRef.current) {
|
|
1626
|
-
setMessages(
|
|
1627
|
-
(current) => current.map(
|
|
1628
|
-
(message) => message.id === messageId ? { ...message, feedback: recorded } : message
|
|
1629
|
-
)
|
|
1630
|
-
);
|
|
1631
|
-
}
|
|
1632
|
-
return true;
|
|
1633
|
-
} catch {
|
|
1634
|
-
return false;
|
|
1635
|
-
}
|
|
1636
|
-
},
|
|
1637
|
-
[client]
|
|
1638
|
-
);
|
|
1639
|
-
const escalate = react.useCallback(
|
|
1640
|
-
async (email, note) => {
|
|
1641
|
-
const token = conversationTokenRef.current;
|
|
1642
|
-
if (!token) {
|
|
1643
|
-
return { outcome: "no-conversation", email: null, createdAt: null };
|
|
1644
|
-
}
|
|
1645
|
-
const attempt = async (captchaAttempt) => {
|
|
1646
|
-
const activeCatalog = catalogRef.current;
|
|
1647
|
-
let recaptchaToken;
|
|
1648
|
-
if (activeCatalog.captchaRequired && activeCatalog.recaptchaSiteKey) {
|
|
1649
|
-
try {
|
|
1650
|
-
recaptchaToken = await getRecaptchaV3Token(
|
|
1651
|
-
activeCatalog.recaptchaSiteKey
|
|
1652
|
-
);
|
|
1653
|
-
} catch (err) {
|
|
1654
|
-
console.warn(
|
|
1655
|
-
"[WebSDK] reCAPTCHA v3 token could not be minted",
|
|
1656
|
-
err
|
|
1657
|
-
);
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
const input = {
|
|
1661
|
-
conversationToken: token,
|
|
1662
|
-
email: email.trim(),
|
|
1663
|
-
note: note?.trim() ? note.trim() : null,
|
|
1664
|
-
...recaptchaToken ? { recaptchaToken } : {},
|
|
1665
|
-
...productId ? { productId } : {}
|
|
1666
|
-
};
|
|
1667
|
-
try {
|
|
1668
|
-
const res = await client.mutate(
|
|
1669
|
-
ESCALATE_EXPLORE_CONVERSATION_MUTATION,
|
|
1670
|
-
{ input }
|
|
1671
|
-
);
|
|
1672
|
-
const first = res?.errors?.[0];
|
|
1673
|
-
const result = res?.data?.escalateExploreConversation;
|
|
1674
|
-
if (first || !result) {
|
|
1675
|
-
const code = typeof first?.extensions?.code === "string" ? first.extensions.code : "ESCALATION_FAILED";
|
|
1676
|
-
if (code === "CAPTCHA_FAILED" && captchaAttempt === 0) {
|
|
1677
|
-
return attempt(1);
|
|
1678
|
-
}
|
|
1679
|
-
return {
|
|
1680
|
-
outcome: escalateOutcomeForCode(code),
|
|
1681
|
-
email: null,
|
|
1682
|
-
createdAt: null
|
|
1683
|
-
};
|
|
1684
|
-
}
|
|
1685
|
-
return {
|
|
1686
|
-
// `created: false` is the double-click, the retry, the reload. The handover
|
|
1687
|
-
// exists, so this is a success — and the address comes from the SERVER, because
|
|
1688
|
-
// on a repeat submit the reply goes to whatever the FIRST handover used.
|
|
1689
|
-
outcome: result.created ? "sent" : "already-handed-over",
|
|
1690
|
-
email: result.escalation?.email ?? null,
|
|
1691
|
-
createdAt: result.escalation?.createdAt ?? null
|
|
1692
|
-
};
|
|
1693
|
-
} catch {
|
|
1694
|
-
return { outcome: "failed", email: null, createdAt: null };
|
|
1695
|
-
}
|
|
1696
|
-
};
|
|
1697
|
-
return attempt(0);
|
|
1698
|
-
},
|
|
1699
|
-
[client, productId]
|
|
1700
|
-
);
|
|
1701
|
-
const canEscalate = hasConversation && !isSharedView;
|
|
1702
|
-
const getConversationToken = react.useCallback(
|
|
1703
|
-
() => conversationTokenRef.current,
|
|
1704
|
-
[]
|
|
1705
|
-
);
|
|
1706
|
-
return {
|
|
1707
|
-
phase,
|
|
1708
|
-
catalog,
|
|
1709
|
-
serverCatalogRaw,
|
|
1710
|
-
messages,
|
|
1711
|
-
activity,
|
|
1712
|
-
remainingToday,
|
|
1713
|
-
error,
|
|
1714
|
-
focusProduct,
|
|
1715
|
-
setFocusProduct,
|
|
1716
|
-
send,
|
|
1717
|
-
stop,
|
|
1718
|
-
retry,
|
|
1719
|
-
reset,
|
|
1720
|
-
canSend,
|
|
1721
|
-
history: archive,
|
|
1722
|
-
openConversation,
|
|
1723
|
-
deleteConversation,
|
|
1724
|
-
clearHistory,
|
|
1725
|
-
recordClick,
|
|
1726
|
-
submitFeedback,
|
|
1727
|
-
escalate,
|
|
1728
|
-
canEscalate,
|
|
1729
|
-
createShareLink,
|
|
1730
|
-
isSharedView,
|
|
1731
|
-
getConversationToken
|
|
1732
|
-
};
|
|
1733
|
-
}
|
|
1734
|
-
var optimisticCounter = 0;
|
|
1735
|
-
function makeOptimisticId() {
|
|
1736
|
-
optimisticCounter += 1;
|
|
1737
|
-
return `boff-explore-local-${Date.now()}-${optimisticCounter}`;
|
|
1738
|
-
}
|
|
1739
|
-
function getRecognitionCtor() {
|
|
1740
|
-
if (typeof window === "undefined") return null;
|
|
1741
|
-
const w = window;
|
|
1742
|
-
return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
|
|
1743
|
-
}
|
|
1744
|
-
function isTouchDevice() {
|
|
1745
|
-
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
1746
|
-
try {
|
|
1747
|
-
return window.matchMedia("(pointer: coarse)").matches;
|
|
1748
|
-
} catch {
|
|
1749
|
-
return false;
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
var MIC_DENIED_MESSAGE = "Microphone access was blocked. Allow it in your browser's site settings to dictate.";
|
|
1753
|
-
var MIC_UNAVAILABLE_MESSAGE = "Dictation isn't available in this browser. You can type instead.";
|
|
1754
|
-
var RELEASE_SETTLE_MS = 250;
|
|
1755
|
-
var RETRY_DELAY_MS = 350;
|
|
1756
|
-
var sleep = (ms) => new Promise((resolve) => {
|
|
1757
|
-
setTimeout(resolve, ms);
|
|
12
|
+
Object.defineProperty(exports, "DEFAULT_EXPLORE_EXAMPLE_PROMPTS", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () { return chunkSK5UGG6A_js.DEFAULT_EXPLORE_EXAMPLE_PROMPTS; }
|
|
15
|
+
});
|
|
16
|
+
Object.defineProperty(exports, "FALLBACK_EXPLORE_LIMITS", {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
get: function () { return chunkSK5UGG6A_js.FALLBACK_EXPLORE_LIMITS; }
|
|
19
|
+
});
|
|
20
|
+
Object.defineProperty(exports, "classifyExploreError", {
|
|
21
|
+
enumerable: true,
|
|
22
|
+
get: function () { return chunkSK5UGG6A_js.classifyExploreError; }
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(exports, "stopSpeaking", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
get: function () { return chunkSK5UGG6A_js.stopSpeaking; }
|
|
27
|
+
});
|
|
28
|
+
Object.defineProperty(exports, "useExploreChat", {
|
|
29
|
+
enumerable: true,
|
|
30
|
+
get: function () { return chunkSK5UGG6A_js.useExploreChat; }
|
|
31
|
+
});
|
|
32
|
+
Object.defineProperty(exports, "useProgressiveReveal", {
|
|
33
|
+
enumerable: true,
|
|
34
|
+
get: function () { return chunkSK5UGG6A_js.useProgressiveReveal; }
|
|
35
|
+
});
|
|
36
|
+
Object.defineProperty(exports, "useSpeechInput", {
|
|
37
|
+
enumerable: true,
|
|
38
|
+
get: function () { return chunkSK5UGG6A_js.useSpeechInput; }
|
|
39
|
+
});
|
|
40
|
+
Object.defineProperty(exports, "useSpeechOutput", {
|
|
41
|
+
enumerable: true,
|
|
42
|
+
get: function () { return chunkSK5UGG6A_js.useSpeechOutput; }
|
|
1758
43
|
});
|
|
1759
|
-
async function ensureMicrophoneAccess() {
|
|
1760
|
-
const media = typeof navigator === "undefined" ? void 0 : navigator.mediaDevices;
|
|
1761
|
-
if (!media?.getUserMedia)
|
|
1762
|
-
return { ok: true, confirmed: false, primed: false };
|
|
1763
|
-
try {
|
|
1764
|
-
const status = await navigator.permissions?.query({
|
|
1765
|
-
name: "microphone"
|
|
1766
|
-
});
|
|
1767
|
-
if (status?.state === "granted")
|
|
1768
|
-
return { ok: true, confirmed: true, primed: false };
|
|
1769
|
-
} catch {
|
|
1770
|
-
}
|
|
1771
|
-
try {
|
|
1772
|
-
const stream = await media.getUserMedia({ audio: true });
|
|
1773
|
-
for (const track of stream.getTracks()) track.stop();
|
|
1774
|
-
return { ok: true, confirmed: true, primed: true };
|
|
1775
|
-
} catch (error) {
|
|
1776
|
-
const name = typeof error === "object" && error !== null && "name" in error ? String(error.name) : "";
|
|
1777
|
-
if (name === "NotAllowedError" || name === "SecurityError") {
|
|
1778
|
-
return { ok: false, message: MIC_DENIED_MESSAGE };
|
|
1779
|
-
}
|
|
1780
|
-
if (name === "NotFoundError" || name === "DevicesNotFoundError") {
|
|
1781
|
-
return { ok: false, message: "No microphone was found." };
|
|
1782
|
-
}
|
|
1783
|
-
return {
|
|
1784
|
-
ok: false,
|
|
1785
|
-
message: "Dictation could not start. You can type instead."
|
|
1786
|
-
};
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
function describeError(code) {
|
|
1790
|
-
switch (code) {
|
|
1791
|
-
case "not-allowed":
|
|
1792
|
-
case "service-not-allowed":
|
|
1793
|
-
return MIC_DENIED_MESSAGE;
|
|
1794
|
-
case "no-speech":
|
|
1795
|
-
return "I didn't catch anything \u2014 try again a little closer to the mic.";
|
|
1796
|
-
case "audio-capture":
|
|
1797
|
-
return "No microphone was found.";
|
|
1798
|
-
case "network":
|
|
1799
|
-
return "Speech recognition needs a network connection.";
|
|
1800
|
-
case "aborted":
|
|
1801
|
-
return "";
|
|
1802
|
-
default:
|
|
1803
|
-
return "Dictation stopped unexpectedly. You can type instead.";
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
function useSpeechInput({
|
|
1807
|
-
onFinalTranscript,
|
|
1808
|
-
lang,
|
|
1809
|
-
onError
|
|
1810
|
-
}) {
|
|
1811
|
-
const [supported] = react.useState(() => getRecognitionCtor() !== null);
|
|
1812
|
-
const [listening, setListening] = react.useState(false);
|
|
1813
|
-
const [interim, setInterim] = react.useState("");
|
|
1814
|
-
const [error, setError] = react.useState(null);
|
|
1815
|
-
const recognitionRef = react.useRef(null);
|
|
1816
|
-
const startTokenRef = react.useRef(0);
|
|
1817
|
-
const micConfirmedRef = react.useRef(false);
|
|
1818
|
-
const wantsListeningRef = react.useRef(false);
|
|
1819
|
-
const retriedRef = react.useRef(false);
|
|
1820
|
-
const finalRef = react.useRef(onFinalTranscript);
|
|
1821
|
-
const errorRef = react.useRef(onError);
|
|
1822
|
-
finalRef.current = onFinalTranscript;
|
|
1823
|
-
errorRef.current = onError;
|
|
1824
|
-
const langRef = react.useRef(lang);
|
|
1825
|
-
langRef.current = lang;
|
|
1826
|
-
const stop = react.useCallback(() => {
|
|
1827
|
-
startTokenRef.current += 1;
|
|
1828
|
-
wantsListeningRef.current = false;
|
|
1829
|
-
setListening(false);
|
|
1830
|
-
setInterim("");
|
|
1831
|
-
const recognition = recognitionRef.current;
|
|
1832
|
-
if (!recognition) return;
|
|
1833
|
-
try {
|
|
1834
|
-
recognition.stop();
|
|
1835
|
-
} catch {
|
|
1836
|
-
}
|
|
1837
|
-
}, []);
|
|
1838
|
-
const openSession = react.useCallback((token) => {
|
|
1839
|
-
const Ctor = getRecognitionCtor();
|
|
1840
|
-
if (!Ctor || token !== startTokenRef.current) return;
|
|
1841
|
-
const recognition = new Ctor();
|
|
1842
|
-
recognition.lang = langRef.current ?? (typeof navigator === "undefined" ? "en-US" : navigator.language || "en-US");
|
|
1843
|
-
recognition.continuous = !isTouchDevice();
|
|
1844
|
-
recognition.interimResults = true;
|
|
1845
|
-
recognition.maxAlternatives = 1;
|
|
1846
|
-
recognition.onstart = () => {
|
|
1847
|
-
if (token !== startTokenRef.current) return;
|
|
1848
|
-
setError(null);
|
|
1849
|
-
setListening(true);
|
|
1850
|
-
};
|
|
1851
|
-
recognition.onresult = (event) => {
|
|
1852
|
-
if (token !== startTokenRef.current) return;
|
|
1853
|
-
let settled = "";
|
|
1854
|
-
let pending = "";
|
|
1855
|
-
for (let i = event.resultIndex; i < event.results.length; i += 1) {
|
|
1856
|
-
const result = event.results[i];
|
|
1857
|
-
if (!result) continue;
|
|
1858
|
-
const text = result[0]?.transcript ?? "";
|
|
1859
|
-
if (result.isFinal) settled += text;
|
|
1860
|
-
else pending += text;
|
|
1861
|
-
}
|
|
1862
|
-
setInterim(pending);
|
|
1863
|
-
if (settled.trim() !== "") finalRef.current(settled);
|
|
1864
|
-
};
|
|
1865
|
-
recognition.onerror = (event) => {
|
|
1866
|
-
if (token !== startTokenRef.current) return;
|
|
1867
|
-
if ((event.error === "not-allowed" || event.error === "service-not-allowed") && micConfirmedRef.current && !retriedRef.current) {
|
|
1868
|
-
retriedRef.current = true;
|
|
1869
|
-
void sleep(RETRY_DELAY_MS).then(() => {
|
|
1870
|
-
if (token !== startTokenRef.current || !wantsListeningRef.current)
|
|
1871
|
-
return;
|
|
1872
|
-
openSessionRef.current?.(token);
|
|
1873
|
-
});
|
|
1874
|
-
return;
|
|
1875
|
-
}
|
|
1876
|
-
const message = micConfirmedRef.current && (event.error === "not-allowed" || event.error === "service-not-allowed") ? MIC_UNAVAILABLE_MESSAGE : describeError(event.error);
|
|
1877
|
-
wantsListeningRef.current = false;
|
|
1878
|
-
setListening(false);
|
|
1879
|
-
setInterim("");
|
|
1880
|
-
if (message !== "") {
|
|
1881
|
-
setError(message);
|
|
1882
|
-
errorRef.current?.(message);
|
|
1883
|
-
}
|
|
1884
|
-
};
|
|
1885
|
-
recognition.onend = () => {
|
|
1886
|
-
if (token !== startTokenRef.current) return;
|
|
1887
|
-
setInterim("");
|
|
1888
|
-
if (wantsListeningRef.current && isTouchDevice()) {
|
|
1889
|
-
void sleep(120).then(() => {
|
|
1890
|
-
if (token !== startTokenRef.current || !wantsListeningRef.current)
|
|
1891
|
-
return;
|
|
1892
|
-
openSessionRef.current?.(token);
|
|
1893
|
-
});
|
|
1894
|
-
return;
|
|
1895
|
-
}
|
|
1896
|
-
setListening(false);
|
|
1897
|
-
};
|
|
1898
|
-
recognitionRef.current = recognition;
|
|
1899
|
-
try {
|
|
1900
|
-
recognition.start();
|
|
1901
|
-
} catch {
|
|
1902
|
-
wantsListeningRef.current = false;
|
|
1903
|
-
setListening(false);
|
|
1904
|
-
}
|
|
1905
|
-
}, []);
|
|
1906
|
-
const openSessionRef = react.useRef(null);
|
|
1907
|
-
openSessionRef.current = openSession;
|
|
1908
|
-
const start = react.useCallback(() => {
|
|
1909
|
-
if (!getRecognitionCtor()) return;
|
|
1910
|
-
startTokenRef.current += 1;
|
|
1911
|
-
const token = startTokenRef.current;
|
|
1912
|
-
wantsListeningRef.current = true;
|
|
1913
|
-
retriedRef.current = false;
|
|
1914
|
-
micConfirmedRef.current = false;
|
|
1915
|
-
setError(null);
|
|
1916
|
-
setListening(true);
|
|
1917
|
-
void ensureMicrophoneAccess().then(async (access) => {
|
|
1918
|
-
if (token !== startTokenRef.current) return;
|
|
1919
|
-
if (!access.ok) {
|
|
1920
|
-
wantsListeningRef.current = false;
|
|
1921
|
-
setListening(false);
|
|
1922
|
-
setError(access.message);
|
|
1923
|
-
errorRef.current?.(access.message);
|
|
1924
|
-
return;
|
|
1925
|
-
}
|
|
1926
|
-
micConfirmedRef.current = access.confirmed;
|
|
1927
|
-
if (access.primed) {
|
|
1928
|
-
await sleep(RELEASE_SETTLE_MS);
|
|
1929
|
-
if (token !== startTokenRef.current) return;
|
|
1930
|
-
}
|
|
1931
|
-
openSessionRef.current?.(token);
|
|
1932
|
-
});
|
|
1933
|
-
}, []);
|
|
1934
|
-
const toggle = react.useCallback(() => {
|
|
1935
|
-
if (listening) stop();
|
|
1936
|
-
else start();
|
|
1937
|
-
}, [listening, start, stop]);
|
|
1938
|
-
react.useEffect(
|
|
1939
|
-
() => () => {
|
|
1940
|
-
startTokenRef.current += 1;
|
|
1941
|
-
wantsListeningRef.current = false;
|
|
1942
|
-
const recognition = recognitionRef.current;
|
|
1943
|
-
if (!recognition) return;
|
|
1944
|
-
recognition.onresult = null;
|
|
1945
|
-
recognition.onerror = null;
|
|
1946
|
-
recognition.onend = null;
|
|
1947
|
-
recognition.onstart = null;
|
|
1948
|
-
try {
|
|
1949
|
-
recognition.abort();
|
|
1950
|
-
} catch {
|
|
1951
|
-
}
|
|
1952
|
-
},
|
|
1953
|
-
[]
|
|
1954
|
-
);
|
|
1955
|
-
return { supported, listening, interim, error, start, stop, toggle };
|
|
1956
|
-
}
|
|
1957
|
-
var SILENT_FAILURE_MS = 1500;
|
|
1958
|
-
function speechPlatform() {
|
|
1959
|
-
if (typeof window === "undefined") return null;
|
|
1960
|
-
const scope = window;
|
|
1961
|
-
const synth = scope.speechSynthesis;
|
|
1962
|
-
const Utterance = scope.SpeechSynthesisUtterance;
|
|
1963
|
-
if (!synth || typeof synth.speak !== "function") return null;
|
|
1964
|
-
if (typeof Utterance !== "function") return null;
|
|
1965
|
-
return { synth, Utterance };
|
|
1966
|
-
}
|
|
1967
|
-
function normaliseTag(tag) {
|
|
1968
|
-
return tag.trim().toLowerCase().replace(/_/g, "-");
|
|
1969
|
-
}
|
|
1970
|
-
function primaryOf(tag) {
|
|
1971
|
-
return normaliseTag(tag).split("-")[0] ?? "";
|
|
1972
|
-
}
|
|
1973
|
-
function pickVoice(voices, lang) {
|
|
1974
|
-
const wanted = normaliseTag(lang);
|
|
1975
|
-
if (wanted === "") return null;
|
|
1976
|
-
const primary = primaryOf(wanted);
|
|
1977
|
-
const exact = voices.filter((voice) => normaliseTag(voice.lang) === wanted);
|
|
1978
|
-
const related = voices.filter((voice) => primaryOf(voice.lang) === primary);
|
|
1979
|
-
return exact.find((voice) => voice.default) ?? exact[0] ?? related.find((voice) => voice.default) ?? related[0] ?? null;
|
|
1980
|
-
}
|
|
1981
|
-
var activeOwner = null;
|
|
1982
|
-
var activeStop = null;
|
|
1983
|
-
function stopSpeaking() {
|
|
1984
|
-
activeStop?.();
|
|
1985
|
-
}
|
|
1986
|
-
function useSpeechOutput({
|
|
1987
|
-
lang
|
|
1988
|
-
} = {}) {
|
|
1989
|
-
const [available, setAvailable] = react.useState(() => speechPlatform() !== null);
|
|
1990
|
-
const [speaking, setSpeaking] = react.useState(false);
|
|
1991
|
-
const utteranceRef = react.useRef(null);
|
|
1992
|
-
const watchdogRef = react.useRef(null);
|
|
1993
|
-
const ownerRef = react.useRef({});
|
|
1994
|
-
const stopRef = react.useRef(() => void 0);
|
|
1995
|
-
const langRef = react.useRef(lang);
|
|
1996
|
-
langRef.current = lang;
|
|
1997
|
-
const clearWatchdog = react.useCallback(() => {
|
|
1998
|
-
if (watchdogRef.current === null) return;
|
|
1999
|
-
clearTimeout(watchdogRef.current);
|
|
2000
|
-
watchdogRef.current = null;
|
|
2001
|
-
}, []);
|
|
2002
|
-
const settle = react.useCallback(() => {
|
|
2003
|
-
clearWatchdog();
|
|
2004
|
-
utteranceRef.current = null;
|
|
2005
|
-
if (activeOwner === ownerRef.current) {
|
|
2006
|
-
activeOwner = null;
|
|
2007
|
-
activeStop = null;
|
|
2008
|
-
}
|
|
2009
|
-
setSpeaking(false);
|
|
2010
|
-
}, [clearWatchdog]);
|
|
2011
|
-
const stop = react.useCallback(() => {
|
|
2012
|
-
const owned = utteranceRef.current !== null;
|
|
2013
|
-
settle();
|
|
2014
|
-
if (owned) speechPlatform()?.synth.cancel();
|
|
2015
|
-
}, [settle]);
|
|
2016
|
-
stopRef.current = stop;
|
|
2017
|
-
const speak = react.useCallback(
|
|
2018
|
-
(text) => {
|
|
2019
|
-
const platform = speechPlatform();
|
|
2020
|
-
if (!platform) {
|
|
2021
|
-
setAvailable(false);
|
|
2022
|
-
return;
|
|
2023
|
-
}
|
|
2024
|
-
const spoken = text.trim();
|
|
2025
|
-
if (spoken === "") return;
|
|
2026
|
-
if (activeStop && activeOwner !== ownerRef.current) activeStop();
|
|
2027
|
-
platform.synth.cancel();
|
|
2028
|
-
const utterance = new platform.Utterance(spoken);
|
|
2029
|
-
const wanted = langRef.current;
|
|
2030
|
-
if (wanted !== void 0 && wanted !== "") {
|
|
2031
|
-
const voice = pickVoice(platform.synth.getVoices(), wanted);
|
|
2032
|
-
utterance.lang = voice?.lang ?? wanted;
|
|
2033
|
-
if (voice) utterance.voice = voice;
|
|
2034
|
-
}
|
|
2035
|
-
const mine = () => utteranceRef.current === utterance;
|
|
2036
|
-
utterance.onstart = () => {
|
|
2037
|
-
if (!mine()) return;
|
|
2038
|
-
clearWatchdog();
|
|
2039
|
-
setSpeaking(true);
|
|
2040
|
-
};
|
|
2041
|
-
utterance.onend = () => {
|
|
2042
|
-
if (!mine()) return;
|
|
2043
|
-
settle();
|
|
2044
|
-
};
|
|
2045
|
-
utterance.onerror = (event) => {
|
|
2046
|
-
if (!mine()) return;
|
|
2047
|
-
const code = typeof event?.error === "string" ? event.error : "";
|
|
2048
|
-
settle();
|
|
2049
|
-
if (code !== "interrupted" && code !== "canceled") setAvailable(false);
|
|
2050
|
-
};
|
|
2051
|
-
utteranceRef.current = utterance;
|
|
2052
|
-
activeOwner = ownerRef.current;
|
|
2053
|
-
activeStop = () => {
|
|
2054
|
-
stopRef.current();
|
|
2055
|
-
};
|
|
2056
|
-
setSpeaking(true);
|
|
2057
|
-
platform.synth.speak(utterance);
|
|
2058
|
-
clearWatchdog();
|
|
2059
|
-
watchdogRef.current = setTimeout(() => {
|
|
2060
|
-
watchdogRef.current = null;
|
|
2061
|
-
if (!mine()) return;
|
|
2062
|
-
if (platform.synth.speaking) return;
|
|
2063
|
-
settle();
|
|
2064
|
-
setAvailable(false);
|
|
2065
|
-
}, SILENT_FAILURE_MS);
|
|
2066
|
-
},
|
|
2067
|
-
[clearWatchdog, settle]
|
|
2068
|
-
);
|
|
2069
|
-
const toggle = react.useCallback(
|
|
2070
|
-
(text) => {
|
|
2071
|
-
if (speaking) stop();
|
|
2072
|
-
else speak(text);
|
|
2073
|
-
},
|
|
2074
|
-
[speak, speaking, stop]
|
|
2075
|
-
);
|
|
2076
|
-
react.useEffect(() => {
|
|
2077
|
-
if (!speaking || typeof window === "undefined") return;
|
|
2078
|
-
const onHide = () => {
|
|
2079
|
-
stopRef.current();
|
|
2080
|
-
};
|
|
2081
|
-
window.addEventListener("pagehide", onHide);
|
|
2082
|
-
return () => window.removeEventListener("pagehide", onHide);
|
|
2083
|
-
}, [speaking]);
|
|
2084
|
-
react.useEffect(
|
|
2085
|
-
() => () => {
|
|
2086
|
-
stopRef.current();
|
|
2087
|
-
},
|
|
2088
|
-
[]
|
|
2089
|
-
);
|
|
2090
|
-
return { supported: available, speaking, speak, stop, toggle };
|
|
2091
|
-
}
|
|
2092
|
-
var DRAIN_TARGET_MS = 1200;
|
|
2093
|
-
var MIN_CHARS_PER_SECOND = 90;
|
|
2094
|
-
var MAX_CHARS_PER_SECOND = 2200;
|
|
2095
|
-
var MAX_STEP_MS = 250;
|
|
2096
|
-
var COMMIT_INTERVAL_MS = 45;
|
|
2097
|
-
var FALLBACK_STEP_MS = 50;
|
|
2098
|
-
var FENCE = /^ {0,3}(?:```|~~~)/;
|
|
2099
|
-
var TABLE_ROW = /^ {0,3}\|/;
|
|
2100
|
-
var EMPTY_BLOCK_MARKER = /^ {0,3}(?:[-+>]|#{1,6}|\d{1,9}[.)])\s*$/;
|
|
2101
|
-
function isWordCharacter(char) {
|
|
2102
|
-
return char !== void 0 && /[\p{L}\p{N}]/u.test(char);
|
|
2103
|
-
}
|
|
2104
|
-
function clamp(value, min, max) {
|
|
2105
|
-
return Math.min(Math.max(value, min), max);
|
|
2106
|
-
}
|
|
2107
|
-
function now() {
|
|
2108
|
-
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
2109
|
-
}
|
|
2110
|
-
function isHighSurrogate(code) {
|
|
2111
|
-
return code >= 55296 && code <= 56319;
|
|
2112
|
-
}
|
|
2113
|
-
function safeInlineEnd(line) {
|
|
2114
|
-
let codeSpan = null;
|
|
2115
|
-
const links = [];
|
|
2116
|
-
const emphasis = [];
|
|
2117
|
-
let at = 0;
|
|
2118
|
-
while (at < line.length) {
|
|
2119
|
-
const char = line[at] ?? "";
|
|
2120
|
-
if (char === "\\") {
|
|
2121
|
-
at += 2;
|
|
2122
|
-
continue;
|
|
2123
|
-
}
|
|
2124
|
-
const innermost = links[links.length - 1];
|
|
2125
|
-
if (innermost !== void 0 && innermost.kind !== "label") {
|
|
2126
|
-
if (char === "(") links.push({ kind: "paren", start: innermost.start });
|
|
2127
|
-
else if (char === ")") links.pop();
|
|
2128
|
-
at += 1;
|
|
2129
|
-
continue;
|
|
2130
|
-
}
|
|
2131
|
-
if (char === "`") {
|
|
2132
|
-
let run = 1;
|
|
2133
|
-
while (line[at + run] === "`") run += 1;
|
|
2134
|
-
codeSpan = codeSpan === null ? at : null;
|
|
2135
|
-
at += run;
|
|
2136
|
-
continue;
|
|
2137
|
-
}
|
|
2138
|
-
if (codeSpan !== null) {
|
|
2139
|
-
at += 1;
|
|
2140
|
-
continue;
|
|
2141
|
-
}
|
|
2142
|
-
if (char === "[") {
|
|
2143
|
-
links.push({ kind: "label", start: line[at - 1] === "!" ? at - 1 : at });
|
|
2144
|
-
at += 1;
|
|
2145
|
-
continue;
|
|
2146
|
-
}
|
|
2147
|
-
if (char === "]" && innermost !== void 0) {
|
|
2148
|
-
links.pop();
|
|
2149
|
-
if (line[at + 1] === "(") {
|
|
2150
|
-
links.push({ kind: "destination", start: innermost.start });
|
|
2151
|
-
at += 2;
|
|
2152
|
-
continue;
|
|
2153
|
-
}
|
|
2154
|
-
at += 1;
|
|
2155
|
-
continue;
|
|
2156
|
-
}
|
|
2157
|
-
if (char === "*" || char === "_" || char === "~") {
|
|
2158
|
-
let run = 1;
|
|
2159
|
-
while (line[at + run] === char) run += 1;
|
|
2160
|
-
const start = at;
|
|
2161
|
-
at += run;
|
|
2162
|
-
const before = line[start - 1];
|
|
2163
|
-
const after = line[start + run];
|
|
2164
|
-
if (run < 2) continue;
|
|
2165
|
-
if (char === "_" && isWordCharacter(before) && isWordCharacter(after)) {
|
|
2166
|
-
continue;
|
|
2167
|
-
}
|
|
2168
|
-
const marker = char.repeat(run);
|
|
2169
|
-
const open2 = emphasis.findIndex((entry) => entry.marker === marker);
|
|
2170
|
-
if (open2 !== -1) {
|
|
2171
|
-
emphasis.splice(open2, 1);
|
|
2172
|
-
continue;
|
|
2173
|
-
}
|
|
2174
|
-
if (after !== void 0 && /\s/.test(after)) continue;
|
|
2175
|
-
emphasis.push({ marker, start });
|
|
2176
|
-
continue;
|
|
2177
|
-
}
|
|
2178
|
-
at += 1;
|
|
2179
|
-
}
|
|
2180
|
-
const open = [
|
|
2181
|
-
codeSpan,
|
|
2182
|
-
links[0]?.start ?? null,
|
|
2183
|
-
emphasis[0]?.start ?? null
|
|
2184
|
-
].filter((value) => value !== null);
|
|
2185
|
-
return open.length === 0 ? line.length : Math.min(...open);
|
|
2186
|
-
}
|
|
2187
|
-
function revealBoundary(text, limit) {
|
|
2188
|
-
if (limit <= 0) return 0;
|
|
2189
|
-
const caughtUp = limit >= text.length;
|
|
2190
|
-
const head = caughtUp ? text : text.slice(0, limit);
|
|
2191
|
-
const lineStart = head.lastIndexOf("\n") + 1;
|
|
2192
|
-
const line = head.slice(lineStart);
|
|
2193
|
-
let fenceStart = null;
|
|
2194
|
-
let tableStart = null;
|
|
2195
|
-
let tableRows = 0;
|
|
2196
|
-
let at = 0;
|
|
2197
|
-
while (at < lineStart) {
|
|
2198
|
-
const breakAt = head.indexOf("\n", at);
|
|
2199
|
-
const end = breakAt === -1 ? lineStart : breakAt;
|
|
2200
|
-
const row = head.slice(at, end);
|
|
2201
|
-
if (fenceStart !== null) {
|
|
2202
|
-
if (FENCE.test(row)) fenceStart = null;
|
|
2203
|
-
} else if (FENCE.test(row)) {
|
|
2204
|
-
fenceStart = at;
|
|
2205
|
-
} else if (TABLE_ROW.test(row)) {
|
|
2206
|
-
if (tableStart === null) {
|
|
2207
|
-
tableStart = at;
|
|
2208
|
-
tableRows = 0;
|
|
2209
|
-
}
|
|
2210
|
-
tableRows += 1;
|
|
2211
|
-
} else if (tableStart !== null) {
|
|
2212
|
-
tableStart = null;
|
|
2213
|
-
tableRows = 0;
|
|
2214
|
-
}
|
|
2215
|
-
at = end + 1;
|
|
2216
|
-
}
|
|
2217
|
-
if (fenceStart === null) {
|
|
2218
|
-
const lastIsRow = TABLE_ROW.test(line);
|
|
2219
|
-
if (lastIsRow && tableStart === null) {
|
|
2220
|
-
tableStart = lineStart;
|
|
2221
|
-
tableRows = 0;
|
|
2222
|
-
} else if (!lastIsRow && line !== "") {
|
|
2223
|
-
tableStart = null;
|
|
2224
|
-
tableRows = 0;
|
|
2225
|
-
}
|
|
2226
|
-
if (tableStart !== null && tableRows < 2) return tableStart;
|
|
2227
|
-
}
|
|
2228
|
-
if (caughtUp) return text.length;
|
|
2229
|
-
if (fenceStart !== null) return fenceStart;
|
|
2230
|
-
if (TABLE_ROW.test(line)) return lineStart;
|
|
2231
|
-
if (EMPTY_BLOCK_MARKER.test(line)) return lineStart;
|
|
2232
|
-
const cut = lineStart + safeInlineEnd(line);
|
|
2233
|
-
return isHighSurrogate(text.charCodeAt(cut - 1)) ? cut - 1 : cut;
|
|
2234
|
-
}
|
|
2235
|
-
function prefersReducedMotion() {
|
|
2236
|
-
if (typeof window === "undefined") return true;
|
|
2237
|
-
try {
|
|
2238
|
-
return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
|
|
2239
|
-
} catch {
|
|
2240
|
-
return false;
|
|
2241
|
-
}
|
|
2242
|
-
}
|
|
2243
|
-
function useProgressiveReveal(text, settled) {
|
|
2244
|
-
const [, setFrame] = react.useState(0);
|
|
2245
|
-
const [reduced] = react.useState(prefersReducedMotion);
|
|
2246
|
-
const immediate = settled || reduced;
|
|
2247
|
-
const textRef = react.useRef(text);
|
|
2248
|
-
const cursorRef = react.useRef(immediate ? text.length : 0);
|
|
2249
|
-
const cutRef = react.useRef(immediate ? text.length : 0);
|
|
2250
|
-
const committedRef = react.useRef(0);
|
|
2251
|
-
if (text !== textRef.current) {
|
|
2252
|
-
const revealed = textRef.current.slice(0, Math.floor(cursorRef.current));
|
|
2253
|
-
if (revealed !== "" && !text.startsWith(revealed)) {
|
|
2254
|
-
cursorRef.current = text.length;
|
|
2255
|
-
}
|
|
2256
|
-
cursorRef.current = Math.min(cursorRef.current, text.length);
|
|
2257
|
-
cutRef.current = Math.min(cutRef.current, text.length);
|
|
2258
|
-
textRef.current = text;
|
|
2259
|
-
}
|
|
2260
|
-
if (immediate) cursorRef.current = text.length;
|
|
2261
|
-
const cut = immediate ? text.length : Math.max(
|
|
2262
|
-
cutRef.current,
|
|
2263
|
-
revealBoundary(text, Math.floor(cursorRef.current))
|
|
2264
|
-
);
|
|
2265
|
-
cutRef.current = cut;
|
|
2266
|
-
react.useEffect(() => {
|
|
2267
|
-
if (immediate) return;
|
|
2268
|
-
if (cursorRef.current >= text.length) return;
|
|
2269
|
-
let cancelled = false;
|
|
2270
|
-
let cancelStep = null;
|
|
2271
|
-
let previous = now();
|
|
2272
|
-
const commit = (at, force) => {
|
|
2273
|
-
if (!force && at - committedRef.current < COMMIT_INTERVAL_MS) return;
|
|
2274
|
-
committedRef.current = at;
|
|
2275
|
-
setFrame((value) => value + 1);
|
|
2276
|
-
};
|
|
2277
|
-
const step = () => {
|
|
2278
|
-
cancelStep = null;
|
|
2279
|
-
if (cancelled) return;
|
|
2280
|
-
const at = now();
|
|
2281
|
-
const elapsed = clamp(at - previous, 0, MAX_STEP_MS);
|
|
2282
|
-
previous = at;
|
|
2283
|
-
const length = textRef.current.length;
|
|
2284
|
-
const pending = length - cursorRef.current;
|
|
2285
|
-
if (pending <= 0) {
|
|
2286
|
-
commit(at, true);
|
|
2287
|
-
return;
|
|
2288
|
-
}
|
|
2289
|
-
const perSecond = clamp(
|
|
2290
|
-
pending / (DRAIN_TARGET_MS / 1e3),
|
|
2291
|
-
MIN_CHARS_PER_SECOND,
|
|
2292
|
-
MAX_CHARS_PER_SECOND
|
|
2293
|
-
);
|
|
2294
|
-
cursorRef.current = Math.min(
|
|
2295
|
-
length,
|
|
2296
|
-
cursorRef.current + perSecond * elapsed / 1e3
|
|
2297
|
-
);
|
|
2298
|
-
const done = cursorRef.current >= length;
|
|
2299
|
-
commit(at, done);
|
|
2300
|
-
if (!done) schedule();
|
|
2301
|
-
};
|
|
2302
|
-
const schedule = () => {
|
|
2303
|
-
if (typeof window.requestAnimationFrame === "function") {
|
|
2304
|
-
const id2 = window.requestAnimationFrame(step);
|
|
2305
|
-
cancelStep = () => window.cancelAnimationFrame(id2);
|
|
2306
|
-
return;
|
|
2307
|
-
}
|
|
2308
|
-
const id = setTimeout(step, FALLBACK_STEP_MS);
|
|
2309
|
-
cancelStep = () => clearTimeout(id);
|
|
2310
|
-
};
|
|
2311
|
-
schedule();
|
|
2312
|
-
return () => {
|
|
2313
|
-
cancelled = true;
|
|
2314
|
-
cancelStep?.();
|
|
2315
|
-
};
|
|
2316
|
-
}, [text, immediate]);
|
|
2317
|
-
return text.slice(0, cut);
|
|
2318
|
-
}
|
|
2319
|
-
|
|
2320
|
-
exports.DEFAULT_EXPLORE_EXAMPLE_PROMPTS = DEFAULT_EXPLORE_EXAMPLE_PROMPTS;
|
|
2321
|
-
exports.FALLBACK_EXPLORE_LIMITS = FALLBACK_EXPLORE_LIMITS;
|
|
2322
|
-
exports.classifyExploreError = classifyExploreError;
|
|
2323
|
-
exports.stopSpeaking = stopSpeaking;
|
|
2324
|
-
exports.useExploreChat = useExploreChat;
|
|
2325
|
-
exports.useProgressiveReveal = useProgressiveReveal;
|
|
2326
|
-
exports.useSpeechInput = useSpeechInput;
|
|
2327
|
-
exports.useSpeechOutput = useSpeechOutput;
|
|
2328
44
|
//# sourceMappingURL=index.js.map
|
|
2329
45
|
//# sourceMappingURL=index.js.map
|