@burdenoff/website-sdk 2026.1104.3 → 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.
Files changed (48) hide show
  1. package/README.md +51 -6
  2. package/dist/avatar-live-view/index.d.mts +1 -1
  3. package/dist/avatar-live-view/index.d.ts +1 -1
  4. package/dist/avatar-live-view/index.js +482 -64
  5. package/dist/avatar-live-view/index.js.map +1 -1
  6. package/dist/avatar-live-view/index.mjs +476 -58
  7. package/dist/avatar-live-view/index.mjs.map +1 -1
  8. package/dist/avatar-mic/index.d.mts +193 -0
  9. package/dist/avatar-mic/index.d.ts +193 -0
  10. package/dist/avatar-mic/index.js +738 -0
  11. package/dist/avatar-mic/index.js.map +1 -0
  12. package/dist/avatar-mic/index.mjs +736 -0
  13. package/dist/avatar-mic/index.mjs.map +1 -0
  14. package/dist/{chunk-5JQGE3XI.mjs → chunk-7HE33NBS.mjs} +4 -111
  15. package/dist/chunk-7HE33NBS.mjs.map +1 -0
  16. package/dist/chunk-JZS6FQT6.mjs +127 -0
  17. package/dist/chunk-JZS6FQT6.mjs.map +1 -0
  18. package/dist/{chunk-22OT6E26.js → chunk-MBVXDFVC.js} +18 -127
  19. package/dist/chunk-MBVXDFVC.js.map +1 -0
  20. package/dist/{chunk-7MSB3EUI.mjs → chunk-MVNN6LLO.mjs} +4 -3
  21. package/dist/chunk-MVNN6LLO.mjs.map +1 -0
  22. package/dist/chunk-N7E6Z2GF.js +132 -0
  23. package/dist/chunk-N7E6Z2GF.js.map +1 -0
  24. package/dist/{chunk-WSEQ2I6I.js → chunk-XIZE37IB.js} +4 -3
  25. package/dist/chunk-XIZE37IB.js.map +1 -0
  26. package/dist/components/explore.d.mts +1 -1
  27. package/dist/components/explore.d.ts +1 -1
  28. package/dist/components/explore.js +20 -20
  29. package/dist/components/explore.mjs +2 -2
  30. package/dist/{explore-BV4dRwoC.d.ts → explore-Dfru4j4m.d.ts} +45 -31
  31. package/dist/{explore-avatar-live-config-DRpS-mqQ.d.ts → explore-avatar-live-config-C-OwXNje.d.ts} +8 -0
  32. package/dist/{explore-avatar-live-config-B6_p0q1c.d.mts → explore-avatar-live-config-OO0hvQCK.d.mts} +8 -0
  33. package/dist/{explore-BgEeGPvY.d.mts → explore-ei8uoko9.d.mts} +45 -31
  34. package/dist/index.d.mts +11 -6
  35. package/dist/index.d.ts +11 -6
  36. package/dist/index.js +70 -68
  37. package/dist/index.js.map +1 -1
  38. package/dist/index.mjs +9 -6
  39. package/dist/index.mjs.map +1 -1
  40. package/package.json +1 -1
  41. package/dist/chunk-22OT6E26.js.map +0 -1
  42. package/dist/chunk-5JQGE3XI.mjs.map +0 -1
  43. package/dist/chunk-7MSB3EUI.mjs.map +0 -1
  44. package/dist/chunk-O4GS3XPZ.js +0 -10
  45. package/dist/chunk-O4GS3XPZ.js.map +0 -1
  46. package/dist/chunk-TT3MHN42.mjs +0 -8
  47. package/dist/chunk-TT3MHN42.mjs.map +0 -1
  48. package/dist/chunk-WSEQ2I6I.js.map +0 -1
@@ -1,16 +1,18 @@
1
1
  'use strict';
2
2
 
3
- var chunkWSEQ2I6I_js = require('../chunk-WSEQ2I6I.js');
4
- var chunkO4GS3XPZ_js = require('../chunk-O4GS3XPZ.js');
3
+ var chunkXIZE37IB_js = require('../chunk-XIZE37IB.js');
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,13 +201,11 @@ 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
  }
