@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
@@ -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>;
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;
21
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
@@ -63,6 +177,42 @@ export declare function useLiveAvatarStage(): ComponentType<LiveAvatarStageProps
63
177
  * is to the store `AssistantSpeechProvider` already subscribes to.
64
178
  */
65
179
  export declare function useLiveAvatarSpeechEngine(seam: AssistantLiveAvatarSeam | undefined): SpeechEngine | undefined;
180
+ /**
181
+ * @internal Exported for the assistant's own hands-free wiring (`useVoiceSession`,
182
+ * `AssistantHandsFreeInterruptToggle`); not part of the module's public surface.
183
+ *
184
+ * Whether the live avatar is ON: never without a seam, so false in every app that passes no
185
+ * `liveAvatar`. Read from the ON switch with or without a stage: what it guards is the avatar's
186
+ * voice, not its picture.
187
+ */
188
+ export declare function useLiveAvatarOn(): boolean;
189
+ /**
190
+ * @internal Exported for `AssistantVoiceSession` alone; not part of the module's public surface.
191
+ *
192
+ * ★★ THE PILOT RULE (BOFF-8822), while the avatar's echo reference (P5) is not in: hands-free
193
+ * interrupt is OFF whenever the avatar is ON. The avatar's voice plays on the speakers, where the
194
+ * microphone hears it, and hands-free would take it for the person speaking over the reply: a
195
+ * barge-in that cuts the avatar off.
196
+ * - Switched ON with hands-free on: hands-free is switched OFF in the setting the person sees (and
197
+ * stored, so it stays off), and this returns true until the avatar is OFF again, for the voice
198
+ * session to say why.
199
+ * - While it is ON: a write that turns hands-free back on is turned back at once, its menu control
200
+ * is `aria-disabled` (it keeps its focus) and says why (`AssistantHandsFreeInterruptToggle`), and the voice session's own
201
+ * reading of the setting is OFF whatever it says (`resolveHandsFreeEnabled`).
202
+ * - Switched OFF: hands-free stays OFF. Nothing turns it back on behind the person's back, and the
203
+ * copy (`avatar.handsFreeOff`) says so and where to turn it back on (More options).
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
+ *
213
+ * Without a seam nothing here subscribes to, reads or writes anything.
214
+ */
215
+ export declare function useLiveAvatarHandsFreeGuard(): boolean;
66
216
  /**
67
217
  * The voice view to SHOW, given the one the user CHOSE; and the avatar kept to
68
218
  * the live conversation (`ended`: over, but still on screen to say why it
@@ -85,6 +235,13 @@ export declare function useLiveAvatarSpeechEngine(seam: AssistantLiveAvatarSeam
85
235
  * ★ Adjusted DURING RENDER, as `AIAssistantPanel` adjusts its preview flag: no
86
236
  * frame shows the stale view, and the update is conditional on the state it
87
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.
88
245
  */
89
246
  export declare function useLiveAvatarVoiceView(chosen: VoiceView, ended: boolean): VoiceView;
