@livelayer/react 0.10.7 → 0.12.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/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.
246
+
247
+ Form IDs are inferred from the form's existing `id` / `name` attribute, falling back to a `data-ll-intent` slug, finally `form_<index>`.
198
248
 
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.
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
 
package/dist/index.d.ts CHANGED
@@ -6,17 +6,13 @@ import { CSSProperties } from 'react';
6
6
  import { ElementType } from 'react';
7
7
  import { ErrorInfo } from 'react';
8
8
  import { FC } from 'react';
9
- import { FormHTMLAttributes } from 'react';
10
9
  import { ForwardRefExoticComponent } from 'react';
11
- import { InputHTMLAttributes } from 'react';
12
10
  import { JSX } from 'react/jsx-runtime';
13
11
  import { LiveKitSession } from '@livelayer/sdk';
14
12
  import { ReactNode } from 'react';
15
13
  import { RefAttributes } from 'react';
16
14
  import { Room } from 'livekit-client';
17
- import { SelectHTMLAttributes } from 'react';
18
15
  import { SessionOptions } from '@livelayer/sdk';
19
- import { TextareaHTMLAttributes } from 'react';
20
16
  import { TranscriptEntry } from '@livelayer/sdk';
21
17
 
22
18
  /**
@@ -33,7 +29,13 @@ import { TranscriptEntry } from '@livelayer/sdk';
33
29
  *
34
30
  * Default (undefined): everything enabled (matches 0.3.x behavior).
35
31
  */
36
- export declare type AgentCapability = "navigate" | "scroll" | "click" | "fill_forms" | "submit_forms" | "read_page";
32
+ export declare type AgentCapability = "navigate" | "scroll" | "click" | "fill_forms" | "submit_forms" | "read_page"
33
+ /**
34
+ * 0.11.0 — LiveKit Agent Tasks structured collection (page-form,
35
+ * agent-declared field list, slide form). The detailed result types
36
+ * live in `./hooks/useCollect` to keep co-located with the consumer.
37
+ */
38
+ | "collect_data";
37
39
 
38
40
  /**
39
41
  * Agent commands streamed over the LiveKit data channel. The base package
@@ -326,6 +328,47 @@ export declare interface AvatarWidgetProps {
326
328
  onConnectionStateChange?: (state: ConnectionState) => void;
327
329
  onAgentEvent?: (e: AgentEventDetail) => void;
328
330
  onAgentCommand?: (cmd: AgentCommand) => void;
331
+ /**
332
+ * Fires once when the agent finishes a structured collection run.
333
+ * One callback covers every collection surface:
334
+ *
335
+ * - `source: "page"` — agent walked the visitor through an
336
+ * on-page <form> (auto-discovered). The
337
+ * values already painted into the matching
338
+ * inputs live as they were recorded.
339
+ * - `source: "agent"` — dashboard-declared field list (Behavior →
340
+ * Data collection in the agent editor).
341
+ * - `source: "slide"` — slide-level form_fields (slide editor's
342
+ * Data collection toggle).
343
+ *
344
+ * Typical use: ship the typed payload to your backend.
345
+ *
346
+ * <AvatarWidget
347
+ * agentId="..."
348
+ * onCollect={(r) => fetch("/api/leads", { method: "POST",
349
+ * body: JSON.stringify(r) })}
350
+ * />
351
+ *
352
+ * For streaming per-field updates use the `useCollect()` hook
353
+ * instead. Don't write to `<input value=...>` from inside this
354
+ * callback — the SDK already auto-painted matching inputs by their
355
+ * `name` attribute before this fires.
356
+ */
357
+ onCollect?: (result: {
358
+ sessionId: string;
359
+ startedAt: string;
360
+ endedAt: string;
361
+ source: "agent" | "slide" | "page";
362
+ slideId?: string;
363
+ formId?: string;
364
+ results: Record<string, {
365
+ fieldId: string;
366
+ fieldName: string;
367
+ value: string;
368
+ kind: string;
369
+ }>;
370
+ summary?: string;
371
+ }) => void;
329
372
  /**
330
373
  * When provided, the widget does not create its own LiveKit session.
331
374
  * The consumer owns connection lifecycle. Use for cross-page
@@ -368,13 +411,25 @@ export declare function clearPageContextCache(): void;
368
411
 
369
412
  export declare function clearRoutesCache(): void;
370
413
 
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;
414
+ export declare interface CollectedField {
415
+ fieldId: string;
416
+ fieldName: string;
417
+ value: string;
418
+ kind: string;
419
+ source: "agent" | "slide" | "page";
420
+ slideId?: string;
421
+ formId?: string;
422
+ }
423
+
424
+ export declare interface CollectedResult {
425
+ sessionId: string;
426
+ startedAt: string;
427
+ endedAt: string;
428
+ source: "agent" | "slide" | "page";
429
+ slideId?: string;
430
+ formId?: string;
431
+ results: Record<string, CollectedField>;
432
+ summary?: string;
378
433
  }
379
434
 
380
435
  export { ConnectionState }
@@ -472,8 +527,6 @@ export declare function extractPageContext(extras?: Record<string, unknown>, opt
472
527
 
473
528
  export declare function extractRoutes(doc?: Document): ExtractedRoute[];
474
529
 
475
- declare type FieldElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
476
-
477
530
  export declare function getCachedPageContext(extras?: Record<string, unknown>, opts?: ExtractOptions): PageContext;
478
531
 
479
532
  export declare function getCachedRoutes(): ExtractedRoute[];
@@ -500,30 +553,6 @@ export declare interface LiveLayerDebugPanelProps {
500
553
  storageKey?: string;
501
554
  }
502
555
 
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
556
  export declare const LiveLayerRegion: ForwardRefExoticComponent<LiveLayerRegionProps & RefAttributes<HTMLElement>>;
528
557
 
529
558
  export declare interface LiveLayerRegionProps {
@@ -669,9 +698,10 @@ export declare interface PageContext {
669
698
  type: string;
670
699
  }>;
671
700
  /**
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)
701
+ * Every <form> on the page is auto-discovered (0.12.0). The agent
702
+ * uses these entries to call `fill_form`, `submit_form`, or
703
+ * `collect_from_page`. Values NEVER included. Forms opted out via
704
+ * `data-ll-skip` or inside a `data-ll-private` subtree are absent.
675
705
  */
