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