@agentable-ui/genui 0.2.0 → 0.3.1
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 +78 -34
- package/dist/index.d.ts +14 -4
- package/dist/index.js +62 -8
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,66 +1,110 @@
|
|
|
1
1
|
# @agentable-ui/genui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Render model-generated interfaces using components you allow. Use `Renderer` for text streamed by your application, or `AgentSurface` for an in-page agent that calls a `present` tool.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
4
6
|
|
|
5
7
|
```bash
|
|
6
8
|
npm install @agentable-ui/genui @agentable-ui/ui @agentable-ui/react @agentable-ui/core
|
|
7
9
|
```
|
|
8
10
|
|
|
11
|
+
## Render a program
|
|
12
|
+
|
|
13
|
+
This complete example renders a static program. Replace `program` with the accumulated text from your model stream and set `isStreaming` while chunks arrive.
|
|
14
|
+
|
|
9
15
|
```tsx
|
|
10
|
-
import {
|
|
16
|
+
import { Renderer } from "@agentable-ui/genui";
|
|
11
17
|
import { uiCatalog } from "@agentable-ui/ui";
|
|
18
|
+
import "@agentable-ui/ui/styles.css";
|
|
19
|
+
|
|
20
|
+
const program = `root = Card("Order summary", [total])
|
|
21
|
+
total = Stat("Revenue", "$48.2k")`;
|
|
12
22
|
|
|
13
|
-
|
|
23
|
+
export default function Summary() {
|
|
24
|
+
return <Renderer components={uiCatalog} response={program} isStreaming={false} />;
|
|
25
|
+
}
|
|
14
26
|
```
|
|
15
27
|
|
|
16
|
-
|
|
28
|
+
`response` is the **full text received so far**, not just the newest chunk. Complete lines render as they arrive; interactions stay disabled until streaming finishes. `onError` receives structured parsing and validation issues. Your app owns model requests, credentials, and streaming transport.
|
|
17
29
|
|
|
18
|
-
##
|
|
30
|
+
## Give a browser agent a surface
|
|
19
31
|
|
|
20
|
-
|
|
32
|
+
```tsx
|
|
33
|
+
import { AgentableProvider } from "@agentable-ui/react";
|
|
34
|
+
import { AgentSurface } from "@agentable-ui/genui";
|
|
35
|
+
import { uiCatalog } from "@agentable-ui/ui";
|
|
36
|
+
import "@agentable-ui/ui/styles.css";
|
|
37
|
+
|
|
38
|
+
export default function App() {
|
|
39
|
+
return (
|
|
40
|
+
<AgentableProvider>
|
|
41
|
+
<AgentSurface components={uiCatalog} name="present" maxNodes={60} />
|
|
42
|
+
</AgentableProvider>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
```
|
|
21
46
|
|
|
22
|
-
|
|
47
|
+
The surface declares one `present` tool with a schema derived from its currently allowed catalog. An agent can supply either a program or a JSON tree:
|
|
23
48
|
|
|
24
|
-
|
|
49
|
+
```json
|
|
50
|
+
{"program":"root = Card(\"Hello\")"}
|
|
51
|
+
```
|
|
25
52
|
|
|
26
|
-
|
|
27
|
-
|
|
53
|
+
```json
|
|
54
|
+
{"root":{"component":"Card","props":{"title":"Hello"}}}
|
|
55
|
+
```
|
|
28
56
|
|
|
29
|
-
|
|
57
|
+
A single tool avoids one tool per visual component; its schema size still depends on the catalog. Use a focused catalog and inspect the result in devtools.
|
|
30
58
|
|
|
31
|
-
|
|
59
|
+
## Bind data and handle events
|
|
32
60
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
61
|
+
- `$data.*` in Lang (or `data.*` in JSON bindings) reads application data passed through `data`. Generated UI cannot write to it. Read paths can filter and count arrays: `$data.items[done=true].length`.
|
|
62
|
+
- `$ui.*` is local interface state. Actions can set, merge, push, remove, increment, or toggle it.
|
|
63
|
+
- A catalog entry maps event names to callback props, for example `events: { press: "onPress" }`.
|
|
64
|
+
- `when` conditionally shows a node. `@Say` forwards a message to `onMessage`.
|
|
65
|
+
|
|
66
|
+
For example:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
root = Stack([input, greeting])
|
|
70
|
+
input = TextInput("Name", value: $ui.name, change: @Set($ui.name, $event))
|
|
71
|
+
greeting = Text("Thanks for sharing your name.", when: $ui.name)
|
|
43
72
|
```
|
|
44
73
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
74
|
+
Tool routing differs by host:
|
|
75
|
+
|
|
76
|
+
| Host | Tool configuration |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `Renderer` | `tools={{ tool_name: callback }}` supplies callbacks. Missing names can fall back to a mounted Agentable registry. Direct callbacks follow your application's policies. |
|
|
79
|
+
| `AgentSurface` | `tools={["tool_name"]}` narrows callable registry tools; `tools={false}` disables tool actions. Registry calls retain confirmation, caching, and invalidation. |
|
|
51
80
|
|
|
52
|
-
|
|
81
|
+
`AgentSurface` can also patch the current tree with `present({ ops })`, using node ids and `add`, `update`, `remove`, or `set` operations. When `data` is supplied, a read-only `<name>_state` tool reports the data shape, UI state, and current tree; set `stateTool={false}` to omit it.
|
|
82
|
+
|
|
83
|
+
## Validation and custom catalogs
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { Stat } from "@agentable-ui/ui";
|
|
53
87
|
|
|
54
|
-
```ts
|
|
55
88
|
const catalog = {
|
|
56
89
|
Stat: {
|
|
57
90
|
component: Stat,
|
|
58
|
-
|
|
59
|
-
|
|
91
|
+
description: "A headline number.",
|
|
92
|
+
props: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
label: { type: "string" },
|
|
96
|
+
value: { type: "string" },
|
|
97
|
+
},
|
|
98
|
+
required: ["label", "value"],
|
|
99
|
+
},
|
|
60
100
|
},
|
|
61
101
|
};
|
|
62
102
|
```
|
|
63
103
|
|
|
64
|
-
|
|
104
|
+
Catalog schemas accept JSON Schema or supported Standard Schema validators. Unknown components, invalid props, and excessive depth/node counts produce structured issues. `AgentSurface` defaults to depth 6 and 60 nodes. Renderer prunes invalid nodes so valid parts can remain visible; validation options depend on the host.
|
|
105
|
+
|
|
106
|
+
Lower-level exports include `validateSpec`, `catalogSchema`, and `renderSpec`. Validate before calling `renderSpec` directly.
|
|
107
|
+
|
|
108
|
+
See [lang](../lang) for syntax, prompts, and parsing; [ui](../ui) for the built-in catalog.
|
|
65
109
|
|
|
66
|
-
MIT
|
|
110
|
+
MIT licensed.
|
package/dist/index.d.ts
CHANGED
|
@@ -10,9 +10,19 @@ interface SurfaceState {
|
|
|
10
10
|
data: Record<string, unknown>;
|
|
11
11
|
ui: Record<string, unknown>;
|
|
12
12
|
}
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
/** A read-only list filter in a path: `items[done=true]`, `items[status!="archived"]`. */
|
|
14
|
+
interface PathFilter {
|
|
15
|
+
field: string;
|
|
16
|
+
op: "=" | "!=";
|
|
17
|
+
value: string | number | boolean;
|
|
18
|
+
}
|
|
19
|
+
type PathKey = string | number | PathFilter;
|
|
20
|
+
/**
|
|
21
|
+
* "a.b[0].c" -> ["a", "b", 0, "c"]; "items[done=true]" -> ["items", {field: "done", op: "=", value: true}].
|
|
22
|
+
* Throws on __proto__, prototype and constructor segments, and on malformed selectors.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parsePath(path: string): PathKey[];
|
|
25
|
+
/** Read a path, following own properties only. Filters narrow arrays. Undefined for missing or unsafe paths. */
|
|
16
26
|
export declare function getPath(obj: unknown, path: string): unknown;
|
|
17
27
|
type StateAction = {
|
|
18
28
|
type: "set";
|
|
@@ -274,4 +284,4 @@ export interface AgentSurfaceProps {
|
|
|
274
284
|
*/
|
|
275
285
|
export declare function AgentSurface({ components, name, id, group, tier, priority, maxDepth, maxNodes, placeholder, onPresent, className, clearable, data, tools, stateTool, onActionError, onMessage }: AgentSurfaceProps): import("react").JSX.Element;
|
|
276
286
|
//#endregion
|
|
277
|
-
export type { MessageAction, RendererError, RendererProps, SpecIssue, StateAction, SurfaceOp, SurfaceState, ToolAction, UIAction, UISpec, UiStore, ValidateOptions };
|
|
287
|
+
export type { MessageAction, PathFilter, PathKey, RendererError, RendererProps, SpecIssue, StateAction, SurfaceOp, SurfaceState, ToolAction, UIAction, UISpec, UiStore, ValidateOptions };
|
package/dist/index.js
CHANGED
|
@@ -10,14 +10,62 @@ const UNSAFE_KEYS = /* @__PURE__ */ new Set([
|
|
|
10
10
|
"prototype",
|
|
11
11
|
"constructor"
|
|
12
12
|
]);
|
|
13
|
-
|
|
13
|
+
const isFilter = (k) => typeof k === "object";
|
|
14
|
+
function literal(raw) {
|
|
15
|
+
if (raw.startsWith("\"")) return raw.slice(1, -1);
|
|
16
|
+
if (raw === "true") return true;
|
|
17
|
+
if (raw === "false") return false;
|
|
18
|
+
if (/^-?\d+(\.\d+)?$/.test(raw)) return Number(raw);
|
|
19
|
+
return raw;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* "a.b[0].c" -> ["a", "b", 0, "c"]; "items[done=true]" -> ["items", {field: "done", op: "=", value: true}].
|
|
23
|
+
* Throws on __proto__, prototype and constructor segments, and on malformed selectors.
|
|
24
|
+
*/
|
|
14
25
|
function parsePath(path) {
|
|
15
26
|
if (!path) return [];
|
|
16
|
-
const keys =
|
|
17
|
-
|
|
27
|
+
const keys = [];
|
|
28
|
+
let i = 0;
|
|
29
|
+
while (i < path.length) {
|
|
30
|
+
const c = path[i];
|
|
31
|
+
if (c === ".") {
|
|
32
|
+
i++;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const rest = path.slice(i);
|
|
36
|
+
if (c === "[") {
|
|
37
|
+
const index = /^\[(\d+)\]/.exec(rest);
|
|
38
|
+
if (index) {
|
|
39
|
+
keys.push(Number(index[1]));
|
|
40
|
+
i += index[0].length;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const filter = /^\[([A-Za-z_][A-Za-z0-9_]*)(!?=)("[^"\]]*"|[A-Za-z0-9_.-]+)\]/.exec(rest);
|
|
44
|
+
if (!filter) throw new Error(`Path "${path}" has a bad selector; use [0] or [field=value].`);
|
|
45
|
+
keys.push({
|
|
46
|
+
field: filter[1],
|
|
47
|
+
op: filter[2],
|
|
48
|
+
value: literal(filter[3])
|
|
49
|
+
});
|
|
50
|
+
i += filter[0].length;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const seg = /^[^.[\]]+/.exec(rest)[0];
|
|
54
|
+
keys.push(/^\d+$/.test(seg) ? Number(seg) : seg);
|
|
55
|
+
i += seg.length;
|
|
56
|
+
}
|
|
57
|
+
for (const k of keys) {
|
|
58
|
+
const name = isFilter(k) ? k.field : k;
|
|
59
|
+
if (typeof name === "string" && UNSAFE_KEYS.has(name)) throw new Error(`Path "${path}" uses the reserved key "${name}".`);
|
|
60
|
+
}
|
|
18
61
|
return keys;
|
|
19
62
|
}
|
|
20
|
-
|
|
63
|
+
function matches(item, f) {
|
|
64
|
+
const v = item !== null && typeof item === "object" && Object.hasOwn(item, f.field) ? item[f.field] : void 0;
|
|
65
|
+
const eq = typeof f.value === "boolean" ? Boolean(v) === f.value : v !== void 0 && v !== null && String(v) === String(f.value);
|
|
66
|
+
return f.op === "=" ? eq : !eq;
|
|
67
|
+
}
|
|
68
|
+
/** Read a path, following own properties only. Filters narrow arrays. Undefined for missing or unsafe paths. */
|
|
21
69
|
function getPath(obj, path) {
|
|
22
70
|
let keys;
|
|
23
71
|
try {
|
|
@@ -27,6 +75,11 @@ function getPath(obj, path) {
|
|
|
27
75
|
}
|
|
28
76
|
let cur = obj;
|
|
29
77
|
for (const key of keys) {
|
|
78
|
+
if (isFilter(key)) {
|
|
79
|
+
if (!Array.isArray(cur)) return void 0;
|
|
80
|
+
cur = cur.filter((item) => matches(item, key));
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
30
83
|
if (cur == null || typeof cur !== "object" || !Object.hasOwn(cur, key)) return void 0;
|
|
31
84
|
cur = cur[key];
|
|
32
85
|
}
|
|
@@ -64,6 +117,7 @@ function setPath(obj, keys, value) {
|
|
|
64
117
|
function uiKeys(path) {
|
|
65
118
|
const keys = parsePath(path);
|
|
66
119
|
if (keys[0] !== "ui" || keys.length < 2) throw new Error(`Actions may only write ui.* paths (got "${path}").`);
|
|
120
|
+
if (keys.some(isFilter)) throw new Error(`Filters like [field=value] are read-only (got "${path}").`);
|
|
67
121
|
return keys.slice(1);
|
|
68
122
|
}
|
|
69
123
|
function reduce(ui, a) {
|
|
@@ -554,8 +608,8 @@ function Renderer({ components, library, response, isStreaming = false, data, to
|
|
|
554
608
|
isStreaming
|
|
555
609
|
]);
|
|
556
610
|
const fail = useCallback((message, node) => {
|
|
557
|
-
|
|
558
|
-
|
|
611
|
+
if (onActionErrorRef.current) onActionErrorRef.current(message, node);
|
|
612
|
+
else setActionError(message);
|
|
559
613
|
}, []);
|
|
560
614
|
const emit = useCallback((node, event, value) => {
|
|
561
615
|
const actions = node.on?.[event];
|
|
@@ -1039,8 +1093,8 @@ function AgentSurface({ components, name = "present", id, group, tier, priority,
|
|
|
1039
1093
|
}
|
|
1040
1094
|
} : null);
|
|
1041
1095
|
const fail = useCallback((message, node) => {
|
|
1042
|
-
|
|
1043
|
-
|
|
1096
|
+
if (onActionErrorRef.current) onActionErrorRef.current(message, node);
|
|
1097
|
+
else setError(message);
|
|
1044
1098
|
}, []);
|
|
1045
1099
|
const emit = useCallback((node, event, value) => {
|
|
1046
1100
|
const actions = node.on?.[event];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentable-ui/genui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Generative UI for Agentable: a streaming Renderer for Agentable Lang programs, AgentSurface with a single validated present tool, catalog schema generation.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@agentable-ui/
|
|
38
|
-
"@agentable-ui/
|
|
39
|
-
"@agentable-ui/
|
|
37
|
+
"@agentable-ui/react": "^0.1.1",
|
|
38
|
+
"@agentable-ui/lang": "^0.2.1",
|
|
39
|
+
"@agentable-ui/core": "^0.2.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"react": "^18.2.0 || ^19.0.0"
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@types/react": "^19.2.18",
|
|
48
48
|
"@types/react-dom": "^19.2.7",
|
|
49
49
|
"@testing-library/react": "^16.3.3",
|
|
50
|
-
"@agentable-ui/ui": "^0.
|
|
50
|
+
"@agentable-ui/ui": "^0.2.1"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"build": "tsdown",
|