@burdenoff/fe-libs 2026.1014.5 → 2026.1015.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) 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 +50 -21
  16. package/dist/shared/assistant/avatar/useAvatarCopy.js +93 -49
  17. package/dist/shared/assistant/i18nManifest.d.ts +23 -2
  18. package/dist/shared/assistant/i18nManifest.js +23 -2
  19. package/dist/shared/assistant/index.d.ts +6 -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 +134 -101
  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 +1261 -1202
  26. package/dist/shared/assistant/ui/assistantLiveAvatarSeam.d.ts +137 -5
  27. package/dist/shared/assistant/ui/assistantLiveAvatarSeam.js +114 -55
  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/panelOverlays.d.ts +24 -3
  33. package/dist/shared/assistant/ui/panelOverlays.js +11 -10
  34. package/dist/shared/assistant/ui/useVoiceSession.d.ts +20 -5
  35. package/dist/shared/assistant/ui/useVoiceSession.js +11 -4
  36. package/dist/shared/assistant/ui/voice/AssistantVoiceSession.d.ts +8 -2
  37. package/dist/shared/assistant/ui/voice/AssistantVoiceSession.js +172 -160
  38. package/dist/shared/assistant/ui/voice/voiceStartGate.d.ts +57 -0
  39. package/dist/shared/assistant/ui/voice/voiceStartGate.js +9 -0
  40. package/dist/shared/assistant/voice/voiceReply.d.ts +23 -0
  41. package/dist/shared/assistant/voice/voiceReply.js +13 -0
  42. package/dist/shared/assistant/voice/voiceSession.d.ts +10 -0
  43. package/dist/shared/assistant/voice/voiceSession.js +7 -0
  44. package/dist/shared-assistant.js +260 -254
  45. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { ComponentType, FC, ReactNode } from 'react';
1
+ import { ComponentType, FC, ReactNode, RefObject } from 'react';
2
2
  import { SpeechEngine } from '../speech/types';
3
3
  import { VoiceView } from './voice/AssistantVoiceSession';
