@burdenoff/website-sdk 2026.1101.2 → 2026.1101.4
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/README.md +193 -5
- package/dist/avatar-live-view/index.js +16 -1
- package/dist/avatar-live-view/index.js.map +1 -1
- package/dist/avatar-live-view/index.mjs +16 -1
- package/dist/avatar-live-view/index.mjs.map +1 -1
- package/dist/{chunk-Y4V5CEWD.mjs → chunk-2LPDCXQH.mjs} +7 -4
- package/dist/chunk-2LPDCXQH.mjs.map +1 -0
- package/dist/{chunk-FFADMVIK.mjs → chunk-ETI2HQEC.mjs} +7 -5
- package/dist/chunk-ETI2HQEC.mjs.map +1 -0
- package/dist/{chunk-OYEUNZJY.js → chunk-PIXTBL63.js} +7 -3
- package/dist/chunk-PIXTBL63.js.map +1 -0
- package/dist/{chunk-O76QDGRM.js → chunk-R742FS5E.js} +8 -4
- package/dist/chunk-R742FS5E.js.map +1 -0
- package/dist/components/explore-cta.d.mts +7 -0
- package/dist/components/explore-cta.d.ts +7 -0
- package/dist/components/explore-cta.js +2 -2
- package/dist/components/explore-cta.mjs +1 -1
- package/dist/components/explore.js +13 -13
- package/dist/components/explore.mjs +1 -1
- package/dist/index.d.mts +95 -2
- package/dist/index.d.ts +95 -2
- package/dist/index.js +201 -38
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +168 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-FFADMVIK.mjs.map +0 -1
- package/dist/chunk-O76QDGRM.js.map +0 -1
- package/dist/chunk-OYEUNZJY.js.map +0 -1
- package/dist/chunk-Y4V5CEWD.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -372,9 +372,16 @@ publishes audio, video or data, and it sends the avatar nothing to say.
|
|
|
372
372
|
production nothing else is added, so no preflight has to complete while the
|
|
373
373
|
page unloads. The server ends sessions on its own deadlines as well.
|
|
374
374
|
- **Consent copy is versioned.** The panel shows a consent card only when it
|
|
375
|
-
holds reviewed copy for exactly the version the server names
|
|
376
|
-
|
|
377
|
-
|
|
375
|
+
holds reviewed copy for exactly the version the server names; otherwise the
|
|
376
|
+
view stays as it was. `web-consent-2026-11-02` is product-neutral: its card
|
|
377
|
+
reads "a suggested question" on every site, whatever `productName` is, so
|
|
378
|
+
every product's site shows the same words and the server can bind one notice
|
|
379
|
+
digest for all of them (the card's title, intro, processing, storage and
|
|
380
|
+
privacy-link lines, each ending with a line feed: 760 bytes, SHA-256
|
|
381
|
+
`2e02b051ceb1163c14bc58fa63a954a12c0d1f2695029a6d4805922d74c793f7`).
|
|
382
|
+
`web-consent-2026-10-31` (which names the product, "a suggested Healthy Bowl
|
|
383
|
+
question") and `web-consent-2026-11` stay, unchanged, for servers that still
|
|
384
|
+
name them.
|
|
378
385
|
- **How a session ended.** When the worker leaves, the server closes the room,
|
|
379
386
|
or the session's time runs out with the room still open, the view says only
|
|
380
387
|
"The live avatar has ended." (key `finished`). The browser
|
|
@@ -439,10 +446,191 @@ Testing hooks (`data-boff-explore`): `avatar-live` (with `data-phase`),
|
|
|
439
446
|
`avatar-live-captions`, `avatar-live-countdown`, `avatar-live-sound`,
|
|
440
447
|
`avatar-live-continue`, `avatar-live-back`, `avatar-live-bot-check`.
|
|
441
448
|
|
|
449
|
+
#### Site assistant dock (`SiteAssistantDock`, every product site)
|
|
450
|
+
|
|
451
|
+
`SiteAssistantDock` is the site-wide assistant in one component: the floating
|
|
452
|
+
"Ask AI" button, which opens the assistant panel (`ExplorePanel`) over the
|
|
453
|
+
current page, and that panel, with the site's product scope and its avatar. It
|
|
454
|
+
replaces `<ExploreCta variant="floating" />` in a site's layout. The behaviour
|
|
455
|
+
is HealthyBowl's, as approved there (BOFF-9697, BOFF-9796):
|
|
456
|
+
|
|
457
|
+
- The button is still a real link to `/explore`: a modified click (new tab) and
|
|
458
|
+
a visitor without JavaScript reach the page; a plain click opens the compact
|
|
459
|
+
panel without leaving the page.
|
|
460
|
+
- Expand and Restore keep the same conversation, and so do closing, reopening
|
|
461
|
+
and going to `/explore` and back: the panel and `/explore` share the
|
|
462
|
+
provider's one conversation. Nothing is asked of a server until the first
|
|
463
|
+
opening; the dock starts the provider's conversation then.
|
|
464
|
+
- Escape and the phone's Back go expanded, then compact, then closed, through
|
|
465
|
+
history entries of the panel's own (the same URL, so the router stays put).
|
|
466
|
+
Closing stops the microphone and reading aloud, and focus goes back to the
|
|
467
|
+
button, which carries `aria-controls` and `aria-expanded`.
|
|
468
|
+
- On `/explore` (`explorePath`, matched as the router matches it: ignoring case
|
|
469
|
+
and a trailing slash) there is no button and the panel hides; an open panel
|
|
470
|
+
shows again on the way out.
|
|
471
|
+
- The panel keeps `bottomOffset` clear at the bottom of the window, so it never
|
|
472
|
+
covers the cookie banner's controls, and the floating button sits that much
|
|
473
|
+
higher (`calc(1.25rem + bottomOffset)`), so the banner does not cover the
|
|
474
|
+
button before the visitor answers it. Unset, the button is where it always was.
|
|
475
|
+
- One dock, one panel, one conversation: render it once, in the layout every
|
|
476
|
+
route shares. A second dock mounted while one is there renders nothing, in
|
|
477
|
+
every build (a development build also logs an error); if the first one
|
|
478
|
+
unmounts, the second takes over.
|
|
479
|
+
|
|
480
|
+
It **needs an `ExploreChatProvider` above it**. Without one a development build
|
|
481
|
+
throws `SiteAssistantDock must be rendered under an ExploreChatProvider`, and a
|
|
482
|
+
production build logs that error and renders the plain floating link to
|
|
483
|
+
`/explore` that sites had before the dock.
|
|
484
|
+
|
|
485
|
+
A site adds these lines.
|
|
486
|
+
|
|
487
|
+
1. **The provider, at the app root**, inside the router and around the routes.
|
|
488
|
+
The dock starts it on the first opening, so `enabled` is for `/explore` only:
|
|
489
|
+
|
|
490
|
+
```tsx
|
|
491
|
+
import { ExploreChatProvider, normalizeRoutePath } from '@burdenoff/website-sdk'
|
|
492
|
+
|
|
493
|
+
function SiteShell() {
|
|
494
|
+
const { pathname, search } = useLocation()
|
|
495
|
+
const onExplore = normalizeRoutePath(pathname) === '/explore'
|
|
496
|
+
// A shared conversation is /explore?c=<token> and nothing else.
|
|
497
|
+
const shareToken = onExplore ? new URLSearchParams(search).get('c') : null
|
|
498
|
+
const pageUrl =
|
|
499
|
+
typeof window === 'undefined' ? undefined : new URL(`${pathname}${search}`, window.location.origin).href
|
|
500
|
+
return (
|
|
501
|
+
<ExploreChatProvider
|
|
502
|
+
key={shareToken ?? ''}
|
|
503
|
+
enabled={onExplore}
|
|
504
|
+
productSlug={PRODUCT_SLUG}
|
|
505
|
+
locale={ASSISTANT.locale}
|
|
506
|
+
pageUrl={pageUrl}
|
|
507
|
+
shareToken={shareToken}
|
|
508
|
+
>
|
|
509
|
+
<AppRoutes />
|
|
510
|
+
</ExploreChatProvider>
|
|
511
|
+
)
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
2. **The config objects** (`src/config/assistant.ts`), built once at module
|
|
516
|
+
scope so the panel's memoised prompt list is not rebuilt on every render:
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
import { websiteAvatarLive } from '@burdenoff/website-sdk'
|
|
520
|
+
import manifest from '@/data/avatar-manifest.<site>.en.json'
|
|
521
|
+
|
|
522
|
+
export const ASSISTANT = {
|
|
523
|
+
productSlug: PRODUCT_SLUG,
|
|
524
|
+
scope: 'product',
|
|
525
|
+
productName: 'Healthy Bowl', // the brand as a reader sees it
|
|
526
|
+
examplePrompts: PROBLEMS.map(({ problem }) => problem), // the questions the manifest lists
|
|
527
|
+
locale: import.meta.env.VITE_DEPLOY_ENV === 'alpha' ? 'en' : undefined,
|
|
528
|
+
} as const
|
|
529
|
+
|
|
530
|
+
export const AVATAR = {
|
|
531
|
+
src: '/avatar-faces/av_stock_001.webp',
|
|
532
|
+
live: websiteAvatarLive({
|
|
533
|
+
deployEnv: import.meta.env.VITE_DEPLOY_ENV,
|
|
534
|
+
siteKey: import.meta.env.VITE_AVATAR_TURNSTILE_SITE_KEY,
|
|
535
|
+
site: '<site>', // as the live avatar service knows it: `healthybowl`
|
|
536
|
+
manifestVersion: manifest.version,
|
|
537
|
+
load: () => import('@burdenoff/website-sdk/avatar-live'),
|
|
538
|
+
}),
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
3. **The dock in the layout**, in place of `<ExploreCta variant="floating" />`:
|
|
543
|
+
|
|
544
|
+
```tsx
|
|
545
|
+
import { SiteAssistantDock } from '@burdenoff/website-sdk'
|
|
546
|
+
|
|
547
|
+
const { pathname } = useLocation()
|
|
548
|
+
const navigate = useNavigate()
|
|
549
|
+
|
|
550
|
+
<SiteAssistantDock
|
|
551
|
+
{...ASSISTANT}
|
|
552
|
+
avatar={AVATAR}
|
|
553
|
+
currentPath={pathname}
|
|
554
|
+
onNavigate={(path) => navigate(path)}
|
|
555
|
+
bottomOffset="var(--hb-cookie-banner-height, 0px)" // YOUR banner's variable, see below
|
|
556
|
+
/>
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
`bottomOffset` must name **the site's own cookie banner's height
|
|
560
|
+
variable**. The SDK sets no such variable and there is no shared name:
|
|
561
|
+
HealthyBowl's banner publishes `--hb-cookie-banner-height`, and another
|
|
562
|
+
site passes whatever its banner publishes. A banner that publishes none
|
|
563
|
+
leaves the panel and the button under it. We recommend every site's banner
|
|
564
|
+
publish its height as a CSS variable on `<html>` while it shows, and remove
|
|
565
|
+
it when it goes:
|
|
566
|
+
|
|
567
|
+
```tsx
|
|
568
|
+
// In the site's cookie banner component.
|
|
569
|
+
const bannerRef = useRef<HTMLDivElement>(null)
|
|
570
|
+
useLayoutEffect(() => {
|
|
571
|
+
const banner = bannerRef.current
|
|
572
|
+
if (!banner) return
|
|
573
|
+
const root = document.documentElement
|
|
574
|
+
const publish = () =>
|
|
575
|
+
root.style.setProperty('--<site>-cookie-banner-height', `${banner.offsetHeight}px`)
|
|
576
|
+
publish()
|
|
577
|
+
const observer = new ResizeObserver(publish) // wrapping text, a resized window
|
|
578
|
+
observer.observe(banner)
|
|
579
|
+
return () => {
|
|
580
|
+
observer.disconnect()
|
|
581
|
+
root.style.removeProperty('--<site>-cookie-banner-height') // answered: nothing to keep clear
|
|
582
|
+
}
|
|
583
|
+
}, [])
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
and keep the `, 0px` fallback in `bottomOffset`, so the dock sits at its
|
|
587
|
+
usual place whenever the banner is not on screen.
|
|
588
|
+
|
|
589
|
+
4. **The build env var** `VITE_AVATAR_TURNSTILE_SITE_KEY`: the site's
|
|
590
|
+
Cloudflare Turnstile site key. `websiteAvatarLive` returns `undefined` (the
|
|
591
|
+
still picture and "Live avatar coming soon", no live avatar) when it is
|
|
592
|
+
unset or empty; on alpha (`VITE_DEPLOY_ENV=alpha`) it accepts Cloudflare's
|
|
593
|
+
always-pass test key `1x00000000000000000000AA` (the alpha gateway runs
|
|
594
|
+
Turnstile in dummy mode) or a real key; everywhere else, production
|
|
595
|
+
included, only a real-looking key (`0x…`, letters, digits, `_` and `-`, 18
|
|
596
|
+
to 64 characters) that is not one of Cloudflare's test keys (`1x…AA`,
|
|
597
|
+
`2x…AB`, `1x…BB`, `2x…BB`, `3x…FF`, or anything of their shape). Install
|
|
598
|
+
`livekit-client` on the site (see [Live avatar](#live-avatar-opt-in-receive-only)),
|
|
599
|
+
and declare the variable in `vite-env.d.ts`.
|
|
600
|
+
`websiteAvatarLive` trusts `VITE_DEPLOY_ENV`, so a production build must
|
|
601
|
+
never be labelled `alpha` (that would let a test key through the check);
|
|
602
|
+
the production gateway's real Turnstile secret would still reject a test
|
|
603
|
+
token, so the live avatar would fail to start rather than run unchecked.
|
|
604
|
+
|
|
605
|
+
5. **The privacy anchor** `/privacy#live-avatar`: the live avatar's consent
|
|
606
|
+
card links there, so the site's privacy page needs an element with
|
|
607
|
+
`id="live-avatar"` describing the live avatar.
|
|
608
|
+
|
|
609
|
+
6. **The still picture** `public/avatar-faces/av_stock_001.webp`: the approved
|
|
610
|
+
avatar picture the Avatar view shows (and shows instead of the live avatar
|
|
611
|
+
when it is off), copied from HealthyBowl's website.
|
|
612
|
+
|
|
613
|
+
**`SiteAssistantDock` props**
|
|
614
|
+
|
|
615
|
+
| Prop | Default | What it does |
|
|
616
|
+
|---|---|---|
|
|
617
|
+
| `productSlug`, `scope`, `productName`, `examplePrompts`, `locale` | – | Passed to the panel's chat unchanged. See [One product's own assistant](#one-products-own-assistant). |
|
|
618
|
+
| `avatar` | – | `{ src, name?, live? }`: the Avatar view's still picture, and `live` (from `websiteAvatarLive`) to opt in to the live avatar. Absent: no Avatar view. |
|
|
619
|
+
| `onNavigate` | – | The router's navigate, for links in the conversation. |
|
|
620
|
+
| `bottomOffset` | `0px` | A CSS length kept clear at the bottom: the site's own cookie banner's height variable (HealthyBowl: `var(--hb-cookie-banner-height, 0px)`). The panel keeps it clear and the floating button sits that much higher. |
|
|
621
|
+
| `className` | – | Extra classes for the floating button. |
|
|
622
|
+
| `panelClassName` | – | Extra classes for the panel. |
|
|
623
|
+
| `currentPath` | `window.location.pathname` | The router's path. Pass it: the fallback cannot see `pushState` navigations. |
|
|
624
|
+
| `explorePath` | `/explore` | The button's link, the route the dock hides on, and the panel's share links. |
|
|
625
|
+
| `label` | `Ask AI` | The button's label and accessible name. |
|
|
626
|
+
| `id` | `boff-explore-panel` | The panel's id, for the button's `aria-controls`. |
|
|
627
|
+
| `zIndex` | `50` | The panel's `z-index`. |
|
|
628
|
+
|
|
442
629
|
**`ExploreCta` props**: `variant` (`'header' \| 'mobile' \| 'floating'`, required),
|
|
443
630
|
`href` (default `/explore`), `label` (default `Ask AI`), `className`,
|
|
444
|
-
`onNavigate`, `onClick`, `currentPath
|
|
445
|
-
|
|
631
|
+
`onNavigate`, `onClick`, `currentPath`, and, for `floating` only,
|
|
632
|
+
`bottomOffset` (a CSS length to lift the bubble by, as on the dock). The
|
|
633
|
+
`floating` variant hides itself on `/explore`. Pass `currentPath` from your router — the fallback
|
|
446
634
|
(`window.location.pathname` + `popstate`) cannot observe SPA `pushState`
|
|
447
635
|
navigations.
|
|
448
636
|
|
|
@@ -474,6 +474,21 @@ var CONSENT = /* @__PURE__ */ new Map([
|
|
|
474
474
|
privacyLink: "How we handle this"
|
|
475
475
|
}
|
|
476
476
|
}
|
|
477
|
+
],
|
|
478
|
+
[
|
|
479
|
+
// The words of web-consent-2026-10-31 with no product name in them: one notice, byte for byte, on every product's
|
|
480
|
+
// site, because the server binds one consent version and one notice digest for all of them.
|
|
481
|
+
"web-consent-2026-11-02",
|
|
482
|
+
{
|
|
483
|
+
en: {
|
|
484
|
+
title: "Live AI avatar (beta)",
|
|
485
|
+
introProduct: null,
|
|
486
|
+
intro: "This is an AI avatar, not a real person. It reads the assistant's first eligible answer to a suggested question aloud for up to a minute. The answer is AI-generated and can be wrong.",
|
|
487
|
+
processing: "When you start, Cloudflare checks for bots. The answer text goes to Google to create the voice. Our server sends that voice through LiveKit to Protoface, which makes the avatar video streamed through LiveKit. Your browser connects to LiveKit, which sees your IP address.",
|
|
488
|
+
storage: "Your microphone and camera are not used. We do not record the live session or store the generated voice. We keep a record of this consent. The privacy notice explains our safeguards, storage periods and the providers' processing. For people aged eighteen and over.",
|
|
489
|
+
privacyLink: "How we handle this"
|
|
490
|
+
}
|
|
491
|
+
}
|
|
477
492
|
]
|
|
478
493
|
]);
|
|
479
494
|
function avatarConsentCopy(version, locale) {
|
|
@@ -1229,7 +1244,7 @@ function AvatarLiveView({
|
|
|
1229
1244
|
className: "w-full max-w-sm rounded-lg border border-border p-4 text-start",
|
|
1230
1245
|
children: [
|
|
1231
1246
|
/* @__PURE__ */ jsxRuntime.jsx("h2", { id: consentTitleId, className: "text-base font-semibold", children: copy.title }),
|
|
1232
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm", children: productName === null ? copy.intro : chunkO4GS3XPZ_js.fill(copy.introProduct, { product: productName }) }),
|
|
1247
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm", children: productName === null || copy.introProduct === null ? copy.intro : chunkO4GS3XPZ_js.fill(copy.introProduct, { product: productName }) }),
|
|
1233
1248
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: copy.processing }),
|
|
1234
1249
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: copy.storage }),
|
|
1235
1250
|
/* @__PURE__ */ jsxRuntime.jsxs(
|