@burdenoff/website-sdk 2026.1104.4 → 2026.1105.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 +27 -2
- 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 +415 -22
- package/dist/avatar-live-view/index.js.map +1 -1
- package/dist/avatar-live-view/index.mjs +410 -17
- package/dist/avatar-live-view/index.mjs.map +1 -1
- package/dist/avatar-mic/index.d.mts +193 -0
- package/dist/avatar-mic/index.d.ts +193 -0
- package/dist/avatar-mic/index.js +738 -0
- package/dist/avatar-mic/index.js.map +1 -0
- package/dist/avatar-mic/index.mjs +736 -0
- package/dist/avatar-mic/index.mjs.map +1 -0
- package/dist/{chunk-7MSB3EUI.mjs → chunk-MVNN6LLO.mjs} +4 -3
- package/dist/chunk-MVNN6LLO.mjs.map +1 -0
- package/dist/{chunk-WSEQ2I6I.js → chunk-XIZE37IB.js} +4 -3
- package/dist/chunk-XIZE37IB.js.map +1 -0
- package/dist/{explore-avatar-live-config-DRpS-mqQ.d.ts → explore-avatar-live-config-C-OwXNje.d.ts} +8 -0
- package/dist/{explore-avatar-live-config-B6_p0q1c.d.mts → explore-avatar-live-config-OO0hvQCK.d.mts} +8 -0
- package/dist/index.d.mts +9 -4
- package/dist/index.d.ts +9 -4
- package/dist/index.js +9 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-7MSB3EUI.mjs.map +0 -1
- package/dist/chunk-WSEQ2I6I.js.map +0 -1
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { turnstileSiteKeyOf, isAvatarLiveLocaleReviewed, isAvatarSite, isAvatarVersion } from '../chunk-
|
|
1
|
+
import { turnstileSiteKeyOf, isAvatarLiveLocaleReviewed, isAvatarSite, isAvatarVersion } from '../chunk-MVNN6LLO.mjs';
|
|
2
2
|
import { answerDisplayText, fill } from '../chunk-JZS6FQT6.mjs';
|
|
3
3
|
import { cn } from '../chunk-R7G6TWIS.mjs';
|
|
4
4
|
import '../chunk-VNS5WDWO.mjs';
|
|
5
5
|
import { useId, useState, useRef, useCallback, useEffect } from 'react';
|
|
6
|
+
import { Square, Mic } from 'lucide-react';
|
|
6
7
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
7
8
|
|
|
8
9
|
// src/components/explore-avatar-api.ts
|
|
9
10
|
var AVAILABILITY_TIMEOUT_MS = 8e3;
|
|
10
11
|
var CREATE_TIMEOUT_MS = 3e4;
|
|
11
12
|
var MESSAGE_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
13
|
+
var STT_TOKEN = /^[A-Za-z0-9._~-]{1,4096}$/;
|
|
12
14
|
var CONVERSATION_TOKEN = /^[A-Za-z0-9._~+/=-]{16,512}$/;
|
|
13
15
|
var LOCALE = /^[A-Za-z0-9-]{1,16}$/;
|
|
14
16
|
var SESSION_ID = /^avs_[0-9a-f]{32}$/;
|
|
@@ -56,6 +58,15 @@ function readQuestions(value) {
|
|
|
56
58
|
if (typeof value !== "string" || !QUESTION_MODES.has(value)) return null;
|
|
57
59
|
return value === "any" ? "any" : "suggested";
|
|
58
60
|
}
|
|
61
|
+
function readSttOffer(value) {
|
|
62
|
+
if (!isRecord(value)) return void 0;
|
|
63
|
+
const keys = Object.keys(value).sort();
|
|
64
|
+
const { token, expiresAt } = value;
|
|
65
|
+
if (keys.length !== 2 || keys[0] !== "expiresAt" || keys[1] !== "token" || typeof token !== "string" || !STT_TOKEN.test(token) || typeof expiresAt !== "number" || !Number.isSafeInteger(expiresAt) || expiresAt <= 0) {
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
return { token, expiresAt };
|
|
69
|
+
}
|
|
59
70
|
function readAvailability(status, body) {
|
|
60
71
|
if (status !== 200 || !isRecord(body)) return INERT;
|
|
61
72
|
const { available, reason } = body;
|
|
@@ -69,9 +80,16 @@ function readAvailability(status, body) {
|
|
|
69
80
|
if (available !== true || typeof consentVersion !== "string" || !isAvatarVersion(consentVersion) || typeof maxSessionSeconds !== "number" || !Number.isInteger(maxSessionSeconds) || maxSessionSeconds < 1 || botCheck === null || questions === null) {
|
|
70
81
|
return INERT;
|
|
71
82
|
}
|
|
83
|
+
const stt = readSttOffer(body.stt);
|
|
72
84
|
return {
|
|
73
85
|
kind: "available",
|
|
74
|
-
offer: {
|
|
86
|
+
offer: {
|
|
87
|
+
consentVersion,
|
|
88
|
+
maxSessionSeconds,
|
|
89
|
+
botCheck,
|
|
90
|
+
questions,
|
|
91
|
+
...stt === void 0 ? {} : { stt }
|
|
92
|
+
}
|
|
75
93
|
};
|
|
76
94
|
}
|
|
77
95
|
const meaning = available ? void 0 : AVAILABILITY_REASONS.get(reason);
|
|
@@ -159,6 +177,13 @@ function deadline(timeoutMs, outer) {
|
|
|
159
177
|
};
|
|
160
178
|
}
|
|
161
179
|
async function requestAvailability(target) {
|
|
180
|
+
try {
|
|
181
|
+
return await requestAvailabilityAnswer(target);
|
|
182
|
+
} catch {
|
|
183
|
+
return INERT;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function requestAvailabilityAnswer(target) {
|
|
162
187
|
if (!isAvatarSite(target.site) || !LOCALE.test(target.locale)) return INERT;
|
|
163
188
|
const query = new URLSearchParams({
|
|
164
189
|
site: target.site,
|
|
@@ -174,8 +199,6 @@ async function requestAvailability(target) {
|
|
|
174
199
|
signal: timer.signal
|
|
175
200
|
});
|
|
176
201
|
return readAvailability(response.status, await readJson(response));
|
|
177
|
-
} catch {
|
|
178
|
-
return INERT;
|
|
179
202
|
} finally {
|
|
180
203
|
timer.done();
|
|
181
204
|
}
|
|
@@ -804,6 +827,318 @@ function avatarConsentCopy(version, locale) {
|
|
|
804
827
|
if (!isAvatarLiveLocaleReviewed(locale)) return null;
|
|
805
828
|
return CONSENT.get(version)?.[locale] ?? null;
|
|
806
829
|
}
|
|
830
|
+
var MIC_EN = {
|
|
831
|
+
micStart: "Ask by voice",
|
|
832
|
+
micStop: "Stop recording",
|
|
833
|
+
micAllow: "Use the microphone",
|
|
834
|
+
micNotNow: "Not now",
|
|
835
|
+
micStarting: "Starting the microphone\u2026",
|
|
836
|
+
micListening: "Listening\u2026",
|
|
837
|
+
micElapsed: "{elapsed} of {limit}",
|
|
838
|
+
micFinishing: "Getting the text\u2026",
|
|
839
|
+
micDone: "Check the text, then press Send.",
|
|
840
|
+
micDoneLimit: "The recording stopped at thirty seconds. Check the text, then press Send.",
|
|
841
|
+
micNoSpeech: "No words were heard. Try again, or type your question.",
|
|
842
|
+
micDenied: "The microphone is blocked in this browser. You can type your question instead.",
|
|
843
|
+
micUnsupported: "The microphone can't be used here. You can type your question instead.",
|
|
844
|
+
micUnavailable: "Voice questions aren't available right now. You can type your question instead.",
|
|
845
|
+
micLimit: "Voice questions have reached their limit for now. You can type your question instead.",
|
|
846
|
+
micBusy: "Voice questions are busy right now. Try again in a moment, or type your question.",
|
|
847
|
+
micFailed: "The microphone stopped before the text came. Try again, or type your question."
|
|
848
|
+
};
|
|
849
|
+
var MIC_STRINGS = { en: MIC_EN };
|
|
850
|
+
function avatarMicStrings(locale) {
|
|
851
|
+
if (!isAvatarLiveLocaleReviewed(locale)) return null;
|
|
852
|
+
return MIC_STRINGS[locale] ?? null;
|
|
853
|
+
}
|
|
854
|
+
var AVATAR_MIC_NOTICE_VERSION = "web-mic-2026-11-05";
|
|
855
|
+
var MIC_NOTICE = /* @__PURE__ */ new Map([
|
|
856
|
+
[
|
|
857
|
+
AVATAR_MIC_NOTICE_VERSION,
|
|
858
|
+
{
|
|
859
|
+
en: {
|
|
860
|
+
title: "Ask with your voice",
|
|
861
|
+
processing: "While you speak, your voice is streamed to our speech service, and Google Cloud Speech-to-Text turns it into text in Google's EU region.",
|
|
862
|
+
storage: "We don't store or log your audio, or the text made from it.",
|
|
863
|
+
text: "The text appears in the box and stays in this browser until you press Send. Nothing is sent by itself. Once you send it, it is a chat message, kept like any other.",
|
|
864
|
+
limits: "Each recording stops after thirty seconds, and voice questions have daily limits.",
|
|
865
|
+
privacyLink: "How we handle this"
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
]
|
|
869
|
+
]);
|
|
870
|
+
function avatarMicNotice(version, locale) {
|
|
871
|
+
if (!isAvatarLiveLocaleReviewed(locale)) return null;
|
|
872
|
+
return MIC_NOTICE.get(version)?.[locale] ?? null;
|
|
873
|
+
}
|
|
874
|
+
var PRIVACY_HREF = "/privacy#live-avatar";
|
|
875
|
+
var LIMIT_SECONDS = 30;
|
|
876
|
+
var TICK_MS = 250;
|
|
877
|
+
var IDLE = { name: "idle" };
|
|
878
|
+
var FOR_GOOD = /* @__PURE__ */ new Set([
|
|
879
|
+
"unavailable",
|
|
880
|
+
"limit"
|
|
881
|
+
]);
|
|
882
|
+
var FAILURE_TEXT = {
|
|
883
|
+
denied: "micDenied",
|
|
884
|
+
unsupported: "micUnsupported",
|
|
885
|
+
unavailable: "micUnavailable",
|
|
886
|
+
limit: "micLimit",
|
|
887
|
+
busy: "micBusy",
|
|
888
|
+
failed: "micFailed"
|
|
889
|
+
};
|
|
890
|
+
function loadAvatarMic() {
|
|
891
|
+
return import('../avatar-mic/index.mjs').then((loaded) => ({
|
|
892
|
+
createAvatarMic: loaded.createAvatarMic
|
|
893
|
+
}));
|
|
894
|
+
}
|
|
895
|
+
function canUseAvatarMic() {
|
|
896
|
+
return typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getUserMedia === "function" && typeof AudioContext !== "undefined" && typeof AudioWorkletNode !== "undefined" && typeof WebSocket !== "undefined";
|
|
897
|
+
}
|
|
898
|
+
function clock(seconds) {
|
|
899
|
+
const whole = Math.max(0, Math.floor(seconds));
|
|
900
|
+
return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, "0")}`;
|
|
901
|
+
}
|
|
902
|
+
function withHeardText(current, heard, maxLength) {
|
|
903
|
+
const text = heard.trim();
|
|
904
|
+
if (text === "") return current;
|
|
905
|
+
const joined = current.trim() === "" ? text : `${current.replace(/\s+$/, "")} ${text}`;
|
|
906
|
+
return joined.slice(0, Math.max(0, maxLength));
|
|
907
|
+
}
|
|
908
|
+
function useAvatarMic({
|
|
909
|
+
enabled,
|
|
910
|
+
locale,
|
|
911
|
+
base,
|
|
912
|
+
offer,
|
|
913
|
+
refreshOffer,
|
|
914
|
+
onText
|
|
915
|
+
}) {
|
|
916
|
+
const S = avatarMicStrings(locale);
|
|
917
|
+
const copy = avatarMicNotice(AVATAR_MIC_NOTICE_VERSION, locale);
|
|
918
|
+
const uid = useId();
|
|
919
|
+
const noticeTitleId = `${uid}-mic-notice`;
|
|
920
|
+
const [withdrawn, setWithdrawn] = useState(null);
|
|
921
|
+
const [supported, setSupported] = useState(false);
|
|
922
|
+
useEffect(() => setSupported(canUseAvatarMic()), []);
|
|
923
|
+
const possible = enabled && supported && base !== null && S !== null && copy !== null;
|
|
924
|
+
const offered = possible && withdrawn === null;
|
|
925
|
+
const [state, setState] = useState(IDLE);
|
|
926
|
+
const [noticeOpen, setNoticeOpen] = useState(false);
|
|
927
|
+
const agreedRef = useRef(false);
|
|
928
|
+
const [loading2, setLoading] = useState(false);
|
|
929
|
+
const [loadFailed, setLoadFailed] = useState(false);
|
|
930
|
+
const [now, setNow] = useState(() => Date.now());
|
|
931
|
+
const micRef = useRef(null);
|
|
932
|
+
const seqRef = useRef(0);
|
|
933
|
+
const buttonRef = useRef(null);
|
|
934
|
+
const allowRef = useRef(null);
|
|
935
|
+
const usedEpochRef = useRef(0);
|
|
936
|
+
const latest = useRef({ offer, refreshOffer, onText, base });
|
|
937
|
+
latest.current = { offer, refreshOffer, onText, base };
|
|
938
|
+
const cancel = useCallback(() => {
|
|
939
|
+
seqRef.current += 1;
|
|
940
|
+
setLoading(false);
|
|
941
|
+
setLoadFailed(false);
|
|
942
|
+
setNoticeOpen(false);
|
|
943
|
+
micRef.current?.mic.cancel();
|
|
944
|
+
}, []);
|
|
945
|
+
useEffect(() => {
|
|
946
|
+
if (!offered) cancel();
|
|
947
|
+
}, [offered, cancel]);
|
|
948
|
+
useEffect(
|
|
949
|
+
() => () => {
|
|
950
|
+
seqRef.current += 1;
|
|
951
|
+
micRef.current?.mic.cancel();
|
|
952
|
+
},
|
|
953
|
+
[]
|
|
954
|
+
);
|
|
955
|
+
const micFor = useCallback(async (target) => {
|
|
956
|
+
const held = micRef.current;
|
|
957
|
+
if (held !== null && held.base === target) return held.mic;
|
|
958
|
+
held?.mic.cancel();
|
|
959
|
+
const loaded = await loadAvatarMic();
|
|
960
|
+
const mic = loaded.createAvatarMic({
|
|
961
|
+
base: target,
|
|
962
|
+
notice: AVATAR_MIC_NOTICE_VERSION,
|
|
963
|
+
offer: () => latest.current.offer,
|
|
964
|
+
refreshOffer: () => latest.current.refreshOffer(),
|
|
965
|
+
onState: setState
|
|
966
|
+
});
|
|
967
|
+
micRef.current = { base: target, mic };
|
|
968
|
+
return mic;
|
|
969
|
+
}, []);
|
|
970
|
+
const press = useCallback(() => {
|
|
971
|
+
const target = latest.current.base;
|
|
972
|
+
if (target === null) return;
|
|
973
|
+
seqRef.current += 1;
|
|
974
|
+
const seq = seqRef.current;
|
|
975
|
+
setLoadFailed(false);
|
|
976
|
+
setLoading(true);
|
|
977
|
+
void micFor(target).then(
|
|
978
|
+
(mic) => {
|
|
979
|
+
if (seqRef.current !== seq) return;
|
|
980
|
+
setLoading(false);
|
|
981
|
+
mic.start();
|
|
982
|
+
},
|
|
983
|
+
() => {
|
|
984
|
+
if (seqRef.current !== seq) return;
|
|
985
|
+
setLoading(false);
|
|
986
|
+
setLoadFailed(true);
|
|
987
|
+
}
|
|
988
|
+
);
|
|
989
|
+
}, [micFor]);
|
|
990
|
+
const recording = loading2 || state.name === "starting" || state.name === "listening";
|
|
991
|
+
const tap = useCallback(() => {
|
|
992
|
+
if (state.name === "listening" || state.name === "starting") {
|
|
993
|
+
micRef.current?.mic.stop();
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
if (loading2) {
|
|
997
|
+
cancel();
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
if (!agreedRef.current) {
|
|
1001
|
+
setNoticeOpen(true);
|
|
1002
|
+
void loadAvatarMic().catch(() => void 0);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
press();
|
|
1006
|
+
}, [cancel, loading2, press, state.name]);
|
|
1007
|
+
const allow = useCallback(() => {
|
|
1008
|
+
agreedRef.current = true;
|
|
1009
|
+
setNoticeOpen(false);
|
|
1010
|
+
buttonRef.current?.focus();
|
|
1011
|
+
press();
|
|
1012
|
+
}, [press]);
|
|
1013
|
+
const notNow = useCallback(() => {
|
|
1014
|
+
setNoticeOpen(false);
|
|
1015
|
+
buttonRef.current?.focus();
|
|
1016
|
+
}, []);
|
|
1017
|
+
useEffect(() => {
|
|
1018
|
+
if (noticeOpen) allowRef.current?.focus();
|
|
1019
|
+
}, [noticeOpen]);
|
|
1020
|
+
const listeningSince = state.name === "listening" ? state.startedAt : null;
|
|
1021
|
+
useEffect(() => {
|
|
1022
|
+
if (listeningSince === null) return;
|
|
1023
|
+
setNow(Date.now());
|
|
1024
|
+
const timer = setInterval(() => setNow(Date.now()), TICK_MS);
|
|
1025
|
+
return () => clearInterval(timer);
|
|
1026
|
+
}, [listeningSince]);
|
|
1027
|
+
useEffect(() => {
|
|
1028
|
+
if (state.name === "done" && state.epoch !== usedEpochRef.current) {
|
|
1029
|
+
usedEpochRef.current = state.epoch;
|
|
1030
|
+
if (state.text !== "") latest.current.onText(state.text);
|
|
1031
|
+
} else if (state.name === "failed" && FOR_GOOD.has(state.reason)) {
|
|
1032
|
+
setWithdrawn(state.reason);
|
|
1033
|
+
}
|
|
1034
|
+
}, [state]);
|
|
1035
|
+
if (!possible || S === null || copy === null) {
|
|
1036
|
+
return { button: null, notice: null, lines: null, cancel };
|
|
1037
|
+
}
|
|
1038
|
+
const status = withdrawn !== null ? S[FAILURE_TEXT[withdrawn]] : 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]] : "";
|
|
1039
|
+
const heard = offered && (state.name === "listening" || state.name === "finishing") ? state.text : "";
|
|
1040
|
+
const button = !offered ? null : /* @__PURE__ */ jsx(
|
|
1041
|
+
"button",
|
|
1042
|
+
{
|
|
1043
|
+
ref: buttonRef,
|
|
1044
|
+
type: "button",
|
|
1045
|
+
"aria-label": recording ? S.micStop : S.micStart,
|
|
1046
|
+
"aria-pressed": recording,
|
|
1047
|
+
title: recording ? S.micStop : S.micStart,
|
|
1048
|
+
"data-boff-explore": "avatar-live-mic",
|
|
1049
|
+
"data-mic": loading2 ? "starting" : state.name,
|
|
1050
|
+
className: cn(
|
|
1051
|
+
"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",
|
|
1052
|
+
recording ? "border-primary bg-primary text-primary-foreground" : "border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground"
|
|
1053
|
+
),
|
|
1054
|
+
onClick: tap,
|
|
1055
|
+
children: recording ? /* @__PURE__ */ jsx(Square, { "aria-hidden": "true", className: "h-4 w-4" }) : /* @__PURE__ */ jsx(Mic, { "aria-hidden": "true", className: "h-4 w-4" })
|
|
1056
|
+
}
|
|
1057
|
+
);
|
|
1058
|
+
const notice = offered && noticeOpen ? /* @__PURE__ */ jsxs(
|
|
1059
|
+
"section",
|
|
1060
|
+
{
|
|
1061
|
+
"aria-labelledby": noticeTitleId,
|
|
1062
|
+
"data-boff-explore": "avatar-live-mic-notice",
|
|
1063
|
+
className: "w-full rounded-lg border border-border p-3 text-start",
|
|
1064
|
+
children: [
|
|
1065
|
+
/* @__PURE__ */ jsx("h3", { id: noticeTitleId, className: "text-sm font-semibold", children: copy.title }),
|
|
1066
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: copy.processing }),
|
|
1067
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: copy.storage }),
|
|
1068
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: copy.text }),
|
|
1069
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: copy.limits }),
|
|
1070
|
+
/* @__PURE__ */ jsx(
|
|
1071
|
+
"a",
|
|
1072
|
+
{
|
|
1073
|
+
href: PRIVACY_HREF,
|
|
1074
|
+
target: "_blank",
|
|
1075
|
+
rel: "noopener noreferrer",
|
|
1076
|
+
"data-boff-explore": "avatar-live-mic-privacy",
|
|
1077
|
+
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",
|
|
1078
|
+
children: copy.privacyLink
|
|
1079
|
+
}
|
|
1080
|
+
),
|
|
1081
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap gap-2", children: [
|
|
1082
|
+
/* @__PURE__ */ jsx(
|
|
1083
|
+
"button",
|
|
1084
|
+
{
|
|
1085
|
+
ref: allowRef,
|
|
1086
|
+
type: "button",
|
|
1087
|
+
"data-boff-explore": "avatar-live-mic-allow",
|
|
1088
|
+
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",
|
|
1089
|
+
onClick: allow,
|
|
1090
|
+
children: S.micAllow
|
|
1091
|
+
}
|
|
1092
|
+
),
|
|
1093
|
+
/* @__PURE__ */ jsx(
|
|
1094
|
+
"button",
|
|
1095
|
+
{
|
|
1096
|
+
type: "button",
|
|
1097
|
+
"data-boff-explore": "avatar-live-mic-not-now",
|
|
1098
|
+
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",
|
|
1099
|
+
onClick: notNow,
|
|
1100
|
+
children: S.micNotNow
|
|
1101
|
+
}
|
|
1102
|
+
)
|
|
1103
|
+
] })
|
|
1104
|
+
]
|
|
1105
|
+
}
|
|
1106
|
+
) : null;
|
|
1107
|
+
const listening = offered && state.name === "listening";
|
|
1108
|
+
const lines = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1109
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
|
|
1110
|
+
listening ? /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: "relative flex h-2 w-2 shrink-0", children: [
|
|
1111
|
+
/* @__PURE__ */ jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75 motion-reduce:animate-none" }),
|
|
1112
|
+
/* @__PURE__ */ jsx("span", { className: "relative inline-flex h-2 w-2 rounded-full bg-primary" })
|
|
1113
|
+
] }) : null,
|
|
1114
|
+
/* @__PURE__ */ jsx("p", { role: "status", "data-boff-explore": "avatar-live-mic-status", children: status }),
|
|
1115
|
+
listening ? /* @__PURE__ */ jsx(
|
|
1116
|
+
"span",
|
|
1117
|
+
{
|
|
1118
|
+
role: "timer",
|
|
1119
|
+
"data-boff-explore": "avatar-live-mic-elapsed",
|
|
1120
|
+
className: "tabular-nums",
|
|
1121
|
+
children: fill(S.micElapsed, {
|
|
1122
|
+
elapsed: clock(
|
|
1123
|
+
Math.min(LIMIT_SECONDS, (now - (listeningSince ?? now)) / 1e3)
|
|
1124
|
+
),
|
|
1125
|
+
limit: clock(LIMIT_SECONDS)
|
|
1126
|
+
})
|
|
1127
|
+
}
|
|
1128
|
+
) : null
|
|
1129
|
+
] }),
|
|
1130
|
+
heard === "" ? null : /* @__PURE__ */ jsx(
|
|
1131
|
+
"p",
|
|
1132
|
+
{
|
|
1133
|
+
"data-boff-explore": "avatar-live-mic-heard",
|
|
1134
|
+
dir: "auto",
|
|
1135
|
+
className: "line-clamp-3 break-words text-sm italic text-muted-foreground",
|
|
1136
|
+
children: heard
|
|
1137
|
+
}
|
|
1138
|
+
)
|
|
1139
|
+
] });
|
|
1140
|
+
return { button, notice, lines, cancel };
|
|
1141
|
+
}
|
|
807
1142
|
|
|
808
1143
|
// src/components/explore-avatar-turnstile.ts
|
|
809
1144
|
var SCRIPT_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
|
|
@@ -919,7 +1254,7 @@ function runTurnstile(container, siteKey, action, cData) {
|
|
|
919
1254
|
}
|
|
920
1255
|
};
|
|
921
1256
|
}
|
|
922
|
-
var
|
|
1257
|
+
var PRIVACY_HREF2 = "/privacy#live-avatar";
|
|
923
1258
|
var SPEAKER_WAIT_MS = 3e4;
|
|
924
1259
|
var END_GRACE_MS = 1e4;
|
|
925
1260
|
var CLOCK_TOLERANCE_MS = 5e3;
|
|
@@ -932,7 +1267,7 @@ var ASK_MAX_LENGTH = 300;
|
|
|
932
1267
|
var MAX_REFERENCES = 64;
|
|
933
1268
|
var USED_ON_KEY = "boff.explore.avatarUsedOn";
|
|
934
1269
|
var PRIMARY = '[data-avatar-primary], [data-boff-explore="panel-continue-chat"]';
|
|
935
|
-
var
|
|
1270
|
+
var IDLE2 = { name: "idle" };
|
|
936
1271
|
var INERT2 = { name: "inert" };
|
|
937
1272
|
function stopAttending(handle) {
|
|
938
1273
|
if (handle.unheard !== null) clearTimeout(handle.unheard);
|
|
@@ -1022,6 +1357,12 @@ function firstExchange(messages, rules = SUGGESTED_RULES) {
|
|
|
1022
1357
|
function inFlight(conversation) {
|
|
1023
1358
|
return conversation?.phase === "sending" || conversation?.phase === "streaming";
|
|
1024
1359
|
}
|
|
1360
|
+
function acceptsQuestion(slot) {
|
|
1361
|
+
return slot.any && !slot.textMode && slot.phase === "needs-answer" && slot.kind === "empty" && slot.reason === null;
|
|
1362
|
+
}
|
|
1363
|
+
function micOfferOf(result) {
|
|
1364
|
+
return result.kind === "available" && result.offer.stt !== void 0 ? { ...result.offer.stt, receivedAt: Date.now() } : null;
|
|
1365
|
+
}
|
|
1025
1366
|
function utcToday() {
|
|
1026
1367
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1027
1368
|
}
|
|
@@ -1091,7 +1432,7 @@ async function loadModule(load) {
|
|
|
1091
1432
|
}
|
|
1092
1433
|
return loaded;
|
|
1093
1434
|
}
|
|
1094
|
-
function
|
|
1435
|
+
function clock2(seconds) {
|
|
1095
1436
|
const whole = Math.max(0, Math.ceil(seconds));
|
|
1096
1437
|
return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, "0")}`;
|
|
1097
1438
|
}
|
|
@@ -1144,7 +1485,7 @@ function AvatarLiveView({
|
|
|
1144
1485
|
const shown = offeredAt(exchange, Date.now());
|
|
1145
1486
|
const { answerId, startOver } = shown;
|
|
1146
1487
|
const offerUntil = answerId === null ? null : exchange.offerUntil;
|
|
1147
|
-
const [phase, setPhase] = useState(
|
|
1488
|
+
const [phase, setPhase] = useState(IDLE2);
|
|
1148
1489
|
const [blocked, setBlocked] = useState(false);
|
|
1149
1490
|
const [now, setNow] = useState(() => Date.now());
|
|
1150
1491
|
const [draft, setDraft] = useState("");
|
|
@@ -1154,7 +1495,7 @@ function AvatarLiveView({
|
|
|
1154
1495
|
const [usedToday, setUsedToday] = useState(false);
|
|
1155
1496
|
const showing = active && !off;
|
|
1156
1497
|
const behindChat = open && !active && !off;
|
|
1157
|
-
const phaseRef = useRef(
|
|
1498
|
+
const phaseRef = useRef(IDLE2);
|
|
1158
1499
|
const latest = useRef({
|
|
1159
1500
|
live,
|
|
1160
1501
|
locale,
|
|
@@ -1170,6 +1511,8 @@ function AvatarLiveView({
|
|
|
1170
1511
|
const waitRef = useRef(null);
|
|
1171
1512
|
const sendingRef = useRef(false);
|
|
1172
1513
|
const textBoxRef = useRef(false);
|
|
1514
|
+
const [micOffer, setMicOffer] = useState(null);
|
|
1515
|
+
const micCancelRef = useRef(() => void 0);
|
|
1173
1516
|
const checkRef = useRef(null);
|
|
1174
1517
|
const askedLocale = useRef(locale);
|
|
1175
1518
|
const startRef = useRef(null);
|
|
@@ -1290,6 +1633,7 @@ function AvatarLiveView({
|
|
|
1290
1633
|
const applyAvailability = useCallback(
|
|
1291
1634
|
(result, locale2) => {
|
|
1292
1635
|
const { live: live2 } = latest.current;
|
|
1636
|
+
setMicOffer(micOfferOf(result));
|
|
1293
1637
|
if (result.kind === "inert") {
|
|
1294
1638
|
setMode("suggested");
|
|
1295
1639
|
go(INERT2);
|
|
@@ -1605,6 +1949,7 @@ function AvatarLiveView({
|
|
|
1605
1949
|
[applyAvailability, bindingHolds, cancelWaiting, exchangeNow, launch]
|
|
1606
1950
|
);
|
|
1607
1951
|
const stop = useCallback(() => {
|
|
1952
|
+
micCancelRef.current();
|
|
1608
1953
|
const current = phaseRef.current;
|
|
1609
1954
|
if (current.name === "waiting") {
|
|
1610
1955
|
cancelWaiting(false);
|
|
@@ -1626,6 +1971,7 @@ function AvatarLiveView({
|
|
|
1626
1971
|
}, [cancelStart, cancelWaiting, finish, go, reoffer]);
|
|
1627
1972
|
const leave = useCallback(
|
|
1628
1973
|
(event) => {
|
|
1974
|
+
micCancelRef.current();
|
|
1629
1975
|
const current = phaseRef.current;
|
|
1630
1976
|
const reason = event === "user" || event === "chat" ? "user" : "page_hide";
|
|
1631
1977
|
const how = reason === "user" ? "stopped" : "interrupted";
|
|
@@ -1636,7 +1982,7 @@ function AvatarLiveView({
|
|
|
1636
1982
|
const controller = checkRef.current;
|
|
1637
1983
|
checkRef.current = null;
|
|
1638
1984
|
controller?.abort();
|
|
1639
|
-
go(
|
|
1985
|
+
go(IDLE2);
|
|
1640
1986
|
return;
|
|
1641
1987
|
}
|
|
1642
1988
|
case "waiting":
|
|
@@ -1644,7 +1990,7 @@ function AvatarLiveView({
|
|
|
1644
1990
|
return;
|
|
1645
1991
|
case "bot-check":
|
|
1646
1992
|
cancelStart(reason);
|
|
1647
|
-
go(reason === "user" ?
|
|
1993
|
+
go(reason === "user" ? IDLE2 : reoffer(current.offer));
|
|
1648
1994
|
return;
|
|
1649
1995
|
case "creating":
|
|
1650
1996
|
if (keepsSession) return;
|
|
@@ -1706,7 +2052,7 @@ function AvatarLiveView({
|
|
|
1706
2052
|
} else {
|
|
1707
2053
|
checkRef.current?.abort();
|
|
1708
2054
|
checkRef.current = null;
|
|
1709
|
-
go(
|
|
2055
|
+
go(IDLE2);
|
|
1710
2056
|
}
|
|
1711
2057
|
}, [active, cancelWaiting, check, go, locale]);
|
|
1712
2058
|
const kind = shown.kind;
|
|
@@ -1951,6 +2297,46 @@ function AvatarLiveView({
|
|
|
1951
2297
|
const textMode = any && (usedToday && (phase.name === "needs-answer" || phase.name === "consent" || phase.name === "declined" || phase.name === "ended") || phase.name === "refused" && phase.reason === "limits");
|
|
1952
2298
|
textBoxRef.current = textMode;
|
|
1953
2299
|
const askState = !any ? void 0 : textMode ? "text" : phase.name === "needs-answer" ? kind === "empty" ? "ready" : kind : phase.name;
|
|
2300
|
+
const askReason = inFlight(conversation) ? S.askBusy : conversation?.canSend ? null : S.askUnavailable;
|
|
2301
|
+
const refreshMicOffer = useCallback(async () => {
|
|
2302
|
+
const { live: live2, locale: locale2, base: base2, headers: headers2 } = latest.current;
|
|
2303
|
+
if (base2 === null) return null;
|
|
2304
|
+
const next = micOfferOf(
|
|
2305
|
+
await requestAvailabilityAnswer({
|
|
2306
|
+
base: base2,
|
|
2307
|
+
headers: headers2,
|
|
2308
|
+
site: live2.site,
|
|
2309
|
+
locale: locale2
|
|
2310
|
+
})
|
|
2311
|
+
);
|
|
2312
|
+
if (next !== null) setMicOffer(next);
|
|
2313
|
+
return next;
|
|
2314
|
+
}, []);
|
|
2315
|
+
const onMicText = useCallback(
|
|
2316
|
+
(text) => {
|
|
2317
|
+
setDraft((current) => withHeardText(current, text, askMaxLength));
|
|
2318
|
+
if (touchFirst) return;
|
|
2319
|
+
rootRef.current?.querySelector(
|
|
2320
|
+
'[data-boff-explore="avatar-live-ask-input"]'
|
|
2321
|
+
)?.focus();
|
|
2322
|
+
},
|
|
2323
|
+
[askMaxLength, touchFirst]
|
|
2324
|
+
);
|
|
2325
|
+
const mic = useAvatarMic({
|
|
2326
|
+
enabled: live.mic === true && micOffer !== null && !off && acceptsQuestion({
|
|
2327
|
+
any,
|
|
2328
|
+
phase: phase.name,
|
|
2329
|
+
kind,
|
|
2330
|
+
textMode,
|
|
2331
|
+
reason: askReason
|
|
2332
|
+
}),
|
|
2333
|
+
locale,
|
|
2334
|
+
base,
|
|
2335
|
+
offer: micOffer,
|
|
2336
|
+
refreshOffer: refreshMicOffer,
|
|
2337
|
+
onText: onMicText
|
|
2338
|
+
});
|
|
2339
|
+
micCancelRef.current = mic.cancel;
|
|
1954
2340
|
const notNow = useCallback(() => {
|
|
1955
2341
|
const current = phaseRef.current;
|
|
1956
2342
|
if ((current.name === "consent" || current.name === "needs-answer") && current.offer.questions === "any") {
|
|
@@ -2119,7 +2505,7 @@ function AvatarLiveView({
|
|
|
2119
2505
|
/* @__PURE__ */ jsxs(
|
|
2120
2506
|
"a",
|
|
2121
2507
|
{
|
|
2122
|
-
href:
|
|
2508
|
+
href: PRIVACY_HREF2,
|
|
2123
2509
|
target: "_blank",
|
|
2124
2510
|
rel: "noopener noreferrer",
|
|
2125
2511
|
"data-boff-explore": "avatar-live-privacy",
|
|
@@ -2211,7 +2597,7 @@ function AvatarLiveView({
|
|
|
2211
2597
|
/* @__PURE__ */ jsxs("p", { className: "text-sm tabular-nums text-muted-foreground", children: [
|
|
2212
2598
|
S.timeLeft,
|
|
2213
2599
|
" ",
|
|
2214
|
-
/* @__PURE__ */ jsx("span", { role: "timer", "data-boff-explore": "avatar-live-countdown", children:
|
|
2600
|
+
/* @__PURE__ */ jsx("span", { role: "timer", "data-boff-explore": "avatar-live-countdown", children: clock2(Math.max(0, (session.endsAt - now) / 1e3)) })
|
|
2215
2601
|
] })
|
|
2216
2602
|
] });
|
|
2217
2603
|
const liveActions = (back) => /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [
|
|
@@ -2428,7 +2814,7 @@ function AvatarLiveView({
|
|
|
2428
2814
|
break;
|
|
2429
2815
|
}
|
|
2430
2816
|
}
|
|
2431
|
-
const reason =
|
|
2817
|
+
const reason = askReason;
|
|
2432
2818
|
const chips = box !== null && shown.kind === "empty" && prompts.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
2433
2819
|
/* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground", children: tryAsking }),
|
|
2434
2820
|
/* @__PURE__ */ jsx("div", { className: "flex max-h-28 flex-wrap gap-2 overflow-y-auto", children: prompts.map((prompt) => /* @__PURE__ */ jsx(
|
|
@@ -2475,6 +2861,7 @@ function AvatarLiveView({
|
|
|
2475
2861
|
children: S.usedToday
|
|
2476
2862
|
}
|
|
2477
2863
|
) : null,
|
|
2864
|
+
box === "spoken" ? mic.notice : null,
|
|
2478
2865
|
box === null ? null : /* @__PURE__ */ jsx(
|
|
2479
2866
|
AskBox,
|
|
2480
2867
|
{
|
|
@@ -2489,7 +2876,9 @@ function AvatarLiveView({
|
|
|
2489
2876
|
footer: S.askFooter,
|
|
2490
2877
|
noFiles: S.askNoFiles,
|
|
2491
2878
|
hint: box === "spoken" ? S.needsAnswerAny : null,
|
|
2492
|
-
touchFirst
|
|
2879
|
+
touchFirst,
|
|
2880
|
+
mic: box === "spoken" ? mic.button : null,
|
|
2881
|
+
micLines: box === "spoken" ? mic.lines : null
|
|
2493
2882
|
}
|
|
2494
2883
|
)
|
|
2495
2884
|
]
|
|
@@ -2636,7 +3025,9 @@ function AskBox({
|
|
|
2636
3025
|
footer,
|
|
2637
3026
|
noFiles,
|
|
2638
3027
|
hint,
|
|
2639
|
-
touchFirst
|
|
3028
|
+
touchFirst,
|
|
3029
|
+
mic,
|
|
3030
|
+
micLines
|
|
2640
3031
|
}) {
|
|
2641
3032
|
const uid = useId();
|
|
2642
3033
|
const reasonId = `${uid}-reason`;
|
|
@@ -2701,6 +3092,7 @@ function AskBox({
|
|
|
2701
3092
|
onDrop
|
|
2702
3093
|
}
|
|
2703
3094
|
),
|
|
3095
|
+
mic,
|
|
2704
3096
|
/* @__PURE__ */ jsx(
|
|
2705
3097
|
"button",
|
|
2706
3098
|
{
|
|
@@ -2714,6 +3106,7 @@ function AskBox({
|
|
|
2714
3106
|
}
|
|
2715
3107
|
)
|
|
2716
3108
|
] }),
|
|
3109
|
+
micLines,
|
|
2717
3110
|
reason === null ? null : /* @__PURE__ */ jsx(
|
|
2718
3111
|
"p",
|
|
2719
3112
|
{
|