@burdenoff/fe-libs 2026.1024.3 → 2026.1026.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/LiveAvatarMinutes.d.ts +24 -0
- package/dist/shared/assistant/avatar/live/LiveAvatarMinutes.js +161 -0
- package/dist/shared/assistant/avatar/live/LiveAvatarStage.js +95 -74
- package/dist/shared/assistant/avatar/live/LiveAvatarTabView.d.ts +6 -0
- package/dist/shared/assistant/avatar/live/LiveAvatarTabView.js +195 -172
- package/dist/shared/assistant/avatar/live/avatarPorts.d.ts +9 -0
- package/dist/shared/assistant/avatar/live/avatarSessionClient.d.ts +77 -1
- package/dist/shared/assistant/avatar/live/avatarSessionClient.js +193 -72
- package/dist/shared/assistant/avatar/live/avatarTransport.d.ts +25 -3
- package/dist/shared/assistant/avatar/live/avatarTransport.js +29 -2
- package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.d.ts +35 -4
- package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.js +136 -79
- package/dist/shared/assistant/avatar/live/liveAvatarController.d.ts +79 -3
- package/dist/shared/assistant/avatar/live/liveAvatarController.js +424 -245
- package/dist/shared/assistant/avatar/live/liveAvatarMinutesView.d.ts +127 -0
- package/dist/shared/assistant/avatar/live/liveAvatarMinutesView.js +168 -0
- package/dist/shared/assistant/avatar/live/liveAvatarStatus.d.ts +3 -0
- package/dist/shared/assistant/avatar/live/liveAvatarStatus.js +11 -7
- package/dist/shared/assistant/avatar/live/liveKitAvatarRoom.js +6 -1
- package/dist/shared/assistant/avatar/useAvatarCopy.d.ts +127 -2
- package/dist/shared/assistant/avatar/useAvatarCopy.js +116 -7
- package/dist/shared/assistant/i18nManifest.d.ts +32 -1
- package/dist/shared/assistant/i18nManifest.js +55 -24
- package/dist/shared/assistant/index.d.ts +2 -0
- package/dist/shared/assistant/ui/AssistantChatArea.js +17 -17
- package/dist/shared/assistant/ui/AssistantMarkdownRenderer.d.ts +5 -2
- package/dist/shared/assistant/ui/assistantLiveAvatarSeam.d.ts +11 -3
- package/dist/shared/assistant/ui/avatar/AssistantAvatarTab.js +78 -76
- package/dist/shared/assistant/ui/avatar/avatarAskGate.d.ts +2 -0
- package/dist/shared/assistant/ui/avatar/avatarAskGate.js +1 -0
- package/dist/shared/events/index.d.ts +2 -1
- package/dist/shared-assistant.js +227 -225
- package/package.json +1 -1
|
@@ -65,6 +65,7 @@ var p = class {
|
|
|
65
65
|
videoPresent = !1;
|
|
66
66
|
degraded = !1;
|
|
67
67
|
audioElement = null;
|
|
68
|
+
elementPaused = !1;
|
|
68
69
|
disposed = !1;
|
|
69
70
|
rejectReady = null;
|
|
70
71
|
mediaReady = !1;
|
|
@@ -85,7 +86,10 @@ var p = class {
|
|
|
85
86
|
return this.readinessStatus;
|
|
86
87
|
}
|
|
87
88
|
get audioBlocked() {
|
|
88
|
-
return this.state === "ready" && !this.options.room.canPlaybackAudio();
|
|
89
|
+
return this.state === "ready" && (!this.options.room.canPlaybackAudio() || this.elementPaused);
|
|
90
|
+
}
|
|
91
|
+
get videoBlocked() {
|
|
92
|
+
return this.state === "ready" && this.options.room.canPlaybackVideo?.() === !1;
|
|
89
93
|
}
|
|
90
94
|
subscribe(e) {
|
|
91
95
|
return this.listeners.add(e), () => {
|
|
@@ -100,6 +104,26 @@ var p = class {
|
|
|
100
104
|
async resumeAudio() {
|
|
101
105
|
await this.options.room.startAudio();
|
|
102
106
|
}
|
|
107
|
+
async resumeVideo() {
|
|
108
|
+
await this.options.room.startVideo?.();
|
|
109
|
+
}
|
|
110
|
+
watchAudioElement(e) {
|
|
111
|
+
if (e === null || typeof e.addEventListener != "function") return;
|
|
112
|
+
let t = () => {
|
|
113
|
+
this.disposed || this.state !== "ready" || e.srcObject === null || this.elementPaused || (this.elementPaused = !0, this.emit({
|
|
114
|
+
type: "audioPlayback",
|
|
115
|
+
canPlay: !1
|
|
116
|
+
}));
|
|
117
|
+
}, n = () => {
|
|
118
|
+
this.elementPaused && (this.elementPaused = !1, this.emit({
|
|
119
|
+
type: "audioPlayback",
|
|
120
|
+
canPlay: this.options.room.canPlaybackAudio()
|
|
121
|
+
}));
|
|
122
|
+
};
|
|
123
|
+
e.addEventListener("pause", t), e.addEventListener("playing", n), this.unsubscribes.push(() => {
|
|
124
|
+
e.removeEventListener("pause", t), e.removeEventListener("playing", n);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
103
127
|
setOutputMuted(e) {
|
|
104
128
|
this.audioElement && (this.audioElement.muted = e);
|
|
105
129
|
}
|
|
@@ -107,7 +131,7 @@ var p = class {
|
|
|
107
131
|
if (this.state !== "idle") throw Error("transport already used");
|
|
108
132
|
this.state = "connecting";
|
|
109
133
|
let { room: t, avatarIdentity: i } = this.options;
|
|
110
|
-
this.audioElement = e.audioElement ?? null, t.registerRpc(n, (e, t) => this.handleFinished(e, t)), t.registerRpc(r, (e) => this.handleStarted(e)), this.unsubscribes.push(t.onParticipantConnected((e) => {
|
|
134
|
+
this.audioElement = e.audioElement ?? null, this.watchAudioElement(this.audioElement), t.registerRpc(n, (e, t) => this.handleFinished(e, t)), t.registerRpc(r, (e) => this.handleStarted(e)), this.unsubscribes.push(t.onParticipantConnected((e) => {
|
|
111
135
|
e === i && (this.avatarPresent = !0, this.checkReady());
|
|
112
136
|
}), t.onParticipantDisconnected((e) => {
|
|
113
137
|
e === i && (this.avatarPresent = !1, this.videoPresent = !1, this.avatarDepartures += 1, this.owedFinishes = 0, this.endAllSegments(), this.emit({ type: "avatarLeft" }));
|
|
@@ -122,6 +146,9 @@ var p = class {
|
|
|
122
146
|
}), t.onAudioPlaybackChanged((e) => this.emit({
|
|
123
147
|
type: "audioPlayback",
|
|
124
148
|
canPlay: e
|
|
149
|
+
}))), t.onVideoPlaybackChanged && this.unsubscribes.push(t.onVideoPlaybackChanged((e) => this.emit({
|
|
150
|
+
type: "videoPlayback",
|
|
151
|
+
canPlay: e
|
|
125
152
|
})));
|
|
126
153
|
try {
|
|
127
154
|
await t.connect(e.url, e.token);
|
|
@@ -7,6 +7,7 @@ import { AvatarRoom, AvatarScheduler } from './avatarPorts';
|
|
|
7
7
|
import { AvatarSessionClient } from './avatarSessionClient';
|
|
8
8
|
import { LiveAvatarVoiceStore } from './LiveAvatarTabView';
|
|
9
9
|
import { LiveAvatarController, LiveAvatarControllerDeps, LiveAvatarLogEvent, LiveAvatarPreferences } from './liveAvatarController';
|
|
10
|
+
import { LiveAvatarMinutesPurchase } from './liveAvatarMinutesView';
|
|
10
11
|
export interface LiveAvatarSeamOptions {
|
|
11
12
|
/** Display names for the avatar ids the server offers (it sends ids only); without one the chooser numbers them. */
|
|
12
13
|
readonly avatarNames?: Readonly<Record<string, string>>;
|
|
@@ -33,6 +34,18 @@ export interface LiveAvatarSeamOptions {
|
|
|
33
34
|
* person's stored choice always wins.
|
|
34
35
|
*/
|
|
35
36
|
readonly defaultAvatarVoiceMode?: AvatarVoiceDefault;
|
|
37
|
+
/**
|
|
38
|
+
* ★ BOFF-9446: whether this person may buy a plan (`access`, the host's purchase check, which follows `billing:create` on the
|
|
39
|
+
* organization's billing account): fe-libs cannot import billing. The Avatar tab asks it only where "See plans" could be offered (none
|
|
40
|
+
* left, or running low, while the server says a plan with more credits exists), and opens the app's plans page with the panel's own
|
|
41
|
+
* navigator (`onNavigate`), never by itself. Absent: nobody is offered "See plans" in the tab, and a person who has run out is told to
|
|
42
|
+
* ask their billing administrator about a plan with more avatar credits. Only `canPurchase` is read. Copied once, here: a host that
|
|
43
|
+
* changes its object later changes nothing.
|
|
44
|
+
*
|
|
45
|
+
* The host knows its own routes, so it says when the person comes back: from any billing page, `controller.refreshMinutes()`; from a
|
|
46
|
+
* plan checkout, `controller.checkMinutesPurchase()` (the tab then says "Checking your plan…" while it checks).
|
|
47
|
+
*/
|
|
48
|
+
readonly minutesPurchase?: LiveAvatarMinutesPurchase;
|
|
36
49
|
readonly onLog?: (event: LiveAvatarLogEvent) => void;
|
|
37
50
|
/** Overrides, for tests and for hosts with their own plumbing. Each defaults to the assistant's own. */
|
|
38
51
|
readonly client?: () => AvatarSessionClient | null;
|
|
@@ -45,7 +58,7 @@ export interface LiveAvatarSeamOptions {
|
|
|
45
58
|
readonly avatarVoiceStore?: LiveAvatarVoiceStore;
|
|
46
59
|
readonly scheduler?: AvatarScheduler;
|
|
47
60
|
readonly browserSupported?: () => boolean;
|
|
48
|
-
readonly controller?: Partial<Pick<LiveAvatarControllerDeps, 'createAudioElement' | 'releaseAudioElement' | 'startMeter' | 'decode' | 'now' | 'newIdempotencyKey' | 'subscribeWorkspaceChange' | 'subscribePageHide' | 'subscribePageShow' | 'idleMs' | 'endBeforeDeadlineMs' | 'connectTimeoutMs' | 'trackerConfig' | 'maxDrainMs' | 'stallMs' | 'readinessProbe'>>;
|
|
61
|
+
readonly controller?: Partial<Pick<LiveAvatarControllerDeps, 'createAudioElement' | 'releaseAudioElement' | 'startMeter' | 'decode' | 'now' | 'newIdempotencyKey' | 'subscribeWorkspaceChange' | 'subscribePageHide' | 'subscribePageHidden' | 'endOnHide' | 'subscribeUserGesture' | 'isPageVisible' | 'subscribePageVisible' | 'subscribePageShow' | 'idleMs' | 'endBeforeDeadlineMs' | 'connectTimeoutMs' | 'trackerConfig' | 'maxDrainMs' | 'stallMs' | 'readinessProbe'>>;
|
|
49
62
|
}
|
|
50
63
|
/** The seam, plus what a host or a test may need of it. */
|
|
51
64
|
export interface LiveAvatarSeamHandle extends AssistantLiveAvatarSeam {
|
|
@@ -58,9 +71,12 @@ export interface LiveAvatarSeamHandle extends AssistantLiveAvatarSeam {
|
|
|
58
71
|
dispose(): void;
|
|
59
72
|
}
|
|
60
73
|
/**
|
|
61
|
-
* Whether this browser
|
|
62
|
-
*
|
|
63
|
-
*
|
|
74
|
+
* Whether this browser can show the avatar: a check of what the avatar needs, on every browser and phone (BOFF-9696, the operator's
|
|
75
|
+
* decision of 2026-10-25; until then desktop Chrome only, GATE1B §10). The pieces: WebRTC (the room), an AudioContext (the level meter),
|
|
76
|
+
* MediaStream, and an OfflineAudioContext at the reply audio's rate (the MP3 decode makes one: avatarAudioDecode.ts). All four are
|
|
77
|
+
* unprefixed: Safari shipped the unprefixed audio contexts together (14.1), and the prefixed offline context's callback-only decode
|
|
78
|
+
* could not serve the avatar anyway. The user agent is never read: a browser that has these runs the avatar, one that lacks any cannot.
|
|
79
|
+
* Where autoplay needs a tap for sound (Safari and every iOS browser), the sound-blocked state asks for it.
|
|
64
80
|
*/
|
|
65
81
|
export declare function isLiveAvatarBrowserSupported(scope?: typeof globalThis): boolean;
|
|
66
82
|
/** The `Authorization` the hosted TTS requests carry (the host's `getHeaders`), else the stored access token. Nothing else is read. */
|
|
@@ -78,4 +94,19 @@ export declare function liveAvatarWorkspaceAuthorization(options: {
|
|
|
78
94
|
readonly fresh: boolean;
|
|
79
95
|
readonly refused?: string;
|
|
80
96
|
}): Promise<string | null>;
|
|
97
|
+
/**
|
|
98
|
+
* ★ BOFF-9696 (S5): a touch-first device (a phone or tablet without a hovering pointer), where a hidden page ends the session. A
|
|
99
|
+
* capability query, never the user agent. Exported for tests only.
|
|
100
|
+
*/
|
|
101
|
+
export declare function isTouchFirstDevice(scope?: {
|
|
102
|
+
readonly matchMedia?: (query: string) => {
|
|
103
|
+
readonly matches: boolean;
|
|
104
|
+
};
|
|
105
|
+
}): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* ★ BOFF-9696: the person's taps, clicks and keys anywhere on the page, in the capture phase so no handler can stop them first. Each
|
|
108
|
+
* is a user gesture, inside which a browser lets a refused video start (iOS unlocks media on `touchend` and `click`). Only trusted
|
|
109
|
+
* events count: a script's synthetic click is no gesture. Exported for tests only.
|
|
110
|
+
*/
|
|
111
|
+
export declare function documentUserGesture(doc?: Pick<Document, 'addEventListener' | 'removeEventListener'> | null): ((listener: () => void) => () => void) | undefined;
|
|
81
112
|
export declare function createLiveAvatarSeam(options?: LiveAvatarSeamOptions): LiveAvatarSeamHandle;
|
|
@@ -4,36 +4,41 @@ import { readSpeechEngineConfig as a } from "../../speech/createSpeechEngine.js"
|
|
|
4
4
|
import { useAvatarPreferencesStore as o } from "../avatarPreferences.js";
|
|
5
5
|
import { realAvatarScheduler as s } from "./avatarPorts.js";
|
|
6
6
|
import { startAvatarAudioMeter as c } from "./avatarAudioMeter.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
7
|
+
import { AVATAR_SAMPLE_RATE as l } from "./avatarPcm16.js";
|
|
8
|
+
import { decodeMp3ToPcm16k as u } from "./avatarAudioDecode.js";
|
|
9
|
+
import { avatarApiBaseFromTtsEndpoint as d, avatarIdempotencyKey as f, createAvatarSessionClient as p } from "./avatarSessionClient.js";
|
|
10
|
+
import { createLiveAvatarController as m } from "./liveAvatarController.js";
|
|
11
|
+
import { openLiveKitAvatarRoom as h } from "./liveKitAvatarRoom.js";
|
|
12
|
+
import { liveAvatarAskState as g } from "./liveAvatarStatus.js";
|
|
13
|
+
import { LiveAvatarStageView as _ } from "./LiveAvatarStage.js";
|
|
14
|
+
import { sanitizeAvatarImages as v } from "./avatarFaces.js";
|
|
15
|
+
import { avatarSpeakerFor as y, hostedAvatarVoice as b, liveAvatarVoicesFrom as x, restoreVoiceWhenOutputLeaves as S } from "./avatarVoice.js";
|
|
16
|
+
import { LiveAvatarTabView as C } from "./LiveAvatarTabView.js";
|
|
17
|
+
import { jsx as w } from "react/jsx-runtime";
|
|
17
18
|
//#region src/shared/assistant/avatar/live/createLiveAvatarSeam.tsx
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
var T = /* @__PURE__ */ new WeakMap();
|
|
20
|
+
function E(e = globalThis) {
|
|
21
|
+
let t = T.get(e);
|
|
22
|
+
if (t !== void 0) return t;
|
|
23
|
+
let n = D(e);
|
|
24
|
+
return T.set(e, n), n;
|
|
25
|
+
}
|
|
26
|
+
function D(e) {
|
|
27
|
+
if (typeof e.RTCPeerConnection != "function" || typeof e.AudioContext != "function" || typeof e.MediaStream != "function" || typeof e.OfflineAudioContext != "function") return !1;
|
|
28
|
+
try {
|
|
29
|
+
return new e.OfflineAudioContext(1, 1, l), !0;
|
|
30
|
+
} catch {
|
|
31
|
+
return !1;
|
|
25
32
|
}
|
|
26
|
-
let i = n?.userAgent ?? "";
|
|
27
|
-
return /\b(?:Chrome|Chromium)\//.test(i) && !/\b(?:Edg|OPR|SamsungBrowser)\/|Mobile|Android|iPhone|iPad/.test(i);
|
|
28
33
|
}
|
|
29
|
-
function
|
|
34
|
+
function O() {
|
|
30
35
|
let e = a().getHeaders?.() ?? {};
|
|
31
36
|
for (let [t, n] of Object.entries(e)) if (t.toLowerCase() === "authorization" && typeof n == "string" && n.trim() !== "") return n.trim();
|
|
32
37
|
let n = t();
|
|
33
38
|
return n ? `Bearer ${n}` : null;
|
|
34
39
|
}
|
|
35
|
-
async function
|
|
36
|
-
let i = n(), a =
|
|
40
|
+
async function k(t) {
|
|
41
|
+
let i = n(), a = O(), o = a?.replace(/^Bearer\s+/i, "") ?? null;
|
|
37
42
|
if (!i || !o) return null;
|
|
38
43
|
let s = t.refused?.replace(/^Bearer\s+/i, ""), c = await e({
|
|
39
44
|
url: r("workspace"),
|
|
@@ -42,38 +47,38 @@ async function E(t) {
|
|
|
42
47
|
currentQuery: "liveAvatarSession",
|
|
43
48
|
...t.fresh ? { issuanceGuard: {
|
|
44
49
|
identityKey: "liveAvatarSession",
|
|
45
|
-
isCurrent: () => n() === i &&
|
|
50
|
+
isCurrent: () => n() === i && O() === a,
|
|
46
51
|
accepts: (e, t) => e !== s && t === i
|
|
47
52
|
} } : {}
|
|
48
53
|
});
|
|
49
54
|
return c ? `Bearer ${c}` : null;
|
|
50
55
|
}
|
|
51
|
-
function
|
|
56
|
+
function A() {
|
|
52
57
|
let e = null;
|
|
53
58
|
return () => {
|
|
54
|
-
let t = typeof location > "u" ? void 0 : location.href, n =
|
|
59
|
+
let t = typeof location > "u" ? void 0 : location.href, n = d(a().endpoint, t);
|
|
55
60
|
return n === null ? null : (e?.base !== n && (e = {
|
|
56
61
|
base: n,
|
|
57
|
-
client:
|
|
62
|
+
client: p({
|
|
58
63
|
baseUrl: n,
|
|
59
|
-
getAuthorization:
|
|
60
|
-
getWorkspaceAuthorization:
|
|
64
|
+
getAuthorization: O,
|
|
65
|
+
getWorkspaceAuthorization: k
|
|
61
66
|
})
|
|
62
67
|
}), e.client);
|
|
63
68
|
};
|
|
64
69
|
}
|
|
65
|
-
var
|
|
70
|
+
var j = {
|
|
66
71
|
getState: () => o.getState(),
|
|
67
72
|
setAvatarEnabled: (e) => o.getState().setAvatarEnabled(e),
|
|
68
73
|
setAvatarId: (e) => o.getState().setAvatarId(e),
|
|
69
74
|
subscribe: (e) => o.subscribe(() => e())
|
|
70
|
-
},
|
|
75
|
+
}, M = {
|
|
71
76
|
getState: () => o.getState(),
|
|
72
77
|
subscribe: (e) => o.subscribe(() => e()),
|
|
73
78
|
setVoiceMode: (e) => o.getState().setVoiceMode(e),
|
|
74
79
|
setAvatarSpeaker: (e) => o.getState().setAvatarSpeaker(e)
|
|
75
80
|
};
|
|
76
|
-
function
|
|
81
|
+
function N(e, t) {
|
|
77
82
|
let n = (e) => {
|
|
78
83
|
i(e) || t();
|
|
79
84
|
};
|
|
@@ -90,15 +95,61 @@ function A(e, t) {
|
|
|
90
95
|
cancel: () => e.cancel()
|
|
91
96
|
};
|
|
92
97
|
}
|
|
93
|
-
function
|
|
98
|
+
function P(e) {
|
|
99
|
+
if (e === void 0) return;
|
|
100
|
+
let { access: t } = e;
|
|
101
|
+
return { access: () => t.call(e) };
|
|
102
|
+
}
|
|
103
|
+
function F() {
|
|
94
104
|
if (typeof document > "u") return null;
|
|
95
105
|
let e = document.createElement("audio");
|
|
96
106
|
return e.autoplay = !0, e.hidden = !0, e.setAttribute("data-testid", "assistant-live-avatar-audio"), document.body.appendChild(e), e;
|
|
97
107
|
}
|
|
98
|
-
function
|
|
108
|
+
function I(e) {
|
|
99
109
|
if (!(typeof window > "u")) return (t) => (window.addEventListener(e, t), () => window.removeEventListener(e, t));
|
|
100
110
|
}
|
|
101
|
-
function
|
|
111
|
+
function L() {
|
|
112
|
+
if (typeof document > "u" || typeof document.addEventListener != "function") return;
|
|
113
|
+
let e = () => document.visibilityState !== "hidden";
|
|
114
|
+
return {
|
|
115
|
+
isVisible: e,
|
|
116
|
+
onHidden: (t) => {
|
|
117
|
+
let n = () => {
|
|
118
|
+
e() || t();
|
|
119
|
+
};
|
|
120
|
+
return document.addEventListener("visibilitychange", n), () => {
|
|
121
|
+
document.removeEventListener("visibilitychange", n);
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
onShown: (t) => {
|
|
125
|
+
let n = () => {
|
|
126
|
+
e() && t();
|
|
127
|
+
}, r = (n) => {
|
|
128
|
+
"persisted" in n && n.persisted === !0 && e() && t();
|
|
129
|
+
}, i = typeof window < "u" && typeof window.addEventListener == "function" ? window : void 0;
|
|
130
|
+
return document.addEventListener("visibilitychange", n), i?.addEventListener("pageshow", r), () => {
|
|
131
|
+
document.removeEventListener("visibilitychange", n), i?.removeEventListener("pageshow", r);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function R(e = globalThis) {
|
|
137
|
+
return typeof e.matchMedia == "function" && e.matchMedia("(hover: none) and (pointer: coarse)").matches;
|
|
138
|
+
}
|
|
139
|
+
function z(e = typeof document > "u" ? null : document) {
|
|
140
|
+
if (!(e === null || typeof e.addEventListener != "function")) return (t) => {
|
|
141
|
+
let n = (e) => {
|
|
142
|
+
e.isTrusted && t();
|
|
143
|
+
};
|
|
144
|
+
return e.addEventListener("touchend", n, {
|
|
145
|
+
capture: !0,
|
|
146
|
+
passive: !0
|
|
147
|
+
}), e.addEventListener("click", n, !0), e.addEventListener("keydown", n, !0), () => {
|
|
148
|
+
e.removeEventListener("touchend", n, { capture: !0 }), e.removeEventListener("click", n, !0), e.removeEventListener("keydown", n, !0);
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function B() {
|
|
102
153
|
if (!(typeof window > "u" || typeof document > "u")) return (e) => {
|
|
103
154
|
let t = window, n = document, r = () => {
|
|
104
155
|
n.visibilityState === "visible" && e();
|
|
@@ -108,24 +159,29 @@ function N() {
|
|
|
108
159
|
};
|
|
109
160
|
};
|
|
110
161
|
}
|
|
111
|
-
function
|
|
112
|
-
let t = e.preferences ??
|
|
113
|
-
client: e.client ??
|
|
114
|
-
openRoom: e.openRoom ??
|
|
162
|
+
function V(e = {}) {
|
|
163
|
+
let t = e.preferences ?? j, n = e.controller ?? {}, r = n.subscribeWorkspaceChange ?? I("burdenoff:workspace-changed"), i = n.subscribePageHide ?? I("pagehide"), a = L(), o = n.isPageVisible ?? a?.isVisible, l = n.subscribePageVisible ?? a?.onShown, d = n.subscribePageHidden ?? a?.onHidden, p = n.endOnHide ?? R, T = n.subscribeUserGesture ?? z(), D = n.subscribePageShow ?? B(), O = n.now ?? (() => Date.now()), k = m({
|
|
164
|
+
client: e.client ?? A(),
|
|
165
|
+
openRoom: e.openRoom ?? h,
|
|
115
166
|
scheduler: e.scheduler ?? s,
|
|
116
167
|
preferences: t,
|
|
117
|
-
decode: n.decode ?? ((e, t) =>
|
|
118
|
-
createAudioElement: n.createAudioElement ??
|
|
168
|
+
decode: n.decode ?? ((e, t) => u(e, { signal: t })),
|
|
169
|
+
createAudioElement: n.createAudioElement ?? F,
|
|
119
170
|
releaseAudioElement: n.releaseAudioElement ?? ((e) => {
|
|
120
171
|
e.srcObject = null, e.remove();
|
|
121
172
|
}),
|
|
122
173
|
startMeter: n.startMeter ?? ((e, t) => typeof MediaStream > "u" ? null : c(new MediaStream([e]), t)),
|
|
123
|
-
browserSupported: e.browserSupported ?? (() =>
|
|
124
|
-
now:
|
|
125
|
-
newIdempotencyKey: n.newIdempotencyKey ?? (() =>
|
|
174
|
+
browserSupported: e.browserSupported ?? (() => E()),
|
|
175
|
+
now: O,
|
|
176
|
+
newIdempotencyKey: n.newIdempotencyKey ?? (() => f()),
|
|
126
177
|
...r ? { subscribeWorkspaceChange: r } : {},
|
|
127
178
|
...i ? { subscribePageHide: i } : {},
|
|
128
|
-
...
|
|
179
|
+
...o ? { isPageVisible: o } : {},
|
|
180
|
+
...l ? { subscribePageVisible: l } : {},
|
|
181
|
+
...d ? { subscribePageHidden: d } : {},
|
|
182
|
+
endOnHide: p,
|
|
183
|
+
...T ? { subscribeUserGesture: T } : {},
|
|
184
|
+
...D ? { subscribePageShow: D } : {},
|
|
129
185
|
...n.idleMs === void 0 ? {} : { idleMs: n.idleMs },
|
|
130
186
|
...n.endBeforeDeadlineMs === void 0 ? {} : { endBeforeDeadlineMs: n.endBeforeDeadlineMs },
|
|
131
187
|
...n.connectTimeoutMs === void 0 ? {} : { connectTimeoutMs: n.connectTimeoutMs },
|
|
@@ -134,34 +190,34 @@ function P(e = {}) {
|
|
|
134
190
|
...n.stallMs === void 0 ? {} : { stallMs: n.stallMs },
|
|
135
191
|
...n.readinessProbe === void 0 ? {} : { readinessProbe: n.readinessProbe },
|
|
136
192
|
...e.onLog ? { onLog: e.onLog } : {}
|
|
137
|
-
}),
|
|
138
|
-
let i =
|
|
139
|
-
avatarVoices:
|
|
193
|
+
}), V = x(e.avatarVoices), H = e.avatarImages === void 0 ? null : v(e.avatarImages), U = e.avatarVoiceStore ?? M, W = () => U.getState(), G = P(e.minutesPurchase), K = (n, r) => {
|
|
194
|
+
let i = y(W(), {
|
|
195
|
+
avatarVoices: V,
|
|
140
196
|
avatarId: t.getState().avatarId,
|
|
141
197
|
...e.defaultAvatarVoiceMode ? { hostDefault: e.defaultAvatarVoiceMode } : {}
|
|
142
198
|
});
|
|
143
|
-
return i === null ? null :
|
|
144
|
-
},
|
|
199
|
+
return i === null ? null : b(i, n, r.listVoices());
|
|
200
|
+
}, q = (e, t) => {
|
|
145
201
|
let n = {
|
|
146
202
|
...e,
|
|
147
203
|
voice: t
|
|
148
204
|
};
|
|
149
|
-
return n.audioOutput =
|
|
205
|
+
return n.audioOutput = S(k.createOutput(), () => {
|
|
150
206
|
n.voice = e.voice;
|
|
151
|
-
}, (e) =>
|
|
152
|
-
|
|
207
|
+
}, (e) => k.subscribe(() => {
|
|
208
|
+
k.getSnapshot().phase !== "ready" && e();
|
|
153
209
|
})), n;
|
|
154
210
|
};
|
|
155
211
|
return {
|
|
156
212
|
decorateSpeechEngine: (e) => {
|
|
157
213
|
let t = (t) => {
|
|
158
|
-
let n =
|
|
214
|
+
let n = K(t.lang, e), r = n === null ? t : {
|
|
159
215
|
...t,
|
|
160
216
|
voice: n
|
|
161
217
|
};
|
|
162
218
|
return {
|
|
163
219
|
voice: n,
|
|
164
|
-
target:
|
|
220
|
+
target: k.route(r, e)
|
|
165
221
|
};
|
|
166
222
|
}, n = null, r = () => {
|
|
167
223
|
let e = n;
|
|
@@ -169,12 +225,12 @@ function P(e = {}) {
|
|
|
169
225
|
}, i = (t, r, i) => {
|
|
170
226
|
let a = !0, o = !1, s = () => void 0, c = () => {
|
|
171
227
|
a && (a = !1, n === c && (n = null), s());
|
|
172
|
-
}, l = () => o ? !1 : (o = !0, c(), !0),
|
|
228
|
+
}, l = () => o ? !1 : (o = !0, c(), !0), u = () => {
|
|
173
229
|
!a || !l() || (e.cancel(), i?.onError?.("output-left"));
|
|
174
230
|
};
|
|
175
|
-
s =
|
|
176
|
-
|
|
177
|
-
}), n = c, r.audioOutput &&=
|
|
231
|
+
s = k.subscribe(() => {
|
|
232
|
+
k.getSnapshot().phase !== "ready" && u();
|
|
233
|
+
}), n = c, r.audioOutput &&= N(r.audioOutput, u), r.fallback = !1, e.speak(t, r, {
|
|
178
234
|
...i,
|
|
179
235
|
onEnd: () => {
|
|
180
236
|
l() && i?.onEnd?.();
|
|
@@ -203,8 +259,8 @@ function P(e = {}) {
|
|
|
203
259
|
}
|
|
204
260
|
let l = s === null ? {
|
|
205
261
|
...a,
|
|
206
|
-
audioOutput:
|
|
207
|
-
} :
|
|
262
|
+
audioOutput: k.createOutput()
|
|
263
|
+
} : q(a, s);
|
|
208
264
|
if (a.avatarOnly === !0) {
|
|
209
265
|
i(n, l, o);
|
|
210
266
|
return;
|
|
@@ -216,8 +272,8 @@ function P(e = {}) {
|
|
|
216
272
|
}
|
|
217
273
|
};
|
|
218
274
|
},
|
|
219
|
-
Stage: ({ expanded: t, fallback: n }) => /* @__PURE__ */
|
|
220
|
-
controller:
|
|
275
|
+
Stage: ({ expanded: t, fallback: n }) => /* @__PURE__ */ w(_, {
|
|
276
|
+
controller: k,
|
|
221
277
|
expanded: t,
|
|
222
278
|
fallback: n,
|
|
223
279
|
...e.avatarNames ? { avatarNames: e.avatarNames } : {}
|
|
@@ -225,33 +281,34 @@ function P(e = {}) {
|
|
|
225
281
|
enabled: {
|
|
226
282
|
subscribe: (e) => t.subscribe(e),
|
|
227
283
|
getSnapshot: () => t.getState().avatarEnabled,
|
|
228
|
-
switchOff: () =>
|
|
229
|
-
voiceLive: (e) =>
|
|
284
|
+
switchOff: () => k.switchOff(),
|
|
285
|
+
voiceLive: (e) => k.setVoiceLive(e)
|
|
230
286
|
},
|
|
231
287
|
tab: {
|
|
232
288
|
offered: {
|
|
233
|
-
subscribe: (e) =>
|
|
234
|
-
getSnapshot: () =>
|
|
289
|
+
subscribe: (e) => k.subscribe(e),
|
|
290
|
+
getSnapshot: () => k.getSnapshot().tabOffered
|
|
235
291
|
},
|
|
236
292
|
ask: {
|
|
237
|
-
subscribe: (e) =>
|
|
238
|
-
getSnapshot: () =>
|
|
293
|
+
subscribe: (e) => k.subscribe(e),
|
|
294
|
+
getSnapshot: () => g(k.getSnapshot(), O())
|
|
239
295
|
},
|
|
240
|
-
setOpen: (e) =>
|
|
241
|
-
heldByVoice: () =>
|
|
242
|
-
View: (t) => /* @__PURE__ */ C
|
|
296
|
+
setOpen: (e) => k.setTabOpen(e),
|
|
297
|
+
heldByVoice: () => k.getSnapshot().heldByVoice,
|
|
298
|
+
View: (t) => /* @__PURE__ */ w(C, {
|
|
243
299
|
...t,
|
|
244
|
-
controller:
|
|
245
|
-
avatarVoices:
|
|
246
|
-
voiceStore:
|
|
300
|
+
controller: k,
|
|
301
|
+
avatarVoices: V,
|
|
302
|
+
voiceStore: U,
|
|
247
303
|
...e.avatarNames ? { avatarNames: e.avatarNames } : {},
|
|
248
|
-
...
|
|
249
|
-
...e.defaultAvatarVoiceMode ? { defaultAvatarVoiceMode: e.defaultAvatarVoiceMode } : {}
|
|
304
|
+
...H ? { avatarImages: H } : {},
|
|
305
|
+
...e.defaultAvatarVoiceMode ? { defaultAvatarVoiceMode: e.defaultAvatarVoiceMode } : {},
|
|
306
|
+
...G ? { minutesPurchase: G } : {}
|
|
250
307
|
})
|
|
251
308
|
},
|
|
252
|
-
controller:
|
|
253
|
-
dispose: () =>
|
|
309
|
+
controller: k,
|
|
310
|
+
dispose: () => k.dispose()
|
|
254
311
|
};
|
|
255
312
|
}
|
|
256
313
|
//#endregion
|
|
257
|
-
export {
|
|
314
|
+
export { V as createLiveAvatarSeam, z as documentUserGesture, E as isLiveAvatarBrowserSupported, R as isTouchFirstDevice, O as liveAvatarAuthorization, k as liveAvatarWorkspaceAuthorization };
|
|
@@ -4,7 +4,7 @@ import { AvatarMeterHandle } from './avatarAudioMeter';
|
|
|
4
4
|
import { AvatarRoom, AvatarScheduler } from './avatarPorts';
|
|
5
5
|
import { AvatarTrackerConfig } from './avatarPlaybackTracker';
|
|
6
6
|
import { AvatarDecodedPiece, AvatarNotice, AvatarReplyOutputs } from './avatarReplyOutputs';
|
|
7
|
-
import { AvatarBudgetScope, AvatarClientEndReason, AvatarSessionClient } from './avatarSessionClient';
|
|
7
|
+
import { AvatarBudgetScope, AvatarClientEndReason, AvatarMinutes, AvatarMinutesReason, AvatarSessionClient, AvatarSessionMinutes } from './avatarSessionClient';
|
|
8
8
|
import { AvatarReadinessProbeOptions } from './avatarTransport';
|
|
9
9
|
/** End after this long without a reply on the avatar while it is on (OD8, BACKEND-DESIGN §8.7). */
|
|
10
10
|
export declare const LIVE_AVATAR_IDLE_MS = 120000;
|
|
@@ -14,6 +14,20 @@ export declare const LIVE_AVATAR_END_BEFORE_DEADLINE_MS = 10000;
|
|
|
14
14
|
export declare const LIVE_AVATAR_REQUEST_TIMEOUT_MS = 8000;
|
|
15
15
|
/** The longest a connect may take (the avatar's own wait for its video, 8 s, plus the readiness probe, 6 s, plus slack). */
|
|
16
16
|
export declare const LIVE_AVATAR_CONNECT_TIMEOUT_MS = 20000;
|
|
17
|
+
/**
|
|
18
|
+
* ★ BOFF-9446: quiet minutes questions are at least this far apart, counted from the last availability question of any kind; one asked
|
|
19
|
+
* sooner waits for it. The check after a purchase keeps its own bounded pace (`LIVE_AVATAR_PURCHASE_POLL_MS`).
|
|
20
|
+
*/
|
|
21
|
+
export declare const LIVE_AVATAR_MINUTES_REFRESH_MS = 15000;
|
|
22
|
+
/** ★ BOFF-9446: a quiet minutes question answered 429 is asked again after its Retry-After, at most this many times in a row. */
|
|
23
|
+
export declare const LIVE_AVATAR_MINUTES_BUSY_RETRIES = 2;
|
|
24
|
+
/** ★ BOFF-9446: the check after a plan purchase asks at most this many times… */
|
|
25
|
+
export declare const LIVE_AVATAR_PURCHASE_POLLS = 5;
|
|
26
|
+
/**
|
|
27
|
+
* …this far apart (0, 6, 12, 18 and 24 s from its first question, which itself comes at least this long after the last availability
|
|
28
|
+
* question of any kind), and stops as soon as the credits available rise.
|
|
29
|
+
*/
|
|
30
|
+
export declare const LIVE_AVATAR_PURCHASE_POLL_MS = 6000;
|
|
17
31
|
export type LiveAvatarPhase =
|
|
18
32
|
/** Nothing running. */
|
|
19
33
|
'off'
|
|
@@ -29,7 +43,10 @@ export type LiveAvatarPhase =
|
|
|
29
43
|
| 'ready'
|
|
30
44
|
/** Ending the session. */
|
|
31
45
|
| 'ending';
|
|
32
|
-
|
|
46
|
+
/** `minutes-ended` (★ BOFF-9446): the time limit of a session whose length was its minutes (D' < D): its minutes ran out. */
|
|
47
|
+
export type LiveAvatarNotice = 'interrupted' | 'voice-only' | 'time-limit' | 'minutes-ended';
|
|
48
|
+
/** ★ BOFF-9446: the bounded check after a plan purchase (`checkMinutesPurchase`): `checking` while it runs. */
|
|
49
|
+
export type LiveAvatarPurchaseCheck = 'idle' | 'checking';
|
|
33
50
|
/** A Turn off this soon after the tab's start click is the second click of a double click, not a choice (BOFF-9341, critic #2). */
|
|
34
51
|
export declare const LIVE_AVATAR_TAB_OFF_GUARD_MS = 1000;
|
|
35
52
|
export interface LiveAvatarSnapshot {
|
|
@@ -76,6 +93,30 @@ export interface LiveAvatarSnapshot {
|
|
|
76
93
|
readonly audioBlocked: boolean;
|
|
77
94
|
/** The avatar's video has arrived. */
|
|
78
95
|
readonly videoReady: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* ★ BOFF-9696: the browser refuses to play the avatar's video (iOS in Low Power Mode, until a user gesture): the views show the stage's
|
|
98
|
+
* placeholder instead of a frozen face. The sound tap (`resumeAudio`) plays it too, and so does the person's next tap or key on the page.
|
|
99
|
+
*/
|
|
100
|
+
readonly videoBlocked: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* ★ BOFF-9446: the minutes block of the latest availability answer (the server sends one in `enforce` only), or null: no answer with
|
|
103
|
+
* one yet (today's answers never have one), an answer with none since, or cleared by a workspace switch or a "not offered" answer.
|
|
104
|
+
* A quiet question (`refreshMinutes`) replaces it; it never changes the phase, and changes the problem only to settle a minutes
|
|
105
|
+
* problem that has no time of its own (see `refreshMinutes`).
|
|
106
|
+
*/
|
|
107
|
+
readonly minutes: AvatarMinutes | null;
|
|
108
|
+
/** ★ BOFF-9446: with the problem `minutes-exhausted`, the reason the server or the minutes answer gave; null otherwise. Goes with it. */
|
|
109
|
+
readonly minutesReason: AvatarMinutesReason | null;
|
|
110
|
+
/** ★ BOFF-9446: the running session's minutes, from its start answer (enforce only); null otherwise. Set at `ready`. */
|
|
111
|
+
readonly sessionMinutes: AvatarSessionMinutes | null;
|
|
112
|
+
/**
|
|
113
|
+
* ★ BOFF-9446: wall-clock ms at which the server ends the running session (create sent + D' - 5 s). Measured on the local scheduler
|
|
114
|
+
* from the moment the create was sent, and placed on the local clock once, at `ready`: never the server's wall clock. Null unless the
|
|
115
|
+
* start answered D'.
|
|
116
|
+
*/
|
|
117
|
+
readonly sessionEndsAt: number | null;
|
|
118
|
+
/** ★ BOFF-9446: the bounded check after a plan purchase (`checkMinutesPurchase`). */
|
|
119
|
+
readonly purchase: LiveAvatarPurchaseCheck;
|
|
79
120
|
}
|
|
80
121
|
/** The person's stored choices (P4), read and written through this narrow port so tests need no storage. */
|
|
81
122
|
export interface LiveAvatarPreferences {
|
|
@@ -115,7 +156,7 @@ export interface LiveAvatarControllerDeps {
|
|
|
115
156
|
readonly releaseAudioElement: (element: HTMLAudioElement) => void;
|
|
116
157
|
/** A level meter on the avatar's received audio; null when none can be built (the output then cannot prove silence). */
|
|
117
158
|
readonly startMeter: (track: MediaStreamTrack, onChange: (audible: boolean) => void) => AvatarMeterHandle | null;
|
|
118
|
-
/** The browser can show the avatar at all (
|
|
159
|
+
/** The browser can show the avatar at all: what it needs is there (any browser or phone since BOFF-9696). */
|
|
119
160
|
readonly browserSupported: () => boolean;
|
|
120
161
|
/**
|
|
121
162
|
* Wall-clock milliseconds: a refusal's retry time (`retryAt`), its lock and its re-check (`recheckRetry`); also the fallback for
|
|
@@ -125,6 +166,19 @@ export interface LiveAvatarControllerDeps {
|
|
|
125
166
|
readonly newIdempotencyKey: () => string;
|
|
126
167
|
readonly subscribeWorkspaceChange?: (listener: () => void) => () => void;
|
|
127
168
|
readonly subscribePageHide?: (listener: () => void) => () => void;
|
|
169
|
+
/**
|
|
170
|
+
* ★ BOFF-9696 (S5): the page was hidden (its document hidden). A phone hides a page without `pagehide` when the person switches apps or
|
|
171
|
+
* locks the screen, and freezes its timers then, so a session left on there would run on unseen until its idle rule or deadline.
|
|
172
|
+
*/
|
|
173
|
+
readonly subscribePageHidden?: (listener: () => void) => () => void;
|
|
174
|
+
/** ★ BOFF-9696 (S5): whether a hidden page ends the session at once (touch devices); absent or false: only `pagehide` does. */
|
|
175
|
+
readonly endOnHide?: () => boolean;
|
|
176
|
+
/** ★ BOFF-9696: the person's next tap or key anywhere on the page (a user gesture), once: it plays a refused video. */
|
|
177
|
+
readonly subscribeUserGesture?: (listener: () => void) => () => void;
|
|
178
|
+
/** ★ BOFF-9446: the page is shown now; no quiet minutes question is asked while it is hidden. Absent: always shown. */
|
|
179
|
+
readonly isPageVisible?: () => boolean;
|
|
180
|
+
/** ★ BOFF-9446: called when the page is shown again (after it was hidden): the minutes are asked again then, quietly. */
|
|
181
|
+
readonly subscribePageVisible?: (listener: () => void) => () => void;
|
|
128
182
|
/**
|
|
129
183
|
* The page is shown again (its document visible again, or shown from the back/forward cache): the listener is `recheckRetry`. The
|
|
130
184
|
* scheduler's monotonic clock does not count a system sleep, so after one a refusal's timer fires late; the wall clock (`now`) does.
|
|
@@ -180,6 +234,28 @@ export interface LiveAvatarController {
|
|
|
180
234
|
setTabOpen(open: boolean): void;
|
|
181
235
|
/** The person declined the disclosure. */
|
|
182
236
|
cancelChoice(): void;
|
|
237
|
+
/**
|
|
238
|
+
* ★ BOFF-9446: ask availability again, QUIETLY, for its minutes only (the host's own reasons, such as the app coming back from its
|
|
239
|
+
* billing pages; the controller asks by itself after an end, when the tab opens and when the page is shown again). The phase and the
|
|
240
|
+
* chooser never change, no problem is raised, and a failure, a 429 included, keeps the last answer. The newer minutes settle a minutes
|
|
241
|
+
* problem that has no time of its own (none left, in use, not set up: Turn on cannot start), as `confirm` would find them: it says
|
|
242
|
+
* what they rule out, or goes when they leave room for a start (AVAILABLE, UNAVAILABLE, no block). A refusal with a time (`retryAt`)
|
|
243
|
+
* waits for it, and the zero-session limit stays (no minutes answer reports it). It asks only once the server has sent minutes (with
|
|
244
|
+
* today's answers it asks nothing), only while the avatar is offered and off or choosing, never while the page is hidden, never before
|
|
245
|
+
* the Retry-After of the last 429 to an availability question, and at most once per `LIVE_AVATAR_MINUTES_REFRESH_MS` (one asked sooner
|
|
246
|
+
* waits for it).
|
|
247
|
+
*/
|
|
248
|
+
refreshMinutes(): void;
|
|
249
|
+
/**
|
|
250
|
+
* ★ BOFF-9446: the person came back from a plan checkout. `purchase` is `checking` while up to `LIVE_AVATAR_PURCHASE_POLLS`
|
|
251
|
+
* quiet questions are asked, `LIVE_AVATAR_PURCHASE_POLL_MS` apart, the first no sooner than that after the last availability question
|
|
252
|
+
* of any kind; it stops as soon as the credits available rise above what they were when it began. Its answers settle the credits
|
|
253
|
+
* problem as a quiet question's do (`refreshMinutes`). Called while a check runs, the check starts again with all its questions, at
|
|
254
|
+
* the same pace. A tick is skipped, and counted, while the page is hidden, while another availability question is out, or before the
|
|
255
|
+
* Retry-After of the last 429 to one. Only once the server has sent minutes (`minutes` not null): a call before then is dropped, not
|
|
256
|
+
* remembered, so the host calls it once the snapshot has minutes.
|
|
257
|
+
*/
|
|
258
|
+
checkMinutesPurchase(): void;
|
|
183
259
|
selectAvatar(avatarId: string): void;
|
|
184
260
|
/**
|
|
185
261
|
* Avatar OFF: the toggle, the compact view's stop, the tab's Turn off, the end of the voice conversation (P2). Idempotent. In the tab,
|