@burdenoff/fe-libs 2026.1016.2 → 2026.1016.3

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 (41) hide show
  1. package/dist/shared/assistant/avatar/live/AvatarFace.d.ts +13 -0
  2. package/dist/shared/assistant/avatar/live/AvatarFace.js +46 -0
  3. package/dist/shared/assistant/avatar/live/LiveAvatarTabView.d.ts +2 -0
  4. package/dist/shared/assistant/avatar/live/LiveAvatarTabView.js +172 -145
  5. package/dist/shared/assistant/avatar/live/avatarFaces.d.ts +31 -0
  6. package/dist/shared/assistant/avatar/live/avatarFaces.js +33 -0
  7. package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.d.ts +12 -0
  8. package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.js +142 -75
  9. package/dist/shared/assistant/avatar/live/liveAvatarController.d.ts +5 -0
  10. package/dist/shared/assistant/avatar/live/liveAvatarController.js +3 -1
  11. package/dist/shared/assistant/avatar/live/liveAvatarStatus.d.ts +14 -0
  12. package/dist/shared/assistant/avatar/live/liveAvatarStatus.js +11 -2
  13. package/dist/shared/assistant/avatar/useAvatarCopy.d.ts +42 -0
  14. package/dist/shared/assistant/avatar/useAvatarCopy.js +113 -75
  15. package/dist/shared/assistant/i18nManifest.d.ts +28 -0
  16. package/dist/shared/assistant/i18nManifest.js +28 -0
  17. package/dist/shared/assistant/index.d.ts +8 -0
  18. package/dist/shared/assistant/speech/replySpeechRequests.d.ts +84 -0
  19. package/dist/shared/assistant/speech/replySpeechRequests.js +70 -0
  20. package/dist/shared/assistant/speech/routingSpeechEngine.js +2 -1
  21. package/dist/shared/assistant/speech/types.d.ts +28 -1
  22. package/dist/shared/assistant/ui/AIAssistantPanel.js +128 -119
  23. package/dist/shared/assistant/ui/AssistantChatArea.js +1572 -1430
  24. package/dist/shared/assistant/ui/AssistantSpeechProvider.js +235 -191
  25. package/dist/shared/assistant/ui/assistantLiveAvatarSeam.d.ts +40 -0
  26. package/dist/shared/assistant/ui/avatar/AssistantAvatarTab.d.ts +6 -0
  27. package/dist/shared/assistant/ui/avatar/AssistantAvatarTab.js +99 -52
  28. package/dist/shared/assistant/ui/avatar/AvatarTabAskBox.d.ts +8 -0
  29. package/dist/shared/assistant/ui/avatar/AvatarTabAskBox.js +87 -0
  30. package/dist/shared/assistant/ui/avatar/AvatarTabLastExchange.d.ts +13 -0
  31. package/dist/shared/assistant/ui/avatar/AvatarTabLastExchange.js +97 -0
  32. package/dist/shared/assistant/ui/avatar/avatarAskGate.d.ts +104 -0
  33. package/dist/shared/assistant/ui/avatar/avatarAskGate.js +39 -0
  34. package/dist/shared/assistant/ui/avatar/avatarTabDraft.d.ts +17 -0
  35. package/dist/shared/assistant/ui/avatar/avatarTabDraft.js +22 -0
  36. package/dist/shared/assistant/ui/avatar/avatarTabExchange.d.ts +47 -0
  37. package/dist/shared/assistant/ui/avatar/avatarTabExchange.js +50 -0
  38. package/dist/shared/assistant/ui/panelOverlays.d.ts +8 -1
  39. package/dist/shared/assistant/ui/panelOverlays.js +1 -1
  40. package/dist/shared-assistant.js +260 -252
  41. package/package.json +1 -1
@@ -1,99 +1,137 @@
1
1
  import { useI18n as e } from "../../providers/shell/I18nProvider.js";
2
2
  import { useAssistantTr as t } from "../i18n.js";
3
- import { useMemo as n } from "react";
3
+ import { VOICE_THINKING_LABEL as n } from "../voice/voiceStatus.js";
4
+ import { useMemo as r } from "react";
4
5
  //#region src/shared/assistant/avatar/useAvatarCopy.ts
