@burdenoff/website-sdk 2026.1105.3 → 2026.1106.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.
@@ -1,4 +1,4 @@
1
- import { turnstileSiteKeyOf, isAvatarLiveLocaleReviewed, isAvatarSite, isAvatarVersion } from '../chunk-JGU6JW4N.mjs';
1
+ import { turnstileSiteKeyOf, isAvatarLiveLocaleReviewed, isAvatarSite, isAvatarVersion } from '../chunk-BHEHF3Y5.mjs';
2
2
  import { answerDisplayText, fill } from '../chunk-JZS6FQT6.mjs';
3
3
  import { cn } from '../chunk-R7G6TWIS.mjs';
4
4
  import '../chunk-VNS5WDWO.mjs';
@@ -70,15 +70,23 @@ function readSttOffer(value) {
70
70
  return { token, expiresAt };
71
71
  }
72
72
  var MIC_MAX_PRESS_SECONDS = 30;
73
+ var AVATAR_MIC_PAUSE_MIN_MS = 2e3;
74
+ var AVATAR_MIC_PAUSE_MAX_MS = 2500;
73
75
  var UNREADABLE_MIC = { mode: "unreadable" };
74
76
  function readMicOffer(value) {
75
77
  if (!isRecord(value)) return UNREADABLE_MIC;
76
78
  const keys = Object.keys(value).sort();
77
- const { mode, maxPressSeconds } = value;
78
- if (keys.length !== 2 || keys[0] !== "maxPressSeconds" || keys[1] !== "mode" || mode !== "browser" || maxPressSeconds !== MIC_MAX_PRESS_SECONDS) {
79
+ const { mode, maxPressSeconds, pauseMs } = value;
80
+ if (keys[0] !== "maxPressSeconds" || keys[1] !== "mode" || mode !== "browser" || maxPressSeconds !== MIC_MAX_PRESS_SECONDS) {
79
81
  return UNREADABLE_MIC;
80
82
  }
81
- return { mode: "browser", maxPressSeconds: MIC_MAX_PRESS_SECONDS };
83
+ if (keys.length === 2) {
84
+ return { mode: "browser", maxPressSeconds: MIC_MAX_PRESS_SECONDS };
85
+ }
86
+ if (keys.length !== 3 || keys[2] !== "pauseMs" || typeof pauseMs !== "number" || !Number.isSafeInteger(pauseMs) || pauseMs < AVATAR_MIC_PAUSE_MIN_MS || pauseMs > AVATAR_MIC_PAUSE_MAX_MS) {
87
+ return UNREADABLE_MIC;
88
+ }
89
+ return { mode: "browser", maxPressSeconds: MIC_MAX_PRESS_SECONDS, pauseMs };
82
90
  }
83
91
  function readAvailability(status, body) {
84
92
  if (status !== 200 || !isRecord(body)) return INERT;
@@ -108,10 +116,13 @@ function readAvailability(status, body) {
108
116
  };
109
117
  }
110
118
  const meaning = available ? void 0 : AVAILABILITY_REASONS.get(reason);
111
- return meaning === void 0 || meaning === "inert" ? INERT : {
119
+ if (meaning === void 0 || meaning === "inert") return INERT;
120
+ const named = body.consentVersion;
121
+ return {
112
122
  kind: "refused",
113
123
  reason: meaning,
114
- questions: body.questions === "any" ? "any" : "suggested"
124
+ questions: body.questions === "any" ? "any" : "suggested",
125
+ ...typeof named === "string" && isAvatarVersion(named) ? { consentVersion: named } : {}
115
126
  };
116
127
  }
117
128
  var CREATE_REFUSALS = /* @__PURE__ */ new Map([
@@ -1081,6 +1092,27 @@ var CONSENT = /* @__PURE__ */ new Map([
1081
1092
  micNotice: BROWSER_MIC_NOTICE_VERSION
1082
1093
  }
1083
1094
  }
1095
+ ],
1096
+ [
1097
+ // Stage C3 (BOFF-9797, the operator's final decision of 2026-11-06, "nothing at all"): the voice conversation with NO
1098
+ // notice in the Avatar tab. Typed questions, Stage B's three minutes, Stage D's answers after the first, the
1099
+ // browser's own recognizer (AVATAR_WEB_MIC=browser) with the server's pause, and no bot check (alpha and prod both
1100
+ // run it with AVATAR_WEB_BOT_CHECK=off). No words, so nothing to agree to: the server must record no consent for it
1101
+ // and hold no notice digest for it, and the create names it as a protocol token only. Speech processing and the
1102
+ // question sent by itself are the site's privacy policy's to describe. PROD waits for the privacy reviewer's
1103
+ // approval of this experience. (The
1104
+ // notice versions web-consent-2026-11-23-v and -24-v, which no environment ever ran, are retired with the dialog
1105
+ // that showed them: this SDK has no copy for them, so a server naming one gets the Avatar tab as it was.)
1106
+ "web-voice-2026-11-25-n",
1107
+ {
1108
+ en: {
1109
+ notice: "none",
1110
+ botCheck: "off",
1111
+ questions: "any",
1112
+ followUps: true,
1113
+ voice: { pauseMs: 2e3 }
1114
+ }
1115
+ }
1084
1116
  ]
1085
1117
  ]);
