@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,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkXIZE37IB_js = require('../chunk-XIZE37IB.js');
3
+ var chunk3AIAJ2L4_js = require('../chunk-3AIAJ2L4.js');
4
4
  var chunkN7E6Z2GF_js = require('../chunk-N7E6Z2GF.js');
5
5
  var chunkC6OJIHNO_js = require('../chunk-C6OJIHNO.js');
6
6
  require('../chunk-ZSTFX7FV.js');
@@ -11,6 +11,8 @@ var jsxRuntime = require('react/jsx-runtime');
11
11
  // src/components/explore-avatar-api.ts
12
12
  var AVAILABILITY_TIMEOUT_MS = 8e3;
13
13
  var CREATE_TIMEOUT_MS = 3e4;
14
+ var TURN_TIMEOUT_MS = 8e3;
15
+ var TURN_ANSWER_TIMEOUT_MS = 25e3;
14
16
  var MESSAGE_ID = /^[A-Za-z0-9_-]{1,64}$/;
15
17
  var STT_TOKEN = /^[A-Za-z0-9._~-]{1,4096}$/;
16
18
  var CONVERSATION_TOKEN = /^[A-Za-z0-9._~+/=-]{16,512}$/;
@@ -69,6 +71,17 @@ function readSttOffer(value) {
69
71
  }
70
72
  return { token, expiresAt };
71
73
  }
