@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.
- package/dist/shared/assistant/avatar/live/AvatarFace.d.ts +13 -0
- package/dist/shared/assistant/avatar/live/AvatarFace.js +46 -0
- package/dist/shared/assistant/avatar/live/LiveAvatarTabView.d.ts +2 -0
- package/dist/shared/assistant/avatar/live/LiveAvatarTabView.js +172 -145
- package/dist/shared/assistant/avatar/live/avatarFaces.d.ts +31 -0
- package/dist/shared/assistant/avatar/live/avatarFaces.js +33 -0
- package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.d.ts +12 -0
- package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.js +142 -75
- package/dist/shared/assistant/avatar/live/liveAvatarController.d.ts +5 -0
- package/dist/shared/assistant/avatar/live/liveAvatarController.js +3 -1
- package/dist/shared/assistant/avatar/live/liveAvatarStatus.d.ts +14 -0
- package/dist/shared/assistant/avatar/live/liveAvatarStatus.js +11 -2
- package/dist/shared/assistant/avatar/useAvatarCopy.d.ts +42 -0
- package/dist/shared/assistant/avatar/useAvatarCopy.js +113 -75
- package/dist/shared/assistant/i18nManifest.d.ts +29 -0
- package/dist/shared/assistant/i18nManifest.js +29 -0
- package/dist/shared/assistant/index.d.ts +8 -0
- package/dist/shared/assistant/speech/hostedTtsSpeechEngine.d.ts +6 -0
- package/dist/shared/assistant/speech/hostedTtsSpeechEngine.js +118 -101
- package/dist/shared/assistant/speech/notifyCancellationSpeechEngine.js +3 -0
- package/dist/shared/assistant/speech/replySpeechRequests.d.ts +84 -0
- package/dist/shared/assistant/speech/replySpeechRequests.js +70 -0
- package/dist/shared/assistant/speech/routingSpeechEngine.js +13 -9
- package/dist/shared/assistant/speech/types.d.ts +39 -1
- package/dist/shared/assistant/ui/AIAssistantPanel.js +128 -119
- package/dist/shared/assistant/ui/AssistantChatArea.js +1572 -1430
- package/dist/shared/assistant/ui/AssistantMessageItem.js +110 -100
- package/dist/shared/assistant/ui/AssistantSpeechProvider.d.ts +10 -0
- package/dist/shared/assistant/ui/AssistantSpeechProvider.js +286 -225
- package/dist/shared/assistant/ui/assistantLiveAvatarSeam.d.ts +40 -0
- package/dist/shared/assistant/ui/avatar/AssistantAvatarTab.d.ts +6 -0
- package/dist/shared/assistant/ui/avatar/AssistantAvatarTab.js +99 -52
- package/dist/shared/assistant/ui/avatar/AvatarTabAskBox.d.ts +8 -0
- package/dist/shared/assistant/ui/avatar/AvatarTabAskBox.js +87 -0
- package/dist/shared/assistant/ui/avatar/AvatarTabLastExchange.d.ts +13 -0
- package/dist/shared/assistant/ui/avatar/AvatarTabLastExchange.js +97 -0
- package/dist/shared/assistant/ui/avatar/avatarAskGate.d.ts +104 -0
- package/dist/shared/assistant/ui/avatar/avatarAskGate.js +39 -0
- package/dist/shared/assistant/ui/avatar/avatarTabDraft.d.ts +17 -0
- package/dist/shared/assistant/ui/avatar/avatarTabDraft.js +22 -0
- package/dist/shared/assistant/ui/avatar/avatarTabExchange.d.ts +47 -0
- package/dist/shared/assistant/ui/avatar/avatarTabExchange.js +50 -0
- package/dist/shared/assistant/ui/panelOverlays.d.ts +8 -1
- package/dist/shared/assistant/ui/panelOverlays.js +1 -1
- package/dist/shared/assistant/ui/useVoiceSession.js +222 -227
- package/dist/shared/assistant/voice/handsfree/playbackReference.d.ts +15 -4
- package/dist/shared/assistant/voice/handsfree/playbackReference.js +9 -6
- package/dist/shared-assistant.js +270 -262
- 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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
|
7
|
-
function
|
|
8
|
-
let [
|
|
9
|
-
return
|
|
10
|
-
if (!
|
|
11
|
-
let r =
|
|
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
|
|
19
|
+
let a = () => {
|
|
14
20
|
let e = typeof r.getBoundingClientRect == "function" ? r.getBoundingClientRect().height : 0;
|
|
15
|
-
|
|
21
|
+
i(Math.round(e));
|
|
16
22
|
};
|
|
17
|
-
if (
|
|
18
|
-
let
|
|
19
|
-
return
|
|
20
|
-
}, [
|
|
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
|
|
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
|
|
28
|
-
let
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
37
|
-
|
|
51
|
+
let r = D.current;
|
|
52
|
+
r !== null && r.tab === t && r.setup !== n || (t.setOpen(!1), e());
|
|
38
53
|
});
|
|
39
54
|
};
|
|
40
|
-
}, [
|
|
41
|
-
let
|
|
42
|
-
|
|
43
|
-
let e =
|
|
44
|
-
|
|
45
|
-
}, [
|
|
46
|
-
|
|
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:
|
|
49
|
-
"aria-labelledby":
|
|
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:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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 {
|
|
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 };
|