@burdenoff/fe-libs 2026.1016.2 → 2026.1017.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 (49) 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 +29 -0
  16. package/dist/shared/assistant/i18nManifest.js +29 -0
  17. package/dist/shared/assistant/index.d.ts +8 -0
  18. package/dist/shared/assistant/speech/hostedTtsSpeechEngine.d.ts +6 -0
  19. package/dist/shared/assistant/speech/hostedTtsSpeechEngine.js +118 -101
  20. package/dist/shared/assistant/speech/notifyCancellationSpeechEngine.js +3 -0
  21. package/dist/shared/assistant/speech/replySpeechRequests.d.ts +84 -0
  22. package/dist/shared/assistant/speech/replySpeechRequests.js +70 -0
  23. package/dist/shared/assistant/speech/routingSpeechEngine.js +13 -9
  24. package/dist/shared/assistant/speech/types.d.ts +39 -1
  25. package/dist/shared/assistant/ui/AIAssistantPanel.js +128 -119
  26. package/dist/shared/assistant/ui/AssistantChatArea.js +1572 -1430
  27. package/dist/shared/assistant/ui/AssistantMessageItem.js +110 -100
  28. package/dist/shared/assistant/ui/AssistantSpeechProvider.d.ts +10 -0
  29. package/dist/shared/assistant/ui/AssistantSpeechProvider.js +286 -225
  30. package/dist/shared/assistant/ui/assistantLiveAvatarSeam.d.ts +40 -0
  31. package/dist/shared/assistant/ui/avatar/AssistantAvatarTab.d.ts +6 -0
  32. package/dist/shared/assistant/ui/avatar/AssistantAvatarTab.js +99 -52
  33. package/dist/shared/assistant/ui/avatar/AvatarTabAskBox.d.ts +8 -0
  34. package/dist/shared/assistant/ui/avatar/AvatarTabAskBox.js +87 -0
  35. package/dist/shared/assistant/ui/avatar/AvatarTabLastExchange.d.ts +13 -0
  36. package/dist/shared/assistant/ui/avatar/AvatarTabLastExchange.js +97 -0
  37. package/dist/shared/assistant/ui/avatar/avatarAskGate.d.ts +104 -0
  38. package/dist/shared/assistant/ui/avatar/avatarAskGate.js +39 -0
  39. package/dist/shared/assistant/ui/avatar/avatarTabDraft.d.ts +17 -0
  40. package/dist/shared/assistant/ui/avatar/avatarTabDraft.js +22 -0
  41. package/dist/shared/assistant/ui/avatar/avatarTabExchange.d.ts +47 -0
  42. package/dist/shared/assistant/ui/avatar/avatarTabExchange.js +50 -0
  43. package/dist/shared/assistant/ui/panelOverlays.d.ts +8 -1
  44. package/dist/shared/assistant/ui/panelOverlays.js +1 -1
  45. package/dist/shared/assistant/ui/useVoiceSession.js +222 -227
  46. package/dist/shared/assistant/voice/handsfree/playbackReference.d.ts +15 -4
  47. package/dist/shared/assistant/voice/handsfree/playbackReference.js +9 -6
  48. package/dist/shared-assistant.js +270 -262
  49. package/package.json +1 -1
@@ -36,9 +36,31 @@ export interface LiveAvatarTabOfferedStore {
36
36
  /** This browser can show the avatar (desktop Chrome first), and it was not refused on this page. */
37
37
  getSnapshot: () => boolean;
38
38
  }
39
+ /**
40
+ * ★ BOFF-9360: whether the avatar can take the answer to a typed question now (`liveAvatarAskState` maps the
41
+ * controller's snapshot): `ready`; `sound-blocked` (on, its sound blocked until a tap); `ending` (stopping, or near its
42
+ * time limit); `starting`; `locked` (off until the time a refusal named); `off`.
43
+ */
44
+ export type LiveAvatarAskState = 'off' | 'locked' | 'starting' | 'ready' | 'sound-blocked' | 'ending';
45
+ /** `LiveAvatarAskState` as a store read with `useSyncExternalStore`. STABLE. */
46
+ export interface LiveAvatarAskStore {
47
+ subscribe: (onChange: () => void) => () => void;
48
+ getSnapshot: () => LiveAvatarAskState;
49
+ }
39
50
  /** The Avatar tab's part of the seam (`createLiveAvatarSeam` builds it). STABLE, like the rest of the seam. */