1086
1118
  function avatarConsentCopy(version, locale) {
@@ -1111,6 +1143,30 @@ function avatarMicStrings(locale) {
1111
1143
  if (!isAvatarLiveLocaleReviewed(locale)) return null;
1112
1144
  return MIC_STRINGS[locale] ?? null;
1113
1145
  }
1146
+ var VOICE_EN = {
1147
+ talk: "Talk",
1148
+ talkInterrupt: "Stop the avatar and talk",
1149
+ stopListening: "Stop listening",
1150
+ start: "Start",
1151
+ cancelled: "The avatar didn't start. Press Start to try again.",
1152
+ starting: "Starting the microphone\u2026",
1153
+ listening: "Listening\u2026",
1154
+ pausing: "Still listening \u2014 take your time",
1155
+ sending: "Sending your question\u2026",
1156
+ again: "The mic listens again after the answer.",
1157
+ off: "Voice is off. Tap Talk to speak, or type your question.",
1158
+ inBox: "Voice is off. Your words are in the box: edit them or press Send.",
1159
+ atLimit: "The mic stopped at thirty seconds. Your words are in the box: press Send to ask.",
1160
+ noQuestion: "No question was heard. Tap Talk to try again, or type your question.",
1161
+ tapNext: "Tap Talk to ask your next question.",
1162
+ headline: "Tap Talk and ask. The avatar answers aloud.",
1163
+ placeholder: "Or type\u2026"
1164
+ };
1165
+ var VOICE_STRINGS = { en: VOICE_EN };
1166
+ function avatarVoiceStrings(locale) {
1167
+ if (!isAvatarLiveLocaleReviewed(locale)) return null;
1168
+ return VOICE_STRINGS[locale] ?? null;
1169
+ }
1114
1170
  var AVATAR_MIC_NOTICE_VERSION = "web-mic-2026-11-05";
1115
1171
  var MIC_NOTICE = /* @__PURE__ */ new Map([
1116
1172
  [
@@ -1188,6 +1244,7 @@ function canUseBrowserMic() {
1188
1244
  if (typeof recognizer !== "function") return false;
1189
1245
  const agent = navigator.userAgent ?? "";
1190
1246
  if (/\bFirefox\//.test(agent) || /\bFxiOS\//.test(agent)) return false;
1247
+ if (/\bOPR\//.test(agent)) return false;
1191
1248
  return !("brave" in navigator);
1192
1249
  }
1193
1250
  var REPLAY_PRIVATE = "rr-block rr-mask";
@@ -1205,20 +1262,33 @@ function noticeOf(source) {
1205
1262
  return source.kind === "stream" ? AVATAR_MIC_NOTICE_VERSION : source.notice;
1206
1263
  }
1207
1264
  function micKeyOf(source, locale) {
1208
- return source.kind === "stream" ? `stream ${source.base}` : `browser ${BROWSER_MIC_LANGUAGES[locale]?.lang ?? ""}`;
1265
+ if (source.kind === "stream") return `stream ${source.base}`;
1266
+ const lang = BROWSER_MIC_LANGUAGES[locale]?.lang ?? "";
1267
+ return source.pauseMs === void 0 ? `browser ${lang}` : `browser ${lang} pause ${source.pauseMs}`;
1268
+ }
1269
+ function underWay(state) {
1270
+ return state.name === "starting" || state.name === "listening" || state.name === "finishing";
1209
1271
  }
1210
1272
  function useAvatarMic({
1211
1273
  enabled,
1212
1274
  locale,
1213
1275
  source,
1214
1276
  onText,
1215
- onStart
1277
+ onStart,
1278
+ onTurn,
1279
+ onPress,
1280
+ echoOf,
1281
+ interruptible = false,
1282
+ note = null
1216
1283
  }) {
1217
1284
  const S = avatarMicStrings(locale);
1218
1285
  const noticeVersion = source === null ? null : noticeOf(source);
1219
- const copy = noticeVersion === null ? null : avatarMicNotice(noticeVersion, locale);
1220
1286
  const kind = source?.kind ?? null;
1221
1287
  const micKey = source === null ? null : micKeyOf(source, locale);
1288
+ const paused = source?.kind === "browser" && source.pauseMs !== void 0;
1289
+ const conversation = paused && noticeVersion !== null && avatarConsentCopy(noticeVersion, locale)?.notice === "none";
1290
+ const copy = noticeVersion === null || paused ? null : avatarMicNotice(noticeVersion, locale);
1291
+ const V = conversation ? avatarVoiceStrings(locale) : null;
1222
1292
  const uid = useId();
1223
1293
  const noticeTitleId = `${uid}-mic-notice`;
1224
1294
  const [withdrawn, setWithdrawn] = useState({});
@@ -1228,13 +1298,19 @@ function useAvatarMic({
1228
1298
  []
1229
1299
  );
1230
1300
  const usable = source === null ? false : source.kind === "stream" ? supported.stream : supported.browser && BROWSER_MIC_LANGUAGES[locale] !== void 0;
1231
- const possible = enabled && kind !== null && usable && S !== null && copy !== null;
1301
+ const showable = kind !== null && usable && S !== null && (paused ? conversation && V !== null : copy !== null);
1302
+ const possible = enabled && showable;
1232
1303
  const withdrawnReason = kind === null ? null : withdrawn[kind] ?? null;
1233
1304
  const offered = possible && withdrawnReason === null;
1234
1305
  const [state, setState] = useState(IDLE);
1235
1306
  const [noticeOpen, setNoticeOpen] = useState(false);
1236
1307
  const agreedRef = useRef(null);
1237
- const [loading2, setLoading] = useState(false);
1308
+ const [loading2, setLoadingState] = useState(false);
1309
+ const loadingRef = useRef(false);
1310
+ const setLoading = useCallback((value) => {
1311
+ loadingRef.current = value;
1312
+ setLoadingState(value);
1313
+ }, []);
1238
1314
  const [loadFailed, setLoadFailed] = useState(false);
1239
1315
  const [now, setNow] = useState(() => Date.now());
1240
1316
  const micRef = useRef(
@@ -1244,15 +1320,39 @@ function useAvatarMic({
1244
1320
  const buttonRef = useRef(null);
1245
1321
  const allowRef = useRef(null);
1246
1322
  const usedRef = useRef(null);
1247
- const latest = useRef({ source, locale, noticeVersion, onText, onStart });
1248
- latest.current = { source, locale, noticeVersion, onText, onStart };
1323
+ const gestureRef = useRef(false);
1324
+ const [, setUsedTick] = useState(0);
1325
+ const latest = useRef({
1326
+ source,
1327
+ locale,
1328
+ noticeVersion,
1329
+ onText,
1330
+ onStart,
1331
+ onTurn,
1332
+ onPress,
1333
+ echoOf,
1334
+ conversation,
1335
+ offered
1336
+ });
1337
+ latest.current = {
1338
+ source,
1339
+ locale,
1340
+ noticeVersion,
1341
+ onText,
1342
+ onStart,
1343
+ onTurn,
1344
+ onPress,
1345
+ echoOf,
1346
+ conversation,
1347
+ offered
1348
+ };
1249
1349
  const cancel = useCallback(() => {
1250
1350
  seqRef.current += 1;
1251
1351
  setLoading(false);
1252
1352
  setLoadFailed(false);
1253
1353
  setNoticeOpen(false);
1254
1354
  micRef.current?.mic.cancel();
1255
- }, []);
1355
+ }, [setLoading]);
1256
1356
  useEffect(() => {
1257
1357
  if (!offered) cancel();
1258
1358
  }, [offered, cancel]);
@@ -1290,11 +1390,22 @@ function useAvatarMic({
1290
1390
  });
1291
1391
  } else {
1292
1392
  const language = BROWSER_MIC_LANGUAGES[latest.current.locale] ?? EN_IN;
1393
+ const { pauseMs } = wanted;
1293
1394
  mic = loaded.createBrowserAvatarMic({
1294
1395
  lang: language.lang,
1295
1396
  fallbackLang: language.fallbackLang,
1296
1397
  maxPressMs: LIMIT_SECONDS * 1e3,
1297
- onState: setState
1398
+ onState: setState,
1399
+ // The voice conversation: each press ends itself on the server's pause. The avatar's own words heard back
1400
+ // are looked for only in a press the conversation started by itself: one the visitor's own tap started runs
1401
+ // while the avatar is quiet, or muted here by that tap (a Talk while it speaks interrupts it), so its words are
1402
+ // the visitor's, whatever of the answer they repeat ("Delivery is free across Tamil Nadu?").
1403
+ ...pauseMs === void 0 ? {} : {
1404
+ endpoint: {
1405
+ pauseMs,
1406
+ echoOf: () => gestureRef.current ? null : latest.current.echoOf?.() ?? null
1407
+ }
1408
+ }
1298
1409
  });
1299
1410
  }
1300
1411
  micRef.current = { key, kind: wanted.kind, mic };
@@ -1302,37 +1413,90 @@ function useAvatarMic({
1302
1413
  },
1303
1414
  []
1304
1415
  );
1305
- const press = useCallback(() => {
1306
- const wanted = latest.current.source;
1307
- if (wanted === null) return;
1308
- seqRef.current += 1;
1309
- const seq = seqRef.current;
1310
- setLoadFailed(false);
1311
- const held = micRef.current;
1312
- const ready = wanted.kind !== "browser" ? null : held !== null && held.key === micKeyOf(wanted, latest.current.locale) ? held.mic : loadedMic !== null ? micOf(loadedMic, wanted) : null;
1313
- if (ready !== null) {
1314
- setLoading(false);
1315
- ready.start();
1316
- return;
1317
- }
1318
- setLoading(true);
1319
- const arriving = held !== null && held.key === micKeyOf(wanted, latest.current.locale) ? Promise.resolve(held.mic) : loadAvatarMic().then((loaded) => micOf(loaded, wanted));
1320
- void arriving.then(
1321
- (mic) => {
1322
- if (seqRef.current !== seq) return;
1323
- setLoading(false);
1324
- mic.start();
1325
- },
1326
- () => {
1327
- if (seqRef.current !== seq) return;
1416
+ const press = useCallback(
1417
+ (gesture) => {
1418
+ const wanted = latest.current.source;
1419
+ if (wanted === null) return;
1420
+ seqRef.current += 1;
1421
+ const seq = seqRef.current;
1422
+ setLoadFailed(false);
1423
+ gestureRef.current = gesture;
1424
+ if (latest.current.conversation) latest.current.onPress?.(gesture);
1425
+ const held = micRef.current;
1426
+ const ready = wanted.kind !== "browser" ? null : held !== null && held.key === micKeyOf(wanted, latest.current.locale) ? held.mic : loadedMic !== null ? micOf(loadedMic, wanted) : null;
1427
+ if (ready !== null) {
1328
1428
  setLoading(false);
1329
- setLoadFailed(true);
1429
+ ready.start();
1430
+ return;
1330
1431
  }
1331
- );
1332
- }, [micOf]);
1432
+ setLoading(true);
1433
+ const arriving = held !== null && held.key === micKeyOf(wanted, latest.current.locale) ? Promise.resolve(held.mic) : loadAvatarMic().then((loaded) => micOf(loaded, wanted));
1434
+ void arriving.then(
1435
+ (mic) => {
1436
+ if (seqRef.current !== seq) return;
1437
+ setLoading(false);
1438
+ mic.start();
1439
+ },
1440
+ () => {
1441
+ if (seqRef.current !== seq) return;
1442
+ setLoading(false);
1443
+ setLoadFailed(true);
1444
+ if (latest.current.conversation) {
1445
+ latest.current.onTurn?.({
1446
+ by: "failed",
1447
+ text: "",
1448
+ echo: false,
1449
+ gesture,
1450
+ failure: "failed"
1451
+ });
1452
+ }
1453
+ }
1454
+ );
1455
+ },
1456
+ [micOf, setLoading]
1457
+ );
1333
1458
  const recording = loading2 || state.name === "starting" || state.name === "listening";
1334
1459
  const finishing = kind === "browser" && state.name === "finishing";
1460
+ const preload = useCallback(() => {
1461
+ const seq = seqRef.current;
1462
+ void loadAvatarMic().then(
1463
+ (loaded) => {
1464
+ const wanted = latest.current.source;
1465
+ if (seqRef.current === seq && wanted?.kind === "browser") {
1466
+ micOf(loaded, wanted);
1467
+ }
1468
+ },
1469
+ () => void 0
1470
+ );
1471
+ }, [micOf]);
1472
+ const pressing = useCallback(() => {
1473
+ const held = micRef.current?.mic.state() ?? IDLE;
1474
+ return loadingRef.current || underWay(held) || (held.name === "done" || held.name === "failed") && held !== usedRef.current;
1475
+ }, []);
1476
+ const stopListening = useCallback(() => {
1477
+ const held = micRef.current?.mic.state() ?? IDLE;
1478
+ let text = "";
1479
+ if (held.name === "listening" || held.name === "finishing") {
1480
+ text = held.text;
1481
+ } else if ((held.name === "done" || held.name === "failed") && held !== usedRef.current) {
1482
+ usedRef.current = held;
1483
+ text = held.text;
1484
+ }
1485
+ const gesture = gestureRef.current;
1486
+ cancel();
1487
+ latest.current.onTurn?.({ by: "tap", text, echo: false, gesture });
1488
+ }, [cancel]);
1335
1489
  const tap = useCallback(() => {
1490
+ if (latest.current.conversation) {
1491
+ if (pressing()) {
1492
+ stopListening();
1493
+ return;
1494
+ }
1495
+ latest.current.onStart?.();
1496
+ agreedRef.current = latest.current.noticeVersion;
1497
+ press(true);
1498
+ return;
1499
+ }
1336
1500
  if (state.name === "listening" || state.name === "starting") {
1337
1501
  micRef.current?.mic.stop();
1338
1502
  return;
@@ -1345,30 +1509,56 @@ function useAvatarMic({
1345
1509
  latest.current.onStart?.();
1346
1510
  if (agreedRef.current !== latest.current.noticeVersion) {
1347
1511
  setNoticeOpen(true);
1348
- const seq = seqRef.current;
1349
- void loadAvatarMic().then(
1350
- (loaded) => {
1351
- const wanted = latest.current.source;
1352
- if (seqRef.current === seq && wanted?.kind === "browser") {
1353
- micOf(loaded, wanted);
1354
- }
1355
- },
1356
- () => void 0
1357
- );
1512
+ preload();
1358
1513
  return;
1359
1514
  }
1360
- press();
1361
- }, [cancel, finishing, loading2, micOf, press, state.name]);
1515
+ press(true);
1516
+ }, [
1517
+ cancel,
1518
+ finishing,
1519
+ loading2,
1520
+ preload,
1521
+ press,
1522
+ pressing,
1523
+ state.name,
1524
+ stopListening
1525
+ ]);
1362
1526
  const allow = useCallback(() => {
1363
1527
  agreedRef.current = latest.current.noticeVersion;
1364
1528
  setNoticeOpen(false);
1365
1529
  buttonRef.current?.focus();
1366
- press();
1530
+ press(true);
1367
1531
  }, [press]);
1368
1532
  const notNow = useCallback(() => {
1369
1533
  setNoticeOpen(false);
1370
1534
  buttonRef.current?.focus();
1371
1535
  }, []);
1536
+ const preparing = conversation && offered;
1537
+ useEffect(() => {
1538
+ if (preparing) preload();
1539
+ }, [preparing, preload]);
1540
+ const listen = useCallback(() => {
1541
+ if (!latest.current.conversation || !latest.current.offered || // Never before the visitor's own Talk on this page, under this version.
1542
+ agreedRef.current !== latest.current.noticeVersion || // A press under way, or one that has ended and whose outcome has not gone to the view yet (still that press: a
1543
+ // question that ended on its pause is about to be sent): the mic must not open over it.
1544
+ pressing()) {
1545
+ return false;
1546
+ }
1547
+ press(false);
1548
+ return true;
1549
+ }, [press, pressing]);
1550
+ const heard = useCallback(() => {
1551
+ const held = micRef.current?.mic.state() ?? IDLE;
1552
+ return held.name === "listening" || held.name === "finishing" ? held.text : "";
1553
+ }, []);
1554
+ const dropLoopPress = useCallback(() => {
1555
+ const held = micRef.current?.mic.state() ?? IDLE;
1556
+ if (!latest.current.conversation || gestureRef.current || !loadingRef.current && !underWay(held)) {
1557
+ return false;
1558
+ }
1559
+ cancel();
1560
+ return true;
1561
+ }, [cancel]);
1372
1562
  useEffect(() => {
1373
1563
  if (noticeOpen) allowRef.current?.focus();
1374
1564
  }, [noticeOpen]);
@@ -1380,28 +1570,71 @@ function useAvatarMic({
1380
1570
  return () => clearInterval(timer);
1381
1571
  }, [listeningSince]);
1382
1572
  useEffect(() => {
1573
+ const voice = latest.current.conversation;
1383
1574
  if (state.name === "done" && state !== usedRef.current) {
1384
1575
  usedRef.current = state;
1385
- if (state.text !== "") latest.current.onText(state.text);
1576
+ if (voice) {
1577
+ latest.current.onTurn?.({
1578
+ by: state.by ?? (state.limit ? "limit" : "tap"),
1579
+ text: state.text,
1580
+ echo: state.echo === true,
1581
+ gesture: gestureRef.current
1582
+ });
1583
+ setUsedTick((tick) => tick + 1);
1584
+ } else if (state.text !== "") {
1585
+ latest.current.onText(state.text);
1586
+ }
1386
1587
  return;
1387
1588
  }
1388
1589
  const pressed = micRef.current?.kind;
1389
- if (state.name === "failed" && pressed !== void 0 && FOR_GOOD[pressed].has(state.reason)) {
1590
+ if (state.name === "failed" && voice && state !== usedRef.current) {
1591
+ usedRef.current = state;
1592
+ const gesture = gestureRef.current;
1593
+ if (gesture && pressed !== void 0 && FOR_GOOD[pressed].has(state.reason)) {
1594
+ const reason = state.reason;
1595
+ setWithdrawn((held) => ({ ...held, [pressed]: reason }));
1596
+ }
1597
+ latest.current.onTurn?.({
1598
+ by: "failed",
1599
+ text: state.text,
1600
+ echo: false,
1601
+ gesture,
1602
+ failure: state.reason
1603
+ });
1604
+ setUsedTick((tick) => tick + 1);
1605
+ return;
1606
+ }
1607
+ if (state.name === "failed" && !voice && pressed !== void 0 && FOR_GOOD[pressed].has(state.reason)) {
1390
1608
  const reason = state.reason;
1391
1609
  setWithdrawn((held) => ({ ...held, [pressed]: reason }));
1392
1610
  }
1393
1611
  }, [state]);
1612
+ const outcomeDue = (state.name === "done" || state.name === "failed") && state !== usedRef.current;
1613
+ const busy = loading2 || state.name === "starting" || state.name === "listening" || state.name === "finishing" || outcomeDue;
1614
+ const voiceControls = {
1615
+ mode: conversation ? "conversation" : "editable",
1616
+ speaking: conversation && offered && state.name === "listening" && state.genuine === true,
1617
+ busy,
1618
+ pressing,
1619
+ listen,
1620
+ heard,
1621
+ dropLoopPress
1622
+ };
1623
+ if (conversation && showable && V !== null && S !== null) {
1624
+ return conversationControls(V, S);
1625
+ }
1394
1626
  if (!possible || S === null || copy === null) {
1395
1627
  return {
1396
1628
  button: null,
1397
1629
  notice: null,
1398
1630
  lines: null,
1399
1631
  cancel,
1400
- recording: false
1632
+ recording: false,
1633
+ ...voiceControls
1401
1634
  };
1402
1635
  }
1403
1636
  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]] : "";
1404
- const heard = offered && (state.name === "listening" || state.name === "finishing") ? state.text : "";
1637
+ const heardText = offered && (state.name === "listening" || state.name === "finishing") ? state.text : "";
1405
1638
  const button = !offered ? null : /* @__PURE__ */ jsx(
1406
1639
  "button",
1407
1640
  {
@@ -1423,62 +1656,11 @@ function useAvatarMic({
1423
1656
  children: recording ? /* @__PURE__ */ jsx(Square, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx(Mic, { "aria-hidden": "true", className: "h-4 w-4" })
1424
1657
  }
1425
1658
  );
1426
- const notice = offered && noticeOpen ? /* @__PURE__ */ jsxs(
1427
- "section",
1428
- {
1429
- "aria-labelledby": noticeTitleId,
1430
- "data-boff-explore": "avatar-live-mic-notice",
1431
- className: "w-full rounded-lg border border-border p-3 text-start",
1432
- children: [
1433
- /* @__PURE__ */ jsx("h3", { id: noticeTitleId, className: "text-sm font-semibold", children: copy.title }),
1434
- /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: copy.processing }),
1435
- /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: copy.storage }),
1436
- /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: copy.text }),
1437
- /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: copy.limits }),
1438
- /* @__PURE__ */ jsx(
1439
- "a",
1440
- {
1441
- href: PRIVACY_HREF,
1442
- target: "_blank",
1443
- rel: "noopener noreferrer",
1444
- "data-boff-explore": "avatar-live-mic-privacy",
1445
- className: "mt-1 inline-block text-xs font-medium text-primary underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1446
- children: copy.privacyLink
1447
- }
1448
- ),
1449
- /* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap gap-2", children: [
1450
- /* @__PURE__ */ jsx(
1451
- "button",
1452
- {
1453
- ref: allowRef,
1454
- type: "button",
1455
- "data-boff-explore": "avatar-live-mic-allow",
1456
- className: "inline-flex min-h-11 items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 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",
1457
- onClick: allow,
1458
- children: S.micAllow
1459
- }
1460
- ),
1461
- /* @__PURE__ */ jsx(
1462
- "button",
1463
- {
1464
- type: "button",
1465
- "data-boff-explore": "avatar-live-mic-not-now",
1466
- className: "inline-flex min-h-11 items-center justify-center rounded-md border border-border bg-background px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground 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",
1467
- onClick: notNow,
1468
- children: S.micNotNow
1469
- }
1470
- )
1471
- ] })
1472
- ]
1473
- }
1474
- ) : null;
1659
+ const notice = offered && noticeOpen ? noticeCard(copy, S.micAllow, S.micNotNow) : null;
1475
1660
  const listening = offered && state.name === "listening";
1476
1661
  const lines = /* @__PURE__ */ jsxs(Fragment, { children: [
1477
1662
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
1478
- listening ? /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: "relative flex h-2 w-2 shrink-0", children: [
1479
- /* @__PURE__ */ jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75 motion-reduce:animate-none" }),
1480
- /* @__PURE__ */ jsx("span", { className: "relative inline-flex h-2 w-2 rounded-full bg-primary" })
1481
- ] }) : null,
1663
+ listening ? listeningDot : null,
1482
1664
  /* @__PURE__ */ jsx("p", { role: "status", "data-boff-explore": "avatar-live-mic-status", children: status }),
1483
1665
  listening ? /* @__PURE__ */ jsx(
1484
1666
  "span",
@@ -1495,7 +1677,69 @@ function useAvatarMic({
1495
1677
  }
1496
1678
  ) : null
1497
1679
  ] }),