184
207
  }
185
- async function requestCreate(target) {
208
+ function requestCreate(target) {
186
209
  const { body } = target;
187
210
  const payload = JSON.stringify({
188
211
  site: body.site,
@@ -192,27 +215,45 @@ async function requestCreate(target) {
192
215
  consentVersion: body.consentVersion,
193
216
  manifestVersion: body.manifestVersion
194
217
  });
195
- const timer = deadline(CREATE_TIMEOUT_MS);
196
- try {
197
- const response = await fetch(`${target.base}/sessions`, {
198
- method: "POST",
199
- credentials: "omit",
200
- cache: "no-store",
201
- headers: {
202
- ...target.headers,
203
- "Content-Type": "application/json",
204
- "Idempotency-Key": target.idempotencyKey,
205
- ...target.botToken === void 0 ? {} : { "x-web-avatar-bot-token": target.botToken }
206
- },
207
- body: payload,
208
- signal: timer.signal
218
+ const controller = new AbortController();
219
+ let answered = false;
220
+ const answer = (async () => {
221
+ try {
222
+ const response = await fetch(`${target.base}/sessions`, {
223
+ method: "POST",
224
+ credentials: "omit",
225
+ cache: "no-store",
226
+ headers: {
227
+ ...target.headers,
228
+ "Content-Type": "application/json",
229
+ "Idempotency-Key": target.idempotencyKey,
230
+ ...target.botToken === void 0 ? {} : { "x-web-avatar-bot-token": target.botToken }
231
+ },
232
+ body: payload,
233
+ signal: controller.signal
234
+ });
235
+ answered = true;
236
+ return readCreate(response.status, await readJson(response));
237
+ } catch {
238
+ return FAILED;
239
+ }
240
+ })();
241
+ return new Promise((resolve) => {
242
+ let late = false;
243
+ const timer = setTimeout(() => {
244
+ late = true;
245
+ if (!answered) controller.abort();
246
+ resolve(FAILED);
247
+ }, CREATE_TIMEOUT_MS);
248
+ void answer.then((result) => {
249
+ if (late) {
250
+ target.onLate?.(result);
251
+ return;
252
+ }
253
+ clearTimeout(timer);
254
+ resolve(result);
209
255
  });
210
- return readCreate(response.status, await readJson(response));
211
- } catch {
212
- return INERT;
213
- } finally {
214
- timer.done();
215
- }
256
+ });
216
257
  }
217
258
  function sendEnd(target) {
218
259
  try {
@@ -785,9 +826,321 @@ var CONSENT = /* @__PURE__ */ new Map([
785
826
  ]
786
827
  ]);
787
828
  function avatarConsentCopy(version, locale) {
788
- if (!chunkWSEQ2I6I_js.isAvatarLiveLocaleReviewed(locale)) return null;
829
+ if (!chunkXIZE37IB_js.isAvatarLiveLocaleReviewed(locale)) return null;
789
830
  return CONSENT.get(version)?.[locale] ?? null;
790
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
+ }
791
1144
 
792
1145
  // src/components/explore-avatar-turnstile.ts
793
1146
  var SCRIPT_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
@@ -903,7 +1256,7 @@ function runTurnstile(container, siteKey, action, cData) {
903
1256
  }
904
1257
  };
905
1258
  }
906
- var PRIVACY_HREF = "/privacy#live-avatar";
1259
+ var PRIVACY_HREF2 = "/privacy#live-avatar";
907
1260
  var SPEAKER_WAIT_MS = 3e4;
908
1261
  var END_GRACE_MS = 1e4;
909
1262
  var CLOCK_TOLERANCE_MS = 5e3;
@@ -916,7 +1269,7 @@ var ASK_MAX_LENGTH = 300;
916
1269
  var MAX_REFERENCES = 64;
917
1270
  var USED_ON_KEY = "boff.explore.avatarUsedOn";
918
1271
  var PRIMARY = '[data-avatar-primary], [data-boff-explore="panel-continue-chat"]';
919
- var IDLE = { name: "idle" };
1272
+ var IDLE2 = { name: "idle" };
920
1273
  var INERT2 = { name: "inert" };
921
1274
  function stopAttending(handle) {
922
1275
  if (handle.unheard !== null) clearTimeout(handle.unheard);
@@ -1006,6 +1359,12 @@ function firstExchange(messages, rules = SUGGESTED_RULES) {
1006
1359
  function inFlight(conversation) {
1007
1360
  return conversation?.phase === "sending" || conversation?.phase === "streaming";
1008
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
+ }
1009
1368
  function utcToday() {
1010
1369
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1011
1370
  }
@@ -1075,7 +1434,7 @@ async function loadModule(load) {
1075
1434
  }
1076
1435
  return loaded;
1077
1436
  }
1078
- function clock(seconds) {
1437
+ function clock2(seconds) {
1079
1438
  const whole = Math.max(0, Math.ceil(seconds));
1080
1439
  return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, "0")}`;
1081
1440
  }
@@ -1128,7 +1487,7 @@ function AvatarLiveView({
1128
1487
  const shown = offeredAt(exchange, Date.now());
1129
1488
  const { answerId, startOver } = shown;
1130
1489
  const offerUntil = answerId === null ? null : exchange.offerUntil;
1131
- const [phase, setPhase] = react.useState(IDLE);
1490
+ const [phase, setPhase] = react.useState(IDLE2);
1132
1491
  const [blocked, setBlocked] = react.useState(false);
1133
1492
  const [now, setNow] = react.useState(() => Date.now());
1134
1493
  const [draft, setDraft] = react.useState("");
@@ -1138,7 +1497,7 @@ function AvatarLiveView({
1138
1497
  const [usedToday, setUsedToday] = react.useState(false);
1139
1498
  const showing = active && !off;
1140
1499
  const behindChat = open && !active && !off;
1141
- const phaseRef = react.useRef(IDLE);
1500
+ const phaseRef = react.useRef(IDLE2);
1142
1501
  const latest = react.useRef({
1143
1502
  live,
1144
1503
  locale,
@@ -1154,6 +1513,8 @@ function AvatarLiveView({
1154
1513
  const waitRef = react.useRef(null);
1155
1514
  const sendingRef = react.useRef(false);
1156
1515
  const textBoxRef = react.useRef(false);
1516
+ const [micOffer, setMicOffer] = react.useState(null);
1517
+ const micCancelRef = react.useRef(() => void 0);
1157
1518
  const checkRef = react.useRef(null);
1158
1519
  const askedLocale = react.useRef(locale);
1159
1520
  const startRef = react.useRef(null);
@@ -1274,6 +1635,7 @@ function AvatarLiveView({
1274
1635
  const applyAvailability = react.useCallback(
1275
1636
  (result, locale2) => {
1276
1637
  const { live: live2 } = latest.current;
1638
+ setMicOffer(micOfferOf(result));
1277
1639
  if (result.kind === "inert") {
1278
1640
  setMode("suggested");
1279
1641
  go(INERT2);
@@ -1291,7 +1653,7 @@ function AvatarLiveView({
1291
1653
  go(INERT2);
1292
1654
  return;
1293
1655
  }
1294
- if (result.offer.botCheck === "turnstile" && chunkWSEQ2I6I_js.turnstileSiteKeyOf(live2) === null) {
1656
+ if (result.offer.botCheck === "turnstile" && chunkXIZE37IB_js.turnstileSiteKeyOf(live2) === null) {
1295
1657
  setMode("suggested");
1296
1658
  go(INERT2);
1297
1659
  return;
@@ -1361,21 +1723,21 @@ function AvatarLiveView({
1361
1723
  [go]
1362
1724
  );
1363
1725
  const settle = react.useCallback(
1364
- (result, run, context) => {
1726
+ (result, givenUp, context) => {
1365
1727
  const created = result.kind === "created" ? {
1366
1728
  sessionId: result.session.sessionId,
1367
1729
  secret: result.session.endSecret
1368
1730
  } : result.kind === "unusable" ? { sessionId: result.sessionId, secret: result.endSecret } : null;
1369
1731
  const claimed = created !== null || result.kind === "refused" && result.reason === "answer_claimed";
1370
1732
  if (claimed) claimedRef.current.add(context.messageId);
1371
- if (run.cancelled !== null) {
1733
+ if (givenUp !== null) {
1372
1734
  if (created !== null) {
1373
1735
  sendEnd({
1374
1736
  base: context.base,
1375
1737
  headers: context.headers,
1376
1738
  sessionId: created.sessionId,
1377
1739
  secret: created.secret,
1378
- reason: run.cancelled
1740
+ reason: givenUp
1379
1741
  });
1380
1742
  }
1381
1743
  const shown2 = phaseRef.current;
@@ -1428,7 +1790,7 @@ function AvatarLiveView({
1428
1790
  return;
1429
1791
  }
1430
1792
  const refusedFrom = exchangeAtStart.offerUntil - ANSWER_OFFER_MS + SERVICE_MAX_AGE_MS;
1431
- const siteKey = offer.botCheck === "turnstile" ? chunkWSEQ2I6I_js.turnstileSiteKeyOf(live2) : null;
1793
+ const siteKey = offer.botCheck === "turnstile" ? chunkXIZE37IB_js.turnstileSiteKeyOf(live2) : null;
1432
1794
  if (offer.botCheck === "turnstile" && (siteKey === null || !canBindBotCheck())) {
1433
1795
  go(INERT2);
1434
1796
  return;
@@ -1484,21 +1846,21 @@ function AvatarLiveView({
1484
1846
  return;
1485
1847
  }
1486
1848
  if (turnstile !== null) go({ name: "creating" });
1849
+ const context = { module: loaded, offer, messageId, base: base2, headers: headers2 };
1487
1850
  const result = await requestCreate({
1488
1851
  base: base2,
1489
1852
  headers: headers2,
1490
1853
  idempotencyKey,
1491
1854
  botToken,
1492
- body: body2
1855
+ body: body2,
1856
+ // An answer past the deadline, after the view has said the live avatar couldn't start: a session it made is
1857
+ // ended at once with its own secret, and never joined. The reason is the visitor's own where they gave the
1858
+ // start up first, else `client_error`. It reads `run` and never writes it, so the deadline's `failed` below is
1859
+ // shown whichever of the two is handled first.
1860
+ onLate: (late) => settle(late, run.cancelled ?? "client_error", context)
1493
1861
  });
1494
1862
  if (startRef.current === run) startRef.current = null;
1495
- settle(result, run, {
1496
- module: loaded,
1497
- offer,
1498
- messageId,
1499
- base: base2,
1500
- headers: headers2
1501
- });
1863
+ settle(result, run.cancelled, context);
1502
1864
  })();
1503
1865
  },
1504
1866
  [exchangeNow, go, reoffer, settle]
@@ -1589,6 +1951,7 @@ function AvatarLiveView({
1589
1951
  [applyAvailability, bindingHolds, cancelWaiting, exchangeNow, launch]
1590
1952
  );
1591
1953
  const stop = react.useCallback(() => {
1954
+ micCancelRef.current();
1592
1955
  const current = phaseRef.current;
1593
1956
  if (current.name === "waiting") {
1594
1957
  cancelWaiting(false);
@@ -1610,6 +1973,7 @@ function AvatarLiveView({
1610
1973
  }, [cancelStart, cancelWaiting, finish, go, reoffer]);
1611
1974
  const leave = react.useCallback(
1612
1975
  (event) => {
1976
+ micCancelRef.current();
1613
1977
  const current = phaseRef.current;
1614
1978
  const reason = event === "user" || event === "chat" ? "user" : "page_hide";
1615
1979
  const how = reason === "user" ? "stopped" : "interrupted";
@@ -1620,7 +1984,7 @@ function AvatarLiveView({
1620
1984
  const controller = checkRef.current;
1621
1985
  checkRef.current = null;
1622
1986
  controller?.abort();
1623
- go(IDLE);
1987
+ go(IDLE2);
1624
1988
  return;
1625
1989
  }
1626
1990
  case "waiting":
@@ -1628,7 +1992,7 @@ function AvatarLiveView({
1628
1992
  return;
1629
1993
  case "bot-check":
1630
1994
  cancelStart(reason);
1631
- go(reason === "user" ? IDLE : reoffer(current.offer));
1995
+ go(reason === "user" ? IDLE2 : reoffer(current.offer));
1632
1996
  return;
1633
1997
  case "creating":
1634
1998
  if (keepsSession) return;
@@ -1690,7 +2054,7 @@ function AvatarLiveView({
1690
2054
  } else {
1691
2055
  checkRef.current?.abort();
1692
2056
  checkRef.current = null;
1693
- go(IDLE);
2057
+ go(IDLE2);
1694
2058
  }
1695
2059
  }, [active, cancelWaiting, check, go, locale]);
1696
2060
  const kind = shown.kind;
@@ -1935,6 +2299,46 @@ function AvatarLiveView({
1935
2299
  const textMode = any && (usedToday && (phase.name === "needs-answer" || phase.name === "consent" || phase.name === "declined" || phase.name === "ended") || phase.name === "refused" && phase.reason === "limits");
1936
2300
  textBoxRef.current = textMode;
1937
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;
1938
2342
  const notNow = react.useCallback(() => {
1939
2343
  const current = phaseRef.current;
1940
2344
  if ((current.name === "consent" || current.name === "needs-answer") && current.offer.questions === "any") {
@@ -2097,13 +2501,13 @@ function AvatarLiveView({
2097
2501
  className: "w-full max-w-sm rounded-lg border border-border p-4 text-start",
2098
2502
  children: [
2099
2503
  /* @__PURE__ */ jsxRuntime.jsx("h2", { id: consentTitleId, className: "text-base font-semibold", children: copy.title }),
2100
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm", children: productName === null || copy.introProduct === null ? copy.intro : chunkO4GS3XPZ_js.fill(copy.introProduct, { product: productName }) }),
2504
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm", children: productName === null || copy.introProduct === null ? copy.intro : chunkN7E6Z2GF_js.fill(copy.introProduct, { product: productName }) }),
2101
2505
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: copy.processing }),
2102
2506
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: copy.storage }),
2103
2507
  /* @__PURE__ */ jsxRuntime.jsxs(
2104
2508
  "a",
2105
2509
  {
2106
- href: PRIVACY_HREF,
2510
+ href: PRIVACY_HREF2,
2107
2511
  target: "_blank",
2108
2512
  rel: "noopener noreferrer",
2109
2513
  "data-boff-explore": "avatar-live-privacy",
@@ -2195,7 +2599,7 @@ function AvatarLiveView({
2195
2599
  /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm tabular-nums text-muted-foreground", children: [
2196
2600
  S.timeLeft,
2197
2601
  " ",
2198
- /* @__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)) })
2199
2603
  ] })
2200
2604
  ] });
2201
2605
  const liveActions = (back) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [
@@ -2277,14 +2681,21 @@ function AvatarLiveView({
2277
2681
  children: lastQuestion.content
2278
2682
  }
2279
2683
  ),
2280
- lastAnswer === void 0 ? unanswered === "waiting" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: activity ?? S.gettingAnswer }) : unanswered === "failed" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: S.answerFailed }) : null : lastAnswer.status === "PENDING" || lastAnswer.status === "RUNNING" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: activity ?? S.gettingAnswer }) : lastAnswer.status === "FAILED" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: S.answerFailed }) : /* @__PURE__ */ jsxRuntime.jsx(
2281
- "p",
2282
- {
2283
- "data-boff-explore": "avatar-live-text-answer",
2284
- dir: "auto",
2285
- className: "line-clamp-6 w-full max-w-sm whitespace-pre-wrap break-words text-start text-sm",
2286
- children: lastAnswer.content
2287
- }
2684
+ lastAnswer === void 0 ? unanswered === "waiting" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: activity ?? S.gettingAnswer }) : unanswered === "failed" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: S.answerFailed }) : null : lastAnswer.status === "PENDING" || lastAnswer.status === "RUNNING" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: activity ?? S.gettingAnswer }) : lastAnswer.status === "FAILED" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: S.answerFailed }) : (
2685
+ // The answer's words, not its markdown: `content` is written for the chat's renderer, and shown as it is
2686
+ // here a link reads `[label](https://…)`. In its spoken form, as the chat's read-aloud says it: a link is its
2687
+ // label (the address is in Chat, one tap away), a table its rows, and the markers of emphasis, headings,
2688
+ // quotes and list items go (`answerDisplayText`). The words are the ones the chat shows, citation markers
2689
+ // included, which the sources in Chat answer to.
2690
+ /* @__PURE__ */ jsxRuntime.jsx(
2691
+ "p",
2692
+ {
2693
+ "data-boff-explore": "avatar-live-text-answer",
2694
+ dir: "auto",
2695
+ className: "line-clamp-6 w-full max-w-sm whitespace-pre-wrap break-words text-start text-sm",
2696
+ children: chunkN7E6Z2GF_js.answerDisplayText(lastAnswer.content)
2697
+ }
2698
+ )
2288
2699
  )
2289
2700
  ] });
2290
2701
  const ended = phase.name === "ended";
@@ -2405,7 +2816,7 @@ function AvatarLiveView({
2405
2816
  break;
2406
2817
  }
2407
2818
  }
2408
- const reason = inFlight(conversation) ? S.askBusy : conversation?.canSend ? null : S.askUnavailable;
2819
+ const reason = askReason;
2409
2820
  const chips = box !== null && shown.kind === "empty" && prompts.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
2410
2821
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-medium text-muted-foreground", children: tryAsking }),
2411
2822
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex max-h-28 flex-wrap gap-2 overflow-y-auto", children: prompts.map((prompt) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -2452,6 +2863,7 @@ function AvatarLiveView({
2452
2863
  children: S.usedToday
2453
2864
  }
