@burdenoff/website-sdk 2026.1105.1 → 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.
@@ -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}$/;
@@ -115,6 +117,21 @@ function errorCode(body) {
115
117
  const { code } = body.error;
116
118
  return typeof code === "string" ? code : null;
117
119
  }
120
+ var MAX_FOLLOW_UP_ANSWERS = 16;
121
+ var MAX_FOLLOW_UP_SECONDS = 600;
122
+ var FOLLOW_UP_KEYS = ["idleSeconds", "maxAnswerWaitSeconds", "maxAnswers"];
123
+ function isWholeBetween(value, max) {
124
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 && value <= max;
125
+ }
126
+ function readFollowUps(value) {
127
+ if (!isRecord(value)) return void 0;
128
+ const keys = Object.keys(value).sort();
129
+ const { maxAnswers, idleSeconds, maxAnswerWaitSeconds } = value;
130
+ 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)) {
131
+ return void 0;
132
+ }
133
+ return { maxAnswers, idleSeconds, maxAnswerWaitSeconds };
134
+ }
118
135
  function readCreated(body) {
119
136
  if (!isRecord(body)) return FAILED;
120
137
  const { sessionId, endSecret } = body;
@@ -132,6 +149,7 @@ function readCreated(body) {
132
149
  if (typeof livekitUrl !== "string" || !livekitUrl.startsWith("wss://") || typeof participantToken !== "string" || participantToken === "" || typeof workerIdentity !== "string" || workerIdentity === "" || typeof locale !== "string" || typeof text !== "string") {
133
150
  return unusable;
134
151
  }
152
+ const followUps = readFollowUps(body.followUps);
135
153
  return {
136
154
  kind: "created",
137
155
  session: {
@@ -140,7 +158,8 @@ function readCreated(body) {
140
158
  endsAt,
141
159
  join: { livekitUrl, participantToken, workerIdentity },
142
160
  captions: { locale, text },
143
- excerpt: body.excerpt === true
161
+ excerpt: body.excerpt === true,
162
+ ...followUps === void 0 ? {} : { followUps }
144
163
  }
145
164
  };
146
165
  }
@@ -271,6 +290,96 @@ function sendEnd(target) {
271
290
  } catch {
272
291
  }
273
292
  }
293
+ var TURN_REFUSALS = /* @__PURE__ */ new Map([
294
+ ["no_time", "no_time"],
295
+ ["max_answers", "max_answers"],
296
+ ["not_eligible", "not_eligible"],
297
+ ["answer_claimed", "answer_claimed"],
298
+ ["limits", "limits"],
299
+ ["paused", "paused"],
300
+ ["busy", "busy"]
301
+ ]);
302
+ var MAX_TURN_RETRY_AFTER_SECONDS = 600;
303
+ function retryAfterOf(body) {
304
+ if (!isRecord(body) || !isRecord(body.error)) return void 0;
305
+ const { retryAfterSeconds } = body.error;
306
+ return isWholeBetween(retryAfterSeconds, MAX_TURN_RETRY_AFTER_SECONDS) ? retryAfterSeconds : void 0;
307
+ }
308
+ var TURN_KEY = /^[A-Za-z0-9_-]{8,64}$/;
309
+ var LOST = { kind: "failed", lost: true };
310
+ var TURN_FAILED = { kind: "failed", lost: false };
311
+ function readAccepted(body) {
312
+ if (!isRecord(body) || body.accepted !== true || !isRecord(body.captions)) {
313
+ return LOST;
314
+ }
315
+ const { locale, text } = body.captions;
316
+ if (typeof locale !== "string" || typeof text !== "string") return LOST;
317
+ return {
318
+ kind: "accepted",
319
+ captions: { locale, text },
320
+ excerpt: body.excerpt === true
321
+ };
322
+ }
323
+ function readTurn(kind, status, body) {
324
+ if (status === 200) {
325
+ if (kind === "answer") return readAccepted(body);
326
+ return isRecord(body) && body.ok === true ? { kind: "ok" } : LOST;
327
+ }
328
+ const code = errorCode(body);
329
+ const refusal = code === null ? void 0 : TURN_REFUSALS.get(code);
330
+ if (refusal !== void 0) {
331
+ const retryAfterSeconds = retryAfterOf(body);
332
+ return retryAfterSeconds === void 0 ? { kind: "refused", reason: refusal } : { kind: "refused", reason: refusal, retryAfterSeconds };
333
+ }
334
+ if (status === 404) return { kind: "gone" };
335
+ return status >= 500 ? LOST : TURN_FAILED;
336
+ }
337
+ function requestTurn(target) {
338
+ const { kind, messageId, idempotencyKey } = target;
339
+ const nonce = newIdempotencyKey();
340
+ 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)) {
341
+ return Promise.resolve(TURN_FAILED);
342
+ }
343
+ const payload = JSON.stringify({
344
+ secret: target.secret,
345
+ kind,
346
+ nonce,
347
+ ...messageId === void 0 ? {} : { messageId },
348
+ ...idempotencyKey === void 0 ? {} : { idempotencyKey }
349
+ });
350
+ const controller = new AbortController();
351
+ const answer = (async () => {
352
+ try {
353
+ const response = await fetch(
354
+ `${target.base}/sessions/${encodeURIComponent(target.sessionId)}/turn`,
355
+ {
356
+ method: "POST",
357
+ credentials: "omit",
358
+ cache: "no-store",
359
+ headers: { ...target.headers },
360
+ body: payload,
361
+ signal: controller.signal
362
+ }
363
+ );
364
+ return readTurn(kind, response.status, await readJson(response));
365
+ } catch {
366
+ return LOST;
367
+ }
368
+ })();
369
+ return new Promise((resolve) => {
370
+ const timer = setTimeout(
371
+ () => {
372
+ controller.abort();
373
+ resolve(LOST);
374
+ },
375
+ kind === "answer" ? TURN_ANSWER_TIMEOUT_MS : TURN_TIMEOUT_MS
376
+ );
377
+ void answer.then((result) => {
378
+ clearTimeout(timer);
379
+ resolve(result);
380
+ });
381
+ });
382
+ }
274
383
  var BOT_BINDING_PREFIX = "boff-web-avatar-cdata/v1";