5
- var r = {
6
+ var i = {
6
7
  hour: "numeric",
7
8
  minute: "2-digit"
8
- }, i = {
9
+ }, a = {
9
10
  month: "short",
10
11
  day: "numeric",
11
12
  hour: "numeric",
12
13
  minute: "2-digit"
13
14
  };
14
- function a(e, t, n) {
15
- let a = new Date(e).toDateString() === new Date(t).toDateString() ? r : i;
15
+ function o(e, t, n) {
16
+ let r = new Date(e).toDateString() === new Date(t).toDateString() ? i : a;
16
17
  try {
17
- return new Intl.DateTimeFormat(n, a).format(e);
18
+ return new Intl.DateTimeFormat(n, r).format(e);
18
19
  } catch {
19
- return new Intl.DateTimeFormat("en", a).format(e);
20
+ return new Intl.DateTimeFormat("en", r).format(e);
20
21
  }
21
22
  }
22
- function o() {
23
- let r = t(), { locale: i } = e();
24
- return n(() => ({
25
- toggle: r("avatar.toggle", "Live avatar"),
26
- toggleHint: r("avatar.toggleHint", "Show the assistant as a speaking avatar during voice conversations."),
27
- stop: r("avatar.stop", "Turn off the avatar"),
28
- consent: r("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."),
29
- consentConfirm: r("avatar.consentConfirm", "Turn on the avatar"),
30
- consentCancel: r("avatar.consentCancel", "Not now"),
31
- stateOff: r("avatar.stateOff", "Avatar off"),
32
- stateOn: r("avatar.stateOn", "Avatar on"),
33
- stateStarting: r("avatar.stateStarting", "Starting the avatar…"),
34
- stateReady: r("avatar.stateReady", "Avatar ready"),
35
- stateEnding: r("avatar.stateEnding", "Stopping the avatar…"),
36
- stateInterrupted: r("avatar.stateInterrupted", "The avatar was interrupted. You can keep talking."),
37
- stateVoiceOnly: r("avatar.stateVoiceOnly", "The avatar is unavailable right now. Continuing with voice only."),
38
- stateTimeLimit: r("avatar.stateTimeLimit", "The avatar reached its time limit. Continuing with voice only."),
39
- stageLabel: r("avatar.stageLabel", "Live avatar"),
40
- stageWaiting: r("avatar.stageWaiting", "Waiting for the avatar…"),
41
- chooserTitle: r("avatar.chooserTitle", "Choose an avatar"),
42
- chooserLabel: r("avatar.chooserLabel", "Avatar"),
43
- chooserNone: r("avatar.chooserNone", "No avatar chosen"),
44
- chooserLoading: r("avatar.chooserLoading", "Loading avatars…"),
45
- chooserEmpty: r("avatar.chooserEmpty", "No avatars are available."),
46
- chooserRetry: r("avatar.chooserRetry", "Try again"),
47
- chooserOption: (e) => r("avatar.chooserOption", "Use {{name}}", { name: e }),
48
- chooserSelected: (e) => r("avatar.chooserSelected", "{{name}} (selected)", { name: e }),
49
- voiceModeLabel: r("avatar.voiceModeLabel", "Avatar voice"),
50
- voiceModeDefaultNamed: (e) => r("avatar.voiceModeDefaultNamed", "Default for this avatar ({{name}})", { name: e }),
51
- voiceModeMine: r("avatar.voiceModeMine", "Keep my current voice"),
52
- voiceModeMineNamed: (e) => r("avatar.voiceModeMineNamed", "Keep my current voice ({{name}})", { name: e }),
53
- voiceModeHint: r("avatar.voiceModeNextReplyHint", "Used from the avatar's next reply. The assistant's own voice doesn't change."),
54
- tabsLabel: r("avatar.tabsLabel", "Assistant views"),
55
- tabChat: r("avatar.tabChat", "Chat"),
56
- tabAvatar: r("avatar.tabAvatar", "Avatar"),
57
- chooserLocked: r("avatar.chooserLocked", "Turn off the avatar to change it."),
58
- tabLeaveHint: r("avatar.tabLeaveHint", "Leaving the Avatar tab turns the avatar off."),
59
- availableAgain: (e) => r("avatar.availableAgain", "You can start it again at {{time}}.", { time: e }),
60
- retryTime: (e, t) => a(e, t, i),
61
- stateTimeLimitNoVoice: r("avatar.stateTimeLimitNoVoice", "The avatar reached its time limit."),
62
- stateVoiceOnlyNoVoice: r("avatar.stateVoiceOnlyNoVoice", "The avatar is unavailable right now."),
63
- turnOnStarting: r("avatar.turnOnStarting", "Starting the avatar…"),
64
- startVoice: r("voiceSession.start", "Start a voice conversation"),
65
- hearVoice: r("voice.preview", "Hear this voice"),
23
+ function s() {
24
+ let i = t(), { locale: a } = e();
25
+ return r(() => ({
26
+ toggle: i("avatar.toggle", "Live avatar"),
27
+ toggleHint: i("avatar.toggleHint", "Show the assistant as a speaking avatar during voice conversations."),
28
+ stop: i("avatar.stop", "Turn off the avatar"),
29
+ consent: i("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."),
30
+ consentConfirm: i("avatar.consentConfirm", "Turn on the avatar"),
31
+ consentCancel: i("avatar.consentCancel", "Not now"),
32
+ stateOff: i("avatar.stateOff", "Avatar off"),
33
+ stateOn: i("avatar.stateOn", "Avatar on"),
34
+ stateStarting: i("avatar.stateStarting", "Starting the avatar…"),
35
+ stateReady: i("avatar.stateReady", "Avatar ready"),
36
+ stateEnding: i("avatar.stateEnding", "Stopping the avatar…"),
37
+ stateInterrupted: i("avatar.stateInterrupted", "The avatar was interrupted. You can keep talking."),
38
+ stateVoiceOnly: i("avatar.stateVoiceOnly", "The avatar is unavailable right now. Continuing with voice only."),
39
+ stateTimeLimit: i("avatar.stateTimeLimit", "The avatar reached its time limit. Continuing with voice only."),
40
+ stageLabel: i("avatar.stageLabel", "Live avatar"),
41
+ stageWaiting: i("avatar.stageWaiting", "Waiting for the avatar…"),
42
+ chooserTitle: i("avatar.chooserTitle", "Choose an avatar"),
43
+ chooserLabel: i("avatar.chooserLabel", "Avatar"),
44
+ chooserNone: i("avatar.chooserNone", "No avatar chosen"),
45
+ chooserLoading: i("avatar.chooserLoading", "Loading avatars…"),
46
+ chooserEmpty: i("avatar.chooserEmpty", "No avatars are available."),
47
+ chooserRetry: i("avatar.chooserRetry", "Try again"),
48
+ chooserOption: (e) => i("avatar.chooserOption", "Use {{name}}", { name: e }),
49
+ chooserSelected: (e) => i("avatar.chooserSelected", "{{name}} (selected)", { name: e }),
50
+ voiceModeLabel: i("avatar.voiceModeLabel", "Avatar voice"),
51
+ voiceModeDefaultNamed: (e) => i("avatar.voiceModeDefaultNamed", "Default for this avatar ({{name}})", { name: e }),
52
+ voiceModeMine: i("avatar.voiceModeMine", "Keep my current voice"),
53
+ voiceModeMineNamed: (e) => i("avatar.voiceModeMineNamed", "Keep my current voice ({{name}})", { name: e }),
54
+ voiceModeHint: i("avatar.voiceModeNextReplyHint", "Used from the avatar's next reply. The assistant's own voice doesn't change."),
55
+ tabsLabel: i("avatar.tabsLabel", "Assistant views"),
56
+ tabChat: i("avatar.tabChat", "Chat"),
57
+ tabAvatar: i("avatar.tabAvatar", "Avatar"),
58
+ chooserLocked: i("avatar.chooserLocked", "Turn off the avatar to change it."),
59
+ tabLeaveHint: i("avatar.tabLeaveHint", "Leaving the Avatar tab turns the avatar off."),
60
+ availableAgain: (e) => i("avatar.availableAgain", "You can start it again at {{time}}.", { time: e }),
61
+ retryTime: (e, t) => o(e, t, a),
62
+ stateTimeLimitNoVoice: i("avatar.stateTimeLimitNoVoice", "The avatar reached its time limit."),
63
+ stateVoiceOnlyNoVoice: i("avatar.stateVoiceOnlyNoVoice", "The avatar is unavailable right now."),
64
+ turnOnStarting: i("avatar.turnOnStarting", "Starting the avatar…"),
65
+ startVoice: i("voiceSession.start", "Start a voice conversation"),
66
+ hearVoice: i("voice.preview", "Hear this voice"),
66
67
  voiceStartBlocked: (e) => {
67
68
  switch (e) {
68
- case "unavailable": return r("avatar.voiceStartUnavailable", "Voice conversations aren’t available here.");
69
- case "mode": return r("avatar.voiceStartMode", "Voice conversations aren’t available in this mode.");
70
- case "read-only": return r("avatar.voiceStartReadOnly", "This conversation can’t take new questions. Start a new conversation to ask by voice.");
71
- case "busy": return r("avatar.voiceStartBusy", "You can start a voice conversation when the assistant is ready.");
72
- case "committing": return r("avatar.voiceStartCommitting", "You can start a voice conversation once your attachments are saved.");
73
- case "dictating": return r("avatar.voiceStartDictating", "Stop dictating to start a voice conversation.");
69
+ case "unavailable": return i("avatar.voiceStartUnavailable", "Voice conversations aren’t available here.");
70
+ case "mode": return i("avatar.voiceStartMode", "Voice conversations aren’t available in this mode.");
71
+ case "read-only": return i("avatar.voiceStartReadOnly", "This conversation can’t take new questions. Start a new conversation to ask by voice.");
72
+ case "busy": return i("avatar.voiceStartBusy", "You can start a voice conversation when the assistant is ready.");
73
+ case "committing": return i("avatar.voiceStartCommitting", "You can start a voice conversation once your attachments are saved.");
74
+ case "dictating": return i("avatar.voiceStartDictating", "Stop dictating to start a voice conversation.");
74
75
  }
75
76
  },
76
- askNeedsVoice: r("avatar.askNeedsVoice", "You can still turn the avatar on, but you ask it questions in a voice conversation."),
77
- handsFreeOff: r("avatar.handsFreeOff", "Hands-free interrupt is off, so the avatar's voice can't interrupt itself. It stays off after you turn the avatar off; turn it back on in More options. You can still tap to interrupt."),
77
+ askNeedsVoice: i("avatar.askNeedsVoice", "You can still turn the avatar on, but you ask it questions in a voice conversation."),
78
+ askByTyping: i("avatar.askByTyping", "You can still turn the avatar on and type your questions below."),
79
+ askNothing: i("avatar.askNothing", "You can't ask the avatar anything in this conversation."),
80
+ askLabel: i("avatar.askLabel", "Type a question for the avatar"),
81
+ askPlaceholder: i("avatar.askPlaceholder", "Ask the avatar…"),
82
+ askHint: i("avatar.askHint", "Enter to send · Shift+Enter for a new line"),
83
+ askSend: i("chatInput.send", "Send message"),
84
+ askBlocked: (e) => {
85
+ switch (e) {
86
+ case "hidden-voice": return null;
87
+ case "read-only": return i("avatar.askReadOnly", "This conversation can't take new questions. Start a new conversation to ask one.");
88
+ case "mode-picked": return i("avatar.askModePicked", "Typed questions work only in Ask. You can change that in Chat, but leaving this tab turns the avatar off.");
89
+ case "mode-escalated": return i("avatar.askMode", "This conversation can make changes, so typed questions to the avatar aren't available here. Start a new conversation to ask one.");
90
+ case "avatar-off": return i("avatar.askNeedsAvatar", "Turn on the avatar to send your question. You can type it now.");
91
+ case "avatar-again": return i("avatar.askAgain", "You can send your question when the avatar is on again.");
92
+ case "sound": return i("avatar.askSound", "Turn on the avatar's sound first.");
93
+ case "starting": return i("avatar.askStarting", "You can send your question when the avatar is ready.");
94
+ case "busy": return i("avatar.askBusy", "You can send your question when the assistant is ready.");
95
+ case "committing": return i("avatar.askCommitting", "You can send your question once the files in Chat are saved.");
96
+ }
97
+ },
98
+ askNoFiles: i("avatar.askNoFiles", "To attach files, use Chat."),
99
+ askEmpty: i("avatar.askEmpty", "Type a question below, and the avatar answers it aloud."),
100
+ askExchangeLabel: i("avatar.askExchangeLabel", "Last question and answer"),
101
+ askYouAsked: i("avatar.askYouAsked", "You asked"),
102
+ askAnswer: i("avatar.askAnswer", "Answer"),
103
+ askAskedIn: (e) => i("avatar.askAskedIn", "Asked in: {{title}}", { title: e }),
104
+ askShowAll: i("avatar.askShowAll", "Show the whole answer"),
105
+ askShowLess: i("avatar.askShowLess", "Show less"),
106
+ askThinking: i(n.key, n.fallback),
107
+ askNotSpoken: i("avatar.askNotSpoken", "The avatar wasn't available, so this answer wasn't read aloud."),
108
+ askCut: i("avatar.askCut", "The avatar stopped before the end of this answer. The whole answer is in Chat."),
109
+ askNotHeard: i("avatar.askNotHeard", "This answer wasn't read aloud: the avatar may still have been finishing the last answer."),
110
+ askFailed: i("avatar.askFailed", "The assistant couldn't answer. Your question is back in the box."),
111
+ askFailedInChat: i("avatar.askFailedInChat", "The assistant couldn't answer. Your question is still in Chat."),
112
+ askAlsoInChat: i("avatar.askAlsoInChat", "This answer is also in Chat."),
113
+ askStop: i("message.stopSpeaking", "Stop reading aloud"),
114
+ askStopShort: i("message.stopSpeakingShort", "Stop"),
115
+ handsFreeOff: i("avatar.handsFreeOff", "Hands-free interrupt is off, so the avatar's voice can't interrupt itself. It stays off after you turn the avatar off; turn it back on in More options. You can still tap to interrupt."),
78
116
  problem: (e, t = {}) => {
79
- let n = t.voiceLive !== !1, i = t.timed === !0;
117
+ let n = t.voiceLive !== !1, r = t.timed === !0;
80
118
  switch (e) {
81
- case "unavailable": return r("avatar.problemUnavailable", "The avatar isn’t available here.");
82
- case "not-ready": return r("avatar.problemNotReady", "This avatar isn’t ready yet. Try another one.");
83
- case "already-running": return r("avatar.problemAlreadyRunning", "An avatar is already running for you.");
84
- case "capacity": return i ? r("avatar.problemCapacityTimed", "Avatars are busy right now.") : r("avatar.problemCapacity", "Avatars are busy right now. Try again in a moment.");
85
- case "rate-limited": return i ? r("avatar.problemRateLimitedTimed", "You started avatars too quickly.") : r("avatar.problemRateLimited", "You started avatars too quickly. Wait a minute and try again.");
86
- case "paused": return r("avatar.problemPaused", "Avatars are paused for now.");
87
- case "budget-exhausted": return r("avatar.problemBudgetExhausted", "Avatar time is used up for now.");
88
- case "start-timeout": return r("avatar.problemStartTimeout", "The avatar took too long to start. Try again in a moment.");
89
- case "provider": return n ? r("avatar.problemProvider", "The avatar had a problem. Voice continues without it.") : r("avatar.problemProviderNoVoice", "The avatar had a problem.");
90
- case "connection": return r("avatar.problemConnection", "The connection to the avatar was lost.");
91
- case "needs-hosted-voice": return r("avatar.problemNeedsHostedVoice", "The avatar needs one of the assistant’s own voices. Choose one in the voice settings.");
92
- case "unsupported-browser": return r("avatar.problemUnsupportedBrowser", "This browser can’t show the avatar.");
93
- case "playback-blocked": return r("avatar.problemPlaybackBlocked", "The browser blocked the avatar’s sound. Tap the avatar to turn it on.");
119
+ case "unavailable": return i("avatar.problemUnavailable", "The avatar isn’t available here.");
120
+ case "not-ready": return i("avatar.problemNotReady", "This avatar isn’t ready yet. Try another one.");
121
+ case "already-running": return i("avatar.problemAlreadyRunning", "An avatar is already running for you.");
122
+ case "capacity": return r ? i("avatar.problemCapacityTimed", "Avatars are busy right now.") : i("avatar.problemCapacity", "Avatars are busy right now. Try again in a moment.");
123
+ case "rate-limited": return r ? i("avatar.problemRateLimitedTimed", "You started avatars too quickly.") : i("avatar.problemRateLimited", "You started avatars too quickly. Wait a minute and try again.");
124
+ case "paused": return i("avatar.problemPaused", "Avatars are paused for now.");
125
+ case "budget-exhausted": return i("avatar.problemBudgetExhausted", "Avatar time is used up for now.");
126
+ case "start-timeout": return i("avatar.problemStartTimeout", "The avatar took too long to start. Try again in a moment.");
127
+ case "provider": return n ? i("avatar.problemProvider", "The avatar had a problem. Voice continues without it.") : i("avatar.problemProviderNoVoice", "The avatar had a problem.");
128
+ case "connection": return i("avatar.problemConnection", "The connection to the avatar was lost.");
129
+ case "needs-hosted-voice": return i("avatar.problemNeedsHostedVoice", "The avatar needs one of the assistant’s own voices. Choose one in the voice settings.");
130
+ case "unsupported-browser": return i("avatar.problemUnsupportedBrowser", "This browser can’t show the avatar.");
131
+ case "playback-blocked": return i("avatar.problemPlaybackBlocked", "The browser blocked the avatar’s sound. Tap the avatar to turn it on.");
94
132
  }
95
133
  }
96
- }), [r, i]);
134
+ }), [i, a]);
97
135
  }
98
136
  //#endregion
99
- export { a as formatAvatarRetryTime, o as useAvatarCopy };
137
+ export { o as formatAvatarRetryTime, s as useAvatarCopy };
@@ -216,7 +216,35 @@ 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.askAgain": "You can send your question when the avatar is on again.";
220
+ readonly "avatar.askAlsoInChat": "This answer is also in Chat.";
221
+ readonly "avatar.askAnswer": "Answer";
222
+ readonly "avatar.askAskedIn": "Asked in: {{title}}";
223
+ readonly "avatar.askBusy": "You can send your question when the assistant is ready.";
224
+ readonly "avatar.askByTyping": "You can still turn the avatar on and type your questions below.";
225
+ readonly "avatar.askCommitting": "You can send your question once the files in Chat are saved.";
226
+ readonly "avatar.askCut": "The avatar stopped before the end of this answer. The whole answer is in Chat.";
227
+ readonly "avatar.askEmpty": "Type a question below, and the avatar answers it aloud.";
228
+ readonly "avatar.askExchangeLabel": "Last question and answer";
229
+ readonly "avatar.askFailed": "The assistant couldn't answer. Your question is back in the box.";
230
+ readonly "avatar.askFailedInChat": "The assistant couldn't answer. Your question is still in Chat.";
231
+ readonly "avatar.askHint": "Enter to send · Shift+Enter for a new line";
232
+ readonly "avatar.askLabel": "Type a question for the avatar";
233
+ readonly "avatar.askMode": "This conversation can make changes, so typed questions to the avatar aren't available here. Start a new conversation to ask one.";
234
+ readonly "avatar.askModePicked": "Typed questions work only in Ask. You can change that in Chat, but leaving this tab turns the avatar off.";
235
+ readonly "avatar.askNeedsAvatar": "Turn on the avatar to send your question. You can type it now.";
219
236
  readonly "avatar.askNeedsVoice": "You can still turn the avatar on, but you ask it questions in a voice conversation.";
237
+ readonly "avatar.askNoFiles": "To attach files, use Chat.";
238
+ readonly "avatar.askNotHeard": "This answer wasn't read aloud: the avatar may still have been finishing the last answer.";
239
+ readonly "avatar.askNotSpoken": "The avatar wasn't available, so this answer wasn't read aloud.";
240
+ readonly "avatar.askNothing": "You can't ask the avatar anything in this conversation.";
241
+ readonly "avatar.askPlaceholder": "Ask the avatar…";
242
+ readonly "avatar.askReadOnly": "This conversation can't take new questions. Start a new conversation to ask one.";
243
+ readonly "avatar.askShowAll": "Show the whole answer";
244
+ readonly "avatar.askShowLess": "Show less";
245
+ readonly "avatar.askSound": "Turn on the avatar's sound first.";
246
+ readonly "avatar.askStarting": "You can send your question when the avatar is ready.";
247
+ readonly "avatar.askYouAsked": "You asked";
220
248
  readonly "avatar.availableAgain": "You can start it again at {{time}}.";
221
249
  readonly "avatar.chooserEmpty": "No avatars are available.";
222
250
  readonly "avatar.chooserLabel": "Avatar";
@@ -110,7 +110,35 @@ 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.askAgain": "You can send your question when the avatar is on again.",
114
+ "avatar.askAlsoInChat": "This answer is also in Chat.",
115
+ "avatar.askAnswer": "Answer",
116
+ "avatar.askAskedIn": "Asked in: {{title}}",
117
+ "avatar.askBusy": "You can send your question when the assistant is ready.",
118
+ "avatar.askByTyping": "You can still turn the avatar on and type your questions below.",
119
+ "avatar.askCommitting": "You can send your question once the files in Chat are saved.",
120
+ "avatar.askCut": "The avatar stopped before the end of this answer. The whole answer is in Chat.",
121
+ "avatar.askEmpty": "Type a question below, and the avatar answers it aloud.",
122
+ "avatar.askExchangeLabel": "Last question and answer",
123
+ "avatar.askFailed": "The assistant couldn't answer. Your question is back in the box.",
124
+ "avatar.askFailedInChat": "The assistant couldn't answer. Your question is still in Chat.",
125
+ "avatar.askHint": "Enter to send · Shift+Enter for a new line",
126
+ "avatar.askLabel": "Type a question for the avatar",
127
+ "avatar.askMode": "This conversation can make changes, so typed questions to the avatar aren't available here. Start a new conversation to ask one.",
128
+ "avatar.askModePicked": "Typed questions work only in Ask. You can change that in Chat, but leaving this tab turns the avatar off.",
129
+ "avatar.askNeedsAvatar": "Turn on the avatar to send your question. You can type it now.",
113
130
  "avatar.askNeedsVoice": "You can still turn the avatar on, but you ask it questions in a voice conversation.",
131
+ "avatar.askNoFiles": "To attach files, use Chat.",
132
+ "avatar.askNotHeard": "This answer wasn't read aloud: the avatar may still have been finishing the last answer.",
133
+ "avatar.askNotSpoken": "The avatar wasn't available, so this answer wasn't read aloud.",
134
+ "avatar.askNothing": "You can't ask the avatar anything in this conversation.",
135
+ "avatar.askPlaceholder": "Ask the avatar…",
136
+ "avatar.askReadOnly": "This conversation can't take new questions. Start a new conversation to ask one.",
137
+ "avatar.askShowAll": "Show the whole answer",
138
+ "avatar.askShowLess": "Show less",
139
+ "avatar.askSound": "Turn on the avatar's sound first.",
140
+ "avatar.askStarting": "You can send your question when the avatar is ready.",
141
+ "avatar.askYouAsked": "You asked",
114
142
  "avatar.availableAgain": "You can start it again at {{time}}.",
115
143
  "avatar.chooserEmpty": "No avatars are available.",
116
144
  "avatar.chooserLabel": "Avatar",
@@ -46,6 +46,7 @@ export * from './speech/routingSpeechEngine';
46
46
  export * from './speech/hostedVoiceCatalogue';
47
47
  export * from './speech/pieceSink';
48
48
  export * from './speech/speechFeed';
49
+ export * from './speech/replySpeechRequests';
49
50
  export * from './voice/useVoiceInput';
50
51
  export * from './voice/voiceStageState';
51
52
  export * from './voice/voiceStatus';
@@ -227,6 +228,11 @@ export * from './ui/assistantLiveAvatarSeam';
227
228
  export * from './ui/AssistantAvatarTabs';
228
229
  export * from './ui/avatar/AssistantAvatarTab';
229
230
  export * from './ui/voice/voiceStartGate';
231
+ export * from './ui/avatar/avatarAskGate';
232
+ export * from './ui/avatar/AvatarTabAskBox';
233
+ export * from './ui/avatar/AvatarTabLastExchange';
234
+ export * from './ui/avatar/avatarTabExchange';
235
+ export * from './ui/avatar/avatarTabDraft';
230
236
  export * from './useAssistantApiAccess';
231
237
  export * from './useConversationTurns';
232
238
  export * from './transportErrors';
@@ -334,6 +340,8 @@ export * from './avatar/live/liveAvatarController';
334
340
  export * from './avatar/live/liveKitAvatarRoom';
335
341
  export * from './avatar/live/LiveAvatarStage';
336
342
  export * from './avatar/live/LiveAvatarTabView';
343
+ export * from './avatar/live/avatarFaces';
344
+ export * from './avatar/live/AvatarFace';
337
345
  export * from './avatar/live/liveAvatarStatus';
338
346
  export * from './avatar/live/createLiveAvatarSeam';
339
347
  export * from './avatar/live/avatarVoice';
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The replies the Avatar tab asked to hear on the avatar (BOFF-9360): typed
3
+ * questions whose answers the avatar speaks, whatever Chat's auto-read is.
4
+ *
5
+ * ★★ Only the tab writes here. A reply is registered where the chat area
6
+ * creates it for a question sent from the tab, with the conversation and the
7
+ * question it belongs to (critic M2): a reply this module has never seen is
8
+ * spoken by today's rules, so a host without the live avatar, and every reply
9
+ * that was not asked from the tab, behaves exactly as before.
10
+ *
11
+ * ★★ Fail-safe by construction. A reply missing from here (evicted, never
12
+ * registered, released) stays text: the worst a gap can do is leave an answer
13
+ * silent, never speak one that should not be heard.
14
+ *
15
+ * ★ Each entry carries what happened to its speech (`outcome`), so the tab can
16
+ * say it without claiming anything was heard: it records what the audio path
17
+ * did, never what the person heard.
18
+ *
19
+ * ★ Module scope on purpose, like `speechFeed` and `supersededTurns`: the chat
20
+ * area (which registers), the speech provider (which speaks and settles) and
21
+ * the tab (which reads) are components that do not know each other.
22
+ *
23
+ * ★ Bounded: the tab asks one question at a time, so 16 entries is far more
24
+ * than one opening of the tab needs; the oldest is dropped first.
25
+ */
26
+ /** What became of one registered reply's speech. */
27
+ export type ReplySpeechOutcome =
28
+ /** Registered; the reply has not been spoken or refused yet. */
29
+ 'pending'
30
+ /** Being spoken. */
31
+ | 'speaking'
32
+ /** Spoken to its end. */
33
+ | 'spoken'
34
+ /** The avatar would not take it (off, covered, ending, sound blocked, no hosted voice), so it was not read aloud. */
35
+ | 'declined'
36
+ /** It left the avatar part way (an end, a failure), and the rest was stopped, never moved to the speakers (critic M1). */
37
+ | 'cut'
38
+ /** The avatar's audio output refused it (D7 b): what it played before could not be shown to have stopped. */
39
+ | 'not-cleared'
40
+ /** A Stop, a newer reply, a conversation switch or a hidden panel stopped it. */
41
+ | 'stopped'
42
+ /** The voice failed. */
43
+ | 'failed'
44
+ /** Released unspoken: its conversation is no longer the open one, or the tab closed. */
45
+ | 'dropped';
46
+ /** One reply the tab asked to hear on the avatar. */
47
+ export interface ReplySpeechRequest {
48
+ /** The conversation the question landed in (it may have been created by the send). */
49
+ readonly conversationId: string;
50
+ readonly questionId: string;
51
+ readonly replyId: string;
52
+ readonly outcome: ReplySpeechOutcome;
53
+ }
54
+ /** Record that the reply `replyId`, the answer to `questionId` in `conversationId`, should be spoken on the avatar. */
55
+ export declare function requestReplyOnAvatar(request: {
56
+ readonly conversationId: string;
57
+ readonly questionId: string;
58
+ readonly replyId: string;
59
+ }): void;
60
+ /** Is `replyId` registered and not yet spoken or refused? Null and undefined never are. */
61
+ export declare function replyWantsAvatar(replyId: string | null | undefined): boolean;
62
+ /** The registered request for `replyId`, or null. */
63
+ export declare function replySpeechRequest(replyId: string | null | undefined): ReplySpeechRequest | null;
64
+ /**
65
+ * What became of `replyId`'s speech. Only a reply still pending or speaking moves, and `speaking` only from
66
+ * `pending`: the first outcome that ends it is the one kept. Unknown ids are ignored.
67
+ */
68
+ export declare function settleReplyRequest(replyId: string | null | undefined, outcome: Exclude<ReplySpeechOutcome, 'pending'>): void;
69
+ /**
70
+ * ★★ Critic M2: release every pending request whose conversation is not `activeConversationId`, read from the live
71
+ * store by the caller. Never on a timer, and never the open conversation's: a send that creates its conversation
72
+ * registers the reply in it before any switch handler runs, so its own reply survives the switch it caused.
73
+ */
74
+ export declare function releaseReplyRequestsOutside(activeConversationId: string | null): void;
75
+ /** Forget every request: the Avatar tab closed. A reply arriving later follows Chat's own auto-read. */
76
+ export declare function clearReplyRequests(): void;
77
+ /** Is any request still waiting for its reply? */
78
+ export declare function hasPendingReplyRequests(): boolean;
79
+ /** Subscribe to every change; returns an unsubscribe function. For `useSyncExternalStore`. */
80
+ export declare function subscribeReplyRequests(listener: () => void): () => void;
81
+ /** A number that changes on every change: a `useSyncExternalStore` snapshot. */
82
+ export declare function replyRequestsVersion(): number;
83
+ /** Tests only. */
84
+ export declare function resetReplyRequestsForTest(): void;
@@ -0,0 +1,70 @@
1
+ //#region src/shared/assistant/speech/replySpeechRequests.ts
2
+ var e = 16, t = /* @__PURE__ */ new Map(), n = /* @__PURE__ */ new Set(), r = 0;
3
+ function i() {
4
+ r += 1;
5
+ for (let e of [...n]) e();
6
+ }
7
+ var a = new Set([
8
+ "spoken",
9
+ "declined",
10
+ "cut",
11
+ "not-cleared",
12
+ "stopped",
13
+ "failed",
14
+ "dropped"
15
+ ]);
16
+ function o(n) {
17
+ for (t.delete(n.replyId), t.set(n.replyId, {
18
+ conversationId: n.conversationId,
19
+ questionId: n.questionId,
20
+ replyId: n.replyId,
21
+ outcome: "pending"
22
+ }); t.size > e;) {
23
+ let e = t.keys().next();
24
+ if (e.done) break;
25
+ t.delete(e.value);
26
+ }
27
+ i();
28
+ }
29
+ function s(e) {
30
+ return typeof e == "string" && t.get(e)?.outcome === "pending";
31
+ }
32
+ function c(e) {
33
+ return typeof e == "string" ? t.get(e) ?? null : null;
34
+ }
35
+ function l(e, n) {
36
+ if (typeof e != "string") return;
37
+ let r = t.get(e);
38
+ !r || a.has(r.outcome) || n === "speaking" && r.outcome !== "pending" || (t.set(e, {
39
+ ...r,
40
+ outcome: n
41
+ }), i());
42
+ }
43
+ function u(e) {
44
+ let n = !1;
45
+ for (let [r, i] of t) i.outcome !== "pending" || i.conversationId === e || (t.set(r, {
46
+ ...i,
47
+ outcome: "dropped"
48
+ }), n = !0);
49
+ n && i();
50
+ }
51
+ function d() {
52
+ t.size !== 0 && (t.clear(), i());
53
+ }
54
+ function f() {
55
+ for (let e of t.values()) if (e.outcome === "pending") return !0;
56
+ return !1;
57
+ }
58
+ function p(e) {
59
+ return n.add(e), () => {
60
+ n.delete(e);
61
+ };
62
+ }
63
+ function m() {
64
+ return r;
65
+ }
66
+ function h() {
67
+ t.clear(), n.clear(), r = 0;
68
+ }
69
+ //#endregion
70
+ export { d as clearReplyRequests, f as hasPendingReplyRequests, u as releaseReplyRequestsOutside, m as replyRequestsVersion, c as replySpeechRequest, s as replyWantsAvatar, o as requestReplyOnAvatar, h as resetReplyRequestsForTest, l as settleReplyRequest, p as subscribeReplyRequests };
@@ -5,7 +5,8 @@ import { selectVoiceForLocale as n } from "../voice/speechVoices.js";
5
5
  function r(e) {
6
6
  switch (e) {
7
7
  case "cancelled":
8
- case "output-not-cleared": return !1;
8
+ case "output-not-cleared":
9
+ case "output-left": return !1;
9
10
  case "not-allowed":
10
11
  case "unsupported":
11
12
  case "unknown": return !0;
@@ -27,6 +27,19 @@ export interface SpeechSpeakOptions {
27
27
  * stop, whether the reply is one `speak()` or a queue that grows by appends.
28
28
  */
29
29
  audioOutput?: SpeechAudioOutput;
30
+ /**
31
+ * ★★ BOFF-9360 (critic M1): heard on the avatar, or nowhere. Set by the
32
+ * speech provider for a reply asked from the Avatar tab while Chat's
33
+ * auto-read is off, and read only by the live avatar's engine decorator
34
+ * (`wouldSpeakOnAvatar`); every other engine ignores it.
35
+ *
36
+ * Such a reply is never moved to the speakers: when its avatar output
37
+ * leaves it (the avatar is switched off, the tab closes, the session ends)
38
+ * or fails, part way or before its first piece, the rest is STOPPED and the
39
+ * reply ends with `onError('output-left')`. A device-voice fallback is off
40
+ * for it too. A refusal (`output-not-cleared`) is reported as it always is.
41
+ */
42
+ avatarOnly?: boolean;
30
43
  }
31
44
  /**
32
45
  * Somewhere other than the speakers to play a reply's hosted audio
@@ -138,8 +151,13 @@ export interface SpeechOutputRefusal {
138
151
  * played before could not be shown to have stopped. Nothing re-speaks it, the
139
152
  * device voice included, and its text stays where it is. Only a reply with an
140
153
  * `audioOutput` can end this way.
154
+ *
155
+ * `output-left` ends an `avatarOnly` reply (BOFF-9360) that stopped going to
156
+ * the avatar, or was never given to it: the rest of it was stopped, never
157
+ * played on the speakers. Only the live avatar's engine decorator reports it,
158
+ * and nothing re-speaks it.
141
159
  */
142
- export type SpeechErrorReason = 'cancelled' | 'not-allowed' | 'unsupported' | 'unknown' | 'output-not-cleared';
160
+ export type SpeechErrorReason = 'cancelled' | 'not-allowed' | 'unsupported' | 'unknown' | 'output-not-cleared' | 'output-left';
143
161
  export interface SpeechSpeakHandlers {
144
162
  /**
145
163
  * Audio has actually begun playing — not merely been requested.
@@ -187,4 +205,13 @@ export interface SpeechEngine {
187
205
  speak(chunks: readonly string[], options: SpeechSpeakOptions, handlers?: SpeechSpeakHandlers): void;
188
206
  /** Stop immediately and drop anything queued. Safe to call when idle. */
189
207
  cancel(): void;
208
+ /**
209
+ * ★ BOFF-9360: would `speak` with these options play the reply on the live
210
+ * avatar right now? The decorated live-avatar engine only (it asks the same
211
+ * per-reply route its `speak` asks, with the same avatar voice, so the two
212
+ * cannot disagree); every other engine omits it, which callers read as
213
+ * "no". Asked by the speech provider before an `avatarOnly` reply starts,
214
+ * and only then.
215
+ */
216
+ wouldSpeakOnAvatar?(options: SpeechSpeakOptions): boolean;
190
217
  }