@burdenoff/fe-libs 2026.1014.4 → 2026.1015.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/shared/assistant/avatar/avatarPreferences.d.ts +61 -14
  2. package/dist/shared/assistant/avatar/avatarPreferences.js +54 -21
  3. package/dist/shared/assistant/avatar/live/LiveAvatarTabView.d.ts +33 -0
  4. package/dist/shared/assistant/avatar/live/LiveAvatarTabView.js +278 -0
  5. package/dist/shared/assistant/avatar/live/avatarSessionClient.d.ts +5 -0
  6. package/dist/shared/assistant/avatar/live/avatarSessionClient.js +25 -6
  7. package/dist/shared/assistant/avatar/live/avatarVoice.d.ts +43 -0
  8. package/dist/shared/assistant/avatar/live/avatarVoice.js +45 -0
  9. package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.d.ts +21 -1
  10. package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.js +74 -24
  11. package/dist/shared/assistant/avatar/live/liveAvatarController.d.ts +47 -8
  12. package/dist/shared/assistant/avatar/live/liveAvatarController.js +244 -161
  13. package/dist/shared/assistant/avatar/live/liveAvatarStatus.d.ts +2 -2
  14. package/dist/shared/assistant/avatar/live/liveAvatarStatus.js +11 -4
  15. package/dist/shared/assistant/avatar/useAvatarCopy.d.ts +52 -21
  16. package/dist/shared/assistant/avatar/useAvatarCopy.js +93 -48
  17. package/dist/shared/assistant/i18nManifest.d.ts +24 -2
  18. package/dist/shared/assistant/i18nManifest.js +24 -2
  19. package/dist/shared/assistant/index.d.ts +5 -0
  20. package/dist/shared/assistant/speech/hostedVoiceCatalogue.d.ts +19 -0
  21. package/dist/shared/assistant/speech/hostedVoiceCatalogue.js +25 -18
  22. package/dist/shared/assistant/ui/AIAssistantPanel.js +136 -103
  23. package/dist/shared/assistant/ui/AssistantAvatarTabs.d.ts +10 -0
  24. package/dist/shared/assistant/ui/AssistantAvatarTabs.js +48 -0
  25. package/dist/shared/assistant/ui/AssistantChatArea.js +1260 -1199
  26. package/dist/shared/assistant/ui/assistantLiveAvatarSeam.d.ts +165 -5
  27. package/dist/shared/assistant/ui/assistantLiveAvatarSeam.js +141 -59
  28. package/dist/shared/assistant/ui/avatar/AssistantAvatarTab.d.ts +13 -0
  29. package/dist/shared/assistant/ui/avatar/AssistantAvatarTab.js +70 -0
  30. package/dist/shared/assistant/ui/composer/AssistantComposer.d.ts +18 -0
  31. package/dist/shared/assistant/ui/composer/AssistantComposer.js +121 -102
  32. package/dist/shared/assistant/ui/composer/AssistantHandsFreeInterruptToggle.d.ts +9 -0
  33. package/dist/shared/assistant/ui/composer/AssistantHandsFreeInterruptToggle.js +28 -14
  34. package/dist/shared/assistant/ui/panelOverlays.d.ts +24 -3
  35. package/dist/shared/assistant/ui/panelOverlays.js +11 -10
  36. package/dist/shared/assistant/ui/useVoiceSession.js +234 -233
  37. package/dist/shared/assistant/ui/voice/AssistantVoiceSession.d.ts +6 -0
  38. package/dist/shared/assistant/ui/voice/AssistantVoiceSession.js +161 -149
  39. package/dist/shared/assistant/ui/voice/VoiceMiniController.js +7 -7
  40. package/dist/shared/assistant/ui/voice/VoiceStage.js +7 -7
  41. package/dist/shared/assistant/ui/voice/voiceStartGate.d.ts +57 -0
  42. package/dist/shared/assistant/ui/voice/voiceStartGate.js +9 -0
  43. package/dist/shared/assistant/voice/handsfree/handsFreeSetting.d.ts +7 -1
  44. package/dist/shared/assistant/voice/handsfree/handsFreeSetting.js +2 -2
  45. package/dist/shared-assistant.js +259 -254
  46. package/package.json +1 -1
