@burdenoff/website-sdk 2026.828.5 → 2026.828.6

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.js CHANGED
@@ -5993,11 +5993,42 @@ function getRecognitionCtor() {
5993
5993
  const w = window;
5994
5994
  return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
5995
5995
  }
5996
+ var MIC_DENIED_MESSAGE = "Microphone access was blocked. Allow it in your browser's site settings to dictate.";
5997
+ async function ensureMicrophoneAccess() {
5998
+ const media = typeof navigator === "undefined" ? void 0 : navigator.mediaDevices;
5999
+ if (!media?.getUserMedia) return { ok: true };
6000
+ try {
6001
+ const status = await navigator.permissions?.query({
6002
+ name: "microphone"
6003
+ });
6004
+ if (status?.state === "granted") return { ok: true };
6005
+ if (status?.state === "denied")
6006
+ return { ok: false, message: MIC_DENIED_MESSAGE };
6007
+ } catch {
6008
+ }
6009
+ try {
6010
+ const stream = await media.getUserMedia({ audio: true });
6011
+ for (const track of stream.getTracks()) track.stop();
6012
+ return { ok: true };
6013
+ } catch (error) {
6014
+ const name = typeof error === "object" && error !== null && "name" in error ? String(error.name) : "";
6015
+ if (name === "NotAllowedError" || name === "SecurityError") {
6016
+ return { ok: false, message: MIC_DENIED_MESSAGE };
6017
+ }
6018
+ if (name === "NotFoundError" || name === "DevicesNotFoundError") {
6019
+ return { ok: false, message: "No microphone was found." };
6020
+ }
6021
+ return {
6022
+ ok: false,
6023
+ message: "Dictation could not start. You can type instead."
6024
+ };
6025
+ }
6026
+ }
5996
6027
  function describeError(code) {
5997
6028
  switch (code) {
5998
6029
  case "not-allowed":
5999
6030
  case "service-not-allowed":
6000
- return "Microphone access was blocked. Allow it in your browser's site settings to dictate.";
6031
+ return MIC_DENIED_MESSAGE;
6001
6032
  case "no-speech":
6002
6033
  return "I didn't catch anything \u2014 try again a little closer to the mic.";
6003
6034
  case "audio-capture":
@@ -6020,11 +6051,22 @@ function useSpeechInput({
6020
6051
  const [interim, setInterim] = React.useState("");
6021
6052
  const [error, setError] = React.useState(null);
6022
6053
  const recognitionRef = React.useRef(null);
6054
+ const startTokenRef = React.useRef(0);
6055
+ const beginRecognitionRef = React.useRef(null);
6056
+ const beginRecognition = React.useCallback(
6057
+ (Ctor, token) => {
6058
+ beginRecognitionRef.current?.(Ctor, token);
6059
+ },
6060
+ []
6061
+ );
6023
6062
  const finalRef = React.useRef(onFinalTranscript);
6024
6063
  const errorRef = React.useRef(onError);
6025
6064
  finalRef.current = onFinalTranscript;
6026
6065
  errorRef.current = onError;
6027
6066
  const stop = React.useCallback(() => {
6067
+ startTokenRef.current += 1;
6068
+ setListening(false);
6069
+ setInterim("");
6028
6070
  const recognition = recognitionRef.current;
6029
6071
  if (!recognition) return;
6030
6072
  try {
@@ -6038,48 +6080,69 @@ function useSpeechInput({
6038
6080
  const Ctor = getRecognitionCtor();
6039
6081
  if (!Ctor) return;
6040
6082
  if (recognitionRef.current) stop();
6041
- const recognition = new Ctor();
6042
- recognition.lang = lang ?? (typeof navigator === "undefined" ? "en-US" : navigator.language || "en-US");
6043
- recognition.continuous = true;
6044
- recognition.interimResults = true;
6045
- recognition.maxAlternatives = 1;
6046
- recognition.onstart = () => {
6047
- setError(null);
6048
- setListening(true);
6049
- };
6050
- recognition.onresult = (event) => {
6051
- let settled = "";
6052
- let pending = "";
6053
- for (let i = event.resultIndex; i < event.results.length; i += 1) {
6054
- const result = event.results[i];
6055
- if (!result) continue;
6056
- const text = result[0]?.transcript ?? "";
6057
- if (result.isFinal) settled += text;
6058
- else pending += text;
6083
+ setError(null);
6084
+ setListening(true);
6085
+ startTokenRef.current += 1;
6086
+ const token = startTokenRef.current;
6087
+ void ensureMicrophoneAccess().then((access) => {
6088
+ if (token !== startTokenRef.current) return;
6089
+ if (!access.ok) {
6090
+ setListening(false);
6091
+ setError(access.message);
6092
+ errorRef.current?.(access.message);
6093
+ return;
6059
6094
  }
6060
- setInterim(pending);
6061
- if (settled.trim() !== "") finalRef.current(settled);
6062
- };
6063
- recognition.onerror = (event) => {
6064
- const message = describeError(event.error);
6065
- setListening(false);
6066
- setInterim("");
6067
- if (message !== "") {
6068
- setError(message);
6069
- errorRef.current?.(message);
6095
+ beginRecognition(Ctor, token);
6096
+ });
6097
+ }, [beginRecognition, stop]);
6098
+ const beginRecognitionImpl = React.useCallback(
6099
+ (Ctor, token) => {
6100
+ const recognition = new Ctor();
6101
+ recognition.lang = lang ?? (typeof navigator === "undefined" ? "en-US" : navigator.language || "en-US");
6102
+ recognition.continuous = true;
6103
+ recognition.interimResults = true;
6104
+ recognition.maxAlternatives = 1;
6105
+ recognition.onstart = () => {
6106
+ if (token !== startTokenRef.current) return;
6107
+ setError(null);
6108
+ setListening(true);
6109
+ };
6110
+ recognition.onresult = (event) => {
6111
+ let settled = "";
6112
+ let pending = "";
6113
+ for (let i = event.resultIndex; i < event.results.length; i += 1) {
6114
+ const result = event.results[i];
6115
+ if (!result) continue;
6116
+ const text = result[0]?.transcript ?? "";
6117
+ if (result.isFinal) settled += text;
6118
+ else pending += text;
6119
+ }
6120
+ setInterim(pending);
6121
+ if (settled.trim() !== "") finalRef.current(settled);
6122
+ };
6123
+ recognition.onerror = (event) => {
6124
+ const message = describeError(event.error);
6125
+ setListening(false);
6126
+ setInterim("");
6127
+ if (message !== "") {
6128
+ setError(message);
6129
+ errorRef.current?.(message);
6130
+ }
6131
+ };
6132
+ recognition.onend = () => {
6133
+ setListening(false);
6134
+ setInterim("");
6135
+ };
6136
+ recognitionRef.current = recognition;
6137
+ try {
6138
+ recognition.start();
6139
+ } catch {
6140
+ setListening(false);
6070
6141
  }
6071
- };
6072
- recognition.onend = () => {
6073
- setListening(false);
6074
- setInterim("");
6075
- };
6076
- recognitionRef.current = recognition;
6077
- try {
6078
- recognition.start();
6079
- } catch {
6080
- setListening(false);
6081
- }
6082
- }, [lang, stop]);
6142
+ },
6143
+ [lang]
6144
+ );
6145
+ beginRecognitionRef.current = beginRecognitionImpl;
6083
6146
  const toggle = React.useCallback(() => {
6084
6147
  if (listening) stop();
6085
6148
  else start();
@@ -6195,10 +6258,33 @@ function initialOf(value, fallback) {
6195
6258
  const source = (value || fallback).trim();
6196
6259
  return source ? source.slice(0, 1).toUpperCase() : "?";
6197
6260
  }
6261
+ function logoUrlOf(url) {
6262
+ try {
6263
+ const { origin, protocol } = new URL(url);
6264
+ if (protocol !== "https:" && protocol !== "http:") return null;
6265
+ return `${origin}/favicon.svg`;
6266
+ } catch {
6267
+ return null;
6268
+ }
6269
+ }
6198
6270
  function ReferenceCard({ reference }) {
6199
6271
  const [imageFailed, setImageFailed] = React.useState(false);
6272
+ const [logoFailed, setLogoFailed] = React.useState(false);
6200
6273
  const host = hostnameOf(reference.url);
6274
+ const logoUrl = logoUrlOf(reference.url);
6201
6275
  const showImage = Boolean(reference.imageUrl) && !imageFailed;
6276
+ const showLogo = Boolean(logoUrl) && !logoFailed;
6277
+ const letterPlate = /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold text-primary/70 sm:text-2xl", children: initialOf(reference.product, reference.title || host) }) });
6278
+ const logoImg = showLogo ? /* @__PURE__ */ jsxRuntime.jsx(
6279
+ "img",
6280
+ {
6281
+ src: logoUrl ?? "",
6282
+ alt: "",
6283
+ loading: "lazy",
6284
+ onError: () => setLogoFailed(true),
6285
+ className: "h-full w-full object-contain p-1.5 sm:p-0"
6286
+ }
6287
+ ) : letterPlate;
6202
6288
  return /* @__PURE__ */ jsxRuntime.jsxs(
6203
6289
  "a",
6204
6290
  {
@@ -6206,9 +6292,10 @@ function ReferenceCard({ reference }) {
6206
6292
  href: reference.url,
6207
6293
  target: "_blank",
6208
6294
  rel: "noopener noreferrer",
6209
- 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",
6295
+ 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",
6210
6296
  children: [
6211
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative aspect-[16/9] w-full overflow-hidden bg-muted", children: showImage ? /* @__PURE__ */ jsxRuntime.jsx(
6297
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative h-11 w-11 shrink-0 overflow-hidden rounded-lg bg-muted sm:hidden", children: logoImg }),
6298
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative hidden aspect-[16/9] w-full overflow-hidden bg-muted sm:block", children: showImage ? /* @__PURE__ */ jsxRuntime.jsx(
6212
6299
  "img",
6213
6300
  {
6214
6301
  src: reference.imageUrl ?? "",
@@ -6217,17 +6304,22 @@ function ReferenceCard({ reference }) {
6217
6304
  onError: () => setImageFailed(true),
6218
6305
  className: "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
6219
6306
  }
6220
- ) : (
6221
- /* Graceful fallback: a token-derived gradient plate, so a missing or
6222
- broken preview still reads as a deliberate card, not a hole. */
6223
- /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx("span", { className: "text-2xl font-semibold text-primary/70", children: initialOf(reference.product, reference.title || host) }) })
6224
- ) }),
6225
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col gap-1.5 p-3", children: [
6307
+ ) : showLogo ? /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(
6308
+ "img",
6309
+ {
6310
+ src: logoUrl ?? "",
6311
+ alt: "",
6312
+ loading: "lazy",
6313
+ onError: () => setLogoFailed(true),
6314
+ className: "max-h-full max-w-full object-contain"
6315
+ }
6316
+ ) }) : letterPlate }),
6317
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5 sm:gap-1.5 sm:p-3", children: [
6226
6318
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "line-clamp-2 text-sm font-semibold leading-snug text-card-foreground", children: reference.title || host || reference.url }),
6227
- reference.description ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "line-clamp-2 text-xs leading-relaxed text-muted-foreground", children: reference.description }) : null,
6228
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-auto flex items-center gap-2 pt-2", children: [
6229
- reference.product ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rounded-full bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground", children: reference.product }) : null,
6230
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-auto inline-flex min-w-0 items-center gap-1 text-xs text-muted-foreground", children: [
6319
+ reference.description ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "line-clamp-2 text-xs leading-relaxed text-muted-foreground max-sm:hidden", children: reference.description }) : null,
6320
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 sm:mt-auto sm:pt-2", children: [
6321
+ reference.product ? /* @__PURE__ */ jsxRuntime.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,
6322
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex min-w-0 items-center gap-1 text-xs text-muted-foreground sm:ml-auto", children: [
6231
6323
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ExternalLink, { className: "h-3 w-3 shrink-0" }),
6232
6324
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: host })
6233
6325
  ] })
