@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,14 +1,16 @@
1
- import { turnstileSiteKeyOf, isAvatarLiveLocaleReviewed, isAvatarSite, isAvatarVersion } from '../chunk-7MSB3EUI.mjs';
2
- import { fill } from '../chunk-TT3MHN42.mjs';
1
+ import { turnstileSiteKeyOf, isAvatarLiveLocaleReviewed, isAvatarSite, isAvatarVersion } from '../chunk-MVNN6LLO.mjs';
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,13 +199,11 @@ 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
  }
182
205
  }
183
- async function requestCreate(target) {
206
+ function requestCreate(target) {
184
207
  const { body } = target;
185
208
  const payload = JSON.stringify({
186
209
  site: body.site,
@@ -190,27 +213,45 @@ async function requestCreate(target) {
190
213
  consentVersion: body.consentVersion,
191
214
  manifestVersion: body.manifestVersion
192
215
  });
193
- const timer = deadline(CREATE_TIMEOUT_MS);
194
- try {
195
- const response = await fetch(`${target.base}/sessions`, {
196
- method: "POST",
197
- credentials: "omit",
198
- cache: "no-store",
199
- headers: {
200
- ...target.headers,
201
- "Content-Type": "application/json",
202
- "Idempotency-Key": target.idempotencyKey,
203
- ...target.botToken === void 0 ? {} : { "x-web-avatar-bot-token": target.botToken }
204
- },
205
- body: payload,
206
- signal: timer.signal
216
+ const controller = new AbortController();
217
+ let answered = false;
218
+ const answer = (async () => {
219
+ try {
220
+ const response = await fetch(`${target.base}/sessions`, {
221
+ method: "POST",
222
+ credentials: "omit",
223
+ cache: "no-store",
224
+ headers: {
225
+ ...target.headers,
226
+ "Content-Type": "application/json",
227
+ "Idempotency-Key": target.idempotencyKey,
228
+ ...target.botToken === void 0 ? {} : { "x-web-avatar-bot-token": target.botToken }
229
+ },
230
+ body: payload,
231
+ signal: controller.signal
232
+ });
233
+ answered = true;
234
+ return readCreate(response.status, await readJson(response));
235
+ } catch {
236
+ return FAILED;
237
+ }
238
+ })();
239
+ return new Promise((resolve) => {
240
+ let late = false;
241
+ const timer = setTimeout(() => {
242
+ late = true;
243
+ if (!answered) controller.abort();
244
+ resolve(FAILED);
245
+ }, CREATE_TIMEOUT_MS);
246
+ void answer.then((result) => {
247
+ if (late) {
248
+ target.onLate?.(result);
249
+ return;
250
+ }
251
+ clearTimeout(timer);
252
+ resolve(result);
207
253
  });
208
- return readCreate(response.status, await readJson(response));
209
- } catch {
210
- return INERT;
211
- } finally {
212
- timer.done();
213
- }
254
+ });
214
255
  }
215
256
  function sendEnd(target) {
216
257
  try {
@@ -786,6 +827,318 @@ function avatarConsentCopy(version, locale) {
786
827
  if (!isAvatarLiveLocaleReviewed(locale)) return null;
787
828
  return CONSENT.get(version)?.[locale] ?? null;
788
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
+ }
789
1142
 
790
1143
  // src/components/explore-avatar-turnstile.ts
791
1144
  var SCRIPT_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
@@ -901,7 +1254,7 @@ function runTurnstile(container, siteKey, action, cData) {
901
1254
  }
902
1255
  };
903
1256
  }
904
- var PRIVACY_HREF = "/privacy#live-avatar";
1257
+ var PRIVACY_HREF2 = "/privacy#live-avatar";
905
1258
  var SPEAKER_WAIT_MS = 3e4;
906
1259
  var END_GRACE_MS = 1e4;
907
1260
  var CLOCK_TOLERANCE_MS = 5e3;
@@ -914,7 +1267,7 @@ var ASK_MAX_LENGTH = 300;
914
1267
  var MAX_REFERENCES = 64;
915
1268
  var USED_ON_KEY = "boff.explore.avatarUsedOn";
916
1269
  var PRIMARY = '[data-avatar-primary], [data-boff-explore="panel-continue-chat"]';
917
- var IDLE = { name: "idle" };
1270
+ var IDLE2 = { name: "idle" };
918
1271
  var INERT2 = { name: "inert" };
919
1272
  function stopAttending(handle) {
920
1273
  if (handle.unheard !== null) clearTimeout(handle.unheard);
@@ -1004,6 +1357,12 @@ function firstExchange(messages, rules = SUGGESTED_RULES) {
1004
1357
  function inFlight(conversation) {
1005
1358
  return conversation?.phase === "sending" || conversation?.phase === "streaming";
1006
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
+ }
1007
1366
  function utcToday() {
1008
1367
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1009
1368
  }
@@ -1073,7 +1432,7 @@ async function loadModule(load) {
1073
1432
  }
1074
1433
  return loaded;
1075
1434
  }
1076
- function clock(seconds) {
1435
+ function clock2(seconds) {
1077
1436
  const whole = Math.max(0, Math.ceil(seconds));
1078
1437
  return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, "0")}`;
1079
1438
  }
@@ -1126,7 +1485,7 @@ function AvatarLiveView({
1126
1485
  const shown = offeredAt(exchange, Date.now());
1127
1486
  const { answerId, startOver } = shown;
1128
1487
  const offerUntil = answerId === null ? null : exchange.offerUntil;
1129
- const [phase, setPhase] = useState(IDLE);
1488
+ const [phase, setPhase] = useState(IDLE2);
1130
1489
  const [blocked, setBlocked] = useState(false);
1131
1490
  const [now, setNow] = useState(() => Date.now());
1132
1491
  const [draft, setDraft] = useState("");
@@ -1136,7 +1495,7 @@ function AvatarLiveView({
1136
1495
  const [usedToday, setUsedToday] = useState(false);
1137
1496
  const showing = active && !off;
1138
1497
  const behindChat = open && !active && !off;
1139
- const phaseRef = useRef(IDLE);
1498
+ const phaseRef = useRef(IDLE2);
1140
1499
  const latest = useRef({
1141
1500
  live,
1142
1501
  locale,
@@ -1152,6 +1511,8 @@ function AvatarLiveView({
1152
1511
  const waitRef = useRef(null);
1153
1512
  const sendingRef = useRef(false);
1154
1513
  const textBoxRef = useRef(false);
1514
+ const [micOffer, setMicOffer] = useState(null);
1515
+ const micCancelRef = useRef(() => void 0);
1155
1516
  const checkRef = useRef(null);
1156
1517
  const askedLocale = useRef(locale);
1157
1518
  const startRef = useRef(null);
@@ -1272,6 +1633,7 @@ function AvatarLiveView({
1272
1633
  const applyAvailability = useCallback(
1273
1634
  (result, locale2) => {
1274
1635
  const { live: live2 } = latest.current;
1636
+ setMicOffer(micOfferOf(result));
1275
1637
  if (result.kind === "inert") {
1276
1638
  setMode("suggested");
1277
1639
  go(INERT2);
@@ -1359,21 +1721,21 @@ function AvatarLiveView({
1359
1721
  [go]
1360
1722
  );
1361
1723
  const settle = useCallback(
1362
- (result, run, context) => {
1724
+ (result, givenUp, context) => {
1363
1725
  const created = result.kind === "created" ? {
1364
1726
  sessionId: result.session.sessionId,
1365
1727
  secret: result.session.endSecret
1366
1728
  } : result.kind === "unusable" ? { sessionId: result.sessionId, secret: result.endSecret } : null;
1367
1729
  const claimed = created !== null || result.kind === "refused" && result.reason === "answer_claimed";
1368
1730
  if (claimed) claimedRef.current.add(context.messageId);
1369
- if (run.cancelled !== null) {
1731
+ if (givenUp !== null) {
1370
1732
  if (created !== null) {
1371
1733
  sendEnd({
1372
1734
  base: context.base,
1373
1735
  headers: context.headers,
1374
1736
  sessionId: created.sessionId,
1375
1737
  secret: created.secret,
1376
- reason: run.cancelled
1738
+ reason: givenUp
1377
1739
  });
1378
1740
  }
1379
1741
  const shown2 = phaseRef.current;
@@ -1482,21 +1844,21 @@ function AvatarLiveView({
1482
1844
  return;
1483
1845
  }
1484
1846
  if (turnstile !== null) go({ name: "creating" });
1847
+ const context = { module: loaded, offer, messageId, base: base2, headers: headers2 };
1485
1848
  const result = await requestCreate({
1486
1849
  base: base2,
1487
1850
  headers: headers2,
1488
1851
  idempotencyKey,
1489
1852
  botToken,
1490
- body: body2
1853
+ body: body2,
1854
+ // An answer past the deadline, after the view has said the live avatar couldn't start: a session it made is
1855
+ // ended at once with its own secret, and never joined. The reason is the visitor's own where they gave the
1856
+ // start up first, else `client_error`. It reads `run` and never writes it, so the deadline's `failed` below is
1857
+ // shown whichever of the two is handled first.
1858
+ onLate: (late) => settle(late, run.cancelled ?? "client_error", context)
1491
1859
  });
1492
1860
  if (startRef.current === run) startRef.current = null;
1493
- settle(result, run, {
1494
- module: loaded,
1495
- offer,
1496
- messageId,
1497
- base: base2,
1498
- headers: headers2
1499
- });
1861
+ settle(result, run.cancelled, context);
1500
1862
  })();
1501
1863
  },
1502
1864
  [exchangeNow, go, reoffer, settle]
@@ -1587,6 +1949,7 @@ function AvatarLiveView({
1587
1949
  [applyAvailability, bindingHolds, cancelWaiting, exchangeNow, launch]
1588
1950
  );
1589
1951
  const stop = useCallback(() => {
1952
+ micCancelRef.current();
1590
1953
  const current = phaseRef.current;
1591
1954
  if (current.name === "waiting") {
1592
1955
  cancelWaiting(false);
@@ -1608,6 +1971,7 @@ function AvatarLiveView({
1608
1971
  }, [cancelStart, cancelWaiting, finish, go, reoffer]);
1609
1972
  const leave = useCallback(
1610
1973
  (event) => {
1974
+ micCancelRef.current();
1611
1975
  const current = phaseRef.current;
1612
1976
  const reason = event === "user" || event === "chat" ? "user" : "page_hide";
1613
1977
  const how = reason === "user" ? "stopped" : "interrupted";
@@ -1618,7 +1982,7 @@ function AvatarLiveView({
1618
1982
  const controller = checkRef.current;
1619
1983
  checkRef.current = null;
1620
1984
  controller?.abort();
1621
- go(IDLE);
1985
+ go(IDLE2);
1622
1986
  return;
1623
1987
  }
1624
1988
  case "waiting":
@@ -1626,7 +1990,7 @@ function AvatarLiveView({
1626
1990
  return;
1627
1991
  case "bot-check":
1628
1992
  cancelStart(reason);
1629
- go(reason === "user" ? IDLE : reoffer(current.offer));
1993
+ go(reason === "user" ? IDLE2 : reoffer(current.offer));
1630
1994
  return;
1631
1995
  case "creating":
1632
1996
  if (keepsSession) return;
@@ -1688,7 +2052,7 @@ function AvatarLiveView({
1688
2052
  } else {
1689
2053
  checkRef.current?.abort();
1690
2054
  checkRef.current = null;
1691
- go(IDLE);
2055
+ go(IDLE2);
1692
2056
  }
1693
2057
  }, [active, cancelWaiting, check, go, locale]);
1694
2058
  const kind = shown.kind;
@@ -1933,6 +2297,46 @@ function AvatarLiveView({
1933
2297
  const textMode = any && (usedToday && (phase.name === "needs-answer" || phase.name === "consent" || phase.name === "declined" || phase.name === "ended") || phase.name === "refused" && phase.reason === "limits");
1934
2298
  textBoxRef.current = textMode;
1935
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;
1936
2340
  const notNow = useCallback(() => {
1937
2341
  const current = phaseRef.current;
1938
2342
  if ((current.name === "consent" || current.name === "needs-answer") && current.offer.questions === "any") {
@@ -2101,7 +2505,7 @@ function AvatarLiveView({
2101
2505
  /* @__PURE__ */ jsxs(
2102
2506
  "a",
2103
2507
  {
2104
- href: PRIVACY_HREF,
2508
+ href: PRIVACY_HREF2,
2105
2509
  target: "_blank",
2106
2510
  rel: "noopener noreferrer",
2107
2511
  "data-boff-explore": "avatar-live-privacy",
@@ -2193,7 +2597,7 @@ function AvatarLiveView({
2193
2597
  /* @__PURE__ */ jsxs("p", { className: "text-sm tabular-nums text-muted-foreground", children: [
2194
2598
  S.timeLeft,
2195
2599
  " ",
2196
- /* @__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)) })
2197
2601
  ] })
2198
2602
  ] });
2199
2603
  const liveActions = (back) => /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [
@@ -2275,14 +2679,21 @@ function AvatarLiveView({
2275
2679
  children: lastQuestion.content
2276
2680
  }
2277
2681
  ),
2278
- lastAnswer === void 0 ? unanswered === "waiting" ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: activity ?? S.gettingAnswer }) : unanswered === "failed" ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: S.answerFailed }) : null : lastAnswer.status === "PENDING" || lastAnswer.status === "RUNNING" ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: activity ?? S.gettingAnswer }) : lastAnswer.status === "FAILED" ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: S.answerFailed }) : /* @__PURE__ */ jsx(
2279
- "p",
2280
- {
2281
- "data-boff-explore": "avatar-live-text-answer",
2282
- dir: "auto",
2283
- className: "line-clamp-6 w-full max-w-sm whitespace-pre-wrap break-words text-start text-sm",
2284
- children: lastAnswer.content
2285
- }
2682
+ lastAnswer === void 0 ? unanswered === "waiting" ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: activity ?? S.gettingAnswer }) : unanswered === "failed" ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: S.answerFailed }) : null : lastAnswer.status === "PENDING" || lastAnswer.status === "RUNNING" ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: activity ?? S.gettingAnswer }) : lastAnswer.status === "FAILED" ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: S.answerFailed }) : (
2683
+ // The answer's words, not its markdown: `content` is written for the chat's renderer, and shown as it is
2684
+ // here a link reads `[label](https://…)`. In its spoken form, as the chat's read-aloud says it: a link is its
2685
+ // label (the address is in Chat, one tap away), a table its rows, and the markers of emphasis, headings,
2686
+ // quotes and list items go (`answerDisplayText`). The words are the ones the chat shows, citation markers
2687
+ // included, which the sources in Chat answer to.
2688
+ /* @__PURE__ */ jsx(
2689
+ "p",
2690
+ {
2691
+ "data-boff-explore": "avatar-live-text-answer",
2692
+ dir: "auto",
2693
+ className: "line-clamp-6 w-full max-w-sm whitespace-pre-wrap break-words text-start text-sm",
2694
+ children: answerDisplayText(lastAnswer.content)
2695
+ }
2696
+ )
2286
2697
  )
2287
2698
  ] });
2288
2699
  const ended = phase.name === "ended";
@@ -2403,7 +2814,7 @@ function AvatarLiveView({
2403
2814
  break;
2404
2815
  }
2405
2816
  }
2406
- const reason = inFlight(conversation) ? S.askBusy : conversation?.canSend ? null : S.askUnavailable;
2817
+ const reason = askReason;
2407
2818
  const chips = box !== null && shown.kind === "empty" && prompts.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
2408
2819
  /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground", children: tryAsking }),
2409
2820
  /* @__PURE__ */ jsx("div", { className: "flex max-h-28 flex-wrap gap-2 overflow-y-auto", children: prompts.map((prompt) => /* @__PURE__ */ jsx(
@@ -2450,6 +2861,7 @@ function AvatarLiveView({
2450
2861
  children: S.usedToday
2451
2862
  }
2452
2863
  ) : null,
2864
+ box === "spoken" ? mic.notice : null,
2453
2865
  box === null ? null : /* @__PURE__ */ jsx(
2454
2866
  AskBox,
2455
2867
  {
@@ -2464,7 +2876,9 @@ function AvatarLiveView({
2464
2876
  footer: S.askFooter,
2465
2877
  noFiles: S.askNoFiles,
2466
2878
  hint: box === "spoken" ? S.needsAnswerAny : null,
2467
- touchFirst
2879
+ touchFirst,
2880
+ mic: box === "spoken" ? mic.button : null,
2881
+ micLines: box === "spoken" ? mic.lines : null
2468
2882
  }
2469
2883
  )
2470
2884
  ]
@@ -2611,7 +3025,9 @@ function AskBox({
2611
3025
  footer,
2612
3026
  noFiles,
2613
3027
  hint,
2614
- touchFirst
3028
+ touchFirst,
3029
+ mic,
3030
+ micLines
2615
3031
  }) {
2616
3032
  const uid = useId();
2617
3033
  const reasonId = `${uid}-reason`;
@@ -2676,6 +3092,7 @@ function AskBox({
2676
3092
  onDrop
2677
3093
  }
2678
3094
  ),
3095
+ mic,
2679
3096
  /* @__PURE__ */ jsx(
2680
3097
  "button",
2681
3098
  {
@@ -2689,6 +3106,7 @@ function AskBox({
2689
3106
  }
2690
3107
  )
2691
3108
  ] }),
3109
+ micLines,
2692
3110
  reason === null ? null : /* @__PURE__ */ jsx(
2693
3111
  "p",
2694
3112
  {