2454
2865
  ) : null,
2866
+ box === "spoken" ? mic.notice : null,
2455
2867
  box === null ? null : /* @__PURE__ */ jsxRuntime.jsx(
2456
2868
  AskBox,
2457
2869
  {
@@ -2466,7 +2878,9 @@ function AvatarLiveView({
2466
2878
  footer: S.askFooter,
2467
2879
  noFiles: S.askNoFiles,
2468
2880
  hint: box === "spoken" ? S.needsAnswerAny : null,
2469
- touchFirst
2881
+ touchFirst,
2882
+ mic: box === "spoken" ? mic.button : null,
2883
+ micLines: box === "spoken" ? mic.lines : null
2470
2884
  }
2471
2885
  )
2472
2886
  ]
@@ -2613,7 +3027,9 @@ function AskBox({
2613
3027
  footer,
2614
3028
  noFiles,
2615
3029
  hint,
2616
- touchFirst
3030
+ touchFirst,
3031
+ mic,
3032
+ micLines
2617
3033
  }) {
2618
3034
  const uid = react.useId();
2619
3035
  const reasonId = `${uid}-reason`;
@@ -2678,6 +3094,7 @@ function AskBox({
2678
3094
  onDrop
2679
3095
  }
2680
3096
  ),
3097
+ mic,
2681
3098
  /* @__PURE__ */ jsxRuntime.jsx(
2682
3099
  "button",
2683
3100
  {
@@ -2691,6 +3108,7 @@ function AskBox({
2691
3108
  }
2692
3109
  )
2693
3110
  ] }),
3111
+ micLines,
2694
3112
  reason === null ? null : /* @__PURE__ */ jsxRuntime.jsx(
2695
3113
  "p",
2696
3114
  {