@tangle-network/ui 11.11.2 → 11.11.3
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/CHANGELOG.md +7 -0
- package/README.md +9 -1
- package/dist/{chat-D-9Fm8A4.js → chat-BUJhNpx2.js} +2 -2
- package/dist/chat.js +1 -1
- package/dist/{document-editor-pane-BcoABlhn.d.ts → document-editor-pane-DHM8DDhb.d.ts} +4 -1
- package/dist/{document-editor-pane-BxlPjaTP.js → document-editor-pane-LyanNypc.js} +6 -14
- package/dist/editor.d.ts +1 -1
- package/dist/editor.js +1 -1
- package/dist/{files-Bug33qCl.js → files-DgPc3vI2.js} +16 -7
- package/dist/files.d.ts +1 -1
- package/dist/files.js +1 -1
- package/dist/{index-DhYmSaLF.d.ts → index-CyGSLGVF.d.ts} +6 -17
- package/dist/index.d.ts +3 -2
- package/dist/index.js +6 -5
- package/dist/{openui-artifact-renderer-DrU1AdVk.js → openui-artifact-renderer-C3bmqfjX.js} +15 -20
- package/dist/openui-schema.d.ts +3 -0
- package/dist/openui-schema.js +2 -0
- package/dist/openui.d.ts +2 -1
- package/dist/openui.js +3 -2
- package/dist/{run-group-BACLbCFl.js → run-group-CxNskmHd.js} +1 -1
- package/dist/run.js +1 -1
- package/dist/schema-BFKgCGH-.d.ts +228 -0
- package/dist/schema-BpW9rSSG.js +9754 -0
- package/package.json +9 -2
- package/src/editor/document-editor-pane.tsx +5 -1
- package/src/files/file-artifact-pane.test.tsx +41 -0
- package/src/files/file-artifact-pane.tsx +3 -0
- package/src/files/file-preview.tsx +16 -3
- package/src/openui/index.ts +10 -0
- package/src/openui/openui-artifact-renderer.stories.tsx +36 -0
- package/src/openui/openui-artifact-renderer.tsx +19 -27
- package/src/openui/schema-compiled.ts +3 -0
- package/src/openui/schema-data.ts +95 -0
- package/src/openui/schema.test.tsx +74 -0
- package/src/openui/schema.ts +136 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export const OPENUI_NODE_TYPES = [
|
|
2
|
+
"heading", "text", "badge", "stat", "key_value", "code", "markdown",
|
|
3
|
+
"table", "actions", "separator", "stack", "grid", "card",
|
|
4
|
+
] as const;
|
|
5
|
+
|
|
6
|
+
const string = { type: "string" } as const;
|
|
7
|
+
const boolean = { type: "boolean" } as const;
|
|
8
|
+
const gap = { enum: ["sm", "md", "lg"] } as const;
|
|
9
|
+
const primitive = { type: ["string", "number", "boolean", "null"] } as const;
|
|
10
|
+
const children = { type: "array", minItems: 1, items: { $ref: "#/$defs/node" } } as const;
|
|
11
|
+
const actions = { type: "array", minItems: 1, items: { $ref: "#/$defs/action" } } as const;
|
|
12
|
+
|
|
13
|
+
function node(type: string, fields: Record<string, unknown>, required: string[] = []) {
|
|
14
|
+
return {
|
|
15
|
+
type: "object",
|
|
16
|
+
required: ["type", ...required],
|
|
17
|
+
properties: { type: { const: type }, id: string, ...fields },
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The model-facing shape and the persistence gate share this exact schema.
|
|
24
|
+
* It covers JSON authored by an agent, not the renderer's programmatic ReactNode
|
|
25
|
+
* cells or onPress callbacks.
|
|
26
|
+
*/
|
|
27
|
+
export const OPENUI_NODE_JSON_SCHEMA = {
|
|
28
|
+
type: "object",
|
|
29
|
+
$ref: "#/$defs/node",
|
|
30
|
+
$defs: {
|
|
31
|
+
node: { oneOf: OPENUI_NODE_TYPES.map((type) => ({ $ref: `#/$defs/${type}` })) },
|
|
32
|
+
action: {
|
|
33
|
+
type: "object",
|
|
34
|
+
required: ["id", "label"],
|
|
35
|
+
properties: {
|
|
36
|
+
id: string,
|
|
37
|
+
label: string,
|
|
38
|
+
tone: { enum: ["default", "secondary", "outline", "ghost", "destructive"] },
|
|
39
|
+
disabled: boolean,
|
|
40
|
+
},
|
|
41
|
+
additionalProperties: false,
|
|
42
|
+
},
|
|
43
|
+
heading: node("heading", {
|
|
44
|
+
text: string, level: { enum: [1, 2, 3, 4] }, kicker: string, meta: string,
|
|
45
|
+
}, ["text"]),
|
|
46
|
+
text: node("text", {
|
|
47
|
+
text: string, tone: { enum: ["default", "muted", "success", "warning", "error"] }, mono: boolean,
|
|
48
|
+
}, ["text"]),
|
|
49
|
+
badge: node("badge", {
|
|
50
|
+
label: string, tone: { enum: ["default", "secondary", "success", "warning", "error", "info", "sandbox"] },
|
|
51
|
+
}, ["label"]),
|
|
52
|
+
stat: node("stat", {
|
|
53
|
+
label: string, value: string, change: string,
|
|
54
|
+
tone: { enum: ["default", "success", "warning", "error", "info"] },
|
|
55
|
+
}, ["label", "value"]),
|
|
56
|
+
key_value: node("key_value", {
|
|
57
|
+
items: {
|
|
58
|
+
type: "array", minItems: 1,
|
|
59
|
+
items: {
|
|
60
|
+
type: "object", required: ["label", "value"],
|
|
61
|
+
properties: { id: string, label: string, value: primitive, tone: { enum: ["default", "muted"] } },
|
|
62
|
+
additionalProperties: false,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
}, ["items"]),
|
|
66
|
+
code: node("code", {
|
|
67
|
+
code: string, language: string, title: string, showLineNumbers: boolean,
|
|
68
|
+
}, ["code"]),
|
|
69
|
+
markdown: node("markdown", { content: string }, ["content"]),
|
|
70
|
+
table: node("table", {
|
|
71
|
+
columns: {
|
|
72
|
+
type: "array", minItems: 1,
|
|
73
|
+
items: {
|
|
74
|
+
type: "object", required: ["key", "header"],
|
|
75
|
+
properties: { key: string, header: string, align: { enum: ["left", "right"] } },
|
|
76
|
+
additionalProperties: false,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
rows: { type: "array", items: { type: "object", additionalProperties: primitive } },
|
|
80
|
+
caption: string,
|
|
81
|
+
}, ["columns", "rows"]),
|
|
82
|
+
actions: node("actions", { actions }, ["actions"]),
|
|
83
|
+
separator: node("separator", { label: string }),
|
|
84
|
+
stack: node("stack", {
|
|
85
|
+
direction: { enum: ["row", "column"] }, gap,
|
|
86
|
+
align: { enum: ["start", "center", "end", "stretch"] }, wrap: boolean, children,
|
|
87
|
+
}, ["children"]),
|
|
88
|
+
grid: node("grid", { columns: { enum: [1, 2, 3, 4] }, gap, children }, ["children"]),
|
|
89
|
+
card: node("card", {
|
|
90
|
+
title: string, description: string, eyebrow: string,
|
|
91
|
+
badge: { $ref: "#/$defs/badge" }, actions, children,
|
|
92
|
+
}),
|
|
93
|
+
},
|
|
94
|
+
} as const;
|
|
95
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { OpenUIArtifactRenderer } from "./openui-artifact-renderer";
|
|
4
|
+
import { OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES, validateOpenUIJsonArtifact, validateOpenUIJsonNode } from "./schema";
|
|
5
|
+
|
|
6
|
+
const validPage = {
|
|
7
|
+
type: "card",
|
|
8
|
+
title: "Release proof",
|
|
9
|
+
badge: { type: "badge", label: "Ready", tone: "success" },
|
|
10
|
+
children: [
|
|
11
|
+
{ type: "heading", text: "Result", level: 2 },
|
|
12
|
+
{ type: "text", text: "Three checks passed." },
|
|
13
|
+
{ type: "stack", children: [
|
|
14
|
+
{ type: "stat", label: "Checks", value: "3" },
|
|
15
|
+
{ type: "key_value", items: [{ label: "Allowed", value: true }, { label: "Missing", value: null }] },
|
|
16
|
+
{ type: "code", code: "ok", language: "text" },
|
|
17
|
+
{ type: "markdown", content: "**Done**" },
|
|
18
|
+
{ type: "table", columns: [{ key: "name", header: "Name" }], rows: [{ name: "one" }] },
|
|
19
|
+
{ type: "actions", actions: [{ id: "open", label: "Open" }] },
|
|
20
|
+
{ type: "separator" },
|
|
21
|
+
{ type: "grid", children: [{ type: "badge", label: "Nested" }] },
|
|
22
|
+
] },
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe("OpenUI JSON contract", () => {
|
|
27
|
+
it("advertises every renderer node and accepts nested JSON with primitive cells", () => {
|
|
28
|
+
expect(OPENUI_NODE_TYPES).toHaveLength(13);
|
|
29
|
+
expect(OPENUI_NODE_JSON_SCHEMA.$defs.node.oneOf).toHaveLength(13);
|
|
30
|
+
expect(validateOpenUIJsonNode(validPage)).toMatchObject({ ok: true, value: validPage });
|
|
31
|
+
expect(validateOpenUIJsonArtifact([validPage, { type: "text", text: "Afterword" }])).toMatchObject({ ok: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("rejects an unsupported top-level or nested section before it can be dropped", () => {
|
|
35
|
+
expect(validateOpenUIJsonNode({ type: "section", children: [{ type: "text", text: "hidden" }] }))
|
|
36
|
+
.toMatchObject({ ok: false, issue: { path: "$", message: expect.stringContaining("section") } });
|
|
37
|
+
const nested = { type: "card", children: [{ type: "section", children: [{ type: "text", text: "hidden" }] }] };
|
|
38
|
+
expect(validateOpenUIJsonNode(nested))
|
|
39
|
+
.toMatchObject({ ok: false, issue: { path: "$.children[0]", message: expect.stringContaining("section") } });
|
|
40
|
+
expect(validateOpenUIJsonArtifact([{ type: "text", text: "visible" }, nested]))
|
|
41
|
+
.toMatchObject({ ok: false, issue: { path: "$[1].children[0]" } });
|
|
42
|
+
expect(validateOpenUIJsonArtifact(Array.from({ length: 1001 }, () => ({ type: "text", text: "too many" }))))
|
|
43
|
+
.toMatchObject({ ok: false, issue: { path: "$[1000]" } });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("rejects fields the renderer would ignore and table cells with no column", () => {
|
|
47
|
+
expect(validateOpenUIJsonNode({ type: "text", text: "hello", body: "hidden" }).ok).toBe(false);
|
|
48
|
+
expect(validateOpenUIJsonNode({ type: "stack", children: [] }).ok).toBe(false);
|
|
49
|
+
expect(validateOpenUIJsonNode({ type: "key_value", items: [{ label: "Broken", value: { nested: true } }] }).ok).toBe(false);
|
|
50
|
+
expect(validateOpenUIJsonNode({ type: "table", columns: [{ key: "shown", header: "Shown" }], rows: [{ hidden: "lost" }] }))
|
|
51
|
+
.toMatchObject({ ok: false, issue: { path: "$.rows[0].hidden" } });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("rejects arrays passed as a single node while keeping optional card fields", () => {
|
|
55
|
+
expect(validateOpenUIJsonNode([{ type: "text", text: "ok" }]))
|
|
56
|
+
.toMatchObject({ ok: false, issue: { path: "$" } });
|
|
57
|
+
expect(validateOpenUIJsonNode({ type: "card" }).ok).toBe(true);
|
|
58
|
+
expect(validateOpenUIJsonNode({ type: "card", title: "A result" }).ok).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("renders an absent prototype-named table cell safely", () => {
|
|
62
|
+
const table = JSON.parse('{"type":"table","columns":[{"key":"__proto__","header":"Value"}],"rows":[{}]}');
|
|
63
|
+
expect(validateOpenUIJsonNode(table).ok).toBe(true);
|
|
64
|
+
render(<OpenUIArtifactRenderer schema={table} />);
|
|
65
|
+
expect(screen.getByText("Value")).toBeTruthy();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("shows an explicit error for an old unsupported artifact", () => {
|
|
69
|
+
const oldArtifact = { type: "card", children: [{ type: "section", children: [{ type: "text", text: "hidden" }] }] };
|
|
70
|
+
render(<OpenUIArtifactRenderer schema={oldArtifact as never} />);
|
|
71
|
+
expect(screen.getByRole("alert").textContent).toContain("$.children[0]");
|
|
72
|
+
expect(screen.getByRole("alert").textContent).toContain("section");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { OpenUIComponentNode } from "./openui-artifact-renderer";
|
|
2
|
+
import type { ValidateFunction } from "ajv";
|
|
3
|
+
import { OPENUI_NODE_TYPES } from "./schema-data";
|
|
4
|
+
import * as validators from "./schema-compiled";
|
|
5
|
+
|
|
6
|
+
export { OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES } from "./schema-data";
|
|
7
|
+
export type { OpenUIComponentNode } from "./openui-artifact-renderer";
|
|
8
|
+
|
|
9
|
+
export interface OpenUISchemaIssue {
|
|
10
|
+
path: string;
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type OpenUISchemaCheck =
|
|
15
|
+
| { ok: true }
|
|
16
|
+
| { ok: false; issue: OpenUISchemaIssue };
|
|
17
|
+
|
|
18
|
+
export type OpenUISchemaValidation<T> =
|
|
19
|
+
| { ok: true; value: T }
|
|
20
|
+
| { ok: false; issue: OpenUISchemaIssue };
|
|
21
|
+
|
|
22
|
+
const nodeTypes: ReadonlySet<string> = new Set(OPENUI_NODE_TYPES);
|
|
23
|
+
|
|
24
|
+
/** Find an unsupported node at any depth before the renderer can omit it. */
|
|
25
|
+
export function findUnsupportedOpenUINode(value: unknown): OpenUISchemaIssue | null {
|
|
26
|
+
const seen = new WeakSet<object>();
|
|
27
|
+
let count = 0;
|
|
28
|
+
|
|
29
|
+
function visit(candidate: unknown, path: string, depth: number): OpenUISchemaIssue | null {
|
|
30
|
+
if (depth > 32 || ++count > 1000) {
|
|
31
|
+
return { path, message: "OpenUI exceeds the 32-level or 1000-node limit." };
|
|
32
|
+
}
|
|
33
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
34
|
+
return { path, message: "Expected an OpenUI node object." };
|
|
35
|
+
}
|
|
36
|
+
if (seen.has(candidate)) return { path, message: "OpenUI nodes must not contain cycles." };
|
|
37
|
+
seen.add(candidate);
|
|
38
|
+
const record = candidate as Record<string, unknown>;
|
|
39
|
+
if (typeof record.type !== "string" || !nodeTypes.has(record.type)) {
|
|
40
|
+
return { path, message: `Unsupported OpenUI node type: ${String(record.type)}.` };
|
|
41
|
+
}
|
|
42
|
+
if (record.type === "stack" || record.type === "grid" || record.type === "card") {
|
|
43
|
+
if (Array.isArray(record.children)) {
|
|
44
|
+
for (const [index, child] of record.children.entries()) {
|
|
45
|
+
const issue = visit(child, `${path}.children[${index}]`, depth + 1);
|
|
46
|
+
if (issue) return issue;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (record.type === "card" && record.badge !== undefined) {
|
|
51
|
+
return visit(record.badge, `${path}.badge`, depth + 1);
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (Array.isArray(value)) {
|
|
57
|
+
for (const [index, candidate] of value.entries()) {
|
|
58
|
+
const issue = visit(candidate, `$[${index}]`, 0);
|
|
59
|
+
if (issue) return issue;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return visit(value, "$", 0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Validate one model-authored JSON node before a product persists or spends on it. */
|
|
67
|
+
export function validateOpenUIJsonNode(value: unknown): OpenUISchemaValidation<OpenUIComponentNode> {
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
return { ok: false, issue: { path: "$", message: "Expected one OpenUI node object; use validateOpenUIJsonArtifact for arrays." } };
|
|
70
|
+
}
|
|
71
|
+
const unsupported = findUnsupportedOpenUINode(value);
|
|
72
|
+
if (unsupported) return { ok: false, issue: unsupported };
|
|
73
|
+
const result = validateNode(value as Record<string, unknown>, "$", 0);
|
|
74
|
+
return result.ok ? { ok: true, value: value as OpenUIComponentNode } : result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Validate the renderer's single-node or nonempty node-array artifact shape. */
|
|
78
|
+
export function validateOpenUIJsonArtifact(value: unknown): OpenUISchemaValidation<OpenUIComponentNode | OpenUIComponentNode[]> {
|
|
79
|
+
if (!Array.isArray(value)) return validateOpenUIJsonNode(value);
|
|
80
|
+
if (value.length === 0) {
|
|
81
|
+
return { ok: false, issue: { path: "$", message: "An OpenUI artifact needs at least one node." } };
|
|
82
|
+
}
|
|
83
|
+
const unsupported = findUnsupportedOpenUINode(value);
|
|
84
|
+
if (unsupported) return { ok: false, issue: unsupported };
|
|
85
|
+
for (const [index, node] of value.entries()) {
|
|
86
|
+
const result = validateOpenUIJsonNode(node);
|
|
87
|
+
if (!result.ok) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
issue: { ...result.issue, path: `$[${index}]${result.issue.path.slice(1)}` },
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { ok: true, value: value as OpenUIComponentNode[] };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function validateNode(record: Record<string, unknown>, path: string, depth: number): OpenUISchemaCheck {
|
|
98
|
+
if (depth > 32) return { ok: false, issue: { path, message: "OpenUI exceeds the 32-level limit." } };
|
|
99
|
+
if (record.type === "stack" || record.type === "grid" || record.type === "card") {
|
|
100
|
+
if (Array.isArray(record.children)) {
|
|
101
|
+
for (const [index, child] of record.children.entries()) {
|
|
102
|
+
const result = validateNode(child as Record<string, unknown>, `${path}.children[${index}]`, depth + 1);
|
|
103
|
+
if (!result.ok) return result;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (record.type === "card" && record.badge && typeof record.badge === "object") {
|
|
108
|
+
const result = validateNode(record.badge as Record<string, unknown>, `${path}.badge`, depth + 1);
|
|
109
|
+
if (!result.ok) return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const validator = validators[record.type as (typeof OPENUI_NODE_TYPES)[number]] as ValidateFunction;
|
|
113
|
+
if (!validator(record)) {
|
|
114
|
+
const error = validator.errors?.[0];
|
|
115
|
+
const field = error?.keyword === "required"
|
|
116
|
+
? String(error.params.missingProperty)
|
|
117
|
+
: error?.keyword === "additionalProperties"
|
|
118
|
+
? String(error.params.additionalProperty)
|
|
119
|
+
: null;
|
|
120
|
+
const suffix = `${error?.instancePath ?? ""}${field ? `/${field}` : ""}`
|
|
121
|
+
.replaceAll(/\/(\d+)/g, "[$1]")
|
|
122
|
+
.replaceAll("/", ".");
|
|
123
|
+
return { ok: false, issue: { path: `${path}${suffix}`, message: error?.message ?? "Invalid OpenUI node." } };
|
|
124
|
+
}
|
|
125
|
+
if (record.type === "table") {
|
|
126
|
+
const columns = new Set((record.columns as Array<{ key: string }>).map((column) => column.key));
|
|
127
|
+
for (const [index, row] of (record.rows as Array<Record<string, unknown>>).entries()) {
|
|
128
|
+
for (const key of Object.keys(row)) {
|
|
129
|
+
if (!columns.has(key)) {
|
|
130
|
+
return { ok: false, issue: { path: `${path}.rows[${index}].${key}`, message: `Table cell ${key} has no displayed column.` } };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { ok: true };
|
|
136
|
+
}
|