275
384
  function webCryptoSubtle() {
276
385
  const subtle = globalThis.crypto?.subtle;
@@ -368,7 +477,14 @@ var EN = {
368
477
  usedToday: "You've had today's avatar session. Answers will show here as text.",
369
478
  needsAnswerAny: "Ask a question here, and the avatar reads the answer aloud.",
370
479
  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."
480
+ notEligibleAny: "The avatar can't read this answer aloud. The answer and sources are in Chat.",
481
+ followUpIdle: "The avatar ends after about thirty seconds without a question.",
482
+ followUpNoTime: "There isn't enough time left in this session to read that answer. It's in Chat.",
483
+ followUpMaxAnswers: "The avatar has read all the answers it can in this session. This one is in Chat.",
484
+ followUpLast: "That was the last answer the avatar can read in this session.",
485
+ followUpLimits: "The live avatar has reached its limit for now. This answer is in Chat.",
486
+ followUpPaused: "The live avatar is paused right now. This answer is in Chat.",
487
+ followUpUnread: "The avatar couldn't read that answer. It's in Chat."
372
488
  };
373
489
  var ZH = {
374
490
  checking: "\u6B63\u5728\u68C0\u67E5\u5B9E\u65F6\u865A\u62DF\u5F62\u8C61\u662F\u5426\u53EF\u7528\u2026",
@@ -429,7 +545,16 @@ var ZH = {
429
545
  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
546
  needsAnswerAny: "\u5728\u8FD9\u91CC\u63D0\u95EE\uFF0C\u865A\u62DF\u5F62\u8C61\u4F1A\u6717\u8BFB\u56DE\u7B54\u3002",
431
547
  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"
548
+ notEligibleAny: "\u865A\u62DF\u5F62\u8C61\u65E0\u6CD5\u6717\u8BFB\u6B64\u56DE\u7B54\u3002\u56DE\u7B54\u548C\u6765\u6E90\u5728\u804A\u5929\u4E2D\u3002",
549
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the lines of a session that reads the answers after the first
550
+ // (Stage D).
551
+ followUpIdle: "\u5982\u679C\u7EA6\u4E09\u5341\u79D2\u5185\u6CA1\u6709\u65B0\u95EE\u9898\uFF0C\u865A\u62DF\u5F62\u8C61\u5C31\u4F1A\u7ED3\u675F\u3002",
552
+ 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",
553
+ 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",
554
+ followUpLast: "\u8FD9\u662F\u865A\u62DF\u5F62\u8C61\u5728\u672C\u6B21\u4F1A\u8BDD\u4E2D\u53EF\u6717\u8BFB\u7684\u6700\u540E\u4E00\u4E2A\u56DE\u7B54\u3002",
555
+ 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",
556
+ followUpPaused: "\u5B9E\u65F6\u865A\u62DF\u5F62\u8C61\u76EE\u524D\u5DF2\u6682\u505C\u3002\u6B64\u56DE\u7B54\u5728\u804A\u5929\u4E2D\u3002",
557
+ followUpUnread: "\u865A\u62DF\u5F62\u8C61\u65E0\u6CD5\u6717\u8BFB\u8BE5\u56DE\u7B54\u3002\u56DE\u7B54\u5728\u804A\u5929\u4E2D\u3002"
433
558
  };
434
559
  var HI = {
435
560
  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 +615,16 @@ var HI = {
490
615
  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
616
  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
617
  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"
618
+ 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",
619
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the lines of a session that reads the answers after the first
620
+ // (Stage D).
621
+ 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",
622
+ 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",
623
+ 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",
624
+ 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",
625
+ 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",
626
+ 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",
627
+ 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
628
  };
495
629
  var ES = {
496
630
  checking: "Comprobando si el avatar en vivo est\xE1 disponible\u2026",
@@ -551,7 +685,16 @@ var ES = {
551
685
  usedToday: "Ya has usado la sesi\xF3n de avatar de hoy. Las respuestas se mostrar\xE1n aqu\xED como texto.",
552
686
  needsAnswerAny: "Haz una pregunta aqu\xED y el avatar leer\xE1 la respuesta en voz alta.",
553
687
  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."
688
+ notEligibleAny: "El avatar no puede leer esta respuesta en voz alta. La respuesta y las fuentes est\xE1n en el chat.",
689
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the lines of a session that reads the answers after the first
690
+ // (Stage D).
691
+ followUpIdle: "El avatar termina tras unos treinta segundos sin preguntas.",
692
+ followUpNoTime: "No queda tiempo suficiente en esta sesi\xF3n para leer esa respuesta. Est\xE1 en el chat.",
693
+ followUpMaxAnswers: "El avatar ya ha le\xEDdo todas las respuestas que puede en esta sesi\xF3n. Esta est\xE1 en el chat.",
694
+ followUpLast: "Esa fue la \xFAltima respuesta que el avatar puede leer en esta sesi\xF3n.",
695
+ followUpLimits: "El avatar en vivo ha alcanzado su l\xEDmite por ahora. Esta respuesta est\xE1 en el chat.",
696
+ followUpPaused: "El avatar en vivo est\xE1 en pausa ahora mismo. Esta respuesta est\xE1 en el chat.",
697
+ followUpUnread: "El avatar no pudo leer esa respuesta. Est\xE1 en el chat."
555
698
  };
556
699
  var AR = {
557
700
  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 +755,16 @@ var AR = {
612
755
  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
756
  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
757
  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."
758
+ 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.",
759
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the lines of a session that reads the answers after the first
760
+ // (Stage D).
761
+ 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.",
762
+ 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.",
763
+ 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.",
764
+ 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.",
765
+ 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.",
766
+ 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.",
767
+ 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
768
  };
617
769
  var TA = {
618
770
  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 +825,16 @@ var TA = {
673
825
  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
826
  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
827
  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."
828
+ 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.",
829
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the lines of a session that reads the answers after the first
830
+ // (Stage D).
831
+ 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.",
832
+ 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.",
833
+ 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.",
834
+ 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.",
835
+ 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.",
836
+ 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.",
837
+ 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
838
  };
678
839
  var STRINGS = {
679
840
  en: EN,
@@ -823,6 +984,46 @@ var CONSENT = /* @__PURE__ */ new Map([
823
984
  questions: "any"
824
985
  }
825
986
  }
987
+ ],
988
+ [
989
+ // Stage D (BOFF-9797), a DRAFT for the operator's review that no environment uses yet (global-speech-svc reserves the
990
+ // same two versions): web-consent-2026-11-12-b with only its intro changed. The avatar reads the answer to each
991
+ // question asked in the Avatar tab, in one session, waits about thirty seconds after an answer for the next question,
992
+ // then ends; the session lasts at most three minutes, and an answer that does not fit the time left is read in part
993
+ // (and said so) or not read, and stays in Chat. The notice files of record and their digests are a gitops change of
994
+ // their own, made with the review.
995
+ "web-consent-2026-11-19-d",
996
+ {
997
+ en: {
998
+ title: "Live AI avatar (beta)",
999
+ introProduct: null,
1000
+ 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.",
1001
+ 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.",
1002
+ 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.",
1003
+ privacyLink: "How we handle this",
1004
+ botCheck: "turnstile",
1005
+ questions: "any",
1006
+ followUps: true
1007
+ }
1008
+ }
1009
+ ],
1010
+ [
1011
+ // The words of web-consent-2026-11-19-d without "When you start, Cloudflare checks for bots.", for a server that runs
1012
+ // no bot check. A DRAFT, like it.
1013
+ "web-consent-2026-11-20-d",
1014
+ {
1015
+ en: {
1016
+ title: "Live AI avatar (beta)",
1017
+ introProduct: null,
1018
+ 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.",
1019
+ 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.",
1020
+ 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.",
1021
+ privacyLink: "How we handle this",
1022
+ botCheck: "off",
1023
+ questions: "any",
1024
+ followUps: true
1025
+ }
1026
+ }
826
1027
  ]
827
1028
  ]);
828
1029
  function avatarConsentCopy(version, locale) {
@@ -913,7 +1114,8 @@ function useAvatarMic({
913
1114
  base,
914
1115
  offer,
915
1116
  refreshOffer,
916
- onText
1117
+ onText,
1118
+ onStart
917
1119
  }) {
918
1120
  const S = avatarMicStrings(locale);
919
1121
  const copy = avatarMicNotice(AVATAR_MIC_NOTICE_VERSION, locale);
@@ -935,8 +1137,8 @@ function useAvatarMic({
935
1137
  const buttonRef = react.useRef(null);
936
1138
  const allowRef = react.useRef(null);
937
1139
  const usedEpochRef = react.useRef(0);
938
- const latest = react.useRef({ offer, refreshOffer, onText, base });
939
- latest.current = { offer, refreshOffer, onText, base };
1140
+ const latest = react.useRef({ offer, refreshOffer, onText, onStart, base });
1141
+ latest.current = { offer, refreshOffer, onText, onStart, base };
940
1142
  const cancel = react.useCallback(() => {
941
1143
  seqRef.current += 1;
942
1144
  setLoading(false);
@@ -999,6 +1201,7 @@ function useAvatarMic({
999
1201
  cancel();
1000
1202
  return;
1001
1203
  }
1204
+ latest.current.onStart?.();
1002
1205
  if (!agreedRef.current) {
1003
1206
  setNoticeOpen(true);
1004
1207
  void loadAvatarMic().catch(() => void 0);
@@ -1035,7 +1238,13 @@ function useAvatarMic({
1035
1238
  }
1036
1239
  }, [state]);
1037
1240
  if (!possible || S === null || copy === null) {
1038
- return { button: null, notice: null, lines: null, cancel };
1241
+ return {
1242
+ button: null,
1243
+ notice: null,
1244
+ lines: null,
1245
+ cancel,
1246
+ recording: false
1247
+ };
1039
1248
  }
1040
1249
  const status = withdrawn !== null ? S[FAILURE_TEXT[withdrawn]] : loading2 ? S.micStarting : loadFailed ? S.micFailed : state.name === "starting" ? S.micStarting : state.name === "listening" ? S.micListening : state.name === "finishing" ? S.micFinishing : state.name === "done" ? state.text === "" ? S.micNoSpeech : state.limit ? S.micDoneLimit : S.micDone : state.name === "failed" ? S[FAILURE_TEXT[state.reason]] : "";
1041
1250
  const heard = offered && (state.name === "listening" || state.name === "finishing") ? state.text : "";
@@ -1139,7 +1348,13 @@ function useAvatarMic({
1139
1348
  }
1140
1349
  )
1141
1350
  ] });
1142
- return { button, notice, lines, cancel };
1351
+ return {
1352
+ button,
1353
+ notice,
1354
+ lines,
1355
+ cancel,
1356
+ recording: offered && state.name === "listening"
1357
+ };
1143
1358
  }
1144
1359
 
1145
1360
  // src/components/explore-avatar-turnstile.ts
@@ -1262,6 +1477,12 @@ var END_GRACE_MS = 1e4;
1262
1477
  var CLOCK_TOLERANCE_MS = 5e3;
1263
1478
  var UNHEARD_GRACE_MS = 5e3;
1264
1479
  var PLAYOUT_TAIL_MS = 5e3;
1480
+ var QUIET_PLAYED_OUT_MS = 2500;
1481
+ var OWED_VOICE_WAIT_MS = 15e3;
1482
+ var LISTENING_MARK_MS = 1e4;
1483
+ var BUSY_RETRY_MS = 2e3;
1484
+ var BUSY_RETRY_MAX_MS = 8e3;
1485
+ var MAX_BUSY_RETRIES = 24;
1265
1486
  var SERVICE_MAX_AGE_MS = 15 * 6e4;
1266
1487
  var ANSWER_OFFER_MS = SERVICE_MAX_AGE_MS - 6e4;
1267
1488
  var MAX_TIMER_MS = 2147483647;
@@ -1276,6 +1497,12 @@ function stopAttending(handle) {
1276
1497
  if (handle.tail !== null) clearTimeout(handle.tail);
1277
1498
  handle.unheard = null;
1278
1499
  handle.tail = null;
1500
+ const run = handle.followUps;
1501
+ if (run === null) return;
1502
+ if (run.quiet !== null) clearTimeout(run.quiet);
1503
+ if (run.retry !== null) clearTimeout(run.retry);
1504
+ run.quiet = null;
1505
+ run.retry = null;
1279
1506
  }
1280
1507
  var REFUSAL_TEXT = {
1281
1508
  busy: "busy",
@@ -1286,6 +1513,18 @@ var REFUSAL_TEXT = {
1286
1513
  not_eligible: "notEligible",
1287
1514
  answer_claimed: "answerClaimed"
1288
1515
  };
1516
+ var FOLLOW_UP_TEXT = {
1517
+ no_time: "followUpNoTime",
1518
+ max_answers: "followUpMaxAnswers",
1519
+ // The same line as a first answer the checks refuse: it stays in Chat as text.
1520
+ not_eligible: "notSpoken",
1521
+ answer_claimed: "answerClaimed",
1522
+ limits: "followUpLimits",
1523
+ paused: "followUpPaused",
1524
+ busy: "followUpUnread",
1525
+ answer_failed: "answerFailed",
1526
+ failed: "followUpUnread"
1527
+ };
1289
1528
  var EMPTY = {
1290
1529
  kind: "empty",
1291
1530
  answerId: null,
@@ -1360,7 +1599,11 @@ function inFlight(conversation) {
1360
1599
  return conversation?.phase === "sending" || conversation?.phase === "streaming";
1361
1600
  }
1362
1601
  function acceptsQuestion(slot) {
1363
- return slot.any && !slot.textMode && slot.phase === "needs-answer" && slot.kind === "empty" && slot.reason === null;
1602
+ if (!slot.any || slot.textMode || slot.reason !== null) return false;
1603
+ return slot.phase === "needs-answer" ? slot.kind === "empty" : slot.phase === "live" && slot.followUp;
1604
+ }
1605
+ function followUpBox(phase, run, token) {
1606
+ return phase.name === "live" && run !== null && phase.session.followUp !== null && !phase.session.followUp.closed && token === run.token;
1364
1607
  }
1365
1608
  function micOfferOf(result) {
1366
1609
  return result.kind === "available" && result.offer.stt !== void 0 ? { ...result.offer.stt, receivedAt: Date.now() } : null;
@@ -1515,6 +1758,7 @@ function AvatarLiveView({
1515
1758
  const textBoxRef = react.useRef(false);
1516
1759
  const [micOffer, setMicOffer] = react.useState(null);
1517
1760
  const micCancelRef = react.useRef(() => void 0);
1761
+ const sendDueRef = react.useRef(() => void 0);
1518
1762
  const checkRef = react.useRef(null);
1519
1763
  const askedLocale = react.useRef(locale);
1520
1764
  const startRef = react.useRef(null);
@@ -1603,10 +1847,13 @@ function AvatarLiveView({
1603
1847
  if (!handle || handle.done || latest.current.live.endUnattended !== true) {
1604
1848
  return;
1605
1849
  }
1606
- const current = phaseRef.current.name;
1607
- if (current !== "pending" && current !== "live") return;
1850
+ const shown2 = phaseRef.current;
1851
+ if (shown2.name !== "pending" && shown2.name !== "live") return;
1852
+ const current = shown2.name;
1608
1853
  const away = !inSight();
1609
- if (away && handle.playedOut && current === "live") {
1854
+ const followUp2 = shown2.session.followUp;
1855
+ const asking = followUp2 !== null && followUp2.pending !== null;
1856
+ if (away && handle.playedOut && current === "live" && !asking) {
1610
1857
  finish("finished", "user");
1611
1858
  return;
1612
1859
  }
@@ -1625,6 +1872,184 @@ function AvatarLiveView({
1625
1872
  }
1626
1873
  }, UNHEARD_GRACE_MS);
1627
1874
  }, [finish, inSight]);
1875
+ const showFollowUp = react.useCallback(
1876
+ (handle, next, spoken) => {
1877
+ const current = phaseRef.current;
1878
+ if (sessionRef.current !== handle || handle.done || current.name !== "pending" && current.name !== "live" || current.session.followUp === null) {
1879
+ return;
1880
+ }
1881
+ const session = {
1882
+ ...current.session,
1883
+ ...spoken,
1884
+ followUp: { ...current.session.followUp, ...next }
1885
+ };
1886
+ go(
1887
+ current.name === "live" ? { name: "live", session } : { name: "pending", session }
1888
+ );
1889
+ },
1890
+ [go]
1891
+ );
1892
+ const tell = react.useCallback(
1893
+ (handle, kind2) => {
1894
+ if (handle.done || handle.followUps === null) return;
1895
+ void requestTurn({
1896
+ base: handle.base,
1897
+ headers: handle.headers,
1898
+ sessionId: handle.session.sessionId,
1899
+ secret: handle.session.endSecret,
1900
+ kind: kind2
1901
+ });
1902
+ },
1903
+ []
1904
+ );
1905
+ const whenQuiet = react.useCallback(
1906
+ (handle, delay) => {
1907
+ const run = handle.followUps;
1908
+ if (run === null) return;
1909
+ const wait = (ms) => {
1910
+ if (run.quiet !== null) clearTimeout(run.quiet);
1911
+ run.quiet = setTimeout(() => {
1912
+ run.quiet = null;
1913
+ if (sessionRef.current !== handle || handle.done || run.voice) {
1914
+ return;
1915
+ }
1916
+ if (run.owed) {
1917
+ run.owed = false;
1918
+ wait(OWED_VOICE_WAIT_MS);
1919
+ return;
1920
+ }
1921
+ handle.playedOut = true;
1922
+ showFollowUp(handle, { speaking: false });
1923
+ attend();
1924
+ sendDueRef.current(handle);
1925
+ }, ms);
1926
+ };
1927
+ wait(delay);
1928
+ },
1929
+ [attend, showFollowUp]
1930
+ );
1931
+ const heardVoice = react.useCallback(
1932
+ (handle, speaking) => {
1933
+ const run = handle.followUps;
1934
+ if (run === null) return;
1935
+ run.voice = speaking;
1936
+ if (!speaking) {
1937
+ whenQuiet(handle, QUIET_PLAYED_OUT_MS);
1938
+ return;
1939
+ }
1940
+ if (run.quiet !== null) clearTimeout(run.quiet);
1941
+ run.quiet = null;
1942
+ run.heard = true;
1943
+ run.starts += 1;
1944
+ run.owed = false;
1945
+ handle.playedOut = false;
1946
+ showFollowUp(handle, { speaking: true });
1947
+ },
1948
+ [showFollowUp, whenQuiet]
1949
+ );
1950
+ const sendDue = react.useCallback(
1951
+ (handle) => {
1952
+ const run = handle.followUps;
1953
+ const due = run?.due ?? null;
1954
+ if (run === null || due === null || due.sending || run.retry !== null || handle.done || sessionRef.current !== handle || run.heard && !handle.playedOut) {
1955
+ return;
1956
+ }
1957
+ due.sending = true;
1958
+ const starts = run.starts;
1959
+ const read = () => requestTurn({
1960
+ base: handle.base,
1961
+ headers: handle.headers,
1962
+ sessionId: handle.session.sessionId,
1963
+ secret: handle.session.endSecret,
1964
+ kind: "answer",
1965
+ messageId: due.messageId,
1966
+ idempotencyKey: due.idempotencyKey
1967
+ });
1968
+ const notRead = (unread, closed) => {
1969
+ run.due = null;
1970
+ showFollowUp(handle, {
1971
+ pending: null,
1972
+ unread: due.known ?? unread,
1973
+ ...closed ? { closed } : {}
1974
+ });
1975
+ attend();
1976
+ };
1977
+ void (async () => {
1978
+ let result = await read();
1979
+ if (result.kind === "failed" && result.lost && !handle.done) {
1980
+ result = await read();
1981
+ }
1982
+ due.sending = false;
1983
+ if (sessionRef.current !== handle || handle.done || run.due !== due) {
1984
+ return;
1985
+ }
1986
+ if (result.kind === "refused" && result.reason === "busy" && due.busy < MAX_BUSY_RETRIES) {
1987
+ due.busy += 1;
1988
+ if (run.heard && !handle.playedOut) return;
1989
+ const delay = Math.max(
1990
+ result.retryAfterSeconds === void 0 ? BUSY_RETRY_MS : result.retryAfterSeconds * 1e3,
1991
+ Math.min(BUSY_RETRY_MAX_MS, BUSY_RETRY_MS * 2 ** (due.busy - 1))
1992
+ );
1993
+ if (Date.now() + delay >= handle.endsAt) {
1994
+ notRead("no_time", false);
1995
+ return;
1996
+ }
1997
+ run.retry = setTimeout(() => {
1998
+ run.retry = null;
1999
+ sendDueRef.current(handle);
2000
+ }, delay);
2001
+ return;
2002
+ }
2003
+ if (result.kind !== "accepted") {
2004
+ notRead(
2005
+ result.kind === "refused" ? result.reason : "failed",
2006
+ result.kind === "gone" || result.kind === "refused" && result.reason === "max_answers"
2007
+ );
2008
+ return;
2009
+ }
2010
+ run.due = null;
2011
+ run.answers += 1;
2012
+ const heardSince = run.starts !== starts;
2013
+ run.owed = !heardSince;
2014
+ const playing = !heardSince || run.voice || run.quiet !== null;
2015
+ if (playing) handle.playedOut = false;
2016
+ showFollowUp(
2017
+ handle,
2018
+ {
2019
+ speaking: playing,
2020
+ pending: null,
2021
+ unread: null,
2022
+ closed: run.answers >= run.limits.maxAnswers
2023
+ },
2024
+ { captions: result.captions, excerpt: result.excerpt }
2025
+ );
2026
+ if (!playing) {
2027
+ attend();
2028
+ return;
2029
+ }
2030
+ if (run.owed && run.heard && !run.voice) whenQuiet(handle, 0);
2031
+ })();
2032
+ },
2033
+ [attend, showFollowUp, whenQuiet]
2034
+ );
2035
+ sendDueRef.current = sendDue;
2036
+ const readFollowUp = react.useCallback(
2037
+ (handle, messageId, known) => {
2038
+ const run = handle.followUps;
2039
+ if (run === null) return;
2040
+ run.sent.add(messageId);
2041
+ const idempotencyKey = newIdempotencyKey();
2042
+ if (idempotencyKey === null) {
2043
+ showFollowUp(handle, { pending: null, unread: known ?? "failed" });
2044
+ attend();
2045
+ return;
2046
+ }
2047
+ run.due = { messageId, idempotencyKey, known, busy: 0, sending: false };
2048
+ showFollowUp(handle, { pending: "reading", unread: null });
2049
+ sendDue(handle);
2050
+ },
2051
+ [attend, sendDue, showFollowUp]
2052
+ );
1628
2053
  const cancelStart = react.useCallback((reason) => {
1629
2054
  const run = startRef.current;
1630
2055
  if (!run) return;
@@ -1688,16 +2113,32 @@ function AvatarLiveView({
1688
2113
  });
1689
2114
  }, [applyAvailability, go]);
1690
2115
  const begin = react.useCallback(
1691
- (session, module, offer, target) => {
2116
+ (session, context) => {
2117
+ const { offer } = context;
1692
2118
  const received = Date.now();
1693
2119
  const longest = offer.maxSessionSeconds * 1e3;
1694
2120
  const ahead = session.endsAt - received;
1695
2121
  const endsAt = ahead > 0 && ahead <= longest + CLOCK_TOLERANCE_MS ? session.endsAt : received + longest;
2122
+ const followUps = session.followUps !== void 0 && offer.copy.followUps === true && offer.questions === "any" ? {
2123
+ limits: session.followUps,
2124
+ token: context.conversationToken,
2125
+ answers: 1,
2126
+ voice: false,
2127
+ heard: false,
2128
+ starts: 0,
2129
+ owed: true,
2130
+ quiet: null,
2131
+ asked: null,
2132
+ due: null,
2133
+ retry: null,
2134
+ sent: /* @__PURE__ */ new Set([context.messageId]),
2135
+ seq: 0
2136
+ } : null;
1696
2137
  sessionRef.current = {
1697
2138
  session,
1698
- base: target.base,
1699
- headers: target.headers,
1700
- module,
2139
+ base: context.base,
2140
+ headers: context.headers,
2141
+ module: context.module,
1701
2142
  room: null,
1702
2143
  joined: null,
1703
2144
  endsAt,
@@ -1707,6 +2148,7 @@ function AvatarLiveView({
1707
2148
  playedOut: false,
1708
2149
  unheard: null,
1709
2150
  tail: null,
2151
+ followUps,
1710
2152
  done: false
1711
2153
  };
1712
2154
  setBlocked(false);
@@ -1716,7 +2158,13 @@ function AvatarLiveView({
1716
2158
  session: {
1717
2159
  endsAt,
1718
2160
  captions: session.captions,
1719
- excerpt: session.excerpt
2161
+ excerpt: session.excerpt,
2162
+ followUp: followUps === null ? null : {
2163
+ speaking: true,
2164
+ pending: null,
2165
+ unread: null,
2166
+ closed: followUps.answers >= followUps.limits.maxAnswers
2167
+ }
1720
2168
  }
1721
2169
  });
1722
2170
  },
@@ -1748,7 +2196,7 @@ function AvatarLiveView({
1748
2196
  }
1749
2197
  switch (result.kind) {
1750
2198
  case "created":
1751
- begin(result.session, context.module, context.offer, context);
2199
+ begin(result.session, context);
1752
2200
  return;
1753
2201
  case "unusable":
1754
2202
  sendEnd({
@@ -1846,7 +2294,14 @@ function AvatarLiveView({
1846
2294
  return;
1847
2295
  }
1848
2296
  if (turnstile !== null) go({ name: "creating" });
1849
- const context = { module: loaded, offer, messageId, base: base2, headers: headers2 };
2297
+ const context = {
2298
+ module: loaded,
2299
+ offer,
2300
+ messageId,
2301
+ conversationToken,
2302
+ base: base2,
2303
+ headers: headers2
2304
+ };
1850
2305
  const result = await requestCreate({
1851
2306
  base: base2,
1852
2307
  headers: headers2,
@@ -2151,6 +2606,9 @@ function AvatarLiveView({
2151
2606
  attend();
2152
2607
  }, PLAYOUT_TAIL_MS);
2153
2608
  return;
2609
+ case "speaking":
2610
+ heardVoice(handle, event.speaking);
2611
+ return;
2154
2612
  case "speaker-left":
2155
2613
  finish(
2156
2614
  wasLive ? "finished" : "failed",
@@ -2163,7 +2621,7 @@ function AvatarLiveView({
2163
2621
  return;
2164
2622
  }
2165
2623
  },
2166
- [attend, finish, goLive, inSight]
2624
+ [attend, finish, goLive, heardVoice, inSight]
2167
2625
  );
2168
2626
  react.useEffect(() => {
2169
2627
  if (phase.name !== "pending") return;
@@ -2299,7 +2757,13 @@ function AvatarLiveView({
2299
2757
  const textMode = any && (usedToday && (phase.name === "needs-answer" || phase.name === "consent" || phase.name === "declined" || phase.name === "ended") || phase.name === "refused" && phase.reason === "limits");
2300
2758
  textBoxRef.current = textMode;
2301
2759
  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;
2760
+ const followUp = phase.name === "live" ? phase.session.followUp : null;
2761
+ const followUpAsks = followUpBox(
2762
+ phase,
2763
+ sessionRef.current?.followUps ?? null,
2764
+ token
2765
+ );
2766
+ const askReason = inFlight(conversation) || followUp !== null && followUp.pending !== null ? S.askBusy : conversation?.canSend ? null : S.askUnavailable;
2303
2767
  const refreshMicOffer = react.useCallback(async () => {
2304
2768
  const { live: live2, locale: locale2, base: base2, headers: headers2 } = latest.current;
2305
2769
  if (base2 === null) return null;
@@ -2324,21 +2788,54 @@ function AvatarLiveView({
2324
2788
  },
2325
2789
  [askMaxLength, touchFirst]
2326
2790
  );
2791
+ const onMicStart = react.useCallback(() => {
2792
+ const handle = sessionRef.current;
2793
+ const current = phaseRef.current;
2794
+ if (handle === null || handle.followUps === null || handle.done || current.name !== "live" || current.session.followUp?.speaking !== true) {
2795
+ return;
2796
+ }
2797
+ const run = handle.followUps;
2798
+ tell(handle, "interrupt");
2799
+ if (run.quiet !== null) clearTimeout(run.quiet);
2800
+ run.quiet = null;
2801
+ run.owed = false;
2802
+ handle.playedOut = true;
2803
+ showFollowUp(handle, { speaking: false });
2804
+ }, [showFollowUp, tell]);
2327
2805
  const mic = useAvatarMic({
2328
2806
  enabled: live.mic === true && micOffer !== null && !off && acceptsQuestion({
2329
2807
  any,
2330
2808
  phase: phase.name,
2331
2809
  kind,
2332
2810
  textMode,
2333
- reason: askReason
2811
+ reason: askReason,
2812
+ followUp: followUpAsks
2334
2813
  }),
2335
2814
  locale,
2336
2815
  base,
2337
2816
  offer: micOffer,
2338
2817
  refreshOffer: refreshMicOffer,
2339
- onText: onMicText
2818
+ onText: onMicText,
2819
+ onStart: onMicStart
2340
2820
  });
2341
2821
  micCancelRef.current = mic.cancel;
2822
+ const micRecording = mic.recording;
2823
+ react.useEffect(() => {
2824
+ const handle = sessionRef.current;
2825
+ if (!micRecording || handle === null || handle.followUps === null || handle.done) {
2826
+ return;
2827
+ }
2828
+ tell(handle, "listening");
2829
+ const every = Math.min(
2830
+ LISTENING_MARK_MS,
2831
+ Math.max(1e3, handle.followUps.limits.idleSeconds * 1e3 / 3)
2832
+ );
2833
+ const timer = setInterval(() => tell(handle, "listening"), every);
2834
+ return () => {
2835
+ clearInterval(timer);
2836
+ if (sessionRef.current === handle) tell(handle, "listening");
2837
+ };
2838
+ }, [micRecording, tell]);
2342
2839
  const notNow = react.useCallback(() => {
2343
2840
  const current = phaseRef.current;
2344
2841
  if ((current.name === "consent" || current.name === "needs-answer") && current.offer.questions === "any") {
@@ -2347,6 +2844,43 @@ function AvatarLiveView({
2347
2844
  }
2348
2845
  onContinueInChat();
2349
2846
  }, [go, onContinueInChat]);
2847
+ const askFollowUp = react.useCallback(
2848
+ (handle, conversation2, question) => {
2849
+ const run = handle.followUps;
2850
+ const current = phaseRef.current;
2851
+ if (run === null || handle.done || current.name !== "live" || current.session.followUp?.pending !== null || !followUpBox(current, run, conversation2.getConversationToken())) {
2852
+ return;
2853
+ }
2854
+ sendingRef.current = true;
2855
+ run.seq += 1;
2856
+ const seq = run.seq;
2857
+ setDraft("");
2858
+ tell(handle, "asking");
2859
+ showFollowUp(handle, { pending: "asking", unread: null });
2860
+ void conversation2.send(question, [], { surface: "AVATAR" }).then((added) => {
2861
+ if (added === null) {
2862
+ setDraft((current2) => current2 === "" ? question : current2);
2863
+ }
2864
+ if (sessionRef.current !== handle || handle.done || run.seq !== seq) {
2865
+ return;
2866
+ }
2867
+ if (added === null || added.conversationToken !== run.token) {
2868
+ showFollowUp(handle, { pending: null });
2869
+ attend();
2870
+ return;
2871
+ }
2872
+ run.asked = {
2873
+ messageId: added.userMessageId,
2874
+ token: added.conversationToken,
2875
+ answerId: null
2876
+ };
2877
+ showFollowUp(handle, { pending: "asking" });
2878
+ }).finally(() => {
2879
+ sendingRef.current = false;
2880
+ });
2881
+ },
2882
+ [attend, showFollowUp, tell]
2883
+ );
2350
2884
  const ask = react.useCallback(
2351
2885
  (text) => {
2352
2886
  const conversation2 = conversationRef.current;
@@ -2355,6 +2889,11 @@ function AvatarLiveView({
2355
2889
  return;
2356
2890
  }
2357
2891
  const spoken = !textBoxRef.current;
2892
+ const handle = sessionRef.current;
2893
+ if (spoken && handle !== null && handle.followUps !== null) {
2894
+ askFollowUp(handle, conversation2, question);
2895
+ return;
2896
+ }
2358
2897
  if (spoken && conversation2.messages.length > 0) return;
2359
2898
  sendingRef.current = true;
2360
2899
  setAskedHere(null);
@@ -2376,8 +2915,64 @@ function AvatarLiveView({
2376
2915
  sendingRef.current = false;
2377
2916
  });
2378
2917
  },
2379
- [setAskedHere]
2918
+ [askFollowUp, setAskedHere]
2380
2919
  );
2920
+ react.useEffect(() => {
2921
+ const handle = sessionRef.current;
2922
+ const run = handle?.followUps ?? null;
2923
+ if (handle === null || run === null || handle.done || run.asked === null) {
2924
+ return;
2925
+ }
2926
+ const asked = run.asked;
2927
+ const chat = conversationRef.current;
2928
+ const messages = chat?.messages ?? [];
2929
+ const at = messages.findIndex((message) => message.id === asked.messageId);
2930
+ const question = at === -1 ? void 0 : messages[at];
2931
+ const answer = at === -1 ? void 0 : messages[at + 1];
2932
+ const drop = (unread2) => {
2933
+ run.asked = null;
2934
+ showFollowUp(handle, { pending: null, unread: unread2 });
2935
+ attend();
2936
+ };
2937
+ const unread = (id, reason) => {
2938
+ if (id === null || run.sent.has(id) || !isAvatarMessageId(id)) {
2939
+ drop(reason);
2940
+ return;
2941
+ }
2942
+ run.asked = null;
2943
+ readFollowUp(handle, id, reason);
2944
+ };
2945
+ if (question === void 0 || (chat?.getConversationToken() ?? null) !== run.token) {
2946
+ drop(null);
2947
+ return;
2948
+ }
2949
+ if (answer === void 0) {
2950
+ if (!inFlight(chat)) unread(asked.answerId, "answer_failed");
2951
+ return;
2952
+ }
2953
+ if (answer.status === "PENDING" || answer.status === "RUNNING") {
2954
+ if (answer.role === "ASSISTANT") asked.answerId = answer.id;
2955
+ return;
2956
+ }
2957
+ if (answer.role !== "ASSISTANT") {
2958
+ unread(asked.answerId, "answer_failed");
2959
+ return;
2960
+ }
2961
+ if (answer.status !== "COMPLETED" || answer.content.trim() === "") {
2962
+ unread(answer.id, "answer_failed");
2963
+ return;
2964
+ }
2965
+ if (run.sent.has(answer.id) || !isAvatarMessageId(answer.id)) {
2966
+ drop("not_eligible");
2967
+ return;
2968
+ }
2969
+ if (at + 2 !== messages.length || !readable(question, answer)) {
2970
+ unread(answer.id, "not_eligible");
2971
+ return;
2972
+ }
2973
+ run.asked = null;
2974
+ readFollowUp(handle, answer.id, null);
2975
+ });
2381
2976
  const newChatForAvatar = react.useCallback(() => {
2382
2977
  const conversation2 = conversationRef.current;
2383
2978
  if (conversation2 === null) return;
@@ -2401,7 +2996,14 @@ function AvatarLiveView({
2401
2996
  const focused = document.activeElement;
2402
2997
  if (focused && focused !== document.body && focused.isConnected) return;
2403
2998
  rootRef.current?.querySelector(PRIMARY)?.focus();
2404
- }, [active, phase.name, askState, shown.pendingAnswerId, blocked]);
2999
+ }, [
3000
+ active,
3001
+ phase.name,
3002
+ askState,
3003
+ shown.pendingAnswerId,
3004
+ blocked,
3005
+ followUpAsks
3006
+ ]);
2405
3007
  const onPlaying = goLive;
2406
3008
  const enablePlayback = react.useCallback(() => {
2407
3009
  void sessionRef.current?.joined?.enablePlayback().catch(() => void 0);
@@ -2801,13 +3403,32 @@ function AvatarLiveView({
2801
3403
  actions2 = stopButton;
2802
3404
  break;
2803
3405
  case "pending":
2804
- case "live":
2805
- status = phase.name === "live" ? speakingLine(phase.session) : S.connecting;
3406
+ case "live": {
2806
3407
  top2 = badge;
2807
3408
  media2 = liveMedia(phase.name === "live");
2808
3409
  body2 = liveBody(phase.session);
2809
3410
  actions2 = liveActions(readInChatButton(false));
3411
+ if (followUp === null) {
3412
+ status = phase.name === "live" ? speakingLine(phase.session) : S.connecting;
3413
+ break;
3414
+ }
3415
+ 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;
3416
+ if (!followUp.speaking && followUp.pending === null && followUp.unread !== null && !followUp.closed) {
3417
+ body2 = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3418
+ body2,
3419
+ /* @__PURE__ */ jsxRuntime.jsx(
3420
+ "p",
3421
+ {
3422
+ "data-boff-explore": "avatar-live-idle",
3423
+ className: "text-xs text-muted-foreground",
3424
+ children: S.followUpIdle
3425
+ }
3426
+ )
3427
+ ] });
3428
+ }
3429
+ if (followUpAsks) box = "spoken";
2810
3430
  break;
3431
+ }
2811
3432
  case "ended":
2812
3433
  status = S[phase.how];
2813
3434
  outcome2 = true;
@@ -2855,7 +3476,7 @@ function AvatarLiveView({
2855
3476
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: S.newChatNote })
2856
3477
  ] }) : null,
2857
3478
  chips,
2858
- box !== null && usedToday ? /* @__PURE__ */ jsxRuntime.jsx(
3479
+ box !== null && usedToday && phase.name !== "live" ? /* @__PURE__ */ jsxRuntime.jsx(
2859
3480
  "p",
2860
3481
  {
2861
3482
  "data-boff-explore": "avatar-live-used-today",