@burdenoff/website-sdk 2026.1105.2 → 2026.1105.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/avatar-live-view/index.d.mts +1 -1
- package/dist/avatar-live-view/index.d.ts +1 -1
- package/dist/avatar-live-view/index.js +269 -78
- package/dist/avatar-live-view/index.js.map +1 -1
- package/dist/avatar-live-view/index.mjs +262 -71
- package/dist/avatar-live-view/index.mjs.map +1 -1
- package/dist/avatar-mic/index.d.mts +187 -62
- package/dist/avatar-mic/index.d.ts +187 -62
- package/dist/avatar-mic/index.js +457 -5
- package/dist/avatar-mic/index.js.map +1 -1
- package/dist/avatar-mic/index.mjs +457 -6
- package/dist/avatar-mic/index.mjs.map +1 -1
- package/dist/{chunk-XIZE37IB.js → chunk-3AIAJ2L4.js} +2 -2
- package/dist/chunk-3AIAJ2L4.js.map +1 -0
- package/dist/{chunk-MVNN6LLO.mjs → chunk-JGU6JW4N.mjs} +2 -2
- package/dist/chunk-JGU6JW4N.mjs.map +1 -0
- package/dist/{explore-avatar-live-config-auLKxSzx.d.ts → explore-avatar-live-config-1zeXdMer.d.ts} +6 -3
- package/dist/{explore-avatar-live-config-BL8FTm0I.d.mts → explore-avatar-live-config-BxiE66Im.d.mts} +6 -3
- package/dist/index.d.mts +6 -5
- package/dist/index.d.ts +6 -5
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-MVNN6LLO.mjs.map +0 -1
- package/dist/chunk-XIZE37IB.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { turnstileSiteKeyOf, isAvatarLiveLocaleReviewed, isAvatarSite, isAvatarVersion } from '../chunk-
|
|
1
|
+
import { turnstileSiteKeyOf, isAvatarLiveLocaleReviewed, isAvatarSite, isAvatarVersion } from '../chunk-JGU6JW4N.mjs';
|
|
2
2
|
import { answerDisplayText, fill } from '../chunk-JZS6FQT6.mjs';
|
|
3
3
|
import { cn } from '../chunk-R7G6TWIS.mjs';
|
|
4
4
|
import '../chunk-VNS5WDWO.mjs';
|
|
@@ -69,6 +69,17 @@ function readSttOffer(value) {
|
|
|
69
69
|
}
|
|
70
70
|
return { token, expiresAt };
|
|
71
71
|
}
|
|
72
|
+
var MIC_MAX_PRESS_SECONDS = 30;
|
|
73
|
+
var UNREADABLE_MIC = { mode: "unreadable" };
|
|
74
|
+
function readMicOffer(value) {
|
|
75
|
+
if (!isRecord(value)) return UNREADABLE_MIC;
|
|
76
|
+
const keys = Object.keys(value).sort();
|
|
77
|
+
const { mode, maxPressSeconds } = value;
|
|
78
|
+
if (keys.length !== 2 || keys[0] !== "maxPressSeconds" || keys[1] !== "mode" || mode !== "browser" || maxPressSeconds !== MIC_MAX_PRESS_SECONDS) {
|
|
79
|
+
return UNREADABLE_MIC;
|
|
80
|
+
}
|
|
81
|
+
return { mode: "browser", maxPressSeconds: MIC_MAX_PRESS_SECONDS };
|
|
82
|
+
}
|
|
72
83
|
function readAvailability(status, body) {
|
|
73
84
|
if (status !== 200 || !isRecord(body)) return INERT;
|
|
74
85
|
const { available, reason } = body;
|
|
@@ -83,6 +94,7 @@ function readAvailability(status, body) {
|
|
|
83
94
|
return INERT;
|
|
84
95
|
}
|
|
85
96
|
const stt = readSttOffer(body.stt);
|
|
97
|
+
const mic = Object.prototype.hasOwnProperty.call(body, "mic") ? readMicOffer(body.mic) : void 0;
|
|
86
98
|
return {
|
|
87
99
|
kind: "available",
|
|
88
100
|
offer: {
|
|
@@ -90,7 +102,8 @@ function readAvailability(status, body) {
|
|
|
90
102
|
maxSessionSeconds,
|
|
91
103
|
botCheck,
|
|
92
104
|
questions,
|
|
93
|
-
...stt === void 0 ? {} : { stt }
|
|
105
|
+
...stt === void 0 ? {} : { stt },
|
|
106
|
+
...mic === void 0 ? {} : { mic }
|
|
94
107
|
}
|
|
95
108
|
};
|
|
96
109
|
}
|
|
@@ -845,6 +858,7 @@ var STRINGS = {
|
|
|
845
858
|
function avatarLiveStrings(locale) {
|
|
846
859
|
return STRINGS[locale] ?? EN;
|
|
847
860
|
}
|
|
861
|
+
var BROWSER_MIC_NOTICE_VERSION = "web-mic-2026-11-21-c";
|
|
848
862
|
var CONSENT = /* @__PURE__ */ new Map([
|
|
849
863
|
[
|
|
850
864
|
"web-consent-2026-11",
|
|
@@ -1022,6 +1036,51 @@ var CONSENT = /* @__PURE__ */ new Map([
|
|
|
1022
1036
|
followUps: true
|
|
1023
1037
|
}
|
|
1024
1038
|
}
|
|
1039
|
+
],
|
|
1040
|
+
[
|
|
1041
|
+
// Stage C (BOFF-9797, the operator's decision of 2026-11-05): the mic through the browser's own speech recognition.
|
|
1042
|
+
// A DRAFT for the operator's privacy review that no environment uses yet (global-speech-svc reserves the same two
|
|
1043
|
+
// versions, and pairs them with its AVATAR_WEB_MIC=browser): web-consent-2026-11-19-d with three changes only. The
|
|
1044
|
+
// intro warns against personal details said into the microphone and says a tap on it stops the answer being read;
|
|
1045
|
+
// the processing says which speech service turns the voice into text (the browser's own, under its own terms, with
|
|
1046
|
+
// the vendors as examples) and that a live session is told of a tap and of a recording, never of the words; the
|
|
1047
|
+
// storage says the camera is not used and that the voice never reaches us, nor the text until Send, in place of
|
|
1048
|
+
// "Your microphone and camera are not used.", which is true only with no mic. The notice files of record and their
|
|
1049
|
+
// digests are a gitops change of their own, made with the review.
|
|
1050
|
+
"web-consent-2026-11-21-c",
|
|
1051
|
+
{
|
|
1052
|
+
en: {
|
|
1053
|
+
title: "Live AI avatar (beta)",
|
|
1054
|
+
introProduct: null,
|
|
1055
|
+
intro: "This is an AI avatar, not a real person. It reads aloud the answers to the questions you ask in the Avatar tab, provided each passes the speech checks. After an answer, it waits about thirty seconds for your next question, then ends. A session lasts at most three minutes; if an answer doesn't fit in the time left, it reads only the first part and says so, or doesn't read it, and the full answer stays in Chat. The answers are AI-generated and can be wrong. Don't put personal details in a question you type. Don't say them into the microphone either. Tapping the microphone while the avatar speaks stops that answer.",
|
|
1056
|
+
processing: "When you start, Cloudflare checks for bots. The answer text goes to Google to create the voice. Our server sends that voice through LiveKit to Protoface, which makes the avatar video streamed through LiveKit. Your browser connects to LiveKit, which sees your IP address. If you use the microphone, the speech service your browser uses turns your voice into text and may receive the audio: for example Google for Chrome on a computer or Android phone, Apple for Safari and on iPhone and iPad, and Microsoft for Edge on a computer, each under its own privacy terms. During a live session, our server is told when you tap the microphone and while you record (never what you say), so the avatar stops and waits.",
|
|
1057
|
+
storage: "Your camera is not used. We never receive your voice: the text appears in the question box for you to check, and none of what you said reaches us until you press Send. We do not record the live session or store the generated voice. We keep a record of this consent. The privacy notice explains our safeguards, storage periods and the providers' processing. For people aged eighteen and over.",
|
|
1058
|
+
privacyLink: "How we handle this",
|
|
1059
|
+
botCheck: "turnstile",
|
|
1060
|
+
questions: "any",
|
|
1061
|
+
followUps: true,
|
|
1062
|
+
micNotice: BROWSER_MIC_NOTICE_VERSION
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
],
|
|
1066
|
+
[
|
|
1067
|
+
// The words of web-consent-2026-11-21-c without "When you start, Cloudflare checks for bots.", for a server that runs
|
|
1068
|
+
// no bot check. A DRAFT, like it.
|
|
1069
|
+
"web-consent-2026-11-22-c",
|
|
1070
|
+
{
|
|
1071
|
+
en: {
|
|
1072
|
+
title: "Live AI avatar (beta)",
|
|
1073
|
+
introProduct: null,
|
|
1074
|
+
intro: "This is an AI avatar, not a real person. It reads aloud the answers to the questions you ask in the Avatar tab, provided each passes the speech checks. After an answer, it waits about thirty seconds for your next question, then ends. A session lasts at most three minutes; if an answer doesn't fit in the time left, it reads only the first part and says so, or doesn't read it, and the full answer stays in Chat. The answers are AI-generated and can be wrong. Don't put personal details in a question you type. Don't say them into the microphone either. Tapping the microphone while the avatar speaks stops that answer.",
|
|
1075
|
+
processing: "The answer text goes to Google to create the voice. Our server sends that voice through LiveKit to Protoface, which makes the avatar video streamed through LiveKit. Your browser connects to LiveKit, which sees your IP address. If you use the microphone, the speech service your browser uses turns your voice into text and may receive the audio: for example Google for Chrome on a computer or Android phone, Apple for Safari and on iPhone and iPad, and Microsoft for Edge on a computer, each under its own privacy terms. During a live session, our server is told when you tap the microphone and while you record (never what you say), so the avatar stops and waits.",
|
|
1076
|
+
storage: "Your camera is not used. We never receive your voice: the text appears in the question box for you to check, and none of what you said reaches us until you press Send. We do not record the live session or store the generated voice. We keep a record of this consent. The privacy notice explains our safeguards, storage periods and the providers' processing. For people aged eighteen and over.",
|
|
1077
|
+
privacyLink: "How we handle this",
|
|
1078
|
+
botCheck: "off",
|
|
1079
|
+
questions: "any",
|
|
1080
|
+
followUps: true,
|
|
1081
|
+
micNotice: BROWSER_MIC_NOTICE_VERSION
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1025
1084
|
]
|
|
1026
1085
|
]);
|
|
1027
1086
|
function avatarConsentCopy(version, locale) {
|
|
@@ -1066,6 +1125,22 @@ var MIC_NOTICE = /* @__PURE__ */ new Map([
|
|
|
1066
1125
|
privacyLink: "How we handle this"
|
|
1067
1126
|
}
|
|
1068
1127
|
}
|
|
1128
|
+
],
|
|
1129
|
+
[
|
|
1130
|
+
// The browser's own speech recognition (Stage C): the browser's speech service, named by example where each is
|
|
1131
|
+
// true, under its own terms; nothing of the voice reaches us, nor the text before Send; a live session told only of
|
|
1132
|
+
// a tap and of a recording. No daily limits: nothing is counted on the server.
|
|
1133
|
+
BROWSER_MIC_NOTICE_VERSION,
|
|
1134
|
+
{
|
|
1135
|
+
en: {
|
|
1136
|
+
title: "Ask with your voice",
|
|
1137
|
+
processing: "The speech service your browser uses turns your voice into text and may receive the audio: for example Google for Chrome on a computer or Android phone, Apple for Safari and on iPhone and iPad, and Microsoft for Edge on a computer. Its own privacy terms apply.",
|
|
1138
|
+
storage: "We never receive your voice, and your camera is not used.",
|
|
1139
|
+
text: "The text appears in the box for you to check. None of what you said reaches us until you press Send; once you send it, it is a chat message, kept like any other. During a live session, we are told when you tap the microphone and while you record (never what you say), so the avatar stops and waits.",
|
|
1140
|
+
limits: "Each recording stops after thirty seconds at the latest.",
|
|
1141
|
+
privacyLink: "How we handle this"
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1069
1144
|
]
|
|
1070
1145
|
]);
|
|
1071
1146
|
function avatarMicNotice(version, locale) {
|
|
@@ -1076,10 +1151,12 @@ var PRIVACY_HREF = "/privacy#live-avatar";
|
|
|
1076
1151
|
var LIMIT_SECONDS = 30;
|
|
1077
1152
|
var TICK_MS = 250;
|
|
1078
1153
|
var IDLE = { name: "idle" };
|
|
1079
|
-
var
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
])
|
|
1154
|
+
var EN_IN = { lang: "en-IN", fallbackLang: "en-US" };
|
|
1155
|
+
var BROWSER_MIC_LANGUAGES = { en: EN_IN };
|
|
1156
|
+
var FOR_GOOD = {
|
|
1157
|
+
stream: /* @__PURE__ */ new Set(["unavailable", "limit"]),
|
|
1158
|
+
browser: /* @__PURE__ */ new Set(["unsupported"])
|
|
1159
|
+
};
|
|
1083
1160
|
var FAILURE_TEXT = {
|
|
1084
1161
|
denied: "micDenied",
|
|
1085
1162
|
unsupported: "micUnsupported",
|
|
@@ -1088,14 +1165,32 @@ var FAILURE_TEXT = {
|
|
|
1088
1165
|
busy: "micBusy",
|
|
1089
1166
|
failed: "micFailed"
|
|
1090
1167
|
};
|
|
1168
|
+
var loadedMic = null;
|
|
1091
1169
|
function loadAvatarMic() {
|
|
1092
|
-
return import('../avatar-mic/index.mjs').then((loaded) =>
|
|
1093
|
-
|
|
1094
|
-
|
|
1170
|
+
return import('../avatar-mic/index.mjs').then((loaded) => {
|
|
1171
|
+
const module = {
|
|
1172
|
+
createAvatarMic: loaded.createAvatarMic,
|
|
1173
|
+
createBrowserAvatarMic: loaded.createBrowserAvatarMic
|
|
1174
|
+
};
|
|
1175
|
+
loadedMic = module;
|
|
1176
|
+
return module;
|
|
1177
|
+
});
|
|
1095
1178
|
}
|
|
1096
1179
|
function canUseAvatarMic() {
|
|
1097
1180
|
return typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getUserMedia === "function" && typeof AudioContext !== "undefined" && typeof AudioWorkletNode !== "undefined" && typeof WebSocket !== "undefined";
|
|
1098
1181
|
}
|
|
1182
|
+
function canUseBrowserMic() {
|
|
1183
|
+
if (typeof window === "undefined" || typeof navigator === "undefined") {
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
if (window.isSecureContext !== true) return false;
|
|
1187
|
+
const recognizer = Reflect.get(window, "SpeechRecognition") ?? Reflect.get(window, "webkitSpeechRecognition");
|
|
1188
|
+
if (typeof recognizer !== "function") return false;
|
|
1189
|
+
const agent = navigator.userAgent ?? "";
|
|
1190
|
+
if (/\bFirefox\//.test(agent) || /\bFxiOS\//.test(agent)) return false;
|
|
1191
|
+
return !("brave" in navigator);
|
|
1192
|
+
}
|
|
1193
|
+
var REPLAY_PRIVATE = "rr-block rr-mask";
|
|
1099
1194
|
function clock(seconds) {
|
|
1100
1195
|
const whole = Math.max(0, Math.floor(seconds));
|
|
1101
1196
|
return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, "0")}`;
|
|
@@ -1106,37 +1201,51 @@ function withHeardText(current, heard, maxLength) {
|
|
|
1106
1201
|
const joined = current.trim() === "" ? text : `${current.replace(/\s+$/, "")} ${text}`;
|
|
1107
1202
|
return joined.slice(0, Math.max(0, maxLength));
|
|
1108
1203
|
}
|
|
1204
|
+
function noticeOf(source) {
|
|
1205
|
+
return source.kind === "stream" ? AVATAR_MIC_NOTICE_VERSION : source.notice;
|
|
1206
|
+
}
|
|
1207
|
+
function micKeyOf(source, locale) {
|
|
1208
|
+
return source.kind === "stream" ? `stream ${source.base}` : `browser ${BROWSER_MIC_LANGUAGES[locale]?.lang ?? ""}`;
|
|
1209
|
+
}
|
|
1109
1210
|
function useAvatarMic({
|
|
1110
1211
|
enabled,
|
|
1111
1212
|
locale,
|
|
1112
|
-
|
|
1113
|
-
offer,
|
|
1114
|
-
refreshOffer,
|
|
1213
|
+
source,
|
|
1115
1214
|
onText,
|
|
1116
1215
|
onStart
|
|
1117
1216
|
}) {
|
|
1118
1217
|
const S = avatarMicStrings(locale);
|
|
1119
|
-
const
|
|
1218
|
+
const noticeVersion = source === null ? null : noticeOf(source);
|
|
1219
|
+
const copy = noticeVersion === null ? null : avatarMicNotice(noticeVersion, locale);
|
|
1220
|
+
const kind = source?.kind ?? null;
|
|
1221
|
+
const micKey = source === null ? null : micKeyOf(source, locale);
|
|
1120
1222
|
const uid = useId();
|
|
1121
1223
|
const noticeTitleId = `${uid}-mic-notice`;
|
|
1122
|
-
const [withdrawn, setWithdrawn] = useState(
|
|
1123
|
-
const [supported, setSupported] = useState(false);
|
|
1124
|
-
useEffect(
|
|
1125
|
-
|
|
1126
|
-
|
|
1224
|
+
const [withdrawn, setWithdrawn] = useState({});
|
|
1225
|
+
const [supported, setSupported] = useState({ stream: false, browser: false });
|
|
1226
|
+
useEffect(
|
|
1227
|
+
() => setSupported({ stream: canUseAvatarMic(), browser: canUseBrowserMic() }),
|
|
1228
|
+
[]
|
|
1229
|
+
);
|
|
1230
|
+
const usable = source === null ? false : source.kind === "stream" ? supported.stream : supported.browser && BROWSER_MIC_LANGUAGES[locale] !== void 0;
|
|
1231
|
+
const possible = enabled && kind !== null && usable && S !== null && copy !== null;
|
|
1232
|
+
const withdrawnReason = kind === null ? null : withdrawn[kind] ?? null;
|
|
1233
|
+
const offered = possible && withdrawnReason === null;
|
|
1127
1234
|
const [state, setState] = useState(IDLE);
|
|
1128
1235
|
const [noticeOpen, setNoticeOpen] = useState(false);
|
|
1129
|
-
const agreedRef = useRef(
|
|
1236
|
+
const agreedRef = useRef(null);
|
|
1130
1237
|
const [loading2, setLoading] = useState(false);
|
|
1131
1238
|
const [loadFailed, setLoadFailed] = useState(false);
|
|
1132
1239
|
const [now, setNow] = useState(() => Date.now());
|
|
1133
|
-
const micRef = useRef(
|
|
1240
|
+
const micRef = useRef(
|
|
1241
|
+
null
|
|
1242
|
+
);
|
|
1134
1243
|
const seqRef = useRef(0);
|
|
1135
1244
|
const buttonRef = useRef(null);
|
|
1136
1245
|
const allowRef = useRef(null);
|
|
1137
|
-
const
|
|
1138
|
-
const latest = useRef({
|
|
1139
|
-
latest.current = {
|
|
1246
|
+
const usedRef = useRef(null);
|
|
1247
|
+
const latest = useRef({ source, locale, noticeVersion, onText, onStart });
|
|
1248
|
+
latest.current = { source, locale, noticeVersion, onText, onStart };
|
|
1140
1249
|
const cancel = useCallback(() => {
|
|
1141
1250
|
seqRef.current += 1;
|
|
1142
1251
|
setLoading(false);
|
|
@@ -1147,6 +1256,12 @@ function useAvatarMic({
|
|
|
1147
1256
|
useEffect(() => {
|
|
1148
1257
|
if (!offered) cancel();
|
|
1149
1258
|
}, [offered, cancel]);
|
|
1259
|
+
const keyRef = useRef(micKey);
|
|
1260
|
+
useEffect(() => {
|
|
1261
|
+
if (keyRef.current === micKey) return;
|
|
1262
|
+
keyRef.current = micKey;
|
|
1263
|
+
cancel();
|
|
1264
|
+
}, [micKey, cancel]);
|
|
1150
1265
|
useEffect(
|
|
1151
1266
|
() => () => {
|
|
1152
1267
|
seqRef.current += 1;
|
|
@@ -1154,29 +1269,55 @@ function useAvatarMic({
|
|
|
1154
1269
|
},
|
|
1155
1270
|
[]
|
|
1156
1271
|
);
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1272
|
+
const micOf = useCallback(
|
|
1273
|
+
(loaded, wanted) => {
|
|
1274
|
+
const key = micKeyOf(wanted, latest.current.locale);
|
|
1275
|
+
const held = micRef.current;
|
|
1276
|
+
if (held !== null && held.key === key) return held.mic;
|
|
1277
|
+
held?.mic.cancel();
|
|
1278
|
+
let mic;
|
|
1279
|
+
if (wanted.kind === "stream") {
|
|
1280
|
+
const stream = () => {
|
|
1281
|
+
const current = latest.current.source;
|
|
1282
|
+
return current?.kind === "stream" ? current : null;
|
|
1283
|
+
};
|
|
1284
|
+
mic = loaded.createAvatarMic({
|
|
1285
|
+
base: wanted.base,
|
|
1286
|
+
notice: AVATAR_MIC_NOTICE_VERSION,
|
|
1287
|
+
offer: () => stream()?.offer ?? null,
|
|
1288
|
+
refreshOffer: () => stream()?.refreshOffer() ?? Promise.resolve(null),
|
|
1289
|
+
onState: setState
|
|
1290
|
+
});
|
|
1291
|
+
} else {
|
|
1292
|
+
const language = BROWSER_MIC_LANGUAGES[latest.current.locale] ?? EN_IN;
|
|
1293
|
+
mic = loaded.createBrowserAvatarMic({
|
|
1294
|
+
lang: language.lang,
|
|
1295
|
+
fallbackLang: language.fallbackLang,
|
|
1296
|
+
maxPressMs: LIMIT_SECONDS * 1e3,
|
|
1297
|
+
onState: setState
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
micRef.current = { key, kind: wanted.kind, mic };
|
|
1301
|
+
return mic;
|
|
1302
|
+
},
|
|
1303
|
+
[]
|
|
1304
|
+
);
|
|
1172
1305
|
const press = useCallback(() => {
|
|
1173
|
-
const
|
|
1174
|
-
if (
|
|
1306
|
+
const wanted = latest.current.source;
|
|
1307
|
+
if (wanted === null) return;
|
|
1175
1308
|
seqRef.current += 1;
|
|
1176
1309
|
const seq = seqRef.current;
|
|
1177
1310
|
setLoadFailed(false);
|
|
1311
|
+
const held = micRef.current;
|
|
1312
|
+
const ready = wanted.kind !== "browser" ? null : held !== null && held.key === micKeyOf(wanted, latest.current.locale) ? held.mic : loadedMic !== null ? micOf(loadedMic, wanted) : null;
|
|
1313
|
+
if (ready !== null) {
|
|
1314
|
+
setLoading(false);
|
|
1315
|
+
ready.start();
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1178
1318
|
setLoading(true);
|
|
1179
|
-
|
|
1319
|
+
const arriving = held !== null && held.key === micKeyOf(wanted, latest.current.locale) ? Promise.resolve(held.mic) : loadAvatarMic().then((loaded) => micOf(loaded, wanted));
|
|
1320
|
+
void arriving.then(
|
|
1180
1321
|
(mic) => {
|
|
1181
1322
|
if (seqRef.current !== seq) return;
|
|
1182
1323
|
setLoading(false);
|
|
@@ -1188,8 +1329,9 @@ function useAvatarMic({
|
|
|
1188
1329
|
setLoadFailed(true);
|
|
1189
1330
|
}
|
|
1190
1331
|
);
|
|
1191
|
-
}, [
|
|
1332
|
+
}, [micOf]);
|
|
1192
1333
|
const recording = loading2 || state.name === "starting" || state.name === "listening";
|
|
1334
|
+
const finishing = kind === "browser" && state.name === "finishing";
|
|
1193
1335
|
const tap = useCallback(() => {
|
|
1194
1336
|
if (state.name === "listening" || state.name === "starting") {
|
|
1195
1337
|
micRef.current?.mic.stop();
|
|
@@ -1199,16 +1341,26 @@ function useAvatarMic({
|
|
|
1199
1341
|
cancel();
|
|
1200
1342
|
return;
|
|
1201
1343
|
}
|
|
1344
|
+
if (finishing) return;
|
|
1202
1345
|
latest.current.onStart?.();
|
|
1203
|
-
if (
|
|
1346
|
+
if (agreedRef.current !== latest.current.noticeVersion) {
|
|
1204
1347
|
setNoticeOpen(true);
|
|
1205
|
-
|
|
1348
|
+
const seq = seqRef.current;
|
|
1349
|
+
void loadAvatarMic().then(
|
|
1350
|
+
(loaded) => {
|
|
1351
|
+
const wanted = latest.current.source;
|
|
1352
|
+
if (seqRef.current === seq && wanted?.kind === "browser") {
|
|
1353
|
+
micOf(loaded, wanted);
|
|
1354
|
+
}
|
|
1355
|
+
},
|
|
1356
|
+
() => void 0
|
|
1357
|
+
);
|
|
1206
1358
|
return;
|
|
1207
1359
|
}
|
|
1208
1360
|
press();
|
|
1209
|
-
}, [cancel, loading2, press, state.name]);
|
|
1361
|
+
}, [cancel, finishing, loading2, micOf, press, state.name]);
|
|
1210
1362
|
const allow = useCallback(() => {
|
|
1211
|
-
agreedRef.current =
|
|
1363
|
+
agreedRef.current = latest.current.noticeVersion;
|
|
1212
1364
|
setNoticeOpen(false);
|
|
1213
1365
|
buttonRef.current?.focus();
|
|
1214
1366
|
press();
|
|
@@ -1228,11 +1380,15 @@ function useAvatarMic({
|
|
|
1228
1380
|
return () => clearInterval(timer);
|
|
1229
1381
|
}, [listeningSince]);
|
|
1230
1382
|
useEffect(() => {
|
|
1231
|
-
if (state.name === "done" && state
|
|
1232
|
-
|
|
1383
|
+
if (state.name === "done" && state !== usedRef.current) {
|
|
1384
|
+
usedRef.current = state;
|
|
1233
1385
|
if (state.text !== "") latest.current.onText(state.text);
|
|
1234
|
-
|
|
1235
|
-
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
const pressed = micRef.current?.kind;
|
|
1389
|
+
if (state.name === "failed" && pressed !== void 0 && FOR_GOOD[pressed].has(state.reason)) {
|
|
1390
|
+
const reason = state.reason;
|
|
1391
|
+
setWithdrawn((held) => ({ ...held, [pressed]: reason }));
|
|
1236
1392
|
}
|
|
1237
1393
|
}, [state]);
|
|
1238
1394
|
if (!possible || S === null || copy === null) {
|
|
@@ -1244,7 +1400,7 @@ function useAvatarMic({
|
|
|
1244
1400
|
recording: false
|
|
1245
1401
|
};
|
|
1246
1402
|
}
|
|
1247
|
-
const status =
|
|
1403
|
+
const status = withdrawnReason !== null ? S[FAILURE_TEXT[withdrawnReason]] : loading2 ? S.micStarting : loadFailed ? S.micFailed : state.name === "starting" ? S.micStarting : state.name === "listening" ? S.micListening : state.name === "finishing" ? S.micFinishing : state.name === "done" ? state.text === "" ? S.micNoSpeech : state.limit ? S.micDoneLimit : S.micDone : state.name === "failed" ? S[FAILURE_TEXT[state.reason]] : "";
|
|
1248
1404
|
const heard = offered && (state.name === "listening" || state.name === "finishing") ? state.text : "";
|
|
1249
1405
|
const button = !offered ? null : /* @__PURE__ */ jsx(
|
|
1250
1406
|
"button",
|
|
@@ -1253,12 +1409,15 @@ function useAvatarMic({
|
|
|
1253
1409
|
type: "button",
|
|
1254
1410
|
"aria-label": recording ? S.micStop : S.micStart,
|
|
1255
1411
|
"aria-pressed": recording,
|
|
1412
|
+
"aria-disabled": finishing ? true : void 0,
|
|
1256
1413
|
title: recording ? S.micStop : S.micStart,
|
|
1257
1414
|
"data-boff-explore": "avatar-live-mic",
|
|
1258
1415
|
"data-mic": loading2 ? "starting" : state.name,
|
|
1416
|
+
"data-mic-source": kind ?? void 0,
|
|
1259
1417
|
className: cn(
|
|
1260
1418
|
"inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background motion-reduce:transition-none",
|
|
1261
|
-
recording ? "border-primary bg-primary text-primary-foreground" : "border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground"
|
|
1419
|
+
recording ? "border-primary bg-primary text-primary-foreground" : "border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
|
|
1420
|
+
finishing && "cursor-wait opacity-50"
|
|
1262
1421
|
),
|
|
1263
1422
|
onClick: tap,
|
|
1264
1423
|
children: recording ? /* @__PURE__ */ jsx(Square, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx(Mic, { "aria-hidden": "true", className: "h-4 w-4" })
|
|
@@ -1341,7 +1500,11 @@ function useAvatarMic({
|
|
|
1341
1500
|
{
|
|
1342
1501
|
"data-boff-explore": "avatar-live-mic-heard",
|
|
1343
1502
|
dir: "auto",
|
|
1344
|
-
className:
|
|
1503
|
+
className: cn(
|
|
1504
|
+
// rrweb's default block and mask classes: a site's session replay records none of what is heard.
|
|
1505
|
+
REPLAY_PRIVATE,
|
|
1506
|
+
"line-clamp-3 break-words text-sm italic text-muted-foreground"
|
|
1507
|
+
),
|
|
1345
1508
|
children: heard
|
|
1346
1509
|
}
|
|
1347
1510
|
)
|
|
@@ -1478,6 +1641,7 @@ var PLAYOUT_TAIL_MS = 5e3;
|
|
|
1478
1641
|
var QUIET_PLAYED_OUT_MS = 2500;
|
|
1479
1642
|
var OWED_VOICE_WAIT_MS = 15e3;
|
|
1480
1643
|
var LISTENING_MARK_MS = 1e4;
|
|
1644
|
+
var LISTENING_MARK_SPACING_MS = 5e3;
|
|
1481
1645
|
var BUSY_RETRY_MS = 2e3;
|
|
1482
1646
|
var BUSY_RETRY_MAX_MS = 8e3;
|
|
1483
1647
|
var MAX_BUSY_RETRIES = 24;
|
|
@@ -1603,8 +1767,15 @@ function acceptsQuestion(slot) {
|
|
|
1603
1767
|
function followUpBox(phase, run, token) {
|
|
1604
1768
|
return phase.name === "live" && run !== null && phase.session.followUp !== null && !phase.session.followUp.closed && token === run.token;
|
|
1605
1769
|
}
|
|
1606
|
-
|
|
1607
|
-
|
|
1770
|
+
var NO_MIC = { kind: "none" };
|
|
1771
|
+
function micChoiceOf(result, locale) {
|
|
1772
|
+
if (result.kind !== "available") return NO_MIC;
|
|
1773
|
+
const { offer } = result;
|
|
1774
|
+
const micNotice = avatarConsentCopy(offer.consentVersion, locale)?.micNotice;
|
|
1775
|
+
if (offer.mic !== void 0) {
|
|
1776
|
+
return offer.mic.mode === "browser" && micNotice !== void 0 ? { kind: "browser", notice: micNotice } : NO_MIC;
|
|
1777
|
+
}
|
|
1778
|
+
return offer.stt !== void 0 && micNotice === void 0 ? { kind: "stream", offer: { ...offer.stt, receivedAt: Date.now() } } : NO_MIC;
|
|
1608
1779
|
}
|
|
1609
1780
|
function utcToday() {
|
|
1610
1781
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
@@ -1754,7 +1925,7 @@ function AvatarLiveView({
|
|
|
1754
1925
|
const waitRef = useRef(null);
|
|
1755
1926
|
const sendingRef = useRef(false);
|
|
1756
1927
|
const textBoxRef = useRef(false);
|
|
1757
|
-
const [
|
|
1928
|
+
const [micChoice, setMicChoice] = useState(NO_MIC);
|
|
1758
1929
|
const micCancelRef = useRef(() => void 0);
|
|
1759
1930
|
const sendDueRef = useRef(() => void 0);
|
|
1760
1931
|
const checkRef = useRef(null);
|
|
@@ -2058,7 +2229,7 @@ function AvatarLiveView({
|
|
|
2058
2229
|
const applyAvailability = useCallback(
|
|
2059
2230
|
(result, locale2) => {
|
|
2060
2231
|
const { live: live2 } = latest.current;
|
|
2061
|
-
|
|
2232
|
+
setMicChoice(micChoiceOf(result, locale2));
|
|
2062
2233
|
if (result.kind === "inert") {
|
|
2063
2234
|
setMode("suggested");
|
|
2064
2235
|
go(INERT2);
|
|
@@ -2130,7 +2301,8 @@ function AvatarLiveView({
|
|
|
2130
2301
|
due: null,
|
|
2131
2302
|
retry: null,
|
|
2132
2303
|
sent: /* @__PURE__ */ new Set([context.messageId]),
|
|
2133
|
-
seq: 0
|
|
2304
|
+
seq: 0,
|
|
2305
|
+
listenedAt: null
|
|
2134
2306
|
} : null;
|
|
2135
2307
|
sessionRef.current = {
|
|
2136
2308
|
session,
|
|
@@ -2765,16 +2937,18 @@ function AvatarLiveView({
|
|
|
2765
2937
|
const refreshMicOffer = useCallback(async () => {
|
|
2766
2938
|
const { live: live2, locale: locale2, base: base2, headers: headers2 } = latest.current;
|
|
2767
2939
|
if (base2 === null) return null;
|
|
2768
|
-
const next =
|
|
2940
|
+
const next = micChoiceOf(
|
|
2769
2941
|
await requestAvailabilityAnswer({
|
|
2770
2942
|
base: base2,
|
|
2771
2943
|
headers: headers2,
|
|
2772
2944
|
site: live2.site,
|
|
2773
2945
|
locale: locale2
|
|
2774
|
-
})
|
|
2946
|
+
}),
|
|
2947
|
+
locale2
|
|
2775
2948
|
);
|
|
2776
|
-
if (next !==
|
|
2777
|
-
|
|
2949
|
+
if (next.kind !== "stream") return null;
|
|
2950
|
+
setMicChoice(next);
|
|
2951
|
+
return next.offer;
|
|
2778
2952
|
}, []);
|
|
2779
2953
|
const onMicText = useCallback(
|
|
2780
2954
|
(text) => {
|
|
@@ -2800,8 +2974,14 @@ function AvatarLiveView({
|
|
|
2800
2974
|
handle.playedOut = true;
|
|
2801
2975
|
showFollowUp(handle, { speaking: false });
|
|
2802
2976
|
}, [showFollowUp, tell]);
|
|
2977
|
+
const micSource = micChoice.kind === "browser" ? { kind: "browser", notice: micChoice.notice } : micChoice.kind === "stream" && base !== null ? {
|
|
2978
|
+
kind: "stream",
|
|
2979
|
+
base,
|
|
2980
|
+
offer: micChoice.offer,
|
|
2981
|
+
refreshOffer: refreshMicOffer
|
|
2982
|
+
} : null;
|
|
2803
2983
|
const mic = useAvatarMic({
|
|
2804
|
-
enabled: live.mic === true &&
|
|
2984
|
+
enabled: live.mic === true && micSource !== null && !off && acceptsQuestion({
|
|
2805
2985
|
any,
|
|
2806
2986
|
phase: phase.name,
|
|
2807
2987
|
kind,
|
|
@@ -2810,9 +2990,7 @@ function AvatarLiveView({
|
|
|
2810
2990
|
followUp: followUpAsks
|
|
2811
2991
|
}),
|
|
2812
2992
|
locale,
|
|
2813
|
-
|
|
2814
|
-
offer: micOffer,
|
|
2815
|
-
refreshOffer: refreshMicOffer,
|
|
2993
|
+
source: micSource,
|
|
2816
2994
|
onText: onMicText,
|
|
2817
2995
|
onStart: onMicStart
|
|
2818
2996
|
});
|
|
@@ -2823,15 +3001,23 @@ function AvatarLiveView({
|
|
|
2823
3001
|
if (!micRecording || handle === null || handle.followUps === null || handle.done) {
|
|
2824
3002
|
return;
|
|
2825
3003
|
}
|
|
2826
|
-
|
|
3004
|
+
const run = handle.followUps;
|
|
2827
3005
|
const every = Math.min(
|
|
2828
3006
|
LISTENING_MARK_MS,
|
|
2829
|
-
Math.max(1e3,
|
|
3007
|
+
Math.max(1e3, run.limits.idleSeconds * 1e3 / 3)
|
|
2830
3008
|
);
|
|
2831
|
-
const
|
|
3009
|
+
const spacing = Math.min(LISTENING_MARK_SPACING_MS, every / 2);
|
|
3010
|
+
const mark = () => {
|
|
3011
|
+
const at = Date.now();
|
|
3012
|
+
if (run.listenedAt !== null && at - run.listenedAt < spacing) return;
|
|
3013
|
+
run.listenedAt = at;
|
|
3014
|
+
tell(handle, "listening");
|
|
3015
|
+
};
|
|
3016
|
+
mark();
|
|
3017
|
+
const timer = setInterval(mark, every);
|
|
2832
3018
|
return () => {
|
|
2833
3019
|
clearInterval(timer);
|
|
2834
|
-
if (sessionRef.current === handle)
|
|
3020
|
+
if (sessionRef.current === handle) mark();
|
|
2835
3021
|
};
|
|
2836
3022
|
}, [micRecording, tell]);
|
|
2837
3023
|
const notNow = useCallback(() => {
|
|
@@ -3703,7 +3889,12 @@ function AskBox({
|
|
|
3703
3889
|
dir: "auto",
|
|
3704
3890
|
"data-avatar-primary": touchFirst ? void 0 : "",
|
|
3705
3891
|
"data-boff-explore": "avatar-live-ask-input",
|
|
3706
|
-
className:
|
|
3892
|
+
className: cn(
|
|
3893
|
+
// Kept out of a site's session replay before Send (rrweb's default classes; `rr-ignore` for its input
|
|
3894
|
+
// events): the text may be what the mic heard, or what the visitor typed.
|
|
3895
|
+
REPLAY_PRIVATE,
|
|
3896
|
+
"rr-ignore max-h-32 min-h-11 flex-1 resize-none rounded-md border border-border bg-background px-3 py-2.5 text-base text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:text-sm"
|
|
3897
|
+
),
|
|
3707
3898
|
onChange: (event) => {
|
|
3708
3899
|
setRefusedFiles(false);
|
|
3709
3900
|
onChange(event.target.value);
|