@burdenoff/fe-libs 2026.1013.8 → 2026.1013.10

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.
@@ -1,5 +1,6 @@
1
1
  import { ComponentType, FC, ReactNode } from 'react';
2
2
  import { SpeechEngine } from '../speech/types';
3
+ import { VoiceView } from './voice/AssistantVoiceSession';
3
4
  export interface LiveAvatarStageProps {
4
5
  /** Full-screen (true) or the in-panel stage (false). */
5
6
  expanded: boolean;
@@ -11,6 +12,42 @@ export interface AssistantLiveAvatarSeam {
11
12
  decorateSpeechEngine: (base: SpeechEngine) => SpeechEngine;
12
13
  /** Drawn by the voice stage instead of the waveform. Optional. */
13
14
  Stage?: ComponentType<LiveAvatarStageProps>;
15
+ /**
16
+ * The avatar's ON switch. Optional. It is SHOWN only when `Stage` is given,
17
+ * since without a stage there is nothing to show; told when a voice
18
+ * conversation is live, and switched OFF at the end of every one, either way.
19
+ */
20
+ enabled?: LiveAvatarEnabledStore;
21
+ }
22
+ /**
23
+ * The avatar's ON flag as a store, read with `useSyncExternalStore`: the voice
24
+ * session subscribes and nothing above it re-renders. It must be STABLE, and
25
+ * the flag it reads must be session-only, never persisted (the avatar
26
+ * preferences' `avatarEnabled`), so every page load starts OFF.
27
+ *
28
+ * ★★ ON is only ever the person's own click, on a control the host offers
29
+ * only while `voiceLive` says a voice conversation is live, and a paid session
30
+ * starts only then: no avatar after its conversation has ended. The panel
31
+ * switches the avatar OFF, never ON.
32
+ */
33
+ export interface LiveAvatarEnabledStore {
34
+ subscribe: (onChange: () => void) => () => void;
35
+ getSnapshot: () => boolean;
36
+ /**
37
+ * Switch the avatar OFF with the SAME action the host's own Avatar control
38
+ * takes, so its paid session ends along the one path the host already has.
39
+ * Called at the end of EVERY voice conversation, even when the avatar is
40
+ * already OFF; again for any ON while a failed one is still on screen; and
41
+ * by the compact view's stop. So it must be idempotent and cheap. It touches
42
+ * no audio: a reply already playing is the speech engine's to finish.
43
+ */
44
+ switchOff: () => void;
45
+ /**
46
+ * Told `true` once a voice conversation is live and `false` when it is over
47
+ * (just before that end's `switchOff`). Outside that window the host offers
48
+ * no ON control and starts no paid session. Idempotent, as `switchOff`.
49
+ */
50
+ voiceLive: (live: boolean) => void;
14
51
  }
15
52
  export declare const LiveAvatarSeamProvider: FC<{
16
53
  value?: AssistantLiveAvatarSeam;
@@ -26,3 +63,83 @@ export declare function useLiveAvatarStage(): ComponentType<LiveAvatarStageProps
26
63
  * is to the store `AssistantSpeechProvider` already subscribes to.
27
64
  */
28
65
  export declare function useLiveAvatarSpeechEngine(seam: AssistantLiveAvatarSeam | undefined): SpeechEngine | undefined;
66
+ /**
67
+ * The voice view to SHOW, given the one the user CHOSE; and the avatar kept to
68
+ * the live conversation (`ended`: over, but still on screen to say why it
69
+ * failed; leaving the screen ends it too).
70
+ *
71
+ * ★★ Switching the avatar ON must show it, and every session opens in the
72
+ * compact view (`mini`), which draws no stage. So while the avatar is ON, a
73
+ * session the user left compact shows the in-panel stage: the same session,
74
+ * re-rendered with the stage the size button already leads to, so nothing above
75
+ * the stage remounts. Switched OFF, it shows their choice again.
76
+ *
77
+ * ★★ The user's choice is never written, and nothing is stored: the next session
78
+ * opens compact as before, with the avatar OFF.
79
+ *
80
+ * ★ The moment they choose a view themselves while it is held (the size button,
81
+ * the change chip, an attachment), that view is theirs and stays when the
82
+ * avatar goes off. A conversation that is over lets go at once too: its
83
+ * failure is explained in the user's view, not first on a stage going away.
84
+ *
85
+ * ★ Adjusted DURING RENDER, as `AIAssistantPanel` adjusts its preview flag: no
86
+ * frame shows the stale view, and the update is conditional on the state it
87
+ * replaces, so it cannot loop.
88
+ */
89
+ export declare function useLiveAvatarVoiceView(chosen: VoiceView, ended: boolean): VoiceView;
90
+ /**
91
+ * @internal Exported for `VoiceMiniController` alone; not part of the module's
92
+ * public surface (the assistant barrel star-exports this module).
93
+ *
94
+ * Whether the compact strip shows `LiveAvatarMiniIndicator`: an avatar that is
95
+ * ON and can be shown. `VoiceMiniController` keeps its bottom band for the
96
+ * chip then, and ONLY then; false, it adds nothing and is the strip it was.
97
+ */
98
+ export declare function useLiveAvatarMiniIndicatorShown(): boolean;
99
+ /**
100
+ * The compact strip's sign that a live avatar is ON, with its stop.
101
+ *
102
+ * ★★ The strip draws no stage, so a person who chose it while the avatar is on
103
+ * would not see that a session, billed by the minute, is still running.
104
+ * Without an avatar that is ON and can be shown it renders nothing, and the
105
+ * strip is byte for byte what it was.
106
+ *
107
+ * ★★ Stop switches the AVATAR off through `switchOff`, the action the host's
108
+ * own Avatar control takes, called with NO arguments (its contract is
109
+ * `() => void`, so a host's own optional parameter never receives the click
110
+ * event). It never ends the voice conversation and never touches audio: the
111
+ * speech engine stays the only speaker. Focus on it goes to the session's
112
+ * microphone control (End while that is disabled), never to <body>, whoever
113
+ * switched the avatar off; to End when the chip goes with its strip.
114
+ *
115
+ * ★★ It handles no keys: Escape and Tab stay the session's (BOFF-8065). It
116
+ * comes after the microphone control, so Tab meets the strip's own control
117
+ * first.
118
+ *
119
+ * ★ "Avatar on" is a state, not a label. The control is an icon named by
120
+ * `avatar.stop`, so no button is named for the opposite of what it does, and
121
+ * its name is all of its words (WCAG 2.5.3, as the session's End).
122
+ *
123
+ * ★★ In the strip's bottom band, which the strip keeps free while this shows
124
+ * (`VoiceMiniController`: its caption column stops above the band, and shows
125
+ * fewer caption lines when two notices leave no room for three), so nothing
126
+ * on the strip overlaps it. Positioned against the voice session as its
127
+ * corner controls are, under the caption column: the session's `px-3` plus the
128
+ * strip's `px-4`, physical `left-*` as `VoiceStatusLine` explains, mirrored
129
+ * under `rtl:` so it never sits under the microphone. Its padding is logical
130
+ * (`ps-*`/`pe-*`, as `AssistantUiOfferLink`'s), so the label keeps 10px from
131
+ * the rounded start and the stop 2px from the end in RTL too. No wider than
132
+ * that column plus 6px of the gap after it (`max-w-*`: 100% less 3.5rem of
133
+ * paddings, the 0.75rem gap and the microphone's 10rem `w-40`, plus 0.375rem),
134
+ * so its stop's target (below) ends at least 6px before the microphone, in
135
+ * either direction, however long a translation of "Avatar on" is.
136
+ *
137
+ * ★ As tall as End in the strip (`h-8`), its stop drawn at 28px; the stop's
138
+ * TARGET is 44px square (`after:-inset-2`, WCAG 2.5.5), and `bottom-1.5` ends
139
+ * that target at the strip's own edge. It keeps a logical margin of 6px at its
140
+ * end (`after:me-1.5`), taken back at its start (`after:-ms-1.5`) so it stays
141
+ * 44px: it ends 2px past the drawn stop, short of the chip's edge, so a tap at
142
+ * the microphone's edge never lands on the stop. Logical, as the padding: it
143
+ * mirrors in RTL.
144
+ */
145
+ export declare const LiveAvatarMiniIndicator: FC;
@@ -1,17 +1,105 @@
1
1
  import { getSpeechEngine as e, subscribeSpeechEngine as t } from "../speech/createSpeechEngine.js";
2
- import { createContext as n, useContext as r, useMemo as i, useSyncExternalStore as a } from "react";
3
- import { jsx as o } from "react/jsx-runtime";
2
+ import { useAvatarCopy as n } from "../avatar/useAvatarCopy.js";
3
+ import { ASSISTANT_VOICE_SESSION_ATTR as r } from "./panelOverlays.js";
4
+ import { createContext as i, useContext as a, useEffect as o, useLayoutEffect as s, useMemo as c, useRef as l, useState as u, useSyncExternalStore as d } from "react";
5
+ import { VideoOff as f } from "lucide-react";
6
+ import { jsx as p, jsxs as m } from "react/jsx-runtime";
4
7
  //#region src/shared/assistant/ui/assistantLiveAvatarSeam.tsx
5
- var s = n(null), c = ({ value: e, children: t }) => /* @__PURE__ */ o(s.Provider, {
8
+ var h = i(null), g = ({ value: e, children: t }) => /* @__PURE__ */ p(h.Provider, {
6
9
  value: e ?? null,
7
10
  children: t
8
11
  });
9
- function l() {
10
- return r(s)?.Stage ?? null;
12
+ function _() {
13
+ return a(h)?.Stage ?? null;
11
14
  }
12
- function u(n) {
13
- let r = a(t, e, e), o = n?.decorateSpeechEngine;
14
- return i(() => o ? o(r) : void 0, [o, r]);
15
+ function v(n) {
16
+ let r = d(t, e, e), i = n?.decorateSpeechEngine;
17
+ return c(() => i ? i(r) : void 0, [i, r]);
15
18
  }
19
+ var y = {
20
+ subscribe: () => () => {},
21
+ getSnapshot: () => !1,
22
+ switchOff: () => {},
23
+ voiceLive: () => {}
24
+ };
25
+ function b(e) {
26
+ return e?.Stage ? e.enabled : void 0;
27
+ }
28
+ function x(e) {
29
+ let t = e ?? y;
30
+ return d(t.subscribe, t.getSnapshot, t.getSnapshot);
31
+ }
32
+ function S(e, t) {
33
+ let n = a(h);
34
+ C(n?.enabled, t);
35
+ let r = b(n), i = x(r), [o, s] = u(() => ({
36
+ on: i,
37
+ chosen: e,
38
+ open: i && e === "mini"
39
+ }));
40
+ if (!r) return e;
41
+ let c = o.open;
42
+ return (o.on !== i || o.chosen !== e) && (c = o.on !== i && i && e === "mini", s({
43
+ on: i,
44
+ chosen: e,
45
+ open: c
46
+ })), c && !t ? "panel" : e;
47
+ }
48
+ function C(e, t) {
49
+ let n = l(0);
50
+ o(() => {
51
+ if (!e) return;
52
+ let r = ++n.current, i = (e) => queueMicrotask(() => {
53
+ n.current === r && e();
54
+ }), a = () => i(() => {
55
+ e.voiceLive(!1), e.switchOff();
56
+ });
57
+ if (!t) return i(() => e.voiceLive(!0)), a;
58
+ a();
59
+ let o = e.getSnapshot();
60
+ return e.subscribe(() => {
61
+ let t = o;
62
+ o = e.getSnapshot(), o && !t && i(() => e.switchOff());
63
+ });
64
+ }, [e, t]);
65
+ }
66
+ function w() {
67
+ return x(b(a(h)));
68
+ }
69
+ var T = () => {
70
+ let e = b(a(h)), t = x(e);
71
+ return e && t ? /* @__PURE__ */ p(O, {
72
+ onStop: e.switchOff,
73
+ isOn: e.getSnapshot
74
+ }) : null;
75
+ }, E = "[data-testid=\"assistant-voice-orb\"]", D = "[data-testid=\"assistant-voice-end\"]", O = ({ onStop: e, isOn: t }) => {
76
+ let i = n(), a = l(null), o = l(t);
77
+ return s(() => {
78
+ let e = a.current, t = o.current;
79
+ return () => {
80
+ if (!e?.contains(e.ownerDocument.activeElement)) return;
81
+ let n = e.closest(`[${r}]`), i = t() ? null : n?.querySelector(E);
82
+ (i && !i.hasAttribute("disabled") ? i : n?.querySelector(D))?.focus();
83
+ };
84
+ }, []), /* @__PURE__ */ m("div", {
85
+ ref: a,
86
+ className: "absolute bottom-1.5 left-7 flex h-8 max-w-[calc(100%-13.875rem)] items-center gap-1 rounded-full border border-border-default bg-bg-elevated ps-2.5 pe-0.5 text-xs font-medium text-text-secondary rtl:left-auto rtl:right-7",
87
+ "data-testid": "assistant-voice-avatar",
88
+ children: [/* @__PURE__ */ p("span", {
89
+ className: "truncate",
90
+ children: i.stateOn
91
+ }), /* @__PURE__ */ p("button", {
92
+ type: "button",
93
+ onClick: () => e(),
94
+ className: "relative flex size-7 shrink-0 items-center justify-center rounded-full transition-colors after:absolute after:-inset-2 after:-ms-1.5 after:me-1.5 hover:bg-bg-raised hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-border-strong",
95
+ "aria-label": i.stop,
96
+ "data-testid": "assistant-voice-avatar-stop",
97
+ children: /* @__PURE__ */ p(f, {
98
+ className: "size-4",
99
+ "aria-hidden": "true"
100
+ })
101
+ })]
102
+ });
103
+ };
16
104
  //#endregion
17
- export { c as LiveAvatarSeamProvider, u as useLiveAvatarSpeechEngine, l as useLiveAvatarStage };
105
+ export { T as LiveAvatarMiniIndicator, g as LiveAvatarSeamProvider, w as useLiveAvatarMiniIndicatorShown, v as useLiveAvatarSpeechEngine, _ as useLiveAvatarStage, S as useLiveAvatarVoiceView };