@livelayer/react 0.2.6 → 0.3.0

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 CHANGED
@@ -1,58 +1,289 @@
1
1
  # @livelayer/react
2
2
 
3
- React component wrapper for the LiveLayer agent widget.
3
+ Drop-in voice/video AI agent widget for React apps. The full-fidelity widget that powers [app.livelayer.studio](https://app.livelayer.studio), packaged for direct mount in your app's DOM (no iframe).
4
4
 
5
- ## Installation
5
+ ## Quickstart (5 minutes)
6
+
7
+ Three files, one published agent, working voice nav.
8
+
9
+ **1. Install**
6
10
 
7
11
  ```bash
8
12
  npm install @livelayer/react
13
+ # or pnpm add @livelayer/react / yarn add @livelayer/react
9
14
  ```
10
15
 
11
- ## Usage
16
+ **2. Get an agent ID** — go to [app.livelayer.studio](https://app.livelayer.studio), publish an agent, copy its ID (looks like `cmobfeluv000bju04ct1cqdb0`).
17
+
18
+ **3. Mount the widget** (Next.js App Router shown — works the same way in any React app):
12
19
 
13
20
  ```tsx
14
- import { LiveLayerWidget } from "@livelayer/react";
21
+ "use client";
22
+
23
+ import { AvatarWidget } from "@livelayer/react";
24
+ import "@livelayer/react/styles.css";
25
+ import { useRouter, usePathname } from "next/navigation";
26
+
27
+ export default function Layout({ children }: { children: React.ReactNode }) {
28
+ const router = useRouter();
29
+ const pathname = usePathname();
15
30
 
16
- function App() {
17
31
  return (
18
- <LiveLayerWidget
19
- agentId="your_agent_id"
20
- onAgentEvent={(e) => console.log(e.eventName, e.data)}
21
- />
32
+ <>
33
+ {children}
34
+ <AvatarWidget
35
+ agentId="cmobfeluv000bju04ct1cqdb0"
36
+ pathname={pathname}
37
+ onNavigate={(href) => router.push(href)}
38
+ hideOn={["/privacy", "/terms", "/legal/*"]}
39
+ />
40
+ </>
22
41
  );
23
42
  }
24
43
  ```
25
44
 
26
- ## Props
45
+ 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.
27
46
 
28
- | Prop | Type | Required | Description |
29
- | --------------- | --------------------------------------- | -------- | --------------------------------------------------- |
30
- | `agentId` | `string` | Yes | The published agent ID to connect to |
31
- | `mode` | `"WIDGET" \| "EMBEDDED"` | No | Override the experience mode from the agent config |
32
- | `onAgentEvent` | `(event: AgentEventDetail) => void` | No | Callback fired when the agent emits a data channel event |
33
- | `className` | `string` | No | CSS class name on the wrapper div |
34
- | `style` | `React.CSSProperties` | No | Inline styles on the wrapper div |
47
+ > **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.
35
48
 
36
- ### AgentEventDetail
49
+ ---
37
50
 
38
- ```ts
39
- interface AgentEventDetail {
40
- eventName: string;
41
- data: Record<string, unknown>;
51
+ ## Recipes
52
+
53
+ ### 1. Voice navigation in Next.js / React Router
54
+
55
+ Pass your router into `onNavigate`. When the agent emits a `navigate` command, the widget calls your callback. The session never reloads.
56
+
57
+ ```tsx
58
+ // Next.js App Router
59
+ import { useRouter } from "next/navigation";
60
+ const router = useRouter();
61
+ <AvatarWidget agentId="..." onNavigate={(href) => router.push(href)} />
62
+
63
+ // React Router v6
64
+ import { useNavigate } from "react-router-dom";
65
+ const navigate = useNavigate();
66
+ <AvatarWidget agentId="..." onNavigate={navigate} />
67
+ ```
68
+
69
+ 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.**
70
+
71
+ You also need to register a `navigate` tool on your agent so it can emit the command. In your agent's tool schema:
72
+
73
+ ```json
74
+ {
75
+ "name": "navigate",
76
+ "description": "Take the user to a different page on this site.",
77
+ "parameters": {
78
+ "type": "object",
79
+ "properties": { "href": { "type": "string" } },
80
+ "required": ["href"]
81
+ }
42
82
  }
43
83
  ```
44
84
 
45
- ## Peer Dependencies
85
+ When the LLM calls `navigate({ href: "/pricing" })`, your agent server publishes `{ type: "navigate", href: "/pricing" }` on the data channel. The widget handles the rest.
86
+
87
+ ### 2. Hide on sensitive routes
88
+
89
+ ```tsx
90
+ <AvatarWidget
91
+ agentId="..."
92
+ pathname={usePathname()}
93
+ hideOn={["/privacy", "/terms", "/cookies", "/legal/**"]}
94
+ />
95
+ ```
96
+
97
+ Glob rules:
98
+ - `*` matches one path segment: `/admin/*` → `/admin/users` but not `/admin/users/edit`
99
+ - `**` matches any depth: `/admin/**` → `/admin`, `/admin/users`, `/admin/users/edit`
100
+ - A `RegExp` or function works too: `hideOn={[/^\/blog\/draft-.+$/, (p) => p.startsWith("/internal")]}`
101
+
102
+ The LiveKit session **stays alive** while hidden. When the user navigates back to an allowed route, the call resumes seamlessly.
103
+
104
+ `showOn` is the inverse — restrict to a whitelist. `hideOn` wins on collisions.
105
+
106
+ ### 3. Let the agent see the page
107
+
108
+ 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>`:
109
+
110
+ ```tsx
111
+ import { LiveLayerRegion } from "@livelayer/react";
112
+
113
+ <LiveLayerRegion id="pricing" intent="show pricing tiers">
114
+ <PricingTable />
115
+ </LiveLayerRegion>
116
+ ```
117
+
118
+ 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.
46
119
 
47
- This package requires React 18 or later:
120
+ To register the agent-side tool:
48
121
 
49
122
  ```json
50
123
  {
51
- "react": ">=18.0.0",
52
- "react-dom": ">=18.0.0"
124
+ "name": "getPageContext",
125
+ "description": "Snapshot of what the user is currently looking at — useful when they ask 'what is this' or 'show me the X'.",
126
+ "parameters": { "type": "object", "properties": {} }
53
127
  }
54
128
  ```
55
129
 
130
+ 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).
131
+
132
+ You can override the default extractor entirely:
133
+
134
+ ```tsx
135
+ <AvatarWidget
136
+ getPageContext={() => ({
137
+ url: window.location.href,
138
+ pathname: window.location.pathname,
139
+ title: document.title,
140
+ regions: [{ id: "cart", text: cartSummary }],
141
+ visibleText: "",
142
+ visibleLinks: [],
143
+ visibleFields: [],
144
+ })}
145
+ />
146
+ ```
147
+
148
+ Or attach extra app state without replacing the walker:
149
+
150
+ ```tsx
151
+ <AvatarWidget
152
+ pageContextExtras={{ userId: user.id, cartItemCount: items.length }}
153
+ />
154
+ ```
155
+
156
+ ### 4. Persist the session across pages (multi-page apps)
157
+
158
+ 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](src/AvatarWidget.tsx) for the contract.
159
+
160
+ ### 5. Custom branding
161
+
162
+ ```tsx
163
+ <AvatarWidget
164
+ branding={{
165
+ primaryColor: "#0ea5e9",
166
+ accentColor: "#f59e0b",
167
+ productName: "Acme Concierge",
168
+ logoUrl: "/logo.png",
169
+ }}
170
+ />
171
+ ```
172
+
173
+ ---
174
+
175
+ ## API reference
176
+
177
+ ### `<AvatarWidget>` (primary)
178
+
179
+ All props are optional except `agentId`.
180
+
181
+ | Prop | Type | Description |
182
+ |---|---|---|
183
+ | `agentId` | `string` | **Required.** The published agent ID. |
184
+ | `apiKey` | `string` | API key for cross-origin auth. Required if your agent isn't public. |
185
+ | `baseUrl` | `string` | Base URL of the LiveLayer API. Defaults to `https://app.livelayer.studio`. |
186
+ | `pathname` | `string` | Current pathname. **Required for Next.js App Router and React Router v6+.** Pass `usePathname()` / `useLocation().pathname`. |
187
+ | `showOn` | `RoutePattern[]` | Render only on matching paths. |
188
+ | `hideOn` | `RoutePattern[]` | Never render on matching paths. Wins over `showOn`. |
189
+ | `onNavigate` | `(href: string) => void` | Called on agent `navigate` command. Wire to your router. |
190
+ | `onScrollToSelector` | `(sel, behavior?) => void` | Called on agent `scroll_to` command. Default: `scrollIntoView({ behavior: "smooth" })`. |
191
+ | `getPageContext` | `() => PageContext \| Promise<PageContext>` | Override the default DOM walker. |
192
+ | `pageContextExtras` | `Record<string, unknown>` | Extra app state attached to every page context snapshot. |
193
+ | `position` | `"top-left" \| "top-right" \| "bottom-left" \| "bottom-right" \| "custom"` | Where the widget docks. Defaults to `"bottom-right"`. |
194
+ | `defaultDisplayMode` | `"hidden" \| "minimized" \| "expanded"` | Initial display mode. |
195
+ | `branding` | `BrandingConfig` | Colors, product name, logo. |
196
+ | `teamMembers` | `TeamMember[]` | Multi-agent picker. |
197
+ | `controlledSession` | `ControlledSession` | Bring-your-own LiveKit Room. |
198
+ | `onAgentCommand` | `(cmd) => void` | Receive non-universal data-channel commands. |
199
+ | `onAgentEvent` | `(e) => void` | Receive ALL data-channel events (including the universal ones). |
200
+
201
+ ### `<LiveLayerRegion>` (page-context primitive)
202
+
203
+ ```tsx
204
+ <LiveLayerRegion id="pricing" intent="show pricing tiers" as="section">
205
+ ...
206
+ </LiveLayerRegion>
207
+ ```
208
+
209
+ Renders a wrapper element with `data-ll-region` + `data-ll-intent` that the page-context extractor prioritizes.
210
+
211
+ ### Hooks (power users)
212
+
213
+ `useLiveKitSession`, `useDisplayMode`, `useAgentInfo`, `usePathname`, `useRouteMatch`, `useAudioLevel`, `useMicrophoneState`, `useCameraState`, `useScreenShareState`, `useMediaDevices`, `useTranscript`. All exported from the package root.
214
+
215
+ ### Types
216
+
217
+ `AvatarWidgetProps`, `RoutePattern`, `PageContext`, `AgentCommand`, `AgentEventDetail`, `TeamMember`, `BrandingConfig`, `WidgetPosition`, `DisplayMode`. All exported from the package root.
218
+
219
+ ---
220
+
221
+ ## Privacy
222
+
223
+ The default page-context walker **never** extracts:
224
+
225
+ - Form values (only labels and field types)
226
+ - Inputs with `type="password"`
227
+ - Inputs with `autocomplete="cc-*"` or `autocomplete="off"`
228
+ - Elements (and their subtrees) with `data-ll-private="true"`
229
+ - The widget itself (`.ll-widget`)
230
+
231
+ To redact additional content:
232
+
233
+ ```tsx
234
+ <div data-ll-private="true">
235
+ <UserBankAccount />
236
+ </div>
237
+ ```
238
+
239
+ Or override `getPageContext` entirely to control exactly what reaches the agent.
240
+
241
+ ---
242
+
243
+ ## Migrating from 0.2.x
244
+
245
+ 0.3.0 is **additive**. All existing 0.2.x code continues to work without changes.
246
+
247
+ **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.
248
+
249
+ ---
250
+
251
+ ## Errors and warnings
252
+
253
+ Every console message from this package starts with `[LiveLayer]` and includes a doc URL. Examples:
254
+
255
+ ```
256
+ [LiveLayer] Agent emitted "navigate" without href. Skipping.
257
+ Check your agent's tool schema.
258
+ See https://livelayer.studio/docs/errors/navigate-missing-href
259
+
260
+ [LiveLayer] scroll_to: no element matched "#pricing-table".
261
+ The user may be on a different page.
262
+ See https://livelayer.studio/docs/errors/scroll-no-match
263
+ ```
264
+
265
+ If you see one of these in production, the doc URL has the explanation and remediation.
266
+
267
+ ---
268
+
269
+ ## Legacy: `<LiveLayerWidget>`
270
+
271
+ The thin web-component wrapper from 0.1.x is still exported for backwards compatibility. New apps should use `<AvatarWidget>`.
272
+
273
+ ```tsx
274
+ import { LiveLayerWidget } from "@livelayer/react";
275
+ <LiveLayerWidget agentId="..." />
276
+ ```
277
+
278
+ ---
279
+
280
+ ## Peer dependencies
281
+
282
+ - `react` >= 18.0.0
283
+ - `react-dom` >= 18.0.0
284
+
285
+ 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.
286
+
56
287
  ## License
57
288
 
58
289
  MIT
package/dist/index.d.ts CHANGED
@@ -3,11 +3,14 @@ import { AgentState } from '@livelayer/sdk';
3
3
  import { Component } from 'react';
4
4
  import { ConnectionState } from '@livelayer/sdk';
5
5
  import { CSSProperties } from 'react';
6
+ import { ElementType } from 'react';
6
7
  import { ErrorInfo } from 'react';
7
8
  import { FC } from 'react';
9
+ import { ForwardRefExoticComponent } from 'react';
8
10
  import { JSX } from 'react/jsx-runtime';
9
11
  import { LiveKitSession } from '@livelayer/sdk';
10
12
  import { ReactNode } from 'react';
13
+ import { RefAttributes } from 'react';
11
14
  import { Room } from 'livekit-client';
12
15
  import { SessionOptions } from '@livelayer/sdk';
13
16
  import { TranscriptEntry } from '@livelayer/sdk';
@@ -92,6 +95,52 @@ export declare interface AvatarWidgetProps {
92
95
  allowScreenShare?: boolean;
93
96
  allowTyping?: boolean;
94
97
  allowMic?: boolean;
98
+ /**
99
+ * Patterns where the widget MAY render. If set, widget renders ONLY on
100
+ * matching paths. See `RoutePattern` for accepted forms (string globs,
101
+ * RegExp, or function predicate). Mutually compatible with `hideOn`.
102
+ *
103
+ * Pass `pathname` alongside this prop in Next.js / React Router apps.
104
+ */
105
+ showOn?: RoutePattern[];
106
+ /**
107
+ * Patterns where the widget will NEVER render. Wins over showOn.
108
+ * Common safe defaults: `["/privacy", "/terms", "/legal/*"]`.
109
+ */
110
+ hideOn?: RoutePattern[];
111
+ /**
112
+ * Current pathname. REQUIRED for Next.js App Router and React Router v6+
113
+ * because their internal routers update before window.location does.
114
+ *
115
+ * @example
116
+ * import { usePathname } from "next/navigation";
117
+ * <AvatarWidget pathname={usePathname()} hideOn={["/privacy"]} />
118
+ */
119
+ pathname?: string;
120
+ /**
121
+ * Called when the agent emits a `navigate` command. Wire to your
122
+ * router. If omitted, the widget falls back to (1) clicking a
123
+ * matching anchor (so Next/RR Link interceptors fire) and then
124
+ * (2) `history.pushState` for plain HTML sites. `window.location`
125
+ * is NEVER used — that would trigger a full reload and kill the
126
+ * session.
127
+ */
128
+ onNavigate?: (href: string) => void;
129
+ /**
130
+ * Called when the agent emits a `scroll_to` command. Default:
131
+ * scrolls the matched element into view smoothly. Override to
132
+ * customize easing or skip the scroll entirely.
133
+ */
134
+ onScrollToSelector?: (selector: string, behavior?: "smooth" | "instant") => void;
135
+ /**
136
+ * Override the default DOM walker. Receives the consumer's
137
+ * `pageContextExtras` and returns a structured context for the
138
+ * agent. Useful for filtering sensitive content or prepending app
139
+ * state that's not in the DOM.
140
+ */
141
+ getPageContext?: (extras?: Record<string, unknown>) => PageContext | Promise<PageContext>;
142
+ /** Free-form metadata bag the agent should always know about. */
143
+ pageContextExtras?: Record<string, unknown>;
95
144
  onConnect?: () => void;
96
145
  onDisconnect?: () => void;
97
146
  onTranscript?: (entries: TranscriptEntry[]) => void;
@@ -137,6 +186,8 @@ export declare interface CameraStateHandle {
137
186
  clearError: () => void;
138
187
  }
139
188
 
189
+ export declare function clearPageContextCache(): void;
190
+
140
191
  export { ConnectionState }
141
192
 
142
193
  /**
@@ -180,6 +231,15 @@ export declare class ErrorBoundary extends Component<Props, State> {
180
231
  render(): ReactNode;
181
232
  }
182
233
 
234
+ declare interface ExtractOptions {
235
+ /** Override doc — for tests. */
236
+ doc?: Document;
237
+ }
238
+
239
+ export declare function extractPageContext(extras?: Record<string, unknown>, opts?: ExtractOptions): PageContext;
240
+
241
+ export declare function getCachedPageContext(extras?: Record<string, unknown>, opts?: ExtractOptions): PageContext;
242
+
183
243
  export declare interface LegacyAgentEventDetail {
184
244
  eventName: string;
185
245
  data: Record<string, unknown>;
@@ -187,6 +247,22 @@ export declare interface LegacyAgentEventDetail {
187
247
 
188
248
  declare type LevelSubscriber = (level: number) => void;
189
249
 
250
+ export declare const LiveLayerRegion: ForwardRefExoticComponent<LiveLayerRegionProps & RefAttributes<HTMLElement>>;
251
+
252
+ export declare interface LiveLayerRegionProps {
253
+ /** Stable identifier for the region. Becomes `data-ll-region`. */
254
+ id: string;
255
+ /** One-line description of what the agent should know about this region. */
256
+ intent?: string;
257
+ /** Element to render. Defaults to "div". */
258
+ as?: ElementType;
259
+ /** Extra class name on the wrapper. */
260
+ className?: string;
261
+ /** Inline styles. */
262
+ style?: CSSProperties;
263
+ children: ReactNode;
264
+ }
265
+
190
266
  /**
191
267
  * React component that renders a `<livelayer-widget>` custom element.
192
268
  *
@@ -220,6 +296,8 @@ export declare interface LiveLayerWidgetProps {
220
296
  style?: React.CSSProperties;
221
297
  }
222
298
 
299
+ export declare function matchesPattern(pattern: RoutePattern, pathname: string): boolean;
300
+
223
301
  export declare interface MediaDevicesHandle {
224
302
  mics: MediaDeviceInfo[];
225
303
  cameras: MediaDeviceInfo[];
@@ -257,6 +335,42 @@ declare interface Options_2 {
257
335
  disablePersistence?: boolean;
258
336
  }
259
337
 
338
+ /**
339
+ * Snapshot of what the user is currently looking at, sent to the agent in
340
+ * response to a `request_page_context` command.
341
+ *
342
+ * Form values, password inputs, and elements marked `data-ll-private="true"`
343
+ * are NEVER included. See README → Privacy.
344
+ */
345
+ export declare interface PageContext {
346
+ /** Full URL at the moment the snapshot was taken. */
347
+ url: string;
348
+ /** document.title at snapshot time. */
349
+ title: string;
350
+ /** Pathname only (no host, no query, no hash). */
351
+ pathname: string;
352
+ /** Author-curated regions via <LiveLayerRegion> — agent should prefer these. */
353
+ regions: Array<{
354
+ id: string;
355
+ intent?: string;
356
+ text: string;
357
+ }>;
358
+ /** Visible content fallback — auto-extracted text from headings/paragraphs. */
359
+ visibleText: string;
360
+ /** Anchor hrefs visible in viewport, top-to-bottom, max 20. */
361
+ visibleLinks: Array<{
362
+ href: string;
363
+ text: string;
364
+ }>;
365
+ /** Form fields visible in viewport — labels and types only, never values. */
366
+ visibleFields: Array<{
367
+ label: string;
368
+ type: string;
369
+ }>;
370
+ /** Free-form metadata bag from the consumer's pageContextExtras prop. */
371
+ extras?: Record<string, unknown>;
372
+ }
373
+
260
374
  declare interface Props {
261
375
  children: ReactNode;
262
376
  /** Callback fired when an error is caught. Useful for telemetry. */
@@ -265,6 +379,22 @@ declare interface Props {
265
379
  fallback?: ReactNode;
266
380
  }
267
381
 
382
+ /**
383
+ * Pattern matched against the current pathname to decide if the widget
384
+ * renders.
385
+ *
386
+ * - `string` — exact match OR glob with `*` (one segment) and `**` (any depth)
387
+ * - `RegExp` — full regex flexibility
388
+ * - function — fully custom predicate
389
+ *
390
+ * Glob examples:
391
+ * "/" only the home route
392
+ * "/admin/X" /admin/foo but NOT /admin/foo/bar (X = single star)
393
+ * "/admin/XX" /admin and any descendant (XX = double star)
394
+ * "/blog/X/comments" /blog/x/comments but not deeper paths (X = single star)
395
+ */
396
+ export declare type RoutePattern = string | RegExp | ((pathname: string) => boolean);
397
+
268
398
  export declare interface ScreenShareStateHandle {
269
399
  isEnabled: boolean;
270
400
  error: string | null;
@@ -275,6 +405,15 @@ export declare interface ScreenShareStateHandle {
275
405
  clearError: () => void;
276
406
  }
277
407
 
408
+ /**
409
+ * Pure: should the widget render at this pathname?
410
+ *
411
+ * @param pathname current path, or undefined if not yet known
412
+ * @param showOn if set, restricts rendering to matching paths only
413
+ * @param hideOn if set, blocks rendering on matching paths (wins)
414
+ */
415
+ export declare function shouldRenderAtPath(pathname: string | undefined, showOn: RoutePattern[] | undefined, hideOn: RoutePattern[] | undefined): boolean;
416
+
278
417
  declare interface State {
279
418
  hasError: boolean;
280
419
  error: Error | null;
@@ -363,6 +502,15 @@ export declare function useMediaDevices(): MediaDevicesHandle;
363
502
 
364
503
  export declare function useMicrophoneState(): MicrophoneStateHandle;
365
504
 
505
+ /**
506
+ * Returns the current pathname, reactive to SPA navigation.
507
+ * Pass `controlledPathname` to skip internal detection (recommended for
508
+ * Next.js App Router and React Router v6+ consumers).
509
+ */
510
+ export declare function usePathname(controlledPathname?: string): string;
511
+
512
+ export declare function useRouteMatch(pathname: string | undefined, showOn: RoutePattern[] | undefined, hideOn: RoutePattern[] | undefined): boolean;
513
+
366
514
  export declare function useScreenShareState(): ScreenShareStateHandle;
367
515
 
368
516
  export declare function useTranscript(): TranscriptHandle;