74
+ var MIC_MAX_PRESS_SECONDS = 30;
75
+ var UNREADABLE_MIC = { mode: "unreadable" };
76
+ function readMicOffer(value) {
77
+ if (!isRecord(value)) return UNREADABLE_MIC;
78
+ const keys = Object.keys(value).sort();
79
+ const { mode, maxPressSeconds } = value;
80
+ if (keys.length !== 2 || keys[0] !== "maxPressSeconds" || keys[1] !== "mode" || mode !== "browser" || maxPressSeconds !== MIC_MAX_PRESS_SECONDS) {
81
+ return UNREADABLE_MIC;
82
+ }
83
+ return { mode: "browser", maxPressSeconds: MIC_MAX_PRESS_SECONDS };
84
+ }
72
85
  function readAvailability(status, body) {
73
86
  if (status !== 200 || !isRecord(body)) return INERT;
74
87
  const { available, reason } = body;
@@ -79,10 +92,11 @@ function readAvailability(status, body) {
79
92
  const { consentVersion, maxSessionSeconds } = body;
80
93
  const botCheck = readBotCheck(body.botCheck);
81
94
  const questions = readQuestions(body.questions);
82
- if (available !== true || typeof consentVersion !== "string" || !chunkXIZE37IB_js.isAvatarVersion(consentVersion) || typeof maxSessionSeconds !== "number" || !Number.isInteger(maxSessionSeconds) || maxSessionSeconds < 1 || botCheck === null || questions === null) {
95
+ if (available !== true || typeof consentVersion !== "string" || !chunk3AIAJ2L4_js.isAvatarVersion(consentVersion) || typeof maxSessionSeconds !== "number" || !Number.isInteger(maxSessionSeconds) || maxSessionSeconds < 1 || botCheck === null || questions === null) {
83
96
  return INERT;
84
97
  }
85
98
  const stt = readSttOffer(body.stt);
99
+ const mic = Object.prototype.hasOwnProperty.call(body, "mic") ? readMicOffer(body.mic) : void 0;
86
100
  return {
87
101
  kind: "available",
88
102
  offer: {
@@ -90,7 +104,8 @@ function readAvailability(status, body) {
90
104
  maxSessionSeconds,
91
105
  botCheck,
92
106
  questions,
93
- ...stt === void 0 ? {} : { stt }
107
+ ...stt === void 0 ? {} : { stt },
108
+ ...mic === void 0 ? {} : { mic }
94
109
  }
95
110
  };
96
111
  }
@@ -115,6 +130,21 @@ function errorCode(body) {
115
130
  const { code } = body.error;
116
131
  return typeof code === "string" ? code : null;
117
132
  }
133
+ var MAX_FOLLOW_UP_ANSWERS = 16;
134
+ var MAX_FOLLOW_UP_SECONDS = 600;
135
+ var FOLLOW_UP_KEYS = ["idleSeconds", "maxAnswerWaitSeconds", "maxAnswers"];
136
+ function isWholeBetween(value, max) {
137
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 && value <= max;
138
+ }
139
+ function readFollowUps(value) {
140
+ if (!isRecord(value)) return void 0;
141
+ const keys = Object.keys(value).sort();
142
+ const { maxAnswers, idleSeconds, maxAnswerWaitSeconds } = value;
143
+ 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)) {
144
+ return void 0;
145
+ }
146
+ return { maxAnswers, idleSeconds, maxAnswerWaitSeconds };
147
+ }
118
148
  function readCreated(body) {
119
149
  if (!isRecord(body)) return FAILED;
120
150
  const { sessionId, endSecret } = body;
@@ -132,6 +162,7 @@ function readCreated(body) {
132
162
  if (typeof livekitUrl !== "string" || !livekitUrl.startsWith("wss://") || typeof participantToken !== "string" || participantToken === "" || typeof workerIdentity !== "string" || workerIdentity === "" || typeof locale !== "string" || typeof text !== "string") {
133
163
  return unusable;
134
164
  }
165
+ const followUps = readFollowUps(body.followUps);
135
166
  return {
136
167
  kind: "created",
137
168
  session: {
@@ -140,7 +171,8 @@ function readCreated(body) {
140
171
  endsAt,
141
172
  join: { livekitUrl, participantToken, workerIdentity },
142
173
  captions: { locale, text },
143
- excerpt: body.excerpt === true
174
+ excerpt: body.excerpt === true,
175
+ ...followUps === void 0 ? {} : { followUps }
144
176
  }
145
177
  };
146
178
  }
@@ -186,7 +218,7 @@ async function requestAvailability(target) {
186
218
  }
187
219
  }
188
220
  async function requestAvailabilityAnswer(target) {
189
- if (!chunkXIZE37IB_js.isAvatarSite(target.site) || !LOCALE.test(target.locale)) return INERT;
221
+ if (!chunk3AIAJ2L4_js.isAvatarSite(target.site) || !LOCALE.test(target.locale)) return INERT;
190
222
  const query = new URLSearchParams({
191
223
  site: target.site,
192
224
  locale: target.locale
@@ -271,6 +303,96 @@ function sendEnd(target) {
271
303
  } catch {
272
304
  }
273
305
  }
306
+ var TURN_REFUSALS = /* @__PURE__ */ new Map([
307
+ ["no_time", "no_time"],
308
+ ["max_answers", "max_answers"],
309
+ ["not_eligible", "not_eligible"],
310
+ ["answer_claimed", "answer_claimed"],
311
+ ["limits", "limits"],
312
+ ["paused", "paused"],
313
+ ["busy", "busy"]
314
+ ]);
315
+ var MAX_TURN_RETRY_AFTER_SECONDS = 600;
316
+ function retryAfterOf(body) {
317
+ if (!isRecord(body) || !isRecord(body.error)) return void 0;
318
+ const { retryAfterSeconds } = body.error;
319
+ return isWholeBetween(retryAfterSeconds, MAX_TURN_RETRY_AFTER_SECONDS) ? retryAfterSeconds : void 0;
320
+ }
321
+ var TURN_KEY = /^[A-Za-z0-9_-]{8,64}$/;
322
+ var LOST = { kind: "failed", lost: true };
323
+ var TURN_FAILED = { kind: "failed", lost: false };
324
+ function readAccepted(body) {
325
+ if (!isRecord(body) || body.accepted !== true || !isRecord(body.captions)) {
326
+ return LOST;
327
+ }
328
+ const { locale, text } = body.captions;
329
+ if (typeof locale !== "string" || typeof text !== "string") return LOST;
330
+ return {
331
+ kind: "accepted",
332
+ captions: { locale, text },
333
+ excerpt: body.excerpt === true
334
+ };
335
+ }
336
+ function readTurn(kind, status, body) {
337
+ if (status === 200) {
338
+ if (kind === "answer") return readAccepted(body);
339
+ return isRecord(body) && body.ok === true ? { kind: "ok" } : LOST;
340
+ }
341
+ const code = errorCode(body);
342
+ const refusal = code === null ? void 0 : TURN_REFUSALS.get(code);
343
+ if (refusal !== void 0) {
344
+ const retryAfterSeconds = retryAfterOf(body);
345
+ return retryAfterSeconds === void 0 ? { kind: "refused", reason: refusal } : { kind: "refused", reason: refusal, retryAfterSeconds };
346
+ }
347
+ if (status === 404) return { kind: "gone" };
348
+ return status >= 500 ? LOST : TURN_FAILED;
349
+ }
350
+ function requestTurn(target) {
351
+ const { kind, messageId, idempotencyKey } = target;
352
+ const nonce = newIdempotencyKey();
353
+ 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)) {
354
+ return Promise.resolve(TURN_FAILED);
355
+ }
356
+ const payload = JSON.stringify({
357
+ secret: target.secret,
358
+ kind,
359
+ nonce,
360
+ ...messageId === void 0 ? {} : { messageId },
361
+ ...idempotencyKey === void 0 ? {} : { idempotencyKey }
362
+ });
363
+ const controller = new AbortController();
364
+ const answer = (async () => {
365
+ try {
366
+ const response = await fetch(
367
+ `${target.base}/sessions/${encodeURIComponent(target.sessionId)}/turn`,
368
+ {
369
+ method: "POST",
370
+ credentials: "omit",
371
+ cache: "no-store",
372
+ headers: { ...target.headers },
373
+ body: payload,
374
+ signal: controller.signal
375
+ }
376
+ );
377
+ return readTurn(kind, response.status, await readJson(response));
378
+ } catch {
379
+ return LOST;
380
+ }
381
+ })();
382
+ return new Promise((resolve) => {
383
+ const timer = setTimeout(
384
+ () => {
385
+ controller.abort();
386
+ resolve(LOST);
387
+ },
388
+ kind === "answer" ? TURN_ANSWER_TIMEOUT_MS : TURN_TIMEOUT_MS
389
+ );
390
+ void answer.then((result) => {
391
+ clearTimeout(timer);
392
+ resolve(result);
393
+ });
394
+ });
395
+ }
274
396
  var BOT_BINDING_PREFIX = "boff-web-avatar-cdata/v1";
275
397
  function webCryptoSubtle() {
276
398
  const subtle = globalThis.crypto?.subtle;
@@ -368,7 +490,14 @@ var EN = {
368
490
  usedToday: "You've had today's avatar session. Answers will show here as text.",
369
491
  needsAnswerAny: "Ask a question here, and the avatar reads the answer aloud.",
370
492
  startNewChatDetailAny: "The avatar reads aloud only the first answer in a new chat started in the Avatar tab, provided it passes the speech checks.",
371
- notEligibleAny: "The avatar can't read this answer aloud. The answer and sources are in Chat."
493
+ notEligibleAny: "The avatar can't read this answer aloud. The answer and sources are in Chat.",
494
+ followUpIdle: "The avatar ends after about thirty seconds without a question.",
495
+ followUpNoTime: "There isn't enough time left in this session to read that answer. It's in Chat.",
496
+ followUpMaxAnswers: "The avatar has read all the answers it can in this session. This one is in Chat.",
497
+ followUpLast: "That was the last answer the avatar can read in this session.",
498
+ followUpLimits: "The live avatar has reached its limit for now. This answer is in Chat.",
499
+ followUpPaused: "The live avatar is paused right now. This answer is in Chat.",
500
+ followUpUnread: "The avatar couldn't read that answer. It's in Chat."
372
501
  };
373
502
  var ZH = {
374
503
  checking: "\u6B63\u5728\u68C0\u67E5\u5B9E\u65F6\u865A\u62DF\u5F62\u8C61\u662F\u5426\u53EF\u7528\u2026",
@@ -429,7 +558,16 @@ var ZH = {
429
558
  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",
430
559
  needsAnswerAny: "\u5728\u8FD9\u91CC\u63D0\u95EE\uFF0C\u865A\u62DF\u5F62\u8C61\u4F1A\u6717\u8BFB\u56DE\u7B54\u3002",
431
560
  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",
432
- notEligibleAny: "\u865A\u62DF\u5F62\u8C61\u65E0\u6CD5\u6717\u8BFB\u6B64\u56DE\u7B54\u3002\u56DE\u7B54\u548C\u6765\u6E90\u5728\u804A\u5929\u4E2D\u3002"
561
+ notEligibleAny: "\u865A\u62DF\u5F62\u8C61\u65E0\u6CD5\u6717\u8BFB\u6B64\u56DE\u7B54\u3002\u56DE\u7B54\u548C\u6765\u6E90\u5728\u804A\u5929\u4E2D\u3002",
562
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the lines of a session that reads the answers after the first
563
+ // (Stage D).
564
+ followUpIdle: "\u5982\u679C\u7EA6\u4E09\u5341\u79D2\u5185\u6CA1\u6709\u65B0\u95EE\u9898\uFF0C\u865A\u62DF\u5F62\u8C61\u5C31\u4F1A\u7ED3\u675F\u3002",
565
+ 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",
566
+ 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",
567
+ followUpLast: "\u8FD9\u662F\u865A\u62DF\u5F62\u8C61\u5728\u672C\u6B21\u4F1A\u8BDD\u4E2D\u53EF\u6717\u8BFB\u7684\u6700\u540E\u4E00\u4E2A\u56DE\u7B54\u3002",
568
+ 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",
569
+ followUpPaused: "\u5B9E\u65F6\u865A\u62DF\u5F62\u8C61\u76EE\u524D\u5DF2\u6682\u505C\u3002\u6B64\u56DE\u7B54\u5728\u804A\u5929\u4E2D\u3002",
570
+ followUpUnread: "\u865A\u62DF\u5F62\u8C61\u65E0\u6CD5\u6717\u8BFB\u8BE5\u56DE\u7B54\u3002\u56DE\u7B54\u5728\u804A\u5929\u4E2D\u3002"
433
571
  };
434
572
  var HI = {
435
573
  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",
@@ -490,7 +628,16 @@ var HI = {
490
628
  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",
491
629
  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",
492
630
  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",
493
- 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"
631
+ 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",
632
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the lines of a session that reads the answers after the first
633
+ // (Stage D).
634
+ 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",
635
+ 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",
636
+ 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",
637
+ 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",
638
+ 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",
639
+ 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",
640
+ 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"
494
641
  };
495
642
  var ES = {
496
643
  checking: "Comprobando si el avatar en vivo est\xE1 disponible\u2026",
@@ -551,7 +698,16 @@ var ES = {
551
698
  usedToday: "Ya has usado la sesi\xF3n de avatar de hoy. Las respuestas se mostrar\xE1n aqu\xED como texto.",
552
699
  needsAnswerAny: "Haz una pregunta aqu\xED y el avatar leer\xE1 la respuesta en voz alta.",
553
700
  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.",
554
- notEligibleAny: "El avatar no puede leer esta respuesta en voz alta. La respuesta y las fuentes est\xE1n en el chat."
701
+ notEligibleAny: "El avatar no puede leer esta respuesta en voz alta. La respuesta y las fuentes est\xE1n en el chat.",
702
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the lines of a session that reads the answers after the first
703
+ // (Stage D).
704
+ followUpIdle: "El avatar termina tras unos treinta segundos sin preguntas.",
705
+ followUpNoTime: "No queda tiempo suficiente en esta sesi\xF3n para leer esa respuesta. Est\xE1 en el chat.",
706
+ followUpMaxAnswers: "El avatar ya ha le\xEDdo todas las respuestas que puede en esta sesi\xF3n. Esta est\xE1 en el chat.",
707
+ followUpLast: "Esa fue la \xFAltima respuesta que el avatar puede leer en esta sesi\xF3n.",
708
+ followUpLimits: "El avatar en vivo ha alcanzado su l\xEDmite por ahora. Esta respuesta est\xE1 en el chat.",
709
+ followUpPaused: "El avatar en vivo est\xE1 en pausa ahora mismo. Esta respuesta est\xE1 en el chat.",
710
+ followUpUnread: "El avatar no pudo leer esa respuesta. Est\xE1 en el chat."
555
711
  };
556
712
  var AR = {
557
713
  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",
@@ -612,7 +768,16 @@ var AR = {
612
768
  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.",
613
769
  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.",
614
770
  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.",
615
- 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."
771
+ 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.",
772
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the lines of a session that reads the answers after the first
773
+ // (Stage D).
774
+ 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.",
775
+ 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.",
776
+ 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.",
777
+ 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.",
778
+ 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.",
779
+ 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.",
780
+ 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."
616
781
  };
617
782
  var TA = {
618
783
  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",
@@ -673,7 +838,16 @@ var TA = {
673
838
  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.",
674
839
  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.",
675
840
  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.",
676
- 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."
841
+ 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.",
842
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the lines of a session that reads the answers after the first
843
+ // (Stage D).
844
+ 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.",
845
+ 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.",
846
+ 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.",
847
+ 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.",
848
+ 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.",
849
+ 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.",
850
+ 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."
677
851
  };
678
852
  var STRINGS = {
679
853
  en: EN,
@@ -686,6 +860,7 @@ var STRINGS = {
686
860
  function avatarLiveStrings(locale) {
687
861
  return STRINGS[locale] ?? EN;
688
862
  }
863
+ var BROWSER_MIC_NOTICE_VERSION = "web-mic-2026-11-21-c";
689
864
  var CONSENT = /* @__PURE__ */ new Map([
690
865
  [
691
866
  "web-consent-2026-11",
@@ -823,10 +998,95 @@ var CONSENT = /* @__PURE__ */ new Map([
823
998
  questions: "any"
824
999
  }
825
1000
  }
1001
+ ],
1002
+ [
1003
+ // Stage D (BOFF-9797), a DRAFT for the operator's review that no environment uses yet (global-speech-svc reserves the
1004
+ // same two versions): web-consent-2026-11-12-b with only its intro changed. The avatar reads the answer to each
1005
+ // question asked in the Avatar tab, in one session, waits about thirty seconds after an answer for the next question,
1006
+ // then ends; the session lasts at most three minutes, and an answer that does not fit the time left is read in part
1007
+ // (and said so) or not read, and stays in Chat. The notice files of record and their digests are a gitops change of
1008
+ // their own, made with the review.
1009
+ "web-consent-2026-11-19-d",
1010
+ {
1011
+ en: {
1012
+ title: "Live AI avatar (beta)",
1013
+ introProduct: null,
1014
+ 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.",
1015
+ 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.",
1016
+ 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.",
1017
+ privacyLink: "How we handle this",
1018
+ botCheck: "turnstile",
1019
+ questions: "any",
1020
+ followUps: true
1021
+ }
1022
+ }
1023
+ ],
1024
+ [
1025
+ // The words of web-consent-2026-11-19-d without "When you start, Cloudflare checks for bots.", for a server that runs
1026
+ // no bot check. A DRAFT, like it.
1027
+ "web-consent-2026-11-20-d",
1028
+ {
1029
+ en: {
1030
+ title: "Live AI avatar (beta)",
1031
+ introProduct: null,
1032
+ 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.",
1033
+ 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.",
1034
+ 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.",
1035
+ privacyLink: "How we handle this",
1036
+ botCheck: "off",
1037
+ questions: "any",
1038
+ followUps: true
1039
+ }
1040
+ }
1041
+ ],
1042
+ [
1043
+ // Stage C (BOFF-9797, the operator's decision of 2026-11-05): the mic through the browser's own speech recognition.
1044
+ // A DRAFT for the operator's privacy review that no environment uses yet (global-speech-svc reserves the same two
1045
+ // versions, and pairs them with its AVATAR_WEB_MIC=browser): web-consent-2026-11-19-d with three changes only. The
1046
+ // intro warns against personal details said into the microphone and says a tap on it stops the answer being read;
1047
+ // the processing says which speech service turns the voice into text (the browser's own, under its own terms, with
1048
+ // the vendors as examples) and that a live session is told of a tap and of a recording, never of the words; the
1049
+ // storage says the camera is not used and that the voice never reaches us, nor the text until Send, in place of
1050
+ // "Your microphone and camera are not used.", which is true only with no mic. The notice files of record and their
1051
+ // digests are a gitops change of their own, made with the review.
1052
+ "web-consent-2026-11-21-c",
1053
+ {
1054
+ en: {
1055
+ title: "Live AI avatar (beta)",
1056
+ introProduct: null,
1057
+ intro: "This is an AI avatar, not a real person. It reads aloud the answers to the questions you ask in the Avatar tab, provided each passes the speech checks. After an answer, it waits about thirty seconds for your next question, then ends. A session lasts at most three minutes; if an answer doesn't fit in the time left, it reads only the first part and says so, or doesn't read it, and the full answer stays in Chat. The answers are AI-generated and can be wrong. Don't put personal details in a question you type. Don't say them into the microphone either. Tapping the microphone while the avatar speaks stops that answer.",
1058
+ processing: "When you start, Cloudflare checks for bots. The answer text goes to Google to create the voice. Our server sends that voice through LiveKit to Protoface, which makes the avatar video streamed through LiveKit. Your browser connects to LiveKit, which sees your IP address. If you use the microphone, the speech service your browser uses turns your voice into text and may receive the audio: for example Google for Chrome on a computer or Android phone, Apple for Safari and on iPhone and iPad, and Microsoft for Edge on a computer, each under its own privacy terms. During a live session, our server is told when you tap the microphone and while you record (never what you say), so the avatar stops and waits.",
1059
+ storage: "Your camera is not used. We never receive your voice: the text appears in the question box for you to check, and none of what you said reaches us until you press Send. We do not record the live session or store the generated voice. We keep a record of this consent. The privacy notice explains our safeguards, storage periods and the providers' processing. For people aged eighteen and over.",
1060
+ privacyLink: "How we handle this",
1061
+ botCheck: "turnstile",
1062
+ questions: "any",
1063
+ followUps: true,
1064
+ micNotice: BROWSER_MIC_NOTICE_VERSION
1065
+ }
1066
+ }
1067
+ ],
1068
+ [
1069
+ // The words of web-consent-2026-11-21-c without "When you start, Cloudflare checks for bots.", for a server that runs
1070
+ // no bot check. A DRAFT, like it.
1071
+ "web-consent-2026-11-22-c",
1072
+ {
1073
+ en: {
1074
+ title: "Live AI avatar (beta)",
1075
+ introProduct: null,
1076
+ intro: "This is an AI avatar, not a real person. It reads aloud the answers to the questions you ask in the Avatar tab, provided each passes the speech checks. After an answer, it waits about thirty seconds for your next question, then ends. A session lasts at most three minutes; if an answer doesn't fit in the time left, it reads only the first part and says so, or doesn't read it, and the full answer stays in Chat. The answers are AI-generated and can be wrong. Don't put personal details in a question you type. Don't say them into the microphone either. Tapping the microphone while the avatar speaks stops that answer.",
1077
+ processing: "The answer text goes to Google to create the voice. Our server sends that voice through LiveKit to Protoface, which makes the avatar video streamed through LiveKit. Your browser connects to LiveKit, which sees your IP address. If you use the microphone, the speech service your browser uses turns your voice into text and may receive the audio: for example Google for Chrome on a computer or Android phone, Apple for Safari and on iPhone and iPad, and Microsoft for Edge on a computer, each under its own privacy terms. During a live session, our server is told when you tap the microphone and while you record (never what you say), so the avatar stops and waits.",
1078
+ storage: "Your camera is not used. We never receive your voice: the text appears in the question box for you to check, and none of what you said reaches us until you press Send. We do not record the live session or store the generated voice. We keep a record of this consent. The privacy notice explains our safeguards, storage periods and the providers' processing. For people aged eighteen and over.",
1079
+ privacyLink: "How we handle this",
1080
+ botCheck: "off",
1081
+ questions: "any",
1082
+ followUps: true,
1083
+ micNotice: BROWSER_MIC_NOTICE_VERSION
1084
+ }
1085
+ }
826
1086
  ]
827
1087
  ]);
828
1088
  function avatarConsentCopy(version, locale) {
829
- if (!chunkXIZE37IB_js.isAvatarLiveLocaleReviewed(locale)) return null;
1089
+ if (!chunk3AIAJ2L4_js.isAvatarLiveLocaleReviewed(locale)) return null;
830
1090
  return CONSENT.get(version)?.[locale] ?? null;
831
1091
  }
832
1092
  var MIC_EN = {
@@ -850,7 +1110,7 @@ var MIC_EN = {
850
1110
  };
851
1111
  var MIC_STRINGS = { en: MIC_EN };
852
1112
  function avatarMicStrings(locale) {
853
- if (!chunkXIZE37IB_js.isAvatarLiveLocaleReviewed(locale)) return null;
1113
+ if (!chunk3AIAJ2L4_js.isAvatarLiveLocaleReviewed(locale)) return null;
854
1114
  return MIC_STRINGS[locale] ?? null;
855
1115
  }
856
1116
  var AVATAR_MIC_NOTICE_VERSION = "web-mic-2026-11-05";
@@ -867,20 +1127,38 @@ var MIC_NOTICE = /* @__PURE__ */ new Map([
867
1127
  privacyLink: "How we handle this"
868
1128
  }
869
1129
  }
1130
+ ],
1131
+ [
1132
+ // The browser's own speech recognition (Stage C): the browser's speech service, named by example where each is
1133
+ // true, under its own terms; nothing of the voice reaches us, nor the text before Send; a live session told only of
1134
+ // a tap and of a recording. No daily limits: nothing is counted on the server.
1135
+ BROWSER_MIC_NOTICE_VERSION,
1136
+ {
1137
+ en: {
1138
+ title: "Ask with your voice",
1139
+ processing: "The speech service your browser uses turns your voice into text and may receive the audio: for example Google for Chrome on a computer or Android phone, Apple for Safari and on iPhone and iPad, and Microsoft for Edge on a computer. Its own privacy terms apply.",
1140
+ storage: "We never receive your voice, and your camera is not used.",
1141
+ text: "The text appears in the box for you to check. None of what you said reaches us until you press Send; once you send it, it is a chat message, kept like any other. During a live session, we are told when you tap the microphone and while you record (never what you say), so the avatar stops and waits.",
1142
+ limits: "Each recording stops after thirty seconds at the latest.",
1143
+ privacyLink: "How we handle this"
1144
+ }
1145
+ }
870
1146
  ]
871
1147
  ]);
872
1148
  function avatarMicNotice(version, locale) {
873
- if (!chunkXIZE37IB_js.isAvatarLiveLocaleReviewed(locale)) return null;
1149
+ if (!chunk3AIAJ2L4_js.isAvatarLiveLocaleReviewed(locale)) return null;
874
1150
  return MIC_NOTICE.get(version)?.[locale] ?? null;
875
1151
  }
876
1152
  var PRIVACY_HREF = "/privacy#live-avatar";
877
1153
  var LIMIT_SECONDS = 30;
878
1154
  var TICK_MS = 250;
879
1155
  var IDLE = { name: "idle" };
880
- var FOR_GOOD = /* @__PURE__ */ new Set([
881
- "unavailable",
882
- "limit"
883
- ]);
1156
+ var EN_IN = { lang: "en-IN", fallbackLang: "en-US" };
1157
+ var BROWSER_MIC_LANGUAGES = { en: EN_IN };
1158
+ var FOR_GOOD = {
1159
+ stream: /* @__PURE__ */ new Set(["unavailable", "limit"]),
1160
+ browser: /* @__PURE__ */ new Set(["unsupported"])
1161
+ };
884
1162
  var FAILURE_TEXT = {
885
1163
  denied: "micDenied",
886
1164
  unsupported: "micUnsupported",
@@ -889,14 +1167,32 @@ var FAILURE_TEXT = {
889
1167
  busy: "micBusy",
890
1168
  failed: "micFailed"
891
1169
  };
1170
+ var loadedMic = null;
892
1171
  function loadAvatarMic() {
893
- return import('../avatar-mic/index.js').then((loaded) => ({
894
- createAvatarMic: loaded.createAvatarMic
895
- }));
1172
+ return import('../avatar-mic/index.js').then((loaded) => {
1173
+ const module = {
1174
+ createAvatarMic: loaded.createAvatarMic,
1175
+ createBrowserAvatarMic: loaded.createBrowserAvatarMic
1176
+ };
1177
+ loadedMic = module;
1178
+ return module;
1179
+ });
896
1180
  }
897
1181
  function canUseAvatarMic() {
898
1182
  return typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getUserMedia === "function" && typeof AudioContext !== "undefined" && typeof AudioWorkletNode !== "undefined" && typeof WebSocket !== "undefined";
899
1183
  }
1184
+ function canUseBrowserMic() {
1185
+ if (typeof window === "undefined" || typeof navigator === "undefined") {
1186
+ return false;
1187
+ }
1188
+ if (window.isSecureContext !== true) return false;
1189
+ const recognizer = Reflect.get(window, "SpeechRecognition") ?? Reflect.get(window, "webkitSpeechRecognition");
1190
+ if (typeof recognizer !== "function") return false;
1191
+ const agent = navigator.userAgent ?? "";
1192
+ if (/\bFirefox\//.test(agent) || /\bFxiOS\//.test(agent)) return false;
1193
+ return !("brave" in navigator);
1194
+ }
1195
+ var REPLAY_PRIVATE = "rr-block rr-mask";
900
1196
  function clock(seconds) {
901
1197
  const whole = Math.max(0, Math.floor(seconds));
902
1198
  return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, "0")}`;
@@ -907,36 +1203,51 @@ function withHeardText(current, heard, maxLength) {
907
1203
  const joined = current.trim() === "" ? text : `${current.replace(/\s+$/, "")} ${text}`;
908
1204
  return joined.slice(0, Math.max(0, maxLength));
909
1205
  }
1206
+ function noticeOf(source) {
1207
+ return source.kind === "stream" ? AVATAR_MIC_NOTICE_VERSION : source.notice;
1208
+ }
1209
+ function micKeyOf(source, locale) {
1210
+ return source.kind === "stream" ? `stream ${source.base}` : `browser ${BROWSER_MIC_LANGUAGES[locale]?.lang ?? ""}`;
1211
+ }
910
1212
  function useAvatarMic({
911
1213
  enabled,
912
1214
  locale,
913
- base,
914
- offer,
915
- refreshOffer,
916
- onText
1215
+ source,
1216
+ onText,
1217
+ onStart
917
1218
  }) {
918
1219
  const S = avatarMicStrings(locale);
919
- const copy = avatarMicNotice(AVATAR_MIC_NOTICE_VERSION, locale);
1220
+ const noticeVersion = source === null ? null : noticeOf(source);
1221
+ const copy = noticeVersion === null ? null : avatarMicNotice(noticeVersion, locale);
1222
+ const kind = source?.kind ?? null;
1223
+ const micKey = source === null ? null : micKeyOf(source, locale);
920
1224
  const uid = react.useId();
921
1225
  const noticeTitleId = `${uid}-mic-notice`;
922
- const [withdrawn, setWithdrawn] = react.useState(null);
923
- const [supported, setSupported] = react.useState(false);
924
- react.useEffect(() => setSupported(canUseAvatarMic()), []);
925
- const possible = enabled && supported && base !== null && S !== null && copy !== null;
926
- const offered = possible && withdrawn === null;
1226
+ const [withdrawn, setWithdrawn] = react.useState({});
1227
+ const [supported, setSupported] = react.useState({ stream: false, browser: false });
1228
+ react.useEffect(
1229
+ () => setSupported({ stream: canUseAvatarMic(), browser: canUseBrowserMic() }),
1230
+ []
1231
+ );
1232
+ const usable = source === null ? false : source.kind === "stream" ? supported.stream : supported.browser && BROWSER_MIC_LANGUAGES[locale] !== void 0;
1233
+ const possible = enabled && kind !== null && usable && S !== null && copy !== null;
1234
+ const withdrawnReason = kind === null ? null : withdrawn[kind] ?? null;
1235
+ const offered = possible && withdrawnReason === null;
927
1236
  const [state, setState] = react.useState(IDLE);
928
1237
  const [noticeOpen, setNoticeOpen] = react.useState(false);
929
- const agreedRef = react.useRef(false);
1238
+ const agreedRef = react.useRef(null);
930
1239
  const [loading2, setLoading] = react.useState(false);
931
1240
  const [loadFailed, setLoadFailed] = react.useState(false);
932
1241
  const [now, setNow] = react.useState(() => Date.now());
933
- const micRef = react.useRef(null);
1242
+ const micRef = react.useRef(
1243
+ null
1244
+ );
934
1245
  const seqRef = react.useRef(0);
935
1246
  const buttonRef = react.useRef(null);
936
1247
  const allowRef = react.useRef(null);
937
- const usedEpochRef = react.useRef(0);
938
- const latest = react.useRef({ offer, refreshOffer, onText, base });
939
- latest.current = { offer, refreshOffer, onText, base };
1248
+ const usedRef = react.useRef(null);
1249
+ const latest = react.useRef({ source, locale, noticeVersion, onText, onStart });
1250
+ latest.current = { source, locale, noticeVersion, onText, onStart };
940
1251
  const cancel = react.useCallback(() => {
941
1252
  seqRef.current += 1;
942
1253
  setLoading(false);
@@ -947,6 +1258,12 @@ function useAvatarMic({
947
1258
  react.useEffect(() => {
948
1259
  if (!offered) cancel();
949
1260
  }, [offered, cancel]);
1261
+ const keyRef = react.useRef(micKey);
1262
+ react.useEffect(() => {
1263
+ if (keyRef.current === micKey) return;
1264
+ keyRef.current = micKey;
1265
+ cancel();
1266
+ }, [micKey, cancel]);
950
1267
  react.useEffect(
951
1268
  () => () => {
952
1269
  seqRef.current += 1;
@@ -954,29 +1271,55 @@ function useAvatarMic({
954
1271
  },
955
1272
  []
956
1273
  );
957
- const micFor = react.useCallback(async (target) => {
958
- const held = micRef.current;
959
- if (held !== null && held.base === target) return held.mic;
960
- held?.mic.cancel();
961
- const loaded = await loadAvatarMic();
962
- const mic = loaded.createAvatarMic({
963
- base: target,
964
- notice: AVATAR_MIC_NOTICE_VERSION,
965
- offer: () => latest.current.offer,
966
- refreshOffer: () => latest.current.refreshOffer(),
967
- onState: setState
968
- });
969
- micRef.current = { base: target, mic };
970
- return mic;
971
- }, []);
1274
+ const micOf = react.useCallback(
1275
+ (loaded, wanted) => {
1276
+ const key = micKeyOf(wanted, latest.current.locale);
1277
+ const held = micRef.current;
1278
+ if (held !== null && held.key === key) return held.mic;
1279
+ held?.mic.cancel();
1280
+ let mic;
1281
+ if (wanted.kind === "stream") {
1282
+ const stream = () => {
1283
+ const current = latest.current.source;
1284
+ return current?.kind === "stream" ? current : null;
1285
+ };
1286
+ mic = loaded.createAvatarMic({
1287
+ base: wanted.base,
1288
+ notice: AVATAR_MIC_NOTICE_VERSION,
1289
+ offer: () => stream()?.offer ?? null,
1290
+ refreshOffer: () => stream()?.refreshOffer() ?? Promise.resolve(null),
1291
+ onState: setState
1292
+ });
1293
+ } else {
1294
+ const language = BROWSER_MIC_LANGUAGES[latest.current.locale] ?? EN_IN;
1295
+ mic = loaded.createBrowserAvatarMic({
1296
+ lang: language.lang,
1297
+ fallbackLang: language.fallbackLang,
1298
+ maxPressMs: LIMIT_SECONDS * 1e3,
1299
+ onState: setState
1300
+ });
1301
+ }
1302
+ micRef.current = { key, kind: wanted.kind, mic };
1303
+ return mic;
1304
+ },
1305
+ []
1306
+ );
972
1307
  const press = react.useCallback(() => {
973
- const target = latest.current.base;
974
- if (target === null) return;
1308
+ const wanted = latest.current.source;
1309
+ if (wanted === null) return;
975
1310
  seqRef.current += 1;
976
1311
  const seq = seqRef.current;
977
1312
  setLoadFailed(false);
1313
+ const held = micRef.current;
1314
+ const ready = wanted.kind !== "browser" ? null : held !== null && held.key === micKeyOf(wanted, latest.current.locale) ? held.mic : loadedMic !== null ? micOf(loadedMic, wanted) : null;
1315
+ if (ready !== null) {
1316
+ setLoading(false);
1317
+ ready.start();
1318
+ return;
1319
+ }
978
1320
  setLoading(true);
979
- void micFor(target).then(
1321
+ const arriving = held !== null && held.key === micKeyOf(wanted, latest.current.locale) ? Promise.resolve(held.mic) : loadAvatarMic().then((loaded) => micOf(loaded, wanted));
1322
+ void arriving.then(
980
1323
  (mic) => {
981
1324
  if (seqRef.current !== seq) return;
982
1325
  setLoading(false);
@@ -988,8 +1331,9 @@ function useAvatarMic({
988
1331
  setLoadFailed(true);
989
1332
  }
990
1333
  );
991
- }, [micFor]);
1334
+ }, [micOf]);
992
1335
  const recording = loading2 || state.name === "starting" || state.name === "listening";
1336
+ const finishing = kind === "browser" && state.name === "finishing";
993
1337
  const tap = react.useCallback(() => {
994
1338
  if (state.name === "listening" || state.name === "starting") {
995
1339
  micRef.current?.mic.stop();
@@ -999,15 +1343,26 @@ function useAvatarMic({
999
1343
  cancel();
1000
1344
  return;
1001
1345
  }
1002
- if (!agreedRef.current) {
1346
+ if (finishing) return;
1347
+ latest.current.onStart?.();
1348
+ if (agreedRef.current !== latest.current.noticeVersion) {
1003
1349
  setNoticeOpen(true);
1004
- void loadAvatarMic().catch(() => void 0);
1350
+ const seq = seqRef.current;
1351
+ void loadAvatarMic().then(
1352
+ (loaded) => {
1353
+ const wanted = latest.current.source;
1354
+ if (seqRef.current === seq && wanted?.kind === "browser") {
1355
+ micOf(loaded, wanted);
1356
+ }
1357
+ },
1358
+ () => void 0
1359
+ );
1005
1360
  return;
1006
1361
  }
1007
1362
  press();
1008
- }, [cancel, loading2, press, state.name]);
1363
+ }, [cancel, finishing, loading2, micOf, press, state.name]);
1009
1364
  const allow = react.useCallback(() => {
1010
- agreedRef.current = true;
1365
+ agreedRef.current = latest.current.noticeVersion;
1011
1366
  setNoticeOpen(false);
1012
1367
  buttonRef.current?.focus();
1013
1368
  press();
@@ -1027,17 +1382,27 @@ function useAvatarMic({
1027
1382
  return () => clearInterval(timer);
1028
1383
  }, [listeningSince]);
1029
1384
  react.useEffect(() => {
1030
- if (state.name === "done" && state.epoch !== usedEpochRef.current) {
1031
- usedEpochRef.current = state.epoch;
1385
+ if (state.name === "done" && state !== usedRef.current) {
1386
+ usedRef.current = state;
1032
1387
  if (state.text !== "") latest.current.onText(state.text);
1033
- } else if (state.name === "failed" && FOR_GOOD.has(state.reason)) {
1034
- setWithdrawn(state.reason);
1388
+ return;
1389
+ }
1390
+ const pressed = micRef.current?.kind;
1391
+ if (state.name === "failed" && pressed !== void 0 && FOR_GOOD[pressed].has(state.reason)) {
1392
+ const reason = state.reason;
1393
+ setWithdrawn((held) => ({ ...held, [pressed]: reason }));
1035
1394
  }
1036
1395
  }, [state]);
1037
1396
  if (!possible || S === null || copy === null) {
1038
- return { button: null, notice: null, lines: null, cancel };
1397
+ return {
1398
+ button: null,
1399
+ notice: null,
1400
+ lines: null,
1401
+ cancel,
1402
+ recording: false
1403
+ };
1039
1404
  }
1040
- const status = withdrawn !== null ? S[FAILURE_TEXT[withdrawn]] : loading2 ? S.micStarting : loadFailed ? S.micFailed : state.name === "starting" ? S.micStarting : state.name === "listening" ? S.micListening : state.name === "finishing" ? S.micFinishing : state.name === "done" ? state.text === "" ? S.micNoSpeech : state.limit ? S.micDoneLimit : S.micDone : state.name === "failed" ? S[FAILURE_TEXT[state.reason]] : "";
1405
+ const status = withdrawnReason !== null ? S[FAILURE_TEXT[withdrawnReason]] : loading2 ? S.micStarting : loadFailed ? S.micFailed : state.name === "starting" ? S.micStarting : state.name === "listening" ? S.micListening : state.name === "finishing" ? S.micFinishing : state.name === "done" ? state.text === "" ? S.micNoSpeech : state.limit ? S.micDoneLimit : S.micDone : state.name === "failed" ? S[FAILURE_TEXT[state.reason]] : "";
1041
1406
  const heard = offered && (state.name === "listening" || state.name === "finishing") ? state.text : "";
1042
1407
  const button = !offered ? null : /* @__PURE__ */ jsxRuntime.jsx(
1043
1408
  "button",
@@ -1046,12 +1411,15 @@ function useAvatarMic({
1046
1411
  type: "button",
1047
1412
  "aria-label": recording ? S.micStop : S.micStart,
1048
1413
  "aria-pressed": recording,
1414
+ "aria-disabled": finishing ? true : void 0,
1049
1415
  title: recording ? S.micStop : S.micStart,
1050
1416
  "data-boff-explore": "avatar-live-mic",
1051
1417
  "data-mic": loading2 ? "starting" : state.name,
1418
+ "data-mic-source": kind ?? void 0,
1052
1419
  className: chunkC6OJIHNO_js.cn(
1053
1420
  "inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background motion-reduce:transition-none",
1054
- recording ? "border-primary bg-primary text-primary-foreground" : "border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground"
1421
+ recording ? "border-primary bg-primary text-primary-foreground" : "border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
1422
+ finishing && "cursor-wait opacity-50"
1055
1423
  ),
1056
1424
  onClick: tap,
1057
1425
  children: recording ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Square, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Mic, { "aria-hidden": "true", className: "h-4 w-4" })
@@ -1134,12 +1502,22 @@ function useAvatarMic({
1134
1502
  {
1135
1503
  "data-boff-explore": "avatar-live-mic-heard",
1136
1504
  dir: "auto",
1137
- className: "line-clamp-3 break-words text-sm italic text-muted-foreground",
1505
+ className: chunkC6OJIHNO_js.cn(
1506
+ // rrweb's default block and mask classes: a site's session replay records none of what is heard.
1507
+ REPLAY_PRIVATE,
1508
+ "line-clamp-3 break-words text-sm italic text-muted-foreground"
1509
+ ),
1138
1510
  children: heard
1139
1511
  }
1140
1512
  )
1141
1513
  ] });
1142
- return { button, notice, lines, cancel };
1514
+ return {
1515
+ button,
1516
+ notice,
1517
+ lines,
1518
+ cancel,
1519
+ recording: offered && state.name === "listening"
1520
+ };
1143
1521
  }
1144
1522
 
1145
1523
  // src/components/explore-avatar-turnstile.ts
@@ -1262,6 +1640,13 @@ var END_GRACE_MS = 1e4;
1262
1640
  var CLOCK_TOLERANCE_MS = 5e3;
1263
1641
  var UNHEARD_GRACE_MS = 5e3;
1264
1642
  var PLAYOUT_TAIL_MS = 5e3;
1643
+ var QUIET_PLAYED_OUT_MS = 2500;
1644
+ var OWED_VOICE_WAIT_MS = 15e3;
1645
+ var LISTENING_MARK_MS = 1e4;
1646
+ var LISTENING_MARK_SPACING_MS = 5e3;
1647
+ var BUSY_RETRY_MS = 2e3;
1648
+ var BUSY_RETRY_MAX_MS = 8e3;
1649
+ var MAX_BUSY_RETRIES = 24;
1265
1650
  var SERVICE_MAX_AGE_MS = 15 * 6e4;
1266
1651
  var ANSWER_OFFER_MS = SERVICE_MAX_AGE_MS - 6e4;
1267
1652
  var MAX_TIMER_MS = 2147483647;
@@ -1276,6 +1661,12 @@ function stopAttending(handle) {
1276
1661
  if (handle.tail !== null) clearTimeout(handle.tail);
1277
1662
  handle.unheard = null;
1278
1663
  handle.tail = null;
1664
+ const run = handle.followUps;
1665
+ if (run === null) return;
1666
+ if (run.quiet !== null) clearTimeout(run.quiet);
1667
+ if (run.retry !== null) clearTimeout(run.retry);
1668
+ run.quiet = null;
1669
+ run.retry = null;
1279
1670
  }
1280
1671
  var REFUSAL_TEXT = {
1281
1672
  busy: "busy",
@@ -1286,6 +1677,18 @@ var REFUSAL_TEXT = {
1286
1677
  not_eligible: "notEligible",
1287
1678
  answer_claimed: "answerClaimed"
1288
1679
  };
1680
+ var FOLLOW_UP_TEXT = {
1681
+ no_time: "followUpNoTime",
1682
+ max_answers: "followUpMaxAnswers",
1683
+ // The same line as a first answer the checks refuse: it stays in Chat as text.
1684
+ not_eligible: "notSpoken",
1685
+ answer_claimed: "answerClaimed",
1686
+ limits: "followUpLimits",
1687
+ paused: "followUpPaused",
1688
+ busy: "followUpUnread",
1689
+ answer_failed: "answerFailed",
1690
+ failed: "followUpUnread"
1691
+ };
1289
1692
  var EMPTY = {
1290
1693
  kind: "empty",
1291
1694
  answerId: null,
@@ -1360,10 +1763,21 @@ function inFlight(conversation) {
1360
1763
  return conversation?.phase === "sending" || conversation?.phase === "streaming";
1361
1764
  }
1362
1765
  function acceptsQuestion(slot) {
1363
- return slot.any && !slot.textMode && slot.phase === "needs-answer" && slot.kind === "empty" && slot.reason === null;
1766
+ if (!slot.any || slot.textMode || slot.reason !== null) return false;
1767
+ return slot.phase === "needs-answer" ? slot.kind === "empty" : slot.phase === "live" && slot.followUp;
1364
1768
  }
1365
- function micOfferOf(result) {
1366
- return result.kind === "available" && result.offer.stt !== void 0 ? { ...result.offer.stt, receivedAt: Date.now() } : null;
1769
+ function followUpBox(phase, run, token) {
1770
+ return phase.name === "live" && run !== null && phase.session.followUp !== null && !phase.session.followUp.closed && token === run.token;
1771
+ }
1772
+ var NO_MIC = { kind: "none" };
1773
+ function micChoiceOf(result, locale) {
1774
+ if (result.kind !== "available") return NO_MIC;
1775
+ const { offer } = result;
1776
+ const micNotice = avatarConsentCopy(offer.consentVersion, locale)?.micNotice;
1777
+ if (offer.mic !== void 0) {
1778
+ return offer.mic.mode === "browser" && micNotice !== void 0 ? { kind: "browser", notice: micNotice } : NO_MIC;
1779
+ }
1780
+ return offer.stt !== void 0 && micNotice === void 0 ? { kind: "stream", offer: { ...offer.stt, receivedAt: Date.now() } } : NO_MIC;
1367
1781
  }
1368
1782
  function utcToday() {
1369
1783
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -1513,8 +1927,9 @@ function AvatarLiveView({
1513
1927
  const waitRef = react.useRef(null);
1514
1928
  const sendingRef = react.useRef(false);
1515
1929
  const textBoxRef = react.useRef(false);
1516
- const [micOffer, setMicOffer] = react.useState(null);
1930
+ const [micChoice, setMicChoice] = react.useState(NO_MIC);
1517
1931
  const micCancelRef = react.useRef(() => void 0);
1932
+ const sendDueRef = react.useRef(() => void 0);
1518
1933
  const checkRef = react.useRef(null);
1519
1934
  const askedLocale = react.useRef(locale);
1520
1935
  const startRef = react.useRef(null);
@@ -1603,10 +2018,13 @@ function AvatarLiveView({
1603
2018
  if (!handle || handle.done || latest.current.live.endUnattended !== true) {
1604
2019
  return;
1605
2020
  }
1606
- const current = phaseRef.current.name;
1607
- if (current !== "pending" && current !== "live") return;
2021
+ const shown2 = phaseRef.current;
2022
+ if (shown2.name !== "pending" && shown2.name !== "live") return;
2023
+ const current = shown2.name;
1608
2024
  const away = !inSight();
1609
- if (away && handle.playedOut && current === "live") {
2025
+ const followUp2 = shown2.session.followUp;
2026
+ const asking = followUp2 !== null && followUp2.pending !== null;
2027
+ if (away && handle.playedOut && current === "live" && !asking) {
1610
2028
  finish("finished", "user");
1611
2029
  return;
1612
2030
  }
@@ -1625,6 +2043,184 @@ function AvatarLiveView({
1625
2043
  }
1626
2044
  }, UNHEARD_GRACE_MS);
1627
2045
  }, [finish, inSight]);
2046
+ const showFollowUp = react.useCallback(
2047
+ (handle, next, spoken) => {
2048
+ const current = phaseRef.current;
2049
+ if (sessionRef.current !== handle || handle.done || current.name !== "pending" && current.name !== "live" || current.session.followUp === null) {
2050
+ return;
2051
+ }
2052
+ const session = {
2053
+ ...current.session,
2054
+ ...spoken,
2055
+ followUp: { ...current.session.followUp, ...next }
2056
+ };
2057
+ go(
2058
+ current.name === "live" ? { name: "live", session } : { name: "pending", session }
2059
+ );
2060
+ },
2061
+ [go]
2062
+ );
2063
+ const tell = react.useCallback(
2064
+ (handle, kind2) => {
2065
+ if (handle.done || handle.followUps === null) return;
2066
+ void requestTurn({
2067
+ base: handle.base,
2068
+ headers: handle.headers,
2069
+ sessionId: handle.session.sessionId,
2070
+ secret: handle.session.endSecret,
2071
+ kind: kind2
2072
+ });
2073
+ },
2074
+ []
2075
+ );
2076
+ const whenQuiet = react.useCallback(
2077
+ (handle, delay) => {
2078
+ const run = handle.followUps;
2079
+ if (run === null) return;
2080
+ const wait = (ms) => {
2081
+ if (run.quiet !== null) clearTimeout(run.quiet);
2082
+ run.quiet = setTimeout(() => {
2083
+ run.quiet = null;
2084
+ if (sessionRef.current !== handle || handle.done || run.voice) {
2085
+ return;
2086
+ }
2087
+ if (run.owed) {
2088
+ run.owed = false;
2089
+ wait(OWED_VOICE_WAIT_MS);
2090
+ return;
2091
+ }
2092
+ handle.playedOut = true;
2093
+ showFollowUp(handle, { speaking: false });
2094
+ attend();
2095
+ sendDueRef.current(handle);
2096
+ }, ms);
2097
+ };
2098
+ wait(delay);
2099
+ },
2100
+ [attend, showFollowUp]
2101
+ );
2102
+ const heardVoice = react.useCallback(
2103
+ (handle, speaking) => {
2104
+ const run = handle.followUps;
2105
+ if (run === null) return;
2106
+ run.voice = speaking;
2107
+ if (!speaking) {
2108
+ whenQuiet(handle, QUIET_PLAYED_OUT_MS);
2109
+ return;
2110
+ }
2111
+ if (run.quiet !== null) clearTimeout(run.quiet);
2112
+ run.quiet = null;
2113
+ run.heard = true;
2114
+ run.starts += 1;
2115
+ run.owed = false;
2116
+ handle.playedOut = false;
2117
+ showFollowUp(handle, { speaking: true });
2118
+ },
2119
+ [showFollowUp, whenQuiet]
2120
+ );
2121
+ const sendDue = react.useCallback(
2122
+ (handle) => {
2123
+ const run = handle.followUps;
2124
+ const due = run?.due ?? null;
2125
+ if (run === null || due === null || due.sending || run.retry !== null || handle.done || sessionRef.current !== handle || run.heard && !handle.playedOut) {
2126
+ return;
2127
+ }
2128
+ due.sending = true;
2129
+ const starts = run.starts;
2130
+ const read = () => requestTurn({
2131
+ base: handle.base,
2132
+ headers: handle.headers,
2133
+ sessionId: handle.session.sessionId,
2134
+ secret: handle.session.endSecret,
2135
+ kind: "answer",
2136
+ messageId: due.messageId,
2137
+ idempotencyKey: due.idempotencyKey
2138
+ });
2139
+ const notRead = (unread, closed) => {
2140
+ run.due = null;
2141
+ showFollowUp(handle, {
2142
+ pending: null,
2143
+ unread: due.known ?? unread,
2144
+ ...closed ? { closed } : {}
2145
+ });
2146
+ attend();
2147
+ };
2148
+ void (async () => {
2149
+ let result = await read();
2150
+ if (result.kind === "failed" && result.lost && !handle.done) {
2151
+ result = await read();
2152
+ }
2153
+ due.sending = false;
2154
+ if (sessionRef.current !== handle || handle.done || run.due !== due) {
2155
+ return;
2156
+ }
2157
+ if (result.kind === "refused" && result.reason === "busy" && due.busy < MAX_BUSY_RETRIES) {
2158
+ due.busy += 1;
2159
+ if (run.heard && !handle.playedOut) return;
2160
+ const delay = Math.max(
2161
+ result.retryAfterSeconds === void 0 ? BUSY_RETRY_MS : result.retryAfterSeconds * 1e3,
2162
+ Math.min(BUSY_RETRY_MAX_MS, BUSY_RETRY_MS * 2 ** (due.busy - 1))
2163
+ );
2164
+ if (Date.now() + delay >= handle.endsAt) {
2165
+ notRead("no_time", false);
2166
+ return;
2167
+ }
2168
+ run.retry = setTimeout(() => {
2169
+ run.retry = null;
2170
+ sendDueRef.current(handle);
2171
+ }, delay);
2172
+ return;
2173
+ }
2174
+ if (result.kind !== "accepted") {
2175
+ notRead(
2176
+ result.kind === "refused" ? result.reason : "failed",
2177
+ result.kind === "gone" || result.kind === "refused" && result.reason === "max_answers"
2178
+ );
2179
+ return;
2180
+ }
2181
+ run.due = null;
2182
+ run.answers += 1;
2183
+ const heardSince = run.starts !== starts;
2184
+ run.owed = !heardSince;
2185
+ const playing = !heardSince || run.voice || run.quiet !== null;
2186
+ if (playing) handle.playedOut = false;
2187
+ showFollowUp(
2188
+ handle,
2189
+ {
2190
+ speaking: playing,
2191
+ pending: null,
2192
+ unread: null,
2193
+ closed: run.answers >= run.limits.maxAnswers
2194
+ },
2195
+ { captions: result.captions, excerpt: result.excerpt }
2196
+ );
2197
+ if (!playing) {
2198
+ attend();
2199
+ return;
2200
+ }
2201
+ if (run.owed && run.heard && !run.voice) whenQuiet(handle, 0);
2202
+ })();
2203
+ },
2204
+ [attend, showFollowUp, whenQuiet]
2205
+ );
2206
+ sendDueRef.current = sendDue;
2207
+ const readFollowUp = react.useCallback(
2208
+ (handle, messageId, known) => {
2209
+ const run = handle.followUps;
2210
+ if (run === null) return;
2211
+ run.sent.add(messageId);
2212
+ const idempotencyKey = newIdempotencyKey();
2213
+ if (idempotencyKey === null) {
2214
+ showFollowUp(handle, { pending: null, unread: known ?? "failed" });
2215
+ attend();
2216
+ return;
2217
+ }
2218
+ run.due = { messageId, idempotencyKey, known, busy: 0, sending: false };
2219
+ showFollowUp(handle, { pending: "reading", unread: null });
2220
+ sendDue(handle);
2221
+ },
2222
+ [attend, sendDue, showFollowUp]
2223
+ );
1628
2224
  const cancelStart = react.useCallback((reason) => {
1629
2225
  const run = startRef.current;
1630
2226
  if (!run) return;
@@ -1635,7 +2231,7 @@ function AvatarLiveView({
1635
2231
  const applyAvailability = react.useCallback(
1636
2232
  (result, locale2) => {
1637
2233
  const { live: live2 } = latest.current;
1638
- setMicOffer(micOfferOf(result));
2234
+ setMicChoice(micChoiceOf(result, locale2));
1639
2235
  if (result.kind === "inert") {
1640
2236
  setMode("suggested");
1641
2237
  go(INERT2);
@@ -1653,7 +2249,7 @@ function AvatarLiveView({
1653
2249
  go(INERT2);
1654
2250
  return;
1655
2251
  }
1656
- if (result.offer.botCheck === "turnstile" && chunkXIZE37IB_js.turnstileSiteKeyOf(live2) === null) {
2252
+ if (result.offer.botCheck === "turnstile" && chunk3AIAJ2L4_js.turnstileSiteKeyOf(live2) === null) {
1657
2253
  setMode("suggested");
1658
2254
  go(INERT2);
1659
2255
  return;
@@ -1688,16 +2284,33 @@ function AvatarLiveView({
1688
2284
  });
1689
2285
  }, [applyAvailability, go]);
1690
2286
  const begin = react.useCallback(
1691
- (session, module, offer, target) => {
2287
+ (session, context) => {
2288
+ const { offer } = context;
1692
2289
  const received = Date.now();
1693
2290
  const longest = offer.maxSessionSeconds * 1e3;
1694
2291
  const ahead = session.endsAt - received;
1695
2292
  const endsAt = ahead > 0 && ahead <= longest + CLOCK_TOLERANCE_MS ? session.endsAt : received + longest;
2293
+ const followUps = session.followUps !== void 0 && offer.copy.followUps === true && offer.questions === "any" ? {
2294
+ limits: session.followUps,
2295
+ token: context.conversationToken,
2296
+ answers: 1,
2297
+ voice: false,
2298
+ heard: false,
2299
+ starts: 0,
2300
+ owed: true,
2301
+ quiet: null,
2302
+ asked: null,
2303
+ due: null,
2304
+ retry: null,
2305
+ sent: /* @__PURE__ */ new Set([context.messageId]),
2306
+ seq: 0,
2307
+ listenedAt: null
2308
+ } : null;
1696
2309
  sessionRef.current = {
1697
2310
  session,
1698
- base: target.base,
1699
- headers: target.headers,
1700
- module,
2311
+ base: context.base,
2312
+ headers: context.headers,
2313
+ module: context.module,
1701
2314
  room: null,
1702
2315
  joined: null,
1703
2316
  endsAt,
@@ -1707,6 +2320,7 @@ function AvatarLiveView({
1707
2320
  playedOut: false,
1708
2321
  unheard: null,
1709
2322
  tail: null,
2323
+ followUps,
1710
2324
  done: false
1711
2325
  };
1712
2326
  setBlocked(false);
@@ -1716,7 +2330,13 @@ function AvatarLiveView({
1716
2330
  session: {
1717
2331
  endsAt,
1718
2332
  captions: session.captions,
1719
- excerpt: session.excerpt
2333
+ excerpt: session.excerpt,
2334
+ followUp: followUps === null ? null : {
2335
+ speaking: true,
2336
+ pending: null,
2337
+ unread: null,
2338
+ closed: followUps.answers >= followUps.limits.maxAnswers
2339
+ }
1720
2340
  }
1721
2341
  });
1722
2342
  },
@@ -1748,7 +2368,7 @@ function AvatarLiveView({
1748
2368
  }
1749
2369
  switch (result.kind) {
1750
2370
  case "created":
1751
- begin(result.session, context.module, context.offer, context);
2371
+ begin(result.session, context);
1752
2372
  return;
1753
2373
  case "unusable":
1754
2374
  sendEnd({
@@ -1790,7 +2410,7 @@ function AvatarLiveView({
1790
2410
  return;
1791
2411
  }
1792
2412
  const refusedFrom = exchangeAtStart.offerUntil - ANSWER_OFFER_MS + SERVICE_MAX_AGE_MS;
1793
- const siteKey = offer.botCheck === "turnstile" ? chunkXIZE37IB_js.turnstileSiteKeyOf(live2) : null;
2413
+ const siteKey = offer.botCheck === "turnstile" ? chunk3AIAJ2L4_js.turnstileSiteKeyOf(live2) : null;
1794
2414
  if (offer.botCheck === "turnstile" && (siteKey === null || !canBindBotCheck())) {
1795
2415
  go(INERT2);
1796
2416
  return;
@@ -1846,7 +2466,14 @@ function AvatarLiveView({
1846
2466
  return;
1847
2467
  }
1848
2468
  if (turnstile !== null) go({ name: "creating" });
1849
- const context = { module: loaded, offer, messageId, base: base2, headers: headers2 };
2469
+ const context = {
2470
+ module: loaded,
2471
+ offer,
2472
+ messageId,
2473
+ conversationToken,
2474
+ base: base2,
2475
+ headers: headers2
2476
+ };
1850
2477
  const result = await requestCreate({
1851
2478
  base: base2,
1852
2479
  headers: headers2,
@@ -2151,6 +2778,9 @@ function AvatarLiveView({
2151
2778
  attend();
2152
2779
  }, PLAYOUT_TAIL_MS);
2153
2780
  return;
2781
+ case "speaking":
2782
+ heardVoice(handle, event.speaking);
2783
+ return;
2154
2784
  case "speaker-left":
2155
2785
  finish(
2156
2786
  wasLive ? "finished" : "failed",
@@ -2163,7 +2793,7 @@ function AvatarLiveView({
2163
2793
  return;
2164
2794
  }
2165
2795
  },
2166
- [attend, finish, goLive, inSight]
2796
+ [attend, finish, goLive, heardVoice, inSight]
2167
2797
  );
2168
2798
  react.useEffect(() => {
2169
2799
  if (phase.name !== "pending") return;
@@ -2299,20 +2929,28 @@ function AvatarLiveView({
2299
2929
  const textMode = any && (usedToday && (phase.name === "needs-answer" || phase.name === "consent" || phase.name === "declined" || phase.name === "ended") || phase.name === "refused" && phase.reason === "limits");
2300
2930
  textBoxRef.current = textMode;
2301
2931
  const askState = !any ? void 0 : textMode ? "text" : phase.name === "needs-answer" ? kind === "empty" ? "ready" : kind : phase.name;
2302
- const askReason = inFlight(conversation) ? S.askBusy : conversation?.canSend ? null : S.askUnavailable;
2932
+ const followUp = phase.name === "live" ? phase.session.followUp : null;
2933
+ const followUpAsks = followUpBox(
2934
+ phase,
2935
+ sessionRef.current?.followUps ?? null,
2936
+ token
2937
+ );
2938
+ const askReason = inFlight(conversation) || followUp !== null && followUp.pending !== null ? S.askBusy : conversation?.canSend ? null : S.askUnavailable;
2303
2939
  const refreshMicOffer = react.useCallback(async () => {
2304
2940
  const { live: live2, locale: locale2, base: base2, headers: headers2 } = latest.current;
2305
2941
  if (base2 === null) return null;
2306
- const next = micOfferOf(
2942
+ const next = micChoiceOf(
2307
2943
  await requestAvailabilityAnswer({
2308
2944
  base: base2,
2309
2945
  headers: headers2,
2310
2946
  site: live2.site,
2311
2947
  locale: locale2
2312
- })
2948
+ }),
2949
+ locale2
2313
2950
  );
2314
- if (next !== null) setMicOffer(next);
2315
- return next;
2951
+ if (next.kind !== "stream") return null;
2952
+ setMicChoice(next);
2953
+ return next.offer;
2316
2954
  }, []);
2317
2955
  const onMicText = react.useCallback(
2318
2956
  (text) => {
@@ -2324,21 +2962,66 @@ function AvatarLiveView({
2324
2962
  },
2325
2963
  [askMaxLength, touchFirst]
2326
2964
  );
2965
+ const onMicStart = react.useCallback(() => {
2966
+ const handle = sessionRef.current;
2967
+ const current = phaseRef.current;
2968
+ if (handle === null || handle.followUps === null || handle.done || current.name !== "live" || current.session.followUp?.speaking !== true) {
2969
+ return;
2970
+ }
2971
+ const run = handle.followUps;
2972
+ tell(handle, "interrupt");
2973
+ if (run.quiet !== null) clearTimeout(run.quiet);
2974
+ run.quiet = null;
2975
+ run.owed = false;
2976
+ handle.playedOut = true;
2977
+ showFollowUp(handle, { speaking: false });
2978
+ }, [showFollowUp, tell]);
2979
+ const micSource = micChoice.kind === "browser" ? { kind: "browser", notice: micChoice.notice } : micChoice.kind === "stream" && base !== null ? {
2980
+ kind: "stream",
2981
+ base,
2982
+ offer: micChoice.offer,
2983
+ refreshOffer: refreshMicOffer
2984
+ } : null;
2327
2985
  const mic = useAvatarMic({
2328
- enabled: live.mic === true && micOffer !== null && !off && acceptsQuestion({
2986
+ enabled: live.mic === true && micSource !== null && !off && acceptsQuestion({
2329
2987
  any,
2330
2988
  phase: phase.name,
2331
2989
  kind,
2332
2990
  textMode,
2333
- reason: askReason
2991
+ reason: askReason,
2992
+ followUp: followUpAsks
2334
2993
  }),
2335
2994
  locale,
2336
- base,
2337
- offer: micOffer,
2338
- refreshOffer: refreshMicOffer,
2339
- onText: onMicText
2995
+ source: micSource,
2996
+ onText: onMicText,
2997
+ onStart: onMicStart
2340
2998
  });
2341
2999
  micCancelRef.current = mic.cancel;
3000
+ const micRecording = mic.recording;
3001
+ react.useEffect(() => {
3002
+ const handle = sessionRef.current;
3003
+ if (!micRecording || handle === null || handle.followUps === null || handle.done) {
3004
+ return;
3005
+ }
3006
+ const run = handle.followUps;
3007
+ const every = Math.min(
3008
+ LISTENING_MARK_MS,
3009
+ Math.max(1e3, run.limits.idleSeconds * 1e3 / 3)
3010
+ );
3011
+ const spacing = Math.min(LISTENING_MARK_SPACING_MS, every / 2);
3012
+ const mark = () => {
3013
+ const at = Date.now();
3014
+ if (run.listenedAt !== null && at - run.listenedAt < spacing) return;
3015
+ run.listenedAt = at;
3016
+ tell(handle, "listening");
3017
+ };
3018
+ mark();
3019
+ const timer = setInterval(mark, every);
3020
+ return () => {
3021
+ clearInterval(timer);
3022
+ if (sessionRef.current === handle) mark();
3023
+ };
3024
+ }, [micRecording, tell]);
2342
3025
  const notNow = react.useCallback(() => {
2343
3026
  const current = phaseRef.current;
2344
3027
  if ((current.name === "consent" || current.name === "needs-answer") && current.offer.questions === "any") {
@@ -2347,6 +3030,43 @@ function AvatarLiveView({
2347
3030
  }
2348
3031
  onContinueInChat();
2349
3032
  }, [go, onContinueInChat]);
3033
+ const askFollowUp = react.useCallback(
3034
+ (handle, conversation2, question) => {
3035
+ const run = handle.followUps;
3036
+ const current = phaseRef.current;
3037
+ if (run === null || handle.done || current.name !== "live" || current.session.followUp?.pending !== null || !followUpBox(current, run, conversation2.getConversationToken())) {
3038
+ return;
3039
+ }
3040
+ sendingRef.current = true;
3041
+ run.seq += 1;
3042
+ const seq = run.seq;
3043
+ setDraft("");
3044
+ tell(handle, "asking");
3045
+ showFollowUp(handle, { pending: "asking", unread: null });
3046
+ void conversation2.send(question, [], { surface: "AVATAR" }).then((added) => {
3047
+ if (added === null) {
3048
+ setDraft((current2) => current2 === "" ? question : current2);
3049
+ }
3050
+ if (sessionRef.current !== handle || handle.done || run.seq !== seq) {
3051
+ return;
3052
+ }
3053
+ if (added === null || added.conversationToken !== run.token) {
3054
+ showFollowUp(handle, { pending: null });
3055
+ attend();
3056
+ return;
3057
+ }
3058
+ run.asked = {
3059
+ messageId: added.userMessageId,
3060
+ token: added.conversationToken,
3061
+ answerId: null
3062
+ };
3063
+ showFollowUp(handle, { pending: "asking" });
3064
+ }).finally(() => {
3065
+ sendingRef.current = false;
3066
+ });
3067
+ },
3068
+ [attend, showFollowUp, tell]
3069
+ );
2350
3070
  const ask = react.useCallback(
2351
3071
  (text) => {
2352
3072
  const conversation2 = conversationRef.current;
@@ -2355,6 +3075,11 @@ function AvatarLiveView({
2355
3075
  return;
2356
3076
  }
2357
3077
  const spoken = !textBoxRef.current;
3078
+ const handle = sessionRef.current;
3079
+ if (spoken && handle !== null && handle.followUps !== null) {
3080
+ askFollowUp(handle, conversation2, question);
3081
+ return;
3082
+ }
2358
3083
  if (spoken && conversation2.messages.length > 0) return;
2359
3084
  sendingRef.current = true;
2360
3085
  setAskedHere(null);
@@ -2376,8 +3101,64 @@ function AvatarLiveView({
2376
3101
  sendingRef.current = false;
2377
3102
  });
2378
3103
  },
2379
- [setAskedHere]
3104
+ [askFollowUp, setAskedHere]
2380
3105
  );
3106
+ react.useEffect(() => {
3107
+ const handle = sessionRef.current;
3108
+ const run = handle?.followUps ?? null;
3109
+ if (handle === null || run === null || handle.done || run.asked === null) {
3110
+ return;
3111
+ }
3112
+ const asked = run.asked;
3113
+ const chat = conversationRef.current;
3114
+ const messages = chat?.messages ?? [];
3115
+ const at = messages.findIndex((message) => message.id === asked.messageId);
3116
+ const question = at === -1 ? void 0 : messages[at];
3117
+ const answer = at === -1 ? void 0 : messages[at + 1];
3118
+ const drop = (unread2) => {
3119
+ run.asked = null;
3120
+ showFollowUp(handle, { pending: null, unread: unread2 });
3121
+ attend();
3122
+ };
3123
+ const unread = (id, reason) => {
3124
+ if (id === null || run.sent.has(id) || !isAvatarMessageId(id)) {
3125
+ drop(reason);
3126
+ return;
3127
+ }
3128
+ run.asked = null;
3129
+ readFollowUp(handle, id, reason);
3130
+ };
3131
+ if (question === void 0 || (chat?.getConversationToken() ?? null) !== run.token) {
3132
+ drop(null);
3133
+ return;
3134
+ }
3135
+ if (answer === void 0) {
3136
+ if (!inFlight(chat)) unread(asked.answerId, "answer_failed");
3137
+ return;
3138
+ }
3139
+ if (answer.status === "PENDING" || answer.status === "RUNNING") {
3140
+ if (answer.role === "ASSISTANT") asked.answerId = answer.id;
3141
+ return;
3142
+ }
3143
+ if (answer.role !== "ASSISTANT") {
3144
+ unread(asked.answerId, "answer_failed");
3145
+ return;
3146
+ }
3147
+ if (answer.status !== "COMPLETED" || answer.content.trim() === "") {
3148
+ unread(answer.id, "answer_failed");
3149
+ return;
3150
+ }
3151
+ if (run.sent.has(answer.id) || !isAvatarMessageId(answer.id)) {
3152
+ drop("not_eligible");
3153
+ return;
3154
+ }
3155
+ if (at + 2 !== messages.length || !readable(question, answer)) {
3156
+ unread(answer.id, "not_eligible");
3157
+ return;
3158
+ }
3159
+ run.asked = null;
3160
+ readFollowUp(handle, answer.id, null);
3161
+ });
2381
3162
  const newChatForAvatar = react.useCallback(() => {
2382
3163
  const conversation2 = conversationRef.current;
2383
3164
  if (conversation2 === null) return;
@@ -2401,7 +3182,14 @@ function AvatarLiveView({
2401
3182
  const focused = document.activeElement;
2402
3183
  if (focused && focused !== document.body && focused.isConnected) return;
2403
3184
  rootRef.current?.querySelector(PRIMARY)?.focus();
2404
- }, [active, phase.name, askState, shown.pendingAnswerId, blocked]);
3185
+ }, [
3186
+ active,
3187
+ phase.name,
3188
+ askState,
3189
+ shown.pendingAnswerId,
3190
+ blocked,
3191
+ followUpAsks
3192
+ ]);
2405
3193
  const onPlaying = goLive;
2406
3194
  const enablePlayback = react.useCallback(() => {
2407
3195
  void sessionRef.current?.joined?.enablePlayback().catch(() => void 0);
@@ -2801,13 +3589,32 @@ function AvatarLiveView({
2801
3589
  actions2 = stopButton;
2802
3590
  break;
2803
3591
  case "pending":
2804
- case "live":
2805
- status = phase.name === "live" ? speakingLine(phase.session) : S.connecting;
3592
+ case "live": {
2806
3593
  top2 = badge;
2807
3594
  media2 = liveMedia(phase.name === "live");
2808
3595
  body2 = liveBody(phase.session);
2809
3596
  actions2 = liveActions(readInChatButton(false));
3597
+ if (followUp === null) {
3598
+ status = phase.name === "live" ? speakingLine(phase.session) : S.connecting;
3599
+ break;
3600
+ }
3601
+ 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;
3602
+ if (!followUp.speaking && followUp.pending === null && followUp.unread !== null && !followUp.closed) {
3603
+ body2 = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3604
+ body2,
3605
+ /* @__PURE__ */ jsxRuntime.jsx(
3606
+ "p",
3607
+ {
3608
+ "data-boff-explore": "avatar-live-idle",
3609
+ className: "text-xs text-muted-foreground",
3610
+ children: S.followUpIdle
3611
+ }
3612
+ )
3613
+ ] });
3614
+ }
3615
+ if (followUpAsks) box = "spoken";
2810
3616
  break;
3617
+ }
2811
3618
  case "ended":
2812
3619
  status = S[phase.how];
2813
3620
  outcome2 = true;
@@ -2855,7 +3662,7 @@ function AvatarLiveView({
2855
3662
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: S.newChatNote })
2856
3663
  ] }) : null,
2857
3664
  chips,
2858
- box !== null && usedToday ? /* @__PURE__ */ jsxRuntime.jsx(
3665
+ box !== null && usedToday && phase.name !== "live" ? /* @__PURE__ */ jsxRuntime.jsx(
2859
3666
  "p",
2860
3667
  {
2861
3668
  "data-boff-explore": "avatar-live-used-today",
@@ -3084,7 +3891,12 @@ function AskBox({
3084
3891
  dir: "auto",
3085
3892
  "data-avatar-primary": touchFirst ? void 0 : "",
3086
3893
  "data-boff-explore": "avatar-live-ask-input",
3087
- className: "max-h-32 min-h-11 flex-1 resize-none rounded-md border border-border bg-background px-3 py-2.5 text-base text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:text-sm",
3894
+ className: chunkC6OJIHNO_js.cn(
3895
+ // Kept out of a site's session replay before Send (rrweb's default classes; `rr-ignore` for its input
3896
+ // events): the text may be what the mic heard, or what the visitor typed.
3897
+ REPLAY_PRIVATE,
3898
+ "rr-ignore max-h-32 min-h-11 flex-1 resize-none rounded-md border border-border bg-background px-3 py-2.5 text-base text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:text-sm"
3899
+ ),
3088
3900
  onChange: (event) => {
3089
3901
  setRefusedFiles(false);
3090
3902
  onChange(event.target.value);