@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,14 +1,16 @@
1
- import { turnstileSiteKeyOf, isAvatarLiveLocaleReviewed, isAvatarSite, isAvatarVersion } from '../chunk-7MSB3EUI.mjs';
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: { consentVersion, maxSessionSeconds, botCheck, questions }
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 PRIVACY_HREF = "/privacy#live-avatar";
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 IDLE = { name: "idle" };
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 clock(seconds) {
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(IDLE);
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(IDLE);
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(IDLE);
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" ? IDLE : reoffer(current.offer));
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(IDLE);
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: PRIVACY_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: clock(Math.max(0, (session.endsAt - now) / 1e3)) })
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 = inFlight(conversation) ? S.askBusy : conversation?.canSend ? null : S.askUnavailable;
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
  {