@burdenoff/fe-libs 2026.1014.4 → 2026.1015.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/shared/assistant/avatar/avatarPreferences.d.ts +61 -14
  2. package/dist/shared/assistant/avatar/avatarPreferences.js +54 -21
  3. package/dist/shared/assistant/avatar/live/LiveAvatarTabView.d.ts +33 -0
  4. package/dist/shared/assistant/avatar/live/LiveAvatarTabView.js +278 -0
  5. package/dist/shared/assistant/avatar/live/avatarSessionClient.d.ts +5 -0
  6. package/dist/shared/assistant/avatar/live/avatarSessionClient.js +25 -6
  7. package/dist/shared/assistant/avatar/live/avatarVoice.d.ts +43 -0
  8. package/dist/shared/assistant/avatar/live/avatarVoice.js +45 -0
  9. package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.d.ts +21 -1
  10. package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.js +74 -24
  11. package/dist/shared/assistant/avatar/live/liveAvatarController.d.ts +47 -8
  12. package/dist/shared/assistant/avatar/live/liveAvatarController.js +244 -161
  13. package/dist/shared/assistant/avatar/live/liveAvatarStatus.d.ts +2 -2
  14. package/dist/shared/assistant/avatar/live/liveAvatarStatus.js +11 -4
  15. package/dist/shared/assistant/avatar/useAvatarCopy.d.ts +52 -21
  16. package/dist/shared/assistant/avatar/useAvatarCopy.js +93 -48
  17. package/dist/shared/assistant/i18nManifest.d.ts +24 -2
  18. package/dist/shared/assistant/i18nManifest.js +24 -2
  19. package/dist/shared/assistant/index.d.ts +5 -0
  20. package/dist/shared/assistant/speech/hostedVoiceCatalogue.d.ts +19 -0
  21. package/dist/shared/assistant/speech/hostedVoiceCatalogue.js +25 -18
  22. package/dist/shared/assistant/ui/AIAssistantPanel.js +136 -103
  23. package/dist/shared/assistant/ui/AssistantAvatarTabs.d.ts +10 -0
  24. package/dist/shared/assistant/ui/AssistantAvatarTabs.js +48 -0
  25. package/dist/shared/assistant/ui/AssistantChatArea.js +1260 -1199
  26. package/dist/shared/assistant/ui/assistantLiveAvatarSeam.d.ts +165 -5
  27. package/dist/shared/assistant/ui/assistantLiveAvatarSeam.js +141 -59
  28. package/dist/shared/assistant/ui/avatar/AssistantAvatarTab.d.ts +13 -0
  29. package/dist/shared/assistant/ui/avatar/AssistantAvatarTab.js +70 -0
  30. package/dist/shared/assistant/ui/composer/AssistantComposer.d.ts +18 -0
  31. package/dist/shared/assistant/ui/composer/AssistantComposer.js +121 -102
  32. package/dist/shared/assistant/ui/composer/AssistantHandsFreeInterruptToggle.d.ts +9 -0
  33. package/dist/shared/assistant/ui/composer/AssistantHandsFreeInterruptToggle.js +28 -14
  34. package/dist/shared/assistant/ui/panelOverlays.d.ts +24 -3
  35. package/dist/shared/assistant/ui/panelOverlays.js +11 -10
  36. package/dist/shared/assistant/ui/useVoiceSession.js +234 -233
  37. package/dist/shared/assistant/ui/voice/AssistantVoiceSession.d.ts +6 -0
  38. package/dist/shared/assistant/ui/voice/AssistantVoiceSession.js +161 -149
  39. package/dist/shared/assistant/ui/voice/VoiceMiniController.js +7 -7
  40. package/dist/shared/assistant/ui/voice/VoiceStage.js +7 -7
  41. package/dist/shared/assistant/ui/voice/voiceStartGate.d.ts +57 -0
  42. package/dist/shared/assistant/ui/voice/voiceStartGate.js +9 -0
  43. package/dist/shared/assistant/voice/handsfree/handsFreeSetting.d.ts +7 -1
  44. package/dist/shared/assistant/voice/handsfree/handsFreeSetting.js +2 -2
  45. package/dist/shared-assistant.js +259 -254
  46. package/package.json +1 -1
@@ -1,36 +1,83 @@
1
1
  import { StoreApi, UseBoundStore } from 'zustand';
2
2
  import { PersistStorage } from 'zustand/middleware';
3
+ import { HostedSpeaker } from '../speech/hostedVoiceCatalogue';
3
4
  /** The base storage name; the profile prefix is added by the profile-scoped storage. */
4
5
  export declare const AVATAR_PREFERENCES_STORAGE_NAME = "hb-ai-assistant-avatar";
