@burdenoff/website-sdk 2026.1104.4 → 2026.1105.2

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 +67 -3
  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 +1059 -45
  11. package/dist/avatar-live-view/index.js.map +1 -1
  12. package/dist/avatar-live-view/index.mjs +1054 -40
  13. package/dist/avatar-live-view/index.mjs.map +1 -1
  14. package/dist/avatar-mic/index.d.mts +193 -0
  15. package/dist/avatar-mic/index.d.ts +193 -0
  16. package/dist/avatar-mic/index.js +738 -0
  17. package/dist/avatar-mic/index.js.map +1 -0
  18. package/dist/avatar-mic/index.mjs +736 -0
  19. package/dist/avatar-mic/index.mjs.map +1 -0
  20. package/dist/{chunk-7MSB3EUI.mjs → chunk-MVNN6LLO.mjs} +4 -3
  21. package/dist/chunk-MVNN6LLO.mjs.map +1 -0
  22. package/dist/{chunk-WSEQ2I6I.js → chunk-XIZE37IB.js} +4 -3
  23. package/dist/chunk-XIZE37IB.js.map +1 -0
  24. package/dist/{explore-avatar-live-config-B6_p0q1c.d.mts → explore-avatar-live-config-BL8FTm0I.d.mts} +9 -1
  25. package/dist/{explore-avatar-live-config-DRpS-mqQ.d.ts → explore-avatar-live-config-auLKxSzx.d.ts} +9 -1
  26. package/dist/index.d.mts +9 -4
  27. package/dist/index.d.ts +9 -4
  28. package/dist/index.js +9 -7
  29. package/dist/index.js.map +1 -1
  30. package/dist/index.mjs +5 -3
  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-7MSB3EUI.mjs.map +0 -1
  36. package/dist/chunk-WSEQ2I6I.js.map +0 -1
@@ -1,14 +1,18 @@
1
- import { turnstileSiteKeyOf, isAvatarLiveLocaleReviewed, isAvatarSite, isAvatarVersion } from '../chunk-7MSB3EUI.mjs';
1
+ import { turnstileSiteKeyOf, isAvatarLiveLocaleReviewed, isAvatarSite, isAvatarVersion } from '../chunk-MVNN6LLO.mjs';
2
2
  import { answerDisplayText, fill } from '../chunk-JZS6FQT6.mjs';
3
3
  import { cn } from '../chunk-R7G6TWIS.mjs';
4
4
  import '../chunk-VNS5WDWO.mjs';
5
5
  import { useId, useState, useRef, useCallback, useEffect } from 'react';
6
+ import { Square, Mic } from 'lucide-react';
6
7
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
8
 
8
9
  // src/components/explore-avatar-api.ts
9
10
  var AVAILABILITY_TIMEOUT_MS = 8e3;
10
11
  var CREATE_TIMEOUT_MS = 3e4;
12
+ var TURN_TIMEOUT_MS = 8e3;
13
+ var TURN_ANSWER_TIMEOUT_MS = 25e3;
11
14
  var MESSAGE_ID = /^[A-Za-z0-9_-]{1,64}$/;
15
+ var STT_TOKEN = /^[A-Za-z0-9._~-]{1,4096}$/;
12
16
  var CONVERSATION_TOKEN = /^[A-Za-z0-9._~+/=-]{16,512}$/;
13
17
  var LOCALE = /^[A-Za-z0-9-]{1,16}$/;
14
18
  var SESSION_ID = /^avs_[0-9a-f]{32}$/;
@@ -56,6 +60,15 @@ function readQuestions(value) {
56
60
  if (typeof value !== "string" || !QUESTION_MODES.has(value)) return null;
57
61
  return value === "any" ? "any" : "suggested";
58
62
  }
63
+ function readSttOffer(value) {
64
+ if (!isRecord(value)) return void 0;
65
+ const keys = Object.keys(value).sort();
66
+ const { token, expiresAt } = value;
67
+ if (keys.length !== 2 || keys[0] !== "expiresAt" || keys[1] !== "token" || typeof token !== "string" || !STT_TOKEN.test(token) || typeof expiresAt !== "number" || !Number.isSafeInteger(expiresAt) || expiresAt <= 0) {
68
+ return void 0;
69
+ }
70
+ return { token, expiresAt };
71
+ }
59
72
  function readAvailability(status, body) {
60
73
  if (status !== 200 || !isRecord(body)) return INERT;
61
74
  const { available, reason } = body;
@@ -69,9 +82,16 @@ function readAvailability(status, body) {
69
82
  if (available !== true || typeof consentVersion !== "string" || !isAvatarVersion(consentVersion) || typeof maxSessionSeconds !== "number" || !Number.isInteger(maxSessionSeconds) || maxSessionSeconds < 1 || botCheck === null || questions === null) {
70
83
  return INERT;
71
84
  }
85
+ const stt = readSttOffer(body.stt);
72
86
  return {
73
87
  kind: "available",
74
- offer: { consentVersion, maxSessionSeconds, botCheck, questions }
88
+ offer: {
89
+ consentVersion,
90
+ maxSessionSeconds,
91
+ botCheck,
92
+ questions,
93
+ ...stt === void 0 ? {} : { stt }
94
+ }
75
95
  };
76
96
  }
77
97
  const meaning = available ? void 0 : AVAILABILITY_REASONS.get(reason);
@@ -95,6 +115,21 @@ function errorCode(body) {
95
115
  const { code } = body.error;
96
116
  return typeof code === "string" ? code : null;
97
117
  }
118
+ var MAX_FOLLOW_UP_ANSWERS = 16;
119
+ var MAX_FOLLOW_UP_SECONDS = 600;
120
+ var FOLLOW_UP_KEYS = ["idleSeconds", "maxAnswerWaitSeconds", "maxAnswers"];
121
+ function isWholeBetween(value, max) {
122
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 && value <= max;
123
+ }
124
+ function readFollowUps(value) {
125
+ if (!isRecord(value)) return void 0;
126
+ const keys = Object.keys(value).sort();
127
+ const { maxAnswers, idleSeconds, maxAnswerWaitSeconds } = value;
128
+ 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)) {
129
+ return void 0;
130
+ }
131
+ return { maxAnswers, idleSeconds, maxAnswerWaitSeconds };
132
+ }
98
133
  function readCreated(body) {
99
134
  if (!isRecord(body)) return FAILED;
100
135
  const { sessionId, endSecret } = body;
@@ -112,6 +147,7 @@ function readCreated(body) {
112
147
  if (typeof livekitUrl !== "string" || !livekitUrl.startsWith("wss://") || typeof participantToken !== "string" || participantToken === "" || typeof workerIdentity !== "string" || workerIdentity === "" || typeof locale !== "string" || typeof text !== "string") {
113
148
  return unusable;
114
149
  }
150
+ const followUps = readFollowUps(body.followUps);
115
151
  return {
116
152
  kind: "created",
117
153
  session: {
@@ -120,7 +156,8 @@ function readCreated(body) {
120
156
  endsAt,
121
157
  join: { livekitUrl, participantToken, workerIdentity },
122
158
  captions: { locale, text },
123
- excerpt: body.excerpt === true
159
+ excerpt: body.excerpt === true,
160
+ ...followUps === void 0 ? {} : { followUps }
124
161
  }
125
162
  };
126
163
  }
@@ -159,6 +196,13 @@ function deadline(timeoutMs, outer) {
159
196
  };
160
197
  }
161
198
  async function requestAvailability(target) {
199
+ try {
200
+ return await requestAvailabilityAnswer(target);
201
+ } catch {
202
+ return INERT;
203
+ }
204
+ }
205
+ async function requestAvailabilityAnswer(target) {
162
206
  if (!isAvatarSite(target.site) || !LOCALE.test(target.locale)) return INERT;
163
207
  const query = new URLSearchParams({
164
208
  site: target.site,
@@ -174,8 +218,6 @@ async function requestAvailability(target) {
174
218
  signal: timer.signal
175
219
  });
176
220
  return readAvailability(response.status, await readJson(response));
177
- } catch {
178
- return INERT;
179
221
  } finally {
180
222
  timer.done();
181
223
  }
@@ -246,6 +288,96 @@ function sendEnd(target) {
246
288
  } catch {
247
289
  }
248
290
  }
291
+ var TURN_REFUSALS = /* @__PURE__ */ new Map([
292
+ ["no_time", "no_time"],
293
+ ["max_answers", "max_answers"],
294
+ ["not_eligible", "not_eligible"],
295
+ ["answer_claimed", "answer_claimed"],
296
+ ["limits", "limits"],
297
+ ["paused", "paused"],
298
+ ["busy", "busy"]
299
+ ]);
300
+ var MAX_TURN_RETRY_AFTER_SECONDS = 600;
301
+ function retryAfterOf(body) {
302
+ if (!isRecord(body) || !isRecord(body.error)) return void 0;
303
+ const { retryAfterSeconds } = body.error;
304
+ return isWholeBetween(retryAfterSeconds, MAX_TURN_RETRY_AFTER_SECONDS) ? retryAfterSeconds : void 0;
305
+ }
306
+ var TURN_KEY = /^[A-Za-z0-9_-]{8,64}$/;
307
+ var LOST = { kind: "failed", lost: true };
308
+ var TURN_FAILED = { kind: "failed", lost: false };
309
+ function readAccepted(body) {
310
+ if (!isRecord(body) || body.accepted !== true || !isRecord(body.captions)) {
311
+ return LOST;
312
+ }
313
+ const { locale, text } = body.captions;
314
+ if (typeof locale !== "string" || typeof text !== "string") return LOST;
315
+ return {
316
+ kind: "accepted",
317
+ captions: { locale, text },
318
+ excerpt: body.excerpt === true
319
+ };
320
+ }
321
+ function readTurn(kind, status, body) {
322
+ if (status === 200) {
323
+ if (kind === "answer") return readAccepted(body);
324
+ return isRecord(body) && body.ok === true ? { kind: "ok" } : LOST;
325
+ }
326
+ const code = errorCode(body);
327
+ const refusal = code === null ? void 0 : TURN_REFUSALS.get(code);
328
+ if (refusal !== void 0) {
329
+ const retryAfterSeconds = retryAfterOf(body);
330
+ return retryAfterSeconds === void 0 ? { kind: "refused", reason: refusal } : { kind: "refused", reason: refusal, retryAfterSeconds };
331
+ }
332
+ if (status === 404) return { kind: "gone" };
333
+ return status >= 500 ? LOST : TURN_FAILED;
334
+ }
335
+ function requestTurn(target) {
336
+ const { kind, messageId, idempotencyKey } = target;
337
+ const nonce = newIdempotencyKey();
338
+ 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)) {
339
+ return Promise.resolve(TURN_FAILED);
340
+ }
341
+ const payload = JSON.stringify({
342
+ secret: target.secret,
343
+ kind,
344
+ nonce,
345
+ ...messageId === void 0 ? {} : { messageId },
346
+ ...idempotencyKey === void 0 ? {} : { idempotencyKey }
347
+ });
348
+ const controller = new AbortController();
349
+ const answer = (async () => {
350
+ try {
351
+ const response = await fetch(
352
+ `${target.base}/sessions/${encodeURIComponent(target.sessionId)}/turn`,
353
+ {
354
+ method: "POST",
355
+ credentials: "omit",
356
+ cache: "no-store",
357
+ headers: { ...target.headers },
358
+ body: payload,
359
+ signal: controller.signal
360
+ }
361
+ );
362
+ return readTurn(kind, response.status, await readJson(response));
363
+ } catch {
364
+ return LOST;
365
+ }
366
+ })();
367
+ return new Promise((resolve) => {
368
+ const timer = setTimeout(
369
+ () => {
370
+ controller.abort();
371
+ resolve(LOST);
372
+ },
373
+ kind === "answer" ? TURN_ANSWER_TIMEOUT_MS : TURN_TIMEOUT_MS
374
+ );
375
+ void answer.then((result) => {
376
+ clearTimeout(timer);
377
+ resolve(result);
378
+ });
379
+ });
380
+ }
249
381
  var BOT_BINDING_PREFIX = "boff-web-avatar-cdata/v1";
