npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@livelayer/react

v0.12.6

Published

LiveLayer agent widget for React — avatar video, team switching, responsive layouts, full-fidelity embed

Downloads

8,993

Readme

@livelayer/react

Drop-in voice/video AI agent widget for React apps. The full-fidelity widget that powers app.livelayer.studio, packaged for direct mount in your app's DOM (no iframe).

Quickstart (5 minutes)

Three files, one published agent, working voice nav.

1. Install

npm install @livelayer/react
# or pnpm add @livelayer/react / yarn add @livelayer/react

2. Get an agent ID — go to app.livelayer.studio, publish an agent, copy its ID (looks like cmobfeluv000bju04ct1cqdb0).

3. Mount the widget (Next.js App Router shown — works the same way in any React app):

"use client";

import { AvatarWidget } from "@livelayer/react";
import "@livelayer/react/styles.css";
import { useRouter, usePathname } from "next/navigation";

export default function Layout({ children }: { children: React.ReactNode }) {
  const router = useRouter();
  const pathname = usePathname();

  return (
    <>
      {children}
      <AvatarWidget
        agentId="cmobfeluv000bju04ct1cqdb0"
        pathname={pathname}
        onNavigate={(href) => router.push(href)}
        hideOn={["/privacy", "/terms", "/legal/*"]}
      />
    </>
  );
}

That's it. The widget docks bottom-right, the agent can navigate users to other pages by voice, and it stays out of the way on legal pages. The LiveKit session survives every SPA route change.

Common gotcha: if the widget renders unstyled, check that you imported @livelayer/react/styles.css. It's a separate import to give consumers the option to scope styles.


Structured data collection (0.12.0)

The agent can run guided, typed Q&A flows backed by LiveKit's voice.AgentTask + beta.workflows.TaskGroup primitives. No new components, no special attributes — just write regular HTML forms. Every <form> is auto-discovered.

import { AvatarWidget } from "@livelayer/react";

function App() {
  return (
    <>
      <AvatarWidget
        agentId="agent_abc"
        onCollect={(result) => {
          // result.results keyed by field name — ship to your CRM.
          fetch("/api/leads", {
            method: "POST",
            body: JSON.stringify(result),
          });
        }}
      />

      {/* Plain HTML — the agent finds this. */}
      <form>
        <label>Email <input name="email" type="email" required /></label>
        <label>Company <input name="company" /></label>
        <button type="submit">Subscribe</button>
      </form>
    </>
  );
}

The agent infers each field's label from the wrapping <label> / aria-label / placeholder, infers the kind from type=, runs a TaskGroup when the visitor wants to fill the form by voice, normalizes each spoken answer per-kind, and paints values into the matching [name="..."] inputs live as it records each one. onCollect fires once with the typed payload when the run finishes.

Streaming per-field updates for a progress UI:

import { useCollect } from "@livelayer/react";

function Progress() {
  const { fields, isCollecting, lastResult } = useCollect();
  return Object.entries(fields).map(([name, value]) => (
    <div key={name}>{name}: {value}</div>
  ));
}

Opt-out when you don't want a form / input visible to the agent:

<form data-ll-skip>...</form>           // exclude the whole form
<input data-ll-private />               // exclude one input
<form data-ll-intent="request a demo">  // disambiguate (still visible)

type="password", autocomplete="cc-*", and autocomplete="off" are ALWAYS excluded — you don't have to mark them.

See docs.livelayer.studio/develop/data-collection for the full result shape, dashboard-declared field lists, slide-level data collection, capability gating, and webhook delivery.


Recipes

1. Voice navigation in Next.js / React Router

Pass your router into onNavigate. When the agent emits a navigate command, the widget calls your callback. The session never reloads.

// Next.js App Router
import { useRouter } from "next/navigation";
const router = useRouter();
<AvatarWidget agentId="..." onNavigate={(href) => router.push(href)} />

// React Router v6
import { useNavigate } from "react-router-dom";
const navigate = useNavigate();
<AvatarWidget agentId="..." onNavigate={navigate} />

If you don't pass onNavigate, the widget falls back to (1) clicking a matching <a href="..."> in the DOM (Next.js <Link> and React Router <Link> both intercept these), then (2) history.pushState for plain HTML pages. It never uses window.location — that's a hard reload that would kill the call.

You also need to register a navigate tool on your agent so it can emit the command. In your agent's tool schema:

{
  "name": "navigate",
  "description": "Take the user to a different page on this site.",
  "parameters": {
    "type": "object",
    "properties": { "href": { "type": "string" } },
    "required": ["href"]
  }
}

When the LLM calls navigate({ href: "/pricing" }), your agent server publishes { type: "navigate", href: "/pricing" } on the data channel. The widget handles the rest.

2. Hide on sensitive routes

<AvatarWidget
  agentId="..."
  pathname={usePathname()}
  hideOn={["/privacy", "/terms", "/cookies", "/legal/**"]}
/>

Glob rules:

  • * matches one path segment: /admin/*/admin/users but not /admin/users/edit
  • ** matches any depth: /admin/**/admin, /admin/users, /admin/users/edit
  • A RegExp or function works too: hideOn={[/^\/blog\/draft-.+$/, (p) => p.startsWith("/internal")]}

