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