@burdenoff/website-sdk 2026.1105.2 → 2026.1105.3

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.
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkXIZE37IB_js = require('../chunk-XIZE37IB.js');
3
+ var chunk3AIAJ2L4_js = require('../chunk-3AIAJ2L4.js');
4
4
  var chunkN7E6Z2GF_js = require('../chunk-N7E6Z2GF.js');
5
5
  var chunkC6OJIHNO_js = require('../chunk-C6OJIHNO.js');
6
6
  require('../chunk-ZSTFX7FV.js');
@@ -71,6 +71,17 @@ function readSttOffer(value) {
71
71
  }
72
72
  return { token, expiresAt };
73
73
  }
74
+ var MIC_MAX_PRESS_SECONDS = 30;
75
+ var UNREADABLE_MIC = { mode: "unreadable" };
76
+ function readMicOffer(value) {
77
+ if (!isRecord(value)) return UNREADABLE_MIC;
78
+ const keys = Object.keys(value).sort();
79
+ const { mode, maxPressSeconds } = value;
80
+ if (keys.length !== 2 || keys[0] !== "maxPressSeconds" || keys[1] !== "mode" || mode !== "browser" || maxPressSeconds !== MIC_MAX_PRESS_SECONDS) {
81
+ return UNREADABLE_MIC;
82
+ }
83
+ return { mode: "browser", maxPressSeconds: MIC_MAX_PRESS_SECONDS };
84
+ }
74
85
  function readAvailability(status, body) {
75
86
  if (status !== 200 || !isRecord(body)) return INERT;
76
87
  const { available, reason } = body;
@@ -81,10 +92,11 @@ function readAvailability(status, body) {
81
92
  const { consentVersion, maxSessionSeconds } = body;
82
93
  const botCheck = readBotCheck(body.botCheck);
83
94
  const questions = readQuestions(body.questions);
84
- if (available !== true || typeof consentVersion !== "string" || !chunkXIZE37IB_js.isAvatarVersion(consentVersion) || typeof maxSessionSeconds !== "number" || !Number.isInteger(maxSessionSeconds) || maxSessionSeconds < 1 || botCheck === null || questions === null) {
95
+ if (available !== true || typeof consentVersion !== "string" || !chunk3AIAJ2L4_js.isAvatarVersion(consentVersion) || typeof maxSessionSeconds !== "number" || !Number.isInteger(maxSessionSeconds) || maxSessionSeconds < 1 || botCheck === null || questions === null) {
85
96
  return INERT;
86
97
  }
87
98
  const stt = readSttOffer(body.stt);
99
+ const mic = Object.prototype.hasOwnProperty.call(body, "mic") ? readMicOffer(body.mic) : void 0;
88
100
  return {
89
101
  kind: "available",
90
102
  offer: {
@@ -92,7 +104,8 @@ function readAvailability(status, body) {
92
104
  maxSessionSeconds,
93
105
  botCheck,
94
106
  questions,
95
- ...stt === void 0 ? {} : { stt }
107
+ ...stt === void 0 ? {} : { stt },
108
+ ...mic === void 0 ? {} : { mic }
96
109
  }
97
110
  };
98
111
  }
@@ -205,7 +218,7 @@ async function requestAvailability(target) {
205
218
  }
206
219
  }
207
220
  async function requestAvailabilityAnswer(target) {
208
- if (!chunkXIZE37IB_js.isAvatarSite(target.site) || !LOCALE.test(target.locale)) return INERT;
221
+ if (!chunk3AIAJ2L4_js.isAvatarSite(target.site) || !LOCALE.test(target.locale)) return INERT;
209
222
  const query = new URLSearchParams({
210
223
  site: target.site,
211
224
  locale: target.locale
@@ -847,6 +860,7 @@ var STRINGS = {
847
860
  function avatarLiveStrings(locale) {
848
861
  return STRINGS[locale] ?? EN;
849
862
  }
863
+ var BROWSER_MIC_NOTICE_VERSION = "web-mic-2026-11-21-c";
850
864
  var CONSENT = /* @__PURE__ */ new Map([
851
865
  [
852
866
  "web-consent-2026-11",
@@ -1024,10 +1038,55 @@ var CONSENT = /* @__PURE__ */ new Map([
1024
1038
  followUps: true
1025
1039
  }
1026
1040
  }
1041
+ ],
1042
+ [
1043
+ // Stage C (BOFF-9797, the operator's decision of 2026-11-05): the mic through the browser's own speech recognition.
1044
+ // A DRAFT for the operator's privacy review that no environment uses yet (global-speech-svc reserves the same two
1045
+ // versions, and pairs them with its AVATAR_WEB_MIC=browser): web-consent-2026-11-19-d with three changes only. The
1046
+ // intro warns against personal details said into the microphone and says a tap on it stops the answer being read;
1047
+ // the processing says which speech service turns the voice into text (the browser's own, under its own terms, with
1048
+ // the vendors as examples) and that a live session is told of a tap and of a recording, never of the words; the
1049
+ // storage says the camera is not used and that the voice never reaches us, nor the text until Send, in place of
1050
+ // "Your microphone and camera are not used.", which is true only with no mic. The notice files of record and their
1051
+ // digests are a gitops change of their own, made with the review.
1052
+ "web-consent-2026-11-21-c",
1053
+ {
1054
+ en: {
1055
+ title: "Live AI avatar (beta)",
1056
+ introProduct: null,
1057
+ intro: "This is an AI avatar, not a real person. It reads aloud the answers to the questions you ask in the Avatar tab, provided each passes the speech checks. After an answer, it waits about thirty seconds for your next question, then ends. A session lasts at most three minutes; if an answer doesn't fit in the time left, it reads only the first part and says so, or doesn't read it, and the full answer stays in Chat. The answers are AI-generated and can be wrong. Don't put personal details in a question you type. Don't say them into the microphone either. Tapping the microphone while the avatar speaks stops that answer.",
1058
+ processing: "When you start, Cloudflare checks for bots. The answer text goes to Google to create the voice. Our server sends that voice through LiveKit to Protoface, which makes the avatar video streamed through LiveKit. Your browser connects to LiveKit, which sees your IP address. If you use the microphone, the speech service your browser uses turns your voice into text and may receive the audio: for example Google for Chrome on a computer or Android phone, Apple for Safari and on iPhone and iPad, and Microsoft for Edge on a computer, each under its own privacy terms. During a live session, our server is told when you tap the microphone and while you record (never what you say), so the avatar stops and waits.",
1059
+ storage: "Your camera is not used. We never receive your voice: the text appears in the question box for you to check, and none of what you said reaches us until you press Send. We do not record the live session or store the generated voice. We keep a record of this consent. The privacy notice explains our safeguards, storage periods and the providers' processing. For people aged eighteen and over.",
1060
+ privacyLink: "How we handle this",
1061
+ botCheck: "turnstile",
1062
+ questions: "any",
1063
+ followUps: true,
1064
+ micNotice: BROWSER_MIC_NOTICE_VERSION
1065
+ }
1066
+ }
1067
+ ],
1068
+ [
1069
+ // The words of web-consent-2026-11-21-c without "When you start, Cloudflare checks for bots.", for a server that runs
1070
+ // no bot check. A DRAFT, like it.
1071
+ "web-consent-2026-11-22-c",
1072
+ {
1073
+ en: {
1074
+ title: "Live AI avatar (beta)",
1075
+ introProduct: null,
1076
+ intro: "This is an AI avatar, not a real person. It reads aloud the answers to the questions you ask in the Avatar tab, provided each passes the speech checks. After an answer, it waits about thirty seconds for your next question, then ends. A session lasts at most three minutes; if an answer doesn't fit in the time left, it reads only the first part and says so, or doesn't read it, and the full answer stays in Chat. The answers are AI-generated and can be wrong. Don't put personal details in a question you type. Don't say them into the microphone either. Tapping the microphone while the avatar speaks stops that answer.",
1077
+ processing: "The answer text goes to Google to create the voice. Our server sends that voice through LiveKit to Protoface, which makes the avatar video streamed through LiveKit. Your browser connects to LiveKit, which sees your IP address. If you use the microphone, the speech service your browser uses turns your voice into text and may receive the audio: for example Google for Chrome on a computer or Android phone, Apple for Safari and on iPhone and iPad, and Microsoft for Edge on a computer, each under its own privacy terms. During a live session, our server is told when you tap the microphone and while you record (never what you say), so the avatar stops and waits.",
1078
+ storage: "Your camera is not used. We never receive your voice: the text appears in the question box for you to check, and none of what you said reaches us until you press Send. We do not record the live session or store the generated voice. We keep a record of this consent. The privacy notice explains our safeguards, storage periods and the providers' processing. For people aged eighteen and over.",
1079
+ privacyLink: "How we handle this",
1080
+ botCheck: "off",
1081
+ questions: "any",
1082
+ followUps: true,
1083
+ micNotice: BROWSER_MIC_NOTICE_VERSION
1084
+ }
1085
+ }
1027
1086
  ]
1028
1087
  ]);
1029
1088
  function avatarConsentCopy(version, locale) {
1030
- if (!chunkXIZE37IB_js.isAvatarLiveLocaleReviewed(locale)) return null;
1089
+ if (!chunk3AIAJ2L4_js.isAvatarLiveLocaleReviewed(locale)) return null;
1031
1090
  return CONSENT.get(version)?.[locale] ?? null;
1032
1091
  }
1033
1092
  var MIC_EN = {
@@ -1051,7 +1110,7 @@ var MIC_EN = {
1051
1110
  };
1052
1111
  var MIC_STRINGS = { en: MIC_EN };
1053
1112
  function avatarMicStrings(locale) {
1054
- if (!chunkXIZE37IB_js.isAvatarLiveLocaleReviewed(locale)) return null;
1113
+ if (!chunk3AIAJ2L4_js.isAvatarLiveLocaleReviewed(locale)) return null;
1055
1114
  return MIC_STRINGS[locale] ?? null;
1056
1115
  }
1057
1116
  var AVATAR_MIC_NOTICE_VERSION = "web-mic-2026-11-05";
@@ -1068,20 +1127,38 @@ var MIC_NOTICE = /* @__PURE__ */ new Map([
1068
1127
  privacyLink: "How we handle this"
1069
1128
  }
1070
1129
  }
1130
+ ],
1131
+ [
1132
+ // The browser's own speech recognition (Stage C): the browser's speech service, named by example where each is
1133
+ // true, under its own terms; nothing of the voice reaches us, nor the text before Send; a live session told only of
1134
+ // a tap and of a recording. No daily limits: nothing is counted on the server.
1135
+ BROWSER_MIC_NOTICE_VERSION,
1136
+ {
1137
+ en: {
1138
+ title: "Ask with your voice",
1139
+ processing: "The speech service your browser uses turns your voice into text and may receive the audio: for example Google for Chrome on a computer or Android phone, Apple for Safari and on iPhone and iPad, and Microsoft for Edge on a computer. Its own privacy terms apply.",
1140
+ storage: "We never receive your voice, and your camera is not used.",
1141
+ text: "The text appears in the box for you to check. None of what you said reaches us until you press Send; once you send it, it is a chat message, kept like any other. During a live session, we are told when you tap the microphone and while you record (never what you say), so the avatar stops and waits.",
1142
+ limits: "Each recording stops after thirty seconds at the latest.",
1143
+ privacyLink: "How we handle this"
1144
+ }
1145
+ }
1071
1146
  ]
1072
1147
  ]);
1073
1148
  function avatarMicNotice(version, locale) {
1074
- if (!chunkXIZE37IB_js.isAvatarLiveLocaleReviewed(locale)) return null;
1149
+ if (!chunk3AIAJ2L4_js.isAvatarLiveLocaleReviewed(locale)) return null;
1075
1150
  return MIC_NOTICE.get(version)?.[locale] ?? null;
1076
1151
  }
1077
1152
  var PRIVACY_HREF = "/privacy#live-avatar";
1078
1153
  var LIMIT_SECONDS = 30;
1079
1154
  var TICK_MS = 250;
1080
1155
  var IDLE = { name: "idle" };
1081
- var FOR_GOOD = /* @__PURE__ */ new Set([
1082
- "unavailable",
1083
- "limit"
1084
- ]);
1156
+ var EN_IN = { lang: "en-IN", fallbackLang: "en-US" };
1157
+ var BROWSER_MIC_LANGUAGES = { en: EN_IN };
1158
+ var FOR_GOOD = {
1159
+ stream: /* @__PURE__ */ new Set(["unavailable", "limit"]),
1160
+ browser: /* @__PURE__ */ new Set(["unsupported"])
1161
+ };
1085
1162
  var FAILURE_TEXT = {
1086
1163
  denied: "micDenied",
1087
1164
  unsupported: "micUnsupported",
@@ -1090,14 +1167,32 @@ var FAILURE_TEXT = {
1090
1167
  busy: "micBusy",
1091
1168
  failed: "micFailed"
1092
1169
  };
1170
+ var loadedMic = null;
1093
1171
  function loadAvatarMic() {
1094
- return import('../avatar-mic/index.js').then((loaded) => ({
1095
- createAvatarMic: loaded.createAvatarMic
1096
- }));
1172
+ return import('../avatar-mic/index.js').then((loaded) => {
1173
+ const module = {
1174
+ createAvatarMic: loaded.createAvatarMic,
1175
+ createBrowserAvatarMic: loaded.createBrowserAvatarMic
1176
+ };
1177
+ loadedMic = module;
1178
+ return module;
1179
+ });
1097
1180
  }
1098
1181
  function canUseAvatarMic() {
1099
1182
  return typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getUserMedia === "function" && typeof AudioContext !== "undefined" && typeof AudioWorkletNode !== "undefined" && typeof WebSocket !== "undefined";
1100
1183
  }
1184
+ function canUseBrowserMic() {
1185
+ if (typeof window === "undefined" || typeof navigator === "undefined") {
1186
+ return false;
1187
+ }
1188
+ if (window.isSecureContext !== true) return false;
1189
+ const recognizer = Reflect.get(window, "SpeechRecognition") ?? Reflect.get(window, "webkitSpeechRecognition");
1190
+ if (typeof recognizer !== "function") return false;
1191
+ const agent = navigator.userAgent ?? "";
1192
+ if (/\bFirefox\//.test(agent) || /\bFxiOS\//.test(agent)) return false;
1193
+ return !("brave" in navigator);
1194
+ }
1195
+ var REPLAY_PRIVATE = "rr-block rr-mask";
1101
1196
  function clock(seconds) {
1102
1197
  const whole = Math.max(0, Math.floor(seconds));
1103
1198
  return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, "0")}`;
@@ -1108,37 +1203,51 @@ function withHeardText(current, heard, maxLength) {
1108
1203
  const joined = current.trim() === "" ? text : `${current.replace(/\s+$/, "")} ${text}`;
1109
1204
  return joined.slice(0, Math.max(0, maxLength));
1110
1205
  }
1206
+ function noticeOf(source) {
1207
+ return source.kind === "stream" ? AVATAR_MIC_NOTICE_VERSION : source.notice;
1208
+ }
1209
+ function micKeyOf(source, locale) {
1210
+ return source.kind === "stream" ? `stream ${source.base}` : `browser ${BROWSER_MIC_LANGUAGES[locale]?.lang ?? ""}`;
1211
+ }
1111
1212
  function useAvatarMic({
1112
1213
  enabled,
1113
1214
  locale,
1114
- base,
1115
- offer,
1116
- refreshOffer,
1215
+ source,
1117
1216
  onText,
1118
1217
  onStart
1119
1218
  }) {
1120
1219
  const S = avatarMicStrings(locale);
1121
- const copy = avatarMicNotice(AVATAR_MIC_NOTICE_VERSION, locale);
1220
+ const noticeVersion = source === null ? null : noticeOf(source);
1221
+ const copy = noticeVersion === null ? null : avatarMicNotice(noticeVersion, locale);
1222
+ const kind = source?.kind ?? null;
1223
+ const micKey = source === null ? null : micKeyOf(source, locale);
1122
1224
  const uid = react.useId();
1123
1225
  const noticeTitleId = `${uid}-mic-notice`;
1124
- const [withdrawn, setWithdrawn] = react.useState(null);
1125
- const [supported, setSupported] = react.useState(false);
1126
- react.useEffect(() => setSupported(canUseAvatarMic()), []);
1127
- const possible = enabled && supported && base !== null && S !== null && copy !== null;
1128
- const offered = possible && withdrawn === null;
1226
+ const [withdrawn, setWithdrawn] = react.useState({});
1227
+ const [supported, setSupported] = react.useState({ stream: false, browser: false });
1228
+ react.useEffect(
1229
+ () => setSupported({ stream: canUseAvatarMic(), browser: canUseBrowserMic() }),
1230
+ []
1231
+ );
1232
+ const usable = source === null ? false : source.kind === "stream" ? supported.stream : supported.browser && BROWSER_MIC_LANGUAGES[locale] !== void 0;
1233
+ const possible = enabled && kind !== null && usable && S !== null && copy !== null;
1234
+ const withdrawnReason = kind === null ? null : withdrawn[kind] ?? null;
1235
+ const offered = possible && withdrawnReason === null;
1129
1236
  const [state, setState] = react.useState(IDLE);
1130
1237
  const [noticeOpen, setNoticeOpen] = react.useState(false);
1131
- const agreedRef = react.useRef(false);
1238
+ const agreedRef = react.useRef(null);
1132
1239
  const [loading2, setLoading] = react.useState(false);
1133
1240
  const [loadFailed, setLoadFailed] = react.useState(false);
1134
1241
  const [now, setNow] = react.useState(() => Date.now());
1135
- const micRef = react.useRef(null);
1242
+ const micRef = react.useRef(
1243
+ null
1244
+ );
1136
1245
  const seqRef = react.useRef(0);
1137
1246
  const buttonRef = react.useRef(null);
1138
1247
  const allowRef = react.useRef(null);
1139
- const usedEpochRef = react.useRef(0);
1140
- const latest = react.useRef({ offer, refreshOffer, onText, onStart, base });
1141
- latest.current = { offer, refreshOffer, onText, onStart, base };
1248
+ const usedRef = react.useRef(null);
1249
+ const latest = react.useRef({ source, locale, noticeVersion, onText, onStart });
1250
+ latest.current = { source, locale, noticeVersion, onText, onStart };
1142
1251
  const cancel = react.useCallback(() => {
1143
1252
  seqRef.current += 1;
1144
1253
  setLoading(false);
@@ -1149,6 +1258,12 @@ function useAvatarMic({
1149
1258
  react.useEffect(() => {
1150
1259
  if (!offered) cancel();
1151
1260
  }, [offered, cancel]);
1261
+ const keyRef = react.useRef(micKey);
1262
+ react.useEffect(() => {
1263
+ if (keyRef.current === micKey) return;
1264
+ keyRef.current = micKey;
1265
+ cancel();
1266
+ }, [micKey, cancel]);
1152
1267
  react.useEffect(
1153
1268
  () => () => {
1154
1269
  seqRef.current += 1;
@@ -1156,29 +1271,55 @@ function useAvatarMic({
1156
1271
  },
1157
1272
  []
1158
1273
  );
1159
- const micFor = react.useCallback(async (target) => {
1160
- const held = micRef.current;
1161
- if (held !== null && held.base === target) return held.mic;
1162
- held?.mic.cancel();
1163
- const loaded = await loadAvatarMic();
1164
- const mic = loaded.createAvatarMic({
1165
- base: target,
1166
- notice: AVATAR_MIC_NOTICE_VERSION,
1167
- offer: () => latest.current.offer,
1168
- refreshOffer: () => latest.current.refreshOffer(),
1169
- onState: setState
1170
- });
1171
- micRef.current = { base: target, mic };
1172
- return mic;
1173
- }, []);
1274
+ const micOf = react.useCallback(
1275
+ (loaded, wanted) => {
1276
+ const key = micKeyOf(wanted, latest.current.locale);
1277
+ const held = micRef.current;
1278
+ if (held !== null && held.key === key) return held.mic;
1279
+ held?.mic.cancel();
1280
+ let mic;
1281
+ if (wanted.kind === "stream") {
1282
+ const stream = () => {
1283
+ const current = latest.current.source;
1284
+ return current?.kind === "stream" ? current : null;
1285
+ };
1286
+ mic = loaded.createAvatarMic({
1287
+ base: wanted.base,
1288
+ notice: AVATAR_MIC_NOTICE_VERSION,
1289
+ offer: () => stream()?.offer ?? null,
1290
+ refreshOffer: () => stream()?.refreshOffer() ?? Promise.resolve(null),
1291
+ onState: setState
1292
+ });
1293
+ } else {
1294
+ const language = BROWSER_MIC_LANGUAGES[latest.current.locale] ?? EN_IN;
1295
+ mic = loaded.createBrowserAvatarMic({
1296
+ lang: language.lang,
1297
+ fallbackLang: language.fallbackLang,
1298
+ maxPressMs: LIMIT_SECONDS * 1e3,
1299
+ onState: setState
1300
+ });
1301
+ }
1302
+ micRef.current = { key, kind: wanted.kind, mic };
1303
+ return mic;
1304
+ },
1305
+ []
1306
+ );
1174
1307
  const press = react.useCallback(() => {
1175
- const target = latest.current.base;
1176
- if (target === null) return;
1308
+ const wanted = latest.current.source;
1309
+ if (wanted === null) return;
1177
1310
  seqRef.current += 1;
1178
1311
  const seq = seqRef.current;
1179
1312
  setLoadFailed(false);
1313
+ const held = micRef.current;
1314
+ const ready = wanted.kind !== "browser" ? null : held !== null && held.key === micKeyOf(wanted, latest.current.locale) ? held.mic : loadedMic !== null ? micOf(loadedMic, wanted) : null;
1315
+ if (ready !== null) {
1316
+ setLoading(false);
1317
+ ready.start();
1318
+ return;
1319
+ }
1180
1320
  setLoading(true);
1181
- void micFor(target).then(
1321
+ const arriving = held !== null && held.key === micKeyOf(wanted, latest.current.locale) ? Promise.resolve(held.mic) : loadAvatarMic().then((loaded) => micOf(loaded, wanted));
1322
+ void arriving.then(
1182
1323
  (mic) => {
1183
1324
  if (seqRef.current !== seq) return;
1184
1325
  setLoading(false);
@@ -1190,8 +1331,9 @@ function useAvatarMic({
1190
1331
  setLoadFailed(true);
1191
1332
  }
1192
1333
  );
1193
- }, [micFor]);
1334
+ }, [micOf]);
1194
1335
  const recording = loading2 || state.name === "starting" || state.name === "listening";
1336
+ const finishing = kind === "browser" && state.name === "finishing";
1195
1337
  const tap = react.useCallback(() => {
1196
1338
  if (state.name === "listening" || state.name === "starting") {
1197
1339
  micRef.current?.mic.stop();
@@ -1201,16 +1343,26 @@ function useAvatarMic({
1201
1343
  cancel();
1202
1344
  return;
1203
1345
  }
1346
+ if (finishing) return;
1204
1347
  latest.current.onStart?.();
1205
- if (!agreedRef.current) {
1348
+ if (agreedRef.current !== latest.current.noticeVersion) {
1206
1349
  setNoticeOpen(true);
1207
- void loadAvatarMic().catch(() => void 0);
1350
+ const seq = seqRef.current;
1351
+ void loadAvatarMic().then(
1352
+ (loaded) => {
1353
+ const wanted = latest.current.source;
1354
+ if (seqRef.current === seq && wanted?.kind === "browser") {
1355
+ micOf(loaded, wanted);
1356
+ }
1357
+ },
1358
+ () => void 0
1359
+ );
1208
1360
  return;
1209
1361
  }
1210
1362
  press();
1211
- }, [cancel, loading2, press, state.name]);
1363
+ }, [cancel, finishing, loading2, micOf, press, state.name]);
1212
1364
  const allow = react.useCallback(() => {
1213
- agreedRef.current = true;
1365
+ agreedRef.current = latest.current.noticeVersion;
1214
1366
  setNoticeOpen(false);
1215
1367
  buttonRef.current?.focus();
1216
1368
  press();
@@ -1230,11 +1382,15 @@ function useAvatarMic({
1230
1382
  return () => clearInterval(timer);
1231
1383
  }, [listeningSince]);
1232
1384
  react.useEffect(() => {
1233
- if (state.name === "done" && state.epoch !== usedEpochRef.current) {
1234
- usedEpochRef.current = state.epoch;
1385
+ if (state.name === "done" && state !== usedRef.current) {
1386
+ usedRef.current = state;
1235
1387
  if (state.text !== "") latest.current.onText(state.text);
1236
- } else if (state.name === "failed" && FOR_GOOD.has(state.reason)) {
1237
- setWithdrawn(state.reason);
1388
+ return;
1389
+ }
1390
+ const pressed = micRef.current?.kind;
1391
+ if (state.name === "failed" && pressed !== void 0 && FOR_GOOD[pressed].has(state.reason)) {
1392
+ const reason = state.reason;
1393
+ setWithdrawn((held) => ({ ...held, [pressed]: reason }));
1238
1394
  }
1239
1395
  }, [state]);
1240
1396
  if (!possible || S === null || copy === null) {
@@ -1246,7 +1402,7 @@ function useAvatarMic({
1246
1402
  recording: false
1247
1403
  };
1248
1404
  }
1249
- const status = withdrawn !== null ? S[FAILURE_TEXT[withdrawn]] : loading2 ? S.micStarting : loadFailed ? S.micFailed : state.name === "starting" ? S.micStarting : state.name === "listening" ? S.micListening : state.name === "finishing" ? S.micFinishing : state.name === "done" ? state.text === "" ? S.micNoSpeech : state.limit ? S.micDoneLimit : S.micDone : state.name === "failed" ? S[FAILURE_TEXT[state.reason]] : "";
1405
+ const status = withdrawnReason !== null ? S[FAILURE_TEXT[withdrawnReason]] : loading2 ? S.micStarting : loadFailed ? S.micFailed : state.name === "starting" ? S.micStarting : state.name === "listening" ? S.micListening : state.name === "finishing" ? S.micFinishing : state.name === "done" ? state.text === "" ? S.micNoSpeech : state.limit ? S.micDoneLimit : S.micDone : state.name === "failed" ? S[FAILURE_TEXT[state.reason]] : "";
1250
1406
  const heard = offered && (state.name === "listening" || state.name === "finishing") ? state.text : "";
1251
1407
  const button = !offered ? null : /* @__PURE__ */ jsxRuntime.jsx(
1252
1408
  "button",
@@ -1255,12 +1411,15 @@ function useAvatarMic({
1255
1411
  type: "button",
1256
1412
  "aria-label": recording ? S.micStop : S.micStart,
1257
1413
  "aria-pressed": recording,
1414
+ "aria-disabled": finishing ? true : void 0,
1258
1415
  title: recording ? S.micStop : S.micStart,
1259
1416
  "data-boff-explore": "avatar-live-mic",
1260
1417
  "data-mic": loading2 ? "starting" : state.name,
1418
+ "data-mic-source": kind ?? void 0,
1261
1419
  className: chunkC6OJIHNO_js.cn(
1262
1420
  "inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background motion-reduce:transition-none",
1263
- recording ? "border-primary bg-primary text-primary-foreground" : "border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground"
1421
+ recording ? "border-primary bg-primary text-primary-foreground" : "border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
1422
+ finishing && "cursor-wait opacity-50"
1264
1423
  ),
1265
1424
  onClick: tap,
1266
1425
  children: recording ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Square, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Mic, { "aria-hidden": "true", className: "h-4 w-4" })
@@ -1343,7 +1502,11 @@ function useAvatarMic({
1343
1502
  {
1344
1503
  "data-boff-explore": "avatar-live-mic-heard",
1345
1504
  dir: "auto",
1346
- className: "line-clamp-3 break-words text-sm italic text-muted-foreground",
1505
+ className: chunkC6OJIHNO_js.cn(
1506
+ // rrweb's default block and mask classes: a site's session replay records none of what is heard.
1507
+ REPLAY_PRIVATE,
1508
+ "line-clamp-3 break-words text-sm italic text-muted-foreground"
1509
+ ),
1347
1510
  children: heard
1348
1511
  }
1349
1512
  )
@@ -1480,6 +1643,7 @@ var PLAYOUT_TAIL_MS = 5e3;
1480
1643
  var QUIET_PLAYED_OUT_MS = 2500;
1481
1644
  var OWED_VOICE_WAIT_MS = 15e3;
1482
1645
  var LISTENING_MARK_MS = 1e4;
1646
+ var LISTENING_MARK_SPACING_MS = 5e3;
1483
1647
  var BUSY_RETRY_MS = 2e3;
1484
1648
  var BUSY_RETRY_MAX_MS = 8e3;
1485
1649
  var MAX_BUSY_RETRIES = 24;
@@ -1605,8 +1769,15 @@ function acceptsQuestion(slot) {
1605
1769
  function followUpBox(phase, run, token) {
1606
1770
  return phase.name === "live" && run !== null && phase.session.followUp !== null && !phase.session.followUp.closed && token === run.token;
1607
1771
  }
1608
- function micOfferOf(result) {
1609
- return result.kind === "available" && result.offer.stt !== void 0 ? { ...result.offer.stt, receivedAt: Date.now() } : null;
1772
+ var NO_MIC = { kind: "none" };
1773
+ function micChoiceOf(result, locale) {
1774
+ if (result.kind !== "available") return NO_MIC;
1775
+ const { offer } = result;
1776
+ const micNotice = avatarConsentCopy(offer.consentVersion, locale)?.micNotice;
1777
+ if (offer.mic !== void 0) {
1778
+ return offer.mic.mode === "browser" && micNotice !== void 0 ? { kind: "browser", notice: micNotice } : NO_MIC;
1779
+ }
1780
+ return offer.stt !== void 0 && micNotice === void 0 ? { kind: "stream", offer: { ...offer.stt, receivedAt: Date.now() } } : NO_MIC;
1610
1781
  }
1611
1782
  function utcToday() {
1612
1783
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -1756,7 +1927,7 @@ function AvatarLiveView({
1756
1927
  const waitRef = react.useRef(null);
1757
1928
  const sendingRef = react.useRef(false);
1758
1929
  const textBoxRef = react.useRef(false);
1759
- const [micOffer, setMicOffer] = react.useState(null);
1930
+ const [micChoice, setMicChoice] = react.useState(NO_MIC);
1760
1931
  const micCancelRef = react.useRef(() => void 0);
1761
1932
  const sendDueRef = react.useRef(() => void 0);
1762
1933
  const checkRef = react.useRef(null);
@@ -2060,7 +2231,7 @@ function AvatarLiveView({
2060
2231
  const applyAvailability = react.useCallback(
2061
2232
  (result, locale2) => {
2062
2233
  const { live: live2 } = latest.current;
2063
- setMicOffer(micOfferOf(result));
2234
+ setMicChoice(micChoiceOf(result, locale2));
2064
2235
  if (result.kind === "inert") {
2065
2236
  setMode("suggested");
2066
2237
  go(INERT2);
@@ -2078,7 +2249,7 @@ function AvatarLiveView({
2078
2249
  go(INERT2);
2079
2250
  return;
2080
2251
  }
2081
- if (result.offer.botCheck === "turnstile" && chunkXIZE37IB_js.turnstileSiteKeyOf(live2) === null) {
2252
+ if (result.offer.botCheck === "turnstile" && chunk3AIAJ2L4_js.turnstileSiteKeyOf(live2) === null) {
2082
2253
  setMode("suggested");
2083
2254
  go(INERT2);
2084
2255
  return;
@@ -2132,7 +2303,8 @@ function AvatarLiveView({
2132
2303
  due: null,
2133
2304
  retry: null,
2134
2305
  sent: /* @__PURE__ */ new Set([context.messageId]),
2135
- seq: 0
2306
+ seq: 0,
2307
+ listenedAt: null
2136
2308
  } : null;
2137
2309
  sessionRef.current = {
2138
2310
  session,
@@ -2238,7 +2410,7 @@ function AvatarLiveView({
2238
2410
  return;
2239
2411
  }
2240
2412
  const refusedFrom = exchangeAtStart.offerUntil - ANSWER_OFFER_MS + SERVICE_MAX_AGE_MS;
2241
- const siteKey = offer.botCheck === "turnstile" ? chunkXIZE37IB_js.turnstileSiteKeyOf(live2) : null;
2413
+ const siteKey = offer.botCheck === "turnstile" ? chunk3AIAJ2L4_js.turnstileSiteKeyOf(live2) : null;
2242
2414
  if (offer.botCheck === "turnstile" && (siteKey === null || !canBindBotCheck())) {
2243
2415
  go(INERT2);
2244
2416
  return;
@@ -2767,16 +2939,18 @@ function AvatarLiveView({
2767
2939
  const refreshMicOffer = react.useCallback(async () => {
2768
2940
  const { live: live2, locale: locale2, base: base2, headers: headers2 } = latest.current;
2769
2941
  if (base2 === null) return null;
2770
- const next = micOfferOf(
2942
+ const next = micChoiceOf(
2771
2943
  await requestAvailabilityAnswer({
2772
2944
  base: base2,
2773
2945
  headers: headers2,
2774
2946
  site: live2.site,
2775
2947
  locale: locale2
2776
- })
2948
+ }),
2949
+ locale2
2777
2950
  );
2778
- if (next !== null) setMicOffer(next);
2779
- return next;
2951
+ if (next.kind !== "stream") return null;
2952
+ setMicChoice(next);
2953
+ return next.offer;
2780
2954
  }, []);
2781
2955
  const onMicText = react.useCallback(
2782
2956
  (text) => {
@@ -2802,8 +2976,14 @@ function AvatarLiveView({
2802
2976
  handle.playedOut = true;
2803
2977
  showFollowUp(handle, { speaking: false });
2804
2978
  }, [showFollowUp, tell]);
2979
+ const micSource = micChoice.kind === "browser" ? { kind: "browser", notice: micChoice.notice } : micChoice.kind === "stream" && base !== null ? {
2980
+ kind: "stream",
2981
+ base,
2982
+ offer: micChoice.offer,
2983
+ refreshOffer: refreshMicOffer
2984
+ } : null;
2805
2985
  const mic = useAvatarMic({
2806
- enabled: live.mic === true && micOffer !== null && !off && acceptsQuestion({
2986
+ enabled: live.mic === true && micSource !== null && !off && acceptsQuestion({
2807
2987
  any,
2808
2988
  phase: phase.name,
2809
2989
  kind,
@@ -2812,9 +2992,7 @@ function AvatarLiveView({
2812
2992
  followUp: followUpAsks
2813
2993
  }),
2814
2994
  locale,
2815
- base,
2816
- offer: micOffer,
2817
- refreshOffer: refreshMicOffer,
2995
+ source: micSource,
2818
2996
  onText: onMicText,
2819
2997
  onStart: onMicStart
2820
2998
  });
@@ -2825,15 +3003,23 @@ function AvatarLiveView({
2825
3003
  if (!micRecording || handle === null || handle.followUps === null || handle.done) {
2826
3004
  return;
2827
3005
  }
2828
- tell(handle, "listening");
3006
+ const run = handle.followUps;
2829
3007
  const every = Math.min(
2830
3008
  LISTENING_MARK_MS,
2831
- Math.max(1e3, handle.followUps.limits.idleSeconds * 1e3 / 3)
3009
+ Math.max(1e3, run.limits.idleSeconds * 1e3 / 3)
2832
3010
  );
2833
- const timer = setInterval(() => tell(handle, "listening"), every);
3011
+ const spacing = Math.min(LISTENING_MARK_SPACING_MS, every / 2);
3012
+ const mark = () => {
3013
+ const at = Date.now();
3014
+ if (run.listenedAt !== null && at - run.listenedAt < spacing) return;
3015
+ run.listenedAt = at;
3016
+ tell(handle, "listening");
3017
+ };
3018
+ mark();
3019
+ const timer = setInterval(mark, every);
2834
3020
  return () => {
2835
3021
  clearInterval(timer);
2836
- if (sessionRef.current === handle) tell(handle, "listening");
3022
+ if (sessionRef.current === handle) mark();
2837
3023
  };
2838
3024
  }, [micRecording, tell]);
2839
3025
  const notNow = react.useCallback(() => {
@@ -3705,7 +3891,12 @@ function AskBox({
3705
3891
  dir: "auto",
3706
3892
  "data-avatar-primary": touchFirst ? void 0 : "",
3707
3893
  "data-boff-explore": "avatar-live-ask-input",
3708
- className: "max-h-32 min-h-11 flex-1 resize-none rounded-md border border-border bg-background px-3 py-2.5 text-base text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:text-sm",
3894
+ className: chunkC6OJIHNO_js.cn(
3895
+ // Kept out of a site's session replay before Send (rrweb's default classes; `rr-ignore` for its input
3896
+ // events): the text may be what the mic heard, or what the visitor typed.
3897
+ REPLAY_PRIVATE,
3898
+ "rr-ignore max-h-32 min-h-11 flex-1 resize-none rounded-md border border-border bg-background px-3 py-2.5 text-base text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:text-sm"
3899
+ ),
3709
3900
  onChange: (event) => {
3710
3901
  setRefusedFiles(false);
3711
3902
  onChange(event.target.value);