90
247
  /**
@@ -116,6 +273,9 @@ export declare function useLiveAvatarMiniIndicatorShown(): boolean;
116
273
  * comes after the microphone control, so Tab meets the strip's own control
117
274
  * first.
118
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
+ *
119
279
  * ★ "Avatar on" is a state, not a label. The control is an icon named by
120
280
  * `avatar.stop`, so no button is named for the opposite of what it does, and
121
281
  * its name is all of its words (WCAG 2.5.3, as the session's End).
@@ -1,100 +1,182 @@
1
- import { getSpeechEngine as e, subscribeSpeechEngine as t } from "../speech/createSpeechEngine.js";
2
- import { useAvatarCopy as n } from "../avatar/useAvatarCopy.js";
3
- import { ASSISTANT_VOICE_SESSION_ATTR as r } from "./panelOverlays.js";
4
- import { createContext as i, useContext as a, useEffect as o, useLayoutEffect as s, useMemo as c, useRef as l, useState as u, useSyncExternalStore as d } from "react";
5
- import { VideoOff as f } from "lucide-react";
6
- import { jsx as p, jsxs as m } from "react/jsx-runtime";
7
- //#region src/shared/assistant/ui/assistantLiveAvatarSeam.tsx
8
- var h = i(null), g = ({ value: e, children: t }) => /* @__PURE__ */ p(h.Provider, {
9
- value: e ?? null,
10
- children: t
1
+ import { useAssistantStore as e } from "../store.js";
2
+ import { getSpeechEngine as t, subscribeSpeechEngine as n } from "../speech/createSpeechEngine.js";
3
+ import { useAvatarCopy as r } from "../avatar/useAvatarCopy.js";
4
+ import { ASSISTANT_VOICE_SESSION_ATTR as i } from "./panelOverlays.js";
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
+ import { VideoOff as p } from "lucide-react";
7
+ import { jsx as m, jsxs as h } from "react/jsx-runtime";
8
+ var g = a({
9
+ open: !1,
10
+ close: () => {},
11
+ ids: null,
12
+ covered: !1
11
13
  });
12
14
  function _() {
13
- return a(h)?.Stage ?? null;
15
+ return o(g);
14
16
  }
15
- function v(n) {
16
- let r = d(t, e, e), i = n?.decorateSpeechEngine;
17
- return c(() => i ? i(r) : void 0, [i, r]);
17
+ function v() {
18
+ return o(x)?.tab ?? null;
18
19
  }
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, {
29
+ value: e ?? null,
30
+ children: t
31
+ });
32
+ function C() {
33
+ let e = o(x)?.Stage ?? null;
34
+ return _().open ? null : e;
35
+ }
36
+ function w(e) {
37
+ let r = f(n, t, t), i = e?.decorateSpeechEngine;
38
+ return l(() => i ? i(r) : void 0, [i, r]);
39
+ }
40
+ var T = {
20
41
  subscribe: () => () => {},
21
42
  getSnapshot: () => !1,
22
43
  switchOff: () => {},
23
44
  voiceLive: () => {}
24
45
  };
25
- function b(e) {
46
+ function E(e) {
26
47
  return e?.Stage ? e.enabled : void 0;
27
48
  }
28
- function x(e) {
29
- let t = e ?? y;
30
- return d(t.subscribe, t.getSnapshot, t.getSnapshot);
49
+ function D(e) {
50
+ let t = e ?? T;
51
+ return f(t.subscribe, t.getSnapshot, t.getSnapshot);
52
+ }
53
+ function O() {
54
+ return D(o(x)?.enabled);
55
+ }
56
+ function k() {
57
+ let t = o(x)?.enabled, n = t ? M(t) : A, r = f(n.subscribe, n.isShown, n.isShown);
58
+ return s(() => {
59
+ if (!t) return;
60
+ let r = () => {
61
+ if (!t.getSnapshot()) {
62
+ n.set(!1);
63
+ return;
64
+ }
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);
70
+ return () => {
71
+ a(), o(), i();
72
+ };
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;
31
106
  }
32
- function S(e, t) {
33
- let n = a(h);
34
- C(n?.enabled, t);
35
- let r = b(n), i = x(r), [o, s] = u(() => ({
36
- 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,
37
112
  chosen: e,
38
- open: i && e === "mini"
113
+ tabOpen: r,
114
+ open: a && e === "mini" && !r
39
115
  }));
40
- if (!r) return e;
41
- let c = o.open;
42
- return (o.on !== i || o.chosen !== e) && (c = o.on !== i && i && e === "mini", s({
43
- 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,
44
120
  chosen: e,
45
- open: c
46
- })), c && !t ? "panel" : e;
121
+ tabOpen: r,
122
+ open: l
123
+ })), l && !t && !r ? "panel" : e;
47
124
  }
48
- function C(e, t) {
49
- let n = l(0);
50
- o(() => {
125
+ function P(e, t, n) {
126
+ let r = u(0), i = u(n);
127
+ c(() => {
128
+ i.current = n;
129
+ }, [n]), s(() => {
51
130
  if (!e) return;
52
- let r = ++n.current, i = (e) => queueMicrotask(() => {
53
- n.current === r && e();
54
- }), a = () => i(() => {
55
- 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();
56
137
  });
57
- if (!t) return i(() => e.voiceLive(!0)), a;
58
- a();
59
- let o = e.getSnapshot();
138
+ if (!t) return a(() => e.voiceLive(!0)), s;
139
+ s();
140
+ let c = e.getSnapshot();
60
141
  return e.subscribe(() => {
61
- let t = o;
62
- o = e.getSnapshot(), o && !t && i(() => e.switchOff());
142
+ let t = c;
143
+ c = e.getSnapshot(), c && !t && a(o);
63
144
  });
64
145
  }, [e, t]);
65
146
  }
66
- function w() {
67
- return x(b(a(h)));
147
+ function F() {
148
+ let e = D(E(o(x))), t = _().open;
149
+ return e && !t;
68
150
  }
69
- var T = () => {
70
- let e = b(a(h)), t = x(e);
71
- return e && t ? /* @__PURE__ */ p(O, {
151
+ var I = () => {
152
+ let e = E(o(x)), t = D(e), n = _().open;
153
+ return e && t && !n ? /* @__PURE__ */ m(z, {
72
154
  onStop: e.switchOff,
73
155
  isOn: e.getSnapshot
74
156
  }) : null;
75
- }, E = "[data-testid=\"assistant-voice-orb\"]", D = "[data-testid=\"assistant-voice-end\"]", O = ({ onStop: e, isOn: t }) => {
76
- let i = n(), a = l(null), o = l(t);
77
- return s(() => {
157
+ }, L = "[data-testid=\"assistant-voice-orb\"]", R = "[data-testid=\"assistant-voice-end\"]", z = ({ onStop: e, isOn: t }) => {
158
+ let n = r(), a = u(null), o = u(t);
159
+ return c(() => {
78
160
  let e = a.current, t = o.current;
79
161
  return () => {
80
162
  if (!e?.contains(e.ownerDocument.activeElement)) return;
81
- let n = e.closest(`[${r}]`), i = t() ? null : n?.querySelector(E);
82
- (i && !i.hasAttribute("disabled") ? i : n?.querySelector(D))?.focus();
163
+ let n = e.closest(`[${i}]`), r = t() ? null : n?.querySelector(L);
164
+ (r && !r.hasAttribute("disabled") ? r : n?.querySelector(R))?.focus();
83
165
  };
84
- }, []), /* @__PURE__ */ m("div", {
166
+ }, []), /* @__PURE__ */ h("div", {
85
167
  ref: a,
86
168
  className: "absolute bottom-1.5 left-7 flex h-8 max-w-[calc(100%-13.875rem)] items-center gap-1 rounded-full border border-border-default bg-bg-elevated ps-2.5 pe-0.5 text-xs font-medium text-text-secondary rtl:left-auto rtl:right-7",
87
169
  "data-testid": "assistant-voice-avatar",
88
- children: [/* @__PURE__ */ p("span", {
170
+ children: [/* @__PURE__ */ m("span", {
89
171
  className: "truncate",
90
- children: i.stateOn
91
- }), /* @__PURE__ */ p("button", {
172
+ children: n.stateOn
173
+ }), /* @__PURE__ */ m("button", {
92
174
  type: "button",
93
175
  onClick: () => e(),
94
176
  className: "relative flex size-7 shrink-0 items-center justify-center rounded-full transition-colors after:absolute after:-inset-2 after:-ms-1.5 after:me-1.5 hover:bg-bg-raised hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-border-strong",
95
- "aria-label": i.stop,
177
+ "aria-label": n.stop,
96
178
  "data-testid": "assistant-voice-avatar-stop",
97
- children: /* @__PURE__ */ p(f, {
179
+ children: /* @__PURE__ */ m(p, {
98
180
  className: "size-4",
99
181
  "aria-hidden": "true"
100
182
  })
@@ -102,4 +184,4 @@ var T = () => {
102
184
  });
103
185
  };
104
186
  //#endregion
105
- export { T as LiveAvatarMiniIndicator, g as LiveAvatarSeamProvider, w as useLiveAvatarMiniIndicatorShown, v as useLiveAvatarSpeechEngine, _ as useLiveAvatarStage, S 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).