1498
- heard === "" ? null : /* @__PURE__ */ jsx(
1680
+ heardLine(heardText)
1681
+ ] });
1682
+ return {
1683
+ button,
1684
+ notice,
1685
+ lines,
1686
+ cancel,
1687
+ recording: offered && state.name === "listening",
1688
+ ...voiceControls
1689
+ };
1690
+ function noticeCard(words, allowLabel, notNowLabel) {
1691
+ return /* @__PURE__ */ jsxs(
1692
+ "section",
1693
+ {
1694
+ "aria-labelledby": noticeTitleId,
1695
+ "data-boff-explore": "avatar-live-mic-notice",
1696
+ className: "w-full rounded-lg border border-border p-3 text-start",
1697
+ children: [
1698
+ /* @__PURE__ */ jsx("h3", { id: noticeTitleId, className: "text-sm font-semibold", children: words.title }),
1699
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: words.processing }),
1700
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: words.storage }),
1701
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: words.text }),
1702
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: words.limits }),
1703
+ /* @__PURE__ */ jsx(
1704
+ "a",
1705
+ {
1706
+ href: PRIVACY_HREF,
1707
+ target: "_blank",
1708
+ rel: "noopener noreferrer",
1709
+ "data-boff-explore": "avatar-live-mic-privacy",
1710
+ className: "mt-1 inline-block text-xs font-medium text-primary underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1711
+ children: words.privacyLink
1712
+ }
1713
+ ),
1714
+ /* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap gap-2", children: [
1715
+ /* @__PURE__ */ jsx(
1716
+ "button",
1717
+ {
1718
+ ref: allowRef,
1719
+ type: "button",
1720
+ "data-boff-explore": "avatar-live-mic-allow",
1721
+ className: "inline-flex min-h-11 items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 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",
1722
+ onClick: allow,
1723
+ children: allowLabel
1724
+ }
1725
+ ),
1726
+ /* @__PURE__ */ jsx(
1727
+ "button",
1728
+ {
1729
+ type: "button",
1730
+ "data-boff-explore": "avatar-live-mic-not-now",
1731
+ className: "inline-flex min-h-11 items-center justify-center rounded-md border border-border bg-background px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground 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",
1732
+ onClick: notNow,
1733
+ children: notNowLabel
1734
+ }
1735
+ )
1736
+ ] })
1737
+ ]
1738
+ }
1739
+ );
1740
+ }
1741
+ function heardLine(text) {
1742
+ return text === "" ? null : /* @__PURE__ */ jsx(
1499
1743
  "p",
1500
1744
  {
1501
1745
  "data-boff-explore": "avatar-live-mic-heard",
@@ -1505,18 +1749,70 @@ function useAvatarMic({
1505
1749
  REPLAY_PRIVATE,
1506
1750
  "line-clamp-3 break-words text-sm italic text-muted-foreground"
1507
1751
  ),
1508
- children: heard
1752
+ children: text
1509
1753
  }
1510
- )
1511
- ] });
1512
- return {
1513
- button,
1514
- notice,
1515
- lines,
1516
- cancel,
1517
- recording: offered && state.name === "listening"
1518
- };
1754
+ );
1755
+ }
1756
+ function conversationControls(V2, S2) {
1757
+ const going = loading2 || state.name === "starting" || state.name === "listening" || state.name === "finishing";
1758
+ const label = busy ? V2.stopListening : V2.talk;
1759
+ const name = busy ? V2.stopListening : interruptible ? V2.talkInterrupt : V2.talk;
1760
+ const talk = !offered ? null : /* @__PURE__ */ jsxs(
1761
+ "button",
1762
+ {
1763
+ ref: buttonRef,
1764
+ type: "button",
1765
+ "aria-label": name,
1766
+ title: name,
1767
+ "data-boff-explore": "avatar-live-mic",
1768
+ "data-mic": loading2 ? "starting" : state.name,
1769
+ "data-mic-source": "browser",
1770
+ "data-mic-mode": "conversation",
1771
+ className: cn(
1772
+ "inline-flex h-11 shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md border px-3 text-sm font-medium 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",
1773
+ busy ? "border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground" : "border-primary bg-primary text-primary-foreground hover:opacity-90"
1774
+ ),
1775
+ onClick: tap,
1776
+ children: [
1777
+ busy ? /* @__PURE__ */ jsx(Square, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx(Mic, { "aria-hidden": "true", className: "h-4 w-4" }),
1778
+ /* @__PURE__ */ jsx("span", { children: label })
1779
+ ]
1780
+ }
1781
+ );
1782
+ const pressLine = !offered ? null : loading2 || state.name === "starting" ? V2.starting : state.name === "listening" ? state.pausing === true ? V2.pausing : V2.listening : state.name === "finishing" ? state.limit ? S2.micFinishing : V2.sending : null;
1783
+ const noteLine = note === null ? "" : note.kind === "line" ? V2[note.line] : S2[FAILURE_TEXT[note.reason]];
1784
+ const line = withdrawnReason !== null ? S2[FAILURE_TEXT[withdrawnReason]] : pressLine ?? noteLine;
1785
+ const heardNow = offered && (state.name === "listening" || state.name === "finishing") ? state.text : "";
1786
+ const listeningNow = offered && state.name === "listening";
1787
+ const shown = possible || line !== "";
1788
+ return {
1789
+ button: talk,
1790
+ notice: null,
1791
+ lines: !shown ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
1792
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
1793
+ listeningNow ? listeningDot : null,
1794
+ /* @__PURE__ */ jsx(
1795
+ "p",
1796
+ {
1797
+ role: "status",
1798
+ "data-boff-explore": "avatar-live-mic-status",
1799
+ "data-voice": going ? "pressing" : note?.kind ?? "idle",
1800
+ children: line
1801
+ }
1802
+ )
1803
+ ] }),
1804
+ heardLine(heardNow)
1805
+ ] }),
1806
+ cancel,
1807
+ recording: offered && state.name === "listening",
1808
+ ...voiceControls
1809
+ };
1810
+ }
1519
1811
  }