40
51
  export interface LiveAvatarTabSeam {
41
52
  readonly offered: LiveAvatarTabOfferedStore;
53
+ /**
54
+ * ★ BOFF-9360: whether a typed question's answer can be spoken by the avatar now; the tab's ask box sends only
55
+ * while it is `ready`. Optional: a tab seam without it is read as `off`, so its box never sends.
56
+ *
57
+ * ★★ THE TYPED-QUESTION CONTRACT. Asking from the tab never starts, confirms, restarts or extends a session:
58
+ * nothing in that path calls `requestOn`, `confirm` or `setVoiceLive`, and a send changes no timer (a reply the
59
+ * avatar speaks re-arms its idle clock and holds the graceful end until it finishes, exactly as a voice reply does,
60
+ * always within the server's own end). D4 holds per reply: a reply goes to the avatar only when it starts while
61
+ * the tab is shown.
62
+ */
63
+ readonly ask?: LiveAvatarAskStore;
42
64
  /**
43
65
  * Told when the Avatar tab opens and closes; idempotent. Opening starts
44
66
  * nothing paid. Closing is the person's OFF for the tab's avatar, except one
@@ -68,6 +90,14 @@ export interface LiveAvatarTabVoice {
68
90
  /** Start it: the composer's own start (`startVoiceWithPrewarm`). Opens the microphone; never called by anything but its button. */
69
91
  readonly start: () => void;
70
92
  }
93
+ /**
94
+ * ★ BOFF-9360: what to say about asking under Turn on while the avatar is off (`avatarTabAskLine`, critic M3):
95
+ * - `none`: typing and voice can both ask (or will, once the avatar is on or the wait is over);
96
+ * - `by-typing`: voice cannot start here for a reason that lasts, but typing can;
97
+ * - `needs-voice`: typing is off here for a reason that lasts, but a voice conversation can ask;
98
+ * - `nothing`: neither can, so turning the avatar on here would be of no use.
99
+ */
100
+ export type LiveAvatarTabAskLine = 'none' | 'by-typing' | 'needs-voice' | 'nothing';
71
101
  /** What the panel hands the tab's `View`. */
72
102
  export interface LiveAvatarTabProps {
73
103
  /**
@@ -83,6 +113,11 @@ export interface LiveAvatarTabProps {
83
113
  readonly leaveHintId: string;
84
114
  /** The "Start a voice conversation" button: focus returns to it when a voice conversation started in the tab ends. */
85
115
  readonly startVoiceRef: RefObject<HTMLButtonElement | null>;
116
+ /**
117
+ * ★ BOFF-9360: the line about asking, when the tab has an ask box. Absent (no box): the line is today's, from
118
+ * `voice.impossible` alone ("…you ask it questions in a voice conversation").
119
+ */
120
+ readonly askLine?: LiveAvatarTabAskLine;
86
121
  }
87
122
  /** The ids the tab row and the two tab panels share (`AIAssistantPanel` makes them). */
