@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.
- package/dist/shared/assistant/avatar/useAvatarCopy.d.ts +51 -0
- package/dist/shared/assistant/avatar/useAvatarCopy.js +52 -0
- package/dist/shared/assistant/i18nManifest.d.ts +38 -0
- package/dist/shared/assistant/i18nManifest.js +38 -0
- package/dist/shared/assistant/index.d.ts +1 -0
- package/dist/shared/assistant/ui/AIAssistantPanel.js +16 -16
- package/dist/shared/assistant/ui/assistantLiveAvatarSeam.d.ts +117 -0
- package/dist/shared/assistant/ui/assistantLiveAvatarSeam.js +97 -9
- package/dist/shared/assistant/ui/voice/AssistantVoiceSession.js +135 -134
- package/dist/shared/assistant/ui/voice/VoiceMiniController.js +54 -49
- package/dist/shared/assistant/ui/voice/VoiceStage.js +39 -35
- package/dist/shared-assistant.js +188 -187
- package/package.json +1 -1
|
@@ -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 {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
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 =
|
|
30
|
-
|
|
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),
|
|
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(
|
|
74
|
+
children: /* @__PURE__ */ D(ie, {
|
|
75
75
|
value: R,
|
|
76
76
|
children: /* @__PURE__ */ D(o, {
|
|
77
77
|
value: L,
|
|
78
|
-
children: /* @__PURE__ */ D(
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
3
|
-
import {
|
|
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
|
|
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
|
|
10
|
-
return
|
|
12
|
+
function _() {
|
|
13
|
+
return a(h)?.Stage ?? null;
|
|
11
14
|
}
|
|
12
|
-
function
|
|
13
|
-
let r =
|
|
14
|
-
return
|
|
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 {
|
|
105
|
+
export { T as LiveAvatarMiniIndicator, g as LiveAvatarSeamProvider, w as useLiveAvatarMiniIndicatorShown, v as useLiveAvatarSpeechEngine, _ as useLiveAvatarStage, S as useLiveAvatarVoiceView };
|