@burdenoff/website-sdk 2026.828.6 → 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,23 +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 };
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 };
1290
- if (status?.state === "denied")
1291
- return { ok: false, message: MIC_DENIED_MESSAGE };
1304
+ if (status?.state === "granted")
1305
+ return { ok: true, confirmed: true, primed: false };
1292
1306
  } catch {
1293
1307
  }
1294
1308
  try {
1295
1309
  const stream = await media.getUserMedia({ audio: true });
1296
1310
  for (const track of stream.getTracks()) track.stop();
1297
- return { ok: true };
1311
+ return { ok: true, confirmed: true, primed: true };
1298
1312
  } catch (error) {
1299
1313
  const name = typeof error === "object" && error !== null && "name" in error ? String(error.name) : "";
1300
1314
  if (name === "NotAllowedError" || name === "SecurityError") {
@@ -1337,19 +1351,18 @@ function useSpeechInput({
1337
1351
  const [error, setError] = react.useState(null);
1338
1352
  const recognitionRef = react.useRef(null);
1339
1353
  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
- );
1354
+ const micConfirmedRef = react.useRef(false);
1355
+ const wantsListeningRef = react.useRef(false);
1356
+ const retriedRef = react.useRef(false);
1347
1357
  const finalRef = react.useRef(onFinalTranscript);
1348
1358
  const errorRef = react.useRef(onError);
1349
1359
  finalRef.current = onFinalTranscript;
1350
1360
  errorRef.current = onError;
1361
+ const langRef = react.useRef(lang);
1362
+ langRef.current = lang;
1351
1363
  const stop = react.useCallback(() => {
1352
1364
  startTokenRef.current += 1;
1365
+ wantsListeningRef.current = false;
1353
1366
  setListening(false);
1354
1367
  setInterim("");
1355
1368
  const recognition = recognitionRef.current;
@@ -1358,82 +1371,111 @@ function useSpeechInput({
1358
1371
  recognition.stop();
1359
1372
  } catch {
1360
1373
  }
1361
- setListening(false);
1362
- setInterim("");
1363
1374
  }, []);
1364
- const start = react.useCallback(() => {
1375
+ const openSession = react.useCallback((token) => {
1365
1376
  const Ctor = getRecognitionCtor();
1366
- if (!Ctor) return;
1367
- if (recognitionRef.current) stop();
1368
- setError(null);
1369
- 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;
1370
1447
  startTokenRef.current += 1;
1371
1448
  const token = startTokenRef.current;
1372
- 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) => {
1373
1455
  if (token !== startTokenRef.current) return;
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 = 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;