88
123
  export interface LiveAvatarTabIds {
@@ -109,6 +144,11 @@ export interface LiveAvatarTabState {
109
144
  readonly ids?: LiveAvatarTabIds | null;
110
145
  /** A full-surface overlay (History, the preview) covers the panel: the tab under it is not shown (D4). */
111
146
  readonly covered?: boolean;
147
+ /**
148
+ * ★ BOFF-9360 (critic M5): the chat area hands the panel how to stop the tab's own answer while it is read aloud
149
+ * and the tab is shown, and null when that ends. The panel's Escape then stops the reading before it leaves the tab.
150
+ */
151
+ readonly setReplyEscape?: (stop: (() => void) | null) => void;
112
152
  }
113
153
  /**
114
154
  * @internal Provided by `AIAssistantPanel` and read by the assistant's own
@@ -1,5 +1,6 @@
1
1
  import { FC } from 'react';
2
2
  import { LiveAvatarTabSeam, LiveAvatarTabVoice } from '../assistantLiveAvatarSeam';
3
+ import { AvatarTabAsk } from './avatarAskGate';
3
4
  export interface AssistantAvatarTabProps {
4
5
  readonly tab: LiveAvatarTabSeam;
5
6
  /** This panel's id, and the Avatar tab that labels it. */
@@ -9,5 +10,10 @@ export interface AssistantAvatarTabProps {
9
10
  /** Nothing covers the tab (D4): see `LiveAvatarTabProps.shown`. */
10
11
  readonly shown: boolean;
11
12
  readonly voice: LiveAvatarTabVoice;
13
+ /**
14
+ * ★ BOFF-9360: typed questions: the gate, the send and the last exchange
15
+ * (`AssistantChatArea`), for the tab's ask box. Absent: no ask box.
16
+ */
17
+ readonly ask?: AvatarTabAsk;
12
18
  }
13
19
  export declare const AssistantAvatarTab: FC<AssistantAvatarTabProps>;
@@ -1,70 +1,117 @@
1
- import { ASSISTANT_VOICE_SESSION_ATTR as e } from "../panelOverlays.js";
2
- import { useLiveAvatarHandsFreeGuard as t } from "../assistantLiveAvatarSeam.js";
3
- import { useEffect as n, useLayoutEffect as r, useRef as i, useState as a } from "react";
4
- import { jsx as o } from "react/jsx-runtime";
1
+ import { clearReplyRequests as e } from "../../speech/replySpeechRequests.js";
2
+ import { useAvatarCopy as t } from "../../avatar/useAvatarCopy.js";
3
+ import { ASSISTANT_VOICE_SESSION_ATTR as n } from "../panelOverlays.js";
4
+ import { useLiveAvatarHandsFreeGuard as r } from "../assistantLiveAvatarSeam.js";
5
+ import { avatarAskBlockerLasts as i } from "./avatarAskGate.js";
6
+ import { AvatarTabAskBox as a } from "./AvatarTabAskBox.js";
7
+ import { AvatarTabLastExchange as o } from "./AvatarTabLastExchange.js";
8
+ import { useAvatarTabExchange as s } from "./avatarTabExchange.js";
9
+ import { useEffect as c, useLayoutEffect as l, useRef as u, useState as d } from "react";
10
+ import { jsx as f, jsxs as p } from "react/jsx-runtime";
5
11
  //#region src/shared/assistant/ui/avatar/AssistantAvatarTab.tsx
6
- var s = 180;
7
- function c(t, n) {
8
- let [i, o] = a(0);
9
- return r(() => {
10
- if (!n) return;
11
- let r = t.current?.parentElement?.querySelector(`[${e}]`);
12
+ var m = 180;
13
+ function h(e, t) {
14
+ let [r, i] = d(0);
15
+ return l(() => {
16
+ if (!t) return;
17
+ let r = e.current?.parentElement?.querySelector(`[${n}]`);
12
18
  if (!r) return;
13
- let i = () => {
19
+ let a = () => {
14
20
  let e = typeof r.getBoundingClientRect == "function" ? r.getBoundingClientRect().height : 0;
15
- o(Math.round(e));
21
+ i(Math.round(e));
16
22
  };
17
- if (i(), typeof ResizeObserver > "u") return;
18
- let a = new ResizeObserver(i);
19
- return a.observe(r), () => a.disconnect();
20
- }, [t, n]), n ? i > 0 ? i : s : 0;
23
+ if (a(), typeof ResizeObserver > "u") return;
24
+ let o = new ResizeObserver(a);
25
+ return o.observe(r), () => o.disconnect();
26
+ }, [e, t]), t ? r > 0 ? r : m : 0;
21
27
  }
22
- function l() {
28
+ function g() {
23
29
  if (typeof document > "u") return !1;
24
30
  let e = document.activeElement;
25
31
  return e === null || e === document.body || !e.isConnected || e.hasAttribute("data-assistant-panel");
26
32
  }
27
- var u = ({ tab: e, id: a, labelledBy: s, leaveHintId: u, shown: d, voice: f }) => {
28
- let p = i(null), m = i(null), h = t(), g = c(p, f.live), _ = i(null);
29
- r(() => {
30
- let t = (_.current?.setup ?? 0) + 1;
31
- return _.current = {
32
- tab: e,
33
- setup: t
34
- }, e.setOpen(!0), () => {
33
+ var _ = ({ state: e, restored: n }) => {
34
+ let r = t();
35
+ return /* @__PURE__ */ f("p", {
36
+ className: "sr-only",
37
+ "aria-live": "polite",
38
+ "aria-atomic": "true",
39
+ "data-testid": "assistant-avatar-tab-ask-live",
40
+ children: e === "thinking" ? r.askThinking : e === "not-spoken" ? r.askNotSpoken : e === "cut" ? r.askCut : e === "not-heard" ? r.askNotHeard : e === "failed" ? n ? r.askFailed : r.askFailedInChat : ""
41
+ });
42
+ }, v = ({ tab: t, id: n, labelledBy: m, leaveHintId: v, shown: y, voice: b, ask: x }) => {
43
+ let S = u(null), C = u(null), w = u(null), T = r(), E = h(S, b.live), D = u(null);
44
+ l(() => {
45
+ let n = (D.current?.setup ?? 0) + 1;
46
+ return D.current = {
47
+ tab: t,
48
+ setup: n
49
+ }, t.setOpen(!0), () => {
35
50
  queueMicrotask(() => {
36
- let n = _.current;
37
- n !== null && n.tab === e && n.setup !== t || e.setOpen(!1);
51
+ let r = D.current;
52
+ r !== null && r.tab === t && r.setup !== n || (t.setOpen(!1), e());
38
53
  });
39
54
  };
40
- }, [e]);
41
- let v = i(f.live);
42
- return n(() => {
43
- let e = v.current;
44
- v.current = f.live, !(!e || f.live) && l() && m.current?.focus();
45
- }, [f.live]), /* @__PURE__ */ o("div", {
46
- ref: p,
55
+ }, [t]);
56
+ let O = u(b.live);
57
+ c(() => {
58
+ let e = O.current;
59
+ O.current = b.live, !(!e || b.live) && g() && w.current?.focus();
60
+ }, [b.live]);
61
+ let k = s(x?.last ?? null, x?.navigatingReplyId ?? null), A = b.live ? void 0 : x, [j, M] = d(null), N = k?.state === "failed" ? k.exchange.replyId : null, P = k?.question ?? "", F = x?.draft;
62
+ l(() => {
63
+ if (N === null || F === void 0 || j?.replyId === N) return;
64
+ let e = F.get().trim() === "";
65
+ e && F.set(P), M({
66
+ replyId: N,
67
+ restored: e
68
+ });
69
+ }, [
70
+ N,
71
+ P,
72
+ F,
73
+ j
74
+ ]);
75
+ let I = j === null || j.replyId !== N || j.restored;
76
+ return /* @__PURE__ */ p("div", {
77
+ ref: S,
47
78
  role: "tabpanel",
48
- id: a,
49
- "aria-labelledby": s,
79
+ id: n,
80
+ "aria-labelledby": m,
50
81
  className: "absolute inset-0 z-[5] flex flex-col bg-bg-surface",
51
82
  "data-testid": "assistant-avatar-tab",
52
- children: /* @__PURE__ */ o("div", {
53
- className: "flex min-h-0 flex-1 flex-col overflow-y-auto px-4 pt-4",
54
- style: {
55
- paddingBottom: g + 16,
56
- scrollPaddingBottom: g + 16
57
- },
58
- "data-testid": "assistant-avatar-tab-column",
59
- children: /* @__PURE__ */ o(e.View, {
60
- shown: d,
61
- voice: f,
62
- handsFreeOff: h,
63
- leaveHintId: u,
64
- startVoiceRef: m
65
- })
66
- })
83
+ children: [
84
+ /* @__PURE__ */ p("div", {
85
+ className: "flex min-h-0 flex-1 flex-col overflow-y-auto px-4 pt-4",
86
+ style: {
87
+ paddingBottom: E + 16,
88
+ scrollPaddingBottom: E + 16
89
+ },
90
+ "data-testid": "assistant-avatar-tab-column",
91
+ children: [/* @__PURE__ */ f(t.View, {
92
+ shown: y,
93
+ voice: b,
94
+ handsFreeOff: T,
95
+ leaveHintId: v,
96
+ startVoiceRef: w,
97
+ ...x ? { askLine: x.line } : {}
98
+ }), A ? /* @__PURE__ */ f(o, {
99
+ view: k,
100
+ invite: !i(A.blocker),
101
+ restored: I,
102
+ focusAfterStop: C
103
+ }, k?.exchange.replyId ?? "none") : null]
104
+ }),
105
+ A ? /* @__PURE__ */ f(a, {
106
+ ask: A,
107
+ inputRef: C
108
+ }) : null,
109
+ x ? /* @__PURE__ */ f(_, {
110
+ state: k?.state ?? null,
111
+ restored: I
112
+ }) : null
113
+ ]
67
114
  });
68
115
  };
69
116
  //#endregion
70
- export { u as AssistantAvatarTab };
117
+ export { v as AssistantAvatarTab };
@@ -0,0 +1,8 @@
1
+ import { FC, RefObject } from 'react';
2
+ import { AvatarTabAsk } from './avatarAskGate';
3
+ export interface AvatarTabAskBoxProps {
4
+ readonly ask: AvatarTabAsk;
5
+ /** The textarea: where focus goes back to after a send, and after Stop. */
6
+ readonly inputRef: RefObject<HTMLTextAreaElement | null>;
7
+ }
8
+ export declare const AvatarTabAskBox: FC<AvatarTabAskBoxProps>;
@@ -0,0 +1,87 @@
1
+ import { useAvatarCopy as e } from "../../avatar/useAvatarCopy.js";
2
+ import { useAvatarTabDraft as t } from "./avatarTabDraft.js";
3
+ import { useId as n, useLayoutEffect as r, useRef as i, useState as a } from "react";
4
+ import { ArrowUp as o } from "lucide-react";
5
+ import { jsx as s, jsxs as c } from "react/jsx-runtime";
6
+ //#region src/shared/assistant/ui/avatar/AvatarTabAskBox.tsx
7
+ var l = "focus:outline-none focus-visible:ring-2 focus-visible:ring-border-strong", u = 229;
8
+ function d(e) {
9
+ return e.nativeEvent.isComposing === !0 || e.nativeEvent.keyCode === u;
10
+ }
11
+ function f(e) {
12
+ return e !== void 0 && Array.from(e).includes("Files");
13
+ }
14
+ var p = ({ ask: u, inputRef: p }) => {
15
+ let m = e(), h = n(), [g, _] = a(!1), v = i(!1);
16
+ r(() => {
17
+ v.current = !1;
18
+ });
19
+ let y = t(u.draft), b = u.blocker === null ? null : m.askBlocked(u.blocker), x = u.blocker === null && y.trim() !== "", S = b ?? (g ? m.askNoFiles : m.askHint), C = (e) => {
20
+ if (!(v.current || !x)) {
21
+ if (v.current = !0, !u.send(u.draft.get()).accepted) {
22
+ v.current = !1;
23
+ return;
24
+ }
25
+ u.draft.set(""), _(!1), e === "button" && p.current?.focus();
26
+ }
27
+ };
28
+ return /* @__PURE__ */ c("div", {
29
+ className: "shrink-0 border-t border-border-subtle bg-bg-surface px-4 pb-3 pt-2",
30
+ "data-testid": "assistant-avatar-tab-ask",
31
+ children: [/* @__PURE__ */ c("div", {
32
+ className: "flex items-end gap-2",
33
+ children: [/* @__PURE__ */ s("textarea", {
34
+ ref: p,
35
+ value: y,
36
+ onChange: (e) => {
37
+ u.draft.set(e.target.value), g && _(!1);
38
+ },
39
+ onKeyDown: (e) => {
40
+ if (d(e)) {
41
+ e.key === "Escape" && e.stopPropagation();
42
+ return;
43
+ }
44
+ e.key !== "Enter" || e.shiftKey || (e.preventDefault(), C("key"));
45
+ },
46
+ onDragOver: (e) => {
47
+ f(e.dataTransfer?.types) && e.preventDefault();
48
+ },
49
+ onDrop: (e) => {
50
+ f(e.dataTransfer?.types) && (e.preventDefault(), e.stopPropagation(), _(!0));
51
+ },
52
+ onPaste: (e) => {
53
+ let t = e.clipboardData;
54
+ f(t?.types) && (_(!0), Array.from(t.types).includes("text/plain") || e.preventDefault());
55
+ },
56
+ rows: 1,
57
+ dir: "auto",
58
+ "aria-label": m.askLabel,
59
+ "aria-describedby": h,
60
+ placeholder: m.askPlaceholder,
61
+ className: `field-sizing-content max-h-[7.5rem] min-h-11 min-w-0 flex-1 resize-none rounded-xl border border-border-default bg-bg-elevated px-3 py-2.5 text-sm text-text-primary placeholder:text-text-muted ${l}`,
62
+ "data-testid": "assistant-avatar-tab-ask-input"
63
+ }), /* @__PURE__ */ s("button", {
64
+ type: "button",
65
+ onClick: () => C("button"),
66
+ "aria-disabled": !x,
67
+ "aria-label": m.askSend,
68
+ "aria-describedby": h,
69
+ title: m.askSend,
70
+ className: `inline-flex size-11 shrink-0 items-center justify-center rounded-xl bg-action-primary-bg 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 ${l}`,
71
+ "data-testid": "assistant-avatar-tab-ask-send",
72
+ children: /* @__PURE__ */ s(o, {
73
+ className: "size-5",
74
+ "aria-hidden": "true"
75
+ })
76
+ })]
77
+ }), /* @__PURE__ */ s("p", {
78
+ id: h,
79
+ className: "mt-1 text-xs text-text-muted",
80
+ "data-testid": "assistant-avatar-tab-ask-hint",
81
+ "data-reason": u.blocker ?? void 0,
82
+ children: S
83
+ })]
84
+ });
85
+ };
86
+ //#endregion
87
+ export { p as AvatarTabAskBox };
@@ -0,0 +1,13 @@
1
+ import { FC, RefObject } from 'react';
2
+ import { AvatarTabExchangeView } from './avatarTabExchange';
3
+ export interface AvatarTabLastExchangeProps {
4
+ /** The last exchange, or null before the first. */
5
+ readonly view: AvatarTabExchangeView | null;
6
+ /** Say the empty prompt before the first question (not where typing is off for a reason that lasts). */
7
+ readonly invite: boolean;
8
+ /** A failed turn's question went back into the box (it was empty), so the line may say so. */
9
+ readonly restored: boolean;
10
+ /** Where focus goes when Stop leaves while holding it: the ask box. */
11
+ readonly focusAfterStop: RefObject<HTMLTextAreaElement | null>;
12
+ }
13
+ export declare const AvatarTabLastExchange: FC<AvatarTabLastExchangeProps>;
@@ -0,0 +1,97 @@
1
+ import { AssistantMarkdownRenderer as e } from "../AssistantMarkdownRenderer.js";
2
+ import { useAvatarCopy as t } from "../../avatar/useAvatarCopy.js";
3
+ import { useAssistantSpeech as n } from "../AssistantSpeechProvider.js";
4
+ import { useCallback as r, useId as i, useLayoutEffect as a, useRef as o, useState as s } from "react";
5
+ import { Square as c } from "lucide-react";
6
+ import { jsx as l, jsxs as u } from "react/jsx-runtime";
7
+ //#region src/shared/assistant/ui/avatar/AvatarTabLastExchange.tsx
8
+ var d = "text-xs font-medium text-text-secondary", f = "text-xs text-text-muted", p = "inline-flex min-h-11 shrink-0 items-center justify-center gap-1.5 rounded-xl border border-border-default bg-bg-elevated px-3 text-xs font-medium text-text-primary transition-colors hover:bg-bg-raised focus:outline-none focus-visible:ring-2 focus-visible:ring-border-strong", m = 160, h = ({ view: h, invite: g, restored: _, focusAfterStop: v }) => {
9
+ let y = t(), b = n(), x = i(), S = o(null), [C, w] = s(!1), T = h?.answer ?? "", [E, D] = s(null), O = E ?? (T.length > m || T.split("\n").length > 4);
10
+ a(() => {
11
+ let e = S.current;
12
+ if (e === null || C) return;
13
+ let t = () => {
14
+ e.clientHeight > 0 && D(e.scrollHeight > e.clientHeight + 1);
15
+ };
16
+ if (t(), typeof ResizeObserver > "u") return;
17
+ let n = new ResizeObserver(t);
18
+ return n.observe(e), () => n.disconnect();
19
+ }, [T, C]);
20
+ let k = r((e) => {
21
+ if (e !== null) return () => {
22
+ e.ownerDocument.activeElement === e && v.current?.focus();
23
+ };
24
+ }, [v]);
25
+ if (h === null) return g ? /* @__PURE__ */ l("p", {
26
+ className: f,
27
+ "data-testid": "assistant-avatar-tab-ask-empty",
28
+ children: y.askEmpty
29
+ }) : null;
30
+ let A = h.state === "speaking", j = h.state === "thinking" ? y.askThinking : h.state === "not-spoken" ? y.askNotSpoken : h.state === "cut" ? y.askCut : h.state === "not-heard" ? y.askNotHeard : h.state === "failed" ? _ ? y.askFailed : y.askFailedInChat : y.askAlsoInChat;
31
+ return /* @__PURE__ */ u("section", {
32
+ "aria-label": y.askExchangeLabel,
33
+ className: "flex min-w-0 shrink-0 flex-col gap-1 border-t border-border-subtle pt-3",
34
+ "data-testid": "assistant-avatar-tab-exchange",
35
+ "data-state": h.state,
36
+ children: [
37
+ /* @__PURE__ */ l("p", {
38
+ className: d,
39
+ children: y.askYouAsked
40
+ }),
41
+ /* @__PURE__ */ l("p", {
42
+ dir: "auto",
43
+ className: "line-clamp-2 break-words text-sm text-text-primary",
44
+ "data-testid": "assistant-avatar-tab-question",
45
+ children: h.question
46
+ }),
47
+ h.askedIn === null ? null : /* @__PURE__ */ l("p", {
48
+ dir: "auto",
49
+ className: `${f} truncate`,
50
+ "data-testid": "assistant-avatar-tab-asked-in",
51
+ children: y.askAskedIn(h.askedIn)
52
+ }),
53
+ /* @__PURE__ */ u("div", {
54
+ className: "flex min-h-11 items-center justify-between gap-2",
55
+ children: [/* @__PURE__ */ l("p", {
56
+ className: d,
57
+ children: y.askAnswer
58
+ }), A ? /* @__PURE__ */ u("button", {
59
+ ref: k,
60
+ type: "button",
61
+ onClick: () => b.stop(),
62
+ "aria-label": y.askStop,
63
+ className: p,
64
+ "data-testid": "assistant-avatar-tab-answer-stop",
65
+ children: [/* @__PURE__ */ l(c, {
66
+ className: "size-3.5",
67
+ "aria-hidden": "true"
68
+ }), /* @__PURE__ */ l("span", { children: y.askStopShort })]
69
+ }) : null]
70
+ }),
71
+ T === "" ? null : /* @__PURE__ */ l("div", {
72
+ ref: S,
73
+ id: x,
74
+ dir: "auto",
75
+ className: `min-w-0 text-sm text-text-primary ${C ? "" : "line-clamp-4"}`,
76
+ "data-testid": "assistant-avatar-tab-answer",
77
+ children: /* @__PURE__ */ l(e, { content: T })
78
+ }),
79
+ T !== "" && (O || C) ? /* @__PURE__ */ l("button", {
80
+ type: "button",
81
+ onClick: () => w((e) => !e),
82
+ "aria-expanded": C,
83
+ "aria-controls": x,
84
+ className: `${p} self-start`,
85
+ "data-testid": "assistant-avatar-tab-answer-toggle",
86
+ children: C ? y.askShowLess : y.askShowAll
87
+ }) : null,
88
+ /* @__PURE__ */ l("p", {
89
+ className: f,
90
+ "data-testid": "assistant-avatar-tab-exchange-status",
91
+ children: j
92
+ })
93
+ ]
94
+ });
95
+ };
96
+ //#endregion
97
+ export { h as AvatarTabLastExchange };
@@ -0,0 +1,104 @@
1
+ import { AutoRouteConversation } from '../../autoRouting';
2
+ import { AssistantMode } from '../../types';
3
+ import { LiveAvatarAskState, LiveAvatarTabAskLine } from '../assistantLiveAvatarSeam';
4
+ import { VoiceStartBlocker } from '../voice/voiceStartGate';
5
+ import { AvatarTabDraft } from './avatarTabDraft';
6
+ /** Why the tab's Send is off, most lasting first. */
7
+ export type AvatarAskBlocker =
8
+ /** A voice conversation is live: the box is hidden, the strip takes its place, the draft is kept. */
9
+ 'hidden-voice'
10
+ /** This conversation takes no new turn (saved before server history). */
11
+ | 'read-only'
12
+ /** The person picked a mode that writes (Auto off): typed questions work only in Ask (critic M4). */
13
+ | 'mode-picked'
14
+ /** Auto would continue in the write mode this conversation was moved up to: a new conversation asks in Ask. */
15
+ | 'mode-escalated'
16
+ /** The avatar is off: Turn on first. Typing is allowed; nothing starts it. */
17
+ | 'avatar-off'
18
+ /** The avatar is stopping, near its time limit, or Turn on waits for a time a refusal named (critic m11). */
19
+ | 'avatar-again'
20
+ /** The avatar is on, but the browser blocks its sound until a tap on its frame (critic m1). */
21
+ | 'sound'
22
+ /** The avatar is being started. */
23
+ | 'starting'
24
+ /** Not now: a turn in flight, an approval, history loading, navigation, the consent dialog, a reply's declines. */
25
+ | 'busy'
26
+ /** Not now: a Chat Send is saving its attachments. */
27
+ | 'committing';
28
+ export interface AvatarAskGateInput {
29
+ /** A voice conversation is live (started in the tab or adopted from Voice). */
30
+ readonly voiceLive: boolean;
31
+ /** The open conversation is read-only. */
32
+ readonly readOnly: boolean;
33
+ /** Where the send would land writes (`avatarAskWrites`), and why; null when it does not. */
34
+ readonly writes: 'picked' | 'escalated' | null;
35
+ /** The avatar, as the tab seam's ask store says (`liveAvatarAskState`). */
36
+ readonly avatar: LiveAvatarAskState;
37
+ /** The conversation cannot take a turn now (the composer's `disabled`, plus the consent dialog and a reply's declines). */
38
+ readonly busy: boolean;
39
+ /** A Chat Send is saving its attachments. */
40
+ readonly committing: boolean;
41
+ }
42
+ /** Null when a typed question can be sent now; else the first reason it cannot, in the pinned order (critic M3). */
43
+ export declare function avatarAskBlocker(input: AvatarAskGateInput): AvatarAskBlocker | null;
44
+ /** A reason nothing the person waits out (or Turn on) will lift here: the conversation or its mode. */
45
+ export declare function avatarAskBlockerLasts(blocker: AvatarAskBlocker | null): boolean;
46
+ /** The line about asking, said under Turn on while the avatar is off (critic M3): `LiveAvatarTabAskLine`. */
47
+ export type AvatarTabAskLine = LiveAvatarTabAskLine;
48
+ export declare function avatarTabAskLine(ask: AvatarAskBlocker | null, voice: VoiceStartBlocker | null): AvatarTabAskLine;
49
+ /**
50
+ * Would a typed question sent now land in a mode that writes, and why? The same routing the send will run: under
51
+ * Auto, `resolveAutoRoute` with intent `read` (a fresh draft never escalates; a conversation Auto already moved up
52
+ * "continues" in the write mode); otherwise the selected mode. `conversation` is the open one, or null when there is
53
+ * none or it is not continued (read-only, another workspace's), as `handleSend` reads it.
54
+ */
55
+ export declare function avatarAskWrites(input: {
56
+ readonly auto: boolean;
57
+ readonly selectedMode: AssistantMode;
58
+ readonly conversation: AutoRouteConversation | null;
59
+ /** The mode is write-capable, or needs confirmed writes. */
60
+ readonly modeWrites: (mode: AssistantMode) => boolean;
61
+ }): 'picked' | 'escalated' | null;
62
+ /** One exchange the tab asked: where its question landed, and the reply it waits for. */
63
+ export interface AvatarTabExchange {
64
+ readonly conversationId: string;
65
+ readonly questionId: string;
66
+ readonly replyId: string;
67
+ }
68
+ /**
69
+ * What a typed question from the tab became (critic M7). The box clears only when it was accepted.
70
+ * - not accepted: nothing was written; the draft stays.
71
+ * - accepted and recorded: the question and its reply's id (the reply may still be on its way).
72
+ * - accepted, nothing recorded: a command carried out that records no exchange (a new conversation, the delete
73
+ * confirmation opening).
74
+ */
75
+ export type AvatarTabAskResult = {
76
+ readonly accepted: false;
77
+ } | ({
78
+ readonly accepted: true;
79
+ readonly recorded: true;
80
+ } & AvatarTabExchange) | {
81
+ readonly accepted: true;
82
+ readonly recorded: false;
83
+ };
84
+ /** What the chat area hands the Avatar tab for its ask box. */
85
+ export interface AvatarTabAsk {
86
+ /** Why Send is off now, or null. */
87
+ readonly blocker: AvatarAskBlocker | null;
88
+ /** The line about asking under Turn on (critic M3). */
89
+ readonly line: AvatarTabAskLine;
90
+ /** Send a typed question; never starts, confirms or extends the avatar. */
91
+ readonly send: (text: string) => AvatarTabAskResult;
92
+ /** The last exchange asked from the tab in this opening, in the open conversation; null before the first. */
93
+ readonly last: AvatarTabExchange | null;
94
+ /**
95
+ * The reply id of a navigation asked from the tab that is still settling, or null. Its reply is written only once the
96
+ * move settles, and a move that is kept writes none: "Thinking…" lasts only while this names the exchange's reply.
97
+ */
98
+ readonly navigatingReplyId: string | null;
99
+ /**
100
+ * The box's words, kept by the chat area: they survive the tab closing and a voice conversation (which hides the
101
+ * box), and are never Chat's draft, nor moved into it. A store, so a keystroke re-renders the box alone.
102
+ */
103
+ readonly draft: AvatarTabDraft;
104
+ }
@@ -0,0 +1,39 @@
1
+ import { resolveAutoRoute as e } from "../../autoRouting.js";
2
+ import { voiceStartBlockerLasts as t } from "../voice/voiceStartGate.js";
3
+ //#region src/shared/assistant/ui/avatar/avatarAskGate.ts
4
+ function n(e) {
5
+ if (e.voiceLive) return "hidden-voice";
6
+ if (e.readOnly) return "read-only";
7
+ if (e.writes === "picked") return "mode-picked";
8
+ if (e.writes === "escalated") return "mode-escalated";
9
+ switch (e.avatar) {
10
+ case "off": return "avatar-off";
11
+ case "locked":
12
+ case "ending": return "avatar-again";
13
+ case "sound-blocked": return "sound";
14
+ case "starting": return "starting";
15
+ case "ready": break;
16
+ default: return e.avatar;
17
+ }
18
+ return e.busy ? "busy" : e.committing ? "committing" : null;
19
+ }
20
+ function r(e) {
21
+ return e === "read-only" || e === "mode-picked" || e === "mode-escalated";
22
+ }
23
+ function i(e, n) {
24
+ let i = r(e), a = t(n);
25
+ return i && a ? "nothing" : i ? "needs-voice" : a ? "by-typing" : "none";
26
+ }
27
+ function a(t) {
28
+ if (t.auto) {
29
+ let n = e({
30
+ intent: "read",
31
+ dictated: !1,
32
+ conversation: t.conversation
33
+ });
34
+ return t.modeWrites(n.mode) ? "escalated" : null;
35
+ }
36
+ return t.modeWrites(t.selectedMode) ? "picked" : null;
37
+ }
38
+ //#endregion
39
+ export { n as avatarAskBlocker, r as avatarAskBlockerLasts, a as avatarAskWrites, i as avatarTabAskLine };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The Avatar tab's ask box words (BOFF-9360), as a small store the chat area owns.
3
+ *
4
+ * ★ Owned by the chat area, so the words survive the tab closing, a voice conversation (which hides the box) and a
5
+ * conversation switch; never Chat's draft, nor moved into it. A store, not the chat area's state: a keystroke
6
+ * re-renders the ask box alone, never the chat area (the transcript under the tab with it) nor the tab's view.
7
+ */
8
+ export interface AvatarTabDraft {
9
+ /** The words now. */
10
+ readonly get: () => string;
11
+ /** Replace the words; listeners hear only a change. */
12
+ readonly set: (text: string) => void;
13
+ readonly subscribe: (listener: () => void) => () => void;
14
+ }
15
+ export declare function createAvatarTabDraft(initial?: string): AvatarTabDraft;
16
+ /** The words, live: only the component that shows them re-renders as they change. */
17
+ export declare function useAvatarTabDraft(draft: AvatarTabDraft): string;
@@ -0,0 +1,22 @@
1
+ import { useSyncExternalStore as e } from "react";
2
+ //#region src/shared/assistant/ui/avatar/avatarTabDraft.ts
3
+ function t(e = "") {
4
+ let t = e, n = /* @__PURE__ */ new Set();
5
+ return {
6
+ get: () => t,
7
+ set: (e) => {
8
+ if (e !== t) {
9
+ t = e;
10
+ for (let e of [...n]) e();
11
+ }
12
+ },
13
+ subscribe: (e) => (n.add(e), () => {
14
+ n.delete(e);
15
+ })
16
+ };
17
+ }
18
+ function n(t) {
19
+ return e(t.subscribe, t.get, t.get);
20
+ }
21
+ //#endregion
22
+ export { t as createAvatarTabDraft, n as useAvatarTabDraft };