1812
+ var listeningDot = /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: "relative flex h-2 w-2 shrink-0", children: [
1813
+ /* @__PURE__ */ jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75 motion-reduce:animate-none" }),
1814
+ /* @__PURE__ */ jsx("span", { className: "relative inline-flex h-2 w-2 rounded-full bg-primary" })
1815
+ ] });
1520
1816
 
1521
1817
  // src/components/explore-avatar-turnstile.ts
1522
1818
  var SCRIPT_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
@@ -1645,6 +1941,9 @@ var LISTENING_MARK_SPACING_MS = 5e3;
1645
1941
  var BUSY_RETRY_MS = 2e3;
1646
1942
  var BUSY_RETRY_MAX_MS = 8e3;
1647
1943
  var MAX_BUSY_RETRIES = 24;
1944
+ var PAUSE_NOTICE_TOLERANCE_MS = 500;
1945
+ var MAX_ECHO_DISCARDS = 2;
1946
+ var LISTEN_AGAIN_MIN_LEFT_MS = 2e4;
1648
1947
  var SERVICE_MAX_AGE_MS = 15 * 6e4;
1649
1948
  var ANSWER_OFFER_MS = SERVICE_MAX_AGE_MS - 6e4;
1650
1949
  var MAX_TIMER_MS = 2147483647;
@@ -1654,6 +1953,29 @@ var USED_ON_KEY = "boff.explore.avatarUsedOn";
1654
1953
  var PRIMARY = '[data-avatar-primary], [data-boff-explore="panel-continue-chat"]';
1655
1954
  var IDLE2 = { name: "idle" };
1656
1955
  var INERT2 = { name: "inert" };
