@burdenoff/website-sdk 2026.1104.2 → 2026.1104.4

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.
package/README.md CHANGED
@@ -387,9 +387,14 @@ publishes audio, video or data, and it sends the avatar nothing to say.
387
387
  browser's own note of a session today, kept in `localStorage` as
388
388
  `boff.explore.avatarUsedOn`), the box asks ordinary questions, without
389
389
  `surface`, and the answer shows in the tab as text, with "Read the answer and
390
- sources in Chat". Pass `currentPath` to `ExplorePanel` (as
391
- `SiteAssistantDock` does) so a page change calls off a "Start when ready"
392
- at once; without it, the window's path is checked before the create.
390
+ sources in Chat". The text is the answer's words, not its markdown: the form
391
+ the chat's read-aloud says (`answerSpeechText`), so a link is its label, a
392
+ table its rows, and the marks of emphasis, headings and quotes go, and on
393
+ this screen list markers as well (each item keeps its own line); citation
394
+ markers such as `[1]` stay, as in the chat. Pass `currentPath` to
395
+ `ExplorePanel` (as `SiteAssistantDock` does) so a page change calls off a
396
+ "Start when ready" at once; without it, the window's path is checked before
397
+ the create.
393
398
  - **One answer: the first, to a suggested question.** The avatar service
394
399
  accepts a conversation only when it holds exactly two messages: the first user
395
400
  message (a suggested question) and the first assistant answer, completed. The
@@ -421,7 +426,22 @@ publishes audio, video or data, and it sends the avatar nothing to say.
421
426
  back (Safari, iOS), a "Tap to turn on sound" button appears. A refusal (busy,
422
427
  limits, region, language, paused, a question the avatar cannot read) is one
423
428
  short sentence and "Continue in Chat". A network or CORS failure, or a route
424
- that is not there, shows the view as it was before.
429
+ that is not there, shows the view as it was before, until the create request
430
+ has been sent: from then on the server may have made the session, so a create
431
+ that gets no answer (its request failing on the way, its body cut off, or
432
+ nothing within 30 s) says "The live avatar couldn't start." with the way back
433
+ to Chat, never "Live avatar coming soon". The 30 s are a timer raced against
434
+ the request, so the view leaves "Connecting…" by then whatever the network
435
+ does. A create is never sent again by itself (the bot check's token is good
436
+ for one, and a second create could be paid twice), and a 201 that arrives
437
+ after the 30 s is never joined: its session is ended at once with its own
438
+ secret (reason `client_error`, or the visitor's own where they had stopped it
439
+ first). For that, only a request with no answer yet is aborted at 30 s: an
440
+ answer whose status is in by then is read to its end, so a 201 whose body is
441
+ still on its way keeps its secret. A request aborted with no answer at all
442
+ brings nothing back where the browser honours the abort, so a session the
443
+ server made for it is ended by the server's own deadlines (`no_show`). A 401,
444
+ 403, 404 or 405 answer is still the view as it was before.
425
445
  - **Ending.** Stop, closing the panel, leaving the page (`pagehide`), and the
426
446
  live avatar going off under a session (the panel's locale changing to one
427
447
  whose strings have not been reviewed) all end the session with a `keepalive`
@@ -494,6 +514,27 @@ publishes audio, video or data, and it sends the avatar nothing to say.
494
514
  chat started in the Avatar tab, provided it passes the speech checks, whether
495
515
  you chose a suggested question or typed your own", and the visitor is told
496
516
  not to put personal details in a typed question.