@@ -6313,7 +6405,8 @@ function UserBubble({ message }) {
6313
6405
  function AssistantBubble({
6314
6406
  message,
6315
6407
  activity,
6316
- onNavigate
6408
+ onNavigate,
6409
+ anchorRef
6317
6410
  }) {
6318
6411
  const streaming = message.status === "PENDING" || message.status === "RUNNING";
6319
6412
  const body = message.content || message.partialContent || "";
@@ -6321,9 +6414,10 @@ function AssistantBubble({
6321
6414
  return /* @__PURE__ */ jsxRuntime.jsxs(
6322
6415
  "div",
6323
6416
  {
6417
+ ref: anchorRef,
6324
6418
  "data-boff-explore": "assistant",
6325
6419
  "data-status": message.status,
6326
- className: "flex gap-3",
6420
+ className: "flex scroll-mt-4 gap-3",
6327
6421
  children: [
6328
6422
  /* @__PURE__ */ jsxRuntime.jsx(
6329
6423
  "span",
@@ -6552,6 +6646,8 @@ function ExplorePage({
6552
6646
  });
6553
6647
  speechStopRef.current = speech.stop;
6554
6648
  const scrollRef = React.useRef(null);
6649
+ const latestAssistantRef = React.useRef(null);
6650
+ const alignedForRef = React.useRef(null);
6555
6651
  const stickToBottomRef = React.useRef(true);
6556
6652
  const composingRef = React.useRef(false);
6557
6653
  const busy = phase === "sending" || phase === "streaming";
@@ -6577,12 +6673,32 @@ function ExplorePage({
6577
6673
  if (!el) return;
6578
6674
  stickToBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
6579
6675
  }, []);
6676
+ const latestAssistantId = React.useMemo(() => {
6677
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
6678
+ const message = messages[i];
6679
+ if (message && message.role === "ASSISTANT") return message.id;
6680
+ }
6681
+ return null;
6682
+ }, [messages]);
6683
+ React.useEffect(() => {
6684
+ const el = scrollRef.current;
6685
+ const anchor = latestAssistantRef.current;
6686
+ if (!el || !anchor || !latestAssistantId) return;
6687
+ if (alignedForRef.current === latestAssistantId) return;
6688
+ if (!stickToBottomRef.current) return;
6689
+ if (anchor.offsetHeight === 0) return;
6690
+ alignedForRef.current = latestAssistantId;
6691
+ const delta = anchor.getBoundingClientRect().top - el.getBoundingClientRect().top;
6692
+ el.scrollTop = Math.max(0, el.scrollTop + delta - 12);
6693
+ }, [latestAssistantId, messages]);
6580
6694
  React.useEffect(() => {
6581
6695
  if (!stickToBottomRef.current) return;
6696
+ if (latestAssistantId && alignedForRef.current === latestAssistantId)
6697
+ return;
6582
6698
  const el = scrollRef.current;
6583
6699
  if (!el) return;
6584
6700
  el.scrollTop = el.scrollHeight;
6585
- }, [messages, activity]);
6701
+ }, [activity, latestAssistantId, messages]);
6586
6702
  const submitDraft = React.useCallback(() => {
6587
6703
  const text = draft.trim();
6588
6704
  if (!text || overLimit || busy || !canSend) return;
@@ -6782,7 +6898,8 @@ function ExplorePage({
6782
6898
  {
6783
6899
  message,
6784
6900
  activity,
6785
- onNavigate
6901
+ onNavigate,
6902
+ anchorRef: message.id === latestAssistantId ? latestAssistantRef : void 0
6786
6903
  },
6787
6904
  message.id
6788
6905
  )