@burdenoff/website-sdk 2026.1105.1 → 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.
Files changed (36) hide show
  1. package/README.md +94 -1
  2. package/dist/avatar-live/index.d.mts +2 -2
  3. package/dist/avatar-live/index.d.ts +2 -2
  4. package/dist/avatar-live/index.js +11 -1
  5. package/dist/avatar-live/index.js.map +1 -1
  6. package/dist/avatar-live/index.mjs +11 -1
  7. package/dist/avatar-live/index.mjs.map +1 -1
  8. package/dist/avatar-live-view/index.d.mts +2 -2
  9. package/dist/avatar-live-view/index.d.ts +2 -2
  10. package/dist/avatar-live-view/index.js +915 -103
  11. package/dist/avatar-live-view/index.js.map +1 -1
  12. package/dist/avatar-live-view/index.mjs +908 -96
  13. package/dist/avatar-live-view/index.mjs.map +1 -1
  14. package/dist/avatar-mic/index.d.mts +187 -62
  15. package/dist/avatar-mic/index.d.ts +187 -62
  16. package/dist/avatar-mic/index.js +457 -5
  17. package/dist/avatar-mic/index.js.map +1 -1
  18. package/dist/avatar-mic/index.mjs +457 -6
  19. package/dist/avatar-mic/index.mjs.map +1 -1
  20. package/dist/{chunk-XIZE37IB.js → chunk-3AIAJ2L4.js} +2 -2
  21. package/dist/chunk-3AIAJ2L4.js.map +1 -0
  22. package/dist/{chunk-MVNN6LLO.mjs → chunk-JGU6JW4N.mjs} +2 -2
  23. package/dist/chunk-JGU6JW4N.mjs.map +1 -0
  24. package/dist/{explore-avatar-live-config-C-OwXNje.d.ts → explore-avatar-live-config-1zeXdMer.d.ts} +7 -4
  25. package/dist/{explore-avatar-live-config-OO0hvQCK.d.mts → explore-avatar-live-config-BxiE66Im.d.mts} +7 -4
  26. package/dist/index.d.mts +6 -5
  27. package/dist/index.d.ts +6 -5
  28. package/dist/index.js +5 -5
  29. package/dist/index.js.map +1 -1
  30. package/dist/index.mjs +1 -1
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/{types-CXqaLlNB.d.mts → types-CTUJNvQu.d.mts} +10 -0
  33. package/dist/{types-CXqaLlNB.d.ts → types-CTUJNvQu.d.ts} +10 -0
  34. package/package.json +1 -1
  35. package/dist/chunk-MVNN6LLO.mjs.map +0 -1
  36. package/dist/chunk-XIZE37IB.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { turnstileSiteKeyOf, isAvatarLiveLocaleReviewed, isAvatarSite, isAvatarVersion } from '../chunk-MVNN6LLO.mjs';
1
+ import { turnstileSiteKeyOf, isAvatarLiveLocaleReviewed, isAvatarSite, isAvatarVersion } from '../chunk-JGU6JW4N.mjs';
2
2
  import { answerDisplayText, fill } from '../chunk-JZS6FQT6.mjs';
3
3
  import { cn } from '../chunk-R7G6TWIS.mjs';
4
4
  import '../chunk-VNS5WDWO.mjs';
@@ -9,6 +9,8 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
9
9
  // src/components/explore-avatar-api.ts
10
10
  var AVAILABILITY_TIMEOUT_MS = 8e3;
11
11
  var CREATE_TIMEOUT_MS = 3e4;
12
+ var TURN_TIMEOUT_MS = 8e3;
13
+ var TURN_ANSWER_TIMEOUT_MS = 25e3;
12
14
  var MESSAGE_ID = /^[A-Za-z0-9_-]{1,64}$/;
13
15
  var STT_TOKEN = /^[A-Za-z0-9._~-]{1,4096}$/;
14
16
  var CONVERSATION_TOKEN = /^[A-Za-z0-9._~+/=-]{16,512}$/;
@@ -67,6 +69,17 @@ function readSttOffer(value) {
67
69
  }
68
70
  return { token, expiresAt };
69
71
  }