The LiveKit session stays alive while hidden. When the user navigates back to an allowed route, the call resumes seamlessly.

showOn is the inverse — restrict to a whitelist. hideOn wins on collisions.

3. Let the agent see the page

When the agent asks "what's the user looking at?", the widget walks the DOM and sends back a structured snapshot. You don't need to do anything for this to work, but you can guide it with <LiveLayerRegion>:

import { LiveLayerRegion } from "@livelayer/react";

<LiveLayerRegion id="pricing" intent="show pricing tiers">
  <PricingTable />
</LiveLayerRegion>

This renders a <div data-ll-region="pricing" data-ll-intent="show pricing tiers"> that the page-context extractor surfaces with priority. The intent is author-language for the agent.

To register the agent-side tool:

{
  "name": "getPageContext",
  "description": "Snapshot of what the user is currently looking at — useful when they ask 'what is this' or 'show me the X'.",
  "parameters": { "type": "object", "properties": {} }
}

When the LLM calls it, your agent publishes { type: "request_page_context" } and waits for the widget's { type: "page_context", context: {...} } response (typically <100ms).

You can override the default extractor entirely:

<AvatarWidget
  getPageContext={() => ({
    url: window.location.href,
    pathname: window.location.pathname,
    title: document.title,
    regions: [{ id: "cart", text: cartSummary }],
    visibleText: "",
    visibleLinks: [],
    visibleFields: [],
  })}
/>

Or attach extra app state without replacing the walker:

<AvatarWidget
  pageContextExtras={{ userId: user.id, cartItemCount: items.length }}
/>

4. Let the agent click + scroll + fill forms (0.4.0)

Click anything the agent should be able to trigger: tag interactive elements with data-ll-action (or any selector you want — button[aria-label="..."] works too).

<button data-ll-action="open-pricing-modal" onClick={openPricing}>
  See pricing
</button>

The agent emits { type: "click", selector: "[data-ll-action='open-pricing-modal']" } and the widget triggers a click. Use onNavigate for nav-shaped clicksclick is for buttons, dialog toggles, expand/collapse, etc.

Page scrolling: the agent can call scroll_page with direction: "up" | "down" | "top" | "bottom". Default behavior scrolls the window by ±1 viewport height. Override with onScrollPage for custom scroll containers.

Forms — auto-discovered. Just write regular HTML:

<form onSubmit={handleSubmit}>
  <label>Name <input name="name" /></label>
  <label>Email <input name="email" type="email" /></label>
  <label>Message <textarea name="message" /></label>
  <button type="submit">Send</button>
</form>

The agent sees these in PageContext.forms and calls:

  • fill_form — sets values via the canonical native-setter pattern (your onChange listeners fire correctly). Use when the agent already has all the answers.
  • collect_from_page — runs a guided sub-conversation that asks for each field one at a time, normalizes spoken input per kind (email letter-by-letter, phone digit grouping, etc), and delivers a typed onCollect payload. Use when the visitor wants to fill the form by voice. See Structured data collection above.
  • submit_form — calls form.requestSubmit(). Publishes { type: "form_submitted", formId } on success or { type: "form_submit_blocked", formId, reason: "validation" } on HTML5 validation failure.

Form IDs are inferred from the form's existing id / name attribute, falling back to a data-ll-intent slug, finally form_<index>.

Opt-out for privacy: <form data-ll-skip>...</form> and <input data-ll-private /> keep things out of the agent's view. type="password", autocomplete="cc-*", and autocomplete="off" are ALWAYS excluded — card fields belong in Stripe Elements; we will not be the rail.

Routes: the agent can call request_routes to get up to 200 deduped <a href> entries from the page (internal flagged separately from external). Useful for "where can I go?" prompts.

5. Restrict what the agent can do (0.4.0)

Compliance / safety knob: pass an allowlist.

<AvatarWidget
  agentId="..."
  capabilities={["read_page", "navigate", "scroll", "fill_forms"]}
  // not in list: "click", "submit_forms"
/>

| Capability | Commands gated | |---|---| | navigate | navigate | | scroll | scroll_to, scroll_page | | click | click | | fill_forms | fill_form, focus_field | | submit_forms | submit_form | | read_page | request_page_context, request_routes |

Default (capabilities undefined) = all enabled. Recommended starter: omit submit_forms for the first few weeks of production. Filling is reversible, submitting isn't.

6. Persist the session across pages (multi-page apps)

For SPAs (Next.js, Remix, React Router), mount the widget at the app root and the session survives route changes automatically. For multi-page apps where the entire React tree unmounts, use controlledSession to own the LiveKit Room yourself and keep it alive across reloads. See the ControlledSession interface for the contract.

7. Custom branding

<AvatarWidget
  branding={{
    primaryColor: "#0ea5e9",
    accentColor: "#f59e0b",
    productName: "Acme Concierge",
    logoUrl: "/logo.png",
  }}
/>

API reference

<AvatarWidget> (primary)

