@burdenoff/website-sdk 2026.828.4 → 2026.828.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1,24 @@
1
- export { D as DEFAULT_EXPLORE_EXAMPLE_PROMPTS, d as ExploreChatError, e as ExploreChatErrorKind, f as ExploreChatPhase, F as FALLBACK_EXPLORE_LIMITS, U as UseExploreChatOptions, u as UseExploreChatResult, v as classifyExploreError, w as useExploreChat } from '../index-CUJTTlYx.mjs';
1
+ export { D as DEFAULT_EXPLORE_EXAMPLE_PROMPTS, E as ExploreChatError, a as ExploreChatErrorKind, b as ExploreChatPhase, F as FALLBACK_EXPLORE_LIMITS, U as UseExploreChatOptions, c as UseExploreChatResult, d as classifyExploreError, u as useExploreChat } from '../use-explore-chat-DhvZhH8f.mjs';
2
+
3
+ interface UseSpeechInputOptions {
4
+ /** Receives settled text as it is recognised. Append it to the draft. */
5
+ onFinalTranscript: (text: string) => void;
6
+ /** BCP-47 tag. Defaults to the browser's language. */
7
+ lang?: string;
8
+ /** Called when the recogniser reports a problem worth showing. */
9
+ onError?: (message: string) => void;
10
+ }
11
+ interface UseSpeechInputResult {
12
+ /** False when the browser has no speech recogniser — hide the control entirely. */
13
+ supported: boolean;
14
+ listening: boolean;
15
+ /** Words recognised but not yet settled; render as a live hint, do not store. */
16
+ interim: string;
17
+ error: string | null;
18
+ start: () => void;
19
+ stop: () => void;
20
+ toggle: () => void;
21
+ }
22
+ declare function useSpeechInput({ onFinalTranscript, lang, onError, }: UseSpeechInputOptions): UseSpeechInputResult;
23
+
24
+ export { type UseSpeechInputOptions, type UseSpeechInputResult, useSpeechInput };
@@ -1 +1,24 @@
1
- export { D as DEFAULT_EXPLORE_EXAMPLE_PROMPTS, d as ExploreChatError, e as ExploreChatErrorKind, f as ExploreChatPhase, F as FALLBACK_EXPLORE_LIMITS, U as UseExploreChatOptions, u as UseExploreChatResult, v as classifyExploreError, w as useExploreChat } from '../index-CUJTTlYx.js';
1
+ export { D as DEFAULT_EXPLORE_EXAMPLE_PROMPTS, E as ExploreChatError, a as ExploreChatErrorKind, b as ExploreChatPhase, F as FALLBACK_EXPLORE_LIMITS, U as UseExploreChatOptions, c as UseExploreChatResult, d as classifyExploreError, u as useExploreChat } from '../use-explore-chat-DhvZhH8f.js';
2
+
3
+ interface UseSpeechInputOptions {
4
+ /** Receives settled text as it is recognised. Append it to the draft. */
5
+ onFinalTranscript: (text: string) => void;
6
+ /** BCP-47 tag. Defaults to the browser's language. */
7
+ lang?: string;
8
+ /** Called when the recogniser reports a problem worth showing. */
9
+ onError?: (message: string) => void;
10
+ }
11
+ interface UseSpeechInputResult {
12
+ /** False when the browser has no speech recogniser — hide the control entirely. */
13
+ supported: boolean;
14
+ listening: boolean;
15
+ /** Words recognised but not yet settled; render as a live hint, do not store. */
16
+ interim: string;
17
+ error: string | null;
18
+ start: () => void;
19
+ stop: () => void;
20
+ toggle: () => void;
21
+ }
22
+ declare function useSpeechInput({ onFinalTranscript, lang, onError, }: UseSpeechInputOptions): UseSpeechInputResult;
23
+
24
+ export { type UseSpeechInputOptions, type UseSpeechInputResult, useSpeechInput };
@@ -561,6 +561,8 @@ async function getRecaptchaV3Token(siteKey, action = EXPLORE_RECAPTCHA_ACTION) {
561
561
 
562
562
  // src/hooks/use-explore-chat.ts
563
563
  var DEFAULT_STORAGE_KEY = "boff.explore.v1";
564
+ var HISTORY_SUFFIX = ".history";
565
+ var MAX_ARCHIVED_CONVERSATIONS = 15;
564
566
  var DEFAULT_POLL_INTERVAL_MS = 1500;
565
567
  var DEFAULT_POLL_TIMEOUT_MS = 9e4;
566
568
  var POLL_REQUEST_TIMEOUT_MS = 1e4;
@@ -699,10 +701,58 @@ function classifyExploreError(code, serverMessage) {
699
701
  retryable: true
700
702
  };
701
703
  }
