@burdenoff/fe-libs 2026.1013.7 → 2026.1013.9

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.
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Every user-visible word of the optional live avatar, in one place (BOFF-8822).
3
+ *
4
+ * ★★ ONE MODULE, LITERAL KEYS. `i18nManifest.test.ts` parses every module under
5
+ * `shared/assistant` for `tr("<key>", "<English>")` calls and fails on a key the
6
+ * manifest lacks, a default that differs, or a manifest entry no call requests.
7
+ * Keeping every call here means the avatar UI imports words and cannot add a
8
+ * string the manifest does not know — and the manifest entries and these calls
9
+ * are one change.
10
+ *
11
+ * ★ The English names no product of ours (`noProductNames.test.ts`). The consent
12
+ * is the one place that names the services the reply audio goes to: the
13
+ * disclosure decided on 2026-10-08, which must stay true to the data flow —
14
+ * only the assistant's reply audio is sent, as data to the avatar; the person's
15
+ * microphone audio never is. It says nothing of credits (2026-10-09): avatar
16
+ * time is billed to the company, and "credits" read as the person's own. Pinned
17
+ * word for word by `avatarConsentCopy.test.ts`.
18
+ */
19
+ /** What the person can be told went wrong; the adapter maps server codes onto these. */
20
+ export type AvatarProblem = 'unavailable' | 'not-ready' | 'already-running' | 'capacity' | 'rate-limited' | 'paused' | 'provider' | 'connection' | 'needs-hosted-voice' | 'unsupported-browser' | 'playback-blocked';
21
+ export interface AvatarCopy {
22
+ toggle: string;
23
+ toggleHint: string;
24
+ stop: string;
25
+ consent: string;
26
+ consentConfirm: string;
27
+ consentCancel: string;
28
+ stateOff: string;
29
+ stateOn: string;
30
+ stateStarting: string;
31
+ stateReady: string;
32
+ stateEnding: string;
33
+ stateInterrupted: string;
34
+ stateVoiceOnly: string;
35
+ stageLabel: string;
36
+ stageWaiting: string;
37
+ chooserTitle: string;
38
+ chooserLabel: string;
39
+ chooserNone: string;
40
+ chooserLoading: string;
41
+ chooserEmpty: string;
42
+ chooserRetry: string;
43
+ chooserOption: (name: string) => string;
44
+ chooserSelected: (name: string) => string;
45
+ voiceModeLabel: string;
46
+ voiceModePaired: string;
47
+ voiceModeMine: string;
48
+ voiceModeHint: string;
49
+ problem: (problem: AvatarProblem) => string;
50
+ }
51
+ export declare function useAvatarCopy(): AvatarCopy;
@@ -0,0 +1,52 @@
1
+ import { useAssistantTr as e } from "../i18n.js";
2
+ import { useMemo as t } from "react";
3
+ //#region src/shared/assistant/avatar/useAvatarCopy.ts
4
+ function n() {
5
+ let n = e();
6
+ return t(() => ({
7
+ toggle: n("avatar.toggle", "Live avatar"),
8
+ toggleHint: n("avatar.toggleHint", "Show the assistant as a speaking avatar during voice conversations."),
9
+ stop: n("avatar.stop", "Turn off the avatar"),
10
+ consent: n("avatar.consent", "Turning on Avatar shares the assistant's reply audio with LiveKit and Protoface to animate the avatar. This feature does not send your microphone audio to Protoface."),
11
+ consentConfirm: n("avatar.consentConfirm", "Turn on the avatar"),
12
+ consentCancel: n("avatar.consentCancel", "Not now"),
13
+ stateOff: n("avatar.stateOff", "Avatar off"),
14
+ stateOn: n("avatar.stateOn", "Avatar on"),
15
+ stateStarting: n("avatar.stateStarting", "Starting the avatar…"),
16
+ stateReady: n("avatar.stateReady", "Avatar ready"),
17
+ stateEnding: n("avatar.stateEnding", "Stopping the avatar…"),
18
+ stateInterrupted: n("avatar.stateInterrupted", "The avatar was interrupted. You can keep talking."),
19
+ stateVoiceOnly: n("avatar.stateVoiceOnly", "The avatar is unavailable right now. Continuing with voice only."),
20
+ stageLabel: n("avatar.stageLabel", "Live avatar"),
21
+ stageWaiting: n("avatar.stageWaiting", "Waiting for the avatar…"),
22
+ chooserTitle: n("avatar.chooserTitle", "Choose an avatar"),
23
+ chooserLabel: n("avatar.chooserLabel", "Avatar"),
24
+ chooserNone: n("avatar.chooserNone", "No avatar chosen"),
25
+ chooserLoading: n("avatar.chooserLoading", "Loading avatars…"),
26
+ chooserEmpty: n("avatar.chooserEmpty", "No avatars are available."),
27
+ chooserRetry: n("avatar.chooserRetry", "Try again"),
28
+ chooserOption: (e) => n("avatar.chooserOption", "Use {{name}}", { name: e }),
29
+ chooserSelected: (e) => n("avatar.chooserSelected", "{{name}} (selected)", { name: e }),
30
+ voiceModeLabel: n("avatar.voiceModeLabel", "Avatar voice"),
31
+ voiceModePaired: n("avatar.voiceModePaired", "Use this avatar's recommended voice"),
32
+ voiceModeMine: n("avatar.voiceModeMine", "Keep my current voice"),
33
+ voiceModeHint: n("avatar.voiceModeHint", "The recommended voice is chosen for this avatar and the language of each reply."),
34
+ problem: (e) => {
35
+ switch (e) {
36
+ case "unavailable": return n("avatar.problemUnavailable", "The avatar isn’t available here.");
37
+ case "not-ready": return n("avatar.problemNotReady", "This avatar isn’t ready yet. Try another one.");
38
+ case "already-running": return n("avatar.problemAlreadyRunning", "An avatar is already running for you.");
39
+ case "capacity": return n("avatar.problemCapacity", "Avatars are busy right now. Try again in a moment.");
40
+ case "rate-limited": return n("avatar.problemRateLimited", "You started avatars too quickly. Wait a minute and try again.");
41
+ case "paused": return n("avatar.problemPaused", "Avatars are paused for now.");
42
+ case "provider": return n("avatar.problemProvider", "The avatar had a problem. Voice continues without it.");
43
+ case "connection": return n("avatar.problemConnection", "The connection to the avatar was lost.");
44
+ case "needs-hosted-voice": return n("avatar.problemNeedsHostedVoice", "The avatar needs one of the assistant’s own voices. Choose one in the voice settings.");
45
+ case "unsupported-browser": return n("avatar.problemUnsupportedBrowser", "This browser can’t show the avatar.");
46
+ case "playback-blocked": return n("avatar.problemPlaybackBlocked", "The browser blocked the avatar’s sound. Tap the avatar to turn it on.");
47
+ }
48
+ }
49
+ }), [n]);
50
+ }
51
+ //#endregion
52
+ export { n as useAvatarCopy };
@@ -216,6 +216,44 @@ export declare const ASSISTANT_I18N_LITERAL_COPY: {
216
216
  readonly "attachments.unsupportedImage": "This device can't read that image format, so it can't be resized to send. Save it as JPEG and try again.";
217
217
  readonly "attachments.uploading": "Uploading…";
218
218
  readonly "attachments.voiceDropped": "{{count}} attachment(s) couldn't be saved and weren't sent.";
219
+ readonly "avatar.chooserEmpty": "No avatars are available.";
220
+ readonly "avatar.chooserLabel": "Avatar";
221
+ readonly "avatar.chooserLoading": "Loading avatars…";
222
+ readonly "avatar.chooserNone": "No avatar chosen";
223
+ readonly "avatar.chooserOption": "Use {{name}}";
224
+ readonly "avatar.chooserRetry": "Try again";
225
+ readonly "avatar.chooserSelected": "{{name}} (selected)";
226
+ readonly "avatar.chooserTitle": "Choose an avatar";
227
+ readonly "avatar.consent": "Turning on Avatar shares the assistant's reply audio with LiveKit and Protoface to animate the avatar. This feature does not send your microphone audio to Protoface.";
228
+ readonly "avatar.consentCancel": "Not now";
229
+ readonly "avatar.consentConfirm": "Turn on the avatar";
230
+ readonly "avatar.problemAlreadyRunning": "An avatar is already running for you.";
231
+ readonly "avatar.problemCapacity": "Avatars are busy right now. Try again in a moment.";
232
+ readonly "avatar.problemConnection": "The connection to the avatar was lost.";
233
+ readonly "avatar.problemNeedsHostedVoice": "The avatar needs one of the assistant’s own voices. Choose one in the voice settings.";
234
+ readonly "avatar.problemNotReady": "This avatar isn’t ready yet. Try another one.";
235
+ readonly "avatar.problemPaused": "Avatars are paused for now.";
236
+ readonly "avatar.problemPlaybackBlocked": "The browser blocked the avatar’s sound. Tap the avatar to turn it on.";
237
+ readonly "avatar.problemProvider": "The avatar had a problem. Voice continues without it.";
238
+ readonly "avatar.problemRateLimited": "You started avatars too quickly. Wait a minute and try again.";
239
+ readonly "avatar.problemUnavailable": "The avatar isn’t available here.";
240
+ readonly "avatar.problemUnsupportedBrowser": "This browser can’t show the avatar.";
241
+ readonly "avatar.stageLabel": "Live avatar";
242
+ readonly "avatar.stageWaiting": "Waiting for the avatar…";
243
+ readonly "avatar.stateEnding": "Stopping the avatar…";
244
+ readonly "avatar.stateInterrupted": "The avatar was interrupted. You can keep talking.";
245
+ readonly "avatar.stateOff": "Avatar off";
246
+ readonly "avatar.stateOn": "Avatar on";
247
+ readonly "avatar.stateReady": "Avatar ready";
248
+ readonly "avatar.stateStarting": "Starting the avatar…";
249
+ readonly "avatar.stateVoiceOnly": "The avatar is unavailable right now. Continuing with voice only.";
250
+ readonly "avatar.stop": "Turn off the avatar";
251
+ readonly "avatar.toggle": "Live avatar";
252
+ readonly "avatar.toggleHint": "Show the assistant as a speaking avatar during voice conversations.";
253
+ readonly "avatar.voiceModeHint": "The recommended voice is chosen for this avatar and the language of each reply.";
254
+ readonly "avatar.voiceModeLabel": "Avatar voice";
255
+ readonly "avatar.voiceModeMine": "Keep my current voice";
256
+ readonly "avatar.voiceModePaired": "Use this avatar's recommended voice";
219
257
  readonly "chatArea.placeholderActions": "What would you like me to do?";
220
258
  readonly "chatArea.placeholderGeneral": "Ask me anything…";
221
259
  readonly "chatInput.ariaLabel": "Message input";
@@ -110,6 +110,44 @@ var m = {
110
110
  "attachments.unsupportedImage": "This device can't read that image format, so it can't be resized to send. Save it as JPEG and try again.",
111
111
  "attachments.uploading": "Uploading…",
112
112
  "attachments.voiceDropped": "{{count}} attachment(s) couldn't be saved and weren't sent.",
113
+ "avatar.chooserEmpty": "No avatars are available.",
114
+ "avatar.chooserLabel": "Avatar",
115
+ "avatar.chooserLoading": "Loading avatars…",
116
+ "avatar.chooserNone": "No avatar chosen",
117
+ "avatar.chooserOption": "Use {{name}}",
118
+ "avatar.chooserRetry": "Try again",
119
+ "avatar.chooserSelected": "{{name}} (selected)",
120
+ "avatar.chooserTitle": "Choose an avatar",
121
+ "avatar.consent": "Turning on Avatar shares the assistant's reply audio with LiveKit and Protoface to animate the avatar. This feature does not send your microphone audio to Protoface.",
122
+ "avatar.consentCancel": "Not now",
123
+ "avatar.consentConfirm": "Turn on the avatar",
124
+ "avatar.problemAlreadyRunning": "An avatar is already running for you.",
125
+ "avatar.problemCapacity": "Avatars are busy right now. Try again in a moment.",
126
+ "avatar.problemConnection": "The connection to the avatar was lost.",
127
+ "avatar.problemNeedsHostedVoice": "The avatar needs one of the assistant’s own voices. Choose one in the voice settings.",
128
+ "avatar.problemNotReady": "This avatar isn’t ready yet. Try another one.",
129
+ "avatar.problemPaused": "Avatars are paused for now.",
130
+ "avatar.problemPlaybackBlocked": "The browser blocked the avatar’s sound. Tap the avatar to turn it on.",
131
+ "avatar.problemProvider": "The avatar had a problem. Voice continues without it.",
132
+ "avatar.problemRateLimited": "You started avatars too quickly. Wait a minute and try again.",
133
+ "avatar.problemUnavailable": "The avatar isn’t available here.",
134
+ "avatar.problemUnsupportedBrowser": "This browser can’t show the avatar.",
135
+ "avatar.stageLabel": "Live avatar",
136
+ "avatar.stageWaiting": "Waiting for the avatar…",
137
+ "avatar.stateEnding": "Stopping the avatar…",
138
+ "avatar.stateInterrupted": "The avatar was interrupted. You can keep talking.",
139
+ "avatar.stateOff": "Avatar off",
140
+ "avatar.stateOn": "Avatar on",
141
+ "avatar.stateReady": "Avatar ready",
142
+ "avatar.stateStarting": "Starting the avatar…",
143
+ "avatar.stateVoiceOnly": "The avatar is unavailable right now. Continuing with voice only.",
144
+ "avatar.stop": "Turn off the avatar",
145
+ "avatar.toggle": "Live avatar",
146
+ "avatar.toggleHint": "Show the assistant as a speaking avatar during voice conversations.",
147
+ "avatar.voiceModeHint": "The recommended voice is chosen for this avatar and the language of each reply.",
148
+ "avatar.voiceModeLabel": "Avatar voice",
149
+ "avatar.voiceModeMine": "Keep my current voice",
150
+ "avatar.voiceModePaired": "Use this avatar's recommended voice",
113
151
  "chatArea.placeholderActions": "What would you like me to do?",
114
152
  "chatArea.placeholderGeneral": "Ask me anything…",
115
153
  "chatInput.ariaLabel": "Message input",
@@ -95,6 +95,7 @@ export * from './adminModeSendGate';
95
95
  export * from './attachmentTurn';
96
96
  export * from './writeModeConsent';
97
97
  export * from './voice/speechLabels';
98
+ export * from './avatar/useAvatarCopy';
98
99
  export * from './voice/replyLanguage';
99
100
  export * from './voice/speechPlayback';
100
101
  export * from './conversation';
@@ -11,14 +11,14 @@ import { hasAssistantPreviewSurface as l } from "../preview/types.js";
11
11
  import { ASSISTANT_OVERLAY_ATTR as u, ASSISTANT_PANEL_ATTR as d, activePanelOverlay as f, escapeDismisses as p, nextPanelOverlays as ee } from "./panelOverlays.js";
12
12
  import { panelOwnedPreviewBridge as te, useHostedAssistantPreviewBridge as ne } from "../preview/bridgeHostContext.js";
13
13
  import { AssistantPreviewOverlay as re } from "../preview/AssistantPreviewOverlay.js";
14
- import { AssistantHeader as ie } from "./AssistantHeader.js";
15
- import { AssistantConversationList as ae } from "./AssistantConversationList.js";
16
- import { AssistantConversationDeleteDialogProvider as m } from "./conversationDeleteDialog.js";
17
- import { AssistantControlConfirmProvider as h } from "./controlConfirmDialog.js";
18
- import { AssistantLeaveConfirmNavigateProvider as g } from "./leaveConfirm.js";
19
- import { useAssistantServerHistory as _ } from "../history/useAssistantServerHistory.js";
20
- import { useFilesServiceWarmup as v } from "../../../storage/warmFilesService.js";
21
- import { LiveAvatarSeamProvider as y, useLiveAvatarSpeechEngine as b } from "./assistantLiveAvatarSeam.js";
14
+ import { LiveAvatarSeamProvider as ie, useLiveAvatarSpeechEngine as ae } from "./assistantLiveAvatarSeam.js";
15
+ import { AssistantHeader as m } from "./AssistantHeader.js";
16
+ import { AssistantConversationList as h } from "./AssistantConversationList.js";
17
+ import { AssistantConversationDeleteDialogProvider as g } from "./conversationDeleteDialog.js";
18
+ import { AssistantControlConfirmProvider as _ } from "./controlConfirmDialog.js";
19
+ import { AssistantLeaveConfirmNavigateProvider as v } from "./leaveConfirm.js";
20
+ import { useAssistantServerHistory as y } from "../history/useAssistantServerHistory.js";
21
+ import { useFilesServiceWarmup as b } from "../../../storage/warmFilesService.js";
22
22
  import { useAssistantHeartbeat as x } from "../useAssistantHeartbeat.js";
23
23
  import { useAssistantKeyboardShortcuts as S } from "../useAssistantKeyboardShortcuts.js";
24
24
  import { AssistantChatArea as C } from "./AssistantChatArea.js";
@@ -26,8 +26,8 @@ import { useMemo as w, useRef as T, useState as E } from "react";
26
26
  import { jsx as D, jsxs as O } from "react/jsx-runtime";
27
27
  //#region src/shared/assistant/ui/AIAssistantPanel.tsx
28
28
  var k = ({ onClose: k, visible: A, docked: j, onToggleDock: M, writeAccess: N, onNavigate: oe, isShellLessTarget: P, connectSourceHref: F, onConnectSource: I, preview: L, liveAvatar: R, serverHistory: z = !1 }) => {
29
- let B = r(), V = b(R);
30
- _(z);
29
+ let B = r(), V = ae(R);
30
+ y(z);
31
31
  let [H, U] = E(!1), W = l(L);
32
32
  H && !W && U(!1);
33
33
  let G = H && W;
@@ -54,7 +54,7 @@ var k = ({ onClose: k, visible: A, docked: j, onToggleDock: M, writeAccess: N, o
54
54
  Y
55
55
  ]));
56
56
  let $ = q.find((e) => e.id === se);
57
- x($?.backendSessionId ?? null, $?.mode ?? null, A ?? !0), v(A ?? !0);
57
+ x($?.backendSessionId ?? null, $?.mode ?? null, A ?? !0), b(A ?? !0);
58
58
  let le = n(A !== !1);
59
59
  return S({
60
60
  panelRef: K,
@@ -71,17 +71,17 @@ var k = ({ onClose: k, visible: A, docked: j, onToggleDock: M, writeAccess: N, o
71
71
  }), /* @__PURE__ */ D(a, {
72
72
  visible: A ?? !0,
73
73
  engine: V,
74
- children: /* @__PURE__ */ D(y, {
74
+ children: /* @__PURE__ */ D(ie, {
75
75
  value: R,
76
76
  children: /* @__PURE__ */ D(o, {
77
77
  value: L,
78
- children: /* @__PURE__ */ D(g, {
78
+ children: /* @__PURE__ */ D(v, {
79
79
  navigate: oe,
80
80
  isShellLessTarget: P,
81
81
  children: /* @__PURE__ */ D(c, {
82
82
  value: F,
83
83
  onOpen: I,
84
- children: /* @__PURE__ */ D(m, { children: /* @__PURE__ */ D(h, { children: /* @__PURE__ */ O("div", {
84
+ children: /* @__PURE__ */ D(g, { children: /* @__PURE__ */ D(_, { children: /* @__PURE__ */ O("div", {
85
85
  ref: K,
86
86
  className: "relative flex h-full w-full flex-col bg-bg-surface",
87
87
  role: "complementary",
@@ -94,7 +94,7 @@ var k = ({ onClose: k, visible: A, docked: j, onToggleDock: M, writeAccess: N, o
94
94
  /* @__PURE__ */ D("div", {
95
95
  className: "contents",
96
96
  inert: X || void 0,
97
- children: /* @__PURE__ */ D(ie, {
97
+ children: /* @__PURE__ */ D(m, {
98
98
  contextLabel: le,
99
99
  onNewConversation: () => ce(),
100
100
  onOpenHistory: () => Q("history"),
@@ -106,7 +106,7 @@ var k = ({ onClose: k, visible: A, docked: j, onToggleDock: M, writeAccess: N, o
106
106
  onToggleDock: M
107
107
  })
108
108
  }),
109
- J && /* @__PURE__ */ D(ae, {}),
109
+ J && /* @__PURE__ */ D(h, {}),
110
110
  G && /* @__PURE__ */ D(re, { onClose: () => U(!1) }),
111
111
  /* @__PURE__ */ D("div", {
112
112
  className: "contents",
@@ -1,5 +1,6 @@
1
1
  import { ComponentType, FC, ReactNode } from 'react';
2
2
  import { SpeechEngine } from '../speech/types';
3
+ import { VoiceView } from './voice/AssistantVoiceSession';
3
4
  export interface LiveAvatarStageProps {
4
5
  /** Full-screen (true) or the in-panel stage (false). */
5
6
  expanded: boolean;
@@ -11,6 +12,42 @@ export interface AssistantLiveAvatarSeam {
11
12
  decorateSpeechEngine: (base: SpeechEngine) => SpeechEngine;
12
13
  /** Drawn by the voice stage instead of the waveform. Optional. */
13
14
  Stage?: ComponentType<LiveAvatarStageProps>;
15
+ /**
16
+ * The avatar's ON switch. Optional. It is SHOWN only when `Stage` is given,
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.
19
+ */
20
+ enabled?: LiveAvatarEnabledStore;
21
+ }
22
+ /**
23
+ * The avatar's ON flag as a store, read with `useSyncExternalStore`: the voice
24
+ * session subscribes and nothing above it re-renders. It must be STABLE, and
25
+ * the flag it reads must be session-only, never persisted (the avatar
26
+ * preferences' `avatarEnabled`), so every page load starts OFF.
27
+ *
28
+ * ★★ ON is only ever the person's own click, on a control the host offers
29
+ * only while `voiceLive` says a voice conversation is live, and a paid session
30
+ * starts only then: no avatar after its conversation has ended. The panel
31
+ * switches the avatar OFF, never ON.
32
+ */
33
+ export interface LiveAvatarEnabledStore {
34
+ subscribe: (onChange: () => void) => () => void;
35
+ getSnapshot: () => boolean;
36
+ /**
37
+ * Switch the avatar OFF with the SAME action the host's own Avatar control
38
+ * takes, so its paid session ends along the one path the host already has.
39
+ * Called at the end of EVERY voice conversation, even when the avatar is
40
+ * already OFF; again for any ON while a failed one is still on screen; and
41
+ * 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.
43
+ */
44
+ switchOff: () => void;
45
+ /**
46
+ * Told `true` once a voice conversation is live and `false` when it is over
47
+ * (just before that end's `switchOff`). Outside that window the host offers
48
+ * no ON control and starts no paid session. Idempotent, as `switchOff`.
49
+ */
50
+ voiceLive: (live: boolean) => void;
14
51
  }
15
52
  export declare const LiveAvatarSeamProvider: FC<{
16
53
  value?: AssistantLiveAvatarSeam;
@@ -26,3 +63,83 @@ export declare function useLiveAvatarStage(): ComponentType<LiveAvatarStageProps
26
63
  * is to the store `AssistantSpeechProvider` already subscribes to.
27
64
  */
28
65
  export declare function useLiveAvatarSpeechEngine(seam: AssistantLiveAvatarSeam | undefined): SpeechEngine | undefined;
66
+ /**
67
+ * The voice view to SHOW, given the one the user CHOSE; and the avatar kept to
68
+ * the live conversation (`ended`: over, but still on screen to say why it
69
+ * failed; leaving the screen ends it too).
70
+ *
71
+ * ★★ Switching the avatar ON must show it, and every session opens in the
72
+ * compact view (`mini`), which draws no stage. So while the avatar is ON, a
73
+ * session the user left compact shows the in-panel stage: the same session,
74
+ * re-rendered with the stage the size button already leads to, so nothing above
75
+ * the stage remounts. Switched OFF, it shows their choice again.
76
+ *
77
+ * ★★ The user's choice is never written, and nothing is stored: the next session
78
+ * opens compact as before, with the avatar OFF.
79
+ *
80
+ * ★ The moment they choose a view themselves while it is held (the size button,
81
+ * the change chip, an attachment), that view is theirs and stays when the
82
+ * avatar goes off. A conversation that is over lets go at once too: its
83
+ * failure is explained in the user's view, not first on a stage going away.
84
+ *
85
+ * ★ Adjusted DURING RENDER, as `AIAssistantPanel` adjusts its preview flag: no
86
+ * frame shows the stale view, and the update is conditional on the state it
87
+ * replaces, so it cannot loop.
88
+ */
89
+ export declare function useLiveAvatarVoiceView(chosen: VoiceView, ended: boolean): VoiceView;
90
+ /**
91
+ * @internal Exported for `VoiceMiniController` alone; not part of the module's
92
+ * public surface (the assistant barrel star-exports this module).
93
+ *
94
+ * Whether the compact strip shows `LiveAvatarMiniIndicator`: an avatar that is
95
+ * ON and can be shown. `VoiceMiniController` keeps its bottom band for the
96
+ * chip then, and ONLY then; false, it adds nothing and is the strip it was.
97
+ */
98
+ export declare function useLiveAvatarMiniIndicatorShown(): boolean;
99
+ /**
100
+ * The compact strip's sign that a live avatar is ON, with its stop.
101
+ *
102
+ * ★★ The strip draws no stage, so a person who chose it while the avatar is on
103
+ * would not see that a session, billed by the minute, is still running.
104
+ * Without an avatar that is ON and can be shown it renders nothing, and the
105
+ * strip is byte for byte what it was.
106
+ *
107
+ * ★★ Stop switches the AVATAR off through `switchOff`, the action the host's
108
+ * own Avatar control takes, called with NO arguments (its contract is
109
+ * `() => void`, so a host's own optional parameter never receives the click
110
+ * event). It never ends the voice conversation and never touches audio: the
111
+ * speech engine stays the only speaker. Focus on it goes to the session's
112
+ * microphone control (End while that is disabled), never to <body>, whoever
113
+ * switched the avatar off; to End when the chip goes with its strip.
114
+ *
115
+ * ★★ It handles no keys: Escape and Tab stay the session's (BOFF-8065). It
116
+ * comes after the microphone control, so Tab meets the strip's own control
117
+ * first.
118
+ *
119
+ * ★ "Avatar on" is a state, not a label. The control is an icon named by
120
+ * `avatar.stop`, so no button is named for the opposite of what it does, and
121
+ * its name is all of its words (WCAG 2.5.3, as the session's End).
122
+ *
123
+ * ★★ In the strip's bottom band, which the strip keeps free while this shows
124
+ * (`VoiceMiniController`: its caption column stops above the band, and shows
125
+ * fewer caption lines when two notices leave no room for three), so nothing
126
+ * on the strip overlaps it. Positioned against the voice session as its
127
+ * corner controls are, under the caption column: the session's `px-3` plus the
128
+ * strip's `px-4`, physical `left-*` as `VoiceStatusLine` explains, mirrored
129
+ * under `rtl:` so it never sits under the microphone. Its padding is logical
130
+ * (`ps-*`/`pe-*`, as `AssistantUiOfferLink`'s), so the label keeps 10px from
131
+ * the rounded start and the stop 2px from the end in RTL too. No wider than
132
+ * that column plus 6px of the gap after it (`max-w-*`: 100% less 3.5rem of
133
+ * paddings, the 0.75rem gap and the microphone's 10rem `w-40`, plus 0.375rem),
134
+ * so its stop's target (below) ends at least 6px before the microphone, in
135
+ * either direction, however long a translation of "Avatar on" is.
136
+ *
137
+ * ★ As tall as End in the strip (`h-8`), its stop drawn at 28px; the stop's
138
+ * TARGET is 44px square (`after:-inset-2`, WCAG 2.5.5), and `bottom-1.5` ends
139
+ * that target at the strip's own edge. It keeps a logical margin of 6px at its
140
+ * end (`after:me-1.5`), taken back at its start (`after:-ms-1.5`) so it stays
141
+ * 44px: it ends 2px past the drawn stop, short of the chip's edge, so a tap at
142
+ * the microphone's edge never lands on the stop. Logical, as the padding: it
143
+ * mirrors in RTL.
144
+ */
145
+ export declare const LiveAvatarMiniIndicator: FC;
@@ -1,17 +1,105 @@
1
1
  import { getSpeechEngine as e, subscribeSpeechEngine as t } from "../speech/createSpeechEngine.js";
2
- import { createContext as n, useContext as r, useMemo as i, useSyncExternalStore as a } from "react";
3
- import { jsx as o } from "react/jsx-runtime";
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";
4
7
  //#region src/shared/assistant/ui/assistantLiveAvatarSeam.tsx
5
- var s = n(null), c = ({ value: e, children: t }) => /* @__PURE__ */ o(s.Provider, {
8
+ var h = i(null), g = ({ value: e, children: t }) => /* @__PURE__ */ p(h.Provider, {
6
9
  value: e ?? null,
7
10
  children: t
8
11
  });
9
- function l() {
10
- return r(s)?.Stage ?? null;
12
+ function _() {
13
+ return a(h)?.Stage ?? null;
11
14
  }
12
- function u(n) {
13
- let r = a(t, e, e), o = n?.decorateSpeechEngine;
14
- return i(() => o ? o(r) : void 0, [o, r]);
15
+ function v(n) {
16
+ let r = d(t, e, e), i = n?.decorateSpeechEngine;
17
+ return c(() => i ? i(r) : void 0, [i, r]);
15
18
  }
19
+ var y = {
20
+ subscribe: () => () => {},
21
+ getSnapshot: () => !1,
22
+ switchOff: () => {},
23
+ voiceLive: () => {}
24
+ };
25
+ function b(e) {
26
+ return e?.Stage ? e.enabled : void 0;
27
+ }
28
+ function x(e) {
29
+ let t = e ?? y;
30
+ return d(t.subscribe, t.getSnapshot, t.getSnapshot);
31
+ }
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,
37
+ chosen: e,
38
+ open: i && e === "mini"
39
+ }));
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,
44
+ chosen: e,
45
+ open: c
46
+ })), c && !t ? "panel" : e;
47
+ }
48
+ function C(e, t) {
49
+ let n = l(0);
50
+ o(() => {
51
+ 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();
56
+ });
57
+ if (!t) return i(() => e.voiceLive(!0)), a;
58
+ a();
59
+ let o = e.getSnapshot();
60
+ return e.subscribe(() => {
61
+ let t = o;
62
+ o = e.getSnapshot(), o && !t && i(() => e.switchOff());
63
+ });
64
+ }, [e, t]);
65
+ }
66
+ function w() {
67
+ return x(b(a(h)));
68
+ }
69
+ var T = () => {
70
+ let e = b(a(h)), t = x(e);
71
+ return e && t ? /* @__PURE__ */ p(O, {
72
+ onStop: e.switchOff,
73
+ isOn: e.getSnapshot
74
+ }) : 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(() => {
78
+ let e = a.current, t = o.current;
79
+ return () => {
80
+ 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();
83
+ };
84
+ }, []), /* @__PURE__ */ m("div", {
85
+ ref: a,
86
+ 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
+ "data-testid": "assistant-voice-avatar",
88
+ children: [/* @__PURE__ */ p("span", {
89
+ className: "truncate",
90
+ children: i.stateOn
91
+ }), /* @__PURE__ */ p("button", {
92
+ type: "button",
93
+ onClick: () => e(),
94
+ 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,
96
+ "data-testid": "assistant-voice-avatar-stop",
97
+ children: /* @__PURE__ */ p(f, {
98
+ className: "size-4",
99
+ "aria-hidden": "true"
100
+ })
101
+ })]
102
+ });
103
+ };
16
104
  //#endregion
17
- export { c as LiveAvatarSeamProvider, u as useLiveAvatarSpeechEngine, l as useLiveAvatarStage };
105
+ export { T as LiveAvatarMiniIndicator, g as LiveAvatarSeamProvider, w as useLiveAvatarMiniIndicatorShown, v as useLiveAvatarSpeechEngine, _ as useLiveAvatarStage, S as useLiveAvatarVoiceView };