4
4
  export interface LiveAvatarStageProps {
@@ -15,10 +15,115 @@ export interface AssistantLiveAvatarSeam {
15
15
  /**
16
16
  * The avatar's ON switch. Optional. It is SHOWN only when `Stage` is given,
17
17
  * since without a stage there is nothing to show; told when a voice
18
- * conversation is live, and switched OFF at the end of every one, either way.
18
+ * conversation is live, and switched OFF at the end of every one, either way
19
+ * (except while the Avatar tab is open, which owns its avatar).
19
20
  */
20
21
  enabled?: LiveAvatarEnabledStore;
22
+ /**
23
+ * The Avatar tab (BOFF-9341). Optional; without it there is no tab, and the
24
+ * panel is exactly what it was.
25
+ *
26
+ * ★★ HOST CONTRACT: hand the seam over only once the server's availability
27
+ * has said the avatar is offered to this person here (as VibeControls does),
28
+ * and keep it stable. The tab asks the same free availability question when
29
+ * it opens, and starts a paid session only on the person's Turn on.
30
+ */
31
+ tab?: LiveAvatarTabSeam;
32
+ }
33
+ /** Whether the Avatar tab may be offered, as a store read with `useSyncExternalStore`. STABLE. */
34
+ export interface LiveAvatarTabOfferedStore {
35
+ subscribe: (onChange: () => void) => () => void;
36
+ /** This browser can show the avatar (desktop Chrome first), and it was not refused on this page. */
37
+ getSnapshot: () => boolean;
38
+ }
39
+ /** The Avatar tab's part of the seam (`createLiveAvatarSeam` builds it). STABLE, like the rest of the seam. */
40
+ export interface LiveAvatarTabSeam {
41
+ readonly offered: LiveAvatarTabOfferedStore;
42
+ /**
43
+ * Told when the Avatar tab opens and closes; idempotent. Opening starts
44
+ * nothing paid. Closing is the person's OFF for the tab's avatar, except one
45
+ * `heldByVoice`, which stays ON for the voice stage.
46
+ */
47
+ readonly setOpen: (open: boolean) => void;
48
+ /**
49
+ * Whether the avatar now ON was turned on outside the tab, during the voice
50
+ * conversation that is still live: closing the tab hands it back to Voice
51
+ * instead of ending it (critic #9). Read when the tab closes.
52
+ */
53
+ readonly heldByVoice: () => boolean;
54
+ /**
55
+ * The tab's avatar area, drawn inside the panel's Avatar tab (`AssistantAvatarTab`): the frame, the chooser, the avatar's voice, the
56
+ * disclosure with Turn on / Turn off, its status line, and the way into the voice conversation. Mounted only while the tab is open.
57
+ */
58
+ readonly View: ComponentType<LiveAvatarTabProps>;
59
+ }
60
+ /** The voice conversation, as the Avatar tab shows it and starts it (the composer's own conversation, `useVoiceSession`). */
61
+ export interface LiveAvatarTabVoice {
62
+ /** A voice conversation is live: its compact strip sits under the tab. */
63
+ readonly live: boolean;
64
+ /** Why one cannot start now, in the person's words (the composer's full check, `voiceStartBlocker`); null when it can. */
65
+ readonly blocked: string | null;
66
+ /** The reason lasts (no voice here, in this mode, or in this conversation): asking the avatar is impossible for now. */
67
+ readonly impossible: boolean;
68
+ /** Start it: the composer's own start (`startVoiceWithPrewarm`). Opens the microphone; never called by anything but its button. */
69
+ readonly start: () => void;
70
+ }
71
+ /** What the panel hands the tab's `View`. */
72
+ export interface LiveAvatarTabProps {
73
+ /**
74
+ * The tab is on screen and nothing covers it (History, the preview, the voice session enlarged, a transcript drawer): replies may
75
+ * go to the avatar only while this is true (D4, critic #4). Per reply: each reply is routed once, when it starts, so a reply already
76
+ * on the avatar when this turns false finishes there; the next goes to the speakers.
77
+ */
78
+ readonly shown: boolean;
79
+ readonly voice: LiveAvatarTabVoice;
80
+ /** Hands-free interrupt was switched off for this avatar (`useLiveAvatarHandsFreeGuard`): said in the tab while no voice is live. */
81
+ readonly handsFreeOff: boolean;
82
+ /** The id the leave hint ("Leaving the Avatar tab turns the avatar off.") must carry: the Chat tab is described by it. */
83
+ readonly leaveHintId: string;
84
+ /** The "Start a voice conversation" button: focus returns to it when a voice conversation started in the tab ends. */
85
+ readonly startVoiceRef: RefObject<HTMLButtonElement | null>;
21
86
  }
87
+ /** The ids the tab row and the two tab panels share (`AIAssistantPanel` makes them). */
88
+ export interface LiveAvatarTabIds {
89
+ readonly chatTab: string;
90
+ readonly avatarTab: string;
91
+ readonly chatPanel: string;
92
+ readonly avatarPanel: string;
93
+ readonly leaveHint: string;
94
+ }
95
+ /**
96
+ * The Avatar tab's state as the panel provides it to the views below it: the
97
+ * chat area, the voice session and the seam's own hooks. Closed by default, so
98
+ * a panel that provides nothing (every host without the tab) is today's.
99
+ */
100
+ export interface LiveAvatarTabState {
101
+ /** The Avatar tab is the view on screen. */
102
+ readonly open: boolean;
103
+ /** Select Chat again: what something in the tab calls to hand the person back to the chat. */
104
+ readonly close: () => void;
105
+ /**
106
+ * The tab row's ids while it is shown, else null (or absent): only then is the chat region a tab panel, so a panel without the
107
+ * tab is today's markup, attribute for attribute.
108
+ */
109
+ readonly ids?: LiveAvatarTabIds | null;
110
+ /** A full-surface overlay (History, the preview) covers the panel: the tab under it is not shown (D4). */
111
+ readonly covered?: boolean;
112
+ }
113
+ /**
114
+ * @internal Provided by `AIAssistantPanel` and read by the assistant's own
115
+ * views; not part of the module's public surface.
116
+ */
117
+ export declare const LiveAvatarTabContext: import('react').Context<LiveAvatarTabState>;
118
+ /** @internal The Avatar tab's state (`LiveAvatarTabContext`): closed without a panel that offers it. */
119
+ export declare function useLiveAvatarTab(): LiveAvatarTabState;
120
+ /** @internal The seam's tab part, or null: what the chat area draws the open tab with. */
121
+ export declare function useLiveAvatarTabSeam(): LiveAvatarTabSeam | null;
122
+ /**
123
+ * @internal For `AIAssistantPanel`: whether the seam offers the Avatar tab now. False without a tab part, and then nothing is
124
+ * subscribed to.
125
+ */
126
+ export declare function useLiveAvatarTabOffered(tab: LiveAvatarTabSeam | undefined): boolean;
22
127
  /**
23
128
  * The avatar's ON flag as a store, read with `useSyncExternalStore`: the voice
24
129
  * session subscribes and nothing above it re-renders. It must be STABLE, and
@@ -28,7 +133,9 @@ export interface AssistantLiveAvatarSeam {
28
133
  * ★★ ON is only ever the person's own click, on a control the host offers
29
134
  * only while `voiceLive` says a voice conversation is live, and a paid session
30
135
  * starts only then: no avatar after its conversation has ended. The panel
31
- * switches the avatar OFF, never ON.
136
+ * switches the avatar OFF, never ON. The one other place is the Avatar tab
137
+ * (BOFF-9341), whose own Turn on starts the avatar with or without a voice
138
+ * conversation, and whose avatar ends when the tab closes.
32
139
  */
33
140
  export interface LiveAvatarEnabledStore {
34
141
  subscribe: (onChange: () => void) => () => void;
@@ -39,7 +146,9 @@ export interface LiveAvatarEnabledStore {
39
146
  * Called at the end of EVERY voice conversation, even when the avatar is
40
147
  * already OFF; again for any ON while a failed one is still on screen; and
41
148
  * by the compact view's stop. So it must be idempotent and cheap. It touches
42
- * no audio: a reply already playing is the speech engine's to finish.
149
+ * no audio: a reply already playing is the speech engine's to finish. None
150
+ * of the three end calls is made while the Avatar tab is open: the tab owns
151
+ * its avatar then.
43
152
  */
44
153
  switchOff: () => void;
45
154
  /**
@@ -53,7 +162,12 @@ export declare const LiveAvatarSeamProvider: FC<{
53
162
  value?: AssistantLiveAvatarSeam;
54
163
  children: ReactNode;
55
164
  }>;
56
- /** The stage component, or null — which is what every host without an avatar gets. */
165
+ /**
166
+ * The stage component, or null — which is what every host without an avatar
167
+ * gets, and what the voice stage gets while the Avatar tab is open: the tab is
168
+ * then the one place the avatar is shown, and the voice stage draws its
169
+ * waveform (BOFF-9341).
170
+ */
57
171
  export declare function useLiveAvatarStage(): ComponentType<LiveAvatarStageProps> | null;
58
172
  /**
59
173
  * The engine the speech provider should be handed, or `undefined` for "use your
@@ -88,6 +202,14 @@ export declare function useLiveAvatarOn(): boolean;
88
202
  * - Switched OFF: hands-free stays OFF. Nothing turns it back on behind the person's back, and the
89
203
  * copy (`avatar.handsFreeOff`) says so and where to turn it back on (More options).
90
204
  *
205
+ * ★ ONE notice per avatar, shared by every view that mounts this guard (BOFF-9341: the voice session
206
+ * and the Avatar tab). Whichever of them sees the ON first switches hands-free off; every one of them
207
+ * then says so, so a voice conversation started after the tab's Turn on still explains it. Which view
208
+ * SHOWS the notice is the views' call: the tab while no voice conversation is live, the session's
209
+ * strip otherwise, so it is said in exactly one place. It is kept while any of them is mounted and
210
+ * the avatar stays ON, and goes with the last of them (a microtask late, so React's development
211
+ * double mount keeps it, as component state was kept before).
212
+ *
91
213
  * Without a seam nothing here subscribes to, reads or writes anything.
92
214
  */
93
215
  export declare function useLiveAvatarHandsFreeGuard(): boolean;
@@ -113,6 +235,13 @@ export declare function useLiveAvatarHandsFreeGuard(): boolean;
113
235
  * ★ Adjusted DURING RENDER, as `AIAssistantPanel` adjusts its preview flag: no
114
236
  * frame shows the stale view, and the update is conditional on the state it
115
237
  * replaces, so it cannot loop.
238
+ *
239
+ * ★★ The Avatar tab (BOFF-9341). While it is open it shows the avatar, so this
240
+ * holds nothing: the user's choice, and whatever changes meanwhile lets go.
241
+ * When it closes on an avatar Voice turned on, in this conversation
242
+ * (`heldByVoice`, critic #9), the avatar stays ON, and over the compact view
243
+ * the hold shows it again here. Any other avatar of the tab ends as the tab
244
+ * closes, so nothing is held for it: no frame shows a stage going away.
116
245
  */
117
246
  export declare function useLiveAvatarVoiceView(chosen: VoiceView, ended: boolean): VoiceView;
118
247
  /**
@@ -144,6 +273,9 @@ export declare function useLiveAvatarMiniIndicatorShown(): boolean;
144
273
  * comes after the microphone control, so Tab meets the strip's own control
145
274
  * first.
146
275
  *
276
+ * ★ Hidden while the Avatar tab is open (BOFF-9341): the tab shows the avatar,
277
+ * and its own Turn off, right above the strip.
278
+ *
147
279
  * ★ "Avatar on" is a state, not a label. The control is an icon named by
148
280
  * `avatar.stop`, so no button is named for the opposite of what it does, and
149
281
  * its name is all of its words (WCAG 2.5.3, as the session's End).
@@ -5,104 +5,163 @@ import { ASSISTANT_VOICE_SESSION_ATTR as i } from "./panelOverlays.js";
5
5
  import { createContext as a, useContext as o, useEffect as s, useLayoutEffect as c, useMemo as l, useRef as u, useState as d, useSyncExternalStore as f } from "react";
6
6
  import { VideoOff as p } from "lucide-react";
7
7
  import { jsx as m, jsxs as h } from "react/jsx-runtime";
8
- //#region src/shared/assistant/ui/assistantLiveAvatarSeam.tsx
9
- var g = a(null), _ = ({ value: e, children: t }) => /* @__PURE__ */ m(g.Provider, {
8
+ var g = a({
9
+ open: !1,
10
+ close: () => {},
11
+ ids: null,
12
+ covered: !1
13
+ });
14
+ function _() {
15
+ return o(g);
16
+ }
17
+ function v() {
18
+ return o(x)?.tab ?? null;
19
+ }
20
+ var y = {
21
+ subscribe: () => () => {},
22
+ getSnapshot: () => !1
23
+ };
24
+ function b(e) {
25
+ let t = e?.offered ?? y;
26
+ return f(t.subscribe, t.getSnapshot, t.getSnapshot);
27
+ }
28
+ var x = a(null), S = ({ value: e, children: t }) => /* @__PURE__ */ m(x.Provider, {
10
29
  value: e ?? null,
11
30
  children: t
12
31
  });
13
- function v() {
14
- return o(g)?.Stage ?? null;
32
+ function C() {
33
+ let e = o(x)?.Stage ?? null;
34
+ return _().open ? null : e;
15
35
  }
16
- function y(e) {
36
+ function w(e) {
17
37
  let r = f(n, t, t), i = e?.decorateSpeechEngine;
18
38
  return l(() => i ? i(r) : void 0, [i, r]);
19
39
  }
20
- var b = {
40
+ var T = {
21
41
  subscribe: () => () => {},
22
42
  getSnapshot: () => !1,
23
43
  switchOff: () => {},
24
44
  voiceLive: () => {}
25
45
  };
26
- function x(e) {
46
+ function E(e) {
27
47
  return e?.Stage ? e.enabled : void 0;
28
48
  }
29
- function S(e) {
30
- let t = e ?? b;
49
+ function D(e) {
50
+ let t = e ?? T;
31
51
  return f(t.subscribe, t.getSnapshot, t.getSnapshot);
32
52
  }
33
- function C() {
34
- return S(o(g)?.enabled);
53
+ function O() {
54
+ return D(o(x)?.enabled);
35
55
  }
36
- function w() {
37
- let t = o(g)?.enabled, [n, r] = d(null);
56
+ function k() {
57
+ let t = o(x)?.enabled, n = t ? M(t) : A, r = f(n.subscribe, n.isShown, n.isShown);
38
58
  return s(() => {
39
59
  if (!t) return;
40
- let n = () => {
60
+ let r = () => {
41
61
  if (!t.getSnapshot()) {
42
- r(null);
62
+ n.set(!1);
43
63
  return;
44
64
  }
45
- let n = e.getState();
46
- n.handsFreeInterrupt && (n.setHandsFreeInterrupt(!1), r(() => t));
47
- };
48
- n();
49
- let i = t.subscribe(n), a = e.subscribe(n);
65
+ let r = e.getState();
66
+ r.handsFreeInterrupt && (r.setHandsFreeInterrupt(!1), n.set(!0));
67
+ }, i = n.hold();
68
+ r();
69
+ let a = t.subscribe(r), o = e.subscribe(r);
50
70
  return () => {
51
- i(), a();
71
+ a(), o(), i();
52
72
  };
53
- }, [t]), n !== null && n === t;
73
+ }, [t, n]), r;
74
+ }
75
+ var A = {
76
+ subscribe: () => () => {},
77
+ isShown: () => !1,
78
+ set: () => {},
79
+ hold: () => () => {}
80
+ }, j = /* @__PURE__ */ new WeakMap();
81
+ function M(e) {
82
+ let t = j.get(e);
83
+ if (t) return t;
84
+ let n = !1, r = 0, i = /* @__PURE__ */ new Set(), a = (e) => {
85
+ if (n !== e) {
86
+ n = e;
87
+ for (let e of [...i]) e();
88
+ }
89
+ }, o = {
90
+ subscribe: (e) => (i.add(e), () => {
91
+ i.delete(e);
92
+ }),
93
+ isShown: () => n,
94
+ set: a,
95
+ hold: () => {
96
+ r += 1;
97
+ let e = !1;
98
+ return () => {
99
+ e || (e = !0, --r, queueMicrotask(() => {
100
+ r === 0 && a(!1);
101
+ }));
102
+ };
103
+ }
104
+ };
105
+ return j.set(e, o), o;
54
106
  }
55
- function T(e, t) {
56
- let n = o(g);
57
- E(n?.enabled, t);
58
- let r = x(n), i = S(r), [a, s] = d(() => ({
59
- on: i,
107
+ function N(e, t) {
108
+ let n = o(x), r = _().open;
109
+ P(n?.enabled, t, r);
110
+ let i = E(n), a = D(i), [s, c] = d(() => ({
111
+ on: a,
60
112
  chosen: e,
61
- open: i && e === "mini"
113
+ tabOpen: r,
114
+ open: a && e === "mini" && !r
62
115
  }));
63
- if (!r) return e;
64
- let c = a.open;
65
- return (a.on !== i || a.chosen !== e) && (c = a.on !== i && i && e === "mini", s({
66
- on: i,
116
+ if (!i) return e;
117
+ let l = s.open;
118
+ return (s.on !== a || s.chosen !== e || s.tabOpen !== r) && (l = r ? !1 : s.tabOpen ? a && e === "mini" && n?.tab?.heldByVoice() === !0 : s.on !== a && a && e === "mini", c({
119
+ on: a,
67
120
  chosen: e,
68
- open: c
69
- })), c && !t ? "panel" : e;
121
+ tabOpen: r,
122
+ open: l
123
+ })), l && !t && !r ? "panel" : e;
70
124
  }
71
- function E(e, t) {
72
- let n = u(0);
73
- s(() => {
125
+ function P(e, t, n) {
126
+ let r = u(0), i = u(n);
127
+ c(() => {
128
+ i.current = n;
129
+ }, [n]), s(() => {
74
130
  if (!e) return;
75
- let r = ++n.current, i = (e) => queueMicrotask(() => {
76
- n.current === r && e();
77
- }), a = () => i(() => {
78
- e.voiceLive(!1), e.switchOff();
131
+ let n = ++r.current, a = (e) => queueMicrotask(() => {
132
+ r.current === n && e();
133
+ }), o = () => {
134
+ i.current || e.switchOff();
135
+ }, s = () => a(() => {
136
+ e.voiceLive(!1), o();
79
137
  });
80
- if (!t) return i(() => e.voiceLive(!0)), a;
81
- a();
82
- let o = e.getSnapshot();
138
+ if (!t) return a(() => e.voiceLive(!0)), s;
139
+ s();
140
+ let c = e.getSnapshot();
83
141
  return e.subscribe(() => {
84
- let t = o;
85
- o = e.getSnapshot(), o && !t && i(() => e.switchOff());
142
+ let t = c;
143
+ c = e.getSnapshot(), c && !t && a(o);
86
144
  });
87
145
  }, [e, t]);
88
146
  }
89
- function D() {
90
- return S(x(o(g)));
147
+ function F() {
148
+ let e = D(E(o(x))), t = _().open;
149
+ return e && !t;
91
150
  }
92
- var O = () => {
93
- let e = x(o(g)), t = S(e);
94
- return e && t ? /* @__PURE__ */ m(j, {
151
+ var I = () => {
152
+ let e = E(o(x)), t = D(e), n = _().open;
153
+ return e && t && !n ? /* @__PURE__ */ m(z, {
95
154
  onStop: e.switchOff,
96
155
  isOn: e.getSnapshot
97
156
  }) : null;
98
- }, k = "[data-testid=\"assistant-voice-orb\"]", A = "[data-testid=\"assistant-voice-end\"]", j = ({ onStop: e, isOn: t }) => {
157
+ }, L = "[data-testid=\"assistant-voice-orb\"]", R = "[data-testid=\"assistant-voice-end\"]", z = ({ onStop: e, isOn: t }) => {
99
158
  let n = r(), a = u(null), o = u(t);
100
159
  return c(() => {
101
160
  let e = a.current, t = o.current;
102
161
  return () => {
103
162
  if (!e?.contains(e.ownerDocument.activeElement)) return;
104
- let n = e.closest(`[${i}]`), r = t() ? null : n?.querySelector(k);
105
- (r && !r.hasAttribute("disabled") ? r : n?.querySelector(A))?.focus();
163
+ let n = e.closest(`[${i}]`), r = t() ? null : n?.querySelector(L);
164
+ (r && !r.hasAttribute("disabled") ? r : n?.querySelector(R))?.focus();
106
165
  };
107
166
  }, []), /* @__PURE__ */ h("div", {
108
167
  ref: a,
@@ -125,4 +184,4 @@ var O = () => {
125
184
  });
126
185
  };
127
186
  //#endregion
128
- export { O as LiveAvatarMiniIndicator, _ as LiveAvatarSeamProvider, w as useLiveAvatarHandsFreeGuard, D as useLiveAvatarMiniIndicatorShown, C as useLiveAvatarOn, y as useLiveAvatarSpeechEngine, v as useLiveAvatarStage, T as useLiveAvatarVoiceView };
187
+ export { I as LiveAvatarMiniIndicator, S as LiveAvatarSeamProvider, g as LiveAvatarTabContext, k as useLiveAvatarHandsFreeGuard, F as useLiveAvatarMiniIndicatorShown, O as useLiveAvatarOn, w as useLiveAvatarSpeechEngine, C as useLiveAvatarStage, _ as useLiveAvatarTab, b as useLiveAvatarTabOffered, v as useLiveAvatarTabSeam, N as useLiveAvatarVoiceView };
@@ -0,0 +1,13 @@
1
+ import { FC } from 'react';
2
+ import { LiveAvatarTabSeam, LiveAvatarTabVoice } from '../assistantLiveAvatarSeam';
3
+ export interface AssistantAvatarTabProps {
4
+ readonly tab: LiveAvatarTabSeam;
5
+ /** This panel's id, and the Avatar tab that labels it. */
6
+ readonly id: string;
7
+ readonly labelledBy: string;
8
+ readonly leaveHintId: string;
9
+ /** Nothing covers the tab (D4): see `LiveAvatarTabProps.shown`. */
10
+ readonly shown: boolean;
11
+ readonly voice: LiveAvatarTabVoice;
12
+ }
13
+ export declare const AssistantAvatarTab: FC<AssistantAvatarTabProps>;
@@ -0,0 +1,70 @@
1
+ import { ASSISTANT_VOICE_SESSION_ATTR as e } from "../panelOverlays.js";
2
+ import { useLiveAvatarHandsFreeGuard as t } from "../assistantLiveAvatarSeam.js";
3
+ import { useEffect as n, useLayoutEffect as r, useRef as i, useState as a } from "react";
4
+ import { jsx as o } from "react/jsx-runtime";
5
+ //#region src/shared/assistant/ui/avatar/AssistantAvatarTab.tsx
6
+ var s = 180;
7
+ function c(t, n) {
8
+ let [i, o] = a(0);
9
+ return r(() => {
10
+ if (!n) return;
11
+ let r = t.current?.parentElement?.querySelector(`[${e}]`);
12
+ if (!r) return;
13
+ let i = () => {
14
+ let e = typeof r.getBoundingClientRect == "function" ? r.getBoundingClientRect().height : 0;
15
+ o(Math.round(e));
16
+ };
17
+ if (i(), typeof ResizeObserver > "u") return;
18
+ let a = new ResizeObserver(i);
19
+ return a.observe(r), () => a.disconnect();
20
+ }, [t, n]), n ? i > 0 ? i : s : 0;
21
+ }
22
+ function l() {
23
+ if (typeof document > "u") return !1;
24
+ let e = document.activeElement;
25
+ return e === null || e === document.body || !e.isConnected || e.hasAttribute("data-assistant-panel");
26
+ }
27
+ var u = ({ tab: e, id: a, labelledBy: s, leaveHintId: u, shown: d, voice: f }) => {
28
+ let p = i(null), m = i(null), h = t(), g = c(p, f.live), _ = i(null);
29
+ r(() => {
30
+ let t = (_.current?.setup ?? 0) + 1;
31
+ return _.current = {
32
+ tab: e,
33
+ setup: t
34
+ }, e.setOpen(!0), () => {
35
+ queueMicrotask(() => {
36
+ let n = _.current;
37
+ n !== null && n.tab === e && n.setup !== t || e.setOpen(!1);
38
+ });
39
+ };
40
+ }, [e]);
41
+ let v = i(f.live);
42
+ return n(() => {
43
+ let e = v.current;
44
+ v.current = f.live, !(!e || f.live) && l() && m.current?.focus();
45
+ }, [f.live]), /* @__PURE__ */ o("div", {
46
+ ref: p,
47
+ role: "tabpanel",
48
+ id: a,
49
+ "aria-labelledby": s,
50
+ className: "absolute inset-0 z-[5] flex flex-col bg-bg-surface",
51
+ "data-testid": "assistant-avatar-tab",
52
+ children: /* @__PURE__ */ o("div", {
53
+ className: "flex min-h-0 flex-1 flex-col overflow-y-auto px-4 pt-4",
54
+ style: {
55
+ paddingBottom: g + 16,
56
+ scrollPaddingBottom: g + 16
57
+ },
58
+ "data-testid": "assistant-avatar-tab-column",
59
+ children: /* @__PURE__ */ o(e.View, {
60
+ shown: d,
61
+ voice: f,
62
+ handsFreeOff: h,
63
+ leaveHintId: u,
64
+ startVoiceRef: m
65
+ })
66
+ })
67
+ });
68
+ };
69
+ //#endregion
70
+ export { u as AssistantAvatarTab };
@@ -55,6 +55,24 @@ interface Props {
55
55
  * sandbox while the person is still typing (BOFF-7801).
56
56
  */
57
57
  onDraftStart?: () => void;
58
+ /**
59
+ * ★ BOFF-9341: something covers the composer (the Avatar tab). Dictation
60
+ * stops, keeping every word it heard in the draft, and stays stopped: a
61
+ * microphone recording behind a surface the person cannot see is a privacy
62
+ * problem, and the tab's own voice conversation needs the microphone.
63
+ */
64
+ suspendDictation?: boolean;
65
+ /**
66
+ * Told what holds the voice conversation back here (BOFF-9341): a Send
67
+ * saving its attachments, and dictation holding the microphone. The rest of
68
+ * the composer's check comes from the props it is given.
69
+ */
70
+ onVoiceStateChange?: (state: AssistantComposerVoiceState) => void;
71
+ }
72
+ /** What in the composer holds a voice conversation back (`onVoiceStateChange`). */
73
+ export interface AssistantComposerVoiceState {
74
+ readonly committing: boolean;
75
+ readonly dictating: boolean;
58
76
  }
59
77
  /**
60
78
  * AssistantComposer — the v2 input surface (per the ai-assistant-v2 mockups).