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