@burdenoff/website-sdk 2026.828.5 → 2026.828.7

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,11 +1276,40 @@ function getRecognitionCtor() {
1276
1276
  const w = window;
1277
1277
  return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
1278
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, confirmed: false };
1283
+ try {
1284
+ const status = await navigator.permissions?.query({
1285
+ name: "microphone"
1286
+ });
1287
+ if (status?.state === "granted") return { ok: true, confirmed: true };
1288
+ } catch {
1289
+ }
1290
+ try {
1291
+ const stream = await media.getUserMedia({ audio: true });
1292
+ for (const track of stream.getTracks()) track.stop();
1293
+ return { ok: true, confirmed: true };
1294
+ } catch (error) {
1295
+ const name = typeof error === "object" && error !== null && "name" in error ? String(error.name) : "";
1296
+ if (name === "NotAllowedError" || name === "SecurityError") {
1297
+ return { ok: false, message: MIC_DENIED_MESSAGE };
1298
+ }
1299
+ if (name === "NotFoundError" || name === "DevicesNotFoundError") {
1300
+ return { ok: false, message: "No microphone was found." };
1301
+ }
1302
+ return {
1303
+ ok: false,
1304
+ message: "Dictation could not start. You can type instead."
1305
+ };
1306
+ }
1307
+ }
1279
1308
  function describeError(code) {
1280
1309
  switch (code) {
1281
1310
  case "not-allowed":
1282
1311
  case "service-not-allowed":
1283
- return "Microphone access was blocked. Allow it in your browser's site settings to dictate.";
1312
+ return MIC_DENIED_MESSAGE;
1284
1313
  case "no-speech":
1285
1314
  return "I didn't catch anything \u2014 try again a little closer to the mic.";
1286
1315
  case "audio-capture":
@@ -1303,11 +1332,23 @@ function useSpeechInput({
1303
1332
  const [interim, setInterim] = useState("");
1304
1333
  const [error, setError] = useState(null);
1305
1334
  const recognitionRef = useRef(null);
1335
+ const startTokenRef = useRef(0);
1336
+ const micConfirmedRef = useRef(false);
1337
+ const beginRecognitionRef = useRef(null);
1338
+ const beginRecognition = useCallback(
1339
+ (Ctor, token) => {
1340
+ beginRecognitionRef.current?.(Ctor, token);
1341
+ },
1342
+ []
1343
+ );
1306
1344
  const finalRef = useRef(onFinalTranscript);
1307
1345
  const errorRef = useRef(onError);
1308
1346
  finalRef.current = onFinalTranscript;
1309
1347
  errorRef.current = onError;
1310
1348
  const stop = useCallback(() => {
1349
+ startTokenRef.current += 1;
1350
+ setListening(false);
1351
+ setInterim("");
1311
1352
  const recognition = recognitionRef.current;
1312
1353
  if (!recognition) return;
1313
1354
  try {
@@ -1321,48 +1362,70 @@ function useSpeechInput({
1321
1362
  const Ctor = getRecognitionCtor();
1322
1363
  if (!Ctor) return;
1323
1364
  if (recognitionRef.current) stop();
1324
- const recognition = new Ctor();
1325
- recognition.lang = lang ?? (typeof navigator === "undefined" ? "en-US" : navigator.language || "en-US");
1326
- recognition.continuous = true;
1327
- recognition.interimResults = true;
1328
- recognition.maxAlternatives = 1;
1329
- recognition.onstart = () => {
1330
- setError(null);
1331
- setListening(true);
1332
- };
1333
- recognition.onresult = (event) => {
1334
- let settled = "";
1335
- let pending = "";
1336
- for (let i = event.resultIndex; i < event.results.length; i += 1) {
1337
- const result = event.results[i];
1338
- if (!result) continue;
1339
- const text = result[0]?.transcript ?? "";
1340
- if (result.isFinal) settled += text;
1341
- else pending += text;
1365
+ setError(null);
1366
+ setListening(true);
1367
+ startTokenRef.current += 1;
1368
+ const token = startTokenRef.current;
1369
+ void ensureMicrophoneAccess().then((access) => {
1370
+ if (token !== startTokenRef.current) return;
1371
+ micConfirmedRef.current = access.ok && access.confirmed;
1372
+ if (!access.ok) {
1373
+ setListening(false);
1374
+ setError(access.message);
1375
+ errorRef.current?.(access.message);
1376
+ return;
1342
1377
  }
1343
- setInterim(pending);
1344
- if (settled.trim() !== "") finalRef.current(settled);
1345
- };
1346
- recognition.onerror = (event) => {
1347
- const message = describeError(event.error);
1348
- setListening(false);
1349
- setInterim("");
1350
- if (message !== "") {
1351
- setError(message);
1352
- errorRef.current?.(message);
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 = 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);
1353
1424
  }
1354
- };
1355
- recognition.onend = () => {
1356
- setListening(false);
1357
- setInterim("");
1358
- };
1359
- recognitionRef.current = recognition;
1360
- try {
1361
- recognition.start();
1362
- } catch {
1363
- setListening(false);
1364
- }
1365
- }, [lang, stop]);
1425
+ },
1426
+ [lang]
1427
+ );
1428
+ beginRecognitionRef.current = beginRecognitionImpl;
1366
1429
  const toggle = useCallback(() => {
1367
1430
  if (listening) stop();
1368
1431
  else start();