@@ -0,0 +1,45 @@
1
+ import { hostedLocaleFor as e, hostedVoiceName as t, isHostedSpeaker as n } from "../../speech/hostedVoiceCatalogue.js";
2
+ import { resolveAvatarVoiceMode as r, resolveStoredAvatarId as i } from "../avatarPreferences.js";
3
+ //#region src/shared/assistant/avatar/live/avatarVoice.ts
4
+ function a(e) {
5
+ let t = /* @__PURE__ */ new Map();
6
+ if (typeof e != "object" || !e) return t;
7
+ for (let [r, a] of Object.entries(e)) i(r) === r && n(a) && t.set(r, a);
8
+ return t;
9
+ }
10
+ function o(e, t) {
11
+ switch (r(e, t.hostDefault)) {
12
+ case "mine": return null;
13
+ case "speaker": return e.avatarSpeaker;
14
+ case "paired": return t.avatarId === null ? null : t.avatarVoices.get(t.avatarId) ?? null;
15
+ }
16
+ }
17
+ function s(n, r, i) {
18
+ let a = e(r);
19
+ if (a === null) return null;
20
+ let o = t(a, n);
21
+ return i.find((e) => e.name === o && e.source === "hosted") ?? null;
22
+ }
23
+ function c(e, t, n) {
24
+ let r = !1, i = null, a = (e) => {
25
+ r || (r = !0, i?.(), i = null, e && t());
26
+ };
27
+ i = n?.(() => a(!0)) ?? null;
28
+ let o = () => a(!0);
29
+ return {
30
+ write: (t, n, r) => {
31
+ let i = e.write(t, n, r);
32
+ return i.then(void 0, o), i;
33
+ },
34
+ drain: () => {
35
+ let t = e.drain();
36
+ return t.then(() => a(!1), o), t;
37
+ },
38
+ retract: (t) => e.retract(t),
39
+ cancel: () => {
40
+ a(!1), e.cancel();
41
+ }
42
+ };
43
+ }
44
+ //#endregion
45
+ export { o as avatarSpeakerFor, s as hostedAvatarVoice, a as liveAvatarVoicesFrom, c as restoreVoiceWhenOutputLeaves };
@@ -1,17 +1,36 @@
1
1
  import { ComponentType } from 'react';
2
+ import { HostedSpeaker } from '../../speech/hostedVoiceCatalogue';
2
3
  import { SpeechEngine } from '../../speech/types';
3
- import { AssistantLiveAvatarSeam, LiveAvatarEnabledStore, LiveAvatarStageProps } from '../../ui/assistantLiveAvatarSeam';
4
+ import { AssistantLiveAvatarSeam, LiveAvatarEnabledStore, LiveAvatarStageProps, LiveAvatarTabSeam } from '../../ui/assistantLiveAvatarSeam';
5
+ import { AvatarVoiceDefault } from '../avatarPreferences';
4
6
  import { AvatarRoom, AvatarScheduler } from './avatarPorts';
5
7
  import { AvatarSessionClient } from './avatarSessionClient';
8
+ import { LiveAvatarVoiceStore } from './LiveAvatarTabView';
6
9
  import { LiveAvatarController, LiveAvatarControllerDeps, LiveAvatarLogEvent, LiveAvatarPreferences } from './liveAvatarController';