5
6
  /**
6
- * Which voice the avatar speaks with: the voice the person already picked for
7
- * the assistant (`mine`, the default: their current voice is kept), or the one
8
- * recommended for the chosen avatar (`paired`).
7
+ * Which voice the avatar speaks with (BOFF-9341). Only replies the avatar takes
8
+ * use it; the assistant's own voice, the one Chat reads aloud with, is never
9
+ * changed by it.
10
+ * - `mine`: the voice the person already picked for the assistant ("Keep my
11
+ * current voice");
12
+ * - `paired`: the speaker the HOST configured for the avatar on screen
13
+ * ("Default for this avatar"), so it follows the face when the face is
14
+ * switched; a face with none configured keeps the assistant's voice;
15
+ * - `speaker`: one of the hosted speakers (`avatarSpeaker`), in the language
16
+ * of each reply.
17
+ */
18
+ export type AvatarVoiceMode = 'mine' | 'paired' | 'speaker';
19
+ /** A mode a host may make the default (`createLiveAvatarSeam`'s `defaultAvatarVoiceMode`): a speaker is the person's own pick. */
20
+ export type AvatarVoiceDefault = Exclude<AvatarVoiceMode, 'speaker'>;
21
+ /** What applies when neither the person nor the host chose: their current voice is kept. */
22
+ export declare const DEFAULT_AVATAR_VOICE_MODE: AvatarVoiceDefault;
23
+ /**
24
+ * What is written. `v` is this file's own format number (see the header); it
25
+ * stays 1, because every field is validated on merge and an older build reads
26
+ * the two it knows (`avatarId`, and `voiceMode`, where anything it does not
27
+ * know reads as `mine`).
28
+ *
29
+ * ★★ `voiceChosen` marks a voice the PERSON chose. Builds before BOFF-9341 had
30
+ * no voice control and wrote `voiceMode: 'mine'` as a default; read as a
31
+ * choice, it would hide the host's default (`defaultAvatarVoiceMode`) from
32
+ * everyone who ever used the avatar. Without the mark, a stored voice is no
33
+ * choice at all.
9
34
  */
10
- export type AvatarVoiceMode = 'paired' | 'mine';
11
- export declare const DEFAULT_AVATAR_VOICE_MODE: AvatarVoiceMode;
12
- /** What is written. `v` is this file's own format number (see the header). */
13
35
  export interface AvatarPreferencesPersisted {
14
36
  v: 1;
15
37
  avatarId: string | null;
16
- voiceMode: AvatarVoiceMode;
38
+ voiceMode: AvatarVoiceMode | null;
39
+ avatarSpeaker: HostedSpeaker | null;
40
+ voiceChosen: boolean;
41
+ }
42
+ /** The person's voice choice for the avatar, as the seam reads it. */
43
+ export interface AvatarVoicePreference {
44
+ /** Null until the person chooses: the host's default applies, else `DEFAULT_AVATAR_VOICE_MODE`. */
45
+ readonly voiceMode: AvatarVoiceMode | null;
46
+ /** The speaker of `voiceMode: 'speaker'`, and null in every other mode. */
47
+ readonly avatarSpeaker: HostedSpeaker | null;
17
48
  }