250
382
  function webCryptoSubtle() {
251
383
  const subtle = globalThis.crypto?.subtle;
@@ -343,7 +475,14 @@ var EN = {
343
475
  usedToday: "You've had today's avatar session. Answers will show here as text.",
344
476
  needsAnswerAny: "Ask a question here, and the avatar reads the answer aloud.",
345
477
  startNewChatDetailAny: "The avatar reads aloud only the first answer in a new chat started in the Avatar tab, provided it passes the speech checks.",
346
- notEligibleAny: "The avatar can't read this answer aloud. The answer and sources are in Chat."
478
+ notEligibleAny: "The avatar can't read this answer aloud. The answer and sources are in Chat.",
479
+ followUpIdle: "The avatar ends after about thirty seconds without a question.",
480
+ followUpNoTime: "There isn't enough time left in this session to read that answer. It's in Chat.",
481
+ followUpMaxAnswers: "The avatar has read all the answers it can in this session. This one is in Chat.",
482
+ followUpLast: "That was the last answer the avatar can read in this session.",
483
+ followUpLimits: "The live avatar has reached its limit for now. This answer is in Chat.",
484
+ followUpPaused: "The live avatar is paused right now. This answer is in Chat.",
485
+ followUpUnread: "The avatar couldn't read that answer. It's in Chat."
347
486
  };
348
487
  var ZH = {
349
488
  checking: "\u6B63\u5728\u68C0\u67E5\u5B9E\u65F6\u865A\u62DF\u5F62\u8C61\u662F\u5426\u53EF\u7528\u2026",
@@ -404,7 +543,16 @@ var ZH = {
404
543
  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",
405
544
  needsAnswerAny: "\u5728\u8FD9\u91CC\u63D0\u95EE\uFF0C\u865A\u62DF\u5F62\u8C61\u4F1A\u6717\u8BFB\u56DE\u7B54\u3002",
406
545
  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",
407
- notEligibleAny: "\u865A\u62DF\u5F62\u8C61\u65E0\u6CD5\u6717\u8BFB\u6B64\u56DE\u7B54\u3002\u56DE\u7B54\u548C\u6765\u6E90\u5728\u804A\u5929\u4E2D\u3002"
546
+ notEligibleAny: "\u865A\u62DF\u5F62\u8C61\u65E0\u6CD5\u6717\u8BFB\u6B64\u56DE\u7B54\u3002\u56DE\u7B54\u548C\u6765\u6E90\u5728\u804A\u5929\u4E2D\u3002",
547
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the lines of a session that reads the answers after the first
548
+ // (Stage D).
549
+ followUpIdle: "\u5982\u679C\u7EA6\u4E09\u5341\u79D2\u5185\u6CA1\u6709\u65B0\u95EE\u9898\uFF0C\u865A\u62DF\u5F62\u8C61\u5C31\u4F1A\u7ED3\u675F\u3002",
550
+ 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",
551
+ 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",
552
+ followUpLast: "\u8FD9\u662F\u865A\u62DF\u5F62\u8C61\u5728\u672C\u6B21\u4F1A\u8BDD\u4E2D\u53EF\u6717\u8BFB\u7684\u6700\u540E\u4E00\u4E2A\u56DE\u7B54\u3002",
553
+ 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",
554
+ followUpPaused: "\u5B9E\u65F6\u865A\u62DF\u5F62\u8C61\u76EE\u524D\u5DF2\u6682\u505C\u3002\u6B64\u56DE\u7B54\u5728\u804A\u5929\u4E2D\u3002",
555
+ followUpUnread: "\u865A\u62DF\u5F62\u8C61\u65E0\u6CD5\u6717\u8BFB\u8BE5\u56DE\u7B54\u3002\u56DE\u7B54\u5728\u804A\u5929\u4E2D\u3002"
408
556
  };
409
557
  var HI = {
410
558
  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",
@@ -465,7 +613,16 @@ var HI = {
465
613
  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",
466
614
  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",
467
615
  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",
468
- 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"
616
+ 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",
617
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the lines of a session that reads the answers after the first
618
+ // (Stage D).
619
+ 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",
620
+ 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",
621
+ 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",
622
+ 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",
623
+ 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",
624
+ 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",
625
+ 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"
469
626
  };
470
627
  var ES = {
471
628
  checking: "Comprobando si el avatar en vivo est\xE1 disponible\u2026",
@@ -526,7 +683,16 @@ var ES = {
526
683
  usedToday: "Ya has usado la sesi\xF3n de avatar de hoy. Las respuestas se mostrar\xE1n aqu\xED como texto.",
527
684
  needsAnswerAny: "Haz una pregunta aqu\xED y el avatar leer\xE1 la respuesta en voz alta.",
528
685
  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.",
529
- notEligibleAny: "El avatar no puede leer esta respuesta en voz alta. La respuesta y las fuentes est\xE1n en el chat."
686
+ notEligibleAny: "El avatar no puede leer esta respuesta en voz alta. La respuesta y las fuentes est\xE1n en el chat.",
687
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the lines of a session that reads the answers after the first
688
+ // (Stage D).
689
+ followUpIdle: "El avatar termina tras unos treinta segundos sin preguntas.",
690
+ followUpNoTime: "No queda tiempo suficiente en esta sesi\xF3n para leer esa respuesta. Est\xE1 en el chat.",
691
+ followUpMaxAnswers: "El avatar ya ha le\xEDdo todas las respuestas que puede en esta sesi\xF3n. Esta est\xE1 en el chat.",
692
+ followUpLast: "Esa fue la \xFAltima respuesta que el avatar puede leer en esta sesi\xF3n.",
693
+ followUpLimits: "El avatar en vivo ha alcanzado su l\xEDmite por ahora. Esta respuesta est\xE1 en el chat.",
694
+ followUpPaused: "El avatar en vivo est\xE1 en pausa ahora mismo. Esta respuesta est\xE1 en el chat.",
695
+ followUpUnread: "El avatar no pudo leer esa respuesta. Est\xE1 en el chat."
530
696
  };
531
697
  var AR = {
532
698
  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",
@@ -587,7 +753,16 @@ var AR = {
587
753
  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.",
588
754
  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.",
589
755
  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.",
590
- 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."
756
+ 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.",
757
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the lines of a session that reads the answers after the first
758
+ // (Stage D).
759
+ 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.",
760
+ 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.",
761
+ 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.",
762
+ 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.",
763
+ 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.",
764
+ 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.",
765
+ 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."
591
766
  };
592
767
  var TA = {
593
768
  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",
@@ -648,7 +823,16 @@ var TA = {
648
823
  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.",
649
824
  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.",
650
825
  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.",
651
- 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."
826
+ 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.",
827
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the lines of a session that reads the answers after the first
828
+ // (Stage D).
829
+ 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.",
830
+ 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.",
831
+ 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.",
832
+ 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.",
833
+ 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.",
834
+ 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.",
835
+ 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."
652
836
  };
653
837
  var STRINGS = {
654
838
  en: EN,
@@ -798,12 +982,378 @@ var CONSENT = /* @__PURE__ */ new Map([
798
982
  questions: "any"
799
983
  }
800
984
  }
985
+ ],
986
+ [
987
+ // Stage D (BOFF-9797), a DRAFT for the operator's review that no environment uses yet (global-speech-svc reserves the
988
+ // same two versions): web-consent-2026-11-12-b with only its intro changed. The avatar reads the answer to each
989
+ // question asked in the Avatar tab, in one session, waits about thirty seconds after an answer for the next question,
990
+ // then ends; the session lasts at most three minutes, and an answer that does not fit the time left is read in part
991
+ // (and said so) or not read, and stays in Chat. The notice files of record and their digests are a gitops change of
992
+ // their own, made with the review.
993
+ "web-consent-2026-11-19-d",
994
+ {
995
+ en: {
996
+ title: "Live AI avatar (beta)",
997
+ introProduct: null,
998
+ 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.",
999
+ 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.",
1000
+ 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.",
1001
+ privacyLink: "How we handle this",
1002
+ botCheck: "turnstile",
1003
+ questions: "any",
1004
+ followUps: true
1005
+ }
1006
+ }
1007
+ ],
1008
+ [
1009
+ // The words of web-consent-2026-11-19-d without "When you start, Cloudflare checks for bots.", for a server that runs
1010
+ // no bot check. A DRAFT, like it.
1011
+ "web-consent-2026-11-20-d",
1012
+ {
1013
+ en: {
1014
+ title: "Live AI avatar (beta)",
1015
+ introProduct: null,
1016
+ 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.",
1017
+ 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.",
1018
+ 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.",
1019
+ privacyLink: "How we handle this",
1020
+ botCheck: "off",
1021
+ questions: "any",
1022
+ followUps: true
1023
+ }
1024
+ }
801
1025
  ]
802
1026
  ]);
803
1027
  function avatarConsentCopy(version, locale) {
804
1028
  if (!isAvatarLiveLocaleReviewed(locale)) return null;
805
1029
  return CONSENT.get(version)?.[locale] ?? null;
806
1030
  }
1031
+ var MIC_EN = {
1032
+ micStart: "Ask by voice",
1033
+ micStop: "Stop recording",
1034
+ micAllow: "Use the microphone",
1035
+ micNotNow: "Not now",
1036
+ micStarting: "Starting the microphone\u2026",
1037
+ micListening: "Listening\u2026",
1038
+ micElapsed: "{elapsed} of {limit}",
1039
+ micFinishing: "Getting the text\u2026",
1040
+ micDone: "Check the text, then press Send.",
1041
+ micDoneLimit: "The recording stopped at thirty seconds. Check the text, then press Send.",
1042
+ micNoSpeech: "No words were heard. Try again, or type your question.",
1043
+ micDenied: "The microphone is blocked in this browser. You can type your question instead.",
1044
+ micUnsupported: "The microphone can't be used here. You can type your question instead.",
1045
+ micUnavailable: "Voice questions aren't available right now. You can type your question instead.",
1046
+ micLimit: "Voice questions have reached their limit for now. You can type your question instead.",
1047
+ micBusy: "Voice questions are busy right now. Try again in a moment, or type your question.",
1048
+ micFailed: "The microphone stopped before the text came. Try again, or type your question."
1049
+ };
1050
+ var MIC_STRINGS = { en: MIC_EN };
1051
+ function avatarMicStrings(locale) {
1052
+ if (!isAvatarLiveLocaleReviewed(locale)) return null;
1053
+ return MIC_STRINGS[locale] ?? null;
1054
+ }
1055
+ var AVATAR_MIC_NOTICE_VERSION = "web-mic-2026-11-05";
1056
+ var MIC_NOTICE = /* @__PURE__ */ new Map([
1057
+ [
1058
+ AVATAR_MIC_NOTICE_VERSION,
1059
+ {
1060
+ en: {
1061
+ title: "Ask with your voice",
1062
+ processing: "While you speak, your voice is streamed to our speech service, and Google Cloud Speech-to-Text turns it into text in Google's EU region.",
1063
+ storage: "We don't store or log your audio, or the text made from it.",
1064
+ text: "The text appears in the box and stays in this browser until you press Send. Nothing is sent by itself. Once you send it, it is a chat message, kept like any other.",
1065
+ limits: "Each recording stops after thirty seconds, and voice questions have daily limits.",
1066
+ privacyLink: "How we handle this"
1067
+ }
1068
+ }
1069
+ ]
1070
+ ]);
1071
+ function avatarMicNotice(version, locale) {
1072
+ if (!isAvatarLiveLocaleReviewed(locale)) return null;
1073
+ return MIC_NOTICE.get(version)?.[locale] ?? null;
1074
+ }
1075
+ var PRIVACY_HREF = "/privacy#live-avatar";
1076
+ var LIMIT_SECONDS = 30;
1077
+ var TICK_MS = 250;
1078
+ var IDLE = { name: "idle" };
1079
+ var FOR_GOOD = /* @__PURE__ */ new Set([
1080
+ "unavailable",
1081
+ "limit"
1082
+ ]);
1083
+ var FAILURE_TEXT = {
1084
+ denied: "micDenied",
1085
+ unsupported: "micUnsupported",
1086
+ unavailable: "micUnavailable",
1087
+ limit: "micLimit",
1088
+ busy: "micBusy",
1089
+ failed: "micFailed"
1090
+ };
1091
+ function loadAvatarMic() {
1092
+ return import('../avatar-mic/index.mjs').then((loaded) => ({
1093
+ createAvatarMic: loaded.createAvatarMic
1094
+ }));
1095
+ }
1096
+ function canUseAvatarMic() {
1097
+ return typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getUserMedia === "function" && typeof AudioContext !== "undefined" && typeof AudioWorkletNode !== "undefined" && typeof WebSocket !== "undefined";
1098
+ }
1099
+ function clock(seconds) {
1100
+ const whole = Math.max(0, Math.floor(seconds));
1101
+ return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, "0")}`;
1102
+ }
1103
+ function withHeardText(current, heard, maxLength) {
1104
+ const text = heard.trim();
1105
+ if (text === "") return current;
1106
+ const joined = current.trim() === "" ? text : `${current.replace(/\s+$/, "")} ${text}`;
1107
+ return joined.slice(0, Math.max(0, maxLength));
1108
+ }
1109
+ function useAvatarMic({
1110
+ enabled,
1111
+ locale,
1112
+ base,
1113
+ offer,
1114
+ refreshOffer,
1115
+ onText,
1116
+ onStart
1117
+ }) {
1118
+ const S = avatarMicStrings(locale);
1119
+ const copy = avatarMicNotice(AVATAR_MIC_NOTICE_VERSION, locale);
1120
+ const uid = useId();
1121
+ const noticeTitleId = `${uid}-mic-notice`;
1122
+ const [withdrawn, setWithdrawn] = useState(null);
1123
+ const [supported, setSupported] = useState(false);
1124
+ useEffect(() => setSupported(canUseAvatarMic()), []);
1125
+ const possible = enabled && supported && base !== null && S !== null && copy !== null;
1126
+ const offered = possible && withdrawn === null;
1127
+ const [state, setState] = useState(IDLE);
1128
+ const [noticeOpen, setNoticeOpen] = useState(false);
1129
+ const agreedRef = useRef(false);
1130
+ const [loading2, setLoading] = useState(false);
1131
+ const [loadFailed, setLoadFailed] = useState(false);
1132
+ const [now, setNow] = useState(() => Date.now());
1133
+ const micRef = useRef(null);
1134
+ const seqRef = useRef(0);
1135
+ const buttonRef = useRef(null);
1136
+ const allowRef = useRef(null);
1137
+ const usedEpochRef = useRef(0);
1138
+ const latest = useRef({ offer, refreshOffer, onText, onStart, base });
1139
+ latest.current = { offer, refreshOffer, onText, onStart, base };
1140
+ const cancel = useCallback(() => {
1141
+ seqRef.current += 1;
1142
+ setLoading(false);
1143
+ setLoadFailed(false);
1144
+ setNoticeOpen(false);
1145
+ micRef.current?.mic.cancel();
1146
+ }, []);
1147
+ useEffect(() => {
1148
+ if (!offered) cancel();
1149
+ }, [offered, cancel]);
1150
+ useEffect(
1151
+ () => () => {
1152
+ seqRef.current += 1;
1153
+ micRef.current?.mic.cancel();
1154
+ },
1155
+ []
1156
+ );
1157
+ const micFor = useCallback(async (target) => {
1158
+ const held = micRef.current;
1159
+ if (held !== null && held.base === target) return held.mic;
1160
+ held?.mic.cancel();
1161
+ const loaded = await loadAvatarMic();
1162
+ const mic = loaded.createAvatarMic({
1163
+ base: target,
1164
+ notice: AVATAR_MIC_NOTICE_VERSION,
1165
+ offer: () => latest.current.offer,
1166
+ refreshOffer: () => latest.current.refreshOffer(),
1167
+ onState: setState
1168
+ });
1169
+ micRef.current = { base: target, mic };
1170
+ return mic;
1171
+ }, []);
1172
+ const press = useCallback(() => {
1173
+ const target = latest.current.base;
1174
+ if (target === null) return;
1175
+ seqRef.current += 1;
1176
+ const seq = seqRef.current;
1177
+ setLoadFailed(false);
1178
+ setLoading(true);
1179
+ void micFor(target).then(
1180
+ (mic) => {
1181
+ if (seqRef.current !== seq) return;
1182
+ setLoading(false);
1183
+ mic.start();
1184
+ },
1185
+ () => {
1186
+ if (seqRef.current !== seq) return;
1187
+ setLoading(false);
1188
+ setLoadFailed(true);
1189
+ }
1190
+ );
1191
+ }, [micFor]);
1192
+ const recording = loading2 || state.name === "starting" || state.name === "listening";
1193
+ const tap = useCallback(() => {
1194
+ if (state.name === "listening" || state.name === "starting") {
1195
+ micRef.current?.mic.stop();
1196
+ return;
1197
+ }
1198
+ if (loading2) {
1199
+ cancel();
1200
+ return;
1201
+ }
1202
+ latest.current.onStart?.();
1203
+ if (!agreedRef.current) {
1204
+ setNoticeOpen(true);
1205
+ void loadAvatarMic().catch(() => void 0);
1206
+ return;
1207
+ }
1208
+ press();
1209
+ }, [cancel, loading2, press, state.name]);
1210
+ const allow = useCallback(() => {
1211
+ agreedRef.current = true;
1212
+ setNoticeOpen(false);
1213
+ buttonRef.current?.focus();
1214
+ press();
1215
+ }, [press]);
1216
+ const notNow = useCallback(() => {
1217
+ setNoticeOpen(false);
1218
+ buttonRef.current?.focus();
1219
+ }, []);
1220
+ useEffect(() => {
1221
+ if (noticeOpen) allowRef.current?.focus();
1222
+ }, [noticeOpen]);
1223
+ const listeningSince = state.name === "listening" ? state.startedAt : null;
1224
+ useEffect(() => {
1225
+ if (listeningSince === null) return;
1226
+ setNow(Date.now());
1227
+ const timer = setInterval(() => setNow(Date.now()), TICK_MS);
1228
+ return () => clearInterval(timer);
1229
+ }, [listeningSince]);
1230
+ useEffect(() => {
1231
+ if (state.name === "done" && state.epoch !== usedEpochRef.current) {
1232
+ usedEpochRef.current = state.epoch;
1233
+ if (state.text !== "") latest.current.onText(state.text);
1234
+ } else if (state.name === "failed" && FOR_GOOD.has(state.reason)) {
1235
+ setWithdrawn(state.reason);
1236
+ }
1237
+ }, [state]);
1238
+ if (!possible || S === null || copy === null) {
1239
+ return {
1240
+ button: null,
1241
+ notice: null,
1242
+ lines: null,
1243
+ cancel,
1244
+ recording: false
1245
+ };
1246
+ }
1247
+ 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]] : "";
1248
+ const heard = offered && (state.name === "listening" || state.name === "finishing") ? state.text : "";
1249
+ const button = !offered ? null : /* @__PURE__ */ jsx(
1250
+ "button",
1251
+ {
1252
+ ref: buttonRef,
1253
+ type: "button",
1254
+ "aria-label": recording ? S.micStop : S.micStart,
1255
+ "aria-pressed": recording,
1256
+ title: recording ? S.micStop : S.micStart,
1257
+ "data-boff-explore": "avatar-live-mic",
1258
+ "data-mic": loading2 ? "starting" : state.name,
1259
+ className: cn(
1260
+ "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",
1261
+ recording ? "border-primary bg-primary text-primary-foreground" : "border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground"
1262
+ ),
1263
+ onClick: tap,
1264
+ children: recording ? /* @__PURE__ */ jsx(Square, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx(Mic, { "aria-hidden": "true", className: "h-4 w-4" })
1265
+ }
1266
+ );
1267
+ const notice = offered && noticeOpen ? /* @__PURE__ */ jsxs(
1268
+ "section",
1269
+ {
1270
+ "aria-labelledby": noticeTitleId,
1271
+ "data-boff-explore": "avatar-live-mic-notice",
1272
+ className: "w-full rounded-lg border border-border p-3 text-start",
1273
+ children: [
1274
+ /* @__PURE__ */ jsx("h3", { id: noticeTitleId, className: "text-sm font-semibold", children: copy.title }),
1275
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: copy.processing }),
1276
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: copy.storage }),
1277
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: copy.text }),
1278
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: copy.limits }),
1279
+ /* @__PURE__ */ jsx(
1280
+ "a",
1281
+ {
1282
+ href: PRIVACY_HREF,
1283
+ target: "_blank",
1284
+ rel: "noopener noreferrer",
1285
+ "data-boff-explore": "avatar-live-mic-privacy",
1286
+ className: "mt-1 inline-block text-xs font-medium text-primary underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1287
+ children: copy.privacyLink
1288
+ }
1289
+ ),
1290
+ /* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap gap-2", children: [
1291
+ /* @__PURE__ */ jsx(
1292
+ "button",
1293
+ {
1294
+ ref: allowRef,
1295
+ type: "button",
1296
+ "data-boff-explore": "avatar-live-mic-allow",
1297
+ className: "inline-flex min-h-11 items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background motion-reduce:transition-none",
1298
+ onClick: allow,
1299
+ children: S.micAllow
1300
+ }
1301
+ ),
1302
+ /* @__PURE__ */ jsx(
1303
+ "button",
1304
+ {
1305
+ type: "button",
1306
+ "data-boff-explore": "avatar-live-mic-not-now",
1307
+ className: "inline-flex min-h-11 items-center justify-center rounded-md border border-border bg-background px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background motion-reduce:transition-none",
1308
+ onClick: notNow,
1309
+ children: S.micNotNow
1310
+ }
1311
+ )
1312
+ ] })
1313
+ ]
1314
+ }
1315
+ ) : null;
1316
+ const listening = offered && state.name === "listening";
1317
+ const lines = /* @__PURE__ */ jsxs(Fragment, { children: [
1318
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
1319
+ listening ? /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: "relative flex h-2 w-2 shrink-0", children: [
1320
+ /* @__PURE__ */ jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75 motion-reduce:animate-none" }),
1321
+ /* @__PURE__ */ jsx("span", { className: "relative inline-flex h-2 w-2 rounded-full bg-primary" })
1322
+ ] }) : null,
1323
+ /* @__PURE__ */ jsx("p", { role: "status", "data-boff-explore": "avatar-live-mic-status", children: status }),
1324
+ listening ? /* @__PURE__ */ jsx(
1325
+ "span",
1326
+ {
1327
+ role: "timer",
1328
+ "data-boff-explore": "avatar-live-mic-elapsed",
1329
+ className: "tabular-nums",
1330
+ children: fill(S.micElapsed, {
1331
+ elapsed: clock(
1332
+ Math.min(LIMIT_SECONDS, (now - (listeningSince ?? now)) / 1e3)
1333
+ ),
1334
+ limit: clock(LIMIT_SECONDS)
1335
+ })
1336
+ }
1337
+ ) : null
1338
+ ] }),
1339
+ heard === "" ? null : /* @__PURE__ */ jsx(
1340
+ "p",
1341
+ {
1342
+ "data-boff-explore": "avatar-live-mic-heard",
1343
+ dir: "auto",
1344
+ className: "line-clamp-3 break-words text-sm italic text-muted-foreground",
1345
+ children: heard
1346
+ }
1347
+ )
1348
+ ] });
1349
+ return {
1350
+ button,
1351
+ notice,
1352
+ lines,
1353
+ cancel,
1354
+ recording: offered && state.name === "listening"
1355
+ };
1356
+ }
807
1357
 
808
1358
  // src/components/explore-avatar-turnstile.ts
809
1359
  var SCRIPT_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
@@ -919,12 +1469,18 @@ function runTurnstile(container, siteKey, action, cData) {
919
1469
  }
920
1470
  };
921
1471
  }
922
- var PRIVACY_HREF = "/privacy#live-avatar";
1472
+ var PRIVACY_HREF2 = "/privacy#live-avatar";
923
1473
  var SPEAKER_WAIT_MS = 3e4;
924
1474
  var END_GRACE_MS = 1e4;
925
1475
  var CLOCK_TOLERANCE_MS = 5e3;
926
1476
  var UNHEARD_GRACE_MS = 5e3;
927
1477
  var PLAYOUT_TAIL_MS = 5e3;
1478
+ var QUIET_PLAYED_OUT_MS = 2500;
1479
+ var OWED_VOICE_WAIT_MS = 15e3;
1480
+ var LISTENING_MARK_MS = 1e4;
1481
+ var BUSY_RETRY_MS = 2e3;
1482
+ var BUSY_RETRY_MAX_MS = 8e3;
1483
+ var MAX_BUSY_RETRIES = 24;
928
1484
  var SERVICE_MAX_AGE_MS = 15 * 6e4;
929
1485
  var ANSWER_OFFER_MS = SERVICE_MAX_AGE_MS - 6e4;
930
1486
  var MAX_TIMER_MS = 2147483647;
@@ -932,13 +1488,19 @@ var ASK_MAX_LENGTH = 300;
932
1488
  var MAX_REFERENCES = 64;
933
1489
  var USED_ON_KEY = "boff.explore.avatarUsedOn";
934
1490
  var PRIMARY = '[data-avatar-primary], [data-boff-explore="panel-continue-chat"]';
935
- var IDLE = { name: "idle" };
1491
+ var IDLE2 = { name: "idle" };
936
1492
  var INERT2 = { name: "inert" };
937
1493
  function stopAttending(handle) {
938
1494
  if (handle.unheard !== null) clearTimeout(handle.unheard);
939
1495
  if (handle.tail !== null) clearTimeout(handle.tail);
940
1496
  handle.unheard = null;
941
1497
  handle.tail = null;
1498
+ const run = handle.followUps;
1499
+ if (run === null) return;
1500
+ if (run.quiet !== null) clearTimeout(run.quiet);
1501
+ if (run.retry !== null) clearTimeout(run.retry);
1502
+ run.quiet = null;
1503
+ run.retry = null;
942
1504
  }
943
1505
  var REFUSAL_TEXT = {
944
1506
  busy: "busy",
@@ -949,6 +1511,18 @@ var REFUSAL_TEXT = {
949
1511
  not_eligible: "notEligible",
950
1512
  answer_claimed: "answerClaimed"
951
1513
  };
1514
+ var FOLLOW_UP_TEXT = {
1515
+ no_time: "followUpNoTime",
1516
+ max_answers: "followUpMaxAnswers",
1517
+ // The same line as a first answer the checks refuse: it stays in Chat as text.
1518
+ not_eligible: "notSpoken",
1519
+ answer_claimed: "answerClaimed",
1520
+ limits: "followUpLimits",
1521
+ paused: "followUpPaused",
1522
+ busy: "followUpUnread",
1523
+ answer_failed: "answerFailed",
1524
+ failed: "followUpUnread"
1525
+ };
952
1526
  var EMPTY = {
953
1527
  kind: "empty",
954
1528
  answerId: null,
@@ -1022,6 +1596,16 @@ function firstExchange(messages, rules = SUGGESTED_RULES) {
1022
1596
  function inFlight(conversation) {
1023
1597
  return conversation?.phase === "sending" || conversation?.phase === "streaming";
1024
1598
  }
1599
+ function acceptsQuestion(slot) {
1600
+ if (!slot.any || slot.textMode || slot.reason !== null) return false;
1601
+ return slot.phase === "needs-answer" ? slot.kind === "empty" : slot.phase === "live" && slot.followUp;
1602
+ }
1603
+ function followUpBox(phase, run, token) {
1604
+ return phase.name === "live" && run !== null && phase.session.followUp !== null && !phase.session.followUp.closed && token === run.token;
1605
+ }
1606
+ function micOfferOf(result) {
1607
+ return result.kind === "available" && result.offer.stt !== void 0 ? { ...result.offer.stt, receivedAt: Date.now() } : null;
1608
+ }
1025
1609
  function utcToday() {
1026
1610
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1027
1611
  }
@@ -1091,7 +1675,7 @@ async function loadModule(load) {
1091
1675
  }
1092
1676
  return loaded;
1093
1677
  }
1094
- function clock(seconds) {
1678
+ function clock2(seconds) {
1095
1679
  const whole = Math.max(0, Math.ceil(seconds));
1096
1680
  return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, "0")}`;
1097
1681
  }
@@ -1144,7 +1728,7 @@ function AvatarLiveView({
1144
1728
  const shown = offeredAt(exchange, Date.now());
1145
1729
  const { answerId, startOver } = shown;
1146
1730
  const offerUntil = answerId === null ? null : exchange.offerUntil;
1147
- const [phase, setPhase] = useState(IDLE);
1731
+ const [phase, setPhase] = useState(IDLE2);
1148
1732
  const [blocked, setBlocked] = useState(false);
1149
1733
  const [now, setNow] = useState(() => Date.now());
1150
1734
  const [draft, setDraft] = useState("");
@@ -1154,7 +1738,7 @@ function AvatarLiveView({
1154
1738
  const [usedToday, setUsedToday] = useState(false);
1155
1739
  const showing = active && !off;
1156
1740
  const behindChat = open && !active && !off;
1157
- const phaseRef = useRef(IDLE);
1741
+ const phaseRef = useRef(IDLE2);
1158
1742
  const latest = useRef({
1159
1743
  live,
1160
1744
  locale,
@@ -1170,6 +1754,9 @@ function AvatarLiveView({
1170
1754
  const waitRef = useRef(null);
1171
1755
  const sendingRef = useRef(false);
1172
1756
  const textBoxRef = useRef(false);
1757
+ const [micOffer, setMicOffer] = useState(null);
1758
+ const micCancelRef = useRef(() => void 0);
1759
+ const sendDueRef = useRef(() => void 0);
1173
1760
  const checkRef = useRef(null);
1174
1761
  const askedLocale = useRef(locale);
1175
1762
  const startRef = useRef(null);
@@ -1258,10 +1845,13 @@ function AvatarLiveView({
1258
1845
  if (!handle || handle.done || latest.current.live.endUnattended !== true) {
1259
1846
  return;
1260
1847
  }
1261
- const current = phaseRef.current.name;
1262
- if (current !== "pending" && current !== "live") return;
1848
+ const shown2 = phaseRef.current;
1849
+ if (shown2.name !== "pending" && shown2.name !== "live") return;
1850
+ const current = shown2.name;
1263
1851
  const away = !inSight();
1264
- if (away && handle.playedOut && current === "live") {
1852
+ const followUp2 = shown2.session.followUp;
1853
+ const asking = followUp2 !== null && followUp2.pending !== null;
1854
+ if (away && handle.playedOut && current === "live" && !asking) {
1265
1855
  finish("finished", "user");
1266
1856
  return;
1267
1857
  }
@@ -1280,6 +1870,184 @@ function AvatarLiveView({
1280
1870
  }
1281
1871
  }, UNHEARD_GRACE_MS);
1282
1872
  }, [finish, inSight]);
1873
+ const showFollowUp = useCallback(
1874
+ (handle, next, spoken) => {
1875
+ const current = phaseRef.current;
1876
+ if (sessionRef.current !== handle || handle.done || current.name !== "pending" && current.name !== "live" || current.session.followUp === null) {
1877
+ return;
1878
+ }
1879
+ const session = {
1880
+ ...current.session,
1881
+ ...spoken,
1882
+ followUp: { ...current.session.followUp, ...next }
1883
+ };
1884
+ go(
1885
+ current.name === "live" ? { name: "live", session } : { name: "pending", session }
1886
+ );
1887
+ },
1888
+ [go]
1889
+ );
1890
+ const tell = useCallback(
1891
+ (handle, kind2) => {
1892
+ if (handle.done || handle.followUps === null) return;
1893
+ void requestTurn({
1894
+ base: handle.base,
1895
+ headers: handle.headers,
1896
+ sessionId: handle.session.sessionId,
1897
+ secret: handle.session.endSecret,
1898
+ kind: kind2
1899
+ });
1900
+ },
1901
+ []
1902
+ );
1903
+ const whenQuiet = useCallback(
1904
+ (handle, delay) => {
1905
+ const run = handle.followUps;
1906
+ if (run === null) return;
1907
+ const wait = (ms) => {
1908
+ if (run.quiet !== null) clearTimeout(run.quiet);
1909
+ run.quiet = setTimeout(() => {
1910
+ run.quiet = null;
1911
+ if (sessionRef.current !== handle || handle.done || run.voice) {
1912
+ return;
1913
+ }
1914
+ if (run.owed) {
1915
+ run.owed = false;
1916
+ wait(OWED_VOICE_WAIT_MS);
1917
+ return;
1918
+ }
1919
+ handle.playedOut = true;
1920
+ showFollowUp(handle, { speaking: false });
1921
+ attend();
1922
+ sendDueRef.current(handle);
1923
+ }, ms);
1924
+ };
1925
+ wait(delay);
1926
+ },
1927
+ [attend, showFollowUp]
1928
+ );
1929
+ const heardVoice = useCallback(
1930
+ (handle, speaking) => {
1931
+ const run = handle.followUps;
1932
+ if (run === null) return;
1933
+ run.voice = speaking;
1934
+ if (!speaking) {
1935
+ whenQuiet(handle, QUIET_PLAYED_OUT_MS);
1936
+ return;
1937
+ }
1938
+ if (run.quiet !== null) clearTimeout(run.quiet);
1939
+ run.quiet = null;
1940
+ run.heard = true;
1941
+ run.starts += 1;
1942
+ run.owed = false;
1943
+ handle.playedOut = false;
1944
+ showFollowUp(handle, { speaking: true });
1945
+ },
1946
+ [showFollowUp, whenQuiet]
1947
+ );
1948
+ const sendDue = useCallback(
1949
+ (handle) => {
1950
+ const run = handle.followUps;
1951
+ const due = run?.due ?? null;
1952
+ if (run === null || due === null || due.sending || run.retry !== null || handle.done || sessionRef.current !== handle || run.heard && !handle.playedOut) {
1953
+ return;
1954
+ }
1955
+ due.sending = true;
1956
+ const starts = run.starts;
1957
+ const read = () => requestTurn({
1958
+ base: handle.base,
1959
+ headers: handle.headers,
1960
+ sessionId: handle.session.sessionId,
1961
+ secret: handle.session.endSecret,
1962
+ kind: "answer",
1963
+ messageId: due.messageId,
1964
+ idempotencyKey: due.idempotencyKey
1965
+ });
1966
+ const notRead = (unread, closed) => {
1967
+ run.due = null;
1968
+ showFollowUp(handle, {
1969
+ pending: null,
1970
+ unread: due.known ?? unread,
1971
+ ...closed ? { closed } : {}
1972
+ });
1973
+ attend();
1974
+ };
1975
+ void (async () => {
1976
+ let result = await read();
1977
+ if (result.kind === "failed" && result.lost && !handle.done) {
1978
+ result = await read();
1979
+ }
1980
+ due.sending = false;
1981
+ if (sessionRef.current !== handle || handle.done || run.due !== due) {
1982
+ return;
1983
+ }
1984
+ if (result.kind === "refused" && result.reason === "busy" && due.busy < MAX_BUSY_RETRIES) {
1985
+ due.busy += 1;
1986
+ if (run.heard && !handle.playedOut) return;
1987
+ const delay = Math.max(
1988
+ result.retryAfterSeconds === void 0 ? BUSY_RETRY_MS : result.retryAfterSeconds * 1e3,
1989
+ Math.min(BUSY_RETRY_MAX_MS, BUSY_RETRY_MS * 2 ** (due.busy - 1))
1990
+ );
1991
+ if (Date.now() + delay >= handle.endsAt) {
1992
+ notRead("no_time", false);
1993
+ return;
1994
+ }
1995
+ run.retry = setTimeout(() => {
1996
+ run.retry = null;
1997
+ sendDueRef.current(handle);
1998
+ }, delay);
1999
+ return;
2000
+ }
2001
+ if (result.kind !== "accepted") {
2002
+ notRead(
2003
+ result.kind === "refused" ? result.reason : "failed",
2004
+ result.kind === "gone" || result.kind === "refused" && result.reason === "max_answers"
2005
+ );
2006
+ return;
2007
+ }
2008
+ run.due = null;
2009
+ run.answers += 1;
2010
+ const heardSince = run.starts !== starts;
2011
+ run.owed = !heardSince;
2012
+ const playing = !heardSince || run.voice || run.quiet !== null;
2013
+ if (playing) handle.playedOut = false;
2014
+ showFollowUp(
2015
+ handle,
2016
+ {
2017
+ speaking: playing,
2018
+ pending: null,
2019
+ unread: null,
2020
+ closed: run.answers >= run.limits.maxAnswers
2021
+ },
2022
+ { captions: result.captions, excerpt: result.excerpt }
2023
+ );
2024
+ if (!playing) {
2025
+ attend();
2026
+ return;
2027
+ }
2028
+ if (run.owed && run.heard && !run.voice) whenQuiet(handle, 0);
2029
+ })();
2030
+ },
2031
+ [attend, showFollowUp, whenQuiet]
2032
+ );
2033
+ sendDueRef.current = sendDue;
2034
+ const readFollowUp = useCallback(
2035
+ (handle, messageId, known) => {
2036
+ const run = handle.followUps;
2037
+ if (run === null) return;
2038
+ run.sent.add(messageId);
2039
+ const idempotencyKey = newIdempotencyKey();
2040
+ if (idempotencyKey === null) {
2041
+ showFollowUp(handle, { pending: null, unread: known ?? "failed" });
2042
+ attend();
2043
+ return;
2044
+ }
2045
+ run.due = { messageId, idempotencyKey, known, busy: 0, sending: false };
2046
+ showFollowUp(handle, { pending: "reading", unread: null });
2047
+ sendDue(handle);
2048
+ },
2049
+ [attend, sendDue, showFollowUp]
2050
+ );
1283
2051
  const cancelStart = useCallback((reason) => {
1284
2052
  const run = startRef.current;
1285
2053
  if (!run) return;
@@ -1290,6 +2058,7 @@ function AvatarLiveView({
1290
2058
  const applyAvailability = useCallback(
1291
2059
  (result, locale2) => {
1292
2060
  const { live: live2 } = latest.current;
2061
+ setMicOffer(micOfferOf(result));
1293
2062
  if (result.kind === "inert") {
1294
2063
  setMode("suggested");
1295
2064
  go(INERT2);
@@ -1342,16 +2111,32 @@ function AvatarLiveView({
1342
2111
  });
1343
2112
  }, [applyAvailability, go]);
1344
2113
  const begin = useCallback(
1345
- (session, module, offer, target) => {
2114
+ (session, context) => {
2115
+ const { offer } = context;
1346
2116
  const received = Date.now();
1347
2117
  const longest = offer.maxSessionSeconds * 1e3;
1348
2118
  const ahead = session.endsAt - received;
1349
2119
  const endsAt = ahead > 0 && ahead <= longest + CLOCK_TOLERANCE_MS ? session.endsAt : received + longest;
2120
+ const followUps = session.followUps !== void 0 && offer.copy.followUps === true && offer.questions === "any" ? {
2121
+ limits: session.followUps,
2122
+ token: context.conversationToken,
2123
+ answers: 1,
2124
+ voice: false,
2125
+ heard: false,
2126
+ starts: 0,
2127
+ owed: true,
2128
+ quiet: null,
2129
+ asked: null,
2130
+ due: null,
2131
+ retry: null,
2132
+ sent: /* @__PURE__ */ new Set([context.messageId]),
2133
+ seq: 0
2134
+ } : null;
1350
2135
  sessionRef.current = {
1351
2136
  session,
1352
- base: target.base,
1353
- headers: target.headers,
1354
- module,
2137
+ base: context.base,
2138
+ headers: context.headers,
2139
+ module: context.module,
1355
2140
  room: null,
1356
2141
  joined: null,
1357
2142
  endsAt,
@@ -1361,6 +2146,7 @@ function AvatarLiveView({
1361
2146
  playedOut: false,
1362
2147
  unheard: null,
1363
2148
  tail: null,
2149
+ followUps,
1364
2150
  done: false
1365
2151
  };
1366
2152
  setBlocked(false);
@@ -1370,7 +2156,13 @@ function AvatarLiveView({
1370
2156
  session: {
1371
2157
  endsAt,
1372
2158
  captions: session.captions,
1373
- excerpt: session.excerpt
2159
+ excerpt: session.excerpt,
2160
+ followUp: followUps === null ? null : {
2161
+ speaking: true,
2162
+ pending: null,
2163
+ unread: null,
2164
+ closed: followUps.answers >= followUps.limits.maxAnswers
2165
+ }
1374
2166
  }
1375
2167
  });
1376
2168
  },
@@ -1402,7 +2194,7 @@ function AvatarLiveView({
1402
2194
  }
1403
2195
  switch (result.kind) {
1404
2196
  case "created":
1405
- begin(result.session, context.module, context.offer, context);
2197
+ begin(result.session, context);
1406
2198
  return;
1407
2199
  case "unusable":
1408
2200
  sendEnd({
@@ -1500,7 +2292,14 @@ function AvatarLiveView({
1500
2292
  return;
1501
2293
  }
1502
2294
  if (turnstile !== null) go({ name: "creating" });
1503
- const context = { module: loaded, offer, messageId, base: base2, headers: headers2 };
2295
+ const context = {
2296
+ module: loaded,
2297
+ offer,
2298
+ messageId,
2299
+ conversationToken,
2300
+ base: base2,
2301
+ headers: headers2
2302
+ };
1504
2303
  const result = await requestCreate({
1505
2304
  base: base2,
1506
2305
  headers: headers2,
@@ -1605,6 +2404,7 @@ function AvatarLiveView({
1605
2404
  [applyAvailability, bindingHolds, cancelWaiting, exchangeNow, launch]
1606
2405
  );
1607
2406
  const stop = useCallback(() => {
2407
+ micCancelRef.current();
1608
2408
  const current = phaseRef.current;
1609
2409
  if (current.name === "waiting") {
1610
2410
  cancelWaiting(false);
@@ -1626,6 +2426,7 @@ function AvatarLiveView({
1626
2426
  }, [cancelStart, cancelWaiting, finish, go, reoffer]);
1627
2427
  const leave = useCallback(
1628
2428
  (event) => {
2429
+ micCancelRef.current();
1629
2430
  const current = phaseRef.current;
1630
2431
  const reason = event === "user" || event === "chat" ? "user" : "page_hide";
1631
2432
  const how = reason === "user" ? "stopped" : "interrupted";
@@ -1636,7 +2437,7 @@ function AvatarLiveView({
1636
2437
  const controller = checkRef.current;
1637
2438
  checkRef.current = null;
1638
2439
  controller?.abort();
1639
- go(IDLE);
2440
+ go(IDLE2);
1640
2441
  return;
1641
2442
  }
1642
2443
  case "waiting":
@@ -1644,7 +2445,7 @@ function AvatarLiveView({
1644
2445
  return;
1645
2446
  case "bot-check":
1646
2447
  cancelStart(reason);
1647
- go(reason === "user" ? IDLE : reoffer(current.offer));
2448
+ go(reason === "user" ? IDLE2 : reoffer(current.offer));
1648
2449
  return;
1649
2450
  case "creating":
1650
2451
  if (keepsSession) return;
@@ -1706,7 +2507,7 @@ function AvatarLiveView({
1706
2507
  } else {
1707
2508
  checkRef.current?.abort();
1708
2509
  checkRef.current = null;
1709
- go(IDLE);
2510
+ go(IDLE2);
1710
2511
  }
1711
2512
  }, [active, cancelWaiting, check, go, locale]);
1712
2513
  const kind = shown.kind;
@@ -1803,6 +2604,9 @@ function AvatarLiveView({
1803
2604
  attend();
1804
2605
  }, PLAYOUT_TAIL_MS);
1805
2606
  return;
2607
+ case "speaking":
2608
+ heardVoice(handle, event.speaking);
2609
+ return;
1806
2610
  case "speaker-left":
1807
2611
  finish(
1808
2612
  wasLive ? "finished" : "failed",
@@ -1815,7 +2619,7 @@ function AvatarLiveView({
1815
2619
  return;
1816
2620
  }
1817
2621
  },
1818
- [attend, finish, goLive, inSight]
2622
+ [attend, finish, goLive, heardVoice, inSight]
1819
2623
  );
1820
2624
  useEffect(() => {
1821
2625
  if (phase.name !== "pending") return;
@@ -1951,6 +2755,85 @@ function AvatarLiveView({
1951
2755
  const textMode = any && (usedToday && (phase.name === "needs-answer" || phase.name === "consent" || phase.name === "declined" || phase.name === "ended") || phase.name === "refused" && phase.reason === "limits");
1952
2756
  textBoxRef.current = textMode;
1953
2757
  const askState = !any ? void 0 : textMode ? "text" : phase.name === "needs-answer" ? kind === "empty" ? "ready" : kind : phase.name;
2758
+ const followUp = phase.name === "live" ? phase.session.followUp : null;
2759
+ const followUpAsks = followUpBox(
2760
+ phase,
2761
+ sessionRef.current?.followUps ?? null,
2762
+ token
2763
+ );
2764
+ const askReason = inFlight(conversation) || followUp !== null && followUp.pending !== null ? S.askBusy : conversation?.canSend ? null : S.askUnavailable;
2765
+ const refreshMicOffer = useCallback(async () => {
2766
+ const { live: live2, locale: locale2, base: base2, headers: headers2 } = latest.current;
2767
+ if (base2 === null) return null;
2768
+ const next = micOfferOf(
2769
+ await requestAvailabilityAnswer({
2770
+ base: base2,
2771
+ headers: headers2,
2772
+ site: live2.site,
2773
+ locale: locale2
2774
+ })
2775
+ );
2776
+ if (next !== null) setMicOffer(next);
2777
+ return next;
2778
+ }, []);
2779
+ const onMicText = useCallback(
2780
+ (text) => {
2781
+ setDraft((current) => withHeardText(current, text, askMaxLength));
2782
+ if (touchFirst) return;
2783
+ rootRef.current?.querySelector(
2784
+ '[data-boff-explore="avatar-live-ask-input"]'
2785
+ )?.focus();
2786
+ },
2787
+ [askMaxLength, touchFirst]
2788
+ );
2789
+ const onMicStart = useCallback(() => {
2790
+ const handle = sessionRef.current;
2791
+ const current = phaseRef.current;
2792
+ if (handle === null || handle.followUps === null || handle.done || current.name !== "live" || current.session.followUp?.speaking !== true) {
2793
+ return;
2794
+ }
2795
+ const run = handle.followUps;
2796
+ tell(handle, "interrupt");
2797
+ if (run.quiet !== null) clearTimeout(run.quiet);
2798
+ run.quiet = null;
2799
+ run.owed = false;
2800
+ handle.playedOut = true;
2801
+ showFollowUp(handle, { speaking: false });
2802
+ }, [showFollowUp, tell]);
2803
+ const mic = useAvatarMic({
2804
+ enabled: live.mic === true && micOffer !== null && !off && acceptsQuestion({
2805
+ any,
2806
+ phase: phase.name,
2807
+ kind,
2808
+ textMode,
2809
+ reason: askReason,
2810
+ followUp: followUpAsks
2811
+ }),
2812
+ locale,
2813
+ base,
2814
+ offer: micOffer,
2815
+ refreshOffer: refreshMicOffer,
2816
+ onText: onMicText,
2817
+ onStart: onMicStart
2818
+ });
2819
+ micCancelRef.current = mic.cancel;
2820
+ const micRecording = mic.recording;
2821
+ useEffect(() => {
2822
+ const handle = sessionRef.current;
2823
+ if (!micRecording || handle === null || handle.followUps === null || handle.done) {
2824
+ return;
2825
+ }
2826
+ tell(handle, "listening");
2827
+ const every = Math.min(
2828
+ LISTENING_MARK_MS,
2829
+ Math.max(1e3, handle.followUps.limits.idleSeconds * 1e3 / 3)
2830
+ );
2831
+ const timer = setInterval(() => tell(handle, "listening"), every);
2832
+ return () => {
2833
+ clearInterval(timer);
2834
+ if (sessionRef.current === handle) tell(handle, "listening");
2835
+ };
2836
+ }, [micRecording, tell]);
1954
2837
  const notNow = useCallback(() => {
1955
2838
  const current = phaseRef.current;
1956
2839
  if ((current.name === "consent" || current.name === "needs-answer") && current.offer.questions === "any") {
@@ -1959,6 +2842,43 @@ function AvatarLiveView({
1959
2842
  }
1960
2843
  onContinueInChat();
1961
2844
  }, [go, onContinueInChat]);
2845
+ const askFollowUp = useCallback(
2846
+ (handle, conversation2, question) => {
2847
+ const run = handle.followUps;
2848
+ const current = phaseRef.current;
2849
+ if (run === null || handle.done || current.name !== "live" || current.session.followUp?.pending !== null || !followUpBox(current, run, conversation2.getConversationToken())) {
2850
+ return;
2851
+ }
2852
+ sendingRef.current = true;
2853
+ run.seq += 1;
2854
+ const seq = run.seq;
2855
+ setDraft("");
2856
+ tell(handle, "asking");
2857
+ showFollowUp(handle, { pending: "asking", unread: null });
2858
+ void conversation2.send(question, [], { surface: "AVATAR" }).then((added) => {
2859
+ if (added === null) {
2860
+ setDraft((current2) => current2 === "" ? question : current2);
2861
+ }
2862
+ if (sessionRef.current !== handle || handle.done || run.seq !== seq) {
2863
+ return;
2864
+ }
2865
+ if (added === null || added.conversationToken !== run.token) {
2866
+ showFollowUp(handle, { pending: null });
2867
+ attend();
2868
+ return;
2869
+ }
2870
+ run.asked = {
2871
+ messageId: added.userMessageId,
2872
+ token: added.conversationToken,
2873
+ answerId: null
2874
+ };
2875
+ showFollowUp(handle, { pending: "asking" });
2876
+ }).finally(() => {
2877
+ sendingRef.current = false;
2878
+ });
2879
+ },
2880
+ [attend, showFollowUp, tell]
2881
+ );
1962
2882
  const ask = useCallback(
1963
2883
  (text) => {
1964
2884
  const conversation2 = conversationRef.current;
@@ -1967,6 +2887,11 @@ function AvatarLiveView({
1967
2887
  return;
1968
2888
  }
1969
2889
  const spoken = !textBoxRef.current;
2890
+ const handle = sessionRef.current;
2891
+ if (spoken && handle !== null && handle.followUps !== null) {
2892
+ askFollowUp(handle, conversation2, question);
2893
+ return;
2894
+ }
1970
2895
  if (spoken && conversation2.messages.length > 0) return;
1971
2896
  sendingRef.current = true;
1972
2897
  setAskedHere(null);
@@ -1988,8 +2913,64 @@ function AvatarLiveView({
1988
2913
  sendingRef.current = false;
1989
2914
  });
1990
2915
  },
1991
- [setAskedHere]
2916
+ [askFollowUp, setAskedHere]
1992
2917
  );
2918
+ useEffect(() => {
2919
+ const handle = sessionRef.current;
2920
+ const run = handle?.followUps ?? null;
2921
+ if (handle === null || run === null || handle.done || run.asked === null) {
2922
+ return;
2923
+ }
2924
+ const asked = run.asked;
2925
+ const chat = conversationRef.current;
2926
+ const messages = chat?.messages ?? [];
2927
+ const at = messages.findIndex((message) => message.id === asked.messageId);
2928
+ const question = at === -1 ? void 0 : messages[at];
2929
+ const answer = at === -1 ? void 0 : messages[at + 1];
2930
+ const drop = (unread2) => {
2931
+ run.asked = null;
2932
+ showFollowUp(handle, { pending: null, unread: unread2 });
2933
+ attend();
2934
+ };
2935
+ const unread = (id, reason) => {
2936
+ if (id === null || run.sent.has(id) || !isAvatarMessageId(id)) {
2937
+ drop(reason);
2938
+ return;
2939
+ }
2940
+ run.asked = null;
2941
+ readFollowUp(handle, id, reason);
2942
+ };
2943
+ if (question === void 0 || (chat?.getConversationToken() ?? null) !== run.token) {
2944
+ drop(null);
2945
+ return;
2946
+ }
2947
+ if (answer === void 0) {
2948
+ if (!inFlight(chat)) unread(asked.answerId, "answer_failed");
2949
+ return;
2950
+ }
2951
+ if (answer.status === "PENDING" || answer.status === "RUNNING") {
2952
+ if (answer.role === "ASSISTANT") asked.answerId = answer.id;
2953
+ return;
2954
+ }
2955
+ if (answer.role !== "ASSISTANT") {
2956
+ unread(asked.answerId, "answer_failed");
2957
+ return;
2958
+ }
2959
+ if (answer.status !== "COMPLETED" || answer.content.trim() === "") {
2960
+ unread(answer.id, "answer_failed");
2961
+ return;
2962
+ }
2963
+ if (run.sent.has(answer.id) || !isAvatarMessageId(answer.id)) {
2964
+ drop("not_eligible");
2965
+ return;
2966
+ }
2967
+ if (at + 2 !== messages.length || !readable(question, answer)) {
2968
+ unread(answer.id, "not_eligible");
2969
+ return;
2970
+ }
2971
+ run.asked = null;
2972
+ readFollowUp(handle, answer.id, null);
2973
+ });
1993
2974
  const newChatForAvatar = useCallback(() => {
1994
2975
  const conversation2 = conversationRef.current;
1995
2976
  if (conversation2 === null) return;
@@ -2013,7 +2994,14 @@ function AvatarLiveView({
2013
2994
  const focused = document.activeElement;
2014
2995
  if (focused && focused !== document.body && focused.isConnected) return;
2015
2996
  rootRef.current?.querySelector(PRIMARY)?.focus();
2016
- }, [active, phase.name, askState, shown.pendingAnswerId, blocked]);
2997
+ }, [
2998
+ active,
2999
+ phase.name,
3000
+ askState,
3001
+ shown.pendingAnswerId,
3002
+ blocked,
3003
+ followUpAsks
3004
+ ]);
2017
3005
  const onPlaying = goLive;
2018
3006
  const enablePlayback = useCallback(() => {
2019
3007
  void sessionRef.current?.joined?.enablePlayback().catch(() => void 0);
@@ -2119,7 +3107,7 @@ function AvatarLiveView({
2119
3107
  /* @__PURE__ */ jsxs(
2120
3108
  "a",
2121
3109
  {
2122
- href: PRIVACY_HREF,
3110
+ href: PRIVACY_HREF2,
2123
3111
  target: "_blank",
2124
3112
  rel: "noopener noreferrer",
2125
3113
  "data-boff-explore": "avatar-live-privacy",
@@ -2211,7 +3199,7 @@ function AvatarLiveView({
2211
3199
  /* @__PURE__ */ jsxs("p", { className: "text-sm tabular-nums text-muted-foreground", children: [
2212
3200
  S.timeLeft,
2213
3201
  " ",
2214
- /* @__PURE__ */ jsx("span", { role: "timer", "data-boff-explore": "avatar-live-countdown", children: clock(Math.max(0, (session.endsAt - now) / 1e3)) })
3202
+ /* @__PURE__ */ jsx("span", { role: "timer", "data-boff-explore": "avatar-live-countdown", children: clock2(Math.max(0, (session.endsAt - now) / 1e3)) })
2215
3203
  ] })
2216
3204
  ] });
2217
3205
  const liveActions = (back) => /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [
@@ -2413,13 +3401,32 @@ function AvatarLiveView({
2413
3401
  actions2 = stopButton;
2414
3402
  break;
2415
3403
  case "pending":
2416
- case "live":
2417
- status = phase.name === "live" ? speakingLine(phase.session) : S.connecting;
3404
+ case "live": {
2418
3405
  top2 = badge;
2419
3406
  media2 = liveMedia(phase.name === "live");
2420
3407
  body2 = liveBody(phase.session);
2421
3408
  actions2 = liveActions(readInChatButton(false));
3409
+ if (followUp === null) {
3410
+ status = phase.name === "live" ? speakingLine(phase.session) : S.connecting;
3411
+ break;
3412
+ }
3413
+ 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;
3414
+ if (!followUp.speaking && followUp.pending === null && followUp.unread !== null && !followUp.closed) {
3415
+ body2 = /* @__PURE__ */ jsxs(Fragment, { children: [
3416
+ body2,
3417
+ /* @__PURE__ */ jsx(
3418
+ "p",
3419
+ {
3420
+ "data-boff-explore": "avatar-live-idle",
3421
+ className: "text-xs text-muted-foreground",
3422
+ children: S.followUpIdle
3423
+ }
3424
+ )
3425
+ ] });
3426
+ }
3427
+ if (followUpAsks) box = "spoken";
2422
3428
  break;
3429
+ }
2423
3430
  case "ended":
2424
3431
  status = S[phase.how];
2425
3432
  outcome2 = true;
@@ -2428,7 +3435,7 @@ function AvatarLiveView({
2428
3435
  break;
2429
3436
  }
2430
3437
  }
2431
- const reason = inFlight(conversation) ? S.askBusy : conversation?.canSend ? null : S.askUnavailable;
3438
+ const reason = askReason;
2432
3439
  const chips = box !== null && shown.kind === "empty" && prompts.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
2433
3440
  /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground", children: tryAsking }),
2434
3441
  /* @__PURE__ */ jsx("div", { className: "flex max-h-28 flex-wrap gap-2 overflow-y-auto", children: prompts.map((prompt) => /* @__PURE__ */ jsx(
@@ -2467,7 +3474,7 @@ function AvatarLiveView({
2467
3474
  /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: S.newChatNote })
2468
3475
  ] }) : null,
2469
3476
  chips,
2470
- box !== null && usedToday ? /* @__PURE__ */ jsx(
3477
+ box !== null && usedToday && phase.name !== "live" ? /* @__PURE__ */ jsx(
2471
3478
  "p",
2472
3479
  {
2473
3480
  "data-boff-explore": "avatar-live-used-today",
@@ -2475,6 +3482,7 @@ function AvatarLiveView({
2475
3482
  children: S.usedToday
2476
3483
  }
2477
3484
  ) : null,
3485
+ box === "spoken" ? mic.notice : null,
2478
3486
  box === null ? null : /* @__PURE__ */ jsx(
2479
3487
  AskBox,
2480
3488
  {
@@ -2489,7 +3497,9 @@ function AvatarLiveView({
2489
3497
  footer: S.askFooter,
2490
3498
  noFiles: S.askNoFiles,
2491
3499
  hint: box === "spoken" ? S.needsAnswerAny : null,
2492
- touchFirst
3500
+ touchFirst,
3501
+ mic: box === "spoken" ? mic.button : null,
3502
+ micLines: box === "spoken" ? mic.lines : null
2493
3503
  }
2494
3504
  )
2495
3505
  ]
@@ -2636,7 +3646,9 @@ function AskBox({
2636
3646
  footer,
2637
3647
  noFiles,
2638
3648
  hint,
2639
- touchFirst
3649
+ touchFirst,
3650
+ mic,
3651
+ micLines
2640
3652
  }) {
2641
3653
  const uid = useId();
2642
3654
  const reasonId = `${uid}-reason`;
@@ -2701,6 +3713,7 @@ function AskBox({
2701
3713
  onDrop
2702
3714
  }
2703
3715
  ),
3716
+ mic,
2704
3717
  /* @__PURE__ */ jsx(
2705
3718
  "button",
2706
3719
  {
@@ -2714,6 +3727,7 @@ function AskBox({
2714
3727
  }
2715
3728
  )
2716
3729
  ] }),
3730
+ micLines,
2717
3731
  reason === null ? null : /* @__PURE__ */ jsx(
2718
3732
  "p",
2719
3733
  {