@burdenoff/website-sdk 2026.828.7 → 2026.829.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1276,21 +1276,37 @@ function getRecognitionCtor() {
1276
1276
  const w = window;
1277
1277
  return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
1278
1278
  }
1279
+ function isTouchDevice() {
1280
+ if (typeof window === "undefined" || !window.matchMedia) return false;
1281
+ try {
1282
+ return window.matchMedia("(pointer: coarse)").matches;
1283
+ } catch {
1284
+ return false;
1285
+ }
1286
+ }
1279
1287
  var MIC_DENIED_MESSAGE = "Microphone access was blocked. Allow it in your browser's site settings to dictate.";
1288
+ var MIC_UNAVAILABLE_MESSAGE = "Dictation isn't available in this browser. You can type instead.";
1289
+ var RELEASE_SETTLE_MS = 250;
1290
+ var RETRY_DELAY_MS = 350;
1291
+ var sleep = (ms) => new Promise((resolve) => {
1292
+ setTimeout(resolve, ms);
1293
+ });
1280
1294
  async function ensureMicrophoneAccess() {
1281
1295
  const media = typeof navigator === "undefined" ? void 0 : navigator.mediaDevices;
1282
- if (!media?.getUserMedia) return { ok: true, confirmed: false };
1296
+ if (!media?.getUserMedia)
1297
+ return { ok: true, confirmed: false, primed: false };
1283
1298
  try {
1284
1299
  const status = await navigator.permissions?.query({
1285
1300
  name: "microphone"
1286
1301
  });
1287
- if (status?.state === "granted") return { ok: true, confirmed: true };
1302
+ if (status?.state === "granted")
1303
+ return { ok: true, confirmed: true, primed: false };
1288
1304
  } catch {
1289
1305
  }
1290
1306
  try {
1291
1307
  const stream = await media.getUserMedia({ audio: true });
1292
1308
  for (const track of stream.getTracks()) track.stop();
1293
- return { ok: true, confirmed: true };
1309
+ return { ok: true, confirmed: true, primed: true };
1294
1310
  } catch (error) {
1295
1311
  const name = typeof error === "object" && error !== null && "name" in error ? String(error.name) : "";
1296
1312
  if (name === "NotAllowedError" || name === "SecurityError") {
@@ -1334,19 +1350,17 @@ function useSpeechInput({
1334
1350
  const recognitionRef = useRef(null);
1335
1351
  const startTokenRef = useRef(0);
1336
1352
  const micConfirmedRef = useRef(false);
1337
- const beginRecognitionRef = useRef(null);
1338
- const beginRecognition = useCallback(
1339
- (Ctor, token) => {
1340
- beginRecognitionRef.current?.(Ctor, token);
1341
- },
1342
- []
1343
- );
1353
+ const wantsListeningRef = useRef(false);
1354
+ const retriedRef = useRef(false);
1344
1355
  const finalRef = useRef(onFinalTranscript);
1345
1356
  const errorRef = useRef(onError);
1346
1357
  finalRef.current = onFinalTranscript;
1347
1358
  errorRef.current = onError;
1359
+ const langRef = useRef(lang);
1360
+ langRef.current = lang;
1348
1361
  const stop = useCallback(() => {
1349
1362
  startTokenRef.current += 1;
1363
+ wantsListeningRef.current = false;
1350
1364
  setListening(false);
1351
1365
  setInterim("");
1352
1366
  const recognition = recognitionRef.current;
@@ -1355,83 +1369,111 @@ function useSpeechInput({
1355
1369
  recognition.stop();
1356
1370
  } catch {
1357
1371
  }
1358
- setListening(false);
1359
- setInterim("");
1360
1372
  }, []);
1361
- const start = useCallback(() => {
1373
+ const openSession = useCallback((token) => {
1362
1374
  const Ctor = getRecognitionCtor();
1363
- if (!Ctor) return;
1364
- if (recognitionRef.current) stop();
1365
- setError(null);
1366
- setListening(true);
1375
+ if (!Ctor || token !== startTokenRef.current) return;
1376
+ const recognition = new Ctor();
1377
+ recognition.lang = langRef.current ?? (typeof navigator === "undefined" ? "en-US" : navigator.language || "en-US");
1378
+ recognition.continuous = !isTouchDevice();
1379
+ recognition.interimResults = true;
1380
+ recognition.maxAlternatives = 1;
1381
+ recognition.onstart = () => {
1382
+ if (token !== startTokenRef.current) return;
1383
+ setError(null);
1384
+ setListening(true);
1385
+ };
1386
+ recognition.onresult = (event) => {
1387
+ if (token !== startTokenRef.current) return;
1388
+ let settled = "";
1389
+ let pending = "";
1390
+ for (let i = event.resultIndex; i < event.results.length; i += 1) {
1391
+ const result = event.results[i];
1392
+ if (!result) continue;
1393
+ const text = result[0]?.transcript ?? "";
1394
+ if (result.isFinal) settled += text;
1395
+ else pending += text;
1396
+ }
1397
+ setInterim(pending);
1398
+ if (settled.trim() !== "") finalRef.current(settled);
1399
+ };
1400
+ recognition.onerror = (event) => {
1401
+ if (token !== startTokenRef.current) return;
1402
+ if ((event.error === "not-allowed" || event.error === "service-not-allowed") && micConfirmedRef.current && !retriedRef.current) {
1403
+ retriedRef.current = true;
1404
+ void sleep(RETRY_DELAY_MS).then(() => {
1405
+ if (token !== startTokenRef.current || !wantsListeningRef.current)
1406
+ return;
1407
+ openSessionRef.current?.(token);
1408
+ });
1409
+ return;
1410
+ }
1411
+ const message = micConfirmedRef.current && (event.error === "not-allowed" || event.error === "service-not-allowed") ? MIC_UNAVAILABLE_MESSAGE : describeError(event.error);
1412
+ wantsListeningRef.current = false;
1413
+ setListening(false);
1414
+ setInterim("");
1415
+ if (message !== "") {
1416
+ setError(message);
1417
+ errorRef.current?.(message);
1418
+ }
1419
+ };
1420
+ recognition.onend = () => {
1421
+ if (token !== startTokenRef.current) return;
1422
+ setInterim("");
1423
+ if (wantsListeningRef.current && isTouchDevice()) {
1424
+ void sleep(120).then(() => {
1425
+ if (token !== startTokenRef.current || !wantsListeningRef.current)
1426
+ return;
1427
+ openSessionRef.current?.(token);
1428
+ });
1429
+ return;
1430
+ }
1431
+ setListening(false);
1432
+ };
1433
+ recognitionRef.current = recognition;
1434
+ try {
1435
+ recognition.start();
1436
+ } catch {
1437
+ wantsListeningRef.current = false;
1438
+ setListening(false);
1439
+ }
1440
+ }, []);
1441
+ const openSessionRef = useRef(null);
1442
+ openSessionRef.current = openSession;
1443
+ const start = useCallback(() => {
1444
+ if (!getRecognitionCtor()) return;
1367
1445
  startTokenRef.current += 1;
1368
1446
  const token = startTokenRef.current;
1369
- void ensureMicrophoneAccess().then((access) => {
1447
+ wantsListeningRef.current = true;
1448
+ retriedRef.current = false;
1449
+ micConfirmedRef.current = false;
1450
+ setError(null);
1451
+ setListening(true);
1452
+ void ensureMicrophoneAccess().then(async (access) => {
1370
1453
  if (token !== startTokenRef.current) return;
1371
- micConfirmedRef.current = access.ok && access.confirmed;
1372
1454
  if (!access.ok) {
1455
+ wantsListeningRef.current = false;
1373
1456
  setListening(false);
1374
1457
  setError(access.message);
1375
1458
  errorRef.current?.(access.message);
1376
1459
  return;
1377
1460
  }
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 = () => {
1461
+ micConfirmedRef.current = access.confirmed;
1462
+ if (access.primed) {
1463
+ await sleep(RELEASE_SETTLE_MS);
1389
1464
  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 = micConfirmedRef.current && (event.error === "not-allowed" || event.error === "service-not-allowed") ? "Dictation isn't available in this browser. You can type instead." : 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
1465
  }
1425
- },
1426
- [lang]
1427
- );
1428
- beginRecognitionRef.current = beginRecognitionImpl;
1466
+ openSessionRef.current?.(token);
1467
+ });
1468
+ }, []);
1429
1469
  const toggle = useCallback(() => {
1430
1470
  if (listening) stop();
1431
1471
  else start();
1432
1472
  }, [listening, start, stop]);
1433
1473
  useEffect(
1434
1474
  () => () => {
1475
+ startTokenRef.current += 1;
1476
+ wantsListeningRef.current = false;
1435
1477
  const recognition = recognitionRef.current;
1436
1478
  if (!recognition) return;
1437
1479
  recognition.onresult = null;