@burdenoff/website-sdk 2026.1105.2 → 2026.1106.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/README.md +133 -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 +1071 -205
- package/dist/avatar-live-view/index.js.map +1 -1
- package/dist/avatar-live-view/index.mjs +1064 -198
- package/dist/avatar-live-view/index.mjs.map +1 -1
- package/dist/avatar-mic/index.d.mts +181 -62
- package/dist/avatar-mic/index.d.ts +181 -62
- package/dist/avatar-mic/index.js +717 -5
- package/dist/avatar-mic/index.js.map +1 -1
- package/dist/avatar-mic/index.mjs +717 -6
- package/dist/avatar-mic/index.mjs.map +1 -1
- package/dist/{chunk-MVNN6LLO.mjs → chunk-BHEHF3Y5.mjs} +2 -2
- package/dist/chunk-BHEHF3Y5.mjs.map +1 -0
- package/dist/{chunk-XIZE37IB.js → chunk-Z2UP22GV.js} +2 -2
- package/dist/chunk-Z2UP22GV.js.map +1 -0
- package/dist/{explore-avatar-live-config-auLKxSzx.d.ts → explore-avatar-live-config-CJstiSAN.d.ts} +10 -3
- package/dist/{explore-avatar-live-config-BL8FTm0I.d.mts → explore-avatar-live-config-D2PGF8fG.d.mts} +10 -3
- package/dist/index.d.mts +5 -4
- package/dist/index.d.ts +5 -4
- 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-BHEHF3Y5.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,25 @@ function readSttOffer(value) {
|
|
|
69
69
|
}
|
|
70
70
|
return { token, expiresAt };
|
|
71
71
|
}
|
|
72
|
+
var MIC_MAX_PRESS_SECONDS = 30;
|
|
73
|
+
var AVATAR_MIC_PAUSE_MIN_MS = 2e3;
|
|
74
|
+
var AVATAR_MIC_PAUSE_MAX_MS = 2500;
|
|
75
|
+
var UNREADABLE_MIC = { mode: "unreadable" };
|
|
76
|
+
function readMicOffer(value) {
|
|
77
|
+
if (!isRecord(value)) return UNREADABLE_MIC;
|
|
78
|
+
const keys = Object.keys(value).sort();
|
|
79
|
+
const { mode, maxPressSeconds, pauseMs } = value;
|
|
80
|
+
if (keys[0] !== "maxPressSeconds" || keys[1] !== "mode" || mode !== "browser" || maxPressSeconds !== MIC_MAX_PRESS_SECONDS) {
|
|
81
|
+
return UNREADABLE_MIC;
|
|
82
|
+
}
|
|
83
|
+
if (keys.length === 2) {
|
|
84
|
+
return { mode: "browser", maxPressSeconds: MIC_MAX_PRESS_SECONDS };
|
|
85
|
+
}
|
|
86
|
+
if (keys.length !== 3 || keys[2] !== "pauseMs" || typeof pauseMs !== "number" || !Number.isSafeInteger(pauseMs) || pauseMs < AVATAR_MIC_PAUSE_MIN_MS || pauseMs > AVATAR_MIC_PAUSE_MAX_MS) {
|
|
87
|
+
return UNREADABLE_MIC;
|
|
88
|
+
}
|
|
89
|
+
return { mode: "browser", maxPressSeconds: MIC_MAX_PRESS_SECONDS, pauseMs };
|
|
90
|
+
}
|
|
72
91
|
function readAvailability(status, body) {
|
|
73
92
|
if (status !== 200 || !isRecord(body)) return INERT;
|
|
74
93
|
const { available, reason } = body;
|
|
@@ -83,6 +102,7 @@ function readAvailability(status, body) {
|
|
|
83
102
|
return INERT;
|
|
84
103
|
}
|
|
85
104
|
const stt = readSttOffer(body.stt);
|
|
105
|
+
const mic = Object.prototype.hasOwnProperty.call(body, "mic") ? readMicOffer(body.mic) : void 0;
|
|
86
106
|
return {
|
|
87
107
|
kind: "available",
|
|
88
108
|
offer: {
|
|
@@ -90,15 +110,19 @@ function readAvailability(status, body) {
|
|
|
90
110
|
maxSessionSeconds,
|
|
91
111
|
botCheck,
|
|
92
112
|
questions,
|
|
93
|
-
...stt === void 0 ? {} : { stt }
|
|
113
|
+
...stt === void 0 ? {} : { stt },
|
|
114
|
+
...mic === void 0 ? {} : { mic }
|
|
94
115
|
}
|
|
95
116
|
};
|
|
96
117
|
}
|
|
97
118
|
const meaning = available ? void 0 : AVAILABILITY_REASONS.get(reason);
|
|
98
|
-
|
|
119
|
+
if (meaning === void 0 || meaning === "inert") return INERT;
|
|
120
|
+
const named = body.consentVersion;
|
|
121
|
+
return {
|
|
99
122
|
kind: "refused",
|
|
100
123
|
reason: meaning,
|
|
101
|
-
questions: body.questions === "any" ? "any" : "suggested"
|
|
124
|
+
questions: body.questions === "any" ? "any" : "suggested",
|
|
125
|
+
...typeof named === "string" && isAvatarVersion(named) ? { consentVersion: named } : {}
|
|
102
126
|
};
|
|
103
127
|
}
|
|
104
128
|
var CREATE_REFUSALS = /* @__PURE__ */ new Map([
|
|
@@ -845,6 +869,7 @@ var STRINGS = {
|
|
|
845
869
|
function avatarLiveStrings(locale) {
|
|
846
870
|
return STRINGS[locale] ?? EN;
|
|
847
871
|
}
|
|
872
|
+
var BROWSER_MIC_NOTICE_VERSION = "web-mic-2026-11-21-c";
|
|
848
873
|
var CONSENT = /* @__PURE__ */ new Map([
|
|
849
874
|
[
|
|
850
875
|
"web-consent-2026-11",
|
|
@@ -1022,6 +1047,72 @@ var CONSENT = /* @__PURE__ */ new Map([
|
|
|
1022
1047
|
followUps: true
|
|
1023
1048
|
}
|
|
1024
1049
|
}
|
|
1050
|
+
],
|
|
1051
|
+
[
|
|
1052
|
+
// Stage C (BOFF-9797, the operator's decision of 2026-11-05): the mic through the browser's own speech recognition.
|
|
1053
|
+
// A DRAFT for the operator's privacy review that no environment uses yet (global-speech-svc reserves the same two
|
|
1054
|
+
// versions, and pairs them with its AVATAR_WEB_MIC=browser): web-consent-2026-11-19-d with three changes only. The
|
|
1055
|
+
// intro warns against personal details said into the microphone and says a tap on it stops the answer being read;
|
|
1056
|
+
// the processing says which speech service turns the voice into text (the browser's own, under its own terms, with
|
|
1057
|
+
// the vendors as examples) and that a live session is told of a tap and of a recording, never of the words; the
|
|
1058
|
+
// storage says the camera is not used and that the voice never reaches us, nor the text until Send, in place of
|
|
1059
|
+
// "Your microphone and camera are not used.", which is true only with no mic. The notice files of record and their
|
|
1060
|
+
// digests are a gitops change of their own, made with the review.
|
|
1061
|
+
"web-consent-2026-11-21-c",
|
|
1062
|
+
{
|
|
1063
|
+
en: {
|
|
1064
|
+
title: "Live AI avatar (beta)",
|
|
1065
|
+
introProduct: null,
|
|
1066
|
+
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.",
|
|
1067
|
+
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.",
|
|
1068
|
+
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.",
|
|
1069
|
+
privacyLink: "How we handle this",
|
|
1070
|
+
botCheck: "turnstile",
|
|
1071
|
+
questions: "any",
|
|
1072
|
+
followUps: true,
|
|
1073
|
+
micNotice: BROWSER_MIC_NOTICE_VERSION
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
],
|
|
1077
|
+
[
|
|
1078
|
+
// The words of web-consent-2026-11-21-c without "When you start, Cloudflare checks for bots.", for a server that runs
|
|
1079
|
+
// no bot check. A DRAFT, like it.
|
|
1080
|
+
"web-consent-2026-11-22-c",
|
|
1081
|
+
{
|
|
1082
|
+
en: {
|
|
1083
|
+
title: "Live AI avatar (beta)",
|
|
1084
|
+
introProduct: null,
|
|
1085
|
+
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.",
|
|
1086
|
+
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.",
|
|
1087
|
+
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.",
|
|
1088
|
+
privacyLink: "How we handle this",
|
|
1089
|
+
botCheck: "off",
|
|
1090
|
+
questions: "any",
|
|
1091
|
+
followUps: true,
|
|
1092
|
+
micNotice: BROWSER_MIC_NOTICE_VERSION
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
],
|
|
1096
|
+
[
|
|
1097
|
+
// Stage C3 (BOFF-9797, the operator's final decision of 2026-11-06, "nothing at all"): the voice conversation with NO
|
|
1098
|
+
// notice in the Avatar tab. Typed questions, Stage B's three minutes, Stage D's answers after the first, the
|
|
1099
|
+
// browser's own recognizer (AVATAR_WEB_MIC=browser) with the server's pause, and no bot check (alpha and prod both
|
|
1100
|
+
// run it with AVATAR_WEB_BOT_CHECK=off). No words, so nothing to agree to: the server must record no consent for it
|
|
1101
|
+
// and hold no notice digest for it, and the create names it as a protocol token only. Speech processing and the
|
|
1102
|
+
// question sent by itself are the site's privacy policy's to describe. PROD waits for the privacy reviewer's
|
|
1103
|
+
// approval of this experience. (The
|
|
1104
|
+
// notice versions web-consent-2026-11-23-v and -24-v, which no environment ever ran, are retired with the dialog
|
|
1105
|
+
// that showed them: this SDK has no copy for them, so a server naming one gets the Avatar tab as it was.)
|
|
1106
|
+
"web-voice-2026-11-25-n",
|
|
1107
|
+
{
|
|
1108
|
+
en: {
|
|
1109
|
+
notice: "none",
|
|
1110
|
+
botCheck: "off",
|
|
1111
|
+
questions: "any",
|
|
1112
|
+
followUps: true,
|
|
1113
|
+
voice: { pauseMs: 2e3 }
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1025
1116
|
]
|
|
1026
1117
|
]);
|
|
1027
1118
|
function avatarConsentCopy(version, locale) {
|
|
@@ -1052,6 +1143,30 @@ function avatarMicStrings(locale) {
|
|
|
1052
1143
|
if (!isAvatarLiveLocaleReviewed(locale)) return null;
|
|
1053
1144
|
return MIC_STRINGS[locale] ?? null;
|
|
1054
1145
|
}
|
|
1146
|
+
var VOICE_EN = {
|
|
1147
|
+
talk: "Talk",
|
|
1148
|
+
talkInterrupt: "Stop the avatar and talk",
|
|
1149
|
+
stopListening: "Stop listening",
|
|
1150
|
+
start: "Start",
|
|
1151
|
+
cancelled: "The avatar didn't start. Press Start to try again.",
|
|
1152
|
+
starting: "Starting the microphone\u2026",
|
|
1153
|
+
listening: "Listening\u2026",
|
|
1154
|
+
pausing: "Still listening \u2014 take your time",
|
|
1155
|
+
sending: "Sending your question\u2026",
|
|
1156
|
+
again: "The mic listens again after the answer.",
|
|
1157
|
+
off: "Voice is off. Tap Talk to speak, or type your question.",
|
|
1158
|
+
inBox: "Voice is off. Your words are in the box: edit them or press Send.",
|
|
1159
|
+
atLimit: "The mic stopped at thirty seconds. Your words are in the box: press Send to ask.",
|
|
1160
|
+
noQuestion: "No question was heard. Tap Talk to try again, or type your question.",
|
|
1161
|
+
tapNext: "Tap Talk to ask your next question.",
|
|
1162
|
+
headline: "Tap Talk and ask. The avatar answers aloud.",
|
|
1163
|
+
placeholder: "Or type\u2026"
|
|
1164
|
+
};
|
|
1165
|
+
var VOICE_STRINGS = { en: VOICE_EN };
|
|
1166
|
+
function avatarVoiceStrings(locale) {
|
|
1167
|
+
if (!isAvatarLiveLocaleReviewed(locale)) return null;
|
|
1168
|
+
return VOICE_STRINGS[locale] ?? null;
|
|
1169
|
+
}
|
|
1055
1170
|
var AVATAR_MIC_NOTICE_VERSION = "web-mic-2026-11-05";
|
|
1056
1171
|
var MIC_NOTICE = /* @__PURE__ */ new Map([
|
|
1057
1172
|
[
|
|
@@ -1066,6 +1181,22 @@ var MIC_NOTICE = /* @__PURE__ */ new Map([
|
|
|
1066
1181
|
privacyLink: "How we handle this"
|
|
1067
1182
|
}
|
|
1068
1183
|
}
|
|
1184
|
+
],
|
|
1185
|
+
[
|
|
1186
|
+
// The browser's own speech recognition (Stage C): the browser's speech service, named by example where each is
|
|
1187
|
+
// true, under its own terms; nothing of the voice reaches us, nor the text before Send; a live session told only of
|
|
1188
|
+
// a tap and of a recording. No daily limits: nothing is counted on the server.
|
|
1189
|
+
BROWSER_MIC_NOTICE_VERSION,
|
|
1190
|
+
{
|
|
1191
|
+
en: {
|
|
1192
|
+
title: "Ask with your voice",
|
|
1193
|
+
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.",
|
|
1194
|
+
storage: "We never receive your voice, and your camera is not used.",
|
|
1195
|
+
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.",
|
|
1196
|
+
limits: "Each recording stops after thirty seconds at the latest.",
|
|
1197
|
+
privacyLink: "How we handle this"
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1069
1200
|
]
|
|
1070
1201
|
]);
|
|
1071
1202
|
function avatarMicNotice(version, locale) {
|
|
@@ -1076,10 +1207,12 @@ var PRIVACY_HREF = "/privacy#live-avatar";
|
|
|
1076
1207
|
var LIMIT_SECONDS = 30;
|
|
1077
1208
|
var TICK_MS = 250;
|
|
1078
1209
|
var IDLE = { name: "idle" };
|
|
1079
|
-
var
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
])
|
|
1210
|
+
var EN_IN = { lang: "en-IN", fallbackLang: "en-US" };
|
|
1211
|
+
var BROWSER_MIC_LANGUAGES = { en: EN_IN };
|
|
1212
|
+
var FOR_GOOD = {
|
|
1213
|
+
stream: /* @__PURE__ */ new Set(["unavailable", "limit"]),
|
|
1214
|
+
browser: /* @__PURE__ */ new Set(["unsupported"])
|
|
1215
|
+
};
|
|
1083
1216
|
var FAILURE_TEXT = {
|
|
1084
1217
|
denied: "micDenied",
|
|
1085
1218
|
unsupported: "micUnsupported",
|
|
@@ -1088,14 +1221,33 @@ var FAILURE_TEXT = {
|
|
|
1088
1221
|
busy: "micBusy",
|
|
1089
1222
|
failed: "micFailed"
|
|
1090
1223
|
};
|
|
1224
|
+
var loadedMic = null;
|
|
1091
1225
|
function loadAvatarMic() {
|
|
1092
|
-
return import('../avatar-mic/index.mjs').then((loaded) =>
|
|
1093
|
-
|
|
1094
|
-
|
|
1226
|
+
return import('../avatar-mic/index.mjs').then((loaded) => {
|
|
1227
|
+
const module = {
|
|
1228
|
+
createAvatarMic: loaded.createAvatarMic,
|
|
1229
|
+
createBrowserAvatarMic: loaded.createBrowserAvatarMic
|
|
1230
|
+
};
|
|
1231
|
+
loadedMic = module;
|
|
1232
|
+
return module;
|
|
1233
|
+
});
|
|
1095
1234
|
}
|
|
1096
1235
|
function canUseAvatarMic() {
|
|
1097
1236
|
return typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getUserMedia === "function" && typeof AudioContext !== "undefined" && typeof AudioWorkletNode !== "undefined" && typeof WebSocket !== "undefined";
|
|
1098
1237
|
}
|
|
1238
|
+
function canUseBrowserMic() {
|
|
1239
|
+
if (typeof window === "undefined" || typeof navigator === "undefined") {
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
if (window.isSecureContext !== true) return false;
|
|
1243
|
+
const recognizer = Reflect.get(window, "SpeechRecognition") ?? Reflect.get(window, "webkitSpeechRecognition");
|
|
1244
|
+
if (typeof recognizer !== "function") return false;
|
|
1245
|
+
const agent = navigator.userAgent ?? "";
|
|
1246
|
+
if (/\bFirefox\//.test(agent) || /\bFxiOS\//.test(agent)) return false;
|
|
1247
|
+
if (/\bOPR\//.test(agent)) return false;
|
|
1248
|
+
return !("brave" in navigator);
|
|
1249
|
+
}
|
|
1250
|
+
var REPLAY_PRIVATE = "rr-block rr-mask";
|
|
1099
1251
|
function clock(seconds) {
|
|
1100
1252
|
const whole = Math.max(0, Math.floor(seconds));
|
|
1101
1253
|
return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, "0")}`;
|
|
@@ -1106,47 +1258,110 @@ function withHeardText(current, heard, maxLength) {
|
|
|
1106
1258
|
const joined = current.trim() === "" ? text : `${current.replace(/\s+$/, "")} ${text}`;
|
|
1107
1259
|
return joined.slice(0, Math.max(0, maxLength));
|
|
1108
1260
|
}
|
|
1261
|
+
function noticeOf(source) {
|
|
1262
|
+
return source.kind === "stream" ? AVATAR_MIC_NOTICE_VERSION : source.notice;
|
|
1263
|
+
}
|
|
1264
|
+
function micKeyOf(source, locale) {
|
|
1265
|
+
if (source.kind === "stream") return `stream ${source.base}`;
|
|
1266
|
+
const lang = BROWSER_MIC_LANGUAGES[locale]?.lang ?? "";
|
|
1267
|
+
return source.pauseMs === void 0 ? `browser ${lang}` : `browser ${lang} pause ${source.pauseMs}`;
|
|
1268
|
+
}
|
|
1269
|
+
function underWay(state) {
|
|
1270
|
+
return state.name === "starting" || state.name === "listening" || state.name === "finishing";
|
|
1271
|
+
}
|
|
1109
1272
|
function useAvatarMic({
|
|
1110
1273
|
enabled,
|
|
1111
1274
|
locale,
|
|
1112
|
-
|
|
1113
|
-
offer,
|
|
1114
|
-
refreshOffer,
|
|
1275
|
+
source,
|
|
1115
1276
|
onText,
|
|
1116
|
-
onStart
|
|
1277
|
+
onStart,
|
|
1278
|
+
onTurn,
|
|
1279
|
+
onPress,
|
|
1280
|
+
echoOf,
|
|
1281
|
+
interruptible = false,
|
|
1282
|
+
note = null
|
|
1117
1283
|
}) {
|
|
1118
1284
|
const S = avatarMicStrings(locale);
|
|
1119
|
-
const
|
|
1285
|
+
const noticeVersion = source === null ? null : noticeOf(source);
|
|
1286
|
+
const kind = source?.kind ?? null;
|
|
1287
|
+
const micKey = source === null ? null : micKeyOf(source, locale);
|
|
1288
|
+
const paused = source?.kind === "browser" && source.pauseMs !== void 0;
|
|
1289
|
+
const conversation = paused && noticeVersion !== null && avatarConsentCopy(noticeVersion, locale)?.notice === "none";
|
|
1290
|
+
const copy = noticeVersion === null || paused ? null : avatarMicNotice(noticeVersion, locale);
|
|
1291
|
+
const V = conversation ? avatarVoiceStrings(locale) : null;
|
|
1120
1292
|
const uid = useId();
|
|
1121
1293
|
const noticeTitleId = `${uid}-mic-notice`;
|
|
1122
|
-
const [withdrawn, setWithdrawn] = useState(
|
|
1123
|
-
const [supported, setSupported] = useState(false);
|
|
1124
|
-
useEffect(
|
|
1125
|
-
|
|
1126
|
-
|
|
1294
|
+
const [withdrawn, setWithdrawn] = useState({});
|
|
1295
|
+
const [supported, setSupported] = useState({ stream: false, browser: false });
|
|
1296
|
+
useEffect(
|
|
1297
|
+
() => setSupported({ stream: canUseAvatarMic(), browser: canUseBrowserMic() }),
|
|
1298
|
+
[]
|
|
1299
|
+
);
|
|
1300
|
+
const usable = source === null ? false : source.kind === "stream" ? supported.stream : supported.browser && BROWSER_MIC_LANGUAGES[locale] !== void 0;
|
|
1301
|
+
const showable = kind !== null && usable && S !== null && (paused ? conversation && V !== null : copy !== null);
|
|
1302
|
+
const possible = enabled && showable;
|
|
1303
|
+
const withdrawnReason = kind === null ? null : withdrawn[kind] ?? null;
|
|
1304
|
+
const offered = possible && withdrawnReason === null;
|
|
1127
1305
|
const [state, setState] = useState(IDLE);
|
|
1128
1306
|
const [noticeOpen, setNoticeOpen] = useState(false);
|
|
1129
|
-
const agreedRef = useRef(
|
|
1130
|
-
const [loading2,
|
|
1307
|
+
const agreedRef = useRef(null);
|
|
1308
|
+
const [loading2, setLoadingState] = useState(false);
|
|
1309
|
+
const loadingRef = useRef(false);
|
|
1310
|
+
const setLoading = useCallback((value) => {
|
|
1311
|
+
loadingRef.current = value;
|
|
1312
|
+
setLoadingState(value);
|
|
1313
|
+
}, []);
|
|
1131
1314
|
const [loadFailed, setLoadFailed] = useState(false);
|
|
1132
1315
|
const [now, setNow] = useState(() => Date.now());
|
|
1133
|
-
const micRef = useRef(
|
|
1316
|
+
const micRef = useRef(
|
|
1317
|
+
null
|
|
1318
|
+
);
|
|
1134
1319
|
const seqRef = useRef(0);
|
|
1135
1320
|
const buttonRef = useRef(null);
|
|
1136
1321
|
const allowRef = useRef(null);
|
|
1137
|
-
const
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1322
|
+
const usedRef = useRef(null);
|
|
1323
|
+
const gestureRef = useRef(false);
|
|
1324
|
+
const [, setUsedTick] = useState(0);
|
|
1325
|
+
const latest = useRef({
|
|
1326
|
+
source,
|
|
1327
|
+
locale,
|
|
1328
|
+
noticeVersion,
|
|
1329
|
+
onText,
|
|
1330
|
+
onStart,
|
|
1331
|
+
onTurn,
|
|
1332
|
+
onPress,
|
|
1333
|
+
echoOf,
|
|
1334
|
+
conversation,
|
|
1335
|
+
offered
|
|
1336
|
+
});
|
|
1337
|
+
latest.current = {
|
|
1338
|
+
source,
|
|
1339
|
+
locale,
|
|
1340
|
+
noticeVersion,
|
|
1341
|
+
onText,
|
|
1342
|
+
onStart,
|
|
1343
|
+
onTurn,
|
|
1344
|
+
onPress,
|
|
1345
|
+
echoOf,
|
|
1346
|
+
conversation,
|
|
1347
|
+
offered
|
|
1348
|
+
};
|
|
1140
1349
|
const cancel = useCallback(() => {
|
|
1141
1350
|
seqRef.current += 1;
|
|
1142
1351
|
setLoading(false);
|
|
1143
1352
|
setLoadFailed(false);
|
|
1144
1353
|
setNoticeOpen(false);
|
|
1145
1354
|
micRef.current?.mic.cancel();
|
|
1146
|
-
}, []);
|
|
1355
|
+
}, [setLoading]);
|
|
1147
1356
|
useEffect(() => {
|
|
1148
1357
|
if (!offered) cancel();
|
|
1149
1358
|
}, [offered, cancel]);
|
|
1359
|
+
const keyRef = useRef(micKey);
|
|
1360
|
+
useEffect(() => {
|
|
1361
|
+
if (keyRef.current === micKey) return;
|
|
1362
|
+
keyRef.current = micKey;
|
|
1363
|
+
cancel();
|
|
1364
|
+
}, [micKey, cancel]);
|
|
1150
1365
|
useEffect(
|
|
1151
1366
|
() => () => {
|
|
1152
1367
|
seqRef.current += 1;
|
|
@@ -1154,43 +1369,134 @@ function useAvatarMic({
|
|
|
1154
1369
|
},
|
|
1155
1370
|
[]
|
|
1156
1371
|
);
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1372
|
+
const micOf = useCallback(
|
|
1373
|
+
(loaded, wanted) => {
|
|
1374
|
+
const key = micKeyOf(wanted, latest.current.locale);
|
|
1375
|
+
const held = micRef.current;
|
|
1376
|
+
if (held !== null && held.key === key) return held.mic;
|
|
1377
|
+
held?.mic.cancel();
|
|
1378
|
+
let mic;
|
|
1379
|
+
if (wanted.kind === "stream") {
|
|
1380
|
+
const stream = () => {
|
|
1381
|
+
const current = latest.current.source;
|
|
1382
|
+
return current?.kind === "stream" ? current : null;
|
|
1383
|
+
};
|
|
1384
|
+
mic = loaded.createAvatarMic({
|
|
1385
|
+
base: wanted.base,
|
|
1386
|
+
notice: AVATAR_MIC_NOTICE_VERSION,
|
|
1387
|
+
offer: () => stream()?.offer ?? null,
|
|
1388
|
+
refreshOffer: () => stream()?.refreshOffer() ?? Promise.resolve(null),
|
|
1389
|
+
onState: setState
|
|
1390
|
+
});
|
|
1391
|
+
} else {
|
|
1392
|
+
const language = BROWSER_MIC_LANGUAGES[latest.current.locale] ?? EN_IN;
|
|
1393
|
+
const { pauseMs } = wanted;
|
|
1394
|
+
mic = loaded.createBrowserAvatarMic({
|
|
1395
|
+
lang: language.lang,
|
|
1396
|
+
fallbackLang: language.fallbackLang,
|
|
1397
|
+
maxPressMs: LIMIT_SECONDS * 1e3,
|
|
1398
|
+
onState: setState,
|
|
1399
|
+
// The voice conversation: each press ends itself on the server's pause. The avatar's own words heard back
|
|
1400
|
+
// are looked for only in a press the conversation started by itself: one the visitor's own tap started runs
|
|
1401
|
+
// while the avatar is quiet, or muted here by that tap (a Talk while it speaks interrupts it), so its words are
|
|
1402
|
+
// the visitor's, whatever of the answer they repeat ("Delivery is free across Tamil Nadu?").
|
|
1403
|
+
...pauseMs === void 0 ? {} : {
|
|
1404
|
+
endpoint: {
|
|
1405
|
+
pauseMs,
|
|
1406
|
+
echoOf: () => gestureRef.current ? null : latest.current.echoOf?.() ?? null
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
micRef.current = { key, kind: wanted.kind, mic };
|
|
1412
|
+
return mic;
|
|
1413
|
+
},
|
|
1414
|
+
[]
|
|
1415
|
+
);
|
|
1416
|
+
const press = useCallback(
|
|
1417
|
+
(gesture) => {
|
|
1418
|
+
const wanted = latest.current.source;
|
|
1419
|
+
if (wanted === null) return;
|
|
1420
|
+
seqRef.current += 1;
|
|
1421
|
+
const seq = seqRef.current;
|
|
1422
|
+
setLoadFailed(false);
|
|
1423
|
+
gestureRef.current = gesture;
|
|
1424
|
+
if (latest.current.conversation) latest.current.onPress?.(gesture);
|
|
1425
|
+
const held = micRef.current;
|
|
1426
|
+
const ready = wanted.kind !== "browser" ? null : held !== null && held.key === micKeyOf(wanted, latest.current.locale) ? held.mic : loadedMic !== null ? micOf(loadedMic, wanted) : null;
|
|
1427
|
+
if (ready !== null) {
|
|
1187
1428
|
setLoading(false);
|
|
1188
|
-
|
|
1429
|
+
ready.start();
|
|
1430
|
+
return;
|
|
1189
1431
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1432
|
+
setLoading(true);
|
|
1433
|
+
const arriving = held !== null && held.key === micKeyOf(wanted, latest.current.locale) ? Promise.resolve(held.mic) : loadAvatarMic().then((loaded) => micOf(loaded, wanted));
|
|
1434
|
+
void arriving.then(
|
|
1435
|
+
(mic) => {
|
|
1436
|
+
if (seqRef.current !== seq) return;
|
|
1437
|
+
setLoading(false);
|
|
1438
|
+
mic.start();
|
|
1439
|
+
},
|
|
1440
|
+
() => {
|
|
1441
|
+
if (seqRef.current !== seq) return;
|
|
1442
|
+
setLoading(false);
|
|
1443
|
+
setLoadFailed(true);
|
|
1444
|
+
if (latest.current.conversation) {
|
|
1445
|
+
latest.current.onTurn?.({
|
|
1446
|
+
by: "failed",
|
|
1447
|
+
text: "",
|
|
1448
|
+
echo: false,
|
|
1449
|
+
gesture,
|
|
1450
|
+
failure: "failed"
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
);
|
|
1455
|
+
},
|
|
1456
|
+
[micOf, setLoading]
|
|
1457
|
+
);
|
|
1192
1458
|
const recording = loading2 || state.name === "starting" || state.name === "listening";
|
|
1459
|
+
const finishing = kind === "browser" && state.name === "finishing";
|
|
1460
|
+
const preload = useCallback(() => {
|
|
1461
|
+
const seq = seqRef.current;
|
|
1462
|
+
void loadAvatarMic().then(
|
|
1463
|
+
(loaded) => {
|
|
1464
|
+
const wanted = latest.current.source;
|
|
1465
|
+
if (seqRef.current === seq && wanted?.kind === "browser") {
|
|
1466
|
+
micOf(loaded, wanted);
|
|
1467
|
+
}
|
|
1468
|
+
},
|
|
1469
|
+
() => void 0
|
|
1470
|
+
);
|
|
1471
|
+
}, [micOf]);
|
|
1472
|
+
const pressing = useCallback(() => {
|
|
1473
|
+
const held = micRef.current?.mic.state() ?? IDLE;
|
|
1474
|
+
return loadingRef.current || underWay(held) || (held.name === "done" || held.name === "failed") && held !== usedRef.current;
|
|
1475
|
+
}, []);
|
|
1476
|
+
const stopListening = useCallback(() => {
|
|
1477
|
+
const held = micRef.current?.mic.state() ?? IDLE;
|
|
1478
|
+
let text = "";
|
|
1479
|
+
if (held.name === "listening" || held.name === "finishing") {
|
|
1480
|
+
text = held.text;
|
|
1481
|
+
} else if ((held.name === "done" || held.name === "failed") && held !== usedRef.current) {
|
|
1482
|
+
usedRef.current = held;
|
|
1483
|
+
text = held.text;
|
|
1484
|
+
}
|
|
1485
|
+
const gesture = gestureRef.current;
|
|
1486
|
+
cancel();
|
|
1487
|
+
latest.current.onTurn?.({ by: "tap", text, echo: false, gesture });
|
|
1488
|
+
}, [cancel]);
|
|
1193
1489
|
const tap = useCallback(() => {
|
|
1490
|
+
if (latest.current.conversation) {
|
|
1491
|
+
if (pressing()) {
|
|
1492
|
+
stopListening();
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
latest.current.onStart?.();
|
|
1496
|
+
agreedRef.current = latest.current.noticeVersion;
|
|
1497
|
+
press(true);
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1194
1500
|
if (state.name === "listening" || state.name === "starting") {
|
|
1195
1501
|
micRef.current?.mic.stop();
|
|
1196
1502
|
return;
|
|
@@ -1199,24 +1505,60 @@ function useAvatarMic({
|
|
|
1199
1505
|
cancel();
|
|
1200
1506
|
return;
|
|
1201
1507
|
}
|
|
1508
|
+
if (finishing) return;
|
|
1202
1509
|
latest.current.onStart?.();
|
|
1203
|
-
if (
|
|
1510
|
+
if (agreedRef.current !== latest.current.noticeVersion) {
|
|
1204
1511
|
setNoticeOpen(true);
|
|
1205
|
-
|
|
1512
|
+
preload();
|
|
1206
1513
|
return;
|
|
1207
1514
|
}
|
|
1208
|
-
press();
|
|
1209
|
-
}, [
|
|
1515
|
+
press(true);
|
|
1516
|
+
}, [
|
|
1517
|
+
cancel,
|
|
1518
|
+
finishing,
|
|
1519
|
+
loading2,
|
|
1520
|
+
preload,
|
|
1521
|
+
press,
|
|
1522
|
+
pressing,
|
|
1523
|
+
state.name,
|
|
1524
|
+
stopListening
|
|
1525
|
+
]);
|
|
1210
1526
|
const allow = useCallback(() => {
|
|
1211
|
-
agreedRef.current =
|
|
1527
|
+
agreedRef.current = latest.current.noticeVersion;
|
|
1212
1528
|
setNoticeOpen(false);
|
|
1213
1529
|
buttonRef.current?.focus();
|
|
1214
|
-
press();
|
|
1530
|
+
press(true);
|
|
1215
1531
|
}, [press]);
|
|
1216
1532
|
const notNow = useCallback(() => {
|
|
1217
1533
|
setNoticeOpen(false);
|
|
1218
1534
|
buttonRef.current?.focus();
|
|
1219
1535
|
}, []);
|
|
1536
|
+
const preparing = conversation && offered;
|
|
1537
|
+
useEffect(() => {
|
|
1538
|
+
if (preparing) preload();
|
|
1539
|
+
}, [preparing, preload]);
|
|
1540
|
+
const listen = useCallback(() => {
|
|
1541
|
+
if (!latest.current.conversation || !latest.current.offered || // Never before the visitor's own Talk on this page, under this version.
|
|
1542
|
+
agreedRef.current !== latest.current.noticeVersion || // A press under way, or one that has ended and whose outcome has not gone to the view yet (still that press: a
|
|
1543
|
+
// question that ended on its pause is about to be sent): the mic must not open over it.
|
|
1544
|
+
pressing()) {
|
|
1545
|
+
return false;
|
|
1546
|
+
}
|
|
1547
|
+
press(false);
|
|
1548
|
+
return true;
|
|
1549
|
+
}, [press, pressing]);
|
|
1550
|
+
const heard = useCallback(() => {
|
|
1551
|
+
const held = micRef.current?.mic.state() ?? IDLE;
|
|
1552
|
+
return held.name === "listening" || held.name === "finishing" ? held.text : "";
|
|
1553
|
+
}, []);
|
|
1554
|
+
const dropLoopPress = useCallback(() => {
|
|
1555
|
+
const held = micRef.current?.mic.state() ?? IDLE;
|
|
1556
|
+
if (!latest.current.conversation || gestureRef.current || !loadingRef.current && !underWay(held)) {
|
|
1557
|
+
return false;
|
|
1558
|
+
}
|
|
1559
|
+
cancel();
|
|
1560
|
+
return true;
|
|
1561
|
+
}, [cancel]);
|
|
1220
1562
|
useEffect(() => {
|
|
1221
1563
|
if (noticeOpen) allowRef.current?.focus();
|
|
1222
1564
|
}, [noticeOpen]);
|
|
@@ -1228,24 +1570,71 @@ function useAvatarMic({
|
|
|
1228
1570
|
return () => clearInterval(timer);
|
|
1229
1571
|
}, [listeningSince]);
|
|
1230
1572
|
useEffect(() => {
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1573
|
+
const voice = latest.current.conversation;
|
|
1574
|
+
if (state.name === "done" && state !== usedRef.current) {
|
|
1575
|
+
usedRef.current = state;
|
|
1576
|
+
if (voice) {
|
|
1577
|
+
latest.current.onTurn?.({
|
|
1578
|
+
by: state.by ?? (state.limit ? "limit" : "tap"),
|
|
1579
|
+
text: state.text,
|
|
1580
|
+
echo: state.echo === true,
|
|
1581
|
+
gesture: gestureRef.current
|
|
1582
|
+
});
|
|
1583
|
+
setUsedTick((tick) => tick + 1);
|
|
1584
|
+
} else if (state.text !== "") {
|
|
1585
|
+
latest.current.onText(state.text);
|
|
1586
|
+
}
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
const pressed = micRef.current?.kind;
|
|
1590
|
+
if (state.name === "failed" && voice && state !== usedRef.current) {
|
|
1591
|
+
usedRef.current = state;
|
|
1592
|
+
const gesture = gestureRef.current;
|
|
1593
|
+
if (gesture && pressed !== void 0 && FOR_GOOD[pressed].has(state.reason)) {
|
|
1594
|
+
const reason = state.reason;
|
|
1595
|
+
setWithdrawn((held) => ({ ...held, [pressed]: reason }));
|
|
1596
|
+
}
|
|
1597
|
+
latest.current.onTurn?.({
|
|
1598
|
+
by: "failed",
|
|
1599
|
+
text: state.text,
|
|
1600
|
+
echo: false,
|
|
1601
|
+
gesture,
|
|
1602
|
+
failure: state.reason
|
|
1603
|
+
});
|
|
1604
|
+
setUsedTick((tick) => tick + 1);
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
if (state.name === "failed" && !voice && pressed !== void 0 && FOR_GOOD[pressed].has(state.reason)) {
|
|
1608
|
+
const reason = state.reason;
|
|
1609
|
+
setWithdrawn((held) => ({ ...held, [pressed]: reason }));
|
|
1236
1610
|
}
|
|
1237
1611
|
}, [state]);
|
|
1612
|
+
const outcomeDue = (state.name === "done" || state.name === "failed") && state !== usedRef.current;
|
|
1613
|
+
const busy = loading2 || state.name === "starting" || state.name === "listening" || state.name === "finishing" || outcomeDue;
|
|
1614
|
+
const voiceControls = {
|
|
1615
|
+
mode: conversation ? "conversation" : "editable",
|
|
1616
|
+
speaking: conversation && offered && state.name === "listening" && state.genuine === true,
|
|
1617
|
+
busy,
|
|
1618
|
+
pressing,
|
|
1619
|
+
listen,
|
|
1620
|
+
heard,
|
|
1621
|
+
dropLoopPress
|
|
1622
|
+
};
|
|
1623
|
+
if (conversation && showable && V !== null && S !== null) {
|
|
1624
|
+
return conversationControls(V, S);
|
|
1625
|
+
}
|
|
1238
1626
|
if (!possible || S === null || copy === null) {
|
|
1239
1627
|
return {
|
|
1240
1628
|
button: null,
|
|
1241
1629
|
notice: null,
|
|
1242
1630
|
lines: null,
|
|
1243
1631
|
cancel,
|
|
1244
|
-
recording: false
|
|
1632
|
+
recording: false,
|
|
1633
|
+
...voiceControls
|
|
1245
1634
|
};
|
|
1246
1635
|
}
|
|
1247
|
-
const status =
|
|
1248
|
-
const
|
|
1636
|
+
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]] : "";
|
|
1637
|
+
const heardText = offered && (state.name === "listening" || state.name === "finishing") ? state.text : "";
|
|
1249
1638
|
const button = !offered ? null : /* @__PURE__ */ jsx(
|
|
1250
1639
|
"button",
|
|
1251
1640
|
{
|
|
@@ -1253,73 +1642,25 @@ function useAvatarMic({
|
|
|
1253
1642
|
type: "button",
|
|
1254
1643
|
"aria-label": recording ? S.micStop : S.micStart,
|
|
1255
1644
|
"aria-pressed": recording,
|
|
1645
|
+
"aria-disabled": finishing ? true : void 0,
|
|
1256
1646
|
title: recording ? S.micStop : S.micStart,
|
|
1257
1647
|
"data-boff-explore": "avatar-live-mic",
|
|
1258
1648
|
"data-mic": loading2 ? "starting" : state.name,
|
|
1649
|
+
"data-mic-source": kind ?? void 0,
|
|
1259
1650
|
className: cn(
|
|
1260
1651
|
"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"
|
|
1652
|
+
recording ? "border-primary bg-primary text-primary-foreground" : "border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
|
|
1653
|
+
finishing && "cursor-wait opacity-50"
|
|
1262
1654
|
),
|
|
1263
1655
|
onClick: tap,
|
|
1264
1656
|
children: recording ? /* @__PURE__ */ jsx(Square, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx(Mic, { "aria-hidden": "true", className: "h-4 w-4" })
|
|
1265
1657
|
}
|
|
1266
1658
|
);
|
|
1267
|
-
const notice = offered && noticeOpen ?
|
|
1268
|
-
"section",
|
|
1269
|
-
{
|
|
1270
|
-
"aria-labelledby": noticeTitleId,
|
|
1271
|
-
"data-boff-explore": "avatar-live-mic-notice",
|
|
1272
|
-
className: "w-full rounded-lg border border-border p-3 text-start",
|
|
1273
|
-
children: [
|
|
1274
|
-
/* @__PURE__ */ jsx("h3", { id: noticeTitleId, className: "text-sm font-semibold", children: copy.title }),
|
|
1275
|
-
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: copy.processing }),
|
|
1276
|
-
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: copy.storage }),
|
|
1277
|
-
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: copy.text }),
|
|
1278
|
-
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: copy.limits }),
|
|
1279
|
-
/* @__PURE__ */ jsx(
|
|
1280
|
-
"a",
|
|
1281
|
-
{
|
|
1282
|
-
href: PRIVACY_HREF,
|
|
1283
|
-
target: "_blank",
|
|
1284
|
-
rel: "noopener noreferrer",
|
|
1285
|
-
"data-boff-explore": "avatar-live-mic-privacy",
|
|
1286
|
-
className: "mt-1 inline-block text-xs font-medium text-primary underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
1287
|
-
children: copy.privacyLink
|
|
1288
|
-
}
|
|
1289
|
-
),
|
|
1290
|
-
/* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap gap-2", children: [
|
|
1291
|
-
/* @__PURE__ */ jsx(
|
|
1292
|
-
"button",
|
|
1293
|
-
{
|
|
1294
|
-
ref: allowRef,
|
|
1295
|
-
type: "button",
|
|
1296
|
-
"data-boff-explore": "avatar-live-mic-allow",
|
|
1297
|
-
className: "inline-flex min-h-11 items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 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",
|
|
1298
|
-
onClick: allow,
|
|
1299
|
-
children: S.micAllow
|
|
1300
|
-
}
|
|
1301
|
-
),
|
|
1302
|
-
/* @__PURE__ */ jsx(
|
|
1303
|
-
"button",
|
|
1304
|
-
{
|
|
1305
|
-
type: "button",
|
|
1306
|
-
"data-boff-explore": "avatar-live-mic-not-now",
|
|
1307
|
-
className: "inline-flex min-h-11 items-center justify-center rounded-md border border-border bg-background px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground 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",
|
|
1308
|
-
onClick: notNow,
|
|
1309
|
-
children: S.micNotNow
|
|
1310
|
-
}
|
|
1311
|
-
)
|
|
1312
|
-
] })
|
|
1313
|
-
]
|
|
1314
|
-
}
|
|
1315
|
-
) : null;
|
|
1659
|
+
const notice = offered && noticeOpen ? noticeCard(copy, S.micAllow, S.micNotNow) : null;
|
|
1316
1660
|
const listening = offered && state.name === "listening";
|
|
1317
1661
|
const lines = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1318
1662
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
|
|
1319
|
-
listening ?
|
|
1320
|
-
/* @__PURE__ */ jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75 motion-reduce:animate-none" }),
|
|
1321
|
-
/* @__PURE__ */ jsx("span", { className: "relative inline-flex h-2 w-2 rounded-full bg-primary" })
|
|
1322
|
-
] }) : null,
|
|
1663
|
+
listening ? listeningDot : null,
|
|
1323
1664
|
/* @__PURE__ */ jsx("p", { role: "status", "data-boff-explore": "avatar-live-mic-status", children: status }),
|
|
1324
1665
|
listening ? /* @__PURE__ */ jsx(
|
|
1325
1666
|
"span",
|
|
@@ -1336,24 +1677,142 @@ function useAvatarMic({
|
|
|
1336
1677
|
}
|
|
1337
1678
|
) : null
|
|
1338
1679
|
] }),
|
|
1339
|
-
|
|
1340
|
-
"p",
|
|
1341
|
-
{
|
|
1342
|
-
"data-boff-explore": "avatar-live-mic-heard",
|
|
1343
|
-
dir: "auto",
|
|
1344
|
-
className: "line-clamp-3 break-words text-sm italic text-muted-foreground",
|
|
1345
|
-
children: heard
|
|
1346
|
-
}
|
|
1347
|
-
)
|
|
1680
|
+
heardLine(heardText)
|
|
1348
1681
|
] });
|
|
1349
1682
|
return {
|
|
1350
1683
|
button,
|
|
1351
1684
|
notice,
|
|
1352
1685
|
lines,
|
|
1353
1686
|
cancel,
|
|
1354
|
-
recording: offered && state.name === "listening"
|
|
1687
|
+
recording: offered && state.name === "listening",
|
|
1688
|
+
...voiceControls
|
|
1355
1689
|
};
|
|
1690
|
+
function noticeCard(words, allowLabel, notNowLabel) {
|
|
1691
|
+
return /* @__PURE__ */ jsxs(
|
|
1692
|
+
"section",
|
|
1693
|
+
{
|
|
1694
|
+
"aria-labelledby": noticeTitleId,
|
|
1695
|
+
"data-boff-explore": "avatar-live-mic-notice",
|
|
1696
|
+
className: "w-full rounded-lg border border-border p-3 text-start",
|
|
1697
|
+
children: [
|
|
1698
|
+
/* @__PURE__ */ jsx("h3", { id: noticeTitleId, className: "text-sm font-semibold", children: words.title }),
|
|
1699
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: words.processing }),
|
|
1700
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: words.storage }),
|
|
1701
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: words.text }),
|
|
1702
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: words.limits }),
|
|
1703
|
+
/* @__PURE__ */ jsx(
|
|
1704
|
+
"a",
|
|
1705
|
+
{
|
|
1706
|
+
href: PRIVACY_HREF,
|
|
1707
|
+
target: "_blank",
|
|
1708
|
+
rel: "noopener noreferrer",
|
|
1709
|
+
"data-boff-explore": "avatar-live-mic-privacy",
|
|
1710
|
+
className: "mt-1 inline-block text-xs font-medium text-primary underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
1711
|
+
children: words.privacyLink
|
|
1712
|
+
}
|
|
1713
|
+
),
|
|
1714
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap gap-2", children: [
|
|
1715
|
+
/* @__PURE__ */ jsx(
|
|
1716
|
+
"button",
|
|
1717
|
+
{
|
|
1718
|
+
ref: allowRef,
|
|
1719
|
+
type: "button",
|
|
1720
|
+
"data-boff-explore": "avatar-live-mic-allow",
|
|
1721
|
+
className: "inline-flex min-h-11 items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 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",
|
|
1722
|
+
onClick: allow,
|
|
1723
|
+
children: allowLabel
|
|
1724
|
+
}
|
|
1725
|
+
),
|
|
1726
|
+
/* @__PURE__ */ jsx(
|
|
1727
|
+
"button",
|
|
1728
|
+
{
|
|
1729
|
+
type: "button",
|
|
1730
|
+
"data-boff-explore": "avatar-live-mic-not-now",
|
|
1731
|
+
className: "inline-flex min-h-11 items-center justify-center rounded-md border border-border bg-background px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground 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",
|
|
1732
|
+
onClick: notNow,
|
|
1733
|
+
children: notNowLabel
|
|
1734
|
+
}
|
|
1735
|
+
)
|
|
1736
|
+
] })
|
|
1737
|
+
]
|
|
1738
|
+
}
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
function heardLine(text) {
|
|
1742
|
+
return text === "" ? null : /* @__PURE__ */ jsx(
|
|
1743
|
+
"p",
|
|
1744
|
+
{
|
|
1745
|
+
"data-boff-explore": "avatar-live-mic-heard",
|
|
1746
|
+
dir: "auto",
|
|
1747
|
+
className: cn(
|
|
1748
|
+
// rrweb's default block and mask classes: a site's session replay records none of what is heard.
|
|
1749
|
+
REPLAY_PRIVATE,
|
|
1750
|
+
"line-clamp-3 break-words text-sm italic text-muted-foreground"
|
|
1751
|
+
),
|
|
1752
|
+
children: text
|
|
1753
|
+
}
|
|
1754
|
+
);
|
|
1755
|
+
}
|
|
1756
|
+
function conversationControls(V2, S2) {
|
|
1757
|
+
const going = loading2 || state.name === "starting" || state.name === "listening" || state.name === "finishing";
|
|
1758
|
+
const label = busy ? V2.stopListening : V2.talk;
|
|
1759
|
+
const name = busy ? V2.stopListening : interruptible ? V2.talkInterrupt : V2.talk;
|
|
1760
|
+
const talk = !offered ? null : /* @__PURE__ */ jsxs(
|
|
1761
|
+
"button",
|
|
1762
|
+
{
|
|
1763
|
+
ref: buttonRef,
|
|
1764
|
+
type: "button",
|
|
1765
|
+
"aria-label": name,
|
|
1766
|
+
title: name,
|
|
1767
|
+
"data-boff-explore": "avatar-live-mic",
|
|
1768
|
+
"data-mic": loading2 ? "starting" : state.name,
|
|
1769
|
+
"data-mic-source": "browser",
|
|
1770
|
+
"data-mic-mode": "conversation",
|
|
1771
|
+
className: cn(
|
|
1772
|
+
"inline-flex h-11 shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md border px-3 text-sm font-medium 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",
|
|
1773
|
+
busy ? "border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground" : "border-primary bg-primary text-primary-foreground hover:opacity-90"
|
|
1774
|
+
),
|
|
1775
|
+
onClick: tap,
|
|
1776
|
+
children: [
|
|
1777
|
+
busy ? /* @__PURE__ */ jsx(Square, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx(Mic, { "aria-hidden": "true", className: "h-4 w-4" }),
|
|
1778
|
+
/* @__PURE__ */ jsx("span", { children: label })
|
|
1779
|
+
]
|
|
1780
|
+
}
|
|
1781
|
+
);
|
|
1782
|
+
const pressLine = !offered ? null : loading2 || state.name === "starting" ? V2.starting : state.name === "listening" ? state.pausing === true ? V2.pausing : V2.listening : state.name === "finishing" ? state.limit ? S2.micFinishing : V2.sending : null;
|
|
1783
|
+
const noteLine = note === null ? "" : note.kind === "line" ? V2[note.line] : S2[FAILURE_TEXT[note.reason]];
|
|
1784
|
+
const line = withdrawnReason !== null ? S2[FAILURE_TEXT[withdrawnReason]] : pressLine ?? noteLine;
|
|
1785
|
+
const heardNow = offered && (state.name === "listening" || state.name === "finishing") ? state.text : "";
|
|
1786
|
+
const listeningNow = offered && state.name === "listening";
|
|
1787
|
+
const shown = possible || line !== "";
|
|
1788
|
+
return {
|
|
1789
|
+
button: talk,
|
|
1790
|
+
notice: null,
|
|
1791
|
+
lines: !shown ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1792
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
|
|
1793
|
+
listeningNow ? listeningDot : null,
|
|
1794
|
+
/* @__PURE__ */ jsx(
|
|
1795
|
+
"p",
|
|
1796
|
+
{
|
|
1797
|
+
role: "status",
|
|
1798
|
+
"data-boff-explore": "avatar-live-mic-status",
|
|
1799
|
+
"data-voice": going ? "pressing" : note?.kind ?? "idle",
|
|
1800
|
+
children: line
|
|
1801
|
+
}
|
|
1802
|
+
)
|
|
1803
|
+
] }),
|
|
1804
|
+
heardLine(heardNow)
|
|
1805
|
+
] }),
|
|
1806
|
+
cancel,
|
|
1807
|
+
recording: offered && state.name === "listening",
|
|
1808
|
+
...voiceControls
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1356
1811
|
}
|
|
1812
|
+
var listeningDot = /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: "relative flex h-2 w-2 shrink-0", children: [
|
|
1813
|
+
/* @__PURE__ */ jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75 motion-reduce:animate-none" }),
|
|
1814
|
+
/* @__PURE__ */ jsx("span", { className: "relative inline-flex h-2 w-2 rounded-full bg-primary" })
|
|
1815
|
+
] });
|
|
1357
1816
|
|
|
1358
1817
|
// src/components/explore-avatar-turnstile.ts
|
|
1359
1818
|
var SCRIPT_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
|
|
@@ -1478,9 +1937,13 @@ var PLAYOUT_TAIL_MS = 5e3;
|
|
|
1478
1937
|
var QUIET_PLAYED_OUT_MS = 2500;
|
|
1479
1938
|
var OWED_VOICE_WAIT_MS = 15e3;
|
|
1480
1939
|
var LISTENING_MARK_MS = 1e4;
|
|
1940
|
+
var LISTENING_MARK_SPACING_MS = 5e3;
|
|
1481
1941
|
var BUSY_RETRY_MS = 2e3;
|
|
1482
1942
|
var BUSY_RETRY_MAX_MS = 8e3;
|
|
1483
1943
|
var MAX_BUSY_RETRIES = 24;
|
|
1944
|
+
var PAUSE_NOTICE_TOLERANCE_MS = 500;
|
|
1945
|
+
var MAX_ECHO_DISCARDS = 2;
|
|
1946
|
+
var LISTEN_AGAIN_MIN_LEFT_MS = 2e4;
|
|
1484
1947
|
var SERVICE_MAX_AGE_MS = 15 * 6e4;
|
|
1485
1948
|
var ANSWER_OFFER_MS = SERVICE_MAX_AGE_MS - 6e4;
|
|
1486
1949
|
var MAX_TIMER_MS = 2147483647;
|
|
@@ -1490,6 +1953,29 @@ var USED_ON_KEY = "boff.explore.avatarUsedOn";
|
|
|
1490
1953
|
var PRIMARY = '[data-avatar-primary], [data-boff-explore="panel-continue-chat"]';
|
|
1491
1954
|
var IDLE2 = { name: "idle" };
|
|
1492
1955
|
var INERT2 = { name: "inert" };
|
|
1956
|
+
var VOICE_OFF = {
|
|
1957
|
+
on: false,
|
|
1958
|
+
arm: null,
|
|
1959
|
+
echoes: 0,
|
|
1960
|
+
note: null
|
|
1961
|
+
};
|
|
1962
|
+
function lineNote(line) {
|
|
1963
|
+
return { kind: "line", line };
|
|
1964
|
+
}
|
|
1965
|
+
function sameNote(a, b) {
|
|
1966
|
+
if (a === null || b === null) return a === b;
|
|
1967
|
+
return a.kind === "line" ? b.kind === "line" && a.line === b.line : b.kind === "failed" && a.reason === b.reason;
|
|
1968
|
+
}
|
|
1969
|
+
var SESSION_REFUSALS = /* @__PURE__ */ new Set([
|
|
1970
|
+
"no_time",
|
|
1971
|
+
"max_answers",
|
|
1972
|
+
"limits",
|
|
1973
|
+
"paused"
|
|
1974
|
+
]);
|
|
1975
|
+
function playedOutNow(handle) {
|
|
1976
|
+
const run = handle.followUps;
|
|
1977
|
+
return run !== null && !run.voice && run.quiet === null && handle.playedOut && !run.owed;
|
|
1978
|
+
}
|
|
1493
1979
|
function stopAttending(handle) {
|
|
1494
1980
|
if (handle.unheard !== null) clearTimeout(handle.unheard);
|
|
1495
1981
|
if (handle.tail !== null) clearTimeout(handle.tail);
|
|
@@ -1603,8 +2089,21 @@ function acceptsQuestion(slot) {
|
|
|
1603
2089
|
function followUpBox(phase, run, token) {
|
|
1604
2090
|
return phase.name === "live" && run !== null && phase.session.followUp !== null && !phase.session.followUp.closed && token === run.token;
|
|
1605
2091
|
}
|
|
1606
|
-
|
|
1607
|
-
|
|
2092
|
+
var NO_MIC = { kind: "none" };
|
|
2093
|
+
function micChoiceOf(result, locale) {
|
|
2094
|
+
if (result.kind !== "available") return NO_MIC;
|
|
2095
|
+
const { offer } = result;
|
|
2096
|
+
const { mic } = offer;
|
|
2097
|
+
const copy = avatarConsentCopy(offer.consentVersion, locale);
|
|
2098
|
+
if (copy?.notice === "none") {
|
|
2099
|
+
const pauseMs = mic?.mode === "browser" ? mic.pauseMs : void 0;
|
|
2100
|
+
return pauseMs !== void 0 && Math.abs(pauseMs - copy.voice.pauseMs) <= PAUSE_NOTICE_TOLERANCE_MS && avatarVoiceStrings(locale) !== null ? { kind: "browser", notice: offer.consentVersion, pauseMs } : NO_MIC;
|
|
2101
|
+
}
|
|
2102
|
+
const micNotice = copy?.micNotice;
|
|
2103
|
+
if (mic !== void 0) {
|
|
2104
|
+
return mic.mode === "browser" && micNotice !== void 0 && mic.pauseMs === void 0 ? { kind: "browser", notice: micNotice } : NO_MIC;
|
|
2105
|
+
}
|
|
2106
|
+
return offer.stt !== void 0 && micNotice === void 0 ? { kind: "stream", offer: { ...offer.stt, receivedAt: Date.now() } } : NO_MIC;
|
|
1608
2107
|
}
|
|
1609
2108
|
function utcToday() {
|
|
1610
2109
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
@@ -1702,6 +2201,7 @@ function AvatarLiveView({
|
|
|
1702
2201
|
conversation
|
|
1703
2202
|
}) {
|
|
1704
2203
|
const S = avatarLiveStrings(locale);
|
|
2204
|
+
const voiceWords = avatarVoiceStrings(locale);
|
|
1705
2205
|
const uid = useId();
|
|
1706
2206
|
const consentTitleId = `${uid}-consent`;
|
|
1707
2207
|
const [questions, setQuestions] = useState("suggested");
|
|
@@ -1730,12 +2230,16 @@ function AvatarLiveView({
|
|
|
1730
2230
|
const offerUntil = answerId === null ? null : exchange.offerUntil;
|
|
1731
2231
|
const [phase, setPhase] = useState(IDLE2);
|
|
1732
2232
|
const [blocked, setBlocked] = useState(false);
|
|
2233
|
+
const blockedRef = useRef(false);
|
|
2234
|
+
blockedRef.current = blocked;
|
|
1733
2235
|
const [now, setNow] = useState(() => Date.now());
|
|
1734
2236
|
const [draft, setDraft] = useState("");
|
|
1735
2237
|
const chatMaxLength = conversation?.catalog.limits.maxMessageLength;
|
|
1736
2238
|
const askMaxLength = chatMaxLength !== void 0 && Number.isFinite(chatMaxLength) && chatMaxLength >= 1 ? Math.min(ASK_MAX_LENGTH, Math.floor(chatMaxLength)) : ASK_MAX_LENGTH;
|
|
1737
2239
|
const [note, setNote] = useState(null);
|
|
1738
2240
|
const [usedToday, setUsedToday] = useState(false);
|
|
2241
|
+
const [voiceOffered, setVoiceOffered] = useState(false);
|
|
2242
|
+
const [noNotice, setNoNotice] = useState(false);
|
|
1739
2243
|
const showing = active && !off;
|
|
1740
2244
|
const behindChat = open && !active && !off;
|
|
1741
2245
|
const phaseRef = useRef(IDLE2);
|
|
@@ -1754,8 +2258,36 @@ function AvatarLiveView({
|
|
|
1754
2258
|
const waitRef = useRef(null);
|
|
1755
2259
|
const sendingRef = useRef(false);
|
|
1756
2260
|
const textBoxRef = useRef(false);
|
|
1757
|
-
const [
|
|
2261
|
+
const [micChoice, setMicChoice] = useState(NO_MIC);
|
|
1758
2262
|
const micCancelRef = useRef(() => void 0);
|
|
2263
|
+
const micControlRef = useRef(null);
|
|
2264
|
+
const voiceRef = useRef(VOICE_OFF);
|
|
2265
|
+
const [, setVoiceTick] = useState(0);
|
|
2266
|
+
const voiceSet = useCallback((next) => {
|
|
2267
|
+
voiceRef.current = { ...voiceRef.current, ...next };
|
|
2268
|
+
setVoiceTick((tick) => tick + 1);
|
|
2269
|
+
}, []);
|
|
2270
|
+
const askMaxLengthRef = useRef(ASK_MAX_LENGTH);
|
|
2271
|
+
const voiceOff = useCallback(
|
|
2272
|
+
(note2, keep) => {
|
|
2273
|
+
const run = voiceRef.current;
|
|
2274
|
+
if (!run.on && run.arm === null) {
|
|
2275
|
+
if (!sameNote(run.note, note2)) voiceSet({ note: note2 });
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
if (keep) {
|
|
2279
|
+
const heard = micControlRef.current?.heard() ?? "";
|
|
2280
|
+
if (heard !== "") {
|
|
2281
|
+
setDraft(
|
|
2282
|
+
(current) => withHeardText(current, heard, askMaxLengthRef.current)
|
|
2283
|
+
);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
micCancelRef.current();
|
|
2287
|
+
voiceSet({ ...VOICE_OFF, note: note2 });
|
|
2288
|
+
},
|
|
2289
|
+
[voiceSet]
|
|
2290
|
+
);
|
|
1759
2291
|
const sendDueRef = useRef(() => void 0);
|
|
1760
2292
|
const checkRef = useRef(null);
|
|
1761
2293
|
const askedLocale = useRef(locale);
|
|
@@ -1812,6 +2344,8 @@ function AvatarLiveView({
|
|
|
1812
2344
|
handle.done = true;
|
|
1813
2345
|
stopAttending(handle);
|
|
1814
2346
|
sessionRef.current = null;
|
|
2347
|
+
voiceOff(null, true);
|
|
2348
|
+
if (audioRef.current !== null) audioRef.current.muted = false;
|
|
1815
2349
|
if (reason !== null) {
|
|
1816
2350
|
sendEnd({
|
|
1817
2351
|
base: handle.base,
|
|
@@ -1827,7 +2361,7 @@ function AvatarLiveView({
|
|
|
1827
2361
|
setBlocked(false);
|
|
1828
2362
|
go({ name: "ended", how });
|
|
1829
2363
|
},
|
|
1830
|
-
[go]
|
|
2364
|
+
[go, voiceOff]
|
|
1831
2365
|
);
|
|
1832
2366
|
const goLive = useCallback(() => {
|
|
1833
2367
|
const current = phaseRef.current;
|
|
@@ -1900,6 +2434,18 @@ function AvatarLiveView({
|
|
|
1900
2434
|
},
|
|
1901
2435
|
[]
|
|
1902
2436
|
);
|
|
2437
|
+
const unmute = useCallback((handle) => {
|
|
2438
|
+
const run = handle.followUps;
|
|
2439
|
+
if (run === null || !run.muted) return;
|
|
2440
|
+
run.muted = false;
|
|
2441
|
+
if (audioRef.current !== null) audioRef.current.muted = false;
|
|
2442
|
+
}, []);
|
|
2443
|
+
const holdMute = useCallback(() => {
|
|
2444
|
+
const run = sessionRef.current?.followUps ?? null;
|
|
2445
|
+
const audio = audioRef.current;
|
|
2446
|
+
if (run === null || !run.muted || audio === null || audio.muted) return;
|
|
2447
|
+
audio.muted = true;
|
|
2448
|
+
}, []);
|
|
1903
2449
|
const whenQuiet = useCallback(
|
|
1904
2450
|
(handle, delay) => {
|
|
1905
2451
|
const run = handle.followUps;
|
|
@@ -1913,10 +2459,12 @@ function AvatarLiveView({
|
|
|
1913
2459
|
}
|
|
1914
2460
|
if (run.owed) {
|
|
1915
2461
|
run.owed = false;
|
|
2462
|
+
run.owedWait = true;
|
|
1916
2463
|
wait(OWED_VOICE_WAIT_MS);
|
|
1917
2464
|
return;
|
|
1918
2465
|
}
|
|
1919
2466
|
handle.playedOut = true;
|
|
2467
|
+
if (!run.mutedUnheard) unmute(handle);
|
|
1920
2468
|
showFollowUp(handle, { speaking: false });
|
|
1921
2469
|
attend();
|
|
1922
2470
|
sendDueRef.current(handle);
|
|
@@ -1924,7 +2472,7 @@ function AvatarLiveView({
|
|
|
1924
2472
|
};
|
|
1925
2473
|
wait(delay);
|
|
1926
2474
|
},
|
|
1927
|
-
[attend, showFollowUp]
|
|
2475
|
+
[attend, showFollowUp, unmute]
|
|
1928
2476
|
);
|
|
1929
2477
|
const heardVoice = useCallback(
|
|
1930
2478
|
(handle, speaking) => {
|
|
@@ -1940,7 +2488,10 @@ function AvatarLiveView({
|
|
|
1940
2488
|
run.heard = true;
|
|
1941
2489
|
run.starts += 1;
|
|
1942
2490
|
run.owed = false;
|
|
2491
|
+
run.owedWait = false;
|
|
2492
|
+
run.mutedUnheard = false;
|
|
1943
2493
|
handle.playedOut = false;
|
|
2494
|
+
if (voiceRef.current.on) micControlRef.current?.dropLoopPress();
|
|
1944
2495
|
showFollowUp(handle, { speaking: true });
|
|
1945
2496
|
},
|
|
1946
2497
|
[showFollowUp, whenQuiet]
|
|
@@ -2007,6 +2558,7 @@ function AvatarLiveView({
|
|
|
2007
2558
|
}
|
|
2008
2559
|
run.due = null;
|
|
2009
2560
|
run.answers += 1;
|
|
2561
|
+
unmute(handle);
|
|
2010
2562
|
const heardSince = run.starts !== starts;
|
|
2011
2563
|
run.owed = !heardSince;
|
|
2012
2564
|
const playing = !heardSince || run.voice || run.quiet !== null;
|
|
@@ -2028,7 +2580,7 @@ function AvatarLiveView({
|
|
|
2028
2580
|
if (run.owed && run.heard && !run.voice) whenQuiet(handle, 0);
|
|
2029
2581
|
})();
|
|
2030
2582
|
},
|
|
2031
|
-
[attend, showFollowUp, whenQuiet]
|
|
2583
|
+
[attend, showFollowUp, unmute, whenQuiet]
|
|
2032
2584
|
);
|
|
2033
2585
|
sendDueRef.current = sendDue;
|
|
2034
2586
|
const readFollowUp = useCallback(
|
|
@@ -2058,19 +2610,26 @@ function AvatarLiveView({
|
|
|
2058
2610
|
const applyAvailability = useCallback(
|
|
2059
2611
|
(result, locale2) => {
|
|
2060
2612
|
const { live: live2 } = latest.current;
|
|
2061
|
-
|
|
2613
|
+
setMicChoice(micChoiceOf(result, locale2));
|
|
2062
2614
|
if (result.kind === "inert") {
|
|
2063
2615
|
setMode("suggested");
|
|
2064
2616
|
go(INERT2);
|
|
2065
2617
|
return;
|
|
2066
2618
|
}
|
|
2067
2619
|
if (result.kind === "refused") {
|
|
2620
|
+
if (result.consentVersion !== void 0) {
|
|
2621
|
+
setNoNotice(
|
|
2622
|
+
avatarConsentCopy(result.consentVersion, locale2)?.notice === "none"
|
|
2623
|
+
);
|
|
2624
|
+
}
|
|
2068
2625
|
setMode(result.questions);
|
|
2069
2626
|
go({ name: "refused", reason: result.reason });
|
|
2070
2627
|
return;
|
|
2071
2628
|
}
|
|
2072
2629
|
setUsedToday(usedTodayHere());
|
|
2073
2630
|
const copy = avatarConsentCopy(result.offer.consentVersion, locale2);
|
|
2631
|
+
setVoiceOffered(copy?.voice !== void 0);
|
|
2632
|
+
setNoNotice(copy?.notice === "none");
|
|
2074
2633
|
if (copy === null || copy.botCheck !== result.offer.botCheck || copy.questions !== result.offer.questions) {
|
|
2075
2634
|
setMode("suggested");
|
|
2076
2635
|
go(INERT2);
|
|
@@ -2125,12 +2684,16 @@ function AvatarLiveView({
|
|
|
2125
2684
|
heard: false,
|
|
2126
2685
|
starts: 0,
|
|
2127
2686
|
owed: true,
|
|
2687
|
+
owedWait: false,
|
|
2128
2688
|
quiet: null,
|
|
2129
2689
|
asked: null,
|
|
2130
2690
|
due: null,
|
|
2131
2691
|
retry: null,
|
|
2132
2692
|
sent: /* @__PURE__ */ new Set([context.messageId]),
|
|
2133
|
-
seq: 0
|
|
2693
|
+
seq: 0,
|
|
2694
|
+
listenedAt: null,
|
|
2695
|
+
muted: false,
|
|
2696
|
+
mutedUnheard: false
|
|
2134
2697
|
} : null;
|
|
2135
2698
|
sessionRef.current = {
|
|
2136
2699
|
session,
|
|
@@ -2149,6 +2712,7 @@ function AvatarLiveView({
|
|
|
2149
2712
|
followUps,
|
|
2150
2713
|
done: false
|
|
2151
2714
|
};
|
|
2715
|
+
if (audioRef.current !== null) audioRef.current.muted = false;
|
|
2152
2716
|
setBlocked(false);
|
|
2153
2717
|
setNow(received);
|
|
2154
2718
|
go({
|
|
@@ -2404,6 +2968,7 @@ function AvatarLiveView({
|
|
|
2404
2968
|
[applyAvailability, bindingHolds, cancelWaiting, exchangeNow, launch]
|
|
2405
2969
|
);
|
|
2406
2970
|
const stop = useCallback(() => {
|
|
2971
|
+
voiceOff(null, true);
|
|
2407
2972
|
micCancelRef.current();
|
|
2408
2973
|
const current = phaseRef.current;
|
|
2409
2974
|
if (current.name === "waiting") {
|
|
@@ -2423,9 +2988,10 @@ function AvatarLiveView({
|
|
|
2423
2988
|
if (current.name === "pending" || current.name === "live") {
|
|
2424
2989
|
finish("stopped", "user");
|
|
2425
2990
|
}
|
|
2426
|
-
}, [cancelStart, cancelWaiting, finish, go, reoffer]);
|
|
2991
|
+
}, [cancelStart, cancelWaiting, finish, go, reoffer, voiceOff]);
|
|
2427
2992
|
const leave = useCallback(
|
|
2428
2993
|
(event) => {
|
|
2994
|
+
voiceOff(sessionRef.current === null ? null : lineNote("tapNext"), false);
|
|
2429
2995
|
micCancelRef.current();
|
|
2430
2996
|
const current = phaseRef.current;
|
|
2431
2997
|
const reason = event === "user" || event === "chat" ? "user" : "page_hide";
|
|
@@ -2466,7 +3032,7 @@ function AvatarLiveView({
|
|
|
2466
3032
|
return;
|
|
2467
3033
|
}
|
|
2468
3034
|
},
|
|
2469
|
-
[cancelStart, cancelWaiting, finish, go, goLive, reoffer]
|
|
3035
|
+
[cancelStart, cancelWaiting, finish, go, goLive, reoffer, voiceOff]
|
|
2470
3036
|
);
|
|
2471
3037
|
useEffect(() => {
|
|
2472
3038
|
if (!showing) {
|
|
@@ -2580,6 +3146,7 @@ function AvatarLiveView({
|
|
|
2580
3146
|
const wasLive = phaseRef.current.name === "live";
|
|
2581
3147
|
switch (event.type) {
|
|
2582
3148
|
case "speaker-track":
|
|
3149
|
+
holdMute();
|
|
2583
3150
|
if (event.kind !== "video") {
|
|
2584
3151
|
handle.voiceArrived = true;
|
|
2585
3152
|
attend();
|
|
@@ -2589,6 +3156,7 @@ function AvatarLiveView({
|
|
|
2589
3156
|
if (!inSight()) goLive();
|
|
2590
3157
|
return;
|
|
2591
3158
|
case "playback-blocked":
|
|
3159
|
+
holdMute();
|
|
2592
3160
|
setBlocked(event.blocked);
|
|
2593
3161
|
handle.soundHeld = event.sound ?? event.blocked;
|
|
2594
3162
|
attend();
|
|
@@ -2619,7 +3187,7 @@ function AvatarLiveView({
|
|
|
2619
3187
|
return;
|
|
2620
3188
|
}
|
|
2621
3189
|
},
|
|
2622
|
-
[attend, finish, goLive, heardVoice, inSight]
|
|
3190
|
+
[attend, finish, goLive, heardVoice, holdMute, inSight]
|
|
2623
3191
|
);
|
|
2624
3192
|
useEffect(() => {
|
|
2625
3193
|
if (phase.name !== "pending") return;
|
|
@@ -2736,6 +3304,7 @@ function AvatarLiveView({
|
|
|
2736
3304
|
wait?.check?.abort();
|
|
2737
3305
|
const handle = sessionRef.current;
|
|
2738
3306
|
sessionRef.current = null;
|
|
3307
|
+
if (audioRef.current !== null) audioRef.current.muted = false;
|
|
2739
3308
|
if (handle && !handle.done) {
|
|
2740
3309
|
handle.done = true;
|
|
2741
3310
|
stopAttending(handle);
|
|
@@ -2752,7 +3321,8 @@ function AvatarLiveView({
|
|
|
2752
3321
|
[]
|
|
2753
3322
|
);
|
|
2754
3323
|
const any = questions === "any";
|
|
2755
|
-
const
|
|
3324
|
+
const usedHere = usedToday && !voiceOffered;
|
|
3325
|
+
const textMode = any && (usedHere && (phase.name === "needs-answer" || phase.name === "consent" || phase.name === "declined" || phase.name === "ended") || phase.name === "refused" && phase.reason === "limits");
|
|
2756
3326
|
textBoxRef.current = textMode;
|
|
2757
3327
|
const askState = !any ? void 0 : textMode ? "text" : phase.name === "needs-answer" ? kind === "empty" ? "ready" : kind : phase.name;
|
|
2758
3328
|
const followUp = phase.name === "live" ? phase.session.followUp : null;
|
|
@@ -2765,16 +3335,18 @@ function AvatarLiveView({
|
|
|
2765
3335
|
const refreshMicOffer = useCallback(async () => {
|
|
2766
3336
|
const { live: live2, locale: locale2, base: base2, headers: headers2 } = latest.current;
|
|
2767
3337
|
if (base2 === null) return null;
|
|
2768
|
-
const next =
|
|
3338
|
+
const next = micChoiceOf(
|
|
2769
3339
|
await requestAvailabilityAnswer({
|
|
2770
3340
|
base: base2,
|
|
2771
3341
|
headers: headers2,
|
|
2772
3342
|
site: live2.site,
|
|
2773
3343
|
locale: locale2
|
|
2774
|
-
})
|
|
3344
|
+
}),
|
|
3345
|
+
locale2
|
|
2775
3346
|
);
|
|
2776
|
-
if (next !==
|
|
2777
|
-
|
|
3347
|
+
if (next.kind !== "stream") return null;
|
|
3348
|
+
setMicChoice(next);
|
|
3349
|
+
return next.offer;
|
|
2778
3350
|
}, []);
|
|
2779
3351
|
const onMicText = useCallback(
|
|
2780
3352
|
(text) => {
|
|
@@ -2793,15 +3365,153 @@ function AvatarLiveView({
|
|
|
2793
3365
|
return;
|
|
2794
3366
|
}
|
|
2795
3367
|
const run = handle.followUps;
|
|
3368
|
+
const voiced = micControlRef.current?.mode === "conversation";
|
|
2796
3369
|
tell(handle, "interrupt");
|
|
3370
|
+
if (voiced) {
|
|
3371
|
+
run.muted = true;
|
|
3372
|
+
run.mutedUnheard = run.owed || run.owedWait;
|
|
3373
|
+
if (audioRef.current !== null) audioRef.current.muted = true;
|
|
3374
|
+
}
|
|
2797
3375
|
if (run.quiet !== null) clearTimeout(run.quiet);
|
|
2798
3376
|
run.quiet = null;
|
|
2799
3377
|
run.owed = false;
|
|
2800
3378
|
handle.playedOut = true;
|
|
2801
3379
|
showFollowUp(handle, { speaking: false });
|
|
2802
|
-
|
|
3380
|
+
if (voiced && !run.voice) whenQuiet(handle, QUIET_PLAYED_OUT_MS);
|
|
3381
|
+
}, [showFollowUp, tell, whenQuiet]);
|
|
3382
|
+
const firstArmable = useCallback(() => {
|
|
3383
|
+
const current = phaseRef.current;
|
|
3384
|
+
return sessionRef.current === null && !textBoxRef.current && current.name === "needs-answer" && current.offer.copy.notice === "none" && exchangeNow().kind === "empty";
|
|
3385
|
+
}, [exchangeNow]);
|
|
3386
|
+
const armFirst = useCallback(
|
|
3387
|
+
(stage, askSeq) => {
|
|
3388
|
+
const current = phaseRef.current;
|
|
3389
|
+
if (current.name !== "needs-answer") return null;
|
|
3390
|
+
const { live: live2, locale: locale2 } = latest.current;
|
|
3391
|
+
unlockSound(audioRef.current);
|
|
3392
|
+
const module = loadModule(live2.load);
|
|
3393
|
+
module.catch(() => void 0);
|
|
3394
|
+
return {
|
|
3395
|
+
stage,
|
|
3396
|
+
consentVersion: current.offer.consentVersion,
|
|
3397
|
+
botCheck: current.offer.botCheck,
|
|
3398
|
+
site: live2.site,
|
|
3399
|
+
locale: locale2,
|
|
3400
|
+
path: pathNow(),
|
|
3401
|
+
module,
|
|
3402
|
+
askSeq
|
|
3403
|
+
};
|
|
3404
|
+
},
|
|
3405
|
+
[pathNow]
|
|
3406
|
+
);
|
|
3407
|
+
const onMicPress = useCallback(
|
|
3408
|
+
(gesture) => {
|
|
3409
|
+
const run = voiceRef.current;
|
|
3410
|
+
if (run.on && run.note === null && !gesture) return;
|
|
3411
|
+
const arm = gesture && firstArmable() ? armFirst("pressing", null) : null;
|
|
3412
|
+
voiceSet({
|
|
3413
|
+
on: true,
|
|
3414
|
+
note: null,
|
|
3415
|
+
...gesture ? { echoes: 0, arm } : {}
|
|
3416
|
+
});
|
|
3417
|
+
},
|
|
3418
|
+
[armFirst, firstArmable, voiceSet]
|
|
3419
|
+
);
|
|
3420
|
+
const echoOf = useCallback(() => {
|
|
3421
|
+
const current = phaseRef.current;
|
|
3422
|
+
return current.name === "live" && current.session.followUp !== null && sessionRef.current?.followUps ? current.session.captions.text : null;
|
|
3423
|
+
}, []);
|
|
3424
|
+
const askRef = useRef(() => null);
|
|
3425
|
+
const askReasonRef = useRef(null);
|
|
3426
|
+
const draftRef = useRef("");
|
|
3427
|
+
draftRef.current = draft;
|
|
3428
|
+
askMaxLengthRef.current = askMaxLength;
|
|
3429
|
+
const autoSend = useCallback(
|
|
3430
|
+
(text) => {
|
|
3431
|
+
const question = text.trim();
|
|
3432
|
+
const refuse = () => {
|
|
3433
|
+
onMicText(text);
|
|
3434
|
+
voiceOff(lineNote("inBox"), false);
|
|
3435
|
+
};
|
|
3436
|
+
if (question === "" || askReasonRef.current !== null || sendingRef.current || question.length > askMaxLengthRef.current || draftRef.current.trim() !== "") {
|
|
3437
|
+
refuse();
|
|
3438
|
+
return;
|
|
3439
|
+
}
|
|
3440
|
+
const sending = askRef.current(question);
|
|
3441
|
+
if (sending === null) {
|
|
3442
|
+
refuse();
|
|
3443
|
+
return;
|
|
3444
|
+
}
|
|
3445
|
+
const arm = voiceRef.current.arm;
|
|
3446
|
+
voiceSet({
|
|
3447
|
+
echoes: 0,
|
|
3448
|
+
note: lineNote("sending"),
|
|
3449
|
+
...arm === null ? {} : { arm: { ...arm, stage: "asked", askSeq: askSeqRef.current } }
|
|
3450
|
+
});
|
|
3451
|
+
void sending.then(
|
|
3452
|
+
(asked) => {
|
|
3453
|
+
if (!asked) {
|
|
3454
|
+
voiceOff(lineNote("off"), false);
|
|
3455
|
+
return;
|
|
3456
|
+
}
|
|
3457
|
+
const run = voiceRef.current;
|
|
3458
|
+
if (run.on && sameNote(run.note, lineNote("sending"))) {
|
|
3459
|
+
voiceSet({ note: lineNote("again") });
|
|
3460
|
+
}
|
|
3461
|
+
},
|
|
3462
|
+
() => voiceOff(lineNote("off"), false)
|
|
3463
|
+
);
|
|
3464
|
+
},
|
|
3465
|
+
[onMicText, voiceOff, voiceSet]
|
|
3466
|
+
);
|
|
3467
|
+
const onMicTurn = useCallback(
|
|
3468
|
+
(turn) => {
|
|
3469
|
+
const run = voiceRef.current;
|
|
3470
|
+
if (!run.on) return;
|
|
3471
|
+
if (turn.by === "pause" && turn.echo) {
|
|
3472
|
+
if (run.echoes >= MAX_ECHO_DISCARDS || run.arm !== null) {
|
|
3473
|
+
voiceOff(lineNote("off"), false);
|
|
3474
|
+
} else {
|
|
3475
|
+
voiceSet({ echoes: run.echoes + 1, note: null });
|
|
3476
|
+
}
|
|
3477
|
+
return;
|
|
3478
|
+
}
|
|
3479
|
+
if (turn.by === "pause") {
|
|
3480
|
+
autoSend(turn.text);
|
|
3481
|
+
return;
|
|
3482
|
+
}
|
|
3483
|
+
if (turn.by === "silence") {
|
|
3484
|
+
voiceOff(lineNote("noQuestion"), false);
|
|
3485
|
+
return;
|
|
3486
|
+
}
|
|
3487
|
+
if (turn.text.trim() !== "") {
|
|
3488
|
+
onMicText(turn.text);
|
|
3489
|
+
voiceOff(lineNote(turn.by === "limit" ? "atLimit" : "inBox"), false);
|
|
3490
|
+
return;
|
|
3491
|
+
}
|
|
3492
|
+
if (turn.by === "failed") {
|
|
3493
|
+
voiceOff(
|
|
3494
|
+
turn.gesture ? { kind: "failed", reason: turn.failure ?? "failed" } : lineNote("tapNext"),
|
|
3495
|
+
false
|
|
3496
|
+
);
|
|
3497
|
+
return;
|
|
3498
|
+
}
|
|
3499
|
+
voiceOff(lineNote("off"), false);
|
|
3500
|
+
},
|
|
3501
|
+
[autoSend, onMicText, voiceOff, voiceSet]
|
|
3502
|
+
);
|
|
3503
|
+
const micSource = micChoice.kind === "browser" ? {
|
|
3504
|
+
kind: "browser",
|
|
3505
|
+
notice: micChoice.notice,
|
|
3506
|
+
...micChoice.pauseMs === void 0 ? {} : { pauseMs: micChoice.pauseMs }
|
|
3507
|
+
} : micChoice.kind === "stream" && base !== null ? {
|
|
3508
|
+
kind: "stream",
|
|
3509
|
+
base,
|
|
3510
|
+
offer: micChoice.offer,
|
|
3511
|
+
refreshOffer: refreshMicOffer
|
|
3512
|
+
} : null;
|
|
2803
3513
|
const mic = useAvatarMic({
|
|
2804
|
-
enabled: live.mic === true &&
|
|
3514
|
+
enabled: live.mic === true && micSource !== null && !off && acceptsQuestion({
|
|
2805
3515
|
any,
|
|
2806
3516
|
phase: phase.name,
|
|
2807
3517
|
kind,
|
|
@@ -2810,28 +3520,41 @@ function AvatarLiveView({
|
|
|
2810
3520
|
followUp: followUpAsks
|
|
2811
3521
|
}),
|
|
2812
3522
|
locale,
|
|
2813
|
-
|
|
2814
|
-
offer: micOffer,
|
|
2815
|
-
refreshOffer: refreshMicOffer,
|
|
3523
|
+
source: micSource,
|
|
2816
3524
|
onText: onMicText,
|
|
2817
|
-
onStart: onMicStart
|
|
3525
|
+
onStart: onMicStart,
|
|
3526
|
+
onTurn: onMicTurn,
|
|
3527
|
+
onPress: onMicPress,
|
|
3528
|
+
echoOf,
|
|
3529
|
+
interruptible: followUp?.speaking === true,
|
|
3530
|
+
note: voiceRef.current.note
|
|
2818
3531
|
});
|
|
2819
3532
|
micCancelRef.current = mic.cancel;
|
|
2820
|
-
|
|
3533
|
+
micControlRef.current = mic;
|
|
3534
|
+
askReasonRef.current = askReason;
|
|
3535
|
+
const micRecording = mic.mode === "conversation" ? mic.speaking : mic.recording;
|
|
2821
3536
|
useEffect(() => {
|
|
2822
3537
|
const handle = sessionRef.current;
|
|
2823
3538
|
if (!micRecording || handle === null || handle.followUps === null || handle.done) {
|
|
2824
3539
|
return;
|
|
2825
3540
|
}
|
|
2826
|
-
|
|
3541
|
+
const run = handle.followUps;
|
|
2827
3542
|
const every = Math.min(
|
|
2828
3543
|
LISTENING_MARK_MS,
|
|
2829
|
-
Math.max(1e3,
|
|
3544
|
+
Math.max(1e3, run.limits.idleSeconds * 1e3 / 3)
|
|
2830
3545
|
);
|
|
2831
|
-
const
|
|
3546
|
+
const spacing = Math.min(LISTENING_MARK_SPACING_MS, every / 2);
|
|
3547
|
+
const mark = () => {
|
|
3548
|
+
const at = Date.now();
|
|
3549
|
+
if (run.listenedAt !== null && at - run.listenedAt < spacing) return;
|
|
3550
|
+
run.listenedAt = at;
|
|
3551
|
+
tell(handle, "listening");
|
|
3552
|
+
};
|
|
3553
|
+
mark();
|
|
3554
|
+
const timer = setInterval(mark, every);
|
|
2832
3555
|
return () => {
|
|
2833
3556
|
clearInterval(timer);
|
|
2834
|
-
if (sessionRef.current === handle)
|
|
3557
|
+
if (sessionRef.current === handle) mark();
|
|
2835
3558
|
};
|
|
2836
3559
|
}, [micRecording, tell]);
|
|
2837
3560
|
const notNow = useCallback(() => {
|
|
@@ -2847,7 +3570,7 @@ function AvatarLiveView({
|
|
|
2847
3570
|
const run = handle.followUps;
|
|
2848
3571
|
const current = phaseRef.current;
|
|
2849
3572
|
if (run === null || handle.done || current.name !== "live" || current.session.followUp?.pending !== null || !followUpBox(current, run, conversation2.getConversationToken())) {
|
|
2850
|
-
return;
|
|
3573
|
+
return null;
|
|
2851
3574
|
}
|
|
2852
3575
|
sendingRef.current = true;
|
|
2853
3576
|
run.seq += 1;
|
|
@@ -2855,17 +3578,17 @@ function AvatarLiveView({
|
|
|
2855
3578
|
setDraft("");
|
|
2856
3579
|
tell(handle, "asking");
|
|
2857
3580
|
showFollowUp(handle, { pending: "asking", unread: null });
|
|
2858
|
-
|
|
3581
|
+
return conversation2.send(question, [], { surface: "AVATAR" }).then((added) => {
|
|
2859
3582
|
if (added === null) {
|
|
2860
3583
|
setDraft((current2) => current2 === "" ? question : current2);
|
|
2861
3584
|
}
|
|
2862
3585
|
if (sessionRef.current !== handle || handle.done || run.seq !== seq) {
|
|
2863
|
-
return;
|
|
3586
|
+
return false;
|
|
2864
3587
|
}
|
|
2865
3588
|
if (added === null || added.conversationToken !== run.token) {
|
|
2866
3589
|
showFollowUp(handle, { pending: null });
|
|
2867
3590
|
attend();
|
|
2868
|
-
return;
|
|
3591
|
+
return false;
|
|
2869
3592
|
}
|
|
2870
3593
|
run.asked = {
|
|
2871
3594
|
messageId: added.userMessageId,
|
|
@@ -2873,6 +3596,7 @@ function AvatarLiveView({
|
|
|
2873
3596
|
answerId: null
|
|
2874
3597
|
};
|
|
2875
3598
|
showFollowUp(handle, { pending: "asking" });
|
|
3599
|
+
return true;
|
|
2876
3600
|
}).finally(() => {
|
|
2877
3601
|
sendingRef.current = false;
|
|
2878
3602
|
});
|
|
@@ -2884,37 +3608,135 @@ function AvatarLiveView({
|
|
|
2884
3608
|
const conversation2 = conversationRef.current;
|
|
2885
3609
|
const question = text.trim();
|
|
2886
3610
|
if (conversation2 === null || question === "" || sendingRef.current) {
|
|
2887
|
-
return;
|
|
3611
|
+
return null;
|
|
2888
3612
|
}
|
|
2889
3613
|
const spoken = !textBoxRef.current;
|
|
2890
3614
|
const handle = sessionRef.current;
|
|
2891
3615
|
if (spoken && handle !== null && handle.followUps !== null) {
|
|
2892
|
-
askFollowUp(handle, conversation2, question);
|
|
2893
|
-
return;
|
|
3616
|
+
return askFollowUp(handle, conversation2, question);
|
|
2894
3617
|
}
|
|
2895
|
-
if (spoken && conversation2.messages.length > 0) return;
|
|
3618
|
+
if (spoken && conversation2.messages.length > 0) return null;
|
|
2896
3619
|
sendingRef.current = true;
|
|
2897
3620
|
setAskedHere(null);
|
|
2898
3621
|
const seq = askSeqRef.current;
|
|
2899
3622
|
setNote(null);
|
|
2900
3623
|
setDraft("");
|
|
2901
|
-
|
|
3624
|
+
return conversation2.send(question, [], spoken ? { surface: "AVATAR" } : {}).then((added) => {
|
|
2902
3625
|
if (added === null) {
|
|
2903
3626
|
setDraft((current) => current === "" ? question : current);
|
|
2904
3627
|
}
|
|
2905
|
-
if (
|
|
2906
|
-
return;
|
|
3628
|
+
if (added === null || spoken && (askSeqRef.current !== seq || added.conversationToken !== (conversationRef.current?.getConversationToken() ?? null))) {
|
|
3629
|
+
return false;
|
|
2907
3630
|
}
|
|
3631
|
+
if (!spoken) return true;
|
|
2908
3632
|
setAskedHere({
|
|
2909
3633
|
messageId: added.userMessageId,
|
|
2910
3634
|
token: added.conversationToken
|
|
2911
3635
|
});
|
|
3636
|
+
return true;
|
|
2912
3637
|
}).finally(() => {
|
|
2913
3638
|
sendingRef.current = false;
|
|
2914
3639
|
});
|
|
2915
3640
|
},
|
|
2916
3641
|
[askFollowUp, setAskedHere]
|
|
2917
3642
|
);
|
|
3643
|
+
askRef.current = ask;
|
|
3644
|
+
const askTyped = useCallback(
|
|
3645
|
+
(text) => {
|
|
3646
|
+
voiceOff(null, false);
|
|
3647
|
+
const armable = firstArmable();
|
|
3648
|
+
const sending = ask(text);
|
|
3649
|
+
if (sending !== null && armable) {
|
|
3650
|
+
voiceSet({ arm: armFirst("asked", askSeqRef.current) });
|
|
3651
|
+
}
|
|
3652
|
+
return sending;
|
|
3653
|
+
},
|
|
3654
|
+
[armFirst, ask, firstArmable, voiceOff, voiceSet]
|
|
3655
|
+
);
|
|
3656
|
+
const onDraftChange = useCallback(
|
|
3657
|
+
(value) => {
|
|
3658
|
+
setDraft(value);
|
|
3659
|
+
const run = voiceRef.current;
|
|
3660
|
+
if (run.on || run.arm !== null) voiceOff(lineNote("off"), true);
|
|
3661
|
+
},
|
|
3662
|
+
[voiceOff]
|
|
3663
|
+
);
|
|
3664
|
+
const armVoiceStart = useCallback(() => {
|
|
3665
|
+
const arm = voiceRef.current.arm;
|
|
3666
|
+
if (arm === null || arm.stage !== "asked") return;
|
|
3667
|
+
const current = phaseRef.current;
|
|
3668
|
+
const exchange2 = exchangeNow();
|
|
3669
|
+
const drop = () => voiceSet({ arm: null });
|
|
3670
|
+
if (arm.askSeq !== askSeqRef.current || current.name !== "needs-answer" && current.name !== "consent") {
|
|
3671
|
+
drop();
|
|
3672
|
+
return;
|
|
3673
|
+
}
|
|
3674
|
+
if (sendingRef.current || exchange2.kind === "asking" && (!exchange2.askedHere || exchange2.pendingAnswerId === null)) {
|
|
3675
|
+
return;
|
|
3676
|
+
}
|
|
3677
|
+
const answerId2 = exchange2.kind === "asking" ? exchange2.pendingAnswerId : exchange2.kind === "answer" ? exchange2.answerId : null;
|
|
3678
|
+
const token2 = conversationRef.current?.getConversationToken() ?? null;
|
|
3679
|
+
const { live: live2, locale: locale2 } = latest.current;
|
|
3680
|
+
const path = pathNow();
|
|
3681
|
+
if (answerId2 === null || !exchange2.askedHere || exchange2.question === null || token2 === null || !isAvatarConversationToken(token2) || arm.consentVersion !== current.offer.consentVersion || arm.botCheck !== current.offer.botCheck || arm.site !== live2.site || arm.locale !== locale2 || arm.path !== path) {
|
|
3682
|
+
drop();
|
|
3683
|
+
return;
|
|
3684
|
+
}
|
|
3685
|
+
voiceSet({ arm: null });
|
|
3686
|
+
waitRef.current = {
|
|
3687
|
+
bound: {
|
|
3688
|
+
token: token2,
|
|
3689
|
+
questionId: exchange2.question.id,
|
|
3690
|
+
answerId: answerId2,
|
|
3691
|
+
site: live2.site,
|
|
3692
|
+
locale: locale2,
|
|
3693
|
+
path,
|
|
3694
|
+
consentVersion: arm.consentVersion,
|
|
3695
|
+
botCheck: arm.botCheck
|
|
3696
|
+
},
|
|
3697
|
+
module: arm.module,
|
|
3698
|
+
check: null
|
|
3699
|
+
};
|
|
3700
|
+
setNote(null);
|
|
3701
|
+
go({ name: "waiting", offer: current.offer });
|
|
3702
|
+
}, [exchangeNow, go, pathNow, voiceSet]);
|
|
3703
|
+
useEffect(() => {
|
|
3704
|
+
armVoiceStart();
|
|
3705
|
+
});
|
|
3706
|
+
useEffect(() => {
|
|
3707
|
+
const run = voiceRef.current;
|
|
3708
|
+
if (!run.on && run.arm === null) return;
|
|
3709
|
+
const current = phaseRef.current;
|
|
3710
|
+
const live2 = current.name === "live" ? current.session.followUp : null;
|
|
3711
|
+
const keeps = (mic.mode === "conversation" || !run.on) && !textMode && (current.name === "needs-answer" && (sendingRef.current || kind === "empty" || kind === "asking" || kind === "answer") || current.name === "consent" || current.name === "waiting" || current.name === "bot-check" || current.name === "creating" || current.name === "pending" || current.name === "live" && followUpAsks && live2 !== null && !live2.closed && (live2.unread === null || !SESSION_REFUSALS.has(live2.unread)));
|
|
3712
|
+
if (!keeps) voiceOff(lineNote("off"), true);
|
|
3713
|
+
});
|
|
3714
|
+
useEffect(() => {
|
|
3715
|
+
const run = voiceRef.current;
|
|
3716
|
+
const controls = micControlRef.current;
|
|
3717
|
+
const handle = sessionRef.current;
|
|
3718
|
+
const followUps = handle?.followUps ?? null;
|
|
3719
|
+
const current = phaseRef.current;
|
|
3720
|
+
if (!run.on || controls === null || controls.mode !== "conversation" || handle === null || followUps === null || handle.done || current.name !== "live") {
|
|
3721
|
+
return;
|
|
3722
|
+
}
|
|
3723
|
+
const shownFollowUp = current.session.followUp;
|
|
3724
|
+
if (shownFollowUp === null || shownFollowUp.speaking || shownFollowUp.pending !== null || shownFollowUp.closed || shownFollowUp.unread !== null && SESSION_REFUSALS.has(shownFollowUp.unread) || !followUpBox(
|
|
3725
|
+
current,
|
|
3726
|
+
followUps,
|
|
3727
|
+
conversationRef.current?.getConversationToken() ?? null
|
|
3728
|
+
)) {
|
|
3729
|
+
return;
|
|
3730
|
+
}
|
|
3731
|
+
if (sendingRef.current || followUps.asked !== null || followUps.due !== null || !playedOutNow(handle) || blockedRef.current || !inSight() || controls.pressing()) {
|
|
3732
|
+
return;
|
|
3733
|
+
}
|
|
3734
|
+
if (handle.endsAt - Date.now() < LISTEN_AGAIN_MIN_LEFT_MS) {
|
|
3735
|
+
voiceOff(lineNote("off"), false);
|
|
3736
|
+
return;
|
|
3737
|
+
}
|
|
3738
|
+
controls.listen();
|
|
3739
|
+
});
|
|
2918
3740
|
useEffect(() => {
|
|
2919
3741
|
const handle = sessionRef.current;
|
|
2920
3742
|
const run = handle?.followUps ?? null;
|
|
@@ -2989,6 +3811,8 @@ function AvatarLiveView({
|
|
|
2989
3811
|
check();
|
|
2990
3812
|
}
|
|
2991
3813
|
}, [check, exchangeNow, go, setAskedHere]);
|
|
3814
|
+
const voiceTalk = mic.mode === "conversation" && mic.button !== null && voiceWords !== null;
|
|
3815
|
+
const voiceArming = voiceRef.current.arm?.stage === "asked";
|
|
2992
3816
|
useEffect(() => {
|
|
2993
3817
|
if (!active) return;
|
|
2994
3818
|
const focused = document.activeElement;
|
|
@@ -3004,8 +3828,12 @@ function AvatarLiveView({
|
|
|
3004
3828
|
]);
|
|
3005
3829
|
const onPlaying = goLive;
|
|
3006
3830
|
const enablePlayback = useCallback(() => {
|
|
3007
|
-
|
|
3008
|
-
|
|
3831
|
+
const room = sessionRef.current?.joined ?? null;
|
|
3832
|
+
if (room === null) return;
|
|
3833
|
+
const resumed = room.enablePlayback();
|
|
3834
|
+
holdMute();
|
|
3835
|
+
void resumed.catch(() => void 0).then(holdMute);
|
|
3836
|
+
}, [holdMute]);
|
|
3009
3837
|
if (off || phase.name === "inert") {
|
|
3010
3838
|
return (
|
|
3011
3839
|
// `display: contents`: the earlier Avatar view's picture, line and button lay out in the pane exactly as before.
|
|
@@ -3124,7 +3952,7 @@ function AvatarLiveView({
|
|
|
3124
3952
|
]
|
|
3125
3953
|
}
|
|
3126
3954
|
);
|
|
3127
|
-
const consentActions = (whenReady) => /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [
|
|
3955
|
+
const consentActions = (whenReady, label = whenReady ? S.startWhenReady : S.start) => /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [
|
|
3128
3956
|
/* @__PURE__ */ jsx(
|
|
3129
3957
|
"button",
|
|
3130
3958
|
{
|
|
@@ -3133,7 +3961,7 @@ function AvatarLiveView({
|
|
|
3133
3961
|
"data-boff-explore": whenReady ? "avatar-live-start-when-ready" : "avatar-live-start",
|
|
3134
3962
|
className: primaryButton,
|
|
3135
3963
|
onClick: whenReady ? startWhenReady : start,
|
|
3136
|
-
children:
|
|
3964
|
+
children: label
|
|
3137
3965
|
}
|
|
3138
3966
|
),
|
|
3139
3967
|
/* @__PURE__ */ jsx(
|
|
@@ -3177,7 +4005,14 @@ function AvatarLiveView({
|
|
|
3177
4005
|
className: "absolute inset-0 h-full w-full object-cover"
|
|
3178
4006
|
}
|
|
3179
4007
|
),
|
|
3180
|
-
any ? null : /* @__PURE__ */ jsx(
|
|
4008
|
+
any ? null : /* @__PURE__ */ jsx(
|
|
4009
|
+
"audio",
|
|
4010
|
+
{
|
|
4011
|
+
ref: audioRef,
|
|
4012
|
+
"data-boff-explore": "avatar-live-audio",
|
|
4013
|
+
onVolumeChange: holdMute
|
|
4014
|
+
}
|
|
4015
|
+
)
|
|
3181
4016
|
]
|
|
3182
4017
|
}
|
|
3183
4018
|
);
|
|
@@ -3310,20 +4145,24 @@ function AvatarLiveView({
|
|
|
3310
4145
|
switch (shown.kind) {
|
|
3311
4146
|
case "empty":
|
|
3312
4147
|
status = note === "new-chat" ? S.newChatStarted : "";
|
|
3313
|
-
lead = /* @__PURE__ */ jsx("p", { className: "text-base font-semibold", children: S.askHeadline });
|
|
4148
|
+
lead = /* @__PURE__ */ jsx("p", { className: "text-base font-semibold", children: voiceTalk && voiceWords !== null ? voiceWords.headline : S.askHeadline });
|
|
3314
4149
|
box = "spoken";
|
|
3315
4150
|
break;
|
|
3316
4151
|
case "asking":
|
|
3317
4152
|
case "answer": {
|
|
3318
|
-
const card = shown.askedHere && shown.pendingAnswerId !== null;
|
|
3319
|
-
|
|
3320
|
-
|
|
4153
|
+
const card = shown.askedHere && shown.pendingAnswerId !== null && !voiceArming;
|
|
4154
|
+
const words = phase.offer.copy.notice === "none" ? null : phase.offer.copy;
|
|
4155
|
+
status = note === "cancelled" ? words === null && voiceWords !== null ? voiceWords.cancelled : S.armedCancelled : S.gettingAnswer;
|
|
4156
|
+
if (card && words !== null) media2 = stillPicture(true);
|
|
3321
4157
|
body2 = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3322
4158
|
questionLine,
|
|
3323
4159
|
activityLine,
|
|
3324
|
-
card ? consentCard(
|
|
4160
|
+
card && words !== null ? consentCard(words) : null
|
|
3325
4161
|
] });
|
|
3326
|
-
actions2 = card ? consentActions(
|
|
4162
|
+
actions2 = card ? consentActions(
|
|
4163
|
+
true,
|
|
4164
|
+
words === null ? voiceWords?.start ?? S.start : S.startWhenReady
|
|
4165
|
+
) : null;
|
|
3327
4166
|
break;
|
|
3328
4167
|
}
|
|
3329
4168
|
case "text-only":
|
|
@@ -3355,6 +4194,16 @@ function AvatarLiveView({
|
|
|
3355
4194
|
}
|
|
3356
4195
|
break;
|
|
3357
4196
|
case "consent":
|
|
4197
|
+
if (voiceArming) {
|
|
4198
|
+
status = S.armed;
|
|
4199
|
+
body2 = questionLine;
|
|
4200
|
+
break;
|
|
4201
|
+
}
|
|
4202
|
+
if (phase.offer.copy.notice === "none") {
|
|
4203
|
+
body2 = questionLine;
|
|
4204
|
+
actions2 = consentActions(false, voiceWords?.start ?? S.start);
|
|
4205
|
+
break;
|
|
4206
|
+
}
|
|
3358
4207
|
media2 = stillPicture(true);
|
|
3359
4208
|
body2 = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3360
4209
|
questionLine,
|
|
@@ -3446,7 +4295,7 @@ function AvatarLiveView({
|
|
|
3446
4295
|
"aria-disabled": reason !== null ? "true" : void 0,
|
|
3447
4296
|
className: "min-h-11 rounded-full border border-border bg-background px-3 py-2 text-start text-sm text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring aria-disabled:cursor-not-allowed aria-disabled:opacity-50 motion-reduce:transition-none",
|
|
3448
4297
|
onClick: () => {
|
|
3449
|
-
if (reason === null)
|
|
4298
|
+
if (reason === null) void askTyped(prompt);
|
|
3450
4299
|
},
|
|
3451
4300
|
children: prompt
|
|
3452
4301
|
},
|
|
@@ -3474,7 +4323,7 @@ function AvatarLiveView({
|
|
|
3474
4323
|
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: S.newChatNote })
|
|
3475
4324
|
] }) : null,
|
|
3476
4325
|
chips,
|
|
3477
|
-
box !== null &&
|
|
4326
|
+
box !== null && usedHere && phase.name !== "live" ? /* @__PURE__ */ jsx(
|
|
3478
4327
|
"p",
|
|
3479
4328
|
{
|
|
3480
4329
|
"data-boff-explore": "avatar-live-used-today",
|
|
@@ -3488,13 +4337,16 @@ function AvatarLiveView({
|
|
|
3488
4337
|
{
|
|
3489
4338
|
value: draft,
|
|
3490
4339
|
maxLength: askMaxLength,
|
|
3491
|
-
onChange:
|
|
3492
|
-
onAsk:
|
|
4340
|
+
onChange: onDraftChange,
|
|
4341
|
+
onAsk: askTyped,
|
|
3493
4342
|
reason,
|
|
3494
4343
|
label: S.askLabel,
|
|
3495
|
-
placeholder:
|
|
4344
|
+
placeholder: box === "spoken" && voiceTalk && voiceWords !== null ? (
|
|
4345
|
+
// None while the mic listens: its line says so, and the box is at its narrowest then.
|
|
4346
|
+
mic.busy ? "" : voiceWords.placeholder
|
|
4347
|
+
) : S.askPlaceholder,
|
|
3496
4348
|
send,
|
|
3497
|
-
footer: S.askFooter,
|
|
4349
|
+
footer: noNotice ? null : S.askFooter,
|
|
3498
4350
|
noFiles: S.askNoFiles,
|
|
3499
4351
|
hint: box === "spoken" ? S.needsAnswerAny : null,
|
|
3500
4352
|
touchFirst,
|
|
@@ -3532,7 +4384,14 @@ function AvatarLiveView({
|
|
|
3532
4384
|
actions2
|
|
3533
4385
|
] }),
|
|
3534
4386
|
foot,
|
|
3535
|
-
/* @__PURE__ */ jsx(
|
|
4387
|
+
/* @__PURE__ */ jsx(
|
|
4388
|
+
"audio",
|
|
4389
|
+
{
|
|
4390
|
+
ref: audioRef,
|
|
4391
|
+
"data-boff-explore": "avatar-live-audio",
|
|
4392
|
+
onVolumeChange: holdMute
|
|
4393
|
+
}
|
|
4394
|
+
)
|
|
3536
4395
|
]
|
|
3537
4396
|
}
|
|
3538
4397
|
);
|
|
@@ -3564,6 +4423,7 @@ function AvatarLiveView({
|
|
|
3564
4423
|
actions = startOver ? startOverActions : continueButton;
|
|
3565
4424
|
break;
|
|
3566
4425
|
case "consent":
|
|
4426
|
+
if (phase.offer.copy.notice === "none") break;
|
|
3567
4427
|
media = stillPicture(true);
|
|
3568
4428
|
body = consentCard(phase.offer.copy);
|
|
3569
4429
|
actions = consentActions(false);
|
|
@@ -3656,6 +4516,7 @@ function AskBox({
|
|
|
3656
4516
|
const footerId = `${uid}-footer`;
|
|
3657
4517
|
const [refusedFiles, setRefusedFiles] = useState(false);
|
|
3658
4518
|
const empty = value.trim() === "";
|
|
4519
|
+
const describedBy = [hint === null ? null : hintId, footer === null ? null : footerId].filter((id) => id !== null).join(" ") || void 0;
|
|
3659
4520
|
const submit = () => {
|
|
3660
4521
|
if (reason !== null || empty) return;
|
|
3661
4522
|
onAsk(value);
|
|
@@ -3698,12 +4559,17 @@ function AskBox({
|
|
|
3698
4559
|
rows: 1,
|
|
3699
4560
|
maxLength,
|
|
3700
4561
|
"aria-label": label,
|
|
3701
|
-
"aria-describedby":
|
|
4562
|
+
"aria-describedby": describedBy,
|
|
3702
4563
|
placeholder,
|
|
3703
4564
|
dir: "auto",
|
|
3704
4565
|
"data-avatar-primary": touchFirst ? void 0 : "",
|
|
3705
4566
|
"data-boff-explore": "avatar-live-ask-input",
|
|
3706
|
-
className:
|
|
4567
|
+
className: cn(
|
|
4568
|
+
// Kept out of a site's session replay before Send (rrweb's default classes; `rr-ignore` for its input
|
|
4569
|
+
// events): the text may be what the mic heard, or what the visitor typed.
|
|
4570
|
+
REPLAY_PRIVATE,
|
|
4571
|
+
"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"
|
|
4572
|
+
),
|
|
3707
4573
|
onChange: (event) => {
|
|
3708
4574
|
setRefusedFiles(false);
|
|
3709
4575
|
onChange(event.target.value);
|
|
@@ -3746,7 +4612,7 @@ function AskBox({
|
|
|
3746
4612
|
}
|
|
3747
4613
|
) : null,
|
|
3748
4614
|
hint === null ? null : /* @__PURE__ */ jsx("p", { id: hintId, className: "sr-only", children: hint }),
|
|
3749
|
-
/* @__PURE__ */ jsx("p", { id: footerId, className: "text-xs text-muted-foreground", children: footer })
|
|
4615
|
+
footer === null ? null : /* @__PURE__ */ jsx("p", { id: footerId, className: "text-xs text-muted-foreground", children: footer })
|
|
3750
4616
|
]
|
|
3751
4617
|
}
|
|
3752
4618
|
);
|