@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
@@ -0,0 +1,13 @@
1
+ import { FC } from 'react';
2
+ export interface AvatarFaceProps {
3
+ /** The face's display name: the initials, and the frame picture's `alt`. */
4
+ readonly name: string;
5
+ /** The picture's URL, already sanitised (`sanitizeAvatarImages`); absent: the initials (pill) or nothing (frame). */
6
+ readonly src?: string | null;
7
+ readonly size: 'pill' | 'frame';
8
+ /** The pill is the selected one: drawn on the filled pill's colours. */
9
+ readonly selected?: boolean;
10
+ /** The picture failed to load, once per `src`. */
11
+ readonly onError?: (src: string) => void;
12
+ }
13
+ export declare const AvatarFace: FC<AvatarFaceProps>;
@@ -0,0 +1,46 @@
1
+ import { faceInitials as e } from "./avatarFaces.js";
2
+ import { useState as t } from "react";
3
+ import { jsx as n } from "react/jsx-runtime";
4
+ //#region src/shared/assistant/avatar/live/AvatarFace.tsx
5
+ var r = "relative inline-flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-full", i = ({ name: i, src: a, size: o, selected: s = !1, onError: c }) => {
6
+ let [l, u] = t(null), d = typeof a == "string" && a.length > 0 && l !== a ? a : null, f = (e) => {
7
+ u(e), c?.(e);
8
+ };
9
+ return o === "frame" ? d === null ? null : /* @__PURE__ */ n("img", {
10
+ src: d,
11
+ alt: i,
12
+ width: 320,
13
+ height: 320,
14
+ loading: "lazy",
15
+ decoding: "async",
16
+ draggable: !1,
17
+ referrerPolicy: "no-referrer",
18
+ onError: () => f(d),
19
+ className: "absolute inset-0 size-full object-cover",
20
+ "data-testid": "assistant-avatar-face-frame"
21
+ }) : d === null ? /* @__PURE__ */ n("span", {
22
+ "aria-hidden": "true",
23
+ className: `${r} border text-sm font-medium ${s ? "border-transparent bg-action-primary-text text-action-primary-bg" : "border-border-subtle bg-bg-sunken text-text-secondary"}`,
24
+ "data-testid": "assistant-avatar-face",
25
+ "data-face": "initials",
26
+ children: e(i)
27
+ }) : /* @__PURE__ */ n("span", {
28
+ className: `${r} ring-1 ${s ? "ring-action-primary-text/40" : "ring-border-subtle"}`,
29
+ "data-testid": "assistant-avatar-face",
30
+ "data-face": "picture",
31
+ children: /* @__PURE__ */ n("img", {
32
+ src: d,
33
+ alt: "",
34
+ width: 32,
35
+ height: 32,
36
+ loading: "lazy",
37
+ decoding: "async",
38
+ draggable: !1,
39
+ referrerPolicy: "no-referrer",
40
+ onError: () => f(d),
41
+ className: "size-full object-cover"
42
+ })
43
+ });
44
+ };
45
+ //#endregion
46
+ export { i as AvatarFace };
@@ -16,6 +16,8 @@ export interface LiveAvatarTabViewProps extends LiveAvatarTabProps {
16
16
  readonly controller: LiveAvatarController;
17
17
  /** Display names for the avatar ids the server offers; without one the chooser numbers them. */
18
18
  readonly avatarNames?: Readonly<Record<string, string>>;
19
+ /** BOFF-9362: a picture per avatar id, already sanitised (`sanitizeAvatarImages`); a face without one shows its initials. */
20
+ readonly avatarImages?: ReadonlyMap<string, string>;
19
21
  /** The host's speaker per avatar ("Default for this avatar"). */
20
22
  readonly avatarVoices: LiveAvatarVoices;
21
23
  readonly defaultAvatarVoiceMode?: AvatarVoiceDefault;
@@ -4,270 +4,297 @@ import { useAssistantSpeech as ee } from "../../ui/AssistantSpeechProvider.js";
4
4
  import { resolveAvatarVoiceMode as te } from "../avatarPreferences.js";
5
5
  import "./liveAvatarController.js";
6
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";
7
+ import { AvatarFace as r } from "./AvatarFace.js";
8
+ import { hostedAvatarVoice as i } from "./avatarVoice.js";
9
+ import { useCallback as a, useEffect as o, useId as s, useLayoutEffect as re, useRef as c, useState as l, useSyncExternalStore as u } from "react";
10
+ import { Volume2 as ie } from "lucide-react";
11
+ import { jsx as d, jsxs as f } from "react/jsx-runtime";
11
12
  //#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];
13
+ var p = "mine", m = "paired", h = "speaker:", g = /-Chirp3-HD-([A-Za-z]+)$/;
14
+ function _(e) {
15
+ let n = g.exec(e)?.[1];
15
16
  return n !== void 0 && t(n) ? n : e;
16
17
  }
17
- function _(e, t) {
18
+ function v(e, t) {
18
19
  let n = t.find((e) => e.source === "hosted")?.lang;
19
- return n === void 0 ? null : r(e, n, t);
20
+ return n === void 0 ? null : i(e, n, t);
20
21
  }
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 ? {
22
+ var y = "text-xs font-medium text-text-secondary", b = "text-xs text-text-muted", x = "focus:outline-none focus-visible:ring-2 focus-visible:ring-border-strong", S = `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 ${x}`, 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 ${x}`, oe = "relative inline-flex min-h-11 min-w-0 max-w-full cursor-pointer items-center gap-2 rounded-full border ps-1.5 pe-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", C = ({ controller: i, shown: g, voice: C, handsFreeOff: ce, leaveHintId: le, startVoiceRef: ue, askLine: w, avatarNames: de, avatarImages: fe, avatarVoices: pe, defaultAvatarVoiceMode: me, voiceStore: T, now: E = Date.now }) => {
23
+ let D = n(), O = ee(), k = s(), he = s(), A = s(), j = s(), M = s(), N = c(null), P = u(i.subscribe, i.getSnapshot, i.getSnapshot), F = u(T.subscribe, T.getState, T.getState);
24
+ o(() => {
25
+ if (g) return i.setStageMounted(!0), () => i.setStageMounted(!1);
26
+ }, [i, g]), o(() => {
27
+ let e = i.getSnapshot();
28
+ e.phase === "off" && e.avatars.length === 0 && i.requestOn({ start: !1 });
29
+ }, [i]);
30
+ let { phase: I } = P, L = I === "starting" || I === "connecting" || I === "ready" || I === "ending", [R, z] = l(!1);
31
+ R && (I === "off" || I === "choosing" || I === "ready" || I === "ending") && z(!1);
32
+ let B = E(), ge = P.retryAt !== null && P.retryAt > B, V = ne(P, D, B), H = P.selectedAvatarId, U = (e) => {
33
+ let t = P.avatars.indexOf(e);
34
+ return de?.[e] ?? (t === -1 ? D.chooserNone : `${D.chooserLabel} ${t + 1}`);
35
+ }, W = I === "ready" ? { kind: "turn-off" } : I === "ending" ? { kind: "ending" } : R || I === "starting" || I === "connecting" ? { kind: "starting" } : P.offered ? {
35
36
  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) => {
37
+ ready: !ge && H !== null && (I === "choosing" || I === "off" && P.avatars.includes(H))
38
+ } : null, G = c(null), K = W?.kind === "turn-off";
39
+ re(() => {
40
+ G.current = K ? E() : null;
41
+ }, [K, E]);
42
+ let _e = a((e) => {
42
43
  if (e !== null) return () => {
43
- e.ownerDocument.activeElement === e && P.current?.focus();
44
+ e.ownerDocument.activeElement === e && N.current?.focus();
44
45
  };
45
- }, []), me = () => {
46
- if (G !== null) switch (G.kind) {
46
+ }, []), ve = () => {
47
+ if (W !== null) switch (W.kind) {
47
48
  case "turn-on":
48
- if (!G.ready || U === null) return;
49
- B(!0), L === "choosing" ? r.confirm() : r.requestOn({
49
+ if (!W.ready || H === null) return;
50
+ z(!0), I === "choosing" ? i.confirm() : i.requestOn({
50
51
  start: !0,
51
- avatarId: U
52
+ avatarId: H
52
53
  });
53
54
  return;
54
55
  case "turn-off": {
55
- let e = K.current;
56
+ let e = G.current;
56
57
  if (e !== null && E() - e < 1e3) return;
57
- r.switchOff();
58
+ i.switchOff();
58
59
  return;
59
60
  }
60
61
  case "starting":
61
62
  case "ending": return;
62
63
  }
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) {
64
+ }, q = te(F, me), J = H === null ? null : pe.get(H) ?? null, Y = q === "speaker" && F.avatarSpeaker !== null ? `${h}${F.avatarSpeaker}` : q === "paired" && J !== null ? m : p, ye = Y === m ? J : Y.startsWith(h) ? F.avatarSpeaker : null, X = Y === p ? O.activeVoiceName : ye === null ? null : v(ye, O.voiceChoices)?.name ?? null, be = O.speakingMessageId !== null || O.spokenChunks !== null, xe = X !== null && !C.live && !be, Se = O.activeVoiceName ? D.voiceModeMineNamed(_(O.activeVoiceName)) : D.voiceModeMine, Ce = (e) => {
65
+ if (e === p || e === m) {
65
66
  T.setVoiceMode(e);
66
67
  return;
67
68
  }
68
69
  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,
70
+ e.startsWith(h) && t(n) && T.setAvatarSpeaker(n);
71
+ }, we = I === "ending" ? D.stateEnding : I === "connecting" || I === "ready" ? D.stageWaiting : I === "starting" ? D.stateStarting : D.stateOff, [Te, Ee] = l(() => /* @__PURE__ */ new Set()), De = a((e) => {
72
+ Ee((t) => t.has(e) ? t : new Set(t).add(e));
73
+ }, []), Z = (e) => {
74
+ let t = fe?.get(e);
75
+ return t === void 0 || Te.has(t) ? null : t;
76
+ }, Q = !L && H !== null ? Z(H) : null, $ = L ? null : w === void 0 ? C.impossible ? D.askNeedsVoice : null : w === "by-typing" ? D.askByTyping : w === "needs-voice" ? D.askNeedsVoice : w === "nothing" ? D.askNothing : null, Oe = I === "off" && P.avatars.length === 0 && P.problem !== null && P.offered;
77
+ return /* @__PURE__ */ f("div", {
78
+ ref: N,
73
79
  tabIndex: -1,
74
80
  className: "flex min-h-full shrink-0 flex-col gap-3 focus:outline-none",
75
81
  "data-testid": "assistant-avatar-tab-view",
76
82
  children: [
77
- /* @__PURE__ */ u("div", {
83
+ /* @__PURE__ */ d("div", {
78
84
  className: "flex max-h-80 min-h-[7.5rem] grow basis-30 justify-center",
79
- children: /* @__PURE__ */ d("div", {
85
+ children: /* @__PURE__ */ f("div", {
80
86
  className: "relative aspect-square h-full max-w-full overflow-hidden rounded-2xl border border-border-subtle bg-bg-sunken",
81
87
  "data-testid": "assistant-avatar-tab-frame",
82
- "data-phase": L,
88
+ "data-phase": I,
83
89
  children: [
84
- R ? /* @__PURE__ */ u("video", {
85
- ref: r.attachVideo,
86
- className: `size-full object-cover ${F.videoReady ? "" : "invisible"}`,
90
+ L ? /* @__PURE__ */ d("video", {
91
+ ref: i.attachVideo,
92
+ className: `size-full object-cover ${P.videoReady ? "" : "invisible"}`,
87
93
  "aria-label": D.stageLabel,
88
94
  autoPlay: !0,
89
95
  muted: !0,
90
96
  playsInline: !0,
91
97
  "data-testid": "assistant-avatar-tab-video"
92
98
  }) : 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
99
+ Q !== null && H !== null ? /* @__PURE__ */ d(r, {
100
+ size: "frame",
101
+ name: U(H),
102
+ src: Q,
103
+ onError: De
104
+ }) : null,
105
+ L && P.videoReady ? null : /* @__PURE__ */ d("p", {
106
+ className: Q === null ? "absolute inset-0 flex items-center justify-center p-3 text-center text-sm text-text-secondary" : "absolute inset-x-0 bottom-0 bg-bg-elevated/90 px-3 py-1.5 text-center text-xs text-text-secondary",
107
+ "data-testid": "assistant-avatar-tab-frame-text",
108
+ children: we
96
109
  }),
97
- F.audioBlocked ? /* @__PURE__ */ d("button", {
110
+ P.audioBlocked ? /* @__PURE__ */ f("button", {
98
111
  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}`,
112
+ onClick: () => i.resumeAudio(),
113
+ 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 ${x}`,
101
114
  "data-testid": "assistant-avatar-tab-sound",
102
- children: [/* @__PURE__ */ u(l, {
115
+ children: [/* @__PURE__ */ d(ie, {
103
116
  className: "size-4 shrink-0",
104
117
  "aria-hidden": "true"
105
- }), /* @__PURE__ */ u("span", { children: D.problem("playback-blocked") })]
118
+ }), /* @__PURE__ */ d("span", { children: D.problem("playback-blocked") })]
106
119
  }) : null
107
120
  ]
108
121
  })
109
122
  }),
110
- /* @__PURE__ */ u("p", {
111
- className: H === null ? "sr-only" : "text-center text-sm text-text-secondary",
123
+ /* @__PURE__ */ d("p", {
124
+ className: V === null ? "sr-only" : "text-center text-sm text-text-secondary",
112
125
  role: "status",
113
126
  "data-testid": "assistant-avatar-tab-status",
114
- children: H ?? ""
127
+ children: V ?? ""
115
128
  }),
116
- F.avatars.length > 1 && !R ? /* @__PURE__ */ d("fieldset", {
129
+ P.avatars.length > 1 && !L ? /* @__PURE__ */ f("fieldset", {
117
130
  className: "flex min-w-0 flex-col gap-1.5",
118
131
  "data-testid": "assistant-avatar-tab-chooser",
119
- children: [/* @__PURE__ */ u("legend", {
120
- className: v,
132
+ children: [/* @__PURE__ */ d("legend", {
133
+ className: y,
121
134
  children: D.chooserTitle
122
- }), /* @__PURE__ */ u("div", {
135
+ }), /* @__PURE__ */ d("div", {
123
136
  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", {
137
+ children: P.avatars.map((e) => {
138
+ let t = e === H, n = U(e);
139
+ return /* @__PURE__ */ f("label", {
127
140
  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
141
  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
- })]
142
+ children: [
143
+ /* @__PURE__ */ d("input", {
144
+ type: "radio",
145
+ name: he,
146
+ value: e,
147
+ checked: t,
148
+ onChange: () => i.selectAvatar(e),
149
+ className: se,
150
+ "data-avatar-id": e
151
+ }),
152
+ /* @__PURE__ */ d(r, {
153
+ size: "pill",
154
+ name: n,
155
+ src: Z(e),
156
+ selected: t,
157
+ onError: De
158
+ }),
159
+ /* @__PURE__ */ d("span", {
160
+ className: "truncate",
161
+ children: n
162
+ })
163
+ ]
141
164
  }, e);
142
165
  })
143
166
  })]
144
- }) : F.avatars.length > 0 && U !== null ? /* @__PURE__ */ d("div", {
167
+ }) : P.avatars.length > 0 && H !== null ? /* @__PURE__ */ f("div", {
145
168
  className: "flex flex-col gap-0.5",
146
169
  "data-testid": "assistant-avatar-tab-chosen",
147
- children: [/* @__PURE__ */ d("p", {
170
+ children: [/* @__PURE__ */ f("p", {
148
171
  className: "flex min-w-0 items-baseline gap-2 text-sm",
149
- children: [/* @__PURE__ */ u("span", {
150
- className: v,
172
+ children: [/* @__PURE__ */ d("span", {
173
+ className: y,
151
174
  children: D.chooserLabel
152
- }), /* @__PURE__ */ u("span", {
175
+ }), /* @__PURE__ */ d("span", {
153
176
  className: "truncate text-text-primary",
154
- children: W(U)
177
+ children: U(H)
155
178
  })]
156
- }), R && F.avatars.length > 1 ? /* @__PURE__ */ u("p", {
157
- className: y,
179
+ }), L && P.avatars.length > 1 ? /* @__PURE__ */ d("p", {
180
+ className: b,
158
181
  children: D.chooserLocked
159
182
  }) : null]
160
- }) : L === "choosing" ? /* @__PURE__ */ u("p", {
161
- className: y,
183
+ }) : I === "choosing" ? /* @__PURE__ */ d("p", {
184
+ className: b,
162
185
  "data-testid": "assistant-avatar-tab-empty",
163
186
  children: D.chooserEmpty
164
187
  }) : null,
165
- /* @__PURE__ */ d("div", {
188
+ /* @__PURE__ */ f("div", {
166
189
  className: "flex flex-col gap-1",
167
190
  children: [
168
- /* @__PURE__ */ u("label", {
169
- htmlFor: j,
170
- className: v,
191
+ /* @__PURE__ */ d("label", {
192
+ htmlFor: A,
193
+ className: y,
171
194
  children: D.voiceModeLabel
172
195
  }),
173
- /* @__PURE__ */ d("div", {
196
+ /* @__PURE__ */ f("div", {
174
197
  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}`,
198
+ children: [/* @__PURE__ */ f("select", {
199
+ id: A,
200
+ value: Y,
201
+ onChange: (e) => Ce(e.target.value),
202
+ "aria-describedby": j,
203
+ 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 ${x}`,
181
204
  "data-testid": "assistant-avatar-tab-voice",
182
205
  children: [
183
- /* @__PURE__ */ u("option", {
184
- value: f,
185
- children: ge
186
- }),
187
- Y === null ? null : /* @__PURE__ */ u("option", {
206
+ /* @__PURE__ */ d("option", {
188
207
  value: p,
189
- children: D.voiceModeDefaultNamed(Y)
208
+ children: Se
190
209
  }),
191
- e.map((e) => /* @__PURE__ */ u("option", {
192
- value: `${m}${e}`,
210
+ J === null ? null : /* @__PURE__ */ d("option", {
211
+ value: m,
212
+ children: D.voiceModeDefaultNamed(J)
213
+ }),
214
+ e.map((e) => /* @__PURE__ */ d("option", {
215
+ value: `${h}${e}`,
193
216
  children: e
194
217
  }, e))
195
218
  ]
196
- }), /* @__PURE__ */ u("button", {
219
+ }), /* @__PURE__ */ d("button", {
197
220
  type: "button",
198
221
  onClick: () => {
199
- $ && Q !== null && O.previewVoice(Q);
222
+ xe && X !== null && O.previewVoice(X);
200
223
  },
201
- "aria-disabled": !$,
224
+ "aria-disabled": !xe,
202
225
  "aria-label": D.hearVoice,
203
226
  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}`,
227
+ 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 ${x}`,
205
228
  "data-testid": "assistant-avatar-tab-hear",
206
- children: /* @__PURE__ */ u(l, {
229
+ children: /* @__PURE__ */ d(ie, {
207
230
  className: "size-5",
208
231
  "aria-hidden": "true"
209
232
  })
210
233
  })]
211
234
  }),
212
- /* @__PURE__ */ u("p", {
213
- id: M,
214
- className: y,
235
+ /* @__PURE__ */ d("p", {
236
+ id: j,
237
+ className: b,
215
238
  children: D.voiceModeHint
216
239
  })
217
240
  ]
218
241
  }),
219
- R ? null : /* @__PURE__ */ u("p", {
242
+ L ? null : /* @__PURE__ */ d("p", {
220
243
  id: k,
221
244
  className: "text-xs text-text-secondary",
222
245
  "data-testid": "assistant-avatar-tab-disclosure",
223
246
  children: D.consent
224
247
  }),
225
- G === null ? null : /* @__PURE__ */ u("button", {
226
- ref: pe,
248
+ W === null ? null : /* @__PURE__ */ d("button", {
249
+ ref: _e,
227
250
  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,
251
+ onClick: ve,
252
+ "aria-disabled": W.kind === "turn-on" ? !W.ready : W.kind !== "turn-off",
253
+ "aria-describedby": W.kind === "turn-on" ? k : void 0,
254
+ className: W.kind === "turn-off" ? S : ae,
232
255
  "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
256
+ "data-slot": W.kind,
257
+ children: W.kind === "turn-off" ? D.stop : W.kind === "starting" ? D.turnOnStarting : W.kind === "ending" ? D.stateEnding : D.consentConfirm
235
258
  }),
236
- ye ? /* @__PURE__ */ u("button", {
259
+ Oe ? /* @__PURE__ */ d("button", {
237
260
  type: "button",
238
- onClick: () => r.requestOn({ start: !1 }),
239
- className: x,
261
+ onClick: () => i.requestOn({ start: !1 }),
262
+ className: S,
240
263
  "data-testid": "assistant-avatar-tab-retry",
241
264
  children: D.chooserRetry
242
265
  }) : null,
243
- S.live ? null : /* @__PURE__ */ d("div", {
266
+ C.live ? null : /* @__PURE__ */ f("div", {
244
267
  className: "flex flex-col gap-1",
245
- children: [/* @__PURE__ */ u("button", {
246
- ref: w,
268
+ children: [/* @__PURE__ */ d("button", {
269
+ ref: ue,
247
270
  type: "button",
248
271
  onClick: () => {
249
- S.blocked === null && S.start();
272
+ C.blocked === null && C.start();
250
273
  },
251
- "aria-disabled": S.blocked !== null,
252
- "aria-describedby": S.blocked === null ? void 0 : N,
253
- className: x,
274
+ "aria-disabled": C.blocked !== null,
275
+ "aria-describedby": C.blocked === null ? void 0 : M,
276
+ className: S,
254
277
  "data-testid": "assistant-avatar-tab-start-voice",
255
278
  children: D.startVoice
256
- }), S.blocked === null ? null : /* @__PURE__ */ u("p", {
257
- id: N,
258
- className: y,
279
+ }), C.blocked === null ? $ === null ? null : /* @__PURE__ */ d("p", {
280
+ className: b,
281
+ "data-testid": "assistant-avatar-tab-ask-note",
282
+ children: $
283
+ }) : /* @__PURE__ */ d("p", {
284
+ id: M,
285
+ className: b,
259
286
  "data-testid": "assistant-avatar-tab-voice-blocked",
260
- children: S.impossible && !R ? `${S.blocked} ${D.askNeedsVoice}` : S.blocked
287
+ children: $ === null ? C.blocked : `${C.blocked} ${$}`
261
288
  })]
262
289
  }),
263
- C && !F.voiceLive ? /* @__PURE__ */ u("p", {
264
- className: y,
290
+ ce && !P.voiceLive ? /* @__PURE__ */ d("p", {
291
+ className: b,
265
292
  "data-testid": "assistant-avatar-tab-hands-free",
266
293
  children: D.handsFreeOff
267
294
  }) : null,
268
- R && !F.heldByVoice ? /* @__PURE__ */ u("p", {
269
- id: ce,
270
- className: y,
295
+ L && !P.heldByVoice ? /* @__PURE__ */ d("p", {
296
+ id: le,
297
+ className: b,
271
298
  "data-testid": "assistant-avatar-tab-leave-hint",
272
299
  children: D.tabLeaveHint
273
300
  }) : null
@@ -275,4 +302,4 @@ var v = "text-xs font-medium text-text-secondary", y = "text-xs text-text-muted"
275
302
  });
276
303
  };
277
304
  //#endregion
278
- export { S as LiveAvatarTabView, _ as avatarPreviewVoice, g as avatarVoiceDisplayName };
305
+ export { C as LiveAvatarTabView, v as avatarPreviewVoice, _ as avatarVoiceDisplayName };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The face pictures of the Avatar tab's chooser and frame (BOFF-9362): the host's pictures, sanitised once, and the
3
+ * initials shown for a face without one. Pure, no React.
4
+ *
5
+ * ★★ NO PICTURE SHIPS IN FE-LIBS. The package is public on npm, and the rights to stills of the provider's faces are
6
+ * the operator's question. Each host imports its own pictures as hashed assets (`avatarImages`, next to its
7
+ * `avatarNames`); a host that gives none shows initials.
8
+ *
9
+ * ★★ A picture is a URL the page fetches, so only two shapes pass: a root-relative path (a hashed asset the app
10
+ * imported, `/assets/...`) or an `https://` URL, and only for an image: its path (the query and hash aside) ends in
11
+ * `.webp`, `.avif`, `.png`, `.jpg`, `.jpeg`, `.gif` or `.svg`. Everything else is dropped silently: `javascript:`,
12
+ * `http:`, a protocol-relative `//host`, `data:`, `blob:`, a page or an endpoint (`/api/logout`), an empty or
13
+ * non-string value, and an id that is not an avatar id. It is only ever an `<img>` source (an SVG there runs no
14
+ * script); the extension keeps a host's typo from making every chooser fetch a page.
15
+ *
16
+ * ★ A host trap: Vite inlines an imported asset under its `assetsInlineLimit` (4 KB by default) as a `data:` URL,
17
+ * which this drops, so a very small face file shows initials. The recommended 320 px WebP is well above it.
18
+ */
19
+ /** Is `value` a picture URL the chooser may load? */
20
+ export declare function isAvatarImageUrl(value: unknown): value is string;
21
+ /**
22
+ * The host's `avatarImages`, as the view reads it: copied once (a host that changes its object later changes nothing
23
+ * here), with every entry that is not an avatar id mapped to a picture URL dropped. Never throws.
24
+ */
25
+ export declare function sanitizeAvatarImages(raw: unknown): ReadonlyMap<string, string>;
26
+ /**
27
+ * The initials of a face's name, for the placeholder: the first grapheme of the first word and, when there is more
28
+ * than one, of the LAST word ("Mary Ann Lee" gives "ML"), upper-cased in the reader's locale. Whole graphemes, so a
29
+ * Tamil, Hindi or Arabic name and a combined character are never cut. An empty name gives "?".
30
+ */
31
+ export declare function faceInitials(name: string): string;
@@ -0,0 +1,33 @@
1
+ import { resolveStoredAvatarId as e } from "../avatarPreferences.js";
2
+ //#region src/shared/assistant/avatar/live/avatarFaces.ts
3
+ var t = /^\/(?![/\\])[^\\\s]*$/, n = /^https:\/\/[^/\\\s]+(?:[/?#][^\\\s]*)?$/i, r = /^https:\/\/[^/?#]*/i, i = /\.(?:webp|avif|png|jpe?g|gif|svg)$/i;
4
+ function a(e) {
5
+ let t = e.replace(r, ""), n = t.search(/[?#]/);
6
+ return n === -1 ? t : t.slice(0, n);
7
+ }
8
+ function o(e) {
9
+ return typeof e != "string" || e.length === 0 || e.length > 2048 || !t.test(e) && !n.test(e) ? !1 : i.test(a(e));
10
+ }
11
+ function s(t) {
12
+ let n = /* @__PURE__ */ new Map();
13
+ if (typeof t != "object" || !t || Array.isArray(t)) return n;
14
+ for (let [r, i] of Object.entries(t)) e(r) === r && o(i) && n.set(r, i);
15
+ return n;
16
+ }
17
+ var c;
18
+ function l() {
19
+ return c === void 0 && (c = typeof Intl < "u" && typeof Intl.Segmenter == "function" ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null), c;
20
+ }
21
+ function u(e) {
22
+ let t = l();
23
+ if (t !== null) for (let { segment: n } of t.segment(e)) return n;
24
+ return Array.from(e)[0] ?? "";
25
+ }
26
+ function d(e) {
27
+ let t = e.trim().split(/\s+/u).filter((e) => e.length > 0), n = t[0];
28
+ if (n === void 0) return "?";
29
+ let r = t.length > 1 ? t[t.length - 1] : void 0;
30
+ return (u(n) + (r === void 0 ? "" : u(r))).toLocaleUpperCase();
31
+ }
32
+ //#endregion
33
+ export { d as faceInitials, o as isAvatarImageUrl, s as sanitizeAvatarImages };
@@ -10,6 +10,18 @@ import { LiveAvatarController, LiveAvatarControllerDeps, LiveAvatarLogEvent, Liv
10
10
  export interface LiveAvatarSeamOptions {
11
11
  /** Display names for the avatar ids the server offers (it sends ids only); without one the chooser numbers them. */
12
12
  readonly avatarNames?: Readonly<Record<string, string>>;
13
+ /**
14
+ * BOFF-9362: a picture per avatar id, shown in the Avatar tab's chooser and, while the avatar is off, in its frame. Host config,
15
+ * like `avatarNames` (the server sends ids only): a root-relative path (a hashed asset the app imported) or an `https://` URL, whose
16
+ * path ends in an image extension (`.webp`, `.avif`, `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`). An id with no entry, or a value of
17
+ * any other shape (`data:`, `blob:`, `http:`, `//host`, `javascript:`, a page), shows the face's initials. Copied once, here: a host
18
+ * that changes its object later changes nothing. ★ fe-libs ships no picture (it is a public package).
19
+ *
20
+ * ★ Vite inlines an imported asset under `build.assetsInlineLimit` (4 KB by default) as a `data:` URL, which is dropped here: a
21
+ * very small face file shows initials with no warning. Keep each face file above the limit (the recommended 320 px WebP is), or
22
+ * exclude the faces folder from inlining.
23
+ */
24
+ readonly avatarImages?: Readonly<Record<string, string>>;
13
25
  /**
14
26
  * The speaker each avatar id speaks with as "Default for this avatar" (BOFF-9341): host config, never inferred. An id without one has
15
27
  * no Default, and a value that is not a hosted speaker is ignored.