704
+ function historyKey(key) {
705
+ return `${key}${HISTORY_SUFFIX}`;
706
+ }
707
+ function readArchive(key) {
708
+ if (typeof window === "undefined") return [];
709
+ try {
710
+ const raw = window.localStorage.getItem(historyKey(key));
711
+ if (!raw) return [];
712
+ const parsed = JSON.parse(raw);
713
+ if (!Array.isArray(parsed)) return [];
714
+ return parsed.filter(
715
+ (entry) => typeof entry === "object" && entry !== null && typeof entry.token === "string" && Array.isArray(entry.messages)
716
+ );
717
+ } catch {
718
+ return [];
719
+ }
720
+ }
721
+ function writeArchive(key, entries) {
722
+ if (typeof window === "undefined") return;
723
+ try {
724
+ if (entries.length === 0) {
725
+ window.localStorage.removeItem(historyKey(key));
726
+ return;
727
+ }
728
+ window.localStorage.setItem(historyKey(key), JSON.stringify(entries));
729
+ } catch {
730
+ }
731
+ }
732
+ function upsertArchive(key, thread) {
733
+ if (!thread.token) return readArchive(key);
734
+ const real = thread.messages.filter(
735
+ (m) => m.content.trim() !== "" || m.role === "USER"
736
+ );
737
+ if (real.length === 0) return readArchive(key);
738
+ const firstUser = real.find((m) => m.role === "USER");
739
+ const entry = {
740
+ token: thread.token,
741
+ title: (firstUser?.content ?? "Conversation").trim().slice(0, 80),
742
+ updatedAt: real[real.length - 1]?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
743
+ messageCount: real.length,
744
+ focusProduct: thread.focusProduct,
745
+ messages: real.slice(-20)
746
+ };
747
+ const rest = readArchive(key).filter((e) => e.token !== entry.token);
748
+ const next = [entry, ...rest].slice(0, MAX_ARCHIVED_CONVERSATIONS);
749
+ writeArchive(key, next);
750
+ return next;
751
+ }
702
752
  function readPersisted(key) {
703
753
  if (typeof window === "undefined") return null;
704
754
  try {
705
- const raw = window.sessionStorage.getItem(key);
755
+ const raw = window.localStorage.getItem(key);
706
756
  if (!raw) return null;
707
757
  const parsed = JSON.parse(raw);
708
758
  if (!parsed || typeof parsed !== "object") return null;
@@ -747,6 +797,8 @@ function useExploreChat(options = {}) {
747
797
  const [remainingToday, setRemainingToday] = react.useState(null);
748
798
  const [focusProduct, setFocusProductState] = react.useState(null);
749
799
  const [turnCount, setTurnCount] = react.useState(0);
800
+ const messagesRef = react.useRef([]);
801
+ const [archive, setArchive] = react.useState([]);
750
802
  const [hydrated, setHydrated] = react.useState(false);
751
803
  const mountedRef = react.useRef(true);
752
804
  const pollGenerationRef = react.useRef(0);
@@ -784,6 +836,7 @@ function useExploreChat(options = {}) {
784
836
  setHydrated(true);
785
837
  return;
786
838
  }
839
+ setArchive(readArchive(storageKey));
787
840
  const stored = readPersisted(storageKey);
788
841
  if (stored) {
789
842
  conversationTokenRef.current = stored.conversationToken;
@@ -812,13 +865,28 @@ function useExploreChat(options = {}) {
812
865
  focusProduct
813
866
  };
814
867
  if (!payload.conversationToken && payload.messages.length === 0 && !payload.focusProduct) {
815
- window.sessionStorage.removeItem(storageKey);
868
+ window.localStorage.removeItem(storageKey);
816
869
  return;
817
870
  }
818
- window.sessionStorage.setItem(storageKey, JSON.stringify(payload));
871
+ window.localStorage.setItem(storageKey, JSON.stringify(payload));
819
872
  } catch {
820
873
  }
821
874
  }, [persist, hydrated, storageKey, messages, focusProduct]);
875
+ react.useEffect(() => {
876
+ messagesRef.current = messages;
877
+ if (!persist || !hydrated) return;
878
+ const settled = messages.some(
879
+ (m) => m.role === "ASSISTANT" && (m.status === "COMPLETED" || m.status === "FAILED")
880
+ );
881
+ if (!settled) return;
882
+ setArchive(
883
+ upsertArchive(storageKey, {
884
+ token: conversationTokenRef.current,
885
+ messages,
886
+ focusProduct: focusProductRef.current
887
+ })
888
+ );
889
+ }, [messages, persist, hydrated, storageKey]);
822
890
  react.useEffect(() => {
823
891
  let cancelled = false;
824
892
  const load = async () => {
@@ -1104,6 +1172,13 @@ function useExploreChat(options = {}) {
1104
1172
  void send(text);
1105
1173
  }, [cancelPolling, send]);
1106
1174
  const reset = react.useCallback(() => {
1175
+ setArchive(
1176
+ upsertArchive(storageKey, {
1177
+ token: conversationTokenRef.current,
1178
+ messages: messagesRef.current,
1179
+ focusProduct: focusProductRef.current
1180
+ })
1181
+ );
1107
1182
  cancelPolling();
1108
1183
  inFlightRef.current = false;
1109
1184
  conversationTokenRef.current = null;
@@ -1116,11 +1191,48 @@ function useExploreChat(options = {}) {
1116
1191
  setPhase(catalogRef.current.enabled ? "ready" : "disabled");
1117
1192
  if (typeof window !== "undefined") {
1118
1193
  try {
1119
- window.sessionStorage.removeItem(storageKey);
1194
+ window.localStorage.removeItem(storageKey);
1120
1195
  } catch {
1121
1196
  }
1122
1197
  }
1123
1198
  }, [cancelPolling, storageKey]);
1199
+ const openConversation = react.useCallback(
1200
+ (token) => {
1201
+ const entry = readArchive(storageKey).find((e) => e.token === token);
1202
+ if (!entry) return;
1203
+ upsertArchive(storageKey, {
1204
+ token: conversationTokenRef.current,
1205
+ messages: messagesRef.current,
1206
+ focusProduct: focusProductRef.current
1207
+ });
1208
+ cancelPolling();
1209
+ inFlightRef.current = false;
1210
+ conversationTokenRef.current = entry.token;
1211
+ turnCountRef.current = entry.messages.filter(
1212
+ (m) => m.role === "USER"
1213
+ ).length;
1214
+ setTurnCount(turnCountRef.current);
1215
+ setMessages(entry.messages);
1216
+ focusProductRef.current = entry.focusProduct;
1217
+ setFocusProductState(entry.focusProduct);
1218
+ setError(null);
1219
+ setPhase(catalogRef.current.enabled ? "ready" : "disabled");
1220
+ setArchive(readArchive(storageKey));
1221
+ },
1222
+ [cancelPolling, storageKey]
1223
+ );
1224
+ const deleteConversation = react.useCallback(
1225
+ (token) => {
1226
+ const next = readArchive(storageKey).filter((e) => e.token !== token);
1227
+ writeArchive(storageKey, next);
1228
+ setArchive(next);
1229
+ },
1230
+ [storageKey]
1231
+ );
1232
+ const clearHistory = react.useCallback(() => {
1233
+ writeArchive(storageKey, []);
1234
+ setArchive([]);
1235
+ }, [storageKey]);
1124
1236
  const setFocusProduct = react.useCallback((slug) => {
1125
1237
  focusProductRef.current = slug;
1126
1238
  setFocusProductState(slug);
@@ -1149,7 +1261,11 @@ function useExploreChat(options = {}) {
1149
1261
  stop,
1150
1262
  retry,
1151
1263
  reset,
1152
- canSend
1264
+ canSend,
1265
+ history: archive,
1266
+ openConversation,
1267
+ deleteConversation,
1268
+ clearHistory
1153
1269
  };
1154
1270
  }
1155
1271
  var optimisticCounter = 0;
@@ -1157,10 +1273,187 @@ function makeOptimisticId() {
1157
1273
  optimisticCounter += 1;
1158
1274
  return `boff-explore-local-${Date.now()}-${optimisticCounter}`;
1159
1275
  }
1276
+ function getRecognitionCtor() {
1277
+ if (typeof window === "undefined") return null;
1278
+ const w = window;
1279
+ return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
1280
+ }
1281
+ var MIC_DENIED_MESSAGE = "Microphone access was blocked. Allow it in your browser's site settings to dictate.";
1282
+ async function ensureMicrophoneAccess() {
1283
+ const media = typeof navigator === "undefined" ? void 0 : navigator.mediaDevices;
1284
+ if (!media?.getUserMedia) return { ok: true };
1285
+ try {
1286
+ const status = await navigator.permissions?.query({
1287
+ name: "microphone"
1288
+ });
1289
+ if (status?.state === "granted") return { ok: true };
1290
+ if (status?.state === "denied")
1291
+ return { ok: false, message: MIC_DENIED_MESSAGE };
1292
+ } catch {
1293
+ }
1294
+ try {
1295
+ const stream = await media.getUserMedia({ audio: true });
1296
+ for (const track of stream.getTracks()) track.stop();
1297
+ return { ok: true };
1298
+ } catch (error) {
1299
+ const name = typeof error === "object" && error !== null && "name" in error ? String(error.name) : "";
1300
+ if (name === "NotAllowedError" || name === "SecurityError") {
1301
+ return { ok: false, message: MIC_DENIED_MESSAGE };
1302
+ }
1303
+ if (name === "NotFoundError" || name === "DevicesNotFoundError") {
1304
+ return { ok: false, message: "No microphone was found." };
1305
+ }
1306
+ return {
1307
+ ok: false,
1308
+ message: "Dictation could not start. You can type instead."
1309
+ };
1310
+ }
1311
+ }
1312
+ function describeError(code) {
1313
+ switch (code) {
1314
+ case "not-allowed":
1315
+ case "service-not-allowed":
1316
+ return MIC_DENIED_MESSAGE;
1317
+ case "no-speech":
1318
+ return "I didn't catch anything \u2014 try again a little closer to the mic.";
1319
+ case "audio-capture":
1320
+ return "No microphone was found.";
1321
+ case "network":
1322
+ return "Speech recognition needs a network connection.";
1323
+ case "aborted":
1324
+ return "";
1325
+ default:
1326
+ return "Dictation stopped unexpectedly. You can type instead.";
1327
+ }
1328
+ }
1329
+ function useSpeechInput({
1330
+ onFinalTranscript,
1331
+ lang,
1332
+ onError
1333
+ }) {
1334
+ const [supported] = react.useState(() => getRecognitionCtor() !== null);
1335
+ const [listening, setListening] = react.useState(false);
1336
+ const [interim, setInterim] = react.useState("");
1337
+ const [error, setError] = react.useState(null);
1338
+ const recognitionRef = react.useRef(null);
1339
+ const startTokenRef = react.useRef(0);
1340
+ const beginRecognitionRef = react.useRef(null);
1341
+ const beginRecognition = react.useCallback(
1342
+ (Ctor, token) => {
1343
+ beginRecognitionRef.current?.(Ctor, token);
1344
+ },
1345
+ []
1346
+ );
1347
+ const finalRef = react.useRef(onFinalTranscript);
1348
+ const errorRef = react.useRef(onError);
1349
+ finalRef.current = onFinalTranscript;
1350
+ errorRef.current = onError;
1351
+ const stop = react.useCallback(() => {
1352
+ startTokenRef.current += 1;
1353
+ setListening(false);
1354
+ setInterim("");
1355
+ const recognition = recognitionRef.current;
1356
+ if (!recognition) return;
1357
+ try {
1358
+ recognition.stop();
1359
+ } catch {
1360
+ }
1361
+ setListening(false);
1362
+ setInterim("");
1363
+ }, []);
1364
+ const start = react.useCallback(() => {
1365
+ const Ctor = getRecognitionCtor();
1366
+ if (!Ctor) return;
1367
+ if (recognitionRef.current) stop();
1368
+ setError(null);
1369
+ setListening(true);
1370
+ startTokenRef.current += 1;
1371
+ const token = startTokenRef.current;
1372
+ void ensureMicrophoneAccess().then((access) => {
1373
+ if (token !== startTokenRef.current) return;
1374
+ if (!access.ok) {
1375
+ setListening(false);
1376
+ setError(access.message);
1377
+ errorRef.current?.(access.message);
1378
+ return;
1379
+ }
1380
+ beginRecognition(Ctor, token);
1381
+ });
1382
+ }, [beginRecognition, stop]);
1383
+ const beginRecognitionImpl = react.useCallback(
1384
+ (Ctor, token) => {
1385
+ const recognition = new Ctor();
1386
+ recognition.lang = lang ?? (typeof navigator === "undefined" ? "en-US" : navigator.language || "en-US");
1387
+ recognition.continuous = true;
1388
+ recognition.interimResults = true;
1389
+ recognition.maxAlternatives = 1;
1390
+ recognition.onstart = () => {
1391
+ if (token !== startTokenRef.current) return;
1392
+ setError(null);
1393
+ setListening(true);
1394
+ };
1395
+ recognition.onresult = (event) => {
1396
+ let settled = "";
1397
+ let pending = "";
1398
+ for (let i = event.resultIndex; i < event.results.length; i += 1) {
1399
+ const result = event.results[i];
1400
+ if (!result) continue;
1401
+ const text = result[0]?.transcript ?? "";
1402
+ if (result.isFinal) settled += text;
1403
+ else pending += text;
1404
+ }
1405
+ setInterim(pending);
1406
+ if (settled.trim() !== "") finalRef.current(settled);
1407
+ };
1408
+ recognition.onerror = (event) => {
1409
+ const message = describeError(event.error);
1410
+ setListening(false);
1411
+ setInterim("");
1412
+ if (message !== "") {
1413
+ setError(message);
1414
+ errorRef.current?.(message);
1415
+ }
1416
+ };
1417
+ recognition.onend = () => {
1418
+ setListening(false);
1419
+ setInterim("");
1420
+ };
1421
+ recognitionRef.current = recognition;
1422
+ try {
1423
+ recognition.start();
1424
+ } catch {
1425
+ setListening(false);
1426
+ }
1427
+ },
1428
+ [lang]
1429
+ );
1430
+ beginRecognitionRef.current = beginRecognitionImpl;
1431
+ const toggle = react.useCallback(() => {
1432
+ if (listening) stop();
1433
+ else start();
1434
+ }, [listening, start, stop]);
1435
+ react.useEffect(
1436
+ () => () => {
1437
+ const recognition = recognitionRef.current;
1438
+ if (!recognition) return;
1439
+ recognition.onresult = null;
1440
+ recognition.onerror = null;
1441
+ recognition.onend = null;
1442
+ recognition.onstart = null;
1443
+ try {
1444
+ recognition.abort();
1445
+ } catch {
1446
+ }
1447
+ },
1448
+ []
1449
+ );
1450
+ return { supported, listening, interim, error, start, stop, toggle };
1451
+ }
1160
1452
 
1161
1453
  exports.DEFAULT_EXPLORE_EXAMPLE_PROMPTS = DEFAULT_EXPLORE_EXAMPLE_PROMPTS;
1162
1454
  exports.FALLBACK_EXPLORE_LIMITS = FALLBACK_EXPLORE_LIMITS;
1163
1455
  exports.classifyExploreError = classifyExploreError;
1164
1456
  exports.useExploreChat = useExploreChat;
1457
+ exports.useSpeechInput = useSpeechInput;
1165
1458
  //# sourceMappingURL=index.js.map
1166
1459
  //# sourceMappingURL=index.js.map