676
706
  forms: Array<{
677
707
  id: string;
@@ -803,6 +833,40 @@ export declare function useAudioLevel(): AudioLevelHandle;
803
833
 
804
834
  export declare function useCameraState(): CameraStateHandle;
805
835
 
836
+ export declare function useCollect(options?: UseCollectOptions): UseCollectHandle;
837
+
838
+ export declare interface UseCollectHandle {
839
+ /** Field name → most-recent value. Resets when a new run starts. */
840
+ fields: Record<string, string>;
841
+ /** True between the first field update and the completion event. */
842
+ isCollecting: boolean;
843
+ /** Most recent completed result; null until at least one run finishes. */
844
+ lastResult: CollectedResult | null;
845
+ /** Clear the running snapshot manually (e.g. on slide change). */
846
+ reset: () => void;
847
+ }
848
+
849
+ export declare interface UseCollectOptions {
850
+ /**
851
+ * Fires for every field as it's recorded. Use for live progress UI.
852
+ * Omit if you only care about the final payload — that's what
853
+ * `<AvatarWidget onCollect />` is for.
854
+ */
855
+ onFieldUpdate?: (update: CollectedField) => void;
856
+ /**
857
+ * Fires once when a collection run finishes. Same payload the
858
+ * `<AvatarWidget onCollect />` prop receives — they're equivalent.
859
+ */
860
+ onComplete?: (result: CollectedResult) => void;
861
+ /**
862
+ * Restrict to one source. `"page"` ignores slide / agent flows;
863
+ * `"slide"` ignores page-form / agent-level flows; `"agent"`
864
+ * ignores everything except dashboard-declared field lists. Default
865
+ * `"all"` listens to every source.
866
+ */
867
+ source?: "all" | "agent" | "slide" | "page";
868
+ }
869
+
806
870
  export declare function useDisplayMode({ value, defaultValue, onChange, }?: Options): [DisplayMode_2, (next: DisplayMode_2) => void];
807
871
 
808
872
  export declare function useDisplayModePersistence({ value, defaultValue, onChange, persistKey, disablePersistence, }?: Options_2): [DisplayMode_2, (next: DisplayMode_2) => void];