517
+ `web-consent-2026-11-12-b` (bot check `turnstile`, 1,010 bytes, SHA-256
518
+ `e8f4495d1e7f179b16de86c1eba0d2528623afb088f904f100e54523ad350c8c`) and
519
+ `web-consent-2026-11-13-b` (bot check `off`, 966 bytes, SHA-256
520
+ `4c88c229da1331ebe0f7e6e32f425612071efb2e4cf01ece813ed827ea8fc174`) are the
521
+ same two with only the timing sentence changed, for the server's Stage B
522
+ sessions of up to three minutes: "It reads for up to three minutes; if an
523
+ answer is longer, it reads the first part, says so, and the full answer stays
524
+ in Chat."
525
+ - **A part-read answer.** Where the server speaks only the first part of an
526
+ answer because the whole of it does not fit the session (the create
527
+ answer's `excerpt: true`, web API contract v1.4, only under the server's
528
+ `AVATAR_WEB_SIZE_BY_AUDIO`), the status line says "The avatar reads the
529
+ first part of this answer. The full answer is in Chat." (key `excerpt`) while
530
+ the avatar speaks, in place of "The avatar is speaking.". The layout is the
531
+ same. Without the field, or with any value but `true`, nothing changes.
532
+ - **Session length.** The countdown and the room's timers follow the server's
533
+ `endsAt` for each session, up to the availability answer's
534
+ `maxSessionSeconds` (three minutes under Stage B); an `endsAt` this
535
+ browser's clock cannot place within that is counted down from
536
+ `maxSessionSeconds` on the browser's own clock. Nothing in the view assumes a
537
+ minute.
497
538
  - **How a session ended.** When the worker leaves, the server closes the room,
498
539
  or the session's time runs out with the room still open, the view says only
499
540
  "The live avatar has ended." (key `finished`). The browser
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var chunkWSEQ2I6I_js = require('../chunk-WSEQ2I6I.js');
4
- var chunkO4GS3XPZ_js = require('../chunk-O4GS3XPZ.js');
4
+ var chunkN7E6Z2GF_js = require('../chunk-N7E6Z2GF.js');
5
5
  var chunkC6OJIHNO_js = require('../chunk-C6OJIHNO.js');
6
6
  require('../chunk-ZSTFX7FV.js');
7
7
  var react = require('react');
@@ -121,7 +121,8 @@ function readCreated(body) {
121
121
  endSecret,
122
122
  endsAt,
123
123
  join: { livekitUrl, participantToken, workerIdentity },
124
- captions: { locale, text }
124
+ captions: { locale, text },
125
+ excerpt: body.excerpt === true
125
126
  }
126
127
  };
127
128
  }
@@ -181,7 +182,7 @@ async function requestAvailability(target) {
181
182
  timer.done();
182
183
  }
183
184
  }
