@livelayer/react 0.10.7 → 0.13.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fusion Studios Code
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -48,6 +48,67 @@ That's it. The widget docks bottom-right, the agent can navigate users to other
48
48
 
49
49
  ---
50
50
 
51
+ ## Structured data collection (0.12.0)
52
+
53
+ The agent can run guided, typed Q&A flows backed by LiveKit's [`voice.AgentTask`](https://docs.livekit.io/agents/logic/tasks/) + `beta.workflows.TaskGroup` primitives. **No new components, no special attributes — just write regular HTML forms.** Every `<form>` is auto-discovered.
54
+
55
+ ```tsx
56
+ import { AvatarWidget } from "@livelayer/react";
57
+
58
+ function App() {
59
+ return (
60
+ <>
61
+ <AvatarWidget
62
+ agentId="agent_abc"
63
+ onCollect={(result) => {
64
+ // result.results keyed by field name — ship to your CRM.
65
+ fetch("/api/leads", {
66
+ method: "POST",
67
+ body: JSON.stringify(result),
68
+ });
69
+ }}
70
+ />
71
+
72
+ {/* Plain HTML — the agent finds this. */}
73
+ <form>
74
+ <label>Email <input name="email" type="email" required /></label>
75
+ <label>Company <input name="company" /></label>
76
+ <button type="submit">Subscribe</button>
77
+ </form>
78
+ </>
79
+ );
80
+ }
81
+ ```
82
+
83
+ 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.
84
+
85
+ **Streaming per-field updates** for a progress UI:
86
+
87
+ ```tsx
88
+ import { useCollect } from "@livelayer/react";
89
+
90
+ function Progress() {
91
+ const { fields, isCollecting, lastResult } = useCollect();
92
+ return Object.entries(fields).map(([name, value]) => (
93
+ <div key={name}>{name}: {value}</div>
94
+ ));
95
+ }
96
+ ```
97
+
98
+ **Opt-out** when you don't want a form / input visible to the agent:
99
+
100
+ ```tsx
101
+ <form data-ll-skip>...</form> // exclude the whole form
102
+ <input data-ll-private /> // exclude one input
103
+ <form data-ll-intent="request a demo"> // disambiguate (still visible)
104
+ ```
105
+
106
+ `type="password"`, `autocomplete="cc-*"`, and `autocomplete="off"` are ALWAYS excluded — you don't have to mark them.
107
+
108
+ See [docs.livelayer.studio/develop/data-collection](https://docs.livelayer.studio/develop/data-collection) for the full result shape, dashboard-declared field lists, slide-level data collection, capability gating, and webhook delivery.
109
+
110
+ ---
111
+
51
112
  ## Recipes
52
113
 
53
114
  ### 1. Voice navigation in Next.js / React Router
@@ -167,36 +228,25 @@ The agent emits `{ type: "click", selector: "[data-ll-action='open-pricing-modal
167
228
 
168
229
  **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.
169
230
 
170
- **Forms** — declarative wrappers. Tag the form and the fields the agent is allowed to fill:
231
+ **Forms** — auto-discovered. Just write regular HTML:
171
232
 
172
233
  ```tsx
173
- import { LiveLayerForm, LiveLayerField } from "@livelayer/react";
174
-
175
- <LiveLayerForm id="contact" intent="contact us — send a message">
176
- <LiveLayerField name="name" label="Your name" />
177
- <LiveLayerField name="email" label="Email" type="email" />
178
- <LiveLayerField name="message" as="textarea" label="Message" />
179
- <button type="submit">Send</button>
180
- </LiveLayerForm>
181
- ```
182
-
183
- Or use raw HTML with `data-ll-form` / `data-ll-field`:
184
-
185
- ```html
186
- <form data-ll-form="contact" data-ll-intent="contact us">
187
- <input data-ll-field="name" name="name" />
188
- <input data-ll-field="email" name="email" type="email" />
189
- <textarea data-ll-field="message" name="message"></textarea>
234
+ <form onSubmit={handleSubmit}>
235
+ <label>Name <input name="name" /></label>
236
+ <label>Email <input name="email" type="email" /></label>
237
+ <label>Message <textarea name="message" /></label>
190
238
  <button type="submit">Send</button>
191
239
  </form>
192
240
  ```
193
241
 
194
242
  The agent sees these in `PageContext.forms` and calls:
195
- - `fill_form` — sets values via the React-controlled-input pattern (your `onChange` listeners fire correctly)
196
- - `focus_field` — moves focus to a specific field
197
- - `submit_form` — calls `form.requestSubmit()`. The widget publishes `{ type: "form_submitted", formId }` on success or `{ type: "form_submit_blocked", formId, reason: "validation" }` on HTML5 validation failure
243
+ - `fill_form` — sets values via the canonical native-setter pattern (your `onChange` listeners fire correctly). Use when the agent already has all the answers.
244
+ - `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](#structured-data-collection-0120) above.
245
+ - `submit_form` — calls `form.requestSubmit()`. Publishes `{ type: "form_submitted", formId }` on success or `{ type: "form_submit_blocked", formId, reason: "validation" }` on HTML5 validation failure.
198
246
 
199
- **Privacy is enforced regardless of tagging**: `type="password"`, `autocomplete="cc-*"`, and `[data-ll-private="true"]` fields are NEVER agent-fillable. Card fields belong in Stripe Elements; we will not be the rail.
247
+ Form IDs are inferred from the form's existing `id` / `name` attribute, falling back to a `data-ll-intent` slug, finally `form_<index>`.
248
+
249
+ **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.
200
250
 
201
251
  **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.
202
252
 
@@ -281,21 +331,34 @@ All props are optional except `agentId`.
281
331
 
282
332
  Renders a wrapper element with `data-ll-region` + `data-ll-intent` that the page-context extractor prioritizes.
283
333
 
284
- ### `<LiveLayerForm>` + `<LiveLayerField>` (form primitives, 0.4.0)
334
+ ### Forms (0.12.0 no wrappers, just plain HTML)
285
335
 
286
336
  ```tsx
287
- <LiveLayerForm id="signup" intent="create account" onSubmit={handleSubmit}>
288
- <LiveLayerField name="email" label="Email" type="email" />
289
- <LiveLayerField name="bio" as="textarea" label="Bio" />
290
- <LiveLayerField name="role" as="select" label="Role">
291
- <option value="dev">Developer</option>
292
- <option value="pm">PM</option>
293
- </LiveLayerField>
337
+ <form onSubmit={handleSubmit}>
338
+ <label>Email <input name="email" type="email" required /></label>
339
+ <label>Bio <textarea name="bio" /></label>
340
+ <label>
341
+ Role
342
+ <select name="role">
343
+ <option value="dev">Developer</option>
344
+ <option value="pm">PM</option>
345
+ </select>
346
+ </label>
294
347
  <button type="submit">Sign up</button>
295
- </LiveLayerForm>
348
+ </form>
296
349
  ```
297
350
 
298
- Equivalent to raw HTML with `data-ll-form` + `data-ll-field` attributes. Untagged forms remain invisible to the agent.
351
+ Every `<form>` is auto-discovered. The agent infers labels from the
352
+ wrapping `<label>` / `aria-label` / `placeholder`, infers kinds from
353
+ the input `type=`, and can either fill in one shot (`fill_form`) or
354
+ walk the visitor through it conversationally (`collect_from_page`).
355
+
356
+ **Opt out** with `data-ll-skip` on a form or `data-ll-private` on an
357
+ input. Browser-native private fields (`type="password"`, `autocomplete="cc-*"`,
358
+ `autocomplete="off"`) are always excluded.
359
+
360
+ See the *Structured data collection* section above for `onCollect` /
361
+ `useCollect`.
299
362
 
300
363
  ### Hooks (power users)
301
364
 
package/dist/index.d.ts CHANGED
@@ -1,22 +1,25 @@
1
1
  import { AgentConfig } from '@livelayer/sdk';
2
2
  import { AgentState } from '@livelayer/sdk';
3
+ import { clearFieldRegistry } from '@livelayer/sdk';
3
4
  import { Component } from 'react';
4
5
  import { ConnectionState } from '@livelayer/sdk';
5
6
  import { CSSProperties } from 'react';
6
7
  import { ElementType } from 'react';
7
8
  import { ErrorInfo } from 'react';
8
9
  import { FC } from 'react';
9
- import { FormHTMLAttributes } from 'react';
10
+ import { FieldKind } from '@livelayer/sdk';
11
+ import { FieldManifest } from '@livelayer/sdk';
12
+ import { FieldOption } from '@livelayer/sdk';
10
13
  import { ForwardRefExoticComponent } from 'react';
11
- import { InputHTMLAttributes } from 'react';
14
+ import { getRegisteredFields } from '@livelayer/sdk';
12
15
  import { JSX } from 'react/jsx-runtime';
13
16
  import { LiveKitSession } from '@livelayer/sdk';
14
17
  import { ReactNode } from 'react';
15
18
  import { RefAttributes } from 'react';
19
+ import { registerFields } from '@livelayer/sdk';
16
20
  import { Room } from 'livekit-client';
17
- import { SelectHTMLAttributes } from 'react';
18
21
  import { SessionOptions } from '@livelayer/sdk';
19
- import { TextareaHTMLAttributes } from 'react';
22
+ import { setFieldValue } from '@livelayer/sdk';
20
23
  import { TranscriptEntry } from '@livelayer/sdk';
21
24
 
22
25
  /**
@@ -33,7 +36,13 @@ import { TranscriptEntry } from '@livelayer/sdk';
33
36
  *
34
37
  * Default (undefined): everything enabled (matches 0.3.x behavior).
35
38
  */
36
- export declare type AgentCapability = "navigate" | "scroll" | "click" | "fill_forms" | "submit_forms" | "read_page";
39
+ export declare type AgentCapability = "navigate" | "scroll" | "click" | "fill_forms" | "submit_forms" | "read_page"
40
+ /**
41
+ * 0.11.0 — LiveKit Agent Tasks structured collection (page-form,
42
+ * agent-declared field list, slide form). The detailed result types
43
+ * live in `./hooks/useCollect` to keep co-located with the consumer.
44
+ */
45
+ | "collect_data";
37
46
 
38
47
  /**
39
48
  * Agent commands streamed over the LiveKit data channel. The base package
@@ -326,6 +335,47 @@ export declare interface AvatarWidgetProps {
326
335
  onConnectionStateChange?: (state: ConnectionState) => void;
327
336
  onAgentEvent?: (e: AgentEventDetail) => void;
328
337
  onAgentCommand?: (cmd: AgentCommand) => void;
338
+ /**
339
+ * Fires once when the agent finishes a structured collection run.
340
+ * One callback covers every collection surface:
341
+ *
342
+ * - `source: "page"` — agent walked the visitor through an
343
+ * on-page <form> (auto-discovered). The
344
+ * values already painted into the matching
345
+ * inputs live as they were recorded.
346
+ * - `source: "agent"` — dashboard-declared field list (Behavior →
347
+ * Data collection in the agent editor).
348
+ * - `source: "slide"` — slide-level form_fields (slide editor's
349
+ * Data collection toggle).
350
+ *
351
+ * Typical use: ship the typed payload to your backend.
352
+ *
353
+ * <AvatarWidget
354
+ * agentId="..."
355
+ * onCollect={(r) => fetch("/api/leads", { method: "POST",
356
+ * body: JSON.stringify(r) })}
357
+ * />
358
+ *
359
+ * For streaming per-field updates use the `useCollect()` hook
360
+ * instead. Don't write to `<input value=...>` from inside this
361
+ * callback — the SDK already auto-painted matching inputs by their
362
+ * `name` attribute before this fires.
363
+ */
364
+ onCollect?: (result: {
365
+ sessionId: string;
366
+ startedAt: string;
367
+ endedAt: string;
368
+ source: "agent" | "slide" | "page";
369
+ slideId?: string;
370
+ formId?: string;
371
+ results: Record<string, {
372
+ fieldId: string;
373
+ fieldName: string;
374
+ value: string;
375
+ kind: string;
376
+ }>;
377
+ summary?: string;
378
+ }) => void;
329
379
  /**
330
380
  * When provided, the widget does not create its own LiveKit session.
331
381
  * The consumer owns connection lifecycle. Use for cross-page
@@ -364,17 +414,31 @@ export declare interface CameraStateHandle {
364
414
  clearError: () => void;
365
415
  }
366
416
 
417
+ export { clearFieldRegistry }
418
+
367
419
  export declare function clearPageContextCache(): void;
368
420
 
369
421
  export declare function clearRoutesCache(): void;
370
422
 
371
- declare interface CommonFieldProps {
372
- /** Field identifier. Becomes `data-ll-field` AND the input's `name`. */
373
- name: string;
374
- /** Visible label. Wrapped in <label>. */
375
- label?: ReactNode;
376
- /** Optional className for the wrapping <label>. */
377
- labelClassName?: string;
423
+ export declare interface CollectedField {
424
+ fieldId: string;
425
+ fieldName: string;
426
+ value: string;
427
+ kind: string;
428
+ source: "agent" | "slide" | "page";
429
+ slideId?: string;
430
+ formId?: string;
431
+ }
432
+
433
+ export declare interface CollectedResult {
434
+ sessionId: string;
435
+ startedAt: string;
436
+ endedAt: string;
437
+ source: "agent" | "slide" | "page";
438
+ slideId?: string;
439
+ formId?: string;
440
+ results: Record<string, CollectedField>;
441
+ summary?: string;
378
442
  }
379
443
 
380
444
  export { ConnectionState }
@@ -472,12 +536,25 @@ export declare function extractPageContext(extras?: Record<string, unknown>, opt
472
536
 
473
537
  export declare function extractRoutes(doc?: Document): ExtractedRoute[];
474
538
 
475
- declare type FieldElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
539
+ export { FieldKind }
540
+
541
+ export { FieldManifest }
542
+
543
+ export { FieldOption }
544
+
545
+ export declare function FieldProvider({ fields, children }: FieldProviderProps): JSX.Element;
546
+
547
+ export declare interface FieldProviderProps {
548
+ fields: FieldManifest[];
549
+ children: ReactNode;
550
+ }
476
551
 
477
552
  export declare function getCachedPageContext(extras?: Record<string, unknown>, opts?: ExtractOptions): PageContext;
478
553
 
479
554
  export declare function getCachedRoutes(): ExtractedRoute[];
480
555
 
556
+ export { getRegisteredFields }
557
+
481
558
  export declare interface LegacyAgentEventDetail {
482
559
  eventName: string;
483
560
  data: Record<string, unknown>;
@@ -500,30 +577,6 @@ export declare interface LiveLayerDebugPanelProps {
500
577
  storageKey?: string;
501
578
  }
502
579
 
503
- export declare const LiveLayerField: ForwardRefExoticComponent<LiveLayerFieldProps & RefAttributes<FieldElement>>;
504
-
505
- export declare type LiveLayerFieldProps = (CommonFieldProps & {
506
- as?: "input";
507
- } & Omit<InputHTMLAttributes<HTMLInputElement>, "name">) | (CommonFieldProps & {
508
- as: "textarea";
509
- } & Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "name">) | (CommonFieldProps & {
510
- as: "select";
511
- children: ReactNode;
512
- } & Omit<SelectHTMLAttributes<HTMLSelectElement>, "name">);
513
-
514
- export declare const LiveLayerForm: ForwardRefExoticComponent<LiveLayerFormProps & RefAttributes<HTMLFormElement>>;
515
-
516
- export declare interface LiveLayerFormProps extends FormHTMLAttributes<HTMLFormElement> {
517
- /** Stable identifier for the form. Becomes `data-ll-form`. */
518
- id: string;
519
- /**
520
- * One-line description the agent sees. e.g. "create account",
521
- * "request a demo". The agent uses this to decide WHEN to fill the
522
- * form (matching user intent → form intent).
523
- */
524
- intent?: string;
525
- }
526
-
527
580
  export declare const LiveLayerRegion: ForwardRefExoticComponent<LiveLayerRegionProps & RefAttributes<HTMLElement>>;
528
581
 
529
582
  export declare interface LiveLayerRegionProps {
@@ -669,9 +722,10 @@ export declare interface PageContext {
669
722
  type: string;
670
723
  }>;
671
724
  /**
672
- * Author-curated forms via <LiveLayerForm> / data-ll-form. The agent
673
- * uses these to call `fill_form` / `submit_form`. Values NEVER included.
674
- * (0.4.0)
725
+ * Every <form> on the page is auto-discovered (0.12.0). The agent
726
+ * uses these entries to call `fill_form`, `submit_form`, or
727
+ * `collect_from_page`. Values NEVER included. Forms opted out via
728
+ * `data-ll-skip` or inside a `data-ll-private` subtree are absent.
675
729
  */
676
730
  forms: Array<{
677
731
  id: string;
@@ -703,6 +757,8 @@ declare interface Props {
703
757
  fallback?: ReactNode;
704
758
  }
705
759
 
760
+ export { registerFields }
761
+
706
762
  /**
707
763
  * The shape consumers pass back from the `getRoutes` callback. Matches
708
764
  * `ExtractedRoute` but every field except `href` is optional — defaults
@@ -742,6 +798,8 @@ export declare interface ScreenShareStateHandle {
742
798
  clearError: () => void;
743
799
  }
744
800
 
801
+ export { setFieldValue }
802
+
745
803
  /**
746
804
  * Pure: should the widget render at this pathname?
747
805
  *
@@ -803,6 +861,40 @@ export declare function useAudioLevel(): AudioLevelHandle;
803
861
 
804
862
  export declare function useCameraState(): CameraStateHandle;
805
863
 
864
+ export declare function useCollect(options?: UseCollectOptions): UseCollectHandle;
865
+
866
+ export declare interface UseCollectHandle {
867
+ /** Field name → most-recent value. Resets when a new run starts. */
868
+ fields: Record<string, string>;
869
+ /** True between the first field update and the completion event. */
870
+ isCollecting: boolean;
871
+ /** Most recent completed result; null until at least one run finishes. */
872
+ lastResult: CollectedResult | null;
873
+ /** Clear the running snapshot manually (e.g. on slide change). */
874
+ reset: () => void;
875
+ }
876
+
877
+ export declare interface UseCollectOptions {
878
+ /**
879
+ * Fires for every field as it's recorded. Use for live progress UI.
880
+ * Omit if you only care about the final payload — that's what
881
+ * `<AvatarWidget onCollect />` is for.
882
+ */
883
+ onFieldUpdate?: (update: CollectedField) => void;
884
+ /**
885
+ * Fires once when a collection run finishes. Same payload the
886
+ * `<AvatarWidget onCollect />` prop receives — they're equivalent.
887
+ */
888
+ onComplete?: (result: CollectedResult) => void;
889
+ /**
890
+ * Restrict to one source. `"page"` ignores slide / agent flows;
891
+ * `"slide"` ignores page-form / agent-level flows; `"agent"`
892
+ * ignores everything except dashboard-declared field lists. Default
893
+ * `"all"` listens to every source.
894
+ */
895
+ source?: "all" | "agent" | "slide" | "page";
896
+ }
897
+
806
898
  export declare function useDisplayMode({ value, defaultValue, onChange, }?: Options): [DisplayMode_2, (next: DisplayMode_2) => void];
807
899
 
808
900
  export declare function useDisplayModePersistence({ value, defaultValue, onChange, persistKey, disablePersistence, }?: Options_2): [DisplayMode_2, (next: DisplayMode_2) => void];
@@ -852,6 +944,13 @@ export declare function useMicrophoneState(): MicrophoneStateHandle;
852
944
  */
853
945
  export declare function usePathname(controlledPathname?: string): string;
854
946
 
947
+ /**
948
+ * Register a set of fields with the LiveLayer SDK. Stable across
949
+ * re-renders by depending on the SERIALIZED field list (so consumers
950
+ * can inline the array literal without memoizing).
951
+ */
952
+ export declare function useRegisterFields(fields: FieldManifest[]): void;
953
+
855
954
  export declare function useRouteMatch(pathname: string | undefined, showOn: RoutePattern[] | undefined, hideOn: RoutePattern[] | undefined): boolean;
856
955
 
857
956
  export declare function useScreenShareState(): ScreenShareStateHandle;