18
- export interface AvatarPreferencesState {
49
+ export interface AvatarPreferencesState extends AvatarVoicePreference {
19
50
  /** Never persisted: every page load starts with the avatar OFF. */
20
51
  avatarEnabled: boolean;
21
52
  /** The chosen avatar, or null before one is chosen. */
22
53
  avatarId: string | null;
23
- voiceMode: AvatarVoiceMode;
24
54
  setAvatarEnabled: (enabled: boolean) => void;
25
55
  setAvatarId: (avatarId: string | null) => void;
26
- setVoiceMode: (mode: AvatarVoiceMode) => void;
56
+ /** The person picks "Keep my current voice" or "Default for this avatar". Anything else is ignored. */
57
+ setVoiceMode: (mode: AvatarVoiceDefault) => void;
58
+ /** The person picks one of the hosted speakers. Anything not in `HOSTED_SPEAKERS` is ignored. */
59
+ setAvatarSpeaker: (speaker: HostedSpeaker) => void;
27
60
  }
28
61
  export declare function resolveStoredAvatarId(value: unknown): string | null;
29
- export declare function resolveStoredAvatarVoiceMode(value: unknown): AvatarVoiceMode;
30
- export declare function avatarPreferencesToPersist(state: Pick<AvatarPreferencesState, 'avatarId' | 'voiceMode'>): AvatarPreferencesPersisted;
62
+ /** A stored voice mode this build knows, or null (no choice): never a guess. */
63
+ export declare function resolveStoredAvatarVoiceMode(value: unknown): AvatarVoiceMode | null;
64
+ /**
65
+ * The voice choice a stored snapshot holds: a mode and, for `speaker`, a speaker
66
+ * this build offers. Not marked as the person's (`voiceChosen`), or a speaker
67
+ * mode without a valid speaker (a hand edit, or a speaker a later build added):
68
+ * no choice, so the host's default applies again rather than a voice nobody
69
+ * picked.
70
+ */
71
+ export declare function resolveStoredAvatarVoice(stored: Record<string, unknown>): AvatarVoicePreference;
72
+ /**
73
+ * The mode that applies: the person's choice ALWAYS wins; then the host's
74
+ * default (`createLiveAvatarSeam`'s `defaultAvatarVoiceMode`); then `mine`.
75
+ */
76
+ export declare function resolveAvatarVoiceMode(preference: AvatarVoicePreference, hostDefault?: AvatarVoiceDefault): AvatarVoiceMode;
77
+ export declare function avatarPreferencesToPersist(state: Pick<AvatarPreferencesState, 'avatarId' | 'voiceMode' | 'avatarSpeaker'>): AvatarPreferencesPersisted;
31
78
  /**
32
- * Fold what storage holds into the live state. Only the two preference fields
33
- * are taken, each validated; `avatarEnabled` and the actions always come from
79
+ * Fold what storage holds into the live state. Only the preference fields are
80
+ * taken, each validated; `avatarEnabled` and the actions always come from
34
81
  * `current`, whatever the stored value says.
35
82
  */
36
83
  export declare function mergePersistedAvatarPreferences(persisted: unknown, current: AvatarPreferencesState): AvatarPreferencesState;
@@ -1,46 +1,79 @@
1
1
  import { createProfileScopedStorage as e } from "../../utils/profileScopedStorage.js";
2
2
  import "../../../shared-utils.js";
3
3
  import { createGuardedAssistantStorage as t } from "../guardedStorage.js";
4
- import { create as n } from "zustand";
5
- import { persist as r } from "zustand/middleware";
4
+ import { isHostedSpeaker as n } from "../speech/hostedVoiceCatalogue.js";
5
+ import { create as r } from "zustand";
6
+ import { persist as i } from "zustand/middleware";
6
7
  //#region src/shared/assistant/avatar/avatarPreferences.ts
7
- var i = "hb-ai-assistant-avatar", a = "mine", o = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
8
- function s(e) {
9
- return typeof e == "string" && o.test(e) ? e : null;
10
- }
8
+ var a = "hb-ai-assistant-avatar", o = "mine", s = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
11
9
  function c(e) {
12
- return e === "mine" || e === "paired" ? e : a;
10
+ return typeof e == "string" && s.test(e) ? e : null;
13
11
  }
14
12
  function l(e) {
13
+ return e === "mine" || e === "paired" || e === "speaker" ? e : null;
14
+ }
15
+ function u(e) {
16
+ let t = {
17
+ voiceMode: null,
18
+ avatarSpeaker: null
19
+ };
20
+ if (e.voiceChosen !== !0) return t;
21
+ let r = l(e.voiceMode);
22
+ return r === null ? t : r === "speaker" ? n(e.avatarSpeaker) ? {
23
+ voiceMode: r,
24
+ avatarSpeaker: e.avatarSpeaker
25
+ } : t : {
26
+ voiceMode: r,
27
+ avatarSpeaker: null
28
+ };
29
+ }
30
+ function d(e, t = o) {
31
+ return e.voiceMode ?? t;
32
+ }
33
+ function f(e) {
15
34
  return {
16
35
  v: 1,
17
36
  avatarId: e.avatarId,
18
- voiceMode: e.voiceMode
37
+ voiceMode: e.voiceMode,
38
+ avatarSpeaker: e.voiceMode === "speaker" ? e.avatarSpeaker : null,
39
+ voiceChosen: e.voiceMode !== null
19
40
  };
20
41
  }
21
- function u(e, t) {
42
+ function p(e, t) {
22
43
  let n = typeof e == "object" && e ? e : {};
23
44
  return {
24
45
  ...t,
25
- avatarId: s(n.avatarId),
26
- voiceMode: c(n.voiceMode)
46
+ avatarId: c(n.avatarId),
47
+ ...u(n)
27
48
  };
28
49
  }
29
- function d(e) {
30
- return n()(r((e) => ({
50
+ function m(e) {
51
+ return r()(i((e) => ({
31
52
  avatarEnabled: !1,
32
53
  avatarId: null,
33
- voiceMode: a,
54
+ voiceMode: null,
55
+ avatarSpeaker: null,
34
56
  setAvatarEnabled: (t) => e({ avatarEnabled: t }),
35
- setAvatarId: (t) => e({ avatarId: s(t) }),
36
- setVoiceMode: (t) => e({ voiceMode: c(t) })
57
+ setAvatarId: (t) => e({ avatarId: c(t) }),
58
+ setVoiceMode: (t) => {
59
+ t !== "mine" && t !== "paired" || e({
60
+ voiceMode: t,
61
+ avatarSpeaker: null
62
+ });
63
+ },
64
+ setAvatarSpeaker: (t) => {
65
+ n(t) && e({
66
+ voiceMode: "speaker",
67
+ avatarSpeaker: t
68
+ });
69
+ }
37
70
  }), {
38
- name: i,
71
+ name: a,
39
72
  storage: e,
40
- partialize: l,
41
- merge: u
73
+ partialize: f,
74
+ merge: p
42
75
  }));
43
76
  }
44
- var f = d(t(e()));
77
+ var h = m(t(e()));
45
78
  //#endregion
46
- export { i as AVATAR_PREFERENCES_STORAGE_NAME, a as DEFAULT_AVATAR_VOICE_MODE, l as avatarPreferencesToPersist, d as createAvatarPreferencesStore, u as mergePersistedAvatarPreferences, s as resolveStoredAvatarId, c as resolveStoredAvatarVoiceMode, f as useAvatarPreferencesStore };
79
+ export { a as AVATAR_PREFERENCES_STORAGE_NAME, o as DEFAULT_AVATAR_VOICE_MODE, f as avatarPreferencesToPersist, m as createAvatarPreferencesStore, p as mergePersistedAvatarPreferences, d as resolveAvatarVoiceMode, c as resolveStoredAvatarId, u as resolveStoredAvatarVoice, l as resolveStoredAvatarVoiceMode, h as useAvatarPreferencesStore };
@@ -0,0 +1,33 @@
1
+ import { FC } from 'react';
2
+ import { HostedSpeaker } from '../../speech/hostedVoiceCatalogue';
3
+ import { SpeechVoiceLike } from '../../speech/types';
4
+ import { LiveAvatarTabProps } from '../../ui/assistantLiveAvatarSeam';
5
+ import { AvatarVoiceDefault, AvatarVoicePreference } from '../avatarPreferences';
6
+ import { LiveAvatarVoices } from './avatarVoice';
7
+ import { LiveAvatarController } from './liveAvatarController';
8
+ /** The person's avatar voice choice: read by the engine decorator per reply, written by the tab's select. STABLE. */
9
+ export interface LiveAvatarVoiceStore {
10
+ getState(): AvatarVoicePreference;
11
+ subscribe(listener: () => void): () => void;
12
+ setVoiceMode(mode: AvatarVoiceDefault): void;
13
+ setAvatarSpeaker(speaker: HostedSpeaker): void;
14
+ }
15
+ export interface LiveAvatarTabViewProps extends LiveAvatarTabProps {
16
+ readonly controller: LiveAvatarController;
17
+ /** Display names for the avatar ids the server offers; without one the chooser numbers them. */
18
+ readonly avatarNames?: Readonly<Record<string, string>>;
19
+ /** The host's speaker per avatar ("Default for this avatar"). */
20
+ readonly avatarVoices: LiveAvatarVoices;
21
+ readonly defaultAvatarVoiceMode?: AvatarVoiceDefault;
22
+ readonly voiceStore: LiveAvatarVoiceStore;
23
+ /** Wall-clock milliseconds: the retry time and the Turn off guard. Tests pass their own. */
24
+ readonly now?: () => number;
25
+ }
26
+ /** A voice name as the person reads it: a hosted voice by its speaker ("Kore"), any other by its own name. */
27
+ export declare function avatarVoiceDisplayName(name: string): string;
28
+ /**
29
+ * `speaker` in the language the person speaks, as the engine lists it for that language (`voiceChoices`); null when it is not offered
30
+ * there (no hosted voice for the language, or a hosted engine latched off, which lists none).
31
+ */
32
+ export declare function avatarPreviewVoice(speaker: HostedSpeaker, voiceChoices: readonly SpeechVoiceLike[]): SpeechVoiceLike | null;
33
+ export declare const LiveAvatarTabView: FC<LiveAvatarTabViewProps>;
@@ -0,0 +1,278 @@
1
+ import { HOSTED_SPEAKERS as e, isHostedSpeaker as t } from "../../speech/hostedVoiceCatalogue.js";
2
+ import { useAvatarCopy as n } from "../useAvatarCopy.js";
3
+ import { useAssistantSpeech as ee } from "../../ui/AssistantSpeechProvider.js";
4
+ import { resolveAvatarVoiceMode as te } from "../avatarPreferences.js";
5
+ import "./liveAvatarController.js";
6
+ import { liveAvatarStatusText as ne } from "./liveAvatarStatus.js";
7
+ import { hostedAvatarVoice as r } from "./avatarVoice.js";
8
+ import { useCallback as re, useEffect as i, useId as a, useLayoutEffect as ie, useRef as o, useState as s, useSyncExternalStore as c } from "react";
9
+ import { Volume2 as l } from "lucide-react";
10
+ import { jsx as u, jsxs as d } from "react/jsx-runtime";
11
+ //#region src/shared/assistant/avatar/live/LiveAvatarTabView.tsx
12
+ var f = "mine", p = "paired", m = "speaker:", h = /-Chirp3-HD-([A-Za-z]+)$/;
13
+ function g(e) {
14
+ let n = h.exec(e)?.[1];
15
+ return n !== void 0 && t(n) ? n : e;
16
+ }
17
+ function _(e, t) {
18
+ let n = t.find((e) => e.source === "hosted")?.lang;
19
+ return n === void 0 ? null : r(e, n, t);
20
+ }
21
+ var v = "text-xs font-medium text-text-secondary", y = "text-xs text-text-muted", b = "focus:outline-none focus-visible:ring-2 focus-visible:ring-border-strong", x = `inline-flex min-h-11 w-full items-center justify-center gap-2 rounded-xl border border-border-default bg-bg-elevated px-4 text-sm font-medium text-text-primary transition-colors hover:bg-bg-raised aria-disabled:cursor-default aria-disabled:opacity-60 aria-disabled:hover:bg-bg-elevated ${b}`, ae = `inline-flex min-h-11 w-full items-center justify-center rounded-xl bg-action-primary-bg px-4 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg-hover aria-disabled:cursor-default aria-disabled:opacity-60 aria-disabled:hover:bg-action-primary-bg ${b}`, oe = "relative inline-flex min-h-11 min-w-0 max-w-full cursor-pointer items-center rounded-full border px-4 text-sm font-medium transition-colors focus-within:ring-2 focus-within:ring-border-strong", se = "absolute inset-0 m-0 size-full cursor-pointer appearance-none rounded-full opacity-0", S = ({ controller: r, shown: h, voice: S, handsFreeOff: C, leaveHintId: ce, startVoiceRef: w, avatarNames: le, avatarVoices: ue, defaultAvatarVoiceMode: de, voiceStore: T, now: E = Date.now }) => {
22
+ let D = n(), O = ee(), k = a(), A = a(), j = a(), M = a(), N = a(), P = o(null), F = c(r.subscribe, r.getSnapshot, r.getSnapshot), I = c(T.subscribe, T.getState, T.getState);
23
+ i(() => {
24
+ if (h) return r.setStageMounted(!0), () => r.setStageMounted(!1);
25
+ }, [r, h]), i(() => {
26
+ let e = r.getSnapshot();
27
+ e.phase === "off" && e.avatars.length === 0 && r.requestOn({ start: !1 });
28
+ }, [r]);
29
+ let { phase: L } = F, R = L === "starting" || L === "connecting" || L === "ready" || L === "ending", [z, B] = s(!1);
30
+ z && (L === "off" || L === "choosing" || L === "ready" || L === "ending") && B(!1);
31
+ let V = E(), fe = F.retryAt !== null && F.retryAt > V, H = ne(F, D, V), U = F.selectedAvatarId, W = (e) => {
32
+ let t = F.avatars.indexOf(e);
33
+ return le?.[e] ?? (t === -1 ? D.chooserNone : `${D.chooserLabel} ${t + 1}`);
34
+ }, G = L === "ready" ? { kind: "turn-off" } : L === "ending" ? { kind: "ending" } : z || L === "starting" || L === "connecting" ? { kind: "starting" } : F.offered ? {
35
+ kind: "turn-on",
36
+ ready: !fe && U !== null && (L === "choosing" || L === "off" && F.avatars.includes(U))
37
+ } : null, K = o(null), q = G?.kind === "turn-off";
38
+ ie(() => {
39
+ K.current = q ? E() : null;
40
+ }, [q, E]);
41
+ let pe = re((e) => {
42
+ if (e !== null) return () => {
43
+ e.ownerDocument.activeElement === e && P.current?.focus();
44
+ };
45
+ }, []), me = () => {
46
+ if (G !== null) switch (G.kind) {
47
+ case "turn-on":
48
+ if (!G.ready || U === null) return;
49
+ B(!0), L === "choosing" ? r.confirm() : r.requestOn({
50
+ start: !0,
51
+ avatarId: U
52
+ });
53
+ return;
54
+ case "turn-off": {
55
+ let e = K.current;
56
+ if (e !== null && E() - e < 1e3) return;
57
+ r.switchOff();
58
+ return;
59
+ }
60
+ case "starting":
61
+ case "ending": return;
62
+ }
63
+ }, J = te(I, de), Y = U === null ? null : ue.get(U) ?? null, X = J === "speaker" && I.avatarSpeaker !== null ? `${m}${I.avatarSpeaker}` : J === "paired" && Y !== null ? p : f, Z = X === p ? Y : X.startsWith(m) ? I.avatarSpeaker : null, Q = X === f ? O.activeVoiceName : Z === null ? null : _(Z, O.voiceChoices)?.name ?? null, he = O.speakingMessageId !== null || O.spokenChunks !== null, $ = Q !== null && !S.live && !he, ge = O.activeVoiceName ? D.voiceModeMineNamed(g(O.activeVoiceName)) : D.voiceModeMine, _e = (e) => {
64
+ if (e === f || e === p) {
65
+ T.setVoiceMode(e);
66
+ return;
67
+ }
68
+ let n = e.slice(8);
69
+ e.startsWith(m) && t(n) && T.setAvatarSpeaker(n);
70
+ }, ve = L === "ending" ? D.stateEnding : L === "connecting" || L === "ready" ? D.stageWaiting : L === "starting" ? D.stateStarting : D.stateOff, ye = L === "off" && F.avatars.length === 0 && F.problem !== null && F.offered;
71
+ return /* @__PURE__ */ d("div", {
72
+ ref: P,
73
+ tabIndex: -1,
74
+ className: "flex min-h-full shrink-0 flex-col gap-3 focus:outline-none",
75
+ "data-testid": "assistant-avatar-tab-view",
76
+ children: [
77
+ /* @__PURE__ */ u("div", {
78
+ className: "flex max-h-80 min-h-[7.5rem] grow basis-30 justify-center",
79
+ children: /* @__PURE__ */ d("div", {
80
+ className: "relative aspect-square h-full max-w-full overflow-hidden rounded-2xl border border-border-subtle bg-bg-sunken",
81
+ "data-testid": "assistant-avatar-tab-frame",
82
+ "data-phase": L,
83
+ children: [
84
+ R ? /* @__PURE__ */ u("video", {
85
+ ref: r.attachVideo,
86
+ className: `size-full object-cover ${F.videoReady ? "" : "invisible"}`,
87
+ "aria-label": D.stageLabel,
88
+ autoPlay: !0,
89
+ muted: !0,
90
+ playsInline: !0,
91
+ "data-testid": "assistant-avatar-tab-video"
92
+ }) : null,
93
+ R && F.videoReady ? null : /* @__PURE__ */ u("p", {
94
+ className: "absolute inset-0 flex items-center justify-center p-3 text-center text-sm text-text-muted",
95
+ children: ve
96
+ }),
97
+ F.audioBlocked ? /* @__PURE__ */ d("button", {
98
+ type: "button",
99
+ onClick: () => r.resumeAudio(),
100
+ className: `absolute inset-x-2 bottom-2 inline-flex min-h-11 items-center justify-center gap-1.5 rounded-xl bg-bg-elevated px-3 text-xs font-medium text-text-primary ${b}`,
101
+ "data-testid": "assistant-avatar-tab-sound",
102
+ children: [/* @__PURE__ */ u(l, {
103
+ className: "size-4 shrink-0",
104
+ "aria-hidden": "true"
105
+ }), /* @__PURE__ */ u("span", { children: D.problem("playback-blocked") })]
106
+ }) : null
107
+ ]
108
+ })
109
+ }),
110
+ /* @__PURE__ */ u("p", {
111
+ className: H === null ? "sr-only" : "text-center text-sm text-text-secondary",
112
+ role: "status",
113
+ "data-testid": "assistant-avatar-tab-status",
114
+ children: H ?? ""
115
+ }),
116
+ F.avatars.length > 1 && !R ? /* @__PURE__ */ d("fieldset", {
117
+ className: "flex min-w-0 flex-col gap-1.5",
118
+ "data-testid": "assistant-avatar-tab-chooser",
119
+ children: [/* @__PURE__ */ u("legend", {
120
+ className: v,
121
+ children: D.chooserTitle
122
+ }), /* @__PURE__ */ u("div", {
123
+ className: "flex flex-wrap gap-2",
124
+ children: F.avatars.map((e) => {
125
+ let t = e === U, n = W(e);
126
+ return /* @__PURE__ */ d("label", {
127
+ className: `${oe} ${t ? "border-transparent bg-action-primary-bg text-action-primary-text" : "border-border-default bg-bg-elevated text-text-secondary hover:bg-bg-raised hover:text-text-primary"}`,
128
+ title: t ? D.chooserSelected(n) : D.chooserOption(n),
129
+ children: [/* @__PURE__ */ u("input", {
130
+ type: "radio",
131
+ name: A,
132
+ value: e,
133
+ checked: t,
134
+ onChange: () => r.selectAvatar(e),
135
+ className: se,
136
+ "data-avatar-id": e
137
+ }), /* @__PURE__ */ u("span", {
138
+ className: "truncate",
139
+ children: n
140
+ })]
141
+ }, e);
142
+ })
143
+ })]
144
+ }) : F.avatars.length > 0 && U !== null ? /* @__PURE__ */ d("div", {
145
+ className: "flex flex-col gap-0.5",
146
+ "data-testid": "assistant-avatar-tab-chosen",
147
+ children: [/* @__PURE__ */ d("p", {
148
+ className: "flex min-w-0 items-baseline gap-2 text-sm",
149
+ children: [/* @__PURE__ */ u("span", {
150
+ className: v,
151
+ children: D.chooserLabel
152
+ }), /* @__PURE__ */ u("span", {
153
+ className: "truncate text-text-primary",
154
+ children: W(U)
155
+ })]
156
+ }), R && F.avatars.length > 1 ? /* @__PURE__ */ u("p", {
157
+ className: y,
158
+ children: D.chooserLocked
159
+ }) : null]
160
+ }) : L === "choosing" ? /* @__PURE__ */ u("p", {
161
+ className: y,
162
+ "data-testid": "assistant-avatar-tab-empty",
163
+ children: D.chooserEmpty
164
+ }) : null,
165
+ /* @__PURE__ */ d("div", {
166
+ className: "flex flex-col gap-1",
167
+ children: [
168
+ /* @__PURE__ */ u("label", {
169
+ htmlFor: j,
170
+ className: v,
171
+ children: D.voiceModeLabel
172
+ }),
173
+ /* @__PURE__ */ d("div", {
174
+ className: "flex items-center gap-2",
175
+ children: [/* @__PURE__ */ d("select", {
176
+ id: j,
177
+ value: X,
178
+ onChange: (e) => _e(e.target.value),
179
+ "aria-describedby": M,
180
+ className: `min-h-11 min-w-0 flex-1 rounded-xl border border-border-default bg-bg-elevated px-3 text-sm text-text-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary ${b}`,
181
+ "data-testid": "assistant-avatar-tab-voice",
182
+ children: [
183
+ /* @__PURE__ */ u("option", {
184
+ value: f,
185
+ children: ge
186
+ }),
187
+ Y === null ? null : /* @__PURE__ */ u("option", {
188
+ value: p,
189
+ children: D.voiceModeDefaultNamed(Y)
190
+ }),
191
+ e.map((e) => /* @__PURE__ */ u("option", {
192
+ value: `${m}${e}`,
193
+ children: e
194
+ }, e))
195
+ ]
196
+ }), /* @__PURE__ */ u("button", {
197
+ type: "button",
198
+ onClick: () => {
199
+ $ && Q !== null && O.previewVoice(Q);
200
+ },
201
+ "aria-disabled": !$,
202
+ "aria-label": D.hearVoice,
203
+ title: D.hearVoice,
204
+ className: `inline-flex size-11 shrink-0 items-center justify-center rounded-xl border border-border-default bg-bg-elevated text-text-secondary transition-colors hover:bg-bg-raised hover:text-text-primary aria-disabled:cursor-default aria-disabled:opacity-60 ${b}`,
205
+ "data-testid": "assistant-avatar-tab-hear",
206
+ children: /* @__PURE__ */ u(l, {
207
+ className: "size-5",
208
+ "aria-hidden": "true"
209
+ })
210
+ })]
211
+ }),
212
+ /* @__PURE__ */ u("p", {
213
+ id: M,
214
+ className: y,
215
+ children: D.voiceModeHint
216
+ })
217
+ ]
218
+ }),
219
+ R ? null : /* @__PURE__ */ u("p", {
220
+ id: k,
221
+ className: "text-xs text-text-secondary",
222
+ "data-testid": "assistant-avatar-tab-disclosure",
223
+ children: D.consent
224
+ }),
225
+ G === null ? null : /* @__PURE__ */ u("button", {
226
+ ref: pe,
227
+ type: "button",
228
+ onClick: me,
229
+ "aria-disabled": G.kind === "turn-on" ? !G.ready : G.kind !== "turn-off",
230
+ "aria-describedby": G.kind === "turn-on" ? k : void 0,
231
+ className: G.kind === "turn-off" ? x : ae,
232
+ "data-testid": "assistant-avatar-tab-switch",
233
+ "data-slot": G.kind,
234
+ children: G.kind === "turn-off" ? D.stop : G.kind === "starting" ? D.turnOnStarting : G.kind === "ending" ? D.stateEnding : D.consentConfirm
235
+ }),
236
+ ye ? /* @__PURE__ */ u("button", {
237
+ type: "button",
238
+ onClick: () => r.requestOn({ start: !1 }),
239
+ className: x,
240
+ "data-testid": "assistant-avatar-tab-retry",
241
+ children: D.chooserRetry
242
+ }) : null,
243
+ S.live ? null : /* @__PURE__ */ d("div", {
244
+ className: "flex flex-col gap-1",
245
+ children: [/* @__PURE__ */ u("button", {
246
+ ref: w,
247
+ type: "button",
248
+ onClick: () => {
249
+ S.blocked === null && S.start();
250
+ },
251
+ "aria-disabled": S.blocked !== null,
252
+ "aria-describedby": S.blocked === null ? void 0 : N,
253
+ className: x,
254
+ "data-testid": "assistant-avatar-tab-start-voice",
255
+ children: D.startVoice
256
+ }), S.blocked === null ? null : /* @__PURE__ */ u("p", {
257
+ id: N,
258
+ className: y,
259
+ "data-testid": "assistant-avatar-tab-voice-blocked",
260
+ children: S.impossible && !R ? `${S.blocked} ${D.askNeedsVoice}` : S.blocked
261
+ })]
262
+ }),
263
+ C && !F.voiceLive ? /* @__PURE__ */ u("p", {
264
+ className: y,
265
+ "data-testid": "assistant-avatar-tab-hands-free",
266
+ children: D.handsFreeOff
267
+ }) : null,
268
+ R && !F.heldByVoice ? /* @__PURE__ */ u("p", {
269
+ id: ce,
270
+ className: y,
271
+ "data-testid": "assistant-avatar-tab-leave-hint",
272
+ children: D.tabLeaveHint
273
+ }) : null
274
+ ]
275
+ });
276
+ };
277
+ //#endregion
278
+ export { S as LiveAvatarTabView, _ as avatarPreviewVoice, g as avatarVoiceDisplayName };
@@ -99,6 +99,11 @@ export interface AvatarRefusal {
99
99
  readonly hide: boolean;
100
100
  /** The server's disclosure version changed (`consent_required`): ask availability again, show the disclosure to confirm again. */
101
101
  readonly showDisclosure?: true;
102
+ /**
103
+ * A "not now" refusal (busy, started too quickly, the avatar time used up): when the server said when to try again (its Retry-After),
104
+ * in seconds. Absent when it said nothing: the person is then told no time, and nothing waits for one.
105
+ */
106
+ readonly retryAfterSeconds?: number;
102
107
  }
103
108
  /**
104
109
  * The person's side of every failure (§3.4): the normal voice always continues; this says what to show. 404 means "not here" (the module
@@ -224,8 +224,24 @@ function y(e) {
224
224
  }
225
225
  };
226
226
  }
227
- function b(e) {
228
- if (!(e instanceof s) || e.status === 0) return {
227
+ var b = new Set([
228
+ "capacity",
229
+ "rate-limited",
230
+ "budget-exhausted"
231
+ ]);
232
+ function x(e) {
233
+ if (!(e instanceof s)) return {
234
+ problem: "connection",
235
+ hide: !1
236
+ };
237
+ let t = S(e);
238
+ return t.problem !== null && b.has(t.problem) && e.retryAfterSeconds !== null ? {
239
+ ...t,
240
+ retryAfterSeconds: e.retryAfterSeconds
241
+ } : t;
242
+ }
243
+ function S(e) {
244
+ if (e.status === 0) return {
229
245
  problem: "connection",
230
246
  hide: !1
231
247
  };
@@ -274,11 +290,14 @@ function b(e) {
274
290
  problem: "start-timeout",
275
291
  hide: !1
276
292
  };
277
- case "avatar_paused":
278
- case "budget_exhausted": return {
293
+ case "avatar_paused": return {
279
294
  problem: "paused",
280
295
  hide: !1
281
296
  };
297
+ case "budget_exhausted": return {
298
+ problem: "budget-exhausted",
299
+ hide: !1
300
+ };
282
301
  case "avatar_unavailable": return {
283
302
  problem: "unavailable",
284
303
  hide: !1
@@ -309,7 +328,7 @@ function b(e) {
309
328
  };
310
329
  }
311
330
  }
312
- function x(e) {
331
+ function C(e) {
313
332
  return e === "unavailable" ? {
314
333
  problem: "capacity",
315
334
  hide: !1
@@ -319,4 +338,4 @@ function x(e) {
319
338
  };
320
339
  }
321
340
  //#endregion
322
- export { e as AVATAR_API_PATH, s as AvatarApiError, a as avatarApiBaseFromTtsEndpoint, x as avatarAvailabilityRefusal, c as avatarIdempotencyKey, b as avatarRefusalFor, y as createAvatarSessionClient };
341
+ export { e as AVATAR_API_PATH, s as AvatarApiError, a as avatarApiBaseFromTtsEndpoint, C as avatarAvailabilityRefusal, c as avatarIdempotencyKey, x as avatarRefusalFor, y as createAvatarSessionClient };
@@ -0,0 +1,43 @@
1
+ import { HostedSpeaker } from '../../speech/hostedVoiceCatalogue';
2
+ import { SpeechAudioOutput, SpeechVoiceLike } from '../../speech/types';
3
+ import { AvatarVoiceDefault, AvatarVoicePreference } from '../avatarPreferences';
4
+ /** The host's speaker per avatar id, validated (`liveAvatarVoicesFrom`). */
5
+ export type LiveAvatarVoices = ReadonlyMap<string, HostedSpeaker>;
6
+ /**
7
+ * The host's `avatarVoices` as this module reads them: only plausible avatar ids, each with a speaker the hosted tier offers. A host's
8
+ * JavaScript can pass anything, and a Map keeps an id such as `constructor` from reading an object's own machinery.
9
+ */
10
+ export declare function liveAvatarVoicesFrom(input: unknown): LiveAvatarVoices;
11
+ /** Everything the speaker depends on besides the person's choice. */
12
+ export interface AvatarSpeakerContext {
13
+ /** The host's default for a person who has not chosen (`createLiveAvatarSeam`'s `defaultAvatarVoiceMode`). */
14
+ readonly hostDefault?: AvatarVoiceDefault;
15
+ readonly avatarVoices: LiveAvatarVoices;
16
+ /** The avatar the reply goes to: the one the person started. */
17
+ readonly avatarId: string | null;
18
+ }
19
+ /** The speaker the avatar's replies use now, or null: the assistant's own voice. */
20
+ export declare function avatarSpeakerFor(preference: AvatarVoicePreference, context: AvatarSpeakerContext): HostedSpeaker | null;
21
+ /**
22
+ * `speaker` in the language of a reply (`lang`, the app's tag), as the engine lists it; null when the vendor has no voice for that
23
+ * language, or the engine does not offer it now (a hosted engine latched off lists no hosted voice). Null keeps the assistant's voice.
24
+ */
25
+ export declare function hostedAvatarVoice(speaker: HostedSpeaker, lang: string, voices: readonly SpeechVoiceLike[]): SpeechVoiceLike | null;
26
+ /**
27
+ * ★★ The avatar left mid-reply (switched off, its session ended, or its output failed): the rest of that reply plays on the speakers
28
+ * (`SpeechAudioOutput`'s leave rule), and must not go on in the AVATAR's voice there. `restore` is called once, the moment the avatar
29
+ * stops being the reply's destination, so every piece synthesised AFTER it is asked for in the assistant's voice. That moment is the
30
+ * first of:
31
+ * - `leaves` reporting it: the session the reply went to is no longer ready (the controller's own state, which changes in the same
32
+ * call that ends the session, before the output answers anything);
33
+ * - the output rejecting a `write` or `drain` (a leave, a failure, or a refusal, after which nothing plays at all).
34
+ *
35
+ * What cannot be changed: the pieces already synthesised when the avatar left. The hosted engine never synthesises a piece twice
36
+ * (a fallback replays the bytes it has) and keeps one piece ahead, so at most two pieces are in the avatar's voice: the one handed to the
37
+ * avatar (not played again when it may have been heard there, R4) and the one fetched after it, which plays on the speakers. Every later
38
+ * piece is in the assistant's voice.
39
+ *
40
+ * The reply over (its last `drain` resolved, or `cancel()`), nothing is watched any more. The output's calls are passed through
41
+ * untouched, their promises included: the engine sees exactly what the output said.
42
+ */
43
+ export declare function restoreVoiceWhenOutputLeaves(output: SpeechAudioOutput, restore: () => void, leaves?: (onLeave: () => void) => () => void): SpeechAudioOutput;