7
10
  export interface LiveAvatarSeamOptions {
8
11
  /** Display names for the avatar ids the server offers (it sends ids only); without one the chooser numbers them. */
9
12
  readonly avatarNames?: Readonly<Record<string, string>>;
13
+ /**
14
+ * The speaker each avatar id speaks with as "Default for this avatar" (BOFF-9341): host config, never inferred. An id without one has
15
+ * no Default, and a value that is not a hosted speaker is ignored.
16
+ */
17
+ readonly avatarVoices?: Readonly<Record<string, HostedSpeaker>>;
18
+ /**
19
+ * The avatar's voice for a person who has not chosen one: `mine` (the default: their current voice is kept) or `paired` (each avatar
20
+ * speaks with its `avatarVoices` speaker, and follows the avatar when it is switched; one with none keeps the assistant's voice). A
21
+ * person's stored choice always wins.
22
+ */
23
+ readonly defaultAvatarVoiceMode?: AvatarVoiceDefault;
10
24
  readonly onLog?: (event: LiveAvatarLogEvent) => void;
11
25
  /** Overrides, for tests and for hosts with their own plumbing. Each defaults to the assistant's own. */
12
26
  readonly client?: () => AvatarSessionClient | null;
13
27
  readonly openRoom?: () => Promise<AvatarRoom>;
14
28
  readonly preferences?: LiveAvatarPreferences;
29
+ /**
30
+ * The person's avatar voice choice: read per reply, and written by the Avatar tab's voice select (never the assistant's own voice).
31
+ * Defaults to the assistant's avatar preferences.
32
+ */
33
+ readonly avatarVoiceStore?: LiveAvatarVoiceStore;
15
34
  readonly scheduler?: AvatarScheduler;
16
35
  readonly browserSupported?: () => boolean;
17
36
  readonly controller?: Partial<Pick<LiveAvatarControllerDeps, 'createAudioElement' | 'releaseAudioElement' | 'startMeter' | 'decode' | 'now' | 'newIdempotencyKey' | 'subscribeWorkspaceChange' | 'subscribePageHide' | 'idleMs' | 'endBeforeDeadlineMs' | 'connectTimeoutMs' | 'trackerConfig' | 'maxDrainMs' | 'stallMs' | 'readinessProbe'>>;
@@ -21,6 +40,7 @@ export interface LiveAvatarSeamHandle extends AssistantLiveAvatarSeam {
21
40
  readonly decorateSpeechEngine: (base: SpeechEngine) => SpeechEngine;
22
41
  readonly Stage: ComponentType<LiveAvatarStageProps>;
23
42
  readonly enabled: LiveAvatarEnabledStore;
43
+ readonly tab: LiveAvatarTabSeam;
24
44
  readonly controller: LiveAvatarController;
25
45
  /** Ends any session and stops listening. The handle is unusable afterwards. */
26
46
  dispose(): void;
@@ -8,9 +8,11 @@ import { avatarApiBaseFromTtsEndpoint as l, avatarIdempotencyKey as u, createAva
8
8
  import { createLiveAvatarController as f } from "./liveAvatarController.js";
9
9
  import { openLiveKitAvatarRoom as p } from "./liveKitAvatarRoom.js";
10
10
  import { LiveAvatarStageView as m } from "./LiveAvatarStage.js";
11
- import { jsx as h } from "react/jsx-runtime";
11
+ import { avatarSpeakerFor as h, hostedAvatarVoice as g, liveAvatarVoicesFrom as _, restoreVoiceWhenOutputLeaves as v } from "./avatarVoice.js";
12
+ import { LiveAvatarTabView as y } from "./LiveAvatarTabView.js";
13
+ import { jsx as b } from "react/jsx-runtime";
12
14
  //#region src/shared/assistant/avatar/live/createLiveAvatarSeam.tsx
13
- function g(e = globalThis) {
15
+ function x(e = globalThis) {
14
16
  let t = e;
15
17
  if (typeof t.RTCPeerConnection != "function" || typeof t.OfflineAudioContext != "function" || typeof t.AudioContext != "function" || typeof t.MediaStream != "function") return !1;
16
18
  let n = t.navigator, r = n?.userAgentData;
@@ -21,14 +23,14 @@ function g(e = globalThis) {
21
23
  let i = n?.userAgent ?? "";
22
24
  return /\b(?:Chrome|Chromium)\//.test(i) && !/\b(?:Edg|OPR|SamsungBrowser)\/|Mobile|Android|iPhone|iPad/.test(i);
23
25
  }
24
- function _() {
26
+ function S() {
25
27
  let e = i().getHeaders?.() ?? {};
26
28
  for (let [t, n] of Object.entries(e)) if (t.toLowerCase() === "authorization" && typeof n == "string" && n.trim() !== "") return n.trim();
27
29
  let n = t();
28
30
  return n ? `Bearer ${n}` : null;
29
31
  }
30
- async function v(t) {
31
- let i = n(), a = _(), o = a?.replace(/^Bearer\s+/i, "") ?? null;
32
+ async function C(t) {
33
+ let i = n(), a = S(), o = a?.replace(/^Bearer\s+/i, "") ?? null;
32
34
  if (!i || !o) return null;
33
35
  let s = t.refused?.replace(/^Bearer\s+/i, ""), c = await e({
34
36
  url: r("workspace"),
@@ -37,13 +39,13 @@ async function v(t) {
37
39
  currentQuery: "liveAvatarSession",
38
40
  ...t.fresh ? { issuanceGuard: {
39
41
  identityKey: "liveAvatarSession",
40
- isCurrent: () => n() === i && _() === a,
42
+ isCurrent: () => n() === i && S() === a,
41
43
  accepts: (e, t) => e !== s && t === i
42
44
  } } : {}
43
45
  });
44
46
  return c ? `Bearer ${c}` : null;
45
47
  }
46
- function y() {
48
+ function w() {
47
49
  let e = null;
48
50
  return () => {
49
51
  let t = typeof location > "u" ? void 0 : location.href, n = l(i().endpoint, t);
@@ -51,39 +53,44 @@ function y() {
51
53
  base: n,
52
54
  client: d({
53
55
  baseUrl: n,
54
- getAuthorization: _,
55
- getWorkspaceAuthorization: v
56
+ getAuthorization: S,
57
+ getWorkspaceAuthorization: C
56
58
  })
57
59
  }), e.client);
58
60
  };
59
61
  }
60
- var b = {
62
+ var T = {
61
63
  getState: () => a.getState(),
62
64
  setAvatarEnabled: (e) => a.getState().setAvatarEnabled(e),
63
65
  setAvatarId: (e) => a.getState().setAvatarId(e),
64
66
  subscribe: (e) => a.subscribe(() => e())
67
+ }, E = {
68
+ getState: () => a.getState(),
69
+ subscribe: (e) => a.subscribe(() => e()),
70
+ setVoiceMode: (e) => a.getState().setVoiceMode(e),
71
+ setAvatarSpeaker: (e) => a.getState().setAvatarSpeaker(e)
65
72
  };
66
- function x() {
73
+ function D() {
67
74
  if (typeof document > "u") return null;
68
75
  let e = document.createElement("audio");
69
76
  return e.autoplay = !0, e.hidden = !0, e.setAttribute("data-testid", "assistant-live-avatar-audio"), document.body.appendChild(e), e;
70
77
  }
71
- function S(e) {
78
+ function O(e) {
72
79
  if (!(typeof window > "u")) return (t) => (window.addEventListener(e, t), () => window.removeEventListener(e, t));
73
80
  }
74
- function C(e = {}) {
75
- let t = e.preferences ?? b, n = e.controller ?? {}, r = n.subscribeWorkspaceChange ?? S("burdenoff:workspace-changed"), i = n.subscribePageHide ?? S("pagehide"), a = f({
76
- client: e.client ?? y(),
81
+ function k(e = {}) {
82
+ let t = e.preferences ?? T, n = e.controller ?? {}, r = n.subscribeWorkspaceChange ?? O("burdenoff:workspace-changed"), i = n.subscribePageHide ?? O("pagehide"), a = f({
83
+ client: e.client ?? w(),
77
84
  openRoom: e.openRoom ?? p,
78
85
  scheduler: e.scheduler ?? o,
79
86
  preferences: t,
80
87
  decode: n.decode ?? ((e, t) => c(e, { signal: t })),
81
- createAudioElement: n.createAudioElement ?? x,
88
+ createAudioElement: n.createAudioElement ?? D,
82
89
  releaseAudioElement: n.releaseAudioElement ?? ((e) => {
83
90
  e.srcObject = null, e.remove();
84
91
  }),
85
92
  startMeter: n.startMeter ?? ((e, t) => typeof MediaStream > "u" ? null : s(new MediaStream([e]), t)),
86
- browserSupported: e.browserSupported ?? (() => g()),
93
+ browserSupported: e.browserSupported ?? (() => x()),
87
94
  now: n.now ?? (() => Date.now()),
88
95
  newIdempotencyKey: n.newIdempotencyKey ?? (() => u()),
89
96
  ...r ? { subscribeWorkspaceChange: r } : {},
@@ -96,20 +103,47 @@ function C(e = {}) {
96
103
  ...n.stallMs === void 0 ? {} : { stallMs: n.stallMs },
97
104
  ...n.readinessProbe === void 0 ? {} : { readinessProbe: n.readinessProbe },
98
105
  ...e.onLog ? { onLog: e.onLog } : {}
99
- });
106
+ }), l = _(e.avatarVoices), d = e.avatarVoiceStore ?? E, S = () => d.getState(), C = (n, r) => {
107
+ let i = h(S(), {
108
+ avatarVoices: l,
109
+ avatarId: t.getState().avatarId,
110
+ ...e.defaultAvatarVoiceMode ? { hostDefault: e.defaultAvatarVoiceMode } : {}
111
+ });
112
+ return i === null ? null : g(i, n, r.listVoices());
113
+ }, k = (e, t) => {
114
+ let n = {
115
+ ...e,
116
+ voice: t
117
+ };
118
+ return n.audioOutput = v(a.createOutput(), () => {
119
+ n.voice = e.voice;
120
+ }, (e) => a.subscribe(() => {
121
+ a.getSnapshot().phase !== "ready" && e();
122
+ })), n;
123
+ };
100
124
  return {
101
125
  decorateSpeechEngine: (e) => ({
102
126
  id: e.id,
103
127
  isSupported: () => e.isSupported(),
104
128
  listVoices: () => e.listVoices(),
105
129
  subscribeVoices: (t) => e.subscribeVoices(t),
106
- speak: (t, n, r) => e.speak(t, a.route(n, e) === "avatar" ? {
107
- ...n,
108
- audioOutput: a.createOutput()
109
- } : n, r),
130
+ speak: (t, n, r) => {
131
+ let i = C(n.lang, e), o = i === null ? n : {
132
+ ...n,
133
+ voice: i
134
+ };
135
+ if (a.route(o, e) !== "avatar") {
136
+ e.speak(t, n, r);
137
+ return;
138
+ }
139
+ e.speak(t, i === null ? {
140
+ ...n,
141
+ audioOutput: a.createOutput()
142
+ } : k(n, i), r);
143
+ },
110
144
  cancel: () => e.cancel()
111
145
  }),
112
- Stage: ({ expanded: t, fallback: n }) => /* @__PURE__ */ h(m, {
146
+ Stage: ({ expanded: t, fallback: n }) => /* @__PURE__ */ b(m, {
113
147
  controller: a,
114
148
  expanded: t,
115
149
  fallback: n,
@@ -121,9 +155,25 @@ function C(e = {}) {
121
155
  switchOff: () => a.switchOff(),
122
156
  voiceLive: (e) => a.setVoiceLive(e)
123
157
  },
158
+ tab: {
159
+ offered: {
160
+ subscribe: (e) => a.subscribe(e),
161
+ getSnapshot: () => a.getSnapshot().tabOffered
162
+ },
163
+ setOpen: (e) => a.setTabOpen(e),
164
+ heldByVoice: () => a.getSnapshot().heldByVoice,
165
+ View: (t) => /* @__PURE__ */ b(y, {
166
+ ...t,
167
+ controller: a,
168
+ avatarVoices: l,
169
+ voiceStore: d,
170
+ ...e.avatarNames ? { avatarNames: e.avatarNames } : {},
171
+ ...e.defaultAvatarVoiceMode ? { defaultAvatarVoiceMode: e.defaultAvatarVoiceMode } : {}
172
+ })
173
+ },
124
174
  controller: a,
125
175
  dispose: () => a.dispose()
126
176
  };
127
177
  }
128
178
  //#endregion
129
- export { C as createLiveAvatarSeam, g as isLiveAvatarBrowserSupported, _ as liveAvatarAuthorization, v as liveAvatarWorkspaceAuthorization };
179
+ export { k as createLiveAvatarSeam, x as isLiveAvatarBrowserSupported, S as liveAvatarAuthorization, C as liveAvatarWorkspaceAuthorization };
@@ -30,11 +30,27 @@ export type LiveAvatarPhase =
30
30
  /** Ending the session. */
31
31
  | 'ending';
32
32
  export type LiveAvatarNotice = 'interrupted' | 'voice-only' | 'time-limit';
33
+ /** A Turn off this soon after the tab's start click is the second click of a double click, not a choice (BOFF-9341, critic #2). */
34
+ export declare const LIVE_AVATAR_TAB_OFF_GUARD_MS = 1000;
33
35
  export interface LiveAvatarSnapshot {
34
- /** A voice conversation is live: the only time the avatar is offered (P2). */
36
+ /** A voice conversation is live: outside the Avatar tab, the only time the avatar is offered (P2). */
35
37
  readonly voiceLive: boolean;
36
- /** The toggle may be shown: the browser can show the avatar, it was not refused on this page, and a voice conversation is live. */
38
+ /**
39
+ * The avatar may be asked for now: the browser can show it, it was not refused on this page, and a voice conversation is live or the
40
+ * Avatar tab is open.
41
+ */
37
42
  readonly offered: boolean;
43
+ /** The Avatar tab is open (BOFF-9341): while it is, the tab owns the avatar. */
44
+ readonly tabOpen: boolean;
45
+ /** The Avatar tab may be offered: the browser can show the avatar, and it was not refused on this page. */
46
+ readonly tabOffered: boolean;
47
+ /**
48
+ * After a "not now" refusal that said when (the server's Retry-After): the wall-clock ms a start may be tried again. Null otherwise.
49
+ * It goes with `problem`: cleared together, and both clear by themselves when the time comes.
50
+ */
51
+ readonly retryAt: number | null;
52
+ /** The avatar was turned on outside the tab during the voice conversation that is still live: closing the tab hands it back to Voice. */
53
+ readonly heldByVoice: boolean;
38
54
  readonly phase: LiveAvatarPhase;
39
55
  /** The avatars the server offers, from the last availability answer. */
40
56
  readonly avatars: readonly string[];
@@ -104,26 +120,49 @@ export interface LiveAvatarControllerDeps {
104
120
  readonly readinessProbe?: AvatarReadinessProbeOptions | false;
105
121
  readonly onLog?: (event: LiveAvatarLogEvent) => void;
106
122
  }
123
+ /** How a `requestOn` ends once the server offers the avatar. */
124
+ export interface LiveAvatarRequestOptions {
125
+ /**
126
+ * Omitted: today's rule (the voice stage's toggle): the chooser, or a start at once when this page already confirmed this disclosure
127
+ * and there is nothing to choose. `false`: the chooser only, never a start (the tab opening, "Try again", a workspace switch).
128
+ * `true`: the person's Turn on, with the disclosure on screen: start with `avatarId` (else the one selected) once the answer comes,
129
+ * only if it is still offered and the disclosure version is the one shown at the click; otherwise the chooser.
130
+ */
131
+ readonly start?: boolean;
132
+ readonly avatarId?: string;
133
+ }
107
134
  export interface LiveAvatarController {
108
135
  subscribe(listener: () => void): () => void;
109
136
  getSnapshot(): LiveAvatarSnapshot;
110
- /** From the seam (P2): a voice conversation is live, or over. */
137
+ /** From the seam (P2): a voice conversation is live, or over. While the Avatar tab is open, its end ends nothing. */
111
138
  setVoiceLive(live: boolean): void;
112
- /** The person asked for the avatar: ask the server whether it is offered, then show the chooser and the disclosure. */
113
- requestOn(): void;
139
+ /** The person asked for the avatar: ask the server whether it is offered, then show the chooser and the disclosure (`options`). */
140
+ requestOn(options?: LiveAvatarRequestOptions): void;
114
141
  /** The person confirmed the disclosure: start a session with the chosen avatar. */
115
142
  confirm(): void;
143
+ /**
144
+ * The Avatar tab opened or closed (BOFF-9341). Open: the avatar may be asked for with no voice conversation, and an avatar already on
145
+ * is adopted; a question already in flight is asked again, choose only. Close: any question, choice or session ends as the person's
146
+ * OFF (`user`), except an avatar `heldByVoice`, which stays on for Voice.
147
+ */
148
+ setTabOpen(open: boolean): void;
116
149
  /** The person declined the disclosure. */
117
150
  cancelChoice(): void;
118
151
  selectAvatar(avatarId: string): void;
119
- /** Avatar OFF: the toggle, the compact view's stop, the end of the voice conversation (P2). Idempotent. */
152
+ /**
153
+ * Avatar OFF: the toggle, the compact view's stop, the tab's Turn off, the end of the voice conversation (P2). Idempotent. In the tab,
154
+ * an OFF within `LIVE_AVATAR_TAB_OFF_GUARD_MS` of the start click, while it is still starting, is ignored (a double click).
155
+ */
120
156
  switchOff(): void;
121
157
  /** A user gesture: let the avatar's sound play (autoplay) and wake the level meter. */
122
158
  resumeAudio(): void;
123
159
  /** The stage that shows the avatar mounted (or unmounted): replies go to the avatar only while it is shown (D4). */
124
160
  setStageMounted(mounted: boolean): void;
125
- /** The stage's video element, or null when it goes. */
126
- attachVideo(element: HTMLVideoElement | null): void;
161
+ /**
162
+ * A view's video element (a React 19 ref callback): the avatar's video shows in the latest one mounted. The returned cleanup releases
163
+ * THAT element only, so a view going never blanks another (the tab adopting the voice stage's avatar). `null`: release the current one.
164
+ */
165
+ attachVideo(element: HTMLVideoElement | null): (() => void) | undefined;
127
166
  /** Where one reply goes: the avatar, or the speakers (untouched). */
128
167
  route(options: SpeechSpeakOptions, base: SpeechEngine): 'avatar' | 'speakers';
129
168
  /** A fresh audio output for one reply routed to the avatar. */