72
+ var MIC_MAX_PRESS_SECONDS = 30;
73
+ var UNREADABLE_MIC = { mode: "unreadable" };
74
+ function readMicOffer(value) {
75
+ if (!isRecord(value)) return UNREADABLE_MIC;
76
+ 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
+ return UNREADABLE_MIC;
80
+ }
81
+ return { mode: "browser", maxPressSeconds: MIC_MAX_PRESS_SECONDS };
82
+ }
70
83
  function readAvailability(status, body) {
71
84
  if (status !== 200 || !isRecord(body)) return INERT;
72
85
  const { available, reason } = body;
@@ -81,6 +94,7 @@ function readAvailability(status, body) {
81
94
  return INERT;
82
95
  }
83
96
  const stt = readSttOffer(body.stt);
97
+ const mic = Object.prototype.hasOwnProperty.call(body, "mic") ? readMicOffer(body.mic) : void 0;
84
98
  return {
85
99
  kind: "available",
86
100
  offer: {
@@ -88,7 +102,8 @@ function readAvailability(status, body) {
88
102
  maxSessionSeconds,
89
103
  botCheck,
90
104
  questions,
91
- ...stt === void 0 ? {} : { stt }
105
+ ...stt === void 0 ? {} : { stt },
106
+ ...mic === void 0 ? {} : { mic }
92
107
  }
93
108
  };
94
109
  }
@@ -113,6 +128,21 @@ function errorCode(body) {
113
128
  const { code } = body.error;
114
129
  return typeof code === "string" ? code : null;
115
130
  }
131
+ var MAX_FOLLOW_UP_ANSWERS = 16;
132
+ var MAX_FOLLOW_UP_SECONDS = 600;
133
+ var FOLLOW_UP_KEYS = ["idleSeconds", "maxAnswerWaitSeconds", "maxAnswers"];
134
+ function isWholeBetween(value, max) {
135
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 && value <= max;
136
+ }
137
+ function readFollowUps(value) {
138
+ if (!isRecord(value)) return void 0;
139
+ const keys = Object.keys(value).sort();
140
+ const { maxAnswers, idleSeconds, maxAnswerWaitSeconds } = value;
141
+ if (keys.length !== FOLLOW_UP_KEYS.length || keys.some((key, at) => key !== FOLLOW_UP_KEYS[at]) || !isWholeBetween(maxAnswers, MAX_FOLLOW_UP_ANSWERS) || !isWholeBetween(idleSeconds, MAX_FOLLOW_UP_SECONDS) || !isWholeBetween(maxAnswerWaitSeconds, MAX_FOLLOW_UP_SECONDS)) {
142
+ return void 0;
143
+ }
144
+ return { maxAnswers, idleSeconds, maxAnswerWaitSeconds };
145
+ }
116
146
  function readCreated(body) {
117
147
  if (!isRecord(body)) return FAILED;
118
148
  const { sessionId, endSecret } = body;
@@ -130,6 +160,7 @@ function readCreated(body) {
130
160
  if (typeof livekitUrl !== "string" || !livekitUrl.startsWith("wss://") || typeof participantToken !== "string" || participantToken === "" || typeof workerIdentity !== "string" || workerIdentity === "" || typeof locale !== "string" || typeof text !== "string") {
131
161
  return unusable;
132
162
  }
163
+ const followUps = readFollowUps(body.followUps);
133
164
  return {
134
165
  kind: "created",
135
166
  session: {
@@ -138,7 +169,8 @@ function readCreated(body) {
138
169
  endsAt,
139
170
  join: { livekitUrl, participantToken, workerIdentity },
140
171
  captions: { locale, text },
141
- excerpt: body.excerpt === true
172
+ excerpt: body.excerpt === true,
173
+ ...followUps === void 0 ? {} : { followUps }
142
174
  }
143
175
  };
144
176
  }
@@ -269,6 +301,96 @@ function sendEnd(target) {
269
301
  } catch {
270
302
  }
271
303
  }
304
+ var TURN_REFUSALS = /* @__PURE__ */ new Map([
305
+ ["no_time", "no_time"],
306
+ ["max_answers", "max_answers"],
307
+ ["not_eligible", "not_eligible"],
308
+ ["answer_claimed", "answer_claimed"],
309
+ ["limits", "limits"],
310
+ ["paused", "paused"],
311
+ ["busy", "busy"]
312
+ ]);
313
+ var MAX_TURN_RETRY_AFTER_SECONDS = 600;
314
+ function retryAfterOf(body) {
315
+ if (!isRecord(body) || !isRecord(body.error)) return void 0;
316
+ const { retryAfterSeconds } = body.error;
317
+ return isWholeBetween(retryAfterSeconds, MAX_TURN_RETRY_AFTER_SECONDS) ? retryAfterSeconds : void 0;
318
+ }
319
+ var TURN_KEY = /^[A-Za-z0-9_-]{8,64}$/;
320
+ var LOST = { kind: "failed", lost: true };
321
+ var TURN_FAILED = { kind: "failed", lost: false };
322
+ function readAccepted(body) {
323
+ if (!isRecord(body) || body.accepted !== true || !isRecord(body.captions)) {
324
+ return LOST;
325
+ }
326
+ const { locale, text } = body.captions;
327
+ if (typeof locale !== "string" || typeof text !== "string") return LOST;
328
+ return {
329
+ kind: "accepted",
330
+ captions: { locale, text },
331
+ excerpt: body.excerpt === true
332
+ };
333
+ }
334
+ function readTurn(kind, status, body) {
335
+ if (status === 200) {
336
+ if (kind === "answer") return readAccepted(body);
337
+ return isRecord(body) && body.ok === true ? { kind: "ok" } : LOST;
338
+ }
339
+ const code = errorCode(body);
340
+ const refusal = code === null ? void 0 : TURN_REFUSALS.get(code);
341
+ if (refusal !== void 0) {
342
+ const retryAfterSeconds = retryAfterOf(body);
343
+ return retryAfterSeconds === void 0 ? { kind: "refused", reason: refusal } : { kind: "refused", reason: refusal, retryAfterSeconds };
344
+ }
345
+ if (status === 404) return { kind: "gone" };
346
+ return status >= 500 ? LOST : TURN_FAILED;
347
+ }
348
+ function requestTurn(target) {
349
+ const { kind, messageId, idempotencyKey } = target;
350
+ const nonce = newIdempotencyKey();
351
+ if (!SESSION_ID.test(target.sessionId) || !END_SECRET.test(target.secret) || nonce === null || (kind === "answer" ? messageId === void 0 || !MESSAGE_ID.test(messageId) : messageId !== void 0 || idempotencyKey !== void 0) || idempotencyKey !== void 0 && !TURN_KEY.test(idempotencyKey)) {
352
+ return Promise.resolve(TURN_FAILED);
353
+ }
354
+ const payload = JSON.stringify({
355
+ secret: target.secret,
356
+ kind,
357
+ nonce,
358
+ ...messageId === void 0 ? {} : { messageId },
359
+ ...idempotencyKey === void 0 ? {} : { idempotencyKey }
360
+ });
361
+ const controller = new AbortController();
362
+ const answer = (async () => {
363
+ try {
364
+ const response = await fetch(
365
+ `${target.base}/sessions/${encodeURIComponent(target.sessionId)}/turn`,
366
+ {
367
+ method: "POST",
368
+ credentials: "omit",
369
+ cache: "no-store",
370
+ headers: { ...target.headers },
371
+ body: payload,
372
+ signal: controller.signal
373
+ }
374
+ );
375
+ return readTurn(kind, response.status, await readJson(response));
376
+ } catch {
377
+ return LOST;
378
+ }
379
+ })();
380
+ return new Promise((resolve) => {
381
+ const timer = setTimeout(
382
+ () => {
383
+ controller.abort();
384
+ resolve(LOST);
385
+ },
386
+ kind === "answer" ? TURN_ANSWER_TIMEOUT_MS : TURN_TIMEOUT_MS
387
+ );
388
+ void answer.then((result) => {
389
+ clearTimeout(timer);
390
+ resolve(result);
391
+ });
392
+ });
393
+ }
272
394
  var BOT_BINDING_PREFIX = "boff-web-avatar-cdata/v1";
273
395
  function webCryptoSubtle() {
274
396
  const subtle = globalThis.crypto?.subtle;
@@ -366,7 +488,14 @@ var EN = {
366
488
  usedToday: "You've had today's avatar session. Answers will show here as text.",
367
489
  needsAnswerAny: "Ask a question here, and the avatar reads the answer aloud.",
368
490
  startNewChatDetailAny: "The avatar reads aloud only the first answer in a new chat started in the Avatar tab, provided it passes the speech checks.",
369
- notEligibleAny: "The avatar can't read this answer aloud. The answer and sources are in Chat."
491
+ notEligibleAny: "The avatar can't read this answer aloud. The answer and sources are in Chat.",
492
+ followUpIdle: "The avatar ends after about thirty seconds without a question.",
493
+ followUpNoTime: "There isn't enough time left in this session to read that answer. It's in Chat.",
494
+ followUpMaxAnswers: "The avatar has read all the answers it can in this session. This one is in Chat.",
495
+ followUpLast: "That was the last answer the avatar can read in this session.",
496
+ followUpLimits: "The live avatar has reached its limit for now. This answer is in Chat.",
497
+ followUpPaused: "The live avatar is paused right now. This answer is in Chat.",
498
+ followUpUnread: "The avatar couldn't read that answer. It's in Chat."
370
499
  };
371
500
  var ZH = {
372
501
  checking: "\u6B63\u5728\u68C0\u67E5\u5B9E\u65F6\u865A\u62DF\u5F62\u8C61\u662F\u5426\u53EF\u7528\u2026",
@@ -427,7 +556,16 @@ var ZH = {
427
556
  usedToday: "\u60A8\u4ECA\u5929\u5DF2\u4F7F\u7528\u8FC7\u865A\u62DF\u5F62\u8C61\u3002\u56DE\u7B54\u5C06\u4EE5\u6587\u5B57\u663E\u793A\u5728\u8FD9\u91CC\u3002",
428
557
  needsAnswerAny: "\u5728\u8FD9\u91CC\u63D0\u95EE\uFF0C\u865A\u62DF\u5F62\u8C61\u4F1A\u6717\u8BFB\u56DE\u7B54\u3002",
429
558
  startNewChatDetailAny: "\u865A\u62DF\u5F62\u8C61\u53EA\u6717\u8BFB\u5728\u201C\u865A\u62DF\u5F62\u8C61\u201D\u6807\u7B7E\u9875\u4E2D\u5F00\u59CB\u7684\u65B0\u804A\u5929\u7684\u7B2C\u4E00\u4E2A\u56DE\u7B54\uFF0C\u4E14\u8BE5\u56DE\u7B54\u987B\u901A\u8FC7\u8BED\u97F3\u68C0\u67E5\u3002",
430
- notEligibleAny: "\u865A\u62DF\u5F62\u8C61\u65E0\u6CD5\u6717\u8BFB\u6B64\u56DE\u7B54\u3002\u56DE\u7B54\u548C\u6765\u6E90\u5728\u804A\u5929\u4E2D\u3002"
559
+ notEligibleAny: "\u865A\u62DF\u5F62\u8C61\u65E0\u6CD5\u6717\u8BFB\u6B64\u56DE\u7B54\u3002\u56DE\u7B54\u548C\u6765\u6E90\u5728\u804A\u5929\u4E2D\u3002",
560
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the lines of a session that reads the answers after the first
561
+ // (Stage D).
562
+ followUpIdle: "\u5982\u679C\u7EA6\u4E09\u5341\u79D2\u5185\u6CA1\u6709\u65B0\u95EE\u9898\uFF0C\u865A\u62DF\u5F62\u8C61\u5C31\u4F1A\u7ED3\u675F\u3002",
563
+ followUpNoTime: "\u672C\u6B21\u4F1A\u8BDD\u5269\u4F59\u65F6\u95F4\u4E0D\u8DB3\uFF0C\u65E0\u6CD5\u6717\u8BFB\u8BE5\u56DE\u7B54\u3002\u56DE\u7B54\u5728\u804A\u5929\u4E2D\u3002",
564
+ followUpMaxAnswers: "\u865A\u62DF\u5F62\u8C61\u5DF2\u6717\u8BFB\u672C\u6B21\u4F1A\u8BDD\u53EF\u6717\u8BFB\u7684\u5168\u90E8\u56DE\u7B54\u3002\u6B64\u56DE\u7B54\u5728\u804A\u5929\u4E2D\u3002",
565
+ followUpLast: "\u8FD9\u662F\u865A\u62DF\u5F62\u8C61\u5728\u672C\u6B21\u4F1A\u8BDD\u4E2D\u53EF\u6717\u8BFB\u7684\u6700\u540E\u4E00\u4E2A\u56DE\u7B54\u3002",
566
+ followUpLimits: "\u5B9E\u65F6\u865A\u62DF\u5F62\u8C61\u6682\u65F6\u5DF2\u8FBE\u5230\u4F7F\u7528\u4E0A\u9650\u3002\u6B64\u56DE\u7B54\u5728\u804A\u5929\u4E2D\u3002",
567
+ followUpPaused: "\u5B9E\u65F6\u865A\u62DF\u5F62\u8C61\u76EE\u524D\u5DF2\u6682\u505C\u3002\u6B64\u56DE\u7B54\u5728\u804A\u5929\u4E2D\u3002",
568
+ followUpUnread: "\u865A\u62DF\u5F62\u8C61\u65E0\u6CD5\u6717\u8BFB\u8BE5\u56DE\u7B54\u3002\u56DE\u7B54\u5728\u804A\u5929\u4E2D\u3002"
431
569
  };
432
570
  var HI = {
433
571
  checking: "\u091C\u093E\u0901\u091A \u0939\u094B \u0930\u0939\u0940 \u0939\u0948 \u0915\u093F \u0932\u093E\u0907\u0935 \u0905\u0935\u0924\u093E\u0930 \u0909\u092A\u0932\u092C\u094D\u0927 \u0939\u0948 \u092F\u093E \u0928\u0939\u0940\u0902\u2026",
@@ -488,7 +626,16 @@ var HI = {
488
626
  usedToday: "\u0906\u091C \u0915\u093E \u0905\u0935\u0924\u093E\u0930 \u0938\u0924\u094D\u0930 \u0906\u092A \u0932\u0947 \u091A\u0941\u0915\u0947 \u0939\u0948\u0902\u0964 \u0909\u0924\u094D\u0924\u0930 \u092F\u0939\u093E\u0901 \u091F\u0947\u0915\u094D\u0938\u094D\u091F \u0915\u0947 \u0930\u0942\u092A \u092E\u0947\u0902 \u0926\u093F\u0916\u0947\u0902\u0917\u0947\u0964",
489
627
  needsAnswerAny: "\u092F\u0939\u093E\u0901 \u092A\u094D\u0930\u0936\u094D\u0928 \u092A\u0942\u091B\u0947\u0902, \u0914\u0930 \u0905\u0935\u0924\u093E\u0930 \u0909\u0924\u094D\u0924\u0930 \u091C\u093C\u094B\u0930 \u0938\u0947 \u092A\u0922\u093C\u0947\u0917\u093E\u0964",
490
628
  startNewChatDetailAny: "\u0905\u0935\u0924\u093E\u0930 \u0915\u0947\u0935\u0932 \u0905\u0935\u0924\u093E\u0930 \u091F\u0948\u092C \u092E\u0947\u0902 \u0936\u0941\u0930\u0942 \u0915\u0940 \u0917\u0908 \u0928\u0908 \u091A\u0948\u091F \u0915\u093E \u092A\u0939\u0932\u093E \u0909\u0924\u094D\u0924\u0930 \u091C\u093C\u094B\u0930 \u0938\u0947 \u092A\u0922\u093C\u0924\u093E \u0939\u0948, \u092C\u0936\u0930\u094D\u0924\u0947 \u0935\u0939 \u0935\u093E\u0923\u0940 \u091C\u093E\u0901\u091A \u092E\u0947\u0902 \u092A\u093E\u0938 \u0939\u094B\u0964",
491
- notEligibleAny: "\u0905\u0935\u0924\u093E\u0930 \u092F\u0939 \u0909\u0924\u094D\u0924\u0930 \u091C\u093C\u094B\u0930 \u0938\u0947 \u0928\u0939\u0940\u0902 \u092A\u0922\u093C \u0938\u0915\u0924\u093E\u0964 \u0909\u0924\u094D\u0924\u0930 \u0914\u0930 \u0938\u094D\u0930\u094B\u0924 \u091A\u0948\u091F \u092E\u0947\u0902 \u0939\u0948\u0902\u0964"
629
+ notEligibleAny: "\u0905\u0935\u0924\u093E\u0930 \u092F\u0939 \u0909\u0924\u094D\u0924\u0930 \u091C\u093C\u094B\u0930 \u0938\u0947 \u0928\u0939\u0940\u0902 \u092A\u0922\u093C \u0938\u0915\u0924\u093E\u0964 \u0909\u0924\u094D\u0924\u0930 \u0914\u0930 \u0938\u094D\u0930\u094B\u0924 \u091A\u0948\u091F \u092E\u0947\u0902 \u0939\u0948\u0902\u0964",
630
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the lines of a session that reads the answers after the first
631
+ // (Stage D).
632
+ followUpIdle: "\u092C\u093F\u0928\u093E \u0915\u093F\u0938\u0940 \u092A\u094D\u0930\u0936\u094D\u0928 \u0915\u0947 \u0932\u0917\u092D\u0917 \u0924\u0940\u0938 \u0938\u0947\u0915\u0902\u0921 \u092C\u093E\u0926 \u0905\u0935\u0924\u093E\u0930 \u0938\u092E\u093E\u092A\u094D\u0924 \u0939\u094B \u091C\u093E\u0924\u093E \u0939\u0948\u0964",
633
+ followUpNoTime: "\u0907\u0938 \u0938\u0924\u094D\u0930 \u092E\u0947\u0902 \u0935\u0939 \u0909\u0924\u094D\u0924\u0930 \u092A\u0922\u093C\u0928\u0947 \u0915\u0947 \u0932\u093F\u090F \u092A\u0930\u094D\u092F\u093E\u092A\u094D\u0924 \u0938\u092E\u092F \u0928\u0939\u0940\u0902 \u092C\u091A\u093E \u0939\u0948\u0964 \u0909\u0924\u094D\u0924\u0930 \u091A\u0948\u091F \u092E\u0947\u0902 \u0939\u0948\u0964",
634
+ followUpMaxAnswers: "\u0905\u0935\u0924\u093E\u0930 \u0907\u0938 \u0938\u0924\u094D\u0930 \u092E\u0947\u0902 \u091C\u093F\u0924\u0928\u0947 \u0909\u0924\u094D\u0924\u0930 \u092A\u0922\u093C \u0938\u0915\u0924\u093E \u0939\u0948, \u0938\u092C \u092A\u0922\u093C \u091A\u0941\u0915\u093E \u0939\u0948\u0964 \u092F\u0939 \u0909\u0924\u094D\u0924\u0930 \u091A\u0948\u091F \u092E\u0947\u0902 \u0939\u0948\u0964",
635
+ followUpLast: "\u092F\u0939 \u0907\u0938 \u0938\u0924\u094D\u0930 \u092E\u0947\u0902 \u0905\u0935\u0924\u093E\u0930 \u0926\u094D\u0935\u093E\u0930\u093E \u092A\u0922\u093C\u093E \u091C\u093E \u0938\u0915\u0928\u0947 \u0935\u093E\u0932\u093E \u0905\u0902\u0924\u093F\u092E \u0909\u0924\u094D\u0924\u0930 \u0925\u093E\u0964",
636
+ followUpLimits: "\u0932\u093E\u0907\u0935 \u0905\u0935\u0924\u093E\u0930 \u0905\u092D\u0940 \u0905\u092A\u0928\u0940 \u0938\u0940\u092E\u093E \u0924\u0915 \u092A\u0939\u0941\u0901\u091A \u0917\u092F\u093E \u0939\u0948\u0964 \u092F\u0939 \u0909\u0924\u094D\u0924\u0930 \u091A\u0948\u091F \u092E\u0947\u0902 \u0939\u0948\u0964",
637
+ followUpPaused: "\u0932\u093E\u0907\u0935 \u0905\u0935\u0924\u093E\u0930 \u0905\u092D\u0940 \u0930\u0941\u0915\u093E \u0939\u0941\u0906 \u0939\u0948\u0964 \u092F\u0939 \u0909\u0924\u094D\u0924\u0930 \u091A\u0948\u091F \u092E\u0947\u0902 \u0939\u0948\u0964",
638
+ followUpUnread: "\u0905\u0935\u0924\u093E\u0930 \u0935\u0939 \u0909\u0924\u094D\u0924\u0930 \u0928\u0939\u0940\u0902 \u092A\u0922\u093C \u0938\u0915\u093E\u0964 \u0909\u0924\u094D\u0924\u0930 \u091A\u0948\u091F \u092E\u0947\u0902 \u0939\u0948\u0964"
492
639
  };
493
640
  var ES = {
494
641
  checking: "Comprobando si el avatar en vivo est\xE1 disponible\u2026",
@@ -549,7 +696,16 @@ var ES = {
549
696
  usedToday: "Ya has usado la sesi\xF3n de avatar de hoy. Las respuestas se mostrar\xE1n aqu\xED como texto.",
550
697
  needsAnswerAny: "Haz una pregunta aqu\xED y el avatar leer\xE1 la respuesta en voz alta.",
551
698
  startNewChatDetailAny: "El avatar solo lee en voz alta la primera respuesta de un chat nuevo iniciado en la pesta\xF1a Avatar, si supera las comprobaciones de voz.",
552
- notEligibleAny: "El avatar no puede leer esta respuesta en voz alta. La respuesta y las fuentes est\xE1n en el chat."
699
+ notEligibleAny: "El avatar no puede leer esta respuesta en voz alta. La respuesta y las fuentes est\xE1n en el chat.",
700
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the lines of a session that reads the answers after the first
701
+ // (Stage D).
702
+ followUpIdle: "El avatar termina tras unos treinta segundos sin preguntas.",
703
+ followUpNoTime: "No queda tiempo suficiente en esta sesi\xF3n para leer esa respuesta. Est\xE1 en el chat.",
704
+ followUpMaxAnswers: "El avatar ya ha le\xEDdo todas las respuestas que puede en esta sesi\xF3n. Esta est\xE1 en el chat.",
705
+ followUpLast: "Esa fue la \xFAltima respuesta que el avatar puede leer en esta sesi\xF3n.",
706
+ followUpLimits: "El avatar en vivo ha alcanzado su l\xEDmite por ahora. Esta respuesta est\xE1 en el chat.",
707
+ followUpPaused: "El avatar en vivo est\xE1 en pausa ahora mismo. Esta respuesta est\xE1 en el chat.",
708
+ followUpUnread: "El avatar no pudo leer esa respuesta. Est\xE1 en el chat."
553
709
  };
554
710
  var AR = {
555
711
  checking: "\u062C\u0627\u0631\u064D \u0627\u0644\u062A\u062D\u0642\u0642 \u0645\u0646 \u062A\u0648\u0641\u0631 \u0627\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0627\u0644\u0645\u0628\u0627\u0634\u0631\u2026",
@@ -610,7 +766,16 @@ var AR = {
610
766
  usedToday: "\u0644\u0642\u062F \u0627\u0633\u062A\u062E\u062F\u0645\u062A \u062C\u0644\u0633\u0629 \u0627\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0644\u0647\u0630\u0627 \u0627\u0644\u064A\u0648\u0645. \u0633\u062A\u0638\u0647\u0631 \u0627\u0644\u0625\u062C\u0627\u0628\u0627\u062A \u0647\u0646\u0627 \u0646\u0635\u064B\u0627.",
611
767
  needsAnswerAny: "\u0627\u0637\u0631\u062D \u0633\u0624\u0627\u0644\u064B\u0627 \u0647\u0646\u0627\u060C \u0648\u0633\u064A\u0642\u0631\u0623 \u0627\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0628\u0635\u0648\u062A \u0639\u0627\u0644\u064D.",
612
768
  startNewChatDetailAny: "\u064A\u0642\u0631\u0623 \u0627\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0628\u0635\u0648\u062A \u0639\u0627\u0644\u064D \u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0627\u0644\u0623\u0648\u0644\u0649 \u0641\u0642\u0637 \u0641\u064A \u062F\u0631\u062F\u0634\u0629 \u062C\u062F\u064A\u062F\u0629 \u0628\u062F\u0623\u062A \u0641\u064A \u0639\u0644\u0627\u0645\u0629 \u062A\u0628\u0648\u064A\u0628 \u0627\u0644\u0623\u0641\u0627\u062A\u0627\u0631\u060C \u0628\u0634\u0631\u0637 \u0623\u0646 \u062A\u062C\u062A\u0627\u0632 \u0641\u062D\u0648\u0635 \u0627\u0644\u0646\u0637\u0642.",
613
- notEligibleAny: "\u0644\u0627 \u064A\u0645\u0643\u0646 \u0644\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0642\u0631\u0627\u0621\u0629 \u0647\u0630\u0647 \u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0628\u0635\u0648\u062A \u0639\u0627\u0644\u064D. \u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0648\u0627\u0644\u0645\u0635\u0627\u062F\u0631 \u0645\u0648\u062C\u0648\u062F\u0629 \u0641\u064A \u0627\u0644\u062F\u0631\u062F\u0634\u0629."
769
+ notEligibleAny: "\u0644\u0627 \u064A\u0645\u0643\u0646 \u0644\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0642\u0631\u0627\u0621\u0629 \u0647\u0630\u0647 \u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0628\u0635\u0648\u062A \u0639\u0627\u0644\u064D. \u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0648\u0627\u0644\u0645\u0635\u0627\u062F\u0631 \u0645\u0648\u062C\u0648\u062F\u0629 \u0641\u064A \u0627\u0644\u062F\u0631\u062F\u0634\u0629.",
770
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the lines of a session that reads the answers after the first
771
+ // (Stage D).
772
+ followUpIdle: "\u064A\u0646\u062A\u0647\u064A \u0627\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0628\u0639\u062F \u0646\u062D\u0648 \u062B\u0644\u0627\u062B\u064A\u0646 \u062B\u0627\u0646\u064A\u0629 \u0645\u0646 \u062F\u0648\u0646 \u0633\u0624\u0627\u0644.",
773
+ followUpNoTime: "\u0644\u0645 \u064A\u062A\u0628\u0642\u064E\u0651 \u0648\u0642\u062A \u0643\u0627\u0641\u064D \u0641\u064A \u0647\u0630\u0647 \u0627\u0644\u062C\u0644\u0633\u0629 \u0644\u0642\u0631\u0627\u0621\u0629 \u062A\u0644\u0643 \u0627\u0644\u0625\u062C\u0627\u0628\u0629. \u0625\u0646\u0647\u0627 \u0641\u064A \u0627\u0644\u062F\u0631\u062F\u0634\u0629.",
774
+ followUpMaxAnswers: "\u0642\u0631\u0623 \u0627\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0643\u0644 \u0627\u0644\u0625\u062C\u0627\u0628\u0627\u062A \u0627\u0644\u062A\u064A \u064A\u0645\u0643\u0646\u0647 \u0642\u0631\u0627\u0621\u062A\u0647\u0627 \u0641\u064A \u0647\u0630\u0647 \u0627\u0644\u062C\u0644\u0633\u0629. \u0647\u0630\u0647 \u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0641\u064A \u0627\u0644\u062F\u0631\u062F\u0634\u0629.",
775
+ followUpLast: "\u0643\u0627\u0646\u062A \u062A\u0644\u0643 \u0622\u062E\u0631 \u0625\u062C\u0627\u0628\u0629 \u064A\u0645\u0643\u0646 \u0644\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0642\u0631\u0627\u0621\u062A\u0647\u0627 \u0641\u064A \u0647\u0630\u0647 \u0627\u0644\u062C\u0644\u0633\u0629.",
776
+ followUpLimits: "\u0628\u0644\u063A \u0627\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0627\u0644\u0645\u0628\u0627\u0634\u0631 \u062D\u062F\u0647 \u0627\u0644\u0622\u0646. \u0647\u0630\u0647 \u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0641\u064A \u0627\u0644\u062F\u0631\u062F\u0634\u0629.",
777
+ followUpPaused: "\u0627\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0627\u0644\u0645\u0628\u0627\u0634\u0631 \u0645\u062A\u0648\u0642\u0641 \u0645\u0624\u0642\u062A\u064B\u0627 \u0627\u0644\u0622\u0646. \u0647\u0630\u0647 \u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0641\u064A \u0627\u0644\u062F\u0631\u062F\u0634\u0629.",
778
+ followUpUnread: "\u062A\u0639\u0630\u0651\u0631 \u0639\u0644\u0649 \u0627\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0642\u0631\u0627\u0621\u0629 \u062A\u0644\u0643 \u0627\u0644\u0625\u062C\u0627\u0628\u0629. \u0625\u0646\u0647\u0627 \u0641\u064A \u0627\u0644\u062F\u0631\u062F\u0634\u0629."
614
779
  };
615
780
  var TA = {
616
781
  checking: "\u0BA8\u0BC7\u0BB0\u0BB2\u0BC8 \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BCD \u0B95\u0BBF\u0B9F\u0BC8\u0B95\u0BCD\u0B95\u0BBF\u0BB1\u0BA4\u0BBE \u0B8E\u0BA9\u0B9A\u0BCD \u0B9A\u0BB0\u0BBF\u0BAA\u0BBE\u0BB0\u0BCD\u0B95\u0BCD\u0B95\u0BBF\u0BB1\u0BA4\u0BC1\u2026",
@@ -671,7 +836,16 @@ var TA = {
671
836
  usedToday: "\u0B87\u0BA9\u0BCD\u0BB1\u0BC8\u0BAF \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BCD \u0B85\u0BAE\u0BB0\u0BCD\u0BB5\u0BC8 \u0BA8\u0BC0\u0B99\u0BCD\u0B95\u0BB3\u0BCD \u0BAA\u0BAF\u0BA9\u0BCD\u0BAA\u0B9F\u0BC1\u0BA4\u0BCD\u0BA4\u0BBF\u0BB5\u0BBF\u0B9F\u0BCD\u0B9F\u0BC0\u0BB0\u0BCD\u0B95\u0BB3\u0BCD. \u0BAA\u0BA4\u0BBF\u0BB2\u0BCD\u0B95\u0BB3\u0BCD \u0B87\u0B99\u0BCD\u0B95\u0BC7 \u0B89\u0BB0\u0BC8\u0BAF\u0BBE\u0B95\u0B95\u0BCD \u0B95\u0BBE\u0B9F\u0BCD\u0B9F\u0BAA\u0BCD\u0BAA\u0B9F\u0BC1\u0BAE\u0BCD.",
672
837
  needsAnswerAny: "\u0B87\u0B99\u0BCD\u0B95\u0BC7 \u0B92\u0BB0\u0BC1 \u0B95\u0BC7\u0BB3\u0BCD\u0BB5\u0BBF\u0BAF\u0BC8\u0B95\u0BCD \u0B95\u0BC7\u0BB3\u0BC1\u0B99\u0BCD\u0B95\u0BB3\u0BCD, \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BC8 \u0B89\u0BB0\u0B95\u0BCD\u0B95\u0BAA\u0BCD \u0BAA\u0B9F\u0BBF\u0B95\u0BCD\u0B95\u0BC1\u0BAE\u0BCD.",
673
838
  startNewChatDetailAny: "\u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BCD \u0BA4\u0BBE\u0BB5\u0BB2\u0BBF\u0BB2\u0BCD \u0BA4\u0BCA\u0B9F\u0B99\u0BCD\u0B95\u0BBF\u0BAF \u0BAA\u0BC1\u0BA4\u0BBF\u0BAF \u0B85\u0BB0\u0B9F\u0BCD\u0B9F\u0BC8\u0BAF\u0BBF\u0BA9\u0BCD \u0BAE\u0BC1\u0BA4\u0BB2\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BC8 \u0BAE\u0B9F\u0BCD\u0B9F\u0BC1\u0BAE\u0BC7, \u0B85\u0BA4\u0BC1 \u0BAA\u0BC7\u0B9A\u0BCD\u0B9A\u0BC1\u0B9A\u0BCD \u0B9A\u0BB0\u0BBF\u0BAA\u0BBE\u0BB0\u0BCD\u0BAA\u0BCD\u0BAA\u0BC1\u0B95\u0BB3\u0BBF\u0BB2\u0BCD \u0BA4\u0BC7\u0BB1\u0BBF\u0BA9\u0BBE\u0BB2\u0BCD, \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BCD \u0B89\u0BB0\u0B95\u0BCD\u0B95\u0BAA\u0BCD \u0BAA\u0B9F\u0BBF\u0B95\u0BCD\u0B95\u0BC1\u0BAE\u0BCD.",
674
- notEligibleAny: "\u0B87\u0BA8\u0BCD\u0BA4\u0BAA\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BC8 \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BBE\u0BB2\u0BCD \u0B89\u0BB0\u0B95\u0BCD\u0B95\u0BAA\u0BCD \u0BAA\u0B9F\u0BBF\u0B95\u0BCD\u0B95 \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BBE\u0BA4\u0BC1. \u0BAA\u0BA4\u0BBF\u0BB2\u0BC1\u0BAE\u0BCD \u0B86\u0BA4\u0BBE\u0BB0\u0B99\u0BCD\u0B95\u0BB3\u0BC1\u0BAE\u0BCD \u0B85\u0BB0\u0B9F\u0BCD\u0B9F\u0BC8\u0BAF\u0BBF\u0BB2\u0BCD \u0B89\u0BB3\u0BCD\u0BB3\u0BA9."
839
+ notEligibleAny: "\u0B87\u0BA8\u0BCD\u0BA4\u0BAA\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BC8 \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BBE\u0BB2\u0BCD \u0B89\u0BB0\u0B95\u0BCD\u0B95\u0BAA\u0BCD \u0BAA\u0B9F\u0BBF\u0B95\u0BCD\u0B95 \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BBE\u0BA4\u0BC1. \u0BAA\u0BA4\u0BBF\u0BB2\u0BC1\u0BAE\u0BCD \u0B86\u0BA4\u0BBE\u0BB0\u0B99\u0BCD\u0B95\u0BB3\u0BC1\u0BAE\u0BCD \u0B85\u0BB0\u0B9F\u0BCD\u0B9F\u0BC8\u0BAF\u0BBF\u0BB2\u0BCD \u0B89\u0BB3\u0BCD\u0BB3\u0BA9.",
840
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the lines of a session that reads the answers after the first
841
+ // (Stage D).
842
+ followUpIdle: "\u0B95\u0BC7\u0BB3\u0BCD\u0BB5\u0BBF \u0B87\u0BB2\u0BCD\u0BB2\u0BBE\u0BAE\u0BB2\u0BCD \u0B9A\u0BC1\u0BAE\u0BBE\u0BB0\u0BCD \u0BAE\u0BC1\u0BAA\u0BCD\u0BAA\u0BA4\u0BC1 \u0BB5\u0BBF\u0BA9\u0BBE\u0B9F\u0BBF\u0B95\u0BB3\u0BCD \u0B95\u0BB4\u0BBF\u0BA4\u0BCD\u0BA4\u0BC1 \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BCD \u0BAE\u0BC1\u0B9F\u0BBF\u0BB5\u0B9F\u0BC8\u0BAF\u0BC1\u0BAE\u0BCD.",
843
+ followUpNoTime: "\u0B85\u0BA8\u0BCD\u0BA4\u0BAA\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BC8\u0BAA\u0BCD \u0BAA\u0B9F\u0BBF\u0B95\u0BCD\u0B95 \u0B87\u0BA8\u0BCD\u0BA4 \u0B85\u0BAE\u0BB0\u0BCD\u0BB5\u0BBF\u0BB2\u0BCD \u0BAA\u0BCB\u0BA4\u0BC1\u0BAE\u0BBE\u0BA9 \u0BA8\u0BC7\u0BB0\u0BAE\u0BCD \u0B87\u0BB2\u0BCD\u0BB2\u0BC8. \u0B85\u0BA4\u0BC1 \u0B85\u0BB0\u0B9F\u0BCD\u0B9F\u0BC8\u0BAF\u0BBF\u0BB2\u0BCD \u0B89\u0BB3\u0BCD\u0BB3\u0BA4\u0BC1.",
844
+ followUpMaxAnswers: "\u0B87\u0BA8\u0BCD\u0BA4 \u0B85\u0BAE\u0BB0\u0BCD\u0BB5\u0BBF\u0BB2\u0BCD \u0BAA\u0B9F\u0BBF\u0B95\u0BCD\u0B95\u0B95\u0BCD\u0B95\u0BC2\u0B9F\u0BBF\u0BAF \u0B8E\u0BB2\u0BCD\u0BB2\u0BBE\u0BAA\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BCD\u0B95\u0BB3\u0BC8\u0BAF\u0BC1\u0BAE\u0BCD \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BCD \u0BAA\u0B9F\u0BBF\u0BA4\u0BCD\u0BA4\u0BC1\u0BB5\u0BBF\u0B9F\u0BCD\u0B9F\u0BA4\u0BC1. \u0B87\u0BA8\u0BCD\u0BA4\u0BAA\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BCD \u0B85\u0BB0\u0B9F\u0BCD\u0B9F\u0BC8\u0BAF\u0BBF\u0BB2\u0BCD \u0B89\u0BB3\u0BCD\u0BB3\u0BA4\u0BC1.",
845
+ followUpLast: "\u0B87\u0BA8\u0BCD\u0BA4 \u0B85\u0BAE\u0BB0\u0BCD\u0BB5\u0BBF\u0BB2\u0BCD \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BCD \u0BAA\u0B9F\u0BBF\u0B95\u0BCD\u0B95\u0B95\u0BCD\u0B95\u0BC2\u0B9F\u0BBF\u0BAF \u0B95\u0B9F\u0BC8\u0B9A\u0BBF \u0BAA\u0BA4\u0BBF\u0BB2\u0BCD \u0B85\u0BA4\u0BC1\u0BA4\u0BBE\u0BA9\u0BCD.",
846
+ followUpLimits: "\u0BA8\u0BC7\u0BB0\u0BB2\u0BC8 \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BCD \u0BA4\u0BB1\u0BCD\u0BAA\u0BCB\u0BA4\u0BC1 \u0B85\u0BA4\u0BA9\u0BCD \u0BB5\u0BB0\u0BAE\u0BCD\u0BAA\u0BC8 \u0B8E\u0B9F\u0BCD\u0B9F\u0BBF\u0BB5\u0BBF\u0B9F\u0BCD\u0B9F\u0BA4\u0BC1. \u0B87\u0BA8\u0BCD\u0BA4\u0BAA\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BCD \u0B85\u0BB0\u0B9F\u0BCD\u0B9F\u0BC8\u0BAF\u0BBF\u0BB2\u0BCD \u0B89\u0BB3\u0BCD\u0BB3\u0BA4\u0BC1.",
847
+ followUpPaused: "\u0BA8\u0BC7\u0BB0\u0BB2\u0BC8 \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BCD \u0BA4\u0BB1\u0BCD\u0BAA\u0BCB\u0BA4\u0BC1 \u0B87\u0B9F\u0BC8\u0BA8\u0BBF\u0BB1\u0BC1\u0BA4\u0BCD\u0BA4\u0BAA\u0BCD\u0BAA\u0B9F\u0BCD\u0B9F\u0BC1\u0BB3\u0BCD\u0BB3\u0BA4\u0BC1. \u0B87\u0BA8\u0BCD\u0BA4\u0BAA\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BCD \u0B85\u0BB0\u0B9F\u0BCD\u0B9F\u0BC8\u0BAF\u0BBF\u0BB2\u0BCD \u0B89\u0BB3\u0BCD\u0BB3\u0BA4\u0BC1.",
848
+ followUpUnread: "\u0B85\u0BA8\u0BCD\u0BA4\u0BAA\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BC8 \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BBE\u0BB2\u0BCD \u0BAA\u0B9F\u0BBF\u0B95\u0BCD\u0B95 \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8. \u0B85\u0BA4\u0BC1 \u0B85\u0BB0\u0B9F\u0BCD\u0B9F\u0BC8\u0BAF\u0BBF\u0BB2\u0BCD \u0B89\u0BB3\u0BCD\u0BB3\u0BA4\u0BC1."
675
849
  };
676
850
  var STRINGS = {
677
851
  en: EN,
@@ -684,6 +858,7 @@ var STRINGS = {
684
858
  function avatarLiveStrings(locale) {
685
859
  return STRINGS[locale] ?? EN;
686
860
  }
861
+ var BROWSER_MIC_NOTICE_VERSION = "web-mic-2026-11-21-c";
687
862
  var CONSENT = /* @__PURE__ */ new Map([
688
863
  [
689
864
  "web-consent-2026-11",
@@ -821,6 +996,91 @@ var CONSENT = /* @__PURE__ */ new Map([
821
996
  questions: "any"
822
997
  }
823
998
  }
999
+ ],
1000
+ [
1001
+ // Stage D (BOFF-9797), a DRAFT for the operator's review that no environment uses yet (global-speech-svc reserves the
1002
+ // same two versions): web-consent-2026-11-12-b with only its intro changed. The avatar reads the answer to each
1003
+ // question asked in the Avatar tab, in one session, waits about thirty seconds after an answer for the next question,
1004
+ // then ends; the session lasts at most three minutes, and an answer that does not fit the time left is read in part
1005
+ // (and said so) or not read, and stays in Chat. The notice files of record and their digests are a gitops change of
1006
+ // their own, made with the review.
1007
+ "web-consent-2026-11-19-d",
1008
+ {
1009
+ en: {
1010
+ title: "Live AI avatar (beta)",
1011
+ introProduct: null,
1012
+ 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.",
1013
+ 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.",
1014
+ storage: "Your microphone and camera are not used. 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.",
1015
+ privacyLink: "How we handle this",
1016
+ botCheck: "turnstile",
1017
+ questions: "any",
1018
+ followUps: true
1019
+ }
1020
+ }
1021
+ ],
1022
+ [
1023
+ // The words of web-consent-2026-11-19-d without "When you start, Cloudflare checks for bots.", for a server that runs
1024
+ // no bot check. A DRAFT, like it.
1025
+ "web-consent-2026-11-20-d",
1026
+ {
1027
+ en: {
1028
+ title: "Live AI avatar (beta)",
1029
+ introProduct: null,
1030
+ 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.",
1031
+ 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.",
1032
+ storage: "Your microphone and camera are not used. 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.",
1033
+ privacyLink: "How we handle this",
1034
+ botCheck: "off",
1035
+ questions: "any",
1036
+ followUps: true
1037
+ }
1038
+ }
1039
+ ],
1040
+ [
1041
+ // Stage C (BOFF-9797, the operator's decision of 2026-11-05): the mic through the browser's own speech recognition.
1042
+ // A DRAFT for the operator's privacy review that no environment uses yet (global-speech-svc reserves the same two
1043
+ // versions, and pairs them with its AVATAR_WEB_MIC=browser): web-consent-2026-11-19-d with three changes only. The
1044
+ // intro warns against personal details said into the microphone and says a tap on it stops the answer being read;
1045
+ // the processing says which speech service turns the voice into text (the browser's own, under its own terms, with
1046
+ // the vendors as examples) and that a live session is told of a tap and of a recording, never of the words; the
1047
+ // storage says the camera is not used and that the voice never reaches us, nor the text until Send, in place of
1048
+ // "Your microphone and camera are not used.", which is true only with no mic. The notice files of record and their
1049
+ // digests are a gitops change of their own, made with the review.
1050
+ "web-consent-2026-11-21-c",
1051
+ {
1052
+ en: {
1053
+ title: "Live AI avatar (beta)",
1054
+ introProduct: null,
1055
+ 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.",
1056
+ 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.",
1057
+ 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.",
1058
+ privacyLink: "How we handle this",
1059
+ botCheck: "turnstile",
1060
+ questions: "any",
1061
+ followUps: true,
1062
+ micNotice: BROWSER_MIC_NOTICE_VERSION
1063
+ }
1064
+ }
1065
+ ],
1066
+ [
1067
+ // The words of web-consent-2026-11-21-c without "When you start, Cloudflare checks for bots.", for a server that runs
1068
+ // no bot check. A DRAFT, like it.
1069
+ "web-consent-2026-11-22-c",
1070
+ {
1071
+ en: {
1072
+ title: "Live AI avatar (beta)",
1073
+ introProduct: null,
1074
+ 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.",
1075
+ 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.",
1076
+ 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.",
1077
+ privacyLink: "How we handle this",
1078
+ botCheck: "off",
1079
+ questions: "any",
1080
+ followUps: true,
1081
+ micNotice: BROWSER_MIC_NOTICE_VERSION
1082
+ }
1083
+ }
824
1084
  ]
825
1085
  ]);
826
1086
  function avatarConsentCopy(version, locale) {
@@ -865,6 +1125,22 @@ var MIC_NOTICE = /* @__PURE__ */ new Map([
865
1125
  privacyLink: "How we handle this"
866
1126
  }
867
1127
  }
1128
+ ],
1129
+ [
1130
+ // The browser's own speech recognition (Stage C): the browser's speech service, named by example where each is
1131
+ // true, under its own terms; nothing of the voice reaches us, nor the text before Send; a live session told only of
1132
+ // a tap and of a recording. No daily limits: nothing is counted on the server.
1133
+ BROWSER_MIC_NOTICE_VERSION,
1134
+ {
1135
+ en: {
1136
+ title: "Ask with your voice",
1137
+ 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.",
1138
+ storage: "We never receive your voice, and your camera is not used.",
1139
+ 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.",
1140
+ limits: "Each recording stops after thirty seconds at the latest.",
1141
+ privacyLink: "How we handle this"
1142
+ }
1143
+ }
868
1144
  ]
869
1145
  ]);
870
1146
  function avatarMicNotice(version, locale) {
@@ -875,10 +1151,12 @@ var PRIVACY_HREF = "/privacy#live-avatar";
875
1151
  var LIMIT_SECONDS = 30;
876
1152
  var TICK_MS = 250;
877
1153
  var IDLE = { name: "idle" };
878
- var FOR_GOOD = /* @__PURE__ */ new Set([
879
- "unavailable",
880
- "limit"
881
- ]);
1154
+ var EN_IN = { lang: "en-IN", fallbackLang: "en-US" };
1155
+ var BROWSER_MIC_LANGUAGES = { en: EN_IN };
1156
+ var FOR_GOOD = {
1157
+ stream: /* @__PURE__ */ new Set(["unavailable", "limit"]),
1158
+ browser: /* @__PURE__ */ new Set(["unsupported"])
1159
+ };
882
1160
  var FAILURE_TEXT = {
883
1161
  denied: "micDenied",
884
1162
  unsupported: "micUnsupported",
@@ -887,14 +1165,32 @@ var FAILURE_TEXT = {
887
1165
  busy: "micBusy",
888
1166
  failed: "micFailed"
889
1167
  };
1168
+ var loadedMic = null;
890
1169
  function loadAvatarMic() {
891
- return import('../avatar-mic/index.mjs').then((loaded) => ({
892
- createAvatarMic: loaded.createAvatarMic
893
- }));
1170
+ return import('../avatar-mic/index.mjs').then((loaded) => {
1171
+ const module = {
1172
+ createAvatarMic: loaded.createAvatarMic,
1173
+ createBrowserAvatarMic: loaded.createBrowserAvatarMic
1174
+ };
1175
+ loadedMic = module;
1176
+ return module;
1177
+ });
894
1178
  }
895
1179
  function canUseAvatarMic() {
896
1180
  return typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getUserMedia === "function" && typeof AudioContext !== "undefined" && typeof AudioWorkletNode !== "undefined" && typeof WebSocket !== "undefined";
897
1181
  }
1182
+ function canUseBrowserMic() {
1183
+ if (typeof window === "undefined" || typeof navigator === "undefined") {
1184
+ return false;
1185
+ }
1186
+ if (window.isSecureContext !== true) return false;
1187
+ const recognizer = Reflect.get(window, "SpeechRecognition") ?? Reflect.get(window, "webkitSpeechRecognition");
1188
+ if (typeof recognizer !== "function") return false;
1189
+ const agent = navigator.userAgent ?? "";
1190
+ if (/\bFirefox\//.test(agent) || /\bFxiOS\//.test(agent)) return false;
1191
+ return !("brave" in navigator);
1192
+ }
1193
+ var REPLAY_PRIVATE = "rr-block rr-mask";
898
1194
  function clock(seconds) {
899
1195
  const whole = Math.max(0, Math.floor(seconds));
900
1196
  return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, "0")}`;
@@ -905,36 +1201,51 @@ function withHeardText(current, heard, maxLength) {
905
1201
  const joined = current.trim() === "" ? text : `${current.replace(/\s+$/, "")} ${text}`;
906
1202
  return joined.slice(0, Math.max(0, maxLength));
907
1203
  }
1204
+ function noticeOf(source) {
1205
+ return source.kind === "stream" ? AVATAR_MIC_NOTICE_VERSION : source.notice;
1206
+ }
1207
+ function micKeyOf(source, locale) {
1208
+ return source.kind === "stream" ? `stream ${source.base}` : `browser ${BROWSER_MIC_LANGUAGES[locale]?.lang ?? ""}`;
1209
+ }
908
1210
  function useAvatarMic({
909
1211
  enabled,
910
1212
  locale,
911
- base,
912
- offer,
913
- refreshOffer,
914
- onText
1213
+ source,
1214
+ onText,
1215
+ onStart
915
1216
  }) {
916
1217
  const S = avatarMicStrings(locale);
917
- const copy = avatarMicNotice(AVATAR_MIC_NOTICE_VERSION, locale);
1218
+ const noticeVersion = source === null ? null : noticeOf(source);
1219
+ const copy = noticeVersion === null ? null : avatarMicNotice(noticeVersion, locale);
1220
+ const kind = source?.kind ?? null;
1221
+ const micKey = source === null ? null : micKeyOf(source, locale);
918
1222
  const uid = useId();
919
1223
  const noticeTitleId = `${uid}-mic-notice`;
920
- const [withdrawn, setWithdrawn] = useState(null);
921
- const [supported, setSupported] = useState(false);
922
- useEffect(() => setSupported(canUseAvatarMic()), []);
923
- const possible = enabled && supported && base !== null && S !== null && copy !== null;
924
- const offered = possible && withdrawn === null;
1224
+ const [withdrawn, setWithdrawn] = useState({});
1225
+ const [supported, setSupported] = useState({ stream: false, browser: false });
1226
+ useEffect(
1227
+ () => setSupported({ stream: canUseAvatarMic(), browser: canUseBrowserMic() }),
1228
+ []
1229
+ );
1230
+ 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;
1232
+ const withdrawnReason = kind === null ? null : withdrawn[kind] ?? null;
1233
+ const offered = possible && withdrawnReason === null;
925
1234
  const [state, setState] = useState(IDLE);
926
1235
  const [noticeOpen, setNoticeOpen] = useState(false);
927
- const agreedRef = useRef(false);
1236
+ const agreedRef = useRef(null);
928
1237
  const [loading2, setLoading] = useState(false);
929
1238
  const [loadFailed, setLoadFailed] = useState(false);
930
1239
  const [now, setNow] = useState(() => Date.now());
931
- const micRef = useRef(null);
1240
+ const micRef = useRef(
1241
+ null
1242
+ );
932
1243
  const seqRef = useRef(0);
933
1244
  const buttonRef = useRef(null);
934
1245
  const allowRef = useRef(null);
935
- const usedEpochRef = useRef(0);
936
- const latest = useRef({ offer, refreshOffer, onText, base });
937
- latest.current = { offer, refreshOffer, onText, base };
1246
+ const usedRef = useRef(null);
1247
+ const latest = useRef({ source, locale, noticeVersion, onText, onStart });
1248
+ latest.current = { source, locale, noticeVersion, onText, onStart };
938
1249
  const cancel = useCallback(() => {
939
1250
  seqRef.current += 1;
940
1251
  setLoading(false);
@@ -945,6 +1256,12 @@ function useAvatarMic({
945
1256
  useEffect(() => {
946
1257
  if (!offered) cancel();
947
1258
  }, [offered, cancel]);
1259
+ const keyRef = useRef(micKey);
1260
+ useEffect(() => {
1261
+ if (keyRef.current === micKey) return;
1262
+ keyRef.current = micKey;
1263
+ cancel();
1264
+ }, [micKey, cancel]);
948
1265
  useEffect(
949
1266
  () => () => {
950
1267
  seqRef.current += 1;
@@ -952,29 +1269,55 @@ function useAvatarMic({
952
1269
  },
953
1270
  []
954
1271
  );
955
- const micFor = useCallback(async (target) => {
956
- const held = micRef.current;
957
- if (held !== null && held.base === target) return held.mic;
958
- held?.mic.cancel();
959
- const loaded = await loadAvatarMic();
960
- const mic = loaded.createAvatarMic({
961
- base: target,
962
- notice: AVATAR_MIC_NOTICE_VERSION,
963
- offer: () => latest.current.offer,
964
- refreshOffer: () => latest.current.refreshOffer(),
965
- onState: setState
966
- });
967
- micRef.current = { base: target, mic };
968
- return mic;
969
- }, []);
1272
+ const micOf = useCallback(
1273
+ (loaded, wanted) => {
1274
+ const key = micKeyOf(wanted, latest.current.locale);
1275
+ const held = micRef.current;
1276
+ if (held !== null && held.key === key) return held.mic;
1277
+ held?.mic.cancel();
1278
+ let mic;
1279
+ if (wanted.kind === "stream") {
1280
+ const stream = () => {
1281
+ const current = latest.current.source;
1282
+ return current?.kind === "stream" ? current : null;
1283
+ };
1284
+ mic = loaded.createAvatarMic({
1285
+ base: wanted.base,
1286
+ notice: AVATAR_MIC_NOTICE_VERSION,
1287
+ offer: () => stream()?.offer ?? null,
1288
+ refreshOffer: () => stream()?.refreshOffer() ?? Promise.resolve(null),
1289
+ onState: setState
1290
+ });
1291
+ } else {
1292
+ const language = BROWSER_MIC_LANGUAGES[latest.current.locale] ?? EN_IN;
1293
+ mic = loaded.createBrowserAvatarMic({
1294
+ lang: language.lang,
1295
+ fallbackLang: language.fallbackLang,
1296
+ maxPressMs: LIMIT_SECONDS * 1e3,
1297
+ onState: setState
1298
+ });
1299
+ }
1300
+ micRef.current = { key, kind: wanted.kind, mic };
1301
+ return mic;
1302
+ },
1303
+ []
1304
+ );
970
1305
  const press = useCallback(() => {
971
- const target = latest.current.base;
972
- if (target === null) return;
1306
+ const wanted = latest.current.source;
1307
+ if (wanted === null) return;
973
1308
  seqRef.current += 1;
974
1309
  const seq = seqRef.current;
975
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
+ }
976
1318
  setLoading(true);
977
- void micFor(target).then(
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(
978
1321
  (mic) => {
979
1322
  if (seqRef.current !== seq) return;
980
1323
  setLoading(false);
@@ -986,8 +1329,9 @@ function useAvatarMic({
986
1329
  setLoadFailed(true);
987
1330
  }
988
1331
  );
989
- }, [micFor]);
1332
+ }, [micOf]);
990
1333
  const recording = loading2 || state.name === "starting" || state.name === "listening";
1334
+ const finishing = kind === "browser" && state.name === "finishing";
991
1335
  const tap = useCallback(() => {
992
1336
  if (state.name === "listening" || state.name === "starting") {
993
1337
  micRef.current?.mic.stop();
@@ -997,15 +1341,26 @@ function useAvatarMic({
997
1341
  cancel();
998
1342
  return;
999
1343
  }
1000
- if (!agreedRef.current) {
1344
+ if (finishing) return;
1345
+ latest.current.onStart?.();
1346
+ if (agreedRef.current !== latest.current.noticeVersion) {
1001
1347
  setNoticeOpen(true);
1002
- void loadAvatarMic().catch(() => void 0);
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
+ );
1003
1358
  return;
1004
1359
  }
1005
1360
  press();
1006
- }, [cancel, loading2, press, state.name]);
1361
+ }, [cancel, finishing, loading2, micOf, press, state.name]);
1007
1362
  const allow = useCallback(() => {
1008
- agreedRef.current = true;
1363
+ agreedRef.current = latest.current.noticeVersion;
1009
1364
  setNoticeOpen(false);
1010
1365
  buttonRef.current?.focus();
1011
1366
  press();
@@ -1025,17 +1380,27 @@ function useAvatarMic({
1025
1380
  return () => clearInterval(timer);
1026
1381
  }, [listeningSince]);
1027
1382
  useEffect(() => {
1028
- if (state.name === "done" && state.epoch !== usedEpochRef.current) {
1029
- usedEpochRef.current = state.epoch;
1383
+ if (state.name === "done" && state !== usedRef.current) {
1384
+ usedRef.current = state;
1030
1385
  if (state.text !== "") latest.current.onText(state.text);
1031
- } else if (state.name === "failed" && FOR_GOOD.has(state.reason)) {
1032
- setWithdrawn(state.reason);
1386
+ return;
1387
+ }
1388
+ const pressed = micRef.current?.kind;
1389
+ if (state.name === "failed" && pressed !== void 0 && FOR_GOOD[pressed].has(state.reason)) {
1390
+ const reason = state.reason;
1391
+ setWithdrawn((held) => ({ ...held, [pressed]: reason }));
1033
1392
  }
1034
1393
  }, [state]);
1035
1394
  if (!possible || S === null || copy === null) {
1036
- return { button: null, notice: null, lines: null, cancel };
1395
+ return {
1396
+ button: null,
1397
+ notice: null,
1398
+ lines: null,
1399
+ cancel,
1400
+ recording: false
1401
+ };
1037
1402
  }
1038
- 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]] : "";
1403
+ 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]] : "";
1039
1404
  const heard = offered && (state.name === "listening" || state.name === "finishing") ? state.text : "";
1040
1405
  const button = !offered ? null : /* @__PURE__ */ jsx(
1041
1406
  "button",
@@ -1044,12 +1409,15 @@ function useAvatarMic({
1044
1409
  type: "button",
1045
1410
  "aria-label": recording ? S.micStop : S.micStart,
1046
1411
  "aria-pressed": recording,
1412
+ "aria-disabled": finishing ? true : void 0,
1047
1413
  title: recording ? S.micStop : S.micStart,
1048
1414
  "data-boff-explore": "avatar-live-mic",
1049
1415
  "data-mic": loading2 ? "starting" : state.name,
1416
+ "data-mic-source": kind ?? void 0,
1050
1417
  className: cn(
1051
1418
  "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",
1052
- recording ? "border-primary bg-primary text-primary-foreground" : "border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground"
1419
+ recording ? "border-primary bg-primary text-primary-foreground" : "border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
1420
+ finishing && "cursor-wait opacity-50"
1053
1421
  ),
1054
1422
  onClick: tap,
1055
1423
  children: recording ? /* @__PURE__ */ jsx(Square, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx(Mic, { "aria-hidden": "true", className: "h-4 w-4" })
@@ -1132,12 +1500,22 @@ function useAvatarMic({
1132
1500
  {
1133
1501
  "data-boff-explore": "avatar-live-mic-heard",
1134
1502
  dir: "auto",
1135
- className: "line-clamp-3 break-words text-sm italic text-muted-foreground",
1503
+ className: cn(
1504
+ // rrweb's default block and mask classes: a site's session replay records none of what is heard.
1505
+ REPLAY_PRIVATE,
1506
+ "line-clamp-3 break-words text-sm italic text-muted-foreground"
1507
+ ),
1136
1508
  children: heard
1137
1509
  }
1138
1510
  )
1139
1511
  ] });
1140
- return { button, notice, lines, cancel };
1512
+ return {
1513
+ button,
1514
+ notice,
1515
+ lines,
1516
+ cancel,
1517
+ recording: offered && state.name === "listening"
1518
+ };
1141
1519
  }
1142
1520
 
1143
1521
  // src/components/explore-avatar-turnstile.ts
@@ -1260,6 +1638,13 @@ var END_GRACE_MS = 1e4;
1260
1638
  var CLOCK_TOLERANCE_MS = 5e3;
1261
1639
  var UNHEARD_GRACE_MS = 5e3;
1262
1640
  var PLAYOUT_TAIL_MS = 5e3;
1641
+ var QUIET_PLAYED_OUT_MS = 2500;
1642
+ var OWED_VOICE_WAIT_MS = 15e3;
1643
+ var LISTENING_MARK_MS = 1e4;
1644
+ var LISTENING_MARK_SPACING_MS = 5e3;
1645
+ var BUSY_RETRY_MS = 2e3;
1646
+ var BUSY_RETRY_MAX_MS = 8e3;
1647
+ var MAX_BUSY_RETRIES = 24;
1263
1648
  var SERVICE_MAX_AGE_MS = 15 * 6e4;
1264
1649
  var ANSWER_OFFER_MS = SERVICE_MAX_AGE_MS - 6e4;
1265
1650
  var MAX_TIMER_MS = 2147483647;
@@ -1274,6 +1659,12 @@ function stopAttending(handle) {
1274
1659
  if (handle.tail !== null) clearTimeout(handle.tail);
1275
1660
  handle.unheard = null;
1276
1661
  handle.tail = null;
1662
+ const run = handle.followUps;
1663
+ if (run === null) return;
1664
+ if (run.quiet !== null) clearTimeout(run.quiet);
1665
+ if (run.retry !== null) clearTimeout(run.retry);
1666
+ run.quiet = null;
1667
+ run.retry = null;
1277
1668
  }
1278
1669
  var REFUSAL_TEXT = {
1279
1670
  busy: "busy",
@@ -1284,6 +1675,18 @@ var REFUSAL_TEXT = {
1284
1675
  not_eligible: "notEligible",
1285
1676
  answer_claimed: "answerClaimed"
1286
1677
  };
1678
+ var FOLLOW_UP_TEXT = {
1679
+ no_time: "followUpNoTime",
1680
+ max_answers: "followUpMaxAnswers",
1681
+ // The same line as a first answer the checks refuse: it stays in Chat as text.
1682
+ not_eligible: "notSpoken",
1683
+ answer_claimed: "answerClaimed",
1684
+ limits: "followUpLimits",
1685
+ paused: "followUpPaused",
1686
+ busy: "followUpUnread",
1687
+ answer_failed: "answerFailed",
1688
+ failed: "followUpUnread"
1689
+ };
1287
1690
  var EMPTY = {
1288
1691
  kind: "empty",
1289
1692
  answerId: null,
@@ -1358,10 +1761,21 @@ function inFlight(conversation) {
1358
1761
  return conversation?.phase === "sending" || conversation?.phase === "streaming";
1359
1762
  }
1360
1763
  function acceptsQuestion(slot) {
1361
- return slot.any && !slot.textMode && slot.phase === "needs-answer" && slot.kind === "empty" && slot.reason === null;
1764
+ if (!slot.any || slot.textMode || slot.reason !== null) return false;
1765
+ return slot.phase === "needs-answer" ? slot.kind === "empty" : slot.phase === "live" && slot.followUp;
1362
1766
  }
1363
- function micOfferOf(result) {
1364
- return result.kind === "available" && result.offer.stt !== void 0 ? { ...result.offer.stt, receivedAt: Date.now() } : null;
1767
+ function followUpBox(phase, run, token) {
1768
+ return phase.name === "live" && run !== null && phase.session.followUp !== null && !phase.session.followUp.closed && token === run.token;
1769
+ }
1770
+ var NO_MIC = { kind: "none" };
1771
+ function micChoiceOf(result, locale) {
1772
+ if (result.kind !== "available") return NO_MIC;
1773
+ 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;
1777
+ }
1778
+ return offer.stt !== void 0 && micNotice === void 0 ? { kind: "stream", offer: { ...offer.stt, receivedAt: Date.now() } } : NO_MIC;
1365
1779
  }
1366
1780
  function utcToday() {
1367
1781
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -1511,8 +1925,9 @@ function AvatarLiveView({
1511
1925
  const waitRef = useRef(null);
1512
1926
  const sendingRef = useRef(false);
1513
1927
  const textBoxRef = useRef(false);
1514
- const [micOffer, setMicOffer] = useState(null);
1928
+ const [micChoice, setMicChoice] = useState(NO_MIC);
1515
1929
  const micCancelRef = useRef(() => void 0);
1930
+ const sendDueRef = useRef(() => void 0);
1516
1931
  const checkRef = useRef(null);
1517
1932
  const askedLocale = useRef(locale);
1518
1933
  const startRef = useRef(null);
@@ -1601,10 +2016,13 @@ function AvatarLiveView({
1601
2016
  if (!handle || handle.done || latest.current.live.endUnattended !== true) {
1602
2017
  return;
1603
2018
  }
1604
- const current = phaseRef.current.name;
1605
- if (current !== "pending" && current !== "live") return;
2019
+ const shown2 = phaseRef.current;
2020
+ if (shown2.name !== "pending" && shown2.name !== "live") return;
2021
+ const current = shown2.name;
1606
2022
  const away = !inSight();
1607
- if (away && handle.playedOut && current === "live") {
2023
+ const followUp2 = shown2.session.followUp;
2024
+ const asking = followUp2 !== null && followUp2.pending !== null;
2025
+ if (away && handle.playedOut && current === "live" && !asking) {
1608
2026
  finish("finished", "user");
1609
2027
  return;
1610
2028
  }
@@ -1623,6 +2041,184 @@ function AvatarLiveView({
1623
2041
  }
1624
2042
  }, UNHEARD_GRACE_MS);
1625
2043
  }, [finish, inSight]);
2044
+ const showFollowUp = useCallback(
2045
+ (handle, next, spoken) => {
2046
+ const current = phaseRef.current;
2047
+ if (sessionRef.current !== handle || handle.done || current.name !== "pending" && current.name !== "live" || current.session.followUp === null) {
2048
+ return;
2049
+ }
2050
+ const session = {
2051
+ ...current.session,
2052
+ ...spoken,
2053
+ followUp: { ...current.session.followUp, ...next }
2054
+ };
2055
+ go(
2056
+ current.name === "live" ? { name: "live", session } : { name: "pending", session }
2057
+ );
2058
+ },
2059
+ [go]
2060
+ );
2061
+ const tell = useCallback(
2062
+ (handle, kind2) => {
2063
+ if (handle.done || handle.followUps === null) return;
2064
+ void requestTurn({
2065
+ base: handle.base,
2066
+ headers: handle.headers,
2067
+ sessionId: handle.session.sessionId,
2068
+ secret: handle.session.endSecret,
2069
+ kind: kind2
2070
+ });
2071
+ },
2072
+ []
2073
+ );
2074
+ const whenQuiet = useCallback(
2075
+ (handle, delay) => {
2076
+ const run = handle.followUps;
2077
+ if (run === null) return;
2078
+ const wait = (ms) => {
2079
+ if (run.quiet !== null) clearTimeout(run.quiet);
2080
+ run.quiet = setTimeout(() => {
2081
+ run.quiet = null;
2082
+ if (sessionRef.current !== handle || handle.done || run.voice) {
2083
+ return;
2084
+ }
2085
+ if (run.owed) {
2086
+ run.owed = false;
2087
+ wait(OWED_VOICE_WAIT_MS);
2088
+ return;
2089
+ }
2090
+ handle.playedOut = true;
2091
+ showFollowUp(handle, { speaking: false });
2092
+ attend();
2093
+ sendDueRef.current(handle);
2094
+ }, ms);
2095
+ };
2096
+ wait(delay);
2097
+ },
2098
+ [attend, showFollowUp]
2099
+ );
2100
+ const heardVoice = useCallback(
2101
+ (handle, speaking) => {
2102
+ const run = handle.followUps;
2103
+ if (run === null) return;
2104
+ run.voice = speaking;
2105
+ if (!speaking) {
2106
+ whenQuiet(handle, QUIET_PLAYED_OUT_MS);
2107
+ return;
2108
+ }
2109
+ if (run.quiet !== null) clearTimeout(run.quiet);
2110
+ run.quiet = null;
2111
+ run.heard = true;
2112
+ run.starts += 1;
2113
+ run.owed = false;
2114
+ handle.playedOut = false;
2115
+ showFollowUp(handle, { speaking: true });
2116
+ },
2117
+ [showFollowUp, whenQuiet]
2118
+ );
2119
+ const sendDue = useCallback(
2120
+ (handle) => {
2121
+ const run = handle.followUps;
2122
+ const due = run?.due ?? null;
2123
+ if (run === null || due === null || due.sending || run.retry !== null || handle.done || sessionRef.current !== handle || run.heard && !handle.playedOut) {
2124
+ return;
2125
+ }
2126
+ due.sending = true;
2127
+ const starts = run.starts;
2128
+ const read = () => requestTurn({
2129
+ base: handle.base,
2130
+ headers: handle.headers,
2131
+ sessionId: handle.session.sessionId,
2132
+ secret: handle.session.endSecret,
2133
+ kind: "answer",
2134
+ messageId: due.messageId,
2135
+ idempotencyKey: due.idempotencyKey
2136
+ });
2137
+ const notRead = (unread, closed) => {
2138
+ run.due = null;
2139
+ showFollowUp(handle, {
2140
+ pending: null,
2141
+ unread: due.known ?? unread,
2142
+ ...closed ? { closed } : {}
2143
+ });
2144
+ attend();
2145
+ };
2146
+ void (async () => {
2147
+ let result = await read();
2148
+ if (result.kind === "failed" && result.lost && !handle.done) {
2149
+ result = await read();
2150
+ }
2151
+ due.sending = false;
2152
+ if (sessionRef.current !== handle || handle.done || run.due !== due) {
2153
+ return;
2154
+ }
2155
+ if (result.kind === "refused" && result.reason === "busy" && due.busy < MAX_BUSY_RETRIES) {
2156
+ due.busy += 1;
2157
+ if (run.heard && !handle.playedOut) return;
2158
+ const delay = Math.max(
2159
+ result.retryAfterSeconds === void 0 ? BUSY_RETRY_MS : result.retryAfterSeconds * 1e3,
2160
+ Math.min(BUSY_RETRY_MAX_MS, BUSY_RETRY_MS * 2 ** (due.busy - 1))
2161
+ );
2162
+ if (Date.now() + delay >= handle.endsAt) {
2163
+ notRead("no_time", false);
2164
+ return;
2165
+ }
2166
+ run.retry = setTimeout(() => {
2167
+ run.retry = null;
2168
+ sendDueRef.current(handle);
2169
+ }, delay);
2170
+ return;
2171
+ }
2172
+ if (result.kind !== "accepted") {
2173
+ notRead(
2174
+ result.kind === "refused" ? result.reason : "failed",
2175
+ result.kind === "gone" || result.kind === "refused" && result.reason === "max_answers"
2176
+ );
2177
+ return;
2178
+ }
2179
+ run.due = null;
2180
+ run.answers += 1;
2181
+ const heardSince = run.starts !== starts;
2182
+ run.owed = !heardSince;
2183
+ const playing = !heardSince || run.voice || run.quiet !== null;
2184
+ if (playing) handle.playedOut = false;
2185
+ showFollowUp(
2186
+ handle,
2187
+ {
2188
+ speaking: playing,
2189
+ pending: null,
2190
+ unread: null,
2191
+ closed: run.answers >= run.limits.maxAnswers
2192
+ },
2193
+ { captions: result.captions, excerpt: result.excerpt }
2194
+ );
2195
+ if (!playing) {
2196
+ attend();
2197
+ return;
2198
+ }
2199
+ if (run.owed && run.heard && !run.voice) whenQuiet(handle, 0);
2200
+ })();
2201
+ },
2202
+ [attend, showFollowUp, whenQuiet]
2203
+ );
2204
+ sendDueRef.current = sendDue;
2205
+ const readFollowUp = useCallback(
2206
+ (handle, messageId, known) => {
2207
+ const run = handle.followUps;
2208
+ if (run === null) return;
2209
+ run.sent.add(messageId);
2210
+ const idempotencyKey = newIdempotencyKey();
2211
+ if (idempotencyKey === null) {
2212
+ showFollowUp(handle, { pending: null, unread: known ?? "failed" });
2213
+ attend();
2214
+ return;
2215
+ }
2216
+ run.due = { messageId, idempotencyKey, known, busy: 0, sending: false };
2217
+ showFollowUp(handle, { pending: "reading", unread: null });
2218
+ sendDue(handle);
2219
+ },
2220
+ [attend, sendDue, showFollowUp]
2221
+ );
1626
2222
  const cancelStart = useCallback((reason) => {
1627
2223
  const run = startRef.current;
1628
2224
  if (!run) return;
@@ -1633,7 +2229,7 @@ function AvatarLiveView({
1633
2229
  const applyAvailability = useCallback(
1634
2230
  (result, locale2) => {
1635
2231
  const { live: live2 } = latest.current;
1636
- setMicOffer(micOfferOf(result));
2232
+ setMicChoice(micChoiceOf(result, locale2));
1637
2233
  if (result.kind === "inert") {
1638
2234
  setMode("suggested");
1639
2235
  go(INERT2);
@@ -1686,16 +2282,33 @@ function AvatarLiveView({
1686
2282
  });
1687
2283
  }, [applyAvailability, go]);
1688
2284
  const begin = useCallback(
1689
- (session, module, offer, target) => {
2285
+ (session, context) => {
2286
+ const { offer } = context;
1690
2287
  const received = Date.now();
1691
2288
  const longest = offer.maxSessionSeconds * 1e3;
1692
2289
  const ahead = session.endsAt - received;
1693
2290
  const endsAt = ahead > 0 && ahead <= longest + CLOCK_TOLERANCE_MS ? session.endsAt : received + longest;
2291
+ const followUps = session.followUps !== void 0 && offer.copy.followUps === true && offer.questions === "any" ? {
2292
+ limits: session.followUps,
2293
+ token: context.conversationToken,
2294
+ answers: 1,
2295
+ voice: false,
2296
+ heard: false,
2297
+ starts: 0,
2298
+ owed: true,
2299
+ quiet: null,
2300
+ asked: null,
2301
+ due: null,
2302
+ retry: null,
2303
+ sent: /* @__PURE__ */ new Set([context.messageId]),
2304
+ seq: 0,
2305
+ listenedAt: null
2306
+ } : null;
1694
2307
  sessionRef.current = {
1695
2308
  session,
1696
- base: target.base,
1697
- headers: target.headers,
1698
- module,
2309
+ base: context.base,
2310
+ headers: context.headers,
2311
+ module: context.module,
1699
2312
  room: null,
1700
2313
  joined: null,
1701
2314
  endsAt,
@@ -1705,6 +2318,7 @@ function AvatarLiveView({
1705
2318
  playedOut: false,
1706
2319
  unheard: null,
1707
2320
  tail: null,
2321
+ followUps,
1708
2322
  done: false
1709
2323
  };
1710
2324
  setBlocked(false);
@@ -1714,7 +2328,13 @@ function AvatarLiveView({
1714
2328
  session: {
1715
2329
  endsAt,
1716
2330
  captions: session.captions,
1717
- excerpt: session.excerpt
2331
+ excerpt: session.excerpt,
2332
+ followUp: followUps === null ? null : {
2333
+ speaking: true,
2334
+ pending: null,
2335
+ unread: null,
2336
+ closed: followUps.answers >= followUps.limits.maxAnswers
2337
+ }
1718
2338
  }
1719
2339
  });
1720
2340
  },
@@ -1746,7 +2366,7 @@ function AvatarLiveView({
1746
2366
  }
1747
2367
  switch (result.kind) {
1748
2368
  case "created":
1749
- begin(result.session, context.module, context.offer, context);
2369
+ begin(result.session, context);
1750
2370
  return;
1751
2371
  case "unusable":
1752
2372
  sendEnd({
@@ -1844,7 +2464,14 @@ function AvatarLiveView({
1844
2464
  return;
1845
2465
  }
1846
2466
  if (turnstile !== null) go({ name: "creating" });
1847
- const context = { module: loaded, offer, messageId, base: base2, headers: headers2 };
2467
+ const context = {
2468
+ module: loaded,
2469
+ offer,
2470
+ messageId,
2471
+ conversationToken,
2472
+ base: base2,
2473
+ headers: headers2
2474
+ };
1848
2475
  const result = await requestCreate({
1849
2476
  base: base2,
1850
2477
  headers: headers2,
@@ -2149,6 +2776,9 @@ function AvatarLiveView({
2149
2776
  attend();
2150
2777
  }, PLAYOUT_TAIL_MS);
2151
2778
  return;
2779
+ case "speaking":
2780
+ heardVoice(handle, event.speaking);
2781
+ return;
2152
2782
  case "speaker-left":
2153
2783
  finish(
2154
2784
  wasLive ? "finished" : "failed",
@@ -2161,7 +2791,7 @@ function AvatarLiveView({
2161
2791
  return;
2162
2792
  }
2163
2793
  },
2164
- [attend, finish, goLive, inSight]
2794
+ [attend, finish, goLive, heardVoice, inSight]
2165
2795
  );
2166
2796
  useEffect(() => {
2167
2797
  if (phase.name !== "pending") return;
@@ -2297,20 +2927,28 @@ function AvatarLiveView({
2297
2927
  const textMode = any && (usedToday && (phase.name === "needs-answer" || phase.name === "consent" || phase.name === "declined" || phase.name === "ended") || phase.name === "refused" && phase.reason === "limits");
2298
2928
  textBoxRef.current = textMode;
2299
2929
  const askState = !any ? void 0 : textMode ? "text" : phase.name === "needs-answer" ? kind === "empty" ? "ready" : kind : phase.name;
2300
- const askReason = inFlight(conversation) ? S.askBusy : conversation?.canSend ? null : S.askUnavailable;
2930
+ const followUp = phase.name === "live" ? phase.session.followUp : null;
2931
+ const followUpAsks = followUpBox(
2932
+ phase,
2933
+ sessionRef.current?.followUps ?? null,
2934
+ token
2935
+ );
2936
+ const askReason = inFlight(conversation) || followUp !== null && followUp.pending !== null ? S.askBusy : conversation?.canSend ? null : S.askUnavailable;
2301
2937
  const refreshMicOffer = useCallback(async () => {
2302
2938
  const { live: live2, locale: locale2, base: base2, headers: headers2 } = latest.current;
2303
2939
  if (base2 === null) return null;
2304
- const next = micOfferOf(
2940
+ const next = micChoiceOf(
2305
2941
  await requestAvailabilityAnswer({
2306
2942
  base: base2,
2307
2943
  headers: headers2,
2308
2944
  site: live2.site,
2309
2945
  locale: locale2
2310
- })
2946
+ }),
2947
+ locale2
2311
2948
  );
2312
- if (next !== null) setMicOffer(next);
2313
- return next;
2949
+ if (next.kind !== "stream") return null;
2950
+ setMicChoice(next);
2951
+ return next.offer;
2314
2952
  }, []);
2315
2953
  const onMicText = useCallback(
2316
2954
  (text) => {
@@ -2322,21 +2960,66 @@ function AvatarLiveView({
2322
2960
  },
2323
2961
  [askMaxLength, touchFirst]
2324
2962
  );
2963
+ const onMicStart = useCallback(() => {
2964
+ const handle = sessionRef.current;
2965
+ const current = phaseRef.current;
2966
+ if (handle === null || handle.followUps === null || handle.done || current.name !== "live" || current.session.followUp?.speaking !== true) {
2967
+ return;
2968
+ }
2969
+ const run = handle.followUps;
2970
+ tell(handle, "interrupt");
2971
+ if (run.quiet !== null) clearTimeout(run.quiet);
2972
+ run.quiet = null;
2973
+ run.owed = false;
2974
+ handle.playedOut = true;
2975
+ showFollowUp(handle, { speaking: false });
2976
+ }, [showFollowUp, tell]);
2977
+ const micSource = micChoice.kind === "browser" ? { kind: "browser", notice: micChoice.notice } : micChoice.kind === "stream" && base !== null ? {
2978
+ kind: "stream",
2979
+ base,
2980
+ offer: micChoice.offer,
2981
+ refreshOffer: refreshMicOffer
2982
+ } : null;
2325
2983
  const mic = useAvatarMic({
2326
- enabled: live.mic === true && micOffer !== null && !off && acceptsQuestion({
2984
+ enabled: live.mic === true && micSource !== null && !off && acceptsQuestion({
2327
2985
  any,
2328
2986
  phase: phase.name,
2329
2987
  kind,
2330
2988
  textMode,
2331
- reason: askReason
2989
+ reason: askReason,
2990
+ followUp: followUpAsks
2332
2991
  }),
2333
2992
  locale,
2334
- base,
2335
- offer: micOffer,
2336
- refreshOffer: refreshMicOffer,
2337
- onText: onMicText
2993
+ source: micSource,
2994
+ onText: onMicText,
2995
+ onStart: onMicStart
2338
2996
  });
2339
2997
  micCancelRef.current = mic.cancel;
2998
+ const micRecording = mic.recording;
2999
+ useEffect(() => {
3000
+ const handle = sessionRef.current;
3001
+ if (!micRecording || handle === null || handle.followUps === null || handle.done) {
3002
+ return;
3003
+ }
3004
+ const run = handle.followUps;
3005
+ const every = Math.min(
3006
+ LISTENING_MARK_MS,
3007
+ Math.max(1e3, run.limits.idleSeconds * 1e3 / 3)
3008
+ );
3009
+ const spacing = Math.min(LISTENING_MARK_SPACING_MS, every / 2);
3010
+ const mark = () => {
3011
+ const at = Date.now();
3012
+ if (run.listenedAt !== null && at - run.listenedAt < spacing) return;
3013
+ run.listenedAt = at;
3014
+ tell(handle, "listening");
3015
+ };
3016
+ mark();
3017
+ const timer = setInterval(mark, every);
3018
+ return () => {
3019
+ clearInterval(timer);
3020
+ if (sessionRef.current === handle) mark();
3021
+ };
3022
+ }, [micRecording, tell]);
2340
3023
  const notNow = useCallback(() => {
2341
3024
  const current = phaseRef.current;
2342
3025
  if ((current.name === "consent" || current.name === "needs-answer") && current.offer.questions === "any") {
@@ -2345,6 +3028,43 @@ function AvatarLiveView({
2345
3028
  }
2346
3029
  onContinueInChat();
2347
3030
  }, [go, onContinueInChat]);
3031
+ const askFollowUp = useCallback(
3032
+ (handle, conversation2, question) => {
3033
+ const run = handle.followUps;
3034
+ const current = phaseRef.current;
3035
+ if (run === null || handle.done || current.name !== "live" || current.session.followUp?.pending !== null || !followUpBox(current, run, conversation2.getConversationToken())) {
3036
+ return;
3037
+ }
3038
+ sendingRef.current = true;
3039
+ run.seq += 1;
3040
+ const seq = run.seq;
3041
+ setDraft("");
3042
+ tell(handle, "asking");
3043
+ showFollowUp(handle, { pending: "asking", unread: null });
3044
+ void conversation2.send(question, [], { surface: "AVATAR" }).then((added) => {
3045
+ if (added === null) {
3046
+ setDraft((current2) => current2 === "" ? question : current2);
3047
+ }
3048
+ if (sessionRef.current !== handle || handle.done || run.seq !== seq) {
3049
+ return;
3050
+ }
3051
+ if (added === null || added.conversationToken !== run.token) {
3052
+ showFollowUp(handle, { pending: null });
3053
+ attend();
3054
+ return;
3055
+ }
3056
+ run.asked = {
3057
+ messageId: added.userMessageId,
3058
+ token: added.conversationToken,
3059
+ answerId: null
3060
+ };
3061
+ showFollowUp(handle, { pending: "asking" });
3062
+ }).finally(() => {
3063
+ sendingRef.current = false;
3064
+ });
3065
+ },
3066
+ [attend, showFollowUp, tell]
3067
+ );
2348
3068
  const ask = useCallback(
2349
3069
  (text) => {
2350
3070
  const conversation2 = conversationRef.current;
@@ -2353,6 +3073,11 @@ function AvatarLiveView({
2353
3073
  return;
2354
3074
  }
2355
3075
  const spoken = !textBoxRef.current;
3076
+ const handle = sessionRef.current;
3077
+ if (spoken && handle !== null && handle.followUps !== null) {
3078
+ askFollowUp(handle, conversation2, question);
3079
+ return;
3080
+ }
2356
3081
  if (spoken && conversation2.messages.length > 0) return;
2357
3082
  sendingRef.current = true;
2358
3083
  setAskedHere(null);
@@ -2374,8 +3099,64 @@ function AvatarLiveView({
2374
3099
  sendingRef.current = false;
2375
3100
  });
2376
3101
  },
2377
- [setAskedHere]
3102
+ [askFollowUp, setAskedHere]
2378
3103
  );
3104
+ useEffect(() => {
3105
+ const handle = sessionRef.current;
3106
+ const run = handle?.followUps ?? null;
3107
+ if (handle === null || run === null || handle.done || run.asked === null) {
3108
+ return;
3109
+ }
3110
+ const asked = run.asked;
3111
+ const chat = conversationRef.current;
3112
+ const messages = chat?.messages ?? [];
3113
+ const at = messages.findIndex((message) => message.id === asked.messageId);
3114
+ const question = at === -1 ? void 0 : messages[at];
3115
+ const answer = at === -1 ? void 0 : messages[at + 1];
3116
+ const drop = (unread2) => {
3117
+ run.asked = null;
3118
+ showFollowUp(handle, { pending: null, unread: unread2 });
3119
+ attend();
3120
+ };
3121
+ const unread = (id, reason) => {
3122
+ if (id === null || run.sent.has(id) || !isAvatarMessageId(id)) {
3123
+ drop(reason);
3124
+ return;
3125
+ }
3126
+ run.asked = null;
3127
+ readFollowUp(handle, id, reason);
3128
+ };
3129
+ if (question === void 0 || (chat?.getConversationToken() ?? null) !== run.token) {
3130
+ drop(null);
3131
+ return;
3132
+ }
3133
+ if (answer === void 0) {
3134
+ if (!inFlight(chat)) unread(asked.answerId, "answer_failed");
3135
+ return;
3136
+ }
3137
+ if (answer.status === "PENDING" || answer.status === "RUNNING") {
3138
+ if (answer.role === "ASSISTANT") asked.answerId = answer.id;
3139
+ return;
3140
+ }
3141
+ if (answer.role !== "ASSISTANT") {
3142
+ unread(asked.answerId, "answer_failed");
3143
+ return;
3144
+ }
3145
+ if (answer.status !== "COMPLETED" || answer.content.trim() === "") {
3146
+ unread(answer.id, "answer_failed");
3147
+ return;
3148
+ }
3149
+ if (run.sent.has(answer.id) || !isAvatarMessageId(answer.id)) {
3150
+ drop("not_eligible");
3151
+ return;
3152
+ }
3153
+ if (at + 2 !== messages.length || !readable(question, answer)) {
3154
+ unread(answer.id, "not_eligible");
3155
+ return;
3156
+ }
3157
+ run.asked = null;
3158
+ readFollowUp(handle, answer.id, null);
3159
+ });
2379
3160
  const newChatForAvatar = useCallback(() => {
2380
3161
  const conversation2 = conversationRef.current;
2381
3162
  if (conversation2 === null) return;
@@ -2399,7 +3180,14 @@ function AvatarLiveView({
2399
3180
  const focused = document.activeElement;
2400
3181
  if (focused && focused !== document.body && focused.isConnected) return;
2401
3182
  rootRef.current?.querySelector(PRIMARY)?.focus();
2402
- }, [active, phase.name, askState, shown.pendingAnswerId, blocked]);
3183
+ }, [
3184
+ active,
3185
+ phase.name,
3186
+ askState,
3187
+ shown.pendingAnswerId,
3188
+ blocked,
3189
+ followUpAsks
3190
+ ]);
2403
3191
  const onPlaying = goLive;
2404
3192
  const enablePlayback = useCallback(() => {
2405
3193
  void sessionRef.current?.joined?.enablePlayback().catch(() => void 0);
@@ -2799,13 +3587,32 @@ function AvatarLiveView({
2799
3587
  actions2 = stopButton;
2800
3588
  break;
2801
3589
  case "pending":
2802
- case "live":
2803
- status = phase.name === "live" ? speakingLine(phase.session) : S.connecting;
3590
+ case "live": {
2804
3591
  top2 = badge;
2805
3592
  media2 = liveMedia(phase.name === "live");
2806
3593
  body2 = liveBody(phase.session);
2807
3594
  actions2 = liveActions(readInChatButton(false));
3595
+ if (followUp === null) {
3596
+ status = phase.name === "live" ? speakingLine(phase.session) : S.connecting;
3597
+ break;
3598
+ }
3599
+ status = followUp.speaking ? speakingLine(phase.session) : followUp.pending !== null ? (followUp.pending === "asking" ? activity : null) ?? S.gettingAnswer : followUp.unread !== null ? S[FOLLOW_UP_TEXT[followUp.unread]] : followUp.closed ? S.followUpLast : S.followUpIdle;
3600
+ if (!followUp.speaking && followUp.pending === null && followUp.unread !== null && !followUp.closed) {
3601
+ body2 = /* @__PURE__ */ jsxs(Fragment, { children: [
3602
+ body2,
3603
+ /* @__PURE__ */ jsx(
3604
+ "p",
3605
+ {
3606
+ "data-boff-explore": "avatar-live-idle",
3607
+ className: "text-xs text-muted-foreground",
3608
+ children: S.followUpIdle
3609
+ }
3610
+ )
3611
+ ] });
3612
+ }
3613
+ if (followUpAsks) box = "spoken";
2808
3614
  break;
3615
+ }
2809
3616
  case "ended":
2810
3617
  status = S[phase.how];
2811
3618
  outcome2 = true;
@@ -2853,7 +3660,7 @@ function AvatarLiveView({
2853
3660
  /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: S.newChatNote })
2854
3661
  ] }) : null,
2855
3662
  chips,
2856
- box !== null && usedToday ? /* @__PURE__ */ jsx(
3663
+ box !== null && usedToday && phase.name !== "live" ? /* @__PURE__ */ jsx(
2857
3664
  "p",
2858
3665
  {
2859
3666
  "data-boff-explore": "avatar-live-used-today",
@@ -3082,7 +3889,12 @@ function AskBox({
3082
3889
  dir: "auto",
3083
3890
  "data-avatar-primary": touchFirst ? void 0 : "",
3084
3891
  "data-boff-explore": "avatar-live-ask-input",
3085
- 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",
3892
+ className: cn(
3893
+ // Kept out of a site's session replay before Send (rrweb's default classes; `rr-ignore` for its input
3894
+ // events): the text may be what the mic heard, or what the visitor typed.
3895
+ REPLAY_PRIVATE,
3896
+ "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"
3897
+ ),
3086
3898
  onChange: (event) => {
3087
3899
  setRefusedFiles(false);
3088
3900
  onChange(event.target.value);