184
- async function requestCreate(target) {
185
+ function requestCreate(target) {
185
186
  const { body } = target;
186
187
  const payload = JSON.stringify({
187
188
  site: body.site,
@@ -191,27 +192,45 @@ async function requestCreate(target) {
191
192
  consentVersion: body.consentVersion,
192
193
  manifestVersion: body.manifestVersion
193
194
  });
194
- const timer = deadline(CREATE_TIMEOUT_MS);
195
- try {
196
- const response = await fetch(`${target.base}/sessions`, {
197
- method: "POST",
198
- credentials: "omit",
199
- cache: "no-store",
200
- headers: {
201
- ...target.headers,
202
- "Content-Type": "application/json",
203
- "Idempotency-Key": target.idempotencyKey,
204
- ...target.botToken === void 0 ? {} : { "x-web-avatar-bot-token": target.botToken }
205
- },
206
- body: payload,
207
- signal: timer.signal
195
+ const controller = new AbortController();
196
+ let answered = false;
197
+ const answer = (async () => {
198
+ try {
199
+ const response = await fetch(`${target.base}/sessions`, {
200
+ method: "POST",
201
+ credentials: "omit",
202
+ cache: "no-store",
203
+ headers: {
204
+ ...target.headers,
205
+ "Content-Type": "application/json",
206
+ "Idempotency-Key": target.idempotencyKey,
207
+ ...target.botToken === void 0 ? {} : { "x-web-avatar-bot-token": target.botToken }
208
+ },
209
+ body: payload,
210
+ signal: controller.signal
211
+ });
212
+ answered = true;
213
+ return readCreate(response.status, await readJson(response));
214
+ } catch {
215
+ return FAILED;
216
+ }
217
+ })();
218
+ return new Promise((resolve) => {
219
+ let late = false;
220
+ const timer = setTimeout(() => {
221
+ late = true;
222
+ if (!answered) controller.abort();
223
+ resolve(FAILED);
224
+ }, CREATE_TIMEOUT_MS);
225
+ void answer.then((result) => {
226
+ if (late) {
227
+ target.onLate?.(result);
228
+ return;
229
+ }
230
+ clearTimeout(timer);
231
+ resolve(result);
208
232
  });
209
- return readCreate(response.status, await readJson(response));
210
- } catch {
211
- return INERT;
212
- } finally {
213
- timer.done();
214
- }
233
+ });
215
234
  }
216
235
  function sendEnd(target) {
217
236
  try {
@@ -294,6 +313,7 @@ var EN = {
294
313
  failed: "The live avatar couldn't start.",
295
314
  interrupted: "The live avatar stopped early.",
296
315
  answerInChat: "The full answer is in Chat.",
316
+ excerpt: "The avatar reads the first part of this answer. The full answer is in Chat.",
297
317
  backToChat: "Back to Chat",
298
318
  busy: "The live avatar is busy right now. Please try again in a few minutes.",
299
319
  limits: "The live avatar has reached its limit for now. Please try again later.",
@@ -351,6 +371,8 @@ var ZH = {
351
371
  failed: "\u5B9E\u65F6\u865A\u62DF\u5F62\u8C61\u65E0\u6CD5\u542F\u52A8\u3002",
352
372
  interrupted: "\u5B9E\u65F6\u865A\u62DF\u5F62\u8C61\u63D0\u524D\u505C\u6B62\u4E86\u3002",
353
373
  answerInChat: "\u5B8C\u6574\u56DE\u7B54\u5728\u804A\u5929\u4E2D\u3002",
374
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the note for a part-read answer (Stage B).
375
+ excerpt: "\u865A\u62DF\u5F62\u8C61\u6717\u8BFB\u8FD9\u4E2A\u56DE\u7B54\u7684\u7B2C\u4E00\u90E8\u5206\u3002\u5B8C\u6574\u56DE\u7B54\u5728\u804A\u5929\u4E2D\u3002",
354
376
  backToChat: "\u8FD4\u56DE\u804A\u5929",
355
377
  busy: "\u5B9E\u65F6\u865A\u62DF\u5F62\u8C61\u76EE\u524D\u7E41\u5FD9\uFF0C\u8BF7\u51E0\u5206\u949F\u540E\u518D\u8BD5\u3002",
356
378
  limits: "\u5B9E\u65F6\u865A\u62DF\u5F62\u8C61\u6682\u65F6\u5DF2\u8FBE\u5230\u4F7F\u7528\u4E0A\u9650\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002",
@@ -410,6 +432,8 @@ var HI = {
410
432
  failed: "\u0932\u093E\u0907\u0935 \u0905\u0935\u0924\u093E\u0930 \u0936\u0941\u0930\u0942 \u0928\u0939\u0940\u0902 \u0939\u094B \u0938\u0915\u093E\u0964",
411
433
  interrupted: "\u0932\u093E\u0907\u0935 \u0905\u0935\u0924\u093E\u0930 \u092C\u0940\u091A \u092E\u0947\u0902 \u0939\u0940 \u0930\u0941\u0915 \u0917\u092F\u093E\u0964",
412
434
  answerInChat: "\u092A\u0942\u0930\u093E \u0909\u0924\u094D\u0924\u0930 \u091A\u0948\u091F \u092E\u0947\u0902 \u0939\u0948\u0964",
435
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the note for a part-read answer (Stage B).
436
+ excerpt: "\u0905\u0935\u0924\u093E\u0930 \u0907\u0938 \u0909\u0924\u094D\u0924\u0930 \u0915\u093E \u092A\u0939\u0932\u093E \u092D\u093E\u0917 \u092A\u0922\u093C\u0924\u093E \u0939\u0948\u0964 \u092A\u0942\u0930\u093E \u0909\u0924\u094D\u0924\u0930 \u091A\u0948\u091F \u092E\u0947\u0902 \u0939\u0948\u0964",
413
437
  backToChat: "\u091A\u0948\u091F \u092A\u0930 \u0935\u093E\u092A\u0938 \u091C\u093E\u090F\u0901",
414
438
  busy: "\u0932\u093E\u0907\u0935 \u0905\u0935\u0924\u093E\u0930 \u0905\u092D\u0940 \u0935\u094D\u092F\u0938\u094D\u0924 \u0939\u0948\u0964 \u0915\u0943\u092A\u092F\u093E \u0915\u0941\u091B \u092E\u093F\u0928\u091F \u092C\u093E\u0926 \u092B\u093F\u0930 \u0915\u094B\u0936\u093F\u0936 \u0915\u0930\u0947\u0902\u0964",
415
439
  limits: "\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 \u0915\u0943\u092A\u092F\u093E \u092C\u093E\u0926 \u092E\u0947\u0902 \u092B\u093F\u0930 \u0915\u094B\u0936\u093F\u0936 \u0915\u0930\u0947\u0902\u0964",
@@ -469,6 +493,8 @@ var ES = {
469
493
  failed: "No se pudo iniciar el avatar en vivo.",
470
494
  interrupted: "El avatar en vivo se detuvo antes de tiempo.",
471
495
  answerInChat: "La respuesta completa est\xE1 en el chat.",
496
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the note for a part-read answer (Stage B).
497
+ excerpt: "El avatar lee la primera parte de esta respuesta. La respuesta completa est\xE1 en el chat.",
472
498
  backToChat: "Volver al chat",
473
499
  busy: "El avatar en vivo est\xE1 ocupado en este momento. Vuelve a intentarlo en unos minutos.",
474
500
  limits: "El avatar en vivo ha alcanzado su l\xEDmite por ahora. Vuelve a intentarlo m\xE1s tarde.",
@@ -528,6 +554,8 @@ var AR = {
528
554
  failed: "\u062A\u0639\u0630\u0651\u0631 \u0628\u062F\u0621 \u0627\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0627\u0644\u0645\u0628\u0627\u0634\u0631.",
529
555
  interrupted: "\u062A\u0648\u0642\u0641 \u0627\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0627\u0644\u0645\u0628\u0627\u0634\u0631 \u0642\u0628\u0644 \u0623\u0646 \u064A\u0646\u062A\u0647\u064A.",
530
556
  answerInChat: "\u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0627\u0644\u0643\u0627\u0645\u0644\u0629 \u0645\u0648\u062C\u0648\u062F\u0629 \u0641\u064A \u0627\u0644\u062F\u0631\u062F\u0634\u0629.",
557
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the note for a part-read answer (Stage B).
558
+ excerpt: "\u064A\u0642\u0631\u0623 \u0627\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0627\u0644\u062C\u0632\u0621 \u0627\u0644\u0623\u0648\u0644 \u0645\u0646 \u0647\u0630\u0647 \u0627\u0644\u0625\u062C\u0627\u0628\u0629. \u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0627\u0644\u0643\u0627\u0645\u0644\u0629 \u0645\u0648\u062C\u0648\u062F\u0629 \u0641\u064A \u0627\u0644\u062F\u0631\u062F\u0634\u0629.",
531
559
  backToChat: "\u0627\u0644\u0639\u0648\u062F\u0629 \u0625\u0644\u0649 \u0627\u0644\u062F\u0631\u062F\u0634\u0629",
532
560
  busy: "\u0627\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0627\u0644\u0645\u0628\u0627\u0634\u0631 \u0645\u0634\u063A\u0648\u0644 \u0627\u0644\u0622\u0646. \u064A\u064F\u0631\u062C\u0649 \u0627\u0644\u0645\u062D\u0627\u0648\u0644\u0629 \u0645\u0631\u0629 \u0623\u062E\u0631\u0649 \u0628\u0639\u062F \u0628\u0636\u0639 \u062F\u0642\u0627\u0626\u0642.",
533
561
  limits: "\u0628\u0644\u063A \u0627\u0644\u0623\u0641\u0627\u062A\u0627\u0631 \u0627\u0644\u0645\u0628\u0627\u0634\u0631 \u062D\u062F\u0651\u0647 \u0641\u064A \u0627\u0644\u0648\u0642\u062A \u0627\u0644\u062D\u0627\u0644\u064A. \u064A\u064F\u0631\u062C\u0649 \u0627\u0644\u0645\u062D\u0627\u0648\u0644\u0629 \u0644\u0627\u062D\u0642\u064B\u0627.",
@@ -587,6 +615,8 @@ var TA = {
587
615
  failed: "\u0BA8\u0BC7\u0BB0\u0BB2\u0BC8 \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BC8\u0BA4\u0BCD \u0BA4\u0BCA\u0B9F\u0B99\u0BCD\u0B95 \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8.",
588
616
  interrupted: "\u0BA8\u0BC7\u0BB0\u0BB2\u0BC8 \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BCD \u0B87\u0B9F\u0BC8\u0BAF\u0BBF\u0BB2\u0BC7\u0BAF\u0BC7 \u0BA8\u0BBF\u0BA9\u0BCD\u0BB1\u0BC1\u0BB5\u0BBF\u0B9F\u0BCD\u0B9F\u0BA4\u0BC1.",
589
617
  answerInChat: "\u0BAE\u0BC1\u0BB4\u0BC1\u0BAA\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BCD \u0B85\u0BB0\u0B9F\u0BCD\u0B9F\u0BC8\u0BAF\u0BBF\u0BB2\u0BCD \u0B89\u0BB3\u0BCD\u0BB3\u0BA4\u0BC1.",
618
+ // TRANSLATION REVIEW PENDING (BOFF-9797): draft, the note for a part-read answer (Stage B).
619
+ excerpt: "\u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BCD \u0B87\u0BA8\u0BCD\u0BA4\u0BAA\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BBF\u0BA9\u0BCD \u0BAE\u0BC1\u0BA4\u0BB2\u0BCD \u0BAA\u0B95\u0BC1\u0BA4\u0BBF\u0BAF\u0BC8\u0BAA\u0BCD \u0BAA\u0B9F\u0BBF\u0B95\u0BCD\u0B95\u0BBF\u0BB1\u0BA4\u0BC1. \u0BAE\u0BC1\u0BB4\u0BC1\u0BAA\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BCD \u0B85\u0BB0\u0B9F\u0BCD\u0B9F\u0BC8\u0BAF\u0BBF\u0BB2\u0BCD \u0B89\u0BB3\u0BCD\u0BB3\u0BA4\u0BC1.",
590
620
  backToChat: "\u0B85\u0BB0\u0B9F\u0BCD\u0B9F\u0BC8\u0B95\u0BCD\u0B95\u0BC1\u0BA4\u0BCD \u0BA4\u0BBF\u0BB0\u0BC1\u0BAE\u0BCD\u0BAA\u0BC1",
591
621
  busy: "\u0BA8\u0BC7\u0BB0\u0BB2\u0BC8 \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BCD \u0B87\u0BAA\u0BCD\u0BAA\u0BCB\u0BA4\u0BC1 \u0BB5\u0BC7\u0BB1\u0BC1 \u0BAA\u0BAF\u0BA9\u0BCD\u0BAA\u0BBE\u0B9F\u0BCD\u0B9F\u0BBF\u0BB2\u0BCD \u0B89\u0BB3\u0BCD\u0BB3\u0BA4\u0BC1. \u0B9A\u0BBF\u0BB2 \u0BA8\u0BBF\u0BAE\u0BBF\u0B9F\u0B99\u0BCD\u0B95\u0BB3\u0BBF\u0BB2\u0BCD \u0BAE\u0BC0\u0BA3\u0BCD\u0B9F\u0BC1\u0BAE\u0BCD \u0BAE\u0BC1\u0BAF\u0BB2\u0BB5\u0BC1\u0BAE\u0BCD.",
592
622
  limits: "\u0BA8\u0BC7\u0BB0\u0BB2\u0BC8 \u0B85\u0BB5\u0BA4\u0BBE\u0BB0\u0BCD \u0BA4\u0BB1\u0BCD\u0BAA\u0BCB\u0BA4\u0BC8\u0B95\u0BCD\u0B95\u0BC1\u0BA4\u0BCD \u0BA4\u0BA9\u0BCD \u0BB5\u0BB0\u0BAE\u0BCD\u0BAA\u0BC8 \u0B8E\u0B9F\u0BCD\u0B9F\u0BBF\u0BB5\u0BBF\u0B9F\u0BCD\u0B9F\u0BA4\u0BC1. \u0BAA\u0BBF\u0BB1\u0B95\u0BC1 \u0BAE\u0BC0\u0BA3\u0BCD\u0B9F\u0BC1\u0BAE\u0BCD \u0BAE\u0BC1\u0BAF\u0BB2\u0BB5\u0BC1\u0BAE\u0BCD.",
@@ -734,6 +764,42 @@ var CONSENT = /* @__PURE__ */ new Map([
734
764
  questions: "any"
735
765
  }
736
766
  }
767
+ ],
768
+ [
769
+ // Stage B (operator decision 2026-11-04): web-consent-2026-11-05 with only its timing sentence changed. A session
770
+ // lasts up to three minutes, sized by the server to the answer's audio; an answer longer than that is read in part,
771
+ // which the avatar says and the Avatar tab notes (`excerpt`), and the full answer stays in Chat. A cap still
772
+ // applies, so the words never say every answer is read whole (gitops docs/avatar-web/en-web-consent-2026-11-12-b.txt).
773
+ "web-consent-2026-11-12-b",
774
+ {
775
+ en: {
776
+ title: "Live AI avatar (beta)",
777
+ introProduct: null,
778
+ intro: "This is an AI avatar, not a real person. It reads aloud the first answer in a new chat started in the Avatar tab, provided it passes the speech checks, whether you chose a suggested question or typed your own. It reads for up to three minutes; if an answer is longer, it reads the first part, says so, and the full answer stays in Chat. The answer is AI-generated and can be wrong. Don't put personal details in a question you type.",
779
+ 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.",
780
+ 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.",
781
+ privacyLink: "How we handle this",
782
+ botCheck: "turnstile",
783
+ questions: "any"
784
+ }
785
+ }
786
+ ],
787
+ [
788
+ // The words of web-consent-2026-11-12-b without "When you start, Cloudflare checks for bots.", for a server that runs
789
+ // no bot check (gitops docs/avatar-web/en-web-consent-2026-11-13-b.txt).
790
+ "web-consent-2026-11-13-b",
791
+ {
792
+ en: {
793
+ title: "Live AI avatar (beta)",
794
+ introProduct: null,
795
+ intro: "This is an AI avatar, not a real person. It reads aloud the first answer in a new chat started in the Avatar tab, provided it passes the speech checks, whether you chose a suggested question or typed your own. It reads for up to three minutes; if an answer is longer, it reads the first part, says so, and the full answer stays in Chat. The answer is AI-generated and can be wrong. Don't put personal details in a question you type.",
796
+ 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.",
797
+ 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.",
798
+ privacyLink: "How we handle this",
799
+ botCheck: "off",
800
+ questions: "any"
801
+ }
802
+ }
737
803
  ]
738
804
  ]);
739
805
  function avatarConsentCopy(version, locale) {
@@ -1303,27 +1369,31 @@ function AvatarLiveView({
1303
1369
  setNow(received);
1304
1370
  go({
1305
1371
  name: "pending",
1306
- session: { endsAt, captions: session.captions }
1372
+ session: {
1373
+ endsAt,
1374
+ captions: session.captions,
1375
+ excerpt: session.excerpt
1376
+ }
1307
1377
  });
1308
1378
  },
1309
1379
  [go]
1310
1380
  );
1311
1381
  const settle = react.useCallback(
1312
- (result, run, context) => {
1382
+ (result, givenUp, context) => {
1313
1383
  const created = result.kind === "created" ? {
1314
1384
  sessionId: result.session.sessionId,
1315
1385
  secret: result.session.endSecret
1316
1386
  } : result.kind === "unusable" ? { sessionId: result.sessionId, secret: result.endSecret } : null;
1317
1387
  const claimed = created !== null || result.kind === "refused" && result.reason === "answer_claimed";
1318
1388
  if (claimed) claimedRef.current.add(context.messageId);
1319
- if (run.cancelled !== null) {
1389
+ if (givenUp !== null) {
1320
1390
  if (created !== null) {
1321
1391
  sendEnd({
1322
1392
  base: context.base,
1323
1393
  headers: context.headers,
1324
1394
  sessionId: created.sessionId,
1325
1395
  secret: created.secret,
1326
- reason: run.cancelled
1396
+ reason: givenUp
1327
1397
  });
1328
1398
  }
1329
1399
  const shown2 = phaseRef.current;
@@ -1432,21 +1502,21 @@ function AvatarLiveView({
1432
1502
  return;
1433
1503
  }
1434
1504
  if (turnstile !== null) go({ name: "creating" });
1505
+ const context = { module: loaded, offer, messageId, base: base2, headers: headers2 };
1435
1506
  const result = await requestCreate({
1436
1507
  base: base2,
1437
1508
  headers: headers2,
1438
1509
  idempotencyKey,
1439
1510
  botToken,
1440
- body: body2
1511
+ body: body2,
1512
+ // An answer past the deadline, after the view has said the live avatar couldn't start: a session it made is
1513
+ // ended at once with its own secret, and never joined. The reason is the visitor's own where they gave the
1514
+ // start up first, else `client_error`. It reads `run` and never writes it, so the deadline's `failed` below is
1515
+ // shown whichever of the two is handled first.
1516
+ onLate: (late) => settle(late, run.cancelled ?? "client_error", context)
1441
1517
  });
1442
1518
  if (startRef.current === run) startRef.current = null;
1443
- settle(result, run, {
1444
- module: loaded,
1445
- offer,
1446
- messageId,
1447
- base: base2,
1448
- headers: headers2
1449
- });
1519
+ settle(result, run.cancelled, context);
1450
1520
  })();
1451
1521
  },
1452
1522
  [exchangeNow, go, reoffer, settle]
@@ -2045,7 +2115,7 @@ function AvatarLiveView({
2045
2115
  className: "w-full max-w-sm rounded-lg border border-border p-4 text-start",
2046
2116
  children: [
2047
2117
  /* @__PURE__ */ jsxRuntime.jsx("h2", { id: consentTitleId, className: "text-base font-semibold", children: copy.title }),
2048
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm", children: productName === null || copy.introProduct === null ? copy.intro : chunkO4GS3XPZ_js.fill(copy.introProduct, { product: productName }) }),
2118
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm", children: productName === null || copy.introProduct === null ? copy.intro : chunkN7E6Z2GF_js.fill(copy.introProduct, { product: productName }) }),
2049
2119
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: copy.processing }),
2050
2120
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: copy.storage }),
2051
2121
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -2125,6 +2195,7 @@ function AvatarLiveView({
2125
2195
  ]
2126
2196
  }
2127
2197
  );
2198
+ const speakingLine = (session) => session.excerpt ? S.excerpt : S.speaking;
2128
2199
  const liveBody = (session) => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2129
2200
  /* @__PURE__ */ jsxRuntime.jsx(
2130
2201
  "section",
@@ -2224,14 +2295,21 @@ function AvatarLiveView({
2224
2295
  children: lastQuestion.content
2225
2296
  }
2226
2297
  ),
2227
- lastAnswer === void 0 ? unanswered === "waiting" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: activity ?? S.gettingAnswer }) : unanswered === "failed" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: S.answerFailed }) : null : lastAnswer.status === "PENDING" || lastAnswer.status === "RUNNING" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: activity ?? S.gettingAnswer }) : lastAnswer.status === "FAILED" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: S.answerFailed }) : /* @__PURE__ */ jsxRuntime.jsx(
2228
- "p",
2229
- {
2230
- "data-boff-explore": "avatar-live-text-answer",
2231
- dir: "auto",
2232
- className: "line-clamp-6 w-full max-w-sm whitespace-pre-wrap break-words text-start text-sm",
2233
- children: lastAnswer.content
2234
- }
2298
+ lastAnswer === void 0 ? unanswered === "waiting" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: activity ?? S.gettingAnswer }) : unanswered === "failed" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: S.answerFailed }) : null : lastAnswer.status === "PENDING" || lastAnswer.status === "RUNNING" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: activity ?? S.gettingAnswer }) : lastAnswer.status === "FAILED" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: S.answerFailed }) : (
2299
+ // The answer's words, not its markdown: `content` is written for the chat's renderer, and shown as it is
2300
+ // here a link reads `[label](https://…)`. In its spoken form, as the chat's read-aloud says it: a link is its
2301
+ // label (the address is in Chat, one tap away), a table its rows, and the markers of emphasis, headings,
2302
+ // quotes and list items go (`answerDisplayText`). The words are the ones the chat shows, citation markers
2303
+ // included, which the sources in Chat answer to.
2304
+ /* @__PURE__ */ jsxRuntime.jsx(
2305
+ "p",
2306
+ {
2307
+ "data-boff-explore": "avatar-live-text-answer",
2308
+ dir: "auto",
2309
+ className: "line-clamp-6 w-full max-w-sm whitespace-pre-wrap break-words text-start text-sm",
2310
+ children: chunkN7E6Z2GF_js.answerDisplayText(lastAnswer.content)
2311
+ }
2312
+ )
2235
2313
  )
2236
2314
  ] });
2237
2315
  const ended = phase.name === "ended";
@@ -2338,7 +2416,7 @@ function AvatarLiveView({
2338
2416
  break;
2339
2417
  case "pending":
2340
2418
  case "live":
2341
- status = phase.name === "live" ? S.speaking : S.connecting;
2419
+ status = phase.name === "live" ? speakingLine(phase.session) : S.connecting;
2342
2420
  top2 = badge;
2343
2421
  media2 = liveMedia(phase.name === "live");
2344
2422
  body2 = liveBody(phase.session);
@@ -2464,7 +2542,7 @@ function AvatarLiveView({
2464
2542
  "bot-check": S.botCheck,
2465
2543
  creating: S.connecting,
2466
2544
  pending: S.connecting,
2467
- live: S.speaking,
2545
+ live: phase.name === "live" ? speakingLine(phase.session) : "",
2468
2546
  ended: phase.name === "ended" ? S[phase.how] : ""
2469
2547
  };
2470
2548
  const outcome = phase.name === "needs-answer" || phase.name === "refused" || phase.name === "ended";