1956
+ var VOICE_OFF = {
1957
+ on: false,
1958
+ arm: null,
1959
+ echoes: 0,
1960
+ note: null
1961
+ };
1962
+ function lineNote(line) {
1963
+ return { kind: "line", line };
1964
+ }
1965
+ function sameNote(a, b) {
1966
+ if (a === null || b === null) return a === b;
1967
+ return a.kind === "line" ? b.kind === "line" && a.line === b.line : b.kind === "failed" && a.reason === b.reason;
1968
+ }
1969
+ var SESSION_REFUSALS = /* @__PURE__ */ new Set([
1970
+ "no_time",
1971
+ "max_answers",
1972
+ "limits",
1973
+ "paused"
1974
+ ]);
1975
+ function playedOutNow(handle) {
1976
+ const run = handle.followUps;
1977
+ return run !== null && !run.voice && run.quiet === null && handle.playedOut && !run.owed;
1978
+ }
1657
1979
  function stopAttending(handle) {
1658
1980
  if (handle.unheard !== null) clearTimeout(handle.unheard);
1659
1981
  if (handle.tail !== null) clearTimeout(handle.tail);
@@ -1771,9 +2093,15 @@ var NO_MIC = { kind: "none" };
1771
2093
  function micChoiceOf(result, locale) {
1772
2094
  if (result.kind !== "available") return NO_MIC;
1773
2095
  const { offer } = result;
1774
- const micNotice = avatarConsentCopy(offer.consentVersion, locale)?.micNotice;
1775
- if (offer.mic !== void 0) {
1776
- return offer.mic.mode === "browser" && micNotice !== void 0 ? { kind: "browser", notice: micNotice } : NO_MIC;
2096
+ const { mic } = offer;
2097
+ const copy = avatarConsentCopy(offer.consentVersion, locale);
2098
+ if (copy?.notice === "none") {
2099
+ const pauseMs = mic?.mode === "browser" ? mic.pauseMs : void 0;
2100
+ return pauseMs !== void 0 && Math.abs(pauseMs - copy.voice.pauseMs) <= PAUSE_NOTICE_TOLERANCE_MS && avatarVoiceStrings(locale) !== null ? { kind: "browser", notice: offer.consentVersion, pauseMs } : NO_MIC;
2101
+ }
2102
+ const micNotice = copy?.micNotice;
2103
+ if (mic !== void 0) {
2104
+ return mic.mode === "browser" && micNotice !== void 0 && mic.pauseMs === void 0 ? { kind: "browser", notice: micNotice } : NO_MIC;
1777
2105
  }
1778
2106
  return offer.stt !== void 0 && micNotice === void 0 ? { kind: "stream", offer: { ...offer.stt, receivedAt: Date.now() } } : NO_MIC;
1779
2107
  }
@@ -1873,6 +2201,7 @@ function AvatarLiveView({
1873
2201
  conversation
1874
2202
  }) {
1875
2203
  const S = avatarLiveStrings(locale);
2204
+ const voiceWords = avatarVoiceStrings(locale);
1876
2205
  const uid = useId();
1877
2206
  const consentTitleId = `${uid}-consent`;
1878
2207
  const [questions, setQuestions] = useState("suggested");
@@ -1901,12 +2230,16 @@ function AvatarLiveView({
1901
2230
  const offerUntil = answerId === null ? null : exchange.offerUntil;
1902
2231
  const [phase, setPhase] = useState(IDLE2);
1903
2232
  const [blocked, setBlocked] = useState(false);
2233
+ const blockedRef = useRef(false);
2234
+ blockedRef.current = blocked;
1904
2235
  const [now, setNow] = useState(() => Date.now());
1905
2236
  const [draft, setDraft] = useState("");
1906
2237
  const chatMaxLength = conversation?.catalog.limits.maxMessageLength;
1907
2238
  const askMaxLength = chatMaxLength !== void 0 && Number.isFinite(chatMaxLength) && chatMaxLength >= 1 ? Math.min(ASK_MAX_LENGTH, Math.floor(chatMaxLength)) : ASK_MAX_LENGTH;
1908
2239
  const [note, setNote] = useState(null);
1909
2240
  const [usedToday, setUsedToday] = useState(false);
2241
+ const [voiceOffered, setVoiceOffered] = useState(false);
2242
+ const [noNotice, setNoNotice] = useState(false);
1910
2243
  const showing = active && !off;
1911
2244
  const behindChat = open && !active && !off;
1912
2245
  const phaseRef = useRef(IDLE2);
@@ -1927,6 +2260,34 @@ function AvatarLiveView({
1927
2260
  const textBoxRef = useRef(false);
1928
2261
  const [micChoice, setMicChoice] = useState(NO_MIC);
1929
2262
  const micCancelRef = useRef(() => void 0);
2263
+ const micControlRef = useRef(null);
2264
+ const voiceRef = useRef(VOICE_OFF);
2265
+ const [, setVoiceTick] = useState(0);
2266
+ const voiceSet = useCallback((next) => {
2267
+ voiceRef.current = { ...voiceRef.current, ...next };
2268
+ setVoiceTick((tick) => tick + 1);
2269
+ }, []);
2270
+ const askMaxLengthRef = useRef(ASK_MAX_LENGTH);
2271
+ const voiceOff = useCallback(
2272
+ (note2, keep) => {
2273
+ const run = voiceRef.current;
2274
+ if (!run.on && run.arm === null) {
2275
+ if (!sameNote(run.note, note2)) voiceSet({ note: note2 });
2276
+ return;
2277
+ }
2278
+ if (keep) {
2279
+ const heard = micControlRef.current?.heard() ?? "";
2280
+ if (heard !== "") {
2281
+ setDraft(
2282
+ (current) => withHeardText(current, heard, askMaxLengthRef.current)
2283
+ );
2284
+ }
2285
+ }
2286
+ micCancelRef.current();
2287
+ voiceSet({ ...VOICE_OFF, note: note2 });
2288
+ },
2289
+ [voiceSet]
2290
+ );
1930
2291
  const sendDueRef = useRef(() => void 0);
1931
2292
  const checkRef = useRef(null);
1932
2293
  const askedLocale = useRef(locale);
@@ -1983,6 +2344,8 @@ function AvatarLiveView({
1983
2344
  handle.done = true;
1984
2345
  stopAttending(handle);
1985
2346
  sessionRef.current = null;
2347
+ voiceOff(null, true);
2348
+ if (audioRef.current !== null) audioRef.current.muted = false;
1986
2349
  if (reason !== null) {
1987
2350
  sendEnd({
1988
2351
  base: handle.base,
@@ -1998,7 +2361,7 @@ function AvatarLiveView({
1998
2361
  setBlocked(false);
1999
2362
  go({ name: "ended", how });
2000
2363
  },
2001
- [go]
2364
+ [go, voiceOff]
2002
2365
  );
2003
2366
  const goLive = useCallback(() => {
2004
2367
  const current = phaseRef.current;
@@ -2071,6 +2434,18 @@ function AvatarLiveView({
2071
2434
  },
2072
2435
  []
2073
2436
  );
2437
+ const unmute = useCallback((handle) => {
2438
+ const run = handle.followUps;
2439
+ if (run === null || !run.muted) return;
2440
+ run.muted = false;
2441
+ if (audioRef.current !== null) audioRef.current.muted = false;
2442
+ }, []);
2443
+ const holdMute = useCallback(() => {
2444
+ const run = sessionRef.current?.followUps ?? null;
2445
+ const audio = audioRef.current;
2446
+ if (run === null || !run.muted || audio === null || audio.muted) return;
2447
+ audio.muted = true;
2448
+ }, []);
2074
2449
  const whenQuiet = useCallback(
2075
2450
  (handle, delay) => {
2076
2451
  const run = handle.followUps;
@@ -2084,10 +2459,12 @@ function AvatarLiveView({
2084
2459
  }
2085
2460
  if (run.owed) {
2086
2461
  run.owed = false;
2462
+ run.owedWait = true;
2087
2463
  wait(OWED_VOICE_WAIT_MS);
2088
2464
  return;
2089
2465
  }
2090
2466
  handle.playedOut = true;
2467
+ if (!run.mutedUnheard) unmute(handle);
2091
2468
  showFollowUp(handle, { speaking: false });
2092
2469
  attend();
2093
2470
  sendDueRef.current(handle);
@@ -2095,7 +2472,7 @@ function AvatarLiveView({
2095
2472
  };
2096
2473
  wait(delay);
2097
2474
  },
2098
- [attend, showFollowUp]
2475
+ [attend, showFollowUp, unmute]
2099
2476
  );
2100
2477
  const heardVoice = useCallback(
2101
2478
  (handle, speaking) => {
@@ -2111,7 +2488,10 @@ function AvatarLiveView({
2111
2488
  run.heard = true;
2112
2489
  run.starts += 1;
2113
2490
  run.owed = false;
2491
+ run.owedWait = false;
2492
+ run.mutedUnheard = false;
2114
2493
  handle.playedOut = false;
2494
+ if (voiceRef.current.on) micControlRef.current?.dropLoopPress();
2115
2495
  showFollowUp(handle, { speaking: true });
2116
2496
  },
2117
2497
  [showFollowUp, whenQuiet]
@@ -2178,6 +2558,7 @@ function AvatarLiveView({
2178
2558
  }
2179
2559
  run.due = null;
2180
2560
  run.answers += 1;
2561
+ unmute(handle);
2181
2562
  const heardSince = run.starts !== starts;
2182
2563
  run.owed = !heardSince;
2183
2564
  const playing = !heardSince || run.voice || run.quiet !== null;
@@ -2199,7 +2580,7 @@ function AvatarLiveView({
2199
2580
  if (run.owed && run.heard && !run.voice) whenQuiet(handle, 0);
2200
2581
  })();
2201
2582
  },
2202
- [attend, showFollowUp, whenQuiet]
2583
+ [attend, showFollowUp, unmute, whenQuiet]
2203
2584
  );
2204
2585
  sendDueRef.current = sendDue;
2205
2586
  const readFollowUp = useCallback(
@@ -2236,12 +2617,19 @@ function AvatarLiveView({
2236
2617
  return;
2237
2618
  }
2238
2619
  if (result.kind === "refused") {
2620
+ if (result.consentVersion !== void 0) {
2621
+ setNoNotice(
2622
+ avatarConsentCopy(result.consentVersion, locale2)?.notice === "none"
2623
+ );
2624
+ }
2239
2625
  setMode(result.questions);
2240
2626
  go({ name: "refused", reason: result.reason });
2241
2627
  return;
2242
2628
  }
2243
2629
  setUsedToday(usedTodayHere());
2244
2630
  const copy = avatarConsentCopy(result.offer.consentVersion, locale2);
2631
+ setVoiceOffered(copy?.voice !== void 0);
2632
+ setNoNotice(copy?.notice === "none");
2245
2633
  if (copy === null || copy.botCheck !== result.offer.botCheck || copy.questions !== result.offer.questions) {
2246
2634
  setMode("suggested");
2247
2635
  go(INERT2);
@@ -2296,13 +2684,16 @@ function AvatarLiveView({
2296
2684
  heard: false,
2297
2685
  starts: 0,
2298
2686
  owed: true,
2687
+ owedWait: false,
2299
2688
  quiet: null,
2300
2689
  asked: null,
2301
2690
  due: null,
2302
2691
  retry: null,
2303
2692
  sent: /* @__PURE__ */ new Set([context.messageId]),
2304
2693
  seq: 0,
2305
- listenedAt: null
2694
+ listenedAt: null,
2695
+ muted: false,
2696
+ mutedUnheard: false
2306
2697
  } : null;
2307
2698
  sessionRef.current = {
2308
2699
  session,
@@ -2321,6 +2712,7 @@ function AvatarLiveView({
2321
2712
  followUps,
2322
2713
  done: false
2323
2714
  };
2715
+ if (audioRef.current !== null) audioRef.current.muted = false;
2324
2716
  setBlocked(false);
2325
2717
  setNow(received);
2326
2718
  go({
@@ -2576,6 +2968,7 @@ function AvatarLiveView({
2576
2968
  [applyAvailability, bindingHolds, cancelWaiting, exchangeNow, launch]
2577
2969
  );
2578
2970
  const stop = useCallback(() => {
2971
+ voiceOff(null, true);
2579
2972
  micCancelRef.current();
2580
2973
  const current = phaseRef.current;
2581
2974
  if (current.name === "waiting") {
@@ -2595,9 +2988,10 @@ function AvatarLiveView({
2595
2988
  if (current.name === "pending" || current.name === "live") {
2596
2989
  finish("stopped", "user");
2597
2990
  }
2598
- }, [cancelStart, cancelWaiting, finish, go, reoffer]);
2991
+ }, [cancelStart, cancelWaiting, finish, go, reoffer, voiceOff]);
2599
2992
  const leave = useCallback(
2600
2993
  (event) => {
2994
+ voiceOff(sessionRef.current === null ? null : lineNote("tapNext"), false);
2601
2995
  micCancelRef.current();
2602
2996
  const current = phaseRef.current;
2603
2997
  const reason = event === "user" || event === "chat" ? "user" : "page_hide";
@@ -2638,7 +3032,7 @@ function AvatarLiveView({
2638
3032
  return;
2639
3033
  }
2640
3034
  },
2641
- [cancelStart, cancelWaiting, finish, go, goLive, reoffer]
3035
+ [cancelStart, cancelWaiting, finish, go, goLive, reoffer, voiceOff]
2642
3036
  );
2643
3037
  useEffect(() => {
2644
3038
  if (!showing) {
@@ -2752,6 +3146,7 @@ function AvatarLiveView({
2752
3146
  const wasLive = phaseRef.current.name === "live";
2753
3147
  switch (event.type) {
2754
3148
  case "speaker-track":
3149
+ holdMute();
2755
3150
  if (event.kind !== "video") {
2756
3151
  handle.voiceArrived = true;
2757
3152
  attend();
@@ -2761,6 +3156,7 @@ function AvatarLiveView({
2761
3156
  if (!inSight()) goLive();
2762
3157
  return;
2763
3158
  case "playback-blocked":
3159
+ holdMute();
2764
3160
  setBlocked(event.blocked);
2765
3161
  handle.soundHeld = event.sound ?? event.blocked;
2766
3162
  attend();
@@ -2791,7 +3187,7 @@ function AvatarLiveView({
2791
3187
  return;
2792
3188
  }
2793
3189
  },
2794
- [attend, finish, goLive, heardVoice, inSight]
3190
+ [attend, finish, goLive, heardVoice, holdMute, inSight]
2795
3191
  );
2796
3192
  useEffect(() => {
2797
3193
  if (phase.name !== "pending") return;
@@ -2908,6 +3304,7 @@ function AvatarLiveView({
2908
3304
  wait?.check?.abort();
2909
3305
  const handle = sessionRef.current;
2910
3306
  sessionRef.current = null;
3307
+ if (audioRef.current !== null) audioRef.current.muted = false;
2911
3308
  if (handle && !handle.done) {
2912
3309
  handle.done = true;
2913
3310
  stopAttending(handle);
@@ -2924,7 +3321,8 @@ function AvatarLiveView({
2924
3321
  []
2925
3322
  );
2926
3323
  const any = questions === "any";
2927
- const textMode = any && (usedToday && (phase.name === "needs-answer" || phase.name === "consent" || phase.name === "declined" || phase.name === "ended") || phase.name === "refused" && phase.reason === "limits");
3324
+ const usedHere = usedToday && !voiceOffered;
3325
+ const textMode = any && (usedHere && (phase.name === "needs-answer" || phase.name === "consent" || phase.name === "declined" || phase.name === "ended") || phase.name === "refused" && phase.reason === "limits");
2928
3326
  textBoxRef.current = textMode;
2929
3327
  const askState = !any ? void 0 : textMode ? "text" : phase.name === "needs-answer" ? kind === "empty" ? "ready" : kind : phase.name;
2930
3328
  const followUp = phase.name === "live" ? phase.session.followUp : null;
@@ -2967,14 +3365,146 @@ function AvatarLiveView({
2967
3365
  return;
2968
3366
  }
2969
3367
  const run = handle.followUps;
3368
+ const voiced = micControlRef.current?.mode === "conversation";
2970
3369
  tell(handle, "interrupt");
3370
+ if (voiced) {
3371
+ run.muted = true;
3372
+ run.mutedUnheard = run.owed || run.owedWait;
3373
+ if (audioRef.current !== null) audioRef.current.muted = true;
3374
+ }
2971
3375
  if (run.quiet !== null) clearTimeout(run.quiet);
2972
3376
  run.quiet = null;
2973
3377
  run.owed = false;
2974
3378
  handle.playedOut = true;
2975
3379
  showFollowUp(handle, { speaking: false });
2976
- }, [showFollowUp, tell]);
2977
- const micSource = micChoice.kind === "browser" ? { kind: "browser", notice: micChoice.notice } : micChoice.kind === "stream" && base !== null ? {
3380
+ if (voiced && !run.voice) whenQuiet(handle, QUIET_PLAYED_OUT_MS);
3381
+ }, [showFollowUp, tell, whenQuiet]);
3382
+ const firstArmable = useCallback(() => {
3383
+ const current = phaseRef.current;
3384
+ return sessionRef.current === null && !textBoxRef.current && current.name === "needs-answer" && current.offer.copy.notice === "none" && exchangeNow().kind === "empty";
3385
+ }, [exchangeNow]);
3386
+ const armFirst = useCallback(
3387
+ (stage, askSeq) => {
3388
+ const current = phaseRef.current;
3389
+ if (current.name !== "needs-answer") return null;
3390
+ const { live: live2, locale: locale2 } = latest.current;
3391
+ unlockSound(audioRef.current);
3392
+ const module = loadModule(live2.load);
3393
+ module.catch(() => void 0);
3394
+ return {
3395
+ stage,
3396
+ consentVersion: current.offer.consentVersion,
3397
+ botCheck: current.offer.botCheck,
3398
+ site: live2.site,
3399
+ locale: locale2,
3400
+ path: pathNow(),
3401
+ module,
3402
+ askSeq
3403
+ };
3404
+ },
3405
+ [pathNow]
3406
+ );
3407
+ const onMicPress = useCallback(
3408
+ (gesture) => {
3409
+ const run = voiceRef.current;
3410
+ if (run.on && run.note === null && !gesture) return;
3411
+ const arm = gesture && firstArmable() ? armFirst("pressing", null) : null;
3412
+ voiceSet({
3413
+ on: true,
3414
+ note: null,
3415
+ ...gesture ? { echoes: 0, arm } : {}
3416
+ });
3417
+ },
3418
+ [armFirst, firstArmable, voiceSet]
3419
+ );
3420
+ const echoOf = useCallback(() => {
3421
+ const current = phaseRef.current;
3422
+ return current.name === "live" && current.session.followUp !== null && sessionRef.current?.followUps ? current.session.captions.text : null;
3423
+ }, []);
3424
+ const askRef = useRef(() => null);
3425
+ const askReasonRef = useRef(null);
3426
+ const draftRef = useRef("");
3427
+ draftRef.current = draft;
3428
+ askMaxLengthRef.current = askMaxLength;
3429
+ const autoSend = useCallback(
3430
+ (text) => {
3431
+ const question = text.trim();
3432
+ const refuse = () => {
3433
+ onMicText(text);
3434
+ voiceOff(lineNote("inBox"), false);
3435
+ };
3436
+ if (question === "" || askReasonRef.current !== null || sendingRef.current || question.length > askMaxLengthRef.current || draftRef.current.trim() !== "") {
3437
+ refuse();
3438
+ return;
3439
+ }
3440
+ const sending = askRef.current(question);
3441
+ if (sending === null) {
3442
+ refuse();
3443
+ return;
3444
+ }
3445
+ const arm = voiceRef.current.arm;
3446
+ voiceSet({
3447
+ echoes: 0,
3448
+ note: lineNote("sending"),
3449
+ ...arm === null ? {} : { arm: { ...arm, stage: "asked", askSeq: askSeqRef.current } }
3450
+ });
3451
+ void sending.then(
3452
+ (asked) => {
3453
+ if (!asked) {
3454
+ voiceOff(lineNote("off"), false);
3455
+ return;
3456
+ }
3457
+ const run = voiceRef.current;
3458
+ if (run.on && sameNote(run.note, lineNote("sending"))) {
3459
+ voiceSet({ note: lineNote("again") });
3460
+ }
3461
+ },
3462
+ () => voiceOff(lineNote("off"), false)
3463
+ );
3464
+ },
3465
+ [onMicText, voiceOff, voiceSet]
3466
+ );
3467
+ const onMicTurn = useCallback(
3468
+ (turn) => {
3469
+ const run = voiceRef.current;
3470
+ if (!run.on) return;
3471
+ if (turn.by === "pause" && turn.echo) {
3472
+ if (run.echoes >= MAX_ECHO_DISCARDS || run.arm !== null) {
3473
+ voiceOff(lineNote("off"), false);
3474
+ } else {
3475
+ voiceSet({ echoes: run.echoes + 1, note: null });
3476
+ }
3477
+ return;
3478
+ }
3479
+ if (turn.by === "pause") {
3480
+ autoSend(turn.text);
3481
+ return;
3482
+ }
3483
+ if (turn.by === "silence") {
3484
+ voiceOff(lineNote("noQuestion"), false);
3485
+ return;
3486
+ }
3487
+ if (turn.text.trim() !== "") {
3488
+ onMicText(turn.text);
3489
+ voiceOff(lineNote(turn.by === "limit" ? "atLimit" : "inBox"), false);
3490
+ return;
3491
+ }
3492
+ if (turn.by === "failed") {
3493
+ voiceOff(
3494
+ turn.gesture ? { kind: "failed", reason: turn.failure ?? "failed" } : lineNote("tapNext"),
3495
+ false
3496
+ );
3497
+ return;
3498
+ }
3499
+ voiceOff(lineNote("off"), false);
3500
+ },
3501
+ [autoSend, onMicText, voiceOff, voiceSet]
3502
+ );
3503
+ const micSource = micChoice.kind === "browser" ? {
3504
+ kind: "browser",
3505
+ notice: micChoice.notice,
3506
+ ...micChoice.pauseMs === void 0 ? {} : { pauseMs: micChoice.pauseMs }
3507
+ } : micChoice.kind === "stream" && base !== null ? {
2978
3508
  kind: "stream",
2979
3509
  base,
2980
3510
  offer: micChoice.offer,
@@ -2992,10 +3522,17 @@ function AvatarLiveView({
2992
3522
  locale,
2993
3523
  source: micSource,
2994
3524
  onText: onMicText,
2995
- onStart: onMicStart
3525
+ onStart: onMicStart,
3526
+ onTurn: onMicTurn,
3527
+ onPress: onMicPress,
3528
+ echoOf,
3529
+ interruptible: followUp?.speaking === true,
3530
+ note: voiceRef.current.note
2996
3531
  });
2997
3532
  micCancelRef.current = mic.cancel;
2998
- const micRecording = mic.recording;
3533
+ micControlRef.current = mic;
3534
+ askReasonRef.current = askReason;
3535
+ const micRecording = mic.mode === "conversation" ? mic.speaking : mic.recording;
2999
3536
  useEffect(() => {
3000
3537
  const handle = sessionRef.current;
3001
3538
  if (!micRecording || handle === null || handle.followUps === null || handle.done) {
@@ -3033,7 +3570,7 @@ function AvatarLiveView({
3033
3570
  const run = handle.followUps;
3034
3571
  const current = phaseRef.current;
3035
3572
  if (run === null || handle.done || current.name !== "live" || current.session.followUp?.pending !== null || !followUpBox(current, run, conversation2.getConversationToken())) {
3036
- return;
3573
+ return null;
3037
3574
  }
3038
3575
  sendingRef.current = true;
3039
3576
  run.seq += 1;
@@ -3041,17 +3578,17 @@ function AvatarLiveView({
3041
3578
  setDraft("");
3042
3579
  tell(handle, "asking");
3043
3580
  showFollowUp(handle, { pending: "asking", unread: null });
3044
- void conversation2.send(question, [], { surface: "AVATAR" }).then((added) => {
3581
+ return conversation2.send(question, [], { surface: "AVATAR" }).then((added) => {
3045
3582
  if (added === null) {
3046
3583
  setDraft((current2) => current2 === "" ? question : current2);
3047
3584
  }
3048
3585
  if (sessionRef.current !== handle || handle.done || run.seq !== seq) {
3049
- return;
3586
+ return false;
3050
3587
  }
3051
3588
  if (added === null || added.conversationToken !== run.token) {
3052
3589
  showFollowUp(handle, { pending: null });
3053
3590
  attend();
3054
- return;
3591
+ return false;
3055
3592
  }
3056
3593
  run.asked = {
3057
3594
  messageId: added.userMessageId,
@@ -3059,6 +3596,7 @@ function AvatarLiveView({
3059
3596
  answerId: null
3060
3597
  };
3061
3598
  showFollowUp(handle, { pending: "asking" });
3599
+ return true;
3062
3600
  }).finally(() => {
3063
3601
  sendingRef.current = false;
3064
3602
  });
@@ -3070,37 +3608,135 @@ function AvatarLiveView({
3070
3608
  const conversation2 = conversationRef.current;
3071
3609
  const question = text.trim();
3072
3610
  if (conversation2 === null || question === "" || sendingRef.current) {
3073
- return;
3611
+ return null;
3074
3612
  }
3075
3613
  const spoken = !textBoxRef.current;
3076
3614
  const handle = sessionRef.current;
3077
3615
  if (spoken && handle !== null && handle.followUps !== null) {
3078
- askFollowUp(handle, conversation2, question);
3079
- return;
3616
+ return askFollowUp(handle, conversation2, question);
3080
3617
  }
3081
- if (spoken && conversation2.messages.length > 0) return;
3618
+ if (spoken && conversation2.messages.length > 0) return null;
3082
3619
  sendingRef.current = true;
3083
3620
  setAskedHere(null);
3084
3621
  const seq = askSeqRef.current;
3085
3622
  setNote(null);
3086
3623
  setDraft("");
3087
- void conversation2.send(question, [], spoken ? { surface: "AVATAR" } : {}).then((added) => {
3624
+ return conversation2.send(question, [], spoken ? { surface: "AVATAR" } : {}).then((added) => {
3088
3625
  if (added === null) {
3089
3626
  setDraft((current) => current === "" ? question : current);
3090
3627
  }
3091
- if (!spoken || added === null || askSeqRef.current !== seq || added.conversationToken !== (conversationRef.current?.getConversationToken() ?? null)) {
3092
- return;
3628
+ if (added === null || spoken && (askSeqRef.current !== seq || added.conversationToken !== (conversationRef.current?.getConversationToken() ?? null))) {
3629
+ return false;
3093
3630
  }
3631
+ if (!spoken) return true;
3094
3632
  setAskedHere({
3095
3633
  messageId: added.userMessageId,
3096
3634
  token: added.conversationToken
3097
3635
  });
3636
+ return true;
3098
3637
  }).finally(() => {
3099
3638
  sendingRef.current = false;
3100
3639
  });
3101
3640
  },
3102
3641
  [askFollowUp, setAskedHere]
3103
3642
  );
3643
+ askRef.current = ask;
3644
+ const askTyped = useCallback(
3645
+ (text) => {
3646
+ voiceOff(null, false);
3647
+ const armable = firstArmable();
3648
+ const sending = ask(text);
3649
+ if (sending !== null && armable) {
3650
+ voiceSet({ arm: armFirst("asked", askSeqRef.current) });
3651
+ }
3652
+ return sending;
3653
+ },
3654
+ [armFirst, ask, firstArmable, voiceOff, voiceSet]
3655
+ );
3656
+ const onDraftChange = useCallback(
3657
+ (value) => {
3658
+ setDraft(value);
3659
+ const run = voiceRef.current;
3660
+ if (run.on || run.arm !== null) voiceOff(lineNote("off"), true);
3661
+ },
3662
+ [voiceOff]
3663
+ );
3664
+ const armVoiceStart = useCallback(() => {
3665
+ const arm = voiceRef.current.arm;
3666
+ if (arm === null || arm.stage !== "asked") return;
3667
+ const current = phaseRef.current;
3668
+ const exchange2 = exchangeNow();
3669
+ const drop = () => voiceSet({ arm: null });
3670
+ if (arm.askSeq !== askSeqRef.current || current.name !== "needs-answer" && current.name !== "consent") {
3671
+ drop();
3672
+ return;
3673
+ }
3674
+ if (sendingRef.current || exchange2.kind === "asking" && (!exchange2.askedHere || exchange2.pendingAnswerId === null)) {
3675
+ return;
3676
+ }
3677
+ const answerId2 = exchange2.kind === "asking" ? exchange2.pendingAnswerId : exchange2.kind === "answer" ? exchange2.answerId : null;
3678
+ const token2 = conversationRef.current?.getConversationToken() ?? null;
3679
+ const { live: live2, locale: locale2 } = latest.current;
3680
+ const path = pathNow();
3681
+ if (answerId2 === null || !exchange2.askedHere || exchange2.question === null || token2 === null || !isAvatarConversationToken(token2) || arm.consentVersion !== current.offer.consentVersion || arm.botCheck !== current.offer.botCheck || arm.site !== live2.site || arm.locale !== locale2 || arm.path !== path) {
3682
+ drop();
3683
+ return;
3684
+ }
3685
+ voiceSet({ arm: null });
3686
+ waitRef.current = {
3687
+ bound: {
3688
+ token: token2,
3689
+ questionId: exchange2.question.id,
3690
+ answerId: answerId2,
3691
+ site: live2.site,
3692
+ locale: locale2,
3693
+ path,
3694
+ consentVersion: arm.consentVersion,
3695
+ botCheck: arm.botCheck
3696
+ },
3697
+ module: arm.module,
3698
+ check: null
3699
+ };
3700
+ setNote(null);
3701
+ go({ name: "waiting", offer: current.offer });
3702
+ }, [exchangeNow, go, pathNow, voiceSet]);
3703
+ useEffect(() => {
3704
+ armVoiceStart();
3705
+ });
3706
+ useEffect(() => {
3707
+ const run = voiceRef.current;
3708
+ if (!run.on && run.arm === null) return;
3709
+ const current = phaseRef.current;
3710
+ const live2 = current.name === "live" ? current.session.followUp : null;
3711
+ const keeps = (mic.mode === "conversation" || !run.on) && !textMode && (current.name === "needs-answer" && (sendingRef.current || kind === "empty" || kind === "asking" || kind === "answer") || current.name === "consent" || current.name === "waiting" || current.name === "bot-check" || current.name === "creating" || current.name === "pending" || current.name === "live" && followUpAsks && live2 !== null && !live2.closed && (live2.unread === null || !SESSION_REFUSALS.has(live2.unread)));
3712
+ if (!keeps) voiceOff(lineNote("off"), true);
3713
+ });
3714
+ useEffect(() => {
3715
+ const run = voiceRef.current;
3716
+ const controls = micControlRef.current;
3717
+ const handle = sessionRef.current;
3718
+ const followUps = handle?.followUps ?? null;
3719
+ const current = phaseRef.current;
3720
+ if (!run.on || controls === null || controls.mode !== "conversation" || handle === null || followUps === null || handle.done || current.name !== "live") {
3721
+ return;
3722
+ }
3723
+ const shownFollowUp = current.session.followUp;
3724
+ if (shownFollowUp === null || shownFollowUp.speaking || shownFollowUp.pending !== null || shownFollowUp.closed || shownFollowUp.unread !== null && SESSION_REFUSALS.has(shownFollowUp.unread) || !followUpBox(
3725
+ current,
3726
+ followUps,
3727
+ conversationRef.current?.getConversationToken() ?? null
3728
+ )) {
3729
+ return;
3730
+ }
3731
+ if (sendingRef.current || followUps.asked !== null || followUps.due !== null || !playedOutNow(handle) || blockedRef.current || !inSight() || controls.pressing()) {
3732
+ return;
3733
+ }
3734
+ if (handle.endsAt - Date.now() < LISTEN_AGAIN_MIN_LEFT_MS) {
3735
+ voiceOff(lineNote("off"), false);
3736
+ return;
3737
+ }
3738
+ controls.listen();
3739
+ });
3104
3740
  useEffect(() => {
3105
3741
  const handle = sessionRef.current;
3106
3742
  const run = handle?.followUps ?? null;
@@ -3175,6 +3811,8 @@ function AvatarLiveView({
3175
3811
  check();
3176
3812
  }
3177
3813
  }, [check, exchangeNow, go, setAskedHere]);
3814
+ const voiceTalk = mic.mode === "conversation" && mic.button !== null && voiceWords !== null;
3815
+ const voiceArming = voiceRef.current.arm?.stage === "asked";
3178
3816
  useEffect(() => {
3179
3817
  if (!active) return;
3180
3818
  const focused = document.activeElement;
@@ -3190,8 +3828,12 @@ function AvatarLiveView({
3190
3828
  ]);
3191
3829
  const onPlaying = goLive;
3192
3830
  const enablePlayback = useCallback(() => {
3193
- void sessionRef.current?.joined?.enablePlayback().catch(() => void 0);
3194
- }, []);
3831
+ const room = sessionRef.current?.joined ?? null;
3832
+ if (room === null) return;
3833
+ const resumed = room.enablePlayback();
3834
+ holdMute();
3835
+ void resumed.catch(() => void 0).then(holdMute);
3836
+ }, [holdMute]);
3195
3837
  if (off || phase.name === "inert") {
3196
3838
  return (
3197
3839
  // `display: contents`: the earlier Avatar view's picture, line and button lay out in the pane exactly as before.
@@ -3310,7 +3952,7 @@ function AvatarLiveView({
3310
3952
  ]
3311
3953
  }
3312
3954
  );
3313
- const consentActions = (whenReady) => /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [
3955
+ const consentActions = (whenReady, label = whenReady ? S.startWhenReady : S.start) => /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [
3314
3956
  /* @__PURE__ */ jsx(
3315
3957
  "button",
3316
3958
  {
@@ -3319,7 +3961,7 @@ function AvatarLiveView({
3319
3961
  "data-boff-explore": whenReady ? "avatar-live-start-when-ready" : "avatar-live-start",
3320
3962
  className: primaryButton,
3321
3963
  onClick: whenReady ? startWhenReady : start,
3322
- children: whenReady ? S.startWhenReady : S.start
3964
+ children: label
3323
3965
  }
3324
3966
  ),
3325
3967
  /* @__PURE__ */ jsx(
@@ -3363,7 +4005,14 @@ function AvatarLiveView({
3363
4005
  className: "absolute inset-0 h-full w-full object-cover"
3364
4006
  }
3365
4007
  ),
3366
- any ? null : /* @__PURE__ */ jsx("audio", { ref: audioRef, "data-boff-explore": "avatar-live-audio" })
4008
+ any ? null : /* @__PURE__ */ jsx(
4009
+ "audio",
4010
+ {
4011
+ ref: audioRef,
4012
+ "data-boff-explore": "avatar-live-audio",
4013
+ onVolumeChange: holdMute
4014
+ }
4015
+ )
3367
4016
  ]
3368
4017
  }
3369
4018
  );
@@ -3496,20 +4145,24 @@ function AvatarLiveView({
3496
4145
  switch (shown.kind) {
3497
4146
  case "empty":
3498
4147
  status = note === "new-chat" ? S.newChatStarted : "";
3499
- lead = /* @__PURE__ */ jsx("p", { className: "text-base font-semibold", children: S.askHeadline });
4148
+ lead = /* @__PURE__ */ jsx("p", { className: "text-base font-semibold", children: voiceTalk && voiceWords !== null ? voiceWords.headline : S.askHeadline });
3500
4149
  box = "spoken";
3501
4150
  break;
3502
4151
  case "asking":
3503
4152
  case "answer": {
3504
- const card = shown.askedHere && shown.pendingAnswerId !== null;
3505
- status = note === "cancelled" ? S.armedCancelled : S.gettingAnswer;
3506
- if (card) media2 = stillPicture(true);
4153
+ const card = shown.askedHere && shown.pendingAnswerId !== null && !voiceArming;
4154
+ const words = phase.offer.copy.notice === "none" ? null : phase.offer.copy;
4155
+ status = note === "cancelled" ? words === null && voiceWords !== null ? voiceWords.cancelled : S.armedCancelled : S.gettingAnswer;
4156
+ if (card && words !== null) media2 = stillPicture(true);
3507
4157
  body2 = /* @__PURE__ */ jsxs(Fragment, { children: [
3508
4158
  questionLine,
3509
4159
  activityLine,
3510
- card ? consentCard(phase.offer.copy) : null
4160
+ card && words !== null ? consentCard(words) : null
3511
4161
  ] });
3512
- actions2 = card ? consentActions(true) : null;
4162
+ actions2 = card ? consentActions(
4163
+ true,
4164
+ words === null ? voiceWords?.start ?? S.start : S.startWhenReady
4165
+ ) : null;
3513
4166
  break;
3514
4167
  }
3515
4168
  case "text-only":
@@ -3541,6 +4194,16 @@ function AvatarLiveView({
3541
4194
  }
3542
4195
  break;
3543
4196
  case "consent":
4197
+ if (voiceArming) {
4198
+ status = S.armed;
4199
+ body2 = questionLine;
4200
+ break;
4201
+ }
4202
+ if (phase.offer.copy.notice === "none") {
4203
+ body2 = questionLine;
4204
+ actions2 = consentActions(false, voiceWords?.start ?? S.start);
4205
+ break;
4206
+ }
3544
4207
  media2 = stillPicture(true);
3545
4208
  body2 = /* @__PURE__ */ jsxs(Fragment, { children: [
3546
4209
  questionLine,
@@ -3632,7 +4295,7 @@ function AvatarLiveView({
3632
4295
  "aria-disabled": reason !== null ? "true" : void 0,
3633
4296
  className: "min-h-11 rounded-full border border-border bg-background px-3 py-2 text-start text-sm text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring aria-disabled:cursor-not-allowed aria-disabled:opacity-50 motion-reduce:transition-none",
3634
4297
  onClick: () => {
3635
- if (reason === null) ask(prompt);
4298
+ if (reason === null) void askTyped(prompt);
3636
4299
  },
3637
4300
  children: prompt
3638
4301
  },
@@ -3660,7 +4323,7 @@ function AvatarLiveView({
3660
4323
  /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: S.newChatNote })
3661
4324
  ] }) : null,
3662
4325
  chips,
3663
- box !== null && usedToday && phase.name !== "live" ? /* @__PURE__ */ jsx(
4326
+ box !== null && usedHere && phase.name !== "live" ? /* @__PURE__ */ jsx(
3664
4327
  "p",
3665
4328
  {
3666
4329
  "data-boff-explore": "avatar-live-used-today",
@@ -3674,13 +4337,16 @@ function AvatarLiveView({
3674
4337
  {
3675
4338
  value: draft,
3676
4339
  maxLength: askMaxLength,
3677
- onChange: setDraft,
3678
- onAsk: ask,
4340
+ onChange: onDraftChange,
4341
+ onAsk: askTyped,
3679
4342
  reason,
3680
4343
  label: S.askLabel,
3681
- placeholder: S.askPlaceholder,
4344
+ placeholder: box === "spoken" && voiceTalk && voiceWords !== null ? (
4345
+ // None while the mic listens: its line says so, and the box is at its narrowest then.
4346
+ mic.busy ? "" : voiceWords.placeholder
4347
+ ) : S.askPlaceholder,
3682
4348
  send,
3683
- footer: S.askFooter,
4349
+ footer: noNotice ? null : S.askFooter,
3684
4350
  noFiles: S.askNoFiles,
3685
4351
  hint: box === "spoken" ? S.needsAnswerAny : null,
3686
4352
  touchFirst,
@@ -3718,7 +4384,14 @@ function AvatarLiveView({
3718
4384
  actions2
3719
4385
  ] }),
3720
4386
  foot,
3721
- /* @__PURE__ */ jsx("audio", { ref: audioRef, "data-boff-explore": "avatar-live-audio" })
4387
+ /* @__PURE__ */ jsx(
4388
+ "audio",
4389
+ {
4390
+ ref: audioRef,
4391
+ "data-boff-explore": "avatar-live-audio",
4392
+ onVolumeChange: holdMute
4393
+ }
4394
+ )
3722
4395
  ]
3723
4396
  }
3724
4397
  );
@@ -3750,6 +4423,7 @@ function AvatarLiveView({
3750
4423
  actions = startOver ? startOverActions : continueButton;
3751
4424
  break;
3752
4425
  case "consent":
4426
+ if (phase.offer.copy.notice === "none") break;
3753
4427
  media = stillPicture(true);
3754
4428
  body = consentCard(phase.offer.copy);
3755
4429
  actions = consentActions(false);
@@ -3842,6 +4516,7 @@ function AskBox({
3842
4516
  const footerId = `${uid}-footer`;
3843
4517
  const [refusedFiles, setRefusedFiles] = useState(false);
3844
4518
  const empty = value.trim() === "";
4519
+ const describedBy = [hint === null ? null : hintId, footer === null ? null : footerId].filter((id) => id !== null).join(" ") || void 0;
3845
4520
  const submit = () => {
3846
4521
  if (reason !== null || empty) return;
3847
4522
  onAsk(value);
@@ -3884,7 +4559,7 @@ function AskBox({
3884
4559
  rows: 1,
3885
4560
  maxLength,
3886
4561
  "aria-label": label,
3887
- "aria-describedby": hint === null ? footerId : `${hintId} ${footerId}`,
4562
+ "aria-describedby": describedBy,
3888
4563
  placeholder,
3889
4564
  dir: "auto",
3890
4565
  "data-avatar-primary": touchFirst ? void 0 : "",
@@ -3937,7 +4612,7 @@ function AskBox({
3937
4612
  }
3938
4613
  ) : null,
3939
4614
  hint === null ? null : /* @__PURE__ */ jsx("p", { id: hintId, className: "sr-only", children: hint }),
3940
- /* @__PURE__ */ jsx("p", { id: footerId, className: "text-xs text-muted-foreground", children: footer })
4615
+ footer === null ? null : /* @__PURE__ */ jsx("p", { id: footerId, className: "text-xs text-muted-foreground", children: footer })
3941
4616
  ]
3942
4617
  }
3943
4618
  );