@burdenoff/website-sdk 2026.828.5 → 2026.828.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -5963,11 +5963,40 @@ function getRecognitionCtor() {
5963
5963
  const w = window;
5964
5964
  return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
5965
5965
  }
5966
+ var MIC_DENIED_MESSAGE = "Microphone access was blocked. Allow it in your browser's site settings to dictate.";
5967
+ async function ensureMicrophoneAccess() {
5968
+ const media = typeof navigator === "undefined" ? void 0 : navigator.mediaDevices;
5969
+ if (!media?.getUserMedia) return { ok: true, confirmed: false };
5970
+ try {
5971
+ const status = await navigator.permissions?.query({
5972
+ name: "microphone"
5973
+ });
5974
+ if (status?.state === "granted") return { ok: true, confirmed: true };
5975
+ } catch {
5976
+ }
5977
+ try {
5978
+ const stream = await media.getUserMedia({ audio: true });
5979
+ for (const track of stream.getTracks()) track.stop();
5980
+ return { ok: true, confirmed: true };
5981
+ } catch (error) {
5982
+ const name = typeof error === "object" && error !== null && "name" in error ? String(error.name) : "";
5983
+ if (name === "NotAllowedError" || name === "SecurityError") {
5984
+ return { ok: false, message: MIC_DENIED_MESSAGE };
5985
+ }
5986
+ if (name === "NotFoundError" || name === "DevicesNotFoundError") {
5987
+ return { ok: false, message: "No microphone was found." };
5988
+ }
5989
+ return {
5990
+ ok: false,
5991
+ message: "Dictation could not start. You can type instead."
5992
+ };
5993
+ }
5994
+ }
5966
5995
  function describeError(code) {
5967
5996
  switch (code) {
5968
5997
  case "not-allowed":
5969
5998
  case "service-not-allowed":
5970
- return "Microphone access was blocked. Allow it in your browser's site settings to dictate.";
5999
+ return MIC_DENIED_MESSAGE;
5971
6000
  case "no-speech":
5972
6001
  return "I didn't catch anything \u2014 try again a little closer to the mic.";
5973
6002
  case "audio-capture":
@@ -5990,11 +6019,23 @@ function useSpeechInput({
5990
6019
  const [interim, setInterim] = useState("");
5991
6020
  const [error, setError] = useState(null);
5992
6021
  const recognitionRef = useRef(null);
6022
+ const startTokenRef = useRef(0);
6023
+ const micConfirmedRef = useRef(false);
6024
+ const beginRecognitionRef = useRef(null);
6025
+ const beginRecognition = useCallback(
6026
+ (Ctor, token) => {
6027
+ beginRecognitionRef.current?.(Ctor, token);
6028
+ },
6029
+ []
6030
+ );
5993
6031
  const finalRef = useRef(onFinalTranscript);
5994
6032
  const errorRef = useRef(onError);
5995
6033
  finalRef.current = onFinalTranscript;
5996
6034
  errorRef.current = onError;
5997
6035
  const stop = useCallback(() => {
6036
+ startTokenRef.current += 1;
6037
+ setListening(false);
6038
+ setInterim("");
5998
6039
  const recognition = recognitionRef.current;
5999
6040
  if (!recognition) return;
6000
6041
  try {
@@ -6008,48 +6049,70 @@ function useSpeechInput({
6008
6049
  const Ctor = getRecognitionCtor();
6009
6050
  if (!Ctor) return;
6010
6051
  if (recognitionRef.current) stop();
6011
- const recognition = new Ctor();
6012
- recognition.lang = lang ?? (typeof navigator === "undefined" ? "en-US" : navigator.language || "en-US");
6013
- recognition.continuous = true;
6014
- recognition.interimResults = true;
6015
- recognition.maxAlternatives = 1;
6016
- recognition.onstart = () => {
6017
- setError(null);
6018
- setListening(true);
6019
- };
6020
- recognition.onresult = (event) => {
6021
- let settled = "";
6022
- let pending = "";
6023
- for (let i = event.resultIndex; i < event.results.length; i += 1) {
6024
- const result = event.results[i];
6025
- if (!result) continue;
6026
- const text = result[0]?.transcript ?? "";
6027
- if (result.isFinal) settled += text;
6028
- else pending += text;
6052
+ setError(null);
6053
+ setListening(true);
6054
+ startTokenRef.current += 1;
6055
+ const token = startTokenRef.current;
6056
+ void ensureMicrophoneAccess().then((access) => {
6057
+ if (token !== startTokenRef.current) return;
6058
+ micConfirmedRef.current = access.ok && access.confirmed;
6059
+ if (!access.ok) {
6060
+ setListening(false);
6061
+ setError(access.message);
6062
+ errorRef.current?.(access.message);
6063
+ return;
6029
6064
  }
6030
- setInterim(pending);
6031
- if (settled.trim() !== "") finalRef.current(settled);
6032
- };
6033
- recognition.onerror = (event) => {
6034
- const message = describeError(event.error);
6035
- setListening(false);
6036
- setInterim("");
6037
- if (message !== "") {
6038
- setError(message);
6039
- errorRef.current?.(message);
6065
+ beginRecognition(Ctor, token);
6066
+ });
6067
+ }, [beginRecognition, stop]);
6068
+ const beginRecognitionImpl = useCallback(
6069
+ (Ctor, token) => {
6070
+ const recognition = new Ctor();
6071
+ recognition.lang = lang ?? (typeof navigator === "undefined" ? "en-US" : navigator.language || "en-US");
6072
+ recognition.continuous = true;
6073
+ recognition.interimResults = true;
6074
+ recognition.maxAlternatives = 1;
6075
+ recognition.onstart = () => {
6076
+ if (token !== startTokenRef.current) return;
6077
+ setError(null);
6078
+ setListening(true);
6079
+ };
6080
+ recognition.onresult = (event) => {
6081
+ let settled = "";
6082
+ let pending = "";
6083
+ for (let i = event.resultIndex; i < event.results.length; i += 1) {
6084
+ const result = event.results[i];
6085
+ if (!result) continue;
6086
+ const text = result[0]?.transcript ?? "";
6087
+ if (result.isFinal) settled += text;
6088
+ else pending += text;
6089
+ }
6090
+ setInterim(pending);
6091
+ if (settled.trim() !== "") finalRef.current(settled);
6092
+ };
6093
+ recognition.onerror = (event) => {
6094
+ const message = micConfirmedRef.current && (event.error === "not-allowed" || event.error === "service-not-allowed") ? "Dictation isn't available in this browser. You can type instead." : describeError(event.error);
6095
+ setListening(false);
6096
+ setInterim("");
6097
+ if (message !== "") {
6098
+ setError(message);
6099
+ errorRef.current?.(message);
6100
+ }
6101
+ };
6102
+ recognition.onend = () => {
6103
+ setListening(false);
6104
+ setInterim("");
6105
+ };
6106
+ recognitionRef.current = recognition;
6107
+ try {
6108
+ recognition.start();
6109
+ } catch {
6110
+ setListening(false);
6040
6111
  }
6041
- };
6042
- recognition.onend = () => {
6043
- setListening(false);
6044
- setInterim("");
6045
- };
6046
- recognitionRef.current = recognition;
6047
- try {
6048
- recognition.start();
6049
- } catch {
6050
- setListening(false);
6051
- }
6052
- }, [lang, stop]);
6112
+ },
6113
+ [lang]
6114
+ );
6115
+ beginRecognitionRef.current = beginRecognitionImpl;
6053
6116
  const toggle = useCallback(() => {
6054
6117
  if (listening) stop();
6055
6118
  else start();
@@ -6165,10 +6228,33 @@ function initialOf(value, fallback) {
6165
6228
  const source = (value || fallback).trim();
6166
6229
  return source ? source.slice(0, 1).toUpperCase() : "?";
6167
6230
  }
6231
+ function logoUrlOf(url) {
6232
+ try {
6233
+ const { origin, protocol } = new URL(url);
6234
+ if (protocol !== "https:" && protocol !== "http:") return null;
6235
+ return `${origin}/favicon.svg`;
6236
+ } catch {
6237
+ return null;
6238
+ }
6239
+ }
6168
6240
  function ReferenceCard({ reference }) {
6169
6241
  const [imageFailed, setImageFailed] = useState(false);
6242
+ const [logoFailed, setLogoFailed] = useState(false);
6170
6243
  const host = hostnameOf(reference.url);
6244
+ const logoUrl = logoUrlOf(reference.url);
6171
6245
  const showImage = Boolean(reference.imageUrl) && !imageFailed;
6246
+ const showLogo = Boolean(logoUrl) && !logoFailed;
6247
+ const letterPlate = /* @__PURE__ */ jsx("div", { className: "flex h-full w-full items-center justify-center bg-gradient-to-br from-primary/25 via-primary/5 to-secondary", children: /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-primary/70 sm:text-2xl", children: initialOf(reference.product, reference.title || host) }) });
6248
+ const logoImg = showLogo ? /* @__PURE__ */ jsx(
6249
+ "img",
6250
+ {
6251
+ src: logoUrl ?? "",
6252
+ alt: "",
6253
+ loading: "lazy",
6254
+ onError: () => setLogoFailed(true),
6255
+ className: "h-full w-full object-contain p-1.5 sm:p-0"
6256
+ }
6257
+ ) : letterPlate;
6172
6258
  return /* @__PURE__ */ jsxs(
6173
6259
  "a",
6174
6260
  {
@@ -6176,9 +6262,10 @@ function ReferenceCard({ reference }) {
6176
6262
  href: reference.url,
6177
6263
  target: "_blank",
6178
6264
  rel: "noopener noreferrer",
6179
- className: "group flex flex-col overflow-hidden rounded-xl border border-border bg-card transition-colors hover:border-primary/40 hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
6265
+ className: "group flex flex-row items-center gap-3 overflow-hidden rounded-xl border border-border bg-card p-2 transition-colors hover:border-primary/40 hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring sm:flex-col sm:items-stretch sm:gap-0 sm:p-0",
6180
6266
  children: [
6181
- /* @__PURE__ */ jsx("div", { className: "relative aspect-[16/9] w-full overflow-hidden bg-muted", children: showImage ? /* @__PURE__ */ jsx(
6267
+ /* @__PURE__ */ jsx("div", { className: "relative h-11 w-11 shrink-0 overflow-hidden rounded-lg bg-muted sm:hidden", children: logoImg }),
6268
+ /* @__PURE__ */ jsx("div", { className: "relative hidden aspect-[16/9] w-full overflow-hidden bg-muted sm:block", children: showImage ? /* @__PURE__ */ jsx(
6182
6269
  "img",
6183
6270
  {
6184
6271
  src: reference.imageUrl ?? "",
@@ -6187,17 +6274,22 @@ function ReferenceCard({ reference }) {
6187
6274
  onError: () => setImageFailed(true),
6188
6275
  className: "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
6189
6276
  }
6190
- ) : (
6191
- /* Graceful fallback: a token-derived gradient plate, so a missing or
6192
- broken preview still reads as a deliberate card, not a hole. */
6193
- /* @__PURE__ */ jsx("div", { className: "flex h-full w-full items-center justify-center bg-gradient-to-br from-primary/25 via-primary/5 to-secondary", children: /* @__PURE__ */ jsx("span", { className: "text-2xl font-semibold text-primary/70", children: initialOf(reference.product, reference.title || host) }) })
6194
- ) }),
6195
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col gap-1.5 p-3", children: [
6277
+ ) : showLogo ? /* @__PURE__ */ jsx("div", { className: "flex h-full w-full items-center justify-center bg-gradient-to-br from-primary/15 via-transparent to-secondary/60 p-6", children: /* @__PURE__ */ jsx(
6278
+ "img",
6279
+ {
6280
+ src: logoUrl ?? "",
6281
+ alt: "",
6282
+ loading: "lazy",
6283
+ onError: () => setLogoFailed(true),
6284
+ className: "max-h-full max-w-full object-contain"
6285
+ }
6286
+ ) }) : letterPlate }),
6287
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5 sm:gap-1.5 sm:p-3", children: [
6196
6288
  /* @__PURE__ */ jsx("p", { className: "line-clamp-2 text-sm font-semibold leading-snug text-card-foreground", children: reference.title || host || reference.url }),
6197
- reference.description ? /* @__PURE__ */ jsx("p", { className: "line-clamp-2 text-xs leading-relaxed text-muted-foreground", children: reference.description }) : null,
6198
- /* @__PURE__ */ jsxs("div", { className: "mt-auto flex items-center gap-2 pt-2", children: [
6199
- reference.product ? /* @__PURE__ */ jsx("span", { className: "rounded-full bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground", children: reference.product }) : null,
6200
- /* @__PURE__ */ jsxs("span", { className: "ml-auto inline-flex min-w-0 items-center gap-1 text-xs text-muted-foreground", children: [
6289
+ reference.description ? /* @__PURE__ */ jsx("p", { className: "line-clamp-2 text-xs leading-relaxed text-muted-foreground max-sm:hidden", children: reference.description }) : null,
6290
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 sm:mt-auto sm:pt-2", children: [
6291
+ reference.product ? /* @__PURE__ */ jsx("span", { className: "rounded-full bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground max-sm:hidden", children: reference.product }) : null,
6292
+ /* @__PURE__ */ jsxs("span", { className: "inline-flex min-w-0 items-center gap-1 text-xs text-muted-foreground sm:ml-auto", children: [
6201
6293
  /* @__PURE__ */ jsx(ExternalLink, { className: "h-3 w-3 shrink-0" }),
6202
6294
  /* @__PURE__ */ jsx("span", { className: "truncate", children: host })
6203
6295
  ] })
@@ -6283,7 +6375,8 @@ function UserBubble({ message }) {
6283
6375
  function AssistantBubble({
6284
6376
  message,
6285
6377
  activity,
6286
- onNavigate
6378
+ onNavigate,
6379
+ anchorRef
6287
6380
  }) {
6288
6381
  const streaming = message.status === "PENDING" || message.status === "RUNNING";
6289
6382
  const body = message.content || message.partialContent || "";
@@ -6291,9 +6384,10 @@ function AssistantBubble({
6291
6384
  return /* @__PURE__ */ jsxs(
6292
6385
  "div",
6293
6386
  {
6387
+ ref: anchorRef,
6294
6388
  "data-boff-explore": "assistant",
6295
6389
  "data-status": message.status,
6296
- className: "flex gap-3",
6390
+ className: "flex scroll-mt-4 gap-3",
6297
6391
  children: [
6298
6392
  /* @__PURE__ */ jsx(
6299
6393
  "span",
@@ -6522,6 +6616,8 @@ function ExplorePage({
6522
6616
  });
6523
6617
  speechStopRef.current = speech.stop;
6524
6618
  const scrollRef = useRef(null);
6619
+ const latestAssistantRef = useRef(null);
6620
+ const alignedForRef = useRef(null);
6525
6621
  const stickToBottomRef = useRef(true);
6526
6622
  const composingRef = useRef(false);
6527
6623
  const busy = phase === "sending" || phase === "streaming";
@@ -6547,12 +6643,32 @@ function ExplorePage({
6547
6643
  if (!el) return;
6548
6644
  stickToBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
6549
6645
  }, []);
6646
+ const latestAssistantId = useMemo(() => {
6647
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
6648
+ const message = messages[i];
6649
+ if (message && message.role === "ASSISTANT") return message.id;
6650
+ }
6651
+ return null;
6652
+ }, [messages]);
6653
+ useEffect(() => {
6654
+ const el = scrollRef.current;
6655
+ const anchor = latestAssistantRef.current;
6656
+ if (!el || !anchor || !latestAssistantId) return;
6657
+ if (alignedForRef.current === latestAssistantId) return;
6658
+ if (!stickToBottomRef.current) return;
6659
+ if (anchor.offsetHeight === 0) return;
6660
+ alignedForRef.current = latestAssistantId;
6661
+ const delta = anchor.getBoundingClientRect().top - el.getBoundingClientRect().top;
6662
+ el.scrollTop = Math.max(0, el.scrollTop + delta - 12);
6663
+ }, [latestAssistantId, messages]);
6550
6664
  useEffect(() => {
6551
6665
  if (!stickToBottomRef.current) return;
6666
+ if (latestAssistantId && alignedForRef.current === latestAssistantId)
6667
+ return;
6552
6668
  const el = scrollRef.current;
6553
6669
  if (!el) return;
6554
6670
  el.scrollTop = el.scrollHeight;
6555
- }, [messages, activity]);
6671
+ }, [activity, latestAssistantId, messages]);
6556
6672
  const submitDraft = useCallback(() => {
6557
6673
  const text = draft.trim();
6558
6674
  if (!text || overLimit || busy || !canSend) return;
@@ -6752,7 +6868,8 @@ function ExplorePage({
6752
6868
  {
6753
6869
  message,
6754
6870
  activity,
6755
- onNavigate
6871
+ onNavigate,
6872
+ anchorRef: message.id === latestAssistantId ? latestAssistantRef : void 0
6756
6873
  },
6757
6874
  message.id
6758
6875
  )