All props are optional except agentId.

| Prop | Type | Description | |---|---|---| | agentId | string | Required. The published agent ID. | | apiKey | string | API key for cross-origin auth. Required if your agent isn't public. | | baseUrl | string | Base URL of the LiveLayer API. Defaults to https://app.livelayer.studio. | | pathname | string | Current pathname. Required for Next.js App Router and React Router v6+. Pass usePathname() / useLocation().pathname. | | showOn | RoutePattern[] | Render only on matching paths. | | hideOn | RoutePattern[] | Never render on matching paths. Wins over showOn. | | onNavigate | (href: string) => void | Called on agent navigate command. Wire to your router. | | onScrollToSelector | (sel, behavior?) => void | Called on agent scroll_to command. Default: scrollIntoView({ behavior: "smooth" }). | | onScrollPage | (direction, behavior?) => void | Called on agent scroll_page command. Default: window.scrollBy / scrollTo. | | onClick | (selector: string) => void | Called on agent click command. Default: document.querySelector(selector)?.click(). | | getPageContext | () => PageContext \| Promise<PageContext> | Override the default DOM walker. | | pageContextExtras | Record<string, unknown> | Extra app state attached to every page context snapshot. | | capabilities | AgentCapability[] | Allowlist gating which commands the agent can run. | | position | "top-left" \| "top-right" \| "bottom-left" \| "bottom-right" \| "custom" | Where the widget docks. Defaults to "bottom-right". | | defaultDisplayMode | "hidden" \| "minimized" \| "expanded" | Initial display mode. | | branding | BrandingConfig | Colors, product name, logo. | | teamMembers | TeamMember[] | Multi-agent picker. | | controlledSession | ControlledSession | Bring-your-own LiveKit Room. | | onAgentCommand | (cmd) => void | Receive non-universal data-channel commands. | | onAgentEvent | (e) => void | Receive ALL data-channel events (including the universal ones). |

<LiveLayerRegion> (page-context primitive)

<LiveLayerRegion id="pricing" intent="show pricing tiers" as="section">
  ...
</LiveLayerRegion>

Renders a wrapper element with data-ll-region + data-ll-intent that the page-context extractor prioritizes.

<LiveLayerForm> + <LiveLayerField> (form primitives, 0.4.0)

<LiveLayerForm id="signup" intent="create account" onSubmit={handleSubmit}>
  <LiveLayerField name="email" label="Email" type="email" />
  <LiveLayerField name="bio" as="textarea" label="Bio" />
  <LiveLayerField name="role" as="select" label="Role">
    <option value="dev">Developer</option>
    <option value="pm">PM</option>
  </LiveLayerField>
  <button type="submit">Sign up</button>
</LiveLayerForm>

Equivalent to raw HTML with data-ll-form + data-ll-field attributes. Untagged forms remain invisible to the agent.

Hooks (power users)

useLiveKitSession, useDisplayMode, useAgentInfo, usePathname, useRouteMatch, useAudioLevel, useMicrophoneState, useCameraState, useScreenShareState, useMediaDevices, useTranscript. All exported from the package root.

Types

AvatarWidgetProps, RoutePattern, PageContext, AgentCommand, AgentEventDetail, TeamMember, BrandingConfig, WidgetPosition, DisplayMode. All exported from the package root.


Privacy

The default page-context walker never extracts:

  • Form values (only labels and field types)
  • Inputs with type="password"
  • Inputs with autocomplete="cc-*" or autocomplete="off"
  • Elements (and their subtrees) with data-ll-private="true"
  • The widget itself (.ll-widget)

To redact additional content:

<div data-ll-private="true">
  <UserBankAccount />
</div>

Or override getPageContext entirely to control exactly what reaches the agent.


Migrating from 0.2.x

0.3.0 is additive. All existing 0.2.x code continues to work without changes.

Soft breaking — observability only: the data-channel commands navigate, scroll_to, and request_page_context are now handled internally by the widget and no longer reach onAgentCommand. If you previously observed them via that callback (unlikely — they were never emitted in 0.2.x), switch to onAgentEvent, which still fires for every message.


Errors and warnings

Every console message from this package starts with [LiveLayer] and includes a doc URL. Examples:

[LiveLayer] Agent emitted "navigate" without href. Skipping.
            Check your agent's tool schema.
            See https://livelayer.studio/docs/errors/navigate-missing-href

[LiveLayer] scroll_to: no element matched "#pricing-table".
            The user may be on a different page.
            See https://livelayer.studio/docs/errors/scroll-no-match

If you see one of these in production, the doc URL has the explanation and remediation.


Legacy: <LiveLayerWidget>

The thin web-component wrapper from 0.1.x is still exported for backwards compatibility. New apps should use <AvatarWidget>.

import { LiveLayerWidget } from "@livelayer/react";
<LiveLayerWidget agentId="..." />

Peer dependencies

  • react >= 18.0.0
  • react-dom >= 18.0.0

No router peer dependency. Works with Next.js App Router, Next.js Pages Router, React Router (any version), Remix, TanStack Router, or no router at all.

License

MIT