json-ui-lite-rn 0.12.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 +230 -0
- package/lib/commonjs/GenUINode.js +113 -0
- package/lib/commonjs/GenUINode.js.map +1 -0
- package/lib/commonjs/GenerativeUIView.js +116 -0
- package/lib/commonjs/GenerativeUIView.js.map +1 -0
- package/lib/commonjs/index.js +78 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/parseGenUIProps.js +53 -0
- package/lib/commonjs/parseGenUIProps.js.map +1 -0
- package/lib/commonjs/prompt.js +27 -0
- package/lib/commonjs/prompt.js.map +1 -0
- package/lib/commonjs/registry.js +70 -0
- package/lib/commonjs/registry.js.map +1 -0
- package/lib/commonjs/tools.js +386 -0
- package/lib/commonjs/tools.js.map +1 -0
- package/lib/module/GenUINode.js +108 -0
- package/lib/module/GenUINode.js.map +1 -0
- package/lib/module/GenerativeUIView.js +111 -0
- package/lib/module/GenerativeUIView.js.map +1 -0
- package/lib/module/index.js +9 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/parseGenUIProps.js +49 -0
- package/lib/module/parseGenUIProps.js.map +1 -0
- package/lib/module/prompt.js +23 -0
- package/lib/module/prompt.js.map +1 -0
- package/lib/module/registry.js +66 -0
- package/lib/module/registry.js.map +1 -0
- package/lib/module/tools.js +383 -0
- package/lib/module/tools.js.map +1 -0
- package/lib/typescript/GenUINode.d.ts +12 -0
- package/lib/typescript/GenUINode.d.ts.map +1 -0
- package/lib/typescript/GenerativeUIView.d.ts +23 -0
- package/lib/typescript/GenerativeUIView.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +8 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/parseGenUIProps.d.ts +20 -0
- package/lib/typescript/parseGenUIProps.d.ts.map +1 -0
- package/lib/typescript/prompt.d.ts +12 -0
- package/lib/typescript/prompt.d.ts.map +1 -0
- package/lib/typescript/registry.d.ts +36 -0
- package/lib/typescript/registry.d.ts.map +1 -0
- package/lib/typescript/tools.d.ts +119 -0
- package/lib/typescript/tools.d.ts.map +1 -0
- package/package.json +56 -0
- package/src/GenUINode.tsx +115 -0
- package/src/GenerativeUIView.tsx +137 -0
- package/src/index.ts +25 -0
- package/src/parseGenUIProps.ts +59 -0
- package/src/prompt.ts +49 -0
- package/src/registry.ts +73 -0
- package/src/tools.ts +392 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import type { JsonUIElement } from './registry'
|
|
4
|
+
|
|
5
|
+
export type GenUIStylesConfig = Record<string, z.ZodTypeAny>
|
|
6
|
+
|
|
7
|
+
export type ParsedGenUIProps = {
|
|
8
|
+
baseStyle: Record<string, unknown>
|
|
9
|
+
text: string | undefined
|
|
10
|
+
label: string | undefined
|
|
11
|
+
props: Record<string, unknown>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ParseGenUIElementPropsOptions = {
|
|
15
|
+
nodeId?: string
|
|
16
|
+
type?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parses a JSON UI element's props into a baseStyle object (validated by style validators)
|
|
21
|
+
* and common fields (text, label). Use this in custom GenUINode implementations to reuse
|
|
22
|
+
* the same style and prop parsing as the default renderer.
|
|
23
|
+
*/
|
|
24
|
+
export function parseGenUIElementProps(
|
|
25
|
+
element: JsonUIElement,
|
|
26
|
+
styleValidators: GenUIStylesConfig,
|
|
27
|
+
options?: ParseGenUIElementPropsOptions
|
|
28
|
+
): ParsedGenUIProps {
|
|
29
|
+
const { nodeId = '', type = '' } = options ?? {}
|
|
30
|
+
const props = element.props ?? {}
|
|
31
|
+
const style = props.style as Record<string, unknown> | undefined
|
|
32
|
+
const flex = props.flex as number | undefined
|
|
33
|
+
const padding = props.padding as number | undefined
|
|
34
|
+
const gap = props.gap as number | undefined
|
|
35
|
+
const text = (props.text ?? props.value ?? props.label) as string | undefined
|
|
36
|
+
const label = (props.label ?? props.text) as string | undefined
|
|
37
|
+
|
|
38
|
+
const baseStyle = {
|
|
39
|
+
...(flex != null ? { flex } : {}),
|
|
40
|
+
...(padding != null ? { padding } : {}),
|
|
41
|
+
...(gap != null ? { gap } : {}),
|
|
42
|
+
...style,
|
|
43
|
+
} as Record<string, unknown>
|
|
44
|
+
|
|
45
|
+
for (const key of Object.keys(props)) {
|
|
46
|
+
const validator = styleValidators[key]
|
|
47
|
+
if (validator) {
|
|
48
|
+
if (validator.safeParse(props[key]).success) {
|
|
49
|
+
baseStyle[key] = props[key]
|
|
50
|
+
} else {
|
|
51
|
+
console.warn(
|
|
52
|
+
`[json-ui-lite-rn] Invalid style prop: ${key} for node ${nodeId} of type ${type}: ${props[key]}`
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { baseStyle, text, label, props }
|
|
59
|
+
}
|
package/src/prompt.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { GEN_UI_STYLE_HINTS } from './registry'
|
|
2
|
+
|
|
3
|
+
type StyleHints = Record<
|
|
4
|
+
string,
|
|
5
|
+
{
|
|
6
|
+
type: 'number' | 'string'
|
|
7
|
+
description?: string
|
|
8
|
+
}
|
|
9
|
+
>
|
|
10
|
+
|
|
11
|
+
export type BuildGenUISystemPromptOptions = {
|
|
12
|
+
additionalInstructions?: string
|
|
13
|
+
requireLayoutReadBeforeAddingNodes?: boolean
|
|
14
|
+
styleHints?: StyleHints
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildGenUISystemPrompt({
|
|
18
|
+
additionalInstructions,
|
|
19
|
+
requireLayoutReadBeforeAddingNodes = true,
|
|
20
|
+
styleHints = GEN_UI_STYLE_HINTS,
|
|
21
|
+
}: BuildGenUISystemPromptOptions = {}) {
|
|
22
|
+
const styleKeysText = Object.entries(styleHints)
|
|
23
|
+
.map(([key, entry]) => {
|
|
24
|
+
let text = `${key} [${entry.type}]`
|
|
25
|
+
if (entry.description) text += ` (${entry.description})`
|
|
26
|
+
return text
|
|
27
|
+
})
|
|
28
|
+
.join(', ')
|
|
29
|
+
|
|
30
|
+
const parts = [
|
|
31
|
+
'You are a helpful assistant.',
|
|
32
|
+
'You have tools to create and update UI nodes. Before any tool calls for UI, ALWAYS CALL getUILayout before and after.',
|
|
33
|
+
'Remember this is React Native, not web, and use simple props.',
|
|
34
|
+
`If you set the "style" prop on a UI node, the possible keys are: ${styleKeysText}.`,
|
|
35
|
+
'Remember NEVER use web values.',
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
if (requireLayoutReadBeforeAddingNodes) {
|
|
39
|
+
parts.push(
|
|
40
|
+
'BEFORE ADDING ANY UI ELEMENTS, GET THE WHOLE UI TREE with getGenUILayout.'
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (additionalInstructions?.trim()) {
|
|
45
|
+
parts.push(additionalInstructions.trim())
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return parts.join(' ')
|
|
49
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export type JsonUIElement = {
|
|
4
|
+
type: string
|
|
5
|
+
props: Record<string, unknown>
|
|
6
|
+
children?: string[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type JsonUISpec = {
|
|
10
|
+
root: string
|
|
11
|
+
elements: Record<string, JsonUIElement>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_GEN_UI_ROOT_ID = 'root'
|
|
15
|
+
|
|
16
|
+
export const GEN_UI_NODE_NAMES = {
|
|
17
|
+
Text: 'Text',
|
|
18
|
+
Paragraph: 'Paragraph',
|
|
19
|
+
Label: 'Label',
|
|
20
|
+
Heading: 'Heading',
|
|
21
|
+
Button: 'Button',
|
|
22
|
+
TextInput: 'TextInput',
|
|
23
|
+
} as const
|
|
24
|
+
|
|
25
|
+
export const GEN_UI_NODE_NAMES_THAT_SUPPORT_CHILDREN: string[] = []
|
|
26
|
+
|
|
27
|
+
export const GEN_UI_NODE_HINTS: Record<keyof typeof GEN_UI_NODE_NAMES, string> =
|
|
28
|
+
{
|
|
29
|
+
Text: 'Single line text. Props: text [string], style [object].',
|
|
30
|
+
Paragraph: 'Long text. Props: text [string], style [object].',
|
|
31
|
+
Label: 'Small label. Props: text [string], style [object].',
|
|
32
|
+
Heading: 'Title text. Props: text [string], style [object].',
|
|
33
|
+
Button: 'Tap button. Props: text [string], style [object].',
|
|
34
|
+
TextInput:
|
|
35
|
+
'Single line text input. Props: placeholder [string], style [object].',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const GEN_UI_STYLES = {
|
|
39
|
+
flex: z.number(),
|
|
40
|
+
padding: z.number(),
|
|
41
|
+
gap: z.number(),
|
|
42
|
+
backgroundColor: z.string(),
|
|
43
|
+
color: z.string(),
|
|
44
|
+
fontSize: z.number(),
|
|
45
|
+
fontWeight: z.string(),
|
|
46
|
+
textAlign: z.string(),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const GEN_UI_STYLE_HINTS: Record<
|
|
50
|
+
keyof typeof GEN_UI_STYLES,
|
|
51
|
+
{ type: 'number' | 'string'; description?: string }
|
|
52
|
+
> = {
|
|
53
|
+
flex: { type: 'number', description: 'Flex grow/shrink basis value.' },
|
|
54
|
+
padding: {
|
|
55
|
+
type: 'number',
|
|
56
|
+
description: 'Padding in density-independent px.',
|
|
57
|
+
},
|
|
58
|
+
gap: {
|
|
59
|
+
type: 'number',
|
|
60
|
+
description: 'Spacing between children in density-independent px.',
|
|
61
|
+
},
|
|
62
|
+
backgroundColor: { type: 'string', description: 'React Native color value.' },
|
|
63
|
+
color: { type: 'string', description: 'Text color value.' },
|
|
64
|
+
fontSize: { type: 'number', description: 'Font size in points.' },
|
|
65
|
+
fontWeight: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
description: 'Font weight string like "400", "600", "bold".',
|
|
68
|
+
},
|
|
69
|
+
textAlign: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
description: 'Text alignment, for example "left", "center", "right".',
|
|
72
|
+
},
|
|
73
|
+
}
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { tool } from 'ai'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_GEN_UI_ROOT_ID,
|
|
6
|
+
GEN_UI_NODE_HINTS,
|
|
7
|
+
GEN_UI_NODE_NAMES_THAT_SUPPORT_CHILDREN,
|
|
8
|
+
type JsonUISpec,
|
|
9
|
+
} from './registry'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Sometimes LLMs call tools with a string instead of an object.
|
|
13
|
+
*/
|
|
14
|
+
function smartParse(
|
|
15
|
+
props: string | Record<string, unknown>
|
|
16
|
+
): Record<string, unknown> {
|
|
17
|
+
return typeof props === 'string' ? JSON.parse(props) : props
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const defaultCreateId = () =>
|
|
21
|
+
`UI-${Date.now().toString(36)}-${Math.random().toString(16).slice(2)}`
|
|
22
|
+
|
|
23
|
+
export type CreateGenTUIoolsOptions<TSpec extends JsonUISpec = JsonUISpec> = {
|
|
24
|
+
contextId: string
|
|
25
|
+
getSpec: (contextId: string) => TSpec | null
|
|
26
|
+
updateSpec: (contextId: string, spec: TSpec | null) => void
|
|
27
|
+
createId?: () => string
|
|
28
|
+
rootId?: string
|
|
29
|
+
nodeHints?: Record<string, string>
|
|
30
|
+
nodeNamesThatSupportChildren?: readonly string[]
|
|
31
|
+
toolWrapper?: <TArgs, TResult>(
|
|
32
|
+
toolName: string,
|
|
33
|
+
execute: (args: TArgs) => Promise<TResult>
|
|
34
|
+
) => (args: TArgs) => Promise<TResult>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const cloneSpec = <TSpec extends JsonUISpec>(spec: TSpec): TSpec =>
|
|
38
|
+
({
|
|
39
|
+
...spec,
|
|
40
|
+
elements: Object.fromEntries(
|
|
41
|
+
Object.entries(spec.elements).map(([id, element]) => [
|
|
42
|
+
id,
|
|
43
|
+
{
|
|
44
|
+
...element,
|
|
45
|
+
props: { ...(element.props ?? {}) },
|
|
46
|
+
children: [...(element.children ?? [])],
|
|
47
|
+
},
|
|
48
|
+
])
|
|
49
|
+
),
|
|
50
|
+
}) as TSpec
|
|
51
|
+
|
|
52
|
+
let mutationQueue: Promise<void> = Promise.resolve()
|
|
53
|
+
|
|
54
|
+
const withMutationLock = async <T>(run: () => Promise<T>): Promise<T> => {
|
|
55
|
+
let release: () => void = () => {}
|
|
56
|
+
const pending = new Promise<void>((resolve) => {
|
|
57
|
+
release = resolve
|
|
58
|
+
})
|
|
59
|
+
const previous = mutationQueue
|
|
60
|
+
mutationQueue = mutationQueue.then(() => pending)
|
|
61
|
+
|
|
62
|
+
await previous
|
|
63
|
+
try {
|
|
64
|
+
return await run()
|
|
65
|
+
} finally {
|
|
66
|
+
release()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Creates generative UI tools that read/update a JSON UI spec.
|
|
72
|
+
*/
|
|
73
|
+
export function createGenUITools<TSpec extends JsonUISpec = JsonUISpec>({
|
|
74
|
+
contextId,
|
|
75
|
+
getSpec,
|
|
76
|
+
updateSpec,
|
|
77
|
+
createId = defaultCreateId,
|
|
78
|
+
rootId = DEFAULT_GEN_UI_ROOT_ID,
|
|
79
|
+
nodeHints = GEN_UI_NODE_HINTS,
|
|
80
|
+
nodeNamesThatSupportChildren = GEN_UI_NODE_NAMES_THAT_SUPPORT_CHILDREN,
|
|
81
|
+
toolWrapper = (_, execute) => execute,
|
|
82
|
+
}: CreateGenTUIoolsOptions<TSpec>) {
|
|
83
|
+
// Serialize mutating tool calls to avoid interleaving writes.
|
|
84
|
+
let cachedSpec: TSpec | null = null
|
|
85
|
+
|
|
86
|
+
const readSpec = (): TSpec | null => {
|
|
87
|
+
if (cachedSpec) return cloneSpec(cachedSpec)
|
|
88
|
+
const spec = getSpec(contextId)
|
|
89
|
+
if (!spec) return null
|
|
90
|
+
cachedSpec = cloneSpec(spec)
|
|
91
|
+
return cloneSpec(cachedSpec)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const commitSpec = (spec: TSpec | null) => {
|
|
95
|
+
cachedSpec = spec ? cloneSpec(spec) : null
|
|
96
|
+
updateSpec(contextId, spec ? cloneSpec(spec) : null)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const getUIRootNode = tool({
|
|
100
|
+
description:
|
|
101
|
+
'Get the root node of the generative UI tree. Returns id, type, props, and children (array of { id, type }). Root always exists with id "root".',
|
|
102
|
+
inputSchema: z.object({}),
|
|
103
|
+
execute: toolWrapper('getUIRootNode', async () => {
|
|
104
|
+
const spec = readSpec()
|
|
105
|
+
if (!spec?.root || !spec.elements[spec.root]) return { root: null }
|
|
106
|
+
const element = spec.elements[spec.root]
|
|
107
|
+
const children = (element.children ?? []).map((id) => ({
|
|
108
|
+
id,
|
|
109
|
+
type: spec.elements[id]?.type ?? 'unknown',
|
|
110
|
+
}))
|
|
111
|
+
return {
|
|
112
|
+
root: {
|
|
113
|
+
id: spec.root,
|
|
114
|
+
type: element.type,
|
|
115
|
+
props: element.props,
|
|
116
|
+
children,
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
}),
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const getUINode = tool({
|
|
123
|
+
description:
|
|
124
|
+
'Get a node by id. Returns id, type, props, and children (array of { id, type }). If id is omitted, returns root node.',
|
|
125
|
+
inputSchema: z.object({
|
|
126
|
+
id: z.string().optional().describe('Node id; omit for root'),
|
|
127
|
+
}),
|
|
128
|
+
execute: toolWrapper('getUINode', async ({ id }) => {
|
|
129
|
+
const spec = readSpec()
|
|
130
|
+
if (!spec) return { node: null }
|
|
131
|
+
const nodeId = id ?? spec.root
|
|
132
|
+
const element = spec.elements[nodeId]
|
|
133
|
+
if (!element) return { node: null }
|
|
134
|
+
const children = (element.children ?? []).map((childId) => ({
|
|
135
|
+
id: childId,
|
|
136
|
+
type: spec.elements[childId]?.type ?? 'unknown',
|
|
137
|
+
}))
|
|
138
|
+
return {
|
|
139
|
+
node: {
|
|
140
|
+
id: nodeId,
|
|
141
|
+
type: element.type,
|
|
142
|
+
props: element.props,
|
|
143
|
+
children,
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
}),
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const getUILayout = tool({
|
|
150
|
+
description: 'Get compact UI layout.',
|
|
151
|
+
inputSchema: z.object({}),
|
|
152
|
+
execute: toolWrapper('getUILayout', async () => {
|
|
153
|
+
const spec = readSpec()
|
|
154
|
+
if (!spec) return { root: null, nodes: [] }
|
|
155
|
+
|
|
156
|
+
const parentByChild: Record<string, string | null> = {}
|
|
157
|
+
for (const [id, element] of Object.entries(spec.elements)) {
|
|
158
|
+
for (const childId of element.children ?? []) {
|
|
159
|
+
parentByChild[childId] = id
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const nodes = Object.entries(spec.elements).map(([id, element]) => ({
|
|
164
|
+
id,
|
|
165
|
+
type: element.type,
|
|
166
|
+
parentId: parentByChild[id] ?? null,
|
|
167
|
+
children: element.children ?? [],
|
|
168
|
+
props: Object.keys(element.props ?? {}),
|
|
169
|
+
}))
|
|
170
|
+
|
|
171
|
+
return { root: spec.root, nodes }
|
|
172
|
+
}),
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const getAvailableUINodes = tool({
|
|
176
|
+
description: 'List nodes + props.',
|
|
177
|
+
inputSchema: z.object({}),
|
|
178
|
+
execute: toolWrapper('getAvailableUINodes', async () => ({
|
|
179
|
+
nodes: Object.entries(nodeHints).map(([name, props]) => ({
|
|
180
|
+
name,
|
|
181
|
+
props,
|
|
182
|
+
})),
|
|
183
|
+
})),
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const setUINodeProps = tool({
|
|
187
|
+
description: 'Set or add props for a node by id.',
|
|
188
|
+
inputSchema: z.object({
|
|
189
|
+
id: z.string().describe('Node id'),
|
|
190
|
+
props: z.string().describe('Props object for the node'),
|
|
191
|
+
replace: z.boolean().optional().describe('Replace existing props'),
|
|
192
|
+
}),
|
|
193
|
+
execute: toolWrapper(
|
|
194
|
+
'setUINodeProps',
|
|
195
|
+
async ({ id, props: propsArg, replace = false }) =>
|
|
196
|
+
withMutationLock(async () => {
|
|
197
|
+
const parsedProps = smartParse(propsArg)
|
|
198
|
+
const spec = readSpec()
|
|
199
|
+
if (!spec) return { success: false, message: 'No UI spec' }
|
|
200
|
+
if (!spec.elements[id]) {
|
|
201
|
+
return { success: false, message: 'Node not found' }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const elements = { ...spec.elements }
|
|
205
|
+
const current = elements[id]
|
|
206
|
+
const nextProps = replace
|
|
207
|
+
? parsedProps
|
|
208
|
+
: { ...current.props, ...parsedProps }
|
|
209
|
+
|
|
210
|
+
elements[id] = { ...current, props: nextProps }
|
|
211
|
+
|
|
212
|
+
commitSpec({ root: spec.root, elements } as TSpec)
|
|
213
|
+
return { success: true }
|
|
214
|
+
})
|
|
215
|
+
),
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
const deleteUINode = tool({
|
|
219
|
+
description:
|
|
220
|
+
'Delete a node by id. Cannot delete the root node (id "root"). Removes the node and its reference from the parent\'s children.',
|
|
221
|
+
inputSchema: z.object({
|
|
222
|
+
id: z.string().describe('Node id to delete'),
|
|
223
|
+
}),
|
|
224
|
+
execute: toolWrapper('deleteUINode', async ({ id }) =>
|
|
225
|
+
withMutationLock(async () => {
|
|
226
|
+
if (id === rootId) {
|
|
227
|
+
return { success: false, message: 'Cannot delete root node' }
|
|
228
|
+
}
|
|
229
|
+
const spec = readSpec()
|
|
230
|
+
if (!spec) return { success: false, message: 'No UI spec' }
|
|
231
|
+
const elements = { ...spec.elements }
|
|
232
|
+
delete elements[id]
|
|
233
|
+
for (const key of Object.keys(elements)) {
|
|
234
|
+
const element = elements[key]
|
|
235
|
+
if (element.children?.includes(id)) {
|
|
236
|
+
elements[key] = {
|
|
237
|
+
...element,
|
|
238
|
+
children: element.children.filter((childId) => childId !== id),
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
commitSpec({ root: spec.root, elements } as TSpec)
|
|
243
|
+
return { success: true }
|
|
244
|
+
})
|
|
245
|
+
),
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
const addUINode = tool({
|
|
249
|
+
description:
|
|
250
|
+
'Add a new node as a child of parentId. Creates element with type and props. Returns new node id. Props must be a valid JSON object.',
|
|
251
|
+
inputSchema: z.object({
|
|
252
|
+
parentId: z.string().optional().describe('Parent node id; omit for root'),
|
|
253
|
+
type: z
|
|
254
|
+
.string()
|
|
255
|
+
.describe('Component type (e.g. Container, Column, Text, Button)'),
|
|
256
|
+
props: z.string().optional().describe('Props object for the node'),
|
|
257
|
+
}),
|
|
258
|
+
execute: toolWrapper(
|
|
259
|
+
'addUINode',
|
|
260
|
+
async ({ parentId, type, props: propsArg }) =>
|
|
261
|
+
withMutationLock(async () => {
|
|
262
|
+
const parsedProps = smartParse(propsArg ?? '{}')
|
|
263
|
+
const spec = readSpec()
|
|
264
|
+
if (!spec) {
|
|
265
|
+
console.warn('[json-ui-lite-rn tool addNode] No UI spec, aborting')
|
|
266
|
+
|
|
267
|
+
return { success: false, message: 'No UI spec' }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
parentId ??= spec.root
|
|
271
|
+
|
|
272
|
+
if (!spec.elements[parentId]) {
|
|
273
|
+
console.warn(
|
|
274
|
+
'[json-ui-lite-rn tool addNode] Parent not found, aborting'
|
|
275
|
+
)
|
|
276
|
+
return { success: false, message: 'Parent not found' }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const newId = createId()
|
|
280
|
+
spec.elements[newId] = {
|
|
281
|
+
type,
|
|
282
|
+
props: parsedProps ?? {},
|
|
283
|
+
children: [],
|
|
284
|
+
}
|
|
285
|
+
let parent = spec.elements[parentId]
|
|
286
|
+
|
|
287
|
+
if (!nodeNamesThatSupportChildren.includes(parent.type)) {
|
|
288
|
+
parent = spec.elements[spec.root]
|
|
289
|
+
parentId = spec.root
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
spec.elements[parentId].children ??= []
|
|
293
|
+
spec.elements[parentId].children!.push(newId)
|
|
294
|
+
|
|
295
|
+
commitSpec({
|
|
296
|
+
root: spec.root,
|
|
297
|
+
elements: spec.elements,
|
|
298
|
+
} as TSpec)
|
|
299
|
+
return { success: true, id: newId }
|
|
300
|
+
})
|
|
301
|
+
),
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
const reorderUINodes = tool({
|
|
305
|
+
description:
|
|
306
|
+
'Move one node among siblings by offset (negative = up, positive = down).',
|
|
307
|
+
inputSchema: z.object({
|
|
308
|
+
nodeId: z.string().describe('Node id to move'),
|
|
309
|
+
offset: z
|
|
310
|
+
.number()
|
|
311
|
+
.describe(
|
|
312
|
+
'Relative index shift among siblings; negative moves earlier, positive moves later'
|
|
313
|
+
),
|
|
314
|
+
}),
|
|
315
|
+
execute: toolWrapper('reorderUINodes', async ({ nodeId, offset }) =>
|
|
316
|
+
withMutationLock(async () => {
|
|
317
|
+
const spec = readSpec()
|
|
318
|
+
if (!spec) return { success: false, message: 'No UI spec' }
|
|
319
|
+
|
|
320
|
+
const findParentId = (childId: string) => {
|
|
321
|
+
for (const [id, element] of Object.entries(spec.elements)) {
|
|
322
|
+
if (element.children?.includes(childId)) return id
|
|
323
|
+
}
|
|
324
|
+
return null
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const nodeParentId = findParentId(nodeId)
|
|
328
|
+
if (!nodeParentId) {
|
|
329
|
+
return {
|
|
330
|
+
success: false,
|
|
331
|
+
message: 'nodeId must exist and have a parent',
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const parentId = nodeParentId
|
|
336
|
+
const parent = spec.elements[parentId]
|
|
337
|
+
if (!parent) return { success: false, message: 'Parent not found' }
|
|
338
|
+
|
|
339
|
+
const currentChildren = [...(parent.children ?? [])]
|
|
340
|
+
const nodeIndex = currentChildren.indexOf(nodeId)
|
|
341
|
+
if (nodeIndex === -1) {
|
|
342
|
+
return {
|
|
343
|
+
success: false,
|
|
344
|
+
message: 'nodeId must be a direct child',
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (offset === 0) {
|
|
349
|
+
return {
|
|
350
|
+
success: true,
|
|
351
|
+
parentId,
|
|
352
|
+
nodeId,
|
|
353
|
+
fromIndex: nodeIndex,
|
|
354
|
+
toIndex: nodeIndex,
|
|
355
|
+
appliedOffset: 0,
|
|
356
|
+
childIds: currentChildren,
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const maxIndex = currentChildren.length - 1
|
|
361
|
+
const toIndex = Math.min(Math.max(nodeIndex + offset, 0), maxIndex)
|
|
362
|
+
currentChildren.splice(nodeIndex, 1)
|
|
363
|
+
currentChildren.splice(toIndex, 0, nodeId)
|
|
364
|
+
|
|
365
|
+
const elements = { ...spec.elements }
|
|
366
|
+
elements[parentId].children = currentChildren
|
|
367
|
+
commitSpec({ root: spec.root, elements } as TSpec)
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
success: true,
|
|
371
|
+
parentId,
|
|
372
|
+
nodeId,
|
|
373
|
+
fromIndex: nodeIndex,
|
|
374
|
+
toIndex,
|
|
375
|
+
appliedOffset: toIndex - nodeIndex,
|
|
376
|
+
childIds: currentChildren,
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
),
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
getUIRootNode,
|
|
384
|
+
getUINode,
|
|
385
|
+
getUILayout,
|
|
386
|
+
getAvailableUINodes,
|
|
387
|
+
setUINodeProps,
|
|
388
|
+
deleteUINode,
|
|
389
|
+
addUINode,
|
|
390
|
+
reorderUINodes,
|
|
391
|
+
}
|
|
392
|
+
}
|