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