@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.
@@ -9,6 +9,8 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
9
9
  // src/components/explore-avatar-api.ts
10
10
  var AVAILABILITY_TIMEOUT_MS = 8e3;
11
11
  var CREATE_TIMEOUT_MS = 3e4;
12
+ var TURN_TIMEOUT_MS = 8e3;
13
+ var TURN_ANSWER_TIMEOUT_MS = 25e3;
12
14
  var MESSAGE_ID = /^[A-Za-z0-9_-]{1,64}$/;
13
15
  var STT_TOKEN = /^[A-Za-z0-9._~-]{1,4096}$/;
14
16
  var CONVERSATION_TOKEN = /^[A-Za-z0-9._~+/=-]{16,512}$/;
@@ -113,6 +115,21 @@ function errorCode(body) {
113
115
  const { code } = body.error;
114
116
  return typeof code === "string" ? code : null;
115
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
+ }
116
133
  function readCreated(body) {
117
134
  if (!isRecord(body)) return FAILED;
118
135
  const { sessionId, endSecret } = body;
@@ -130,6 +147,7 @@ function readCreated(body) {
130
147
  if (typeof livekitUrl !== "string" || !livekitUrl.startsWith("wss://") || typeof participantToken !== "string" || participantToken === "" || typeof workerIdentity !== "string" || workerIdentity === "" || typeof locale !== "string" || typeof text !== "string") {
131
148
  return unusable;
132
149
  }
150
+ const followUps = readFollowUps(body.followUps);
133
151
  return {
134
152
  kind: "created",
135
153
  session: {
@@ -138,7 +156,8 @@ function readCreated(body) {
138
156
  endsAt,
139
157
  join: { livekitUrl, participantToken, workerIdentity },
140
158
  captions: { locale, text },
141
- excerpt: body.excerpt === true
159
+ excerpt: body.excerpt === true,
160
+ ...followUps === void 0 ? {} : { followUps }
142
161
  }
143
162
  };
144
163
  }
@@ -269,6 +288,96 @@ function sendEnd(target) {
269
288
  } catch {
270
289
  }
271
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
+ }
272
381
  var BOT_BINDING_PREFIX = "boff-web-avatar-cdata/v1";
273
382
  function webCryptoSubtle() {
274
383
  const subtle = globalThis.crypto?.subtle;
@@ -366,7 +475,14 @@ var EN = {
366
475
  usedToday: "You've had today's avatar session. Answers will show here as text.",
367
476
  needsAnswerAny: "Ask a question here, and the avatar reads the answer aloud.",
368
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.",
369
- 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."
370
486
  };
371
487
  var ZH = {
372
488
  checking: "\u6B63\u5728\u68C0\u67E5\u5B9E\u65F6\u865A\u62DF\u5F62\u8C61\u662F\u5426\u53EF\u7528\u2026",
@@ -427,7 +543,16 @@ var ZH = {
427
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",
428
544
  needsAnswerAny: "\u5728\u8FD9\u91CC\u63D0\u95EE\uFF0C\u865A\u62DF\u5F62\u8C61\u4F1A\u6717\u8BFB\u56DE\u7B54\u3002",
429
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",
430
- 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"
431
556
  };
432
557
  var HI = {
433
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",
@@ -488,7 +613,16 @@ var HI = {
488
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",
489
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",
490
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",
491
- notEligibleAny: "\u0905\u0935\u0924\u093E\u0930 \u092F\u0939 \u0909\u0924\u094D\u0924\u0930 \u091C\u093C\u094B\u0930 \u0938\u0947 \u0928\u0939\u0940\u0902 \u092A\u0922\u093C \u0938\u0915\u0924\u093E\u0964 \u0909\u0924\u094D\u0924\u0930 \u0914\u0930 \u0938\u094D\u0930\u094B\u0924 \u091A\u0948\u091F \u092E\u0947\u0902 \u0939\u0948\u0902\u0964"
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"
492
626
  };
493
627
  var ES = {
494
628
  checking: "Comprobando si el avatar en vivo est\xE1 disponible\u2026",
@@ -549,7 +683,16 @@ var ES = {
549
683
  usedToday: "Ya has usado la sesi\xF3n de avatar de hoy. Las respuestas se mostrar\xE1n aqu\xED como texto.",
550
684
  needsAnswerAny: "Haz una pregunta aqu\xED y el avatar leer\xE1 la respuesta en voz alta.",
551
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.",
552
- 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."
553
696
  };
554
697
  var AR = {
555
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",
@@ -610,7 +753,16 @@ var AR = {
610
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.",
611
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.",
612
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.",
613
- notEligibleAny: "\u0644\u0627 \u064A\u0645\u0643\u0646 \u0644\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0642\u0631\u0627\u0621\u0629 \u0647\u0630\u0647 \u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0628\u0635\u0648\u062A \u0639\u0627\u0644\u064D. \u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0648\u0627\u0644\u0645\u0635\u0627\u062F\u0631 \u0645\u0648\u062C\u0648\u062F\u0629 \u0641\u064A \u0627\u0644\u062F\u0631\u062F\u0634\u0629."
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."
614
766
  };
615
767
  var TA = {
616
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",
@@ -671,7 +823,16 @@ var TA = {
671
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.",
672
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.",
673
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.",
674
- notEligibleAny: "\u0B87\u0BA8\u0BCD\u0BA4\u0BAA\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BC8 \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BBE\u0BB2\u0BCD \u0B89\u0BB0\u0B95\u0BCD\u0B95\u0BAA\u0BCD \u0BAA\u0B9F\u0BBF\u0B95\u0BCD\u0B95 \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BBE\u0BA4\u0BC1. \u0BAA\u0BA4\u0BBF\u0BB2\u0BC1\u0BAE\u0BCD \u0B86\u0BA4\u0BBE\u0BB0\u0B99\u0BCD\u0B95\u0BB3\u0BC1\u0BAE\u0BCD \u0B85\u0BB0\u0B9F\u0BCD\u0B9F\u0BC8\u0BAF\u0BBF\u0BB2\u0BCD \u0B89\u0BB3\u0BCD\u0BB3\u0BA9."
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."
675
836
  };
676
837
  var STRINGS = {
677
838
  en: EN,
@@ -821,6 +982,46 @@ var CONSENT = /* @__PURE__ */ new Map([
821
982
  questions: "any"
822
983
  }
823
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
+ }
824
1025
  ]
825
1026
  ]);
826
1027
  function avatarConsentCopy(version, locale) {
@@ -911,7 +1112,8 @@ function useAvatarMic({
911
1112
  base,
912
1113
  offer,
913
1114
  refreshOffer,
914
- onText
1115
+ onText,
1116
+ onStart
915
1117
  }) {
916
1118
  const S = avatarMicStrings(locale);
917
1119
  const copy = avatarMicNotice(AVATAR_MIC_NOTICE_VERSION, locale);
@@ -933,8 +1135,8 @@ function useAvatarMic({
933
1135
  const buttonRef = useRef(null);
934
1136
  const allowRef = useRef(null);
935
1137
  const usedEpochRef = useRef(0);
936
- const latest = useRef({ offer, refreshOffer, onText, base });
937
- latest.current = { offer, refreshOffer, onText, base };
1138
+ const latest = useRef({ offer, refreshOffer, onText, onStart, base });
1139
+ latest.current = { offer, refreshOffer, onText, onStart, base };
938
1140
  const cancel = useCallback(() => {
939
1141
  seqRef.current += 1;
940
1142
  setLoading(false);
@@ -997,6 +1199,7 @@ function useAvatarMic({
997
1199
  cancel();
998
1200
  return;
999
1201
  }
1202
+ latest.current.onStart?.();
1000
1203
  if (!agreedRef.current) {
1001
1204
  setNoticeOpen(true);
1002
1205
  void loadAvatarMic().catch(() => void 0);
@@ -1033,7 +1236,13 @@ function useAvatarMic({
1033
1236
  }
1034
1237
  }, [state]);
1035
1238
  if (!possible || S === null || copy === null) {
1036
- return { button: null, notice: null, lines: null, cancel };
1239
+ return {
1240
+ button: null,
1241
+ notice: null,
1242
+ lines: null,
1243
+ cancel,
1244
+ recording: false
1245
+ };
1037
1246
  }
1038
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]] : "";
1039
1248
  const heard = offered && (state.name === "listening" || state.name === "finishing") ? state.text : "";
@@ -1137,7 +1346,13 @@ function useAvatarMic({
1137
1346
  }
1138
1347
  )
1139
1348
  ] });
1140
- return { button, notice, lines, cancel };
1349
+ return {
1350
+ button,
1351
+ notice,
1352
+ lines,
1353
+ cancel,
1354
+ recording: offered && state.name === "listening"
1355
+ };
1141
1356
  }
1142
1357
 
1143
1358
  // src/components/explore-avatar-turnstile.ts
@@ -1260,6 +1475,12 @@ var END_GRACE_MS = 1e4;
1260
1475
  var CLOCK_TOLERANCE_MS = 5e3;
1261
1476
  var UNHEARD_GRACE_MS = 5e3;
1262
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;
1263
1484
  var SERVICE_MAX_AGE_MS = 15 * 6e4;
1264
1485
  var ANSWER_OFFER_MS = SERVICE_MAX_AGE_MS - 6e4;
1265
1486
  var MAX_TIMER_MS = 2147483647;
@@ -1274,6 +1495,12 @@ function stopAttending(handle) {
1274
1495
  if (handle.tail !== null) clearTimeout(handle.tail);
1275
1496
  handle.unheard = null;
1276
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;
1277
1504
  }
1278
1505
  var REFUSAL_TEXT = {
1279
1506
  busy: "busy",
@@ -1284,6 +1511,18 @@ var REFUSAL_TEXT = {
1284
1511
  not_eligible: "notEligible",
1285
1512
  answer_claimed: "answerClaimed"
1286
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
+ };
1287
1526
  var EMPTY = {
1288
1527
  kind: "empty",
1289
1528
  answerId: null,
@@ -1358,7 +1597,11 @@ function inFlight(conversation) {
1358
1597
  return conversation?.phase === "sending" || conversation?.phase === "streaming";
1359
1598
  }
1360
1599
  function acceptsQuestion(slot) {
1361
- return slot.any && !slot.textMode && slot.phase === "needs-answer" && slot.kind === "empty" && slot.reason === null;
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;
1362
1605
  }
1363
1606
  function micOfferOf(result) {
1364
1607
  return result.kind === "available" && result.offer.stt !== void 0 ? { ...result.offer.stt, receivedAt: Date.now() } : null;
@@ -1513,6 +1756,7 @@ function AvatarLiveView({
1513
1756
  const textBoxRef = useRef(false);
1514
1757
  const [micOffer, setMicOffer] = useState(null);
1515
1758
  const micCancelRef = useRef(() => void 0);
1759
+ const sendDueRef = useRef(() => void 0);
1516
1760
  const checkRef = useRef(null);
1517
1761
  const askedLocale = useRef(locale);
1518
1762
  const startRef = useRef(null);
@@ -1601,10 +1845,13 @@ function AvatarLiveView({
1601
1845
  if (!handle || handle.done || latest.current.live.endUnattended !== true) {
1602
1846
  return;
1603
1847
  }
1604
- const current = phaseRef.current.name;
1605
- 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;
1606
1851
  const away = !inSight();
1607
- 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) {
1608
1855
  finish("finished", "user");
1609
1856
  return;
1610
1857
  }
@@ -1623,6 +1870,184 @@ function AvatarLiveView({
1623
1870
  }
1624
1871
  }, UNHEARD_GRACE_MS);
1625
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
+ );
1626
2051
  const cancelStart = useCallback((reason) => {
1627
2052
  const run = startRef.current;
1628
2053
  if (!run) return;
@@ -1686,16 +2111,32 @@ function AvatarLiveView({
1686
2111
  });
1687
2112
  }, [applyAvailability, go]);
1688
2113
  const begin = useCallback(
1689
- (session, module, offer, target) => {
2114
+ (session, context) => {
2115
+ const { offer } = context;
1690
2116
  const received = Date.now();
1691
2117
  const longest = offer.maxSessionSeconds * 1e3;
1692
2118
  const ahead = session.endsAt - received;
1693
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;
1694
2135
  sessionRef.current = {
1695
2136
  session,
1696
- base: target.base,
1697
- headers: target.headers,
1698
- module,
2137
+ base: context.base,
2138
+ headers: context.headers,
2139
+ module: context.module,
1699
2140
  room: null,
1700
2141
  joined: null,
1701
2142
  endsAt,
@@ -1705,6 +2146,7 @@ function AvatarLiveView({
1705
2146
  playedOut: false,
1706
2147
  unheard: null,
1707
2148
  tail: null,
2149
+ followUps,
1708
2150
  done: false
1709
2151
  };
1710
2152
  setBlocked(false);
@@ -1714,7 +2156,13 @@ function AvatarLiveView({
1714
2156
  session: {
1715
2157
  endsAt,
1716
2158
  captions: session.captions,
1717
- 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
+ }
1718
2166
  }
1719
2167
  });
1720
2168
  },
@@ -1746,7 +2194,7 @@ function AvatarLiveView({
1746
2194
  }
1747
2195
  switch (result.kind) {
1748
2196
  case "created":
1749
- begin(result.session, context.module, context.offer, context);
2197
+ begin(result.session, context);
1750
2198
  return;
1751
2199
  case "unusable":
1752
2200
  sendEnd({
@@ -1844,7 +2292,14 @@ function AvatarLiveView({
1844
2292
  return;
1845
2293
  }
1846
2294
  if (turnstile !== null) go({ name: "creating" });
1847
- 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
+ };
1848
2303
  const result = await requestCreate({
1849
2304
  base: base2,
1850
2305
  headers: headers2,
@@ -2149,6 +2604,9 @@ function AvatarLiveView({
2149
2604
  attend();
2150
2605
  }, PLAYOUT_TAIL_MS);
2151
2606
  return;
2607
+ case "speaking":
2608
+ heardVoice(handle, event.speaking);
2609
+ return;
2152
2610
  case "speaker-left":
2153
2611
  finish(
2154
2612
  wasLive ? "finished" : "failed",
@@ -2161,7 +2619,7 @@ function AvatarLiveView({
2161
2619
  return;
2162
2620
  }
2163
2621
  },
2164
- [attend, finish, goLive, inSight]
2622
+ [attend, finish, goLive, heardVoice, inSight]
2165
2623
  );
2166
2624
  useEffect(() => {
2167
2625
  if (phase.name !== "pending") return;
@@ -2297,7 +2755,13 @@ function AvatarLiveView({
2297
2755
  const textMode = any && (usedToday && (phase.name === "needs-answer" || phase.name === "consent" || phase.name === "declined" || phase.name === "ended") || phase.name === "refused" && phase.reason === "limits");
2298
2756
  textBoxRef.current = textMode;
2299
2757
  const askState = !any ? void 0 : textMode ? "text" : phase.name === "needs-answer" ? kind === "empty" ? "ready" : kind : phase.name;
2300
- const askReason = inFlight(conversation) ? S.askBusy : conversation?.canSend ? null : S.askUnavailable;
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;
2301
2765
  const refreshMicOffer = useCallback(async () => {
2302
2766
  const { live: live2, locale: locale2, base: base2, headers: headers2 } = latest.current;
2303
2767
  if (base2 === null) return null;
@@ -2322,21 +2786,54 @@ function AvatarLiveView({
2322
2786
  },
2323
2787
  [askMaxLength, touchFirst]
2324
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]);
2325
2803
  const mic = useAvatarMic({
2326
2804
  enabled: live.mic === true && micOffer !== null && !off && acceptsQuestion({
2327
2805
  any,
2328
2806
  phase: phase.name,
2329
2807
  kind,
2330
2808
  textMode,
2331
- reason: askReason
2809
+ reason: askReason,
2810
+ followUp: followUpAsks
2332
2811
  }),
2333
2812
  locale,
2334
2813
  base,
2335
2814
  offer: micOffer,
2336
2815
  refreshOffer: refreshMicOffer,
2337
- onText: onMicText
2816
+ onText: onMicText,
2817
+ onStart: onMicStart
2338
2818
  });
2339
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]);
2340
2837
  const notNow = useCallback(() => {
2341
2838
  const current = phaseRef.current;
2342
2839
  if ((current.name === "consent" || current.name === "needs-answer") && current.offer.questions === "any") {
@@ -2345,6 +2842,43 @@ function AvatarLiveView({
2345
2842
  }
2346
2843
  onContinueInChat();
2347
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
+ );
2348
2882
  const ask = useCallback(
2349
2883
  (text) => {
2350
2884
  const conversation2 = conversationRef.current;
@@ -2353,6 +2887,11 @@ function AvatarLiveView({
2353
2887
  return;
2354
2888
  }
2355
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
+ }
2356
2895
  if (spoken && conversation2.messages.length > 0) return;
2357
2896
  sendingRef.current = true;
2358
2897
  setAskedHere(null);
@@ -2374,8 +2913,64 @@ function AvatarLiveView({
2374
2913
  sendingRef.current = false;
2375
2914
  });
2376
2915
  },
2377
- [setAskedHere]
2916
+ [askFollowUp, setAskedHere]
2378
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
+ });
2379
2974
  const newChatForAvatar = useCallback(() => {
2380
2975
  const conversation2 = conversationRef.current;
2381
2976
  if (conversation2 === null) return;
@@ -2399,7 +2994,14 @@ function AvatarLiveView({
2399
2994
  const focused = document.activeElement;
2400
2995
  if (focused && focused !== document.body && focused.isConnected) return;
2401
2996
  rootRef.current?.querySelector(PRIMARY)?.focus();
2402
- }, [active, phase.name, askState, shown.pendingAnswerId, blocked]);
2997
+ }, [
2998
+ active,
2999
+ phase.name,
3000
+ askState,
3001
+ shown.pendingAnswerId,
3002
+ blocked,
3003
+ followUpAsks
3004
+ ]);
2403
3005
  const onPlaying = goLive;
2404
3006
  const enablePlayback = useCallback(() => {
2405
3007
  void sessionRef.current?.joined?.enablePlayback().catch(() => void 0);
@@ -2799,13 +3401,32 @@ function AvatarLiveView({
2799
3401
  actions2 = stopButton;
2800
3402
  break;
2801
3403
  case "pending":
2802
- case "live":
2803
- status = phase.name === "live" ? speakingLine(phase.session) : S.connecting;
3404
+ case "live": {
2804
3405
  top2 = badge;
2805
3406
  media2 = liveMedia(phase.name === "live");
2806
3407
  body2 = liveBody(phase.session);
2807
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";
2808
3428
  break;
3429
+ }
2809
3430
  case "ended":
2810
3431
  status = S[phase.how];
2811
3432
  outcome2 = true;
@@ -2853,7 +3474,7 @@ function AvatarLiveView({
2853
3474
  /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: S.newChatNote })
2854
3475
  ] }) : null,
2855
3476
  chips,
2856
- box !== null && usedToday ? /* @__PURE__ */ jsx(
3477
+ box !== null && usedToday && phase.name !== "live" ? /* @__PURE__ */ jsx(
2857
3478
  "p",
2858
3479
  {
2859
3480
  "data-boff-explore": "avatar-live-used-today",