@json-render/core 0.4.3 → 0.5.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 +134 -6
- package/dist/index.d.mts +404 -178
- package/dist/index.d.ts +404 -178
- package/dist/index.js +611 -80
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +601 -80
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
ActionBindingSchema: () => ActionBindingSchema,
|
|
23
24
|
ActionConfirmSchema: () => ActionConfirmSchema,
|
|
24
25
|
ActionOnErrorSchema: () => ActionOnErrorSchema,
|
|
25
26
|
ActionOnSuccessSchema: () => ActionOnSuccessSchema,
|
|
@@ -33,7 +34,11 @@ __export(index_exports, {
|
|
|
33
34
|
ValidationConfigSchema: () => ValidationConfigSchema,
|
|
34
35
|
VisibilityConditionSchema: () => VisibilityConditionSchema,
|
|
35
36
|
action: () => action,
|
|
37
|
+
actionBinding: () => actionBinding,
|
|
38
|
+
addByPath: () => addByPath,
|
|
36
39
|
applySpecStreamPatch: () => applySpecStreamPatch,
|
|
40
|
+
autoFixSpec: () => autoFixSpec,
|
|
41
|
+
buildUserPrompt: () => buildUserPrompt,
|
|
37
42
|
builtInValidationFunctions: () => builtInValidationFunctions,
|
|
38
43
|
check: () => check,
|
|
39
44
|
compileSpecStream: () => compileSpecStream,
|
|
@@ -45,16 +50,21 @@ __export(index_exports, {
|
|
|
45
50
|
evaluateVisibility: () => evaluateVisibility,
|
|
46
51
|
executeAction: () => executeAction,
|
|
47
52
|
findFormValue: () => findFormValue,
|
|
53
|
+
formatSpecIssues: () => formatSpecIssues,
|
|
48
54
|
generateCatalogPrompt: () => generateCatalogPrompt,
|
|
49
55
|
generateSystemPrompt: () => generateSystemPrompt,
|
|
50
56
|
getByPath: () => getByPath,
|
|
51
57
|
interpolateString: () => interpolateString,
|
|
52
58
|
parseSpecStreamLine: () => parseSpecStreamLine,
|
|
59
|
+
removeByPath: () => removeByPath,
|
|
53
60
|
resolveAction: () => resolveAction,
|
|
54
61
|
resolveDynamicValue: () => resolveDynamicValue,
|
|
62
|
+
resolveElementProps: () => resolveElementProps,
|
|
63
|
+
resolvePropValue: () => resolvePropValue,
|
|
55
64
|
runValidation: () => runValidation,
|
|
56
65
|
runValidationCheck: () => runValidationCheck,
|
|
57
66
|
setByPath: () => setByPath,
|
|
67
|
+
validateSpec: () => validateSpec,
|
|
58
68
|
visibility: () => visibility
|
|
59
69
|
});
|
|
60
70
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -80,26 +90,36 @@ var DynamicBooleanSchema = import_zod.z.union([
|
|
|
80
90
|
import_zod.z.boolean(),
|
|
81
91
|
import_zod.z.object({ path: import_zod.z.string() })
|
|
82
92
|
]);
|
|
83
|
-
function resolveDynamicValue(value,
|
|
93
|
+
function resolveDynamicValue(value, stateModel) {
|
|
84
94
|
if (value === null || value === void 0) {
|
|
85
95
|
return void 0;
|
|
86
96
|
}
|
|
87
97
|
if (typeof value === "object" && "path" in value) {
|
|
88
|
-
return getByPath(
|
|
98
|
+
return getByPath(stateModel, value.path);
|
|
89
99
|
}
|
|
90
100
|
return value;
|
|
91
101
|
}
|
|
102
|
+
function unescapeJsonPointer(token) {
|
|
103
|
+
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
104
|
+
}
|
|
105
|
+
function parseJsonPointer(path) {
|
|
106
|
+
const raw = path.startsWith("/") ? path.slice(1).split("/") : path.split("/");
|
|
107
|
+
return raw.map(unescapeJsonPointer);
|
|
108
|
+
}
|
|
92
109
|
function getByPath(obj, path) {
|
|
93
110
|
if (!path || path === "/") {
|
|
94
111
|
return obj;
|
|
95
112
|
}
|
|
96
|
-
const segments =
|
|
113
|
+
const segments = parseJsonPointer(path);
|
|
97
114
|
let current = obj;
|
|
98
115
|
for (const segment of segments) {
|
|
99
116
|
if (current === null || current === void 0) {
|
|
100
117
|
return void 0;
|
|
101
118
|
}
|
|
102
|
-
if (
|
|
119
|
+
if (Array.isArray(current)) {
|
|
120
|
+
const index = parseInt(segment, 10);
|
|
121
|
+
current = current[index];
|
|
122
|
+
} else if (typeof current === "object") {
|
|
103
123
|
current = current[segment];
|
|
104
124
|
} else {
|
|
105
125
|
return void 0;
|
|
@@ -111,13 +131,13 @@ function isNumericIndex(str) {
|
|
|
111
131
|
return /^\d+$/.test(str);
|
|
112
132
|
}
|
|
113
133
|
function setByPath(obj, path, value) {
|
|
114
|
-
const segments =
|
|
134
|
+
const segments = parseJsonPointer(path);
|
|
115
135
|
if (segments.length === 0) return;
|
|
116
136
|
let current = obj;
|
|
117
137
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
118
138
|
const segment = segments[i];
|
|
119
139
|
const nextSegment = segments[i + 1];
|
|
120
|
-
const nextIsNumeric = nextSegment !== void 0 && isNumericIndex(nextSegment);
|
|
140
|
+
const nextIsNumeric = nextSegment !== void 0 && (isNumericIndex(nextSegment) || nextSegment === "-");
|
|
121
141
|
if (Array.isArray(current)) {
|
|
122
142
|
const index = parseInt(segment, 10);
|
|
123
143
|
if (current[index] === void 0 || typeof current[index] !== "object") {
|
|
@@ -133,12 +153,95 @@ function setByPath(obj, path, value) {
|
|
|
133
153
|
}
|
|
134
154
|
const lastSegment = segments[segments.length - 1];
|
|
135
155
|
if (Array.isArray(current)) {
|
|
136
|
-
|
|
137
|
-
|
|
156
|
+
if (lastSegment === "-") {
|
|
157
|
+
current.push(value);
|
|
158
|
+
} else {
|
|
159
|
+
const index = parseInt(lastSegment, 10);
|
|
160
|
+
current[index] = value;
|
|
161
|
+
}
|
|
138
162
|
} else {
|
|
139
163
|
current[lastSegment] = value;
|
|
140
164
|
}
|
|
141
165
|
}
|
|
166
|
+
function addByPath(obj, path, value) {
|
|
167
|
+
const segments = parseJsonPointer(path);
|
|
168
|
+
if (segments.length === 0) return;
|
|
169
|
+
let current = obj;
|
|
170
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
171
|
+
const segment = segments[i];
|
|
172
|
+
const nextSegment = segments[i + 1];
|
|
173
|
+
const nextIsNumeric = nextSegment !== void 0 && (isNumericIndex(nextSegment) || nextSegment === "-");
|
|
174
|
+
if (Array.isArray(current)) {
|
|
175
|
+
const index = parseInt(segment, 10);
|
|
176
|
+
if (current[index] === void 0 || typeof current[index] !== "object") {
|
|
177
|
+
current[index] = nextIsNumeric ? [] : {};
|
|
178
|
+
}
|
|
179
|
+
current = current[index];
|
|
180
|
+
} else {
|
|
181
|
+
if (!(segment in current) || typeof current[segment] !== "object") {
|
|
182
|
+
current[segment] = nextIsNumeric ? [] : {};
|
|
183
|
+
}
|
|
184
|
+
current = current[segment];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const lastSegment = segments[segments.length - 1];
|
|
188
|
+
if (Array.isArray(current)) {
|
|
189
|
+
if (lastSegment === "-") {
|
|
190
|
+
current.push(value);
|
|
191
|
+
} else {
|
|
192
|
+
const index = parseInt(lastSegment, 10);
|
|
193
|
+
current.splice(index, 0, value);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
current[lastSegment] = value;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function removeByPath(obj, path) {
|
|
200
|
+
const segments = parseJsonPointer(path);
|
|
201
|
+
if (segments.length === 0) return;
|
|
202
|
+
let current = obj;
|
|
203
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
204
|
+
const segment = segments[i];
|
|
205
|
+
if (Array.isArray(current)) {
|
|
206
|
+
const index = parseInt(segment, 10);
|
|
207
|
+
if (current[index] === void 0 || typeof current[index] !== "object") {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
current = current[index];
|
|
211
|
+
} else {
|
|
212
|
+
if (!(segment in current) || typeof current[segment] !== "object") {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
current = current[segment];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const lastSegment = segments[segments.length - 1];
|
|
219
|
+
if (Array.isArray(current)) {
|
|
220
|
+
const index = parseInt(lastSegment, 10);
|
|
221
|
+
if (index >= 0 && index < current.length) {
|
|
222
|
+
current.splice(index, 1);
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
delete current[lastSegment];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function deepEqual(a, b) {
|
|
229
|
+
if (a === b) return true;
|
|
230
|
+
if (a === null || b === null) return false;
|
|
231
|
+
if (typeof a !== typeof b) return false;
|
|
232
|
+
if (typeof a !== "object") return false;
|
|
233
|
+
if (Array.isArray(a)) {
|
|
234
|
+
if (!Array.isArray(b)) return false;
|
|
235
|
+
if (a.length !== b.length) return false;
|
|
236
|
+
return a.every((item, i) => deepEqual(item, b[i]));
|
|
237
|
+
}
|
|
238
|
+
const aObj = a;
|
|
239
|
+
const bObj = b;
|
|
240
|
+
const aKeys = Object.keys(aObj);
|
|
241
|
+
const bKeys = Object.keys(bObj);
|
|
242
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
243
|
+
return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));
|
|
244
|
+
}
|
|
142
245
|
function findFormValue(fieldName, params, data) {
|
|
143
246
|
if (params?.[fieldName] !== void 0) {
|
|
144
247
|
const val = params[fieldName];
|
|
@@ -187,10 +290,38 @@ function parseSpecStreamLine(line) {
|
|
|
187
290
|
}
|
|
188
291
|
}
|
|
189
292
|
function applySpecStreamPatch(obj, patch) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
293
|
+
switch (patch.op) {
|
|
294
|
+
case "add":
|
|
295
|
+
addByPath(obj, patch.path, patch.value);
|
|
296
|
+
break;
|
|
297
|
+
case "replace":
|
|
298
|
+
setByPath(obj, patch.path, patch.value);
|
|
299
|
+
break;
|
|
300
|
+
case "remove":
|
|
301
|
+
removeByPath(obj, patch.path);
|
|
302
|
+
break;
|
|
303
|
+
case "move": {
|
|
304
|
+
if (!patch.from) break;
|
|
305
|
+
const moveValue = getByPath(obj, patch.from);
|
|
306
|
+
removeByPath(obj, patch.from);
|
|
307
|
+
addByPath(obj, patch.path, moveValue);
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
case "copy": {
|
|
311
|
+
if (!patch.from) break;
|
|
312
|
+
const copyValue = getByPath(obj, patch.from);
|
|
313
|
+
addByPath(obj, patch.path, copyValue);
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
case "test": {
|
|
317
|
+
const actual = getByPath(obj, patch.path);
|
|
318
|
+
if (!deepEqual(actual, patch.value)) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
`Test operation failed: value at "${patch.path}" does not match`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
194
325
|
}
|
|
195
326
|
return obj;
|
|
196
327
|
}
|
|
@@ -292,7 +423,7 @@ var VisibilityConditionSchema = import_zod2.z.union([
|
|
|
292
423
|
LogicExpressionSchema
|
|
293
424
|
]);
|
|
294
425
|
function evaluateLogicExpression(expr, ctx) {
|
|
295
|
-
const {
|
|
426
|
+
const { stateModel } = ctx;
|
|
296
427
|
if ("and" in expr) {
|
|
297
428
|
return expr.and.every((subExpr) => evaluateLogicExpression(subExpr, ctx));
|
|
298
429
|
}
|
|
@@ -303,30 +434,30 @@ function evaluateLogicExpression(expr, ctx) {
|
|
|
303
434
|
return !evaluateLogicExpression(expr.not, ctx);
|
|
304
435
|
}
|
|
305
436
|
if ("path" in expr) {
|
|
306
|
-
const value = resolveDynamicValue({ path: expr.path },
|
|
437
|
+
const value = resolveDynamicValue({ path: expr.path }, stateModel);
|
|
307
438
|
return Boolean(value);
|
|
308
439
|
}
|
|
309
440
|
if ("eq" in expr) {
|
|
310
441
|
const [left, right] = expr.eq;
|
|
311
|
-
const leftValue = resolveDynamicValue(left,
|
|
312
|
-
const rightValue = resolveDynamicValue(right,
|
|
442
|
+
const leftValue = resolveDynamicValue(left, stateModel);
|
|
443
|
+
const rightValue = resolveDynamicValue(right, stateModel);
|
|
313
444
|
return leftValue === rightValue;
|
|
314
445
|
}
|
|
315
446
|
if ("neq" in expr) {
|
|
316
447
|
const [left, right] = expr.neq;
|
|
317
|
-
const leftValue = resolveDynamicValue(left,
|
|
318
|
-
const rightValue = resolveDynamicValue(right,
|
|
448
|
+
const leftValue = resolveDynamicValue(left, stateModel);
|
|
449
|
+
const rightValue = resolveDynamicValue(right, stateModel);
|
|
319
450
|
return leftValue !== rightValue;
|
|
320
451
|
}
|
|
321
452
|
if ("gt" in expr) {
|
|
322
453
|
const [left, right] = expr.gt;
|
|
323
454
|
const leftValue = resolveDynamicValue(
|
|
324
455
|
left,
|
|
325
|
-
|
|
456
|
+
stateModel
|
|
326
457
|
);
|
|
327
458
|
const rightValue = resolveDynamicValue(
|
|
328
459
|
right,
|
|
329
|
-
|
|
460
|
+
stateModel
|
|
330
461
|
);
|
|
331
462
|
if (typeof leftValue === "number" && typeof rightValue === "number") {
|
|
332
463
|
return leftValue > rightValue;
|
|
@@ -337,11 +468,11 @@ function evaluateLogicExpression(expr, ctx) {
|
|
|
337
468
|
const [left, right] = expr.gte;
|
|
338
469
|
const leftValue = resolveDynamicValue(
|
|
339
470
|
left,
|
|
340
|
-
|
|
471
|
+
stateModel
|
|
341
472
|
);
|
|
342
473
|
const rightValue = resolveDynamicValue(
|
|
343
474
|
right,
|
|
344
|
-
|
|
475
|
+
stateModel
|
|
345
476
|
);
|
|
346
477
|
if (typeof leftValue === "number" && typeof rightValue === "number") {
|
|
347
478
|
return leftValue >= rightValue;
|
|
@@ -352,11 +483,11 @@ function evaluateLogicExpression(expr, ctx) {
|
|
|
352
483
|
const [left, right] = expr.lt;
|
|
353
484
|
const leftValue = resolveDynamicValue(
|
|
354
485
|
left,
|
|
355
|
-
|
|
486
|
+
stateModel
|
|
356
487
|
);
|
|
357
488
|
const rightValue = resolveDynamicValue(
|
|
358
489
|
right,
|
|
359
|
-
|
|
490
|
+
stateModel
|
|
360
491
|
);
|
|
361
492
|
if (typeof leftValue === "number" && typeof rightValue === "number") {
|
|
362
493
|
return leftValue < rightValue;
|
|
@@ -367,11 +498,11 @@ function evaluateLogicExpression(expr, ctx) {
|
|
|
367
498
|
const [left, right] = expr.lte;
|
|
368
499
|
const leftValue = resolveDynamicValue(
|
|
369
500
|
left,
|
|
370
|
-
|
|
501
|
+
stateModel
|
|
371
502
|
);
|
|
372
503
|
const rightValue = resolveDynamicValue(
|
|
373
504
|
right,
|
|
374
|
-
|
|
505
|
+
stateModel
|
|
375
506
|
);
|
|
376
507
|
if (typeof leftValue === "number" && typeof rightValue === "number") {
|
|
377
508
|
return leftValue <= rightValue;
|
|
@@ -388,7 +519,7 @@ function evaluateVisibility(condition, ctx) {
|
|
|
388
519
|
return condition;
|
|
389
520
|
}
|
|
390
521
|
if ("path" in condition && !("and" in condition) && !("or" in condition)) {
|
|
391
|
-
const value = resolveDynamicValue({ path: condition.path }, ctx.
|
|
522
|
+
const value = resolveDynamicValue({ path: condition.path }, ctx.stateModel);
|
|
392
523
|
return Boolean(value);
|
|
393
524
|
}
|
|
394
525
|
if ("auth" in condition) {
|
|
@@ -442,6 +573,44 @@ var visibility = {
|
|
|
442
573
|
lte: (left, right) => ({ lte: [left, right] })
|
|
443
574
|
};
|
|
444
575
|
|
|
576
|
+
// src/props.ts
|
|
577
|
+
function isPathExpression(value) {
|
|
578
|
+
return typeof value === "object" && value !== null && "$path" in value && typeof value.$path === "string";
|
|
579
|
+
}
|
|
580
|
+
function isCondExpression(value) {
|
|
581
|
+
return typeof value === "object" && value !== null && "$cond" in value && "$then" in value && "$else" in value;
|
|
582
|
+
}
|
|
583
|
+
function resolvePropValue(value, ctx) {
|
|
584
|
+
if (value === null || value === void 0) {
|
|
585
|
+
return value;
|
|
586
|
+
}
|
|
587
|
+
if (isPathExpression(value)) {
|
|
588
|
+
return getByPath(ctx.stateModel, value.$path);
|
|
589
|
+
}
|
|
590
|
+
if (isCondExpression(value)) {
|
|
591
|
+
const result = evaluateVisibility(value.$cond, ctx);
|
|
592
|
+
return resolvePropValue(result ? value.$then : value.$else, ctx);
|
|
593
|
+
}
|
|
594
|
+
if (Array.isArray(value)) {
|
|
595
|
+
return value.map((item) => resolvePropValue(item, ctx));
|
|
596
|
+
}
|
|
597
|
+
if (typeof value === "object") {
|
|
598
|
+
const resolved = {};
|
|
599
|
+
for (const [key, val] of Object.entries(value)) {
|
|
600
|
+
resolved[key] = resolvePropValue(val, ctx);
|
|
601
|
+
}
|
|
602
|
+
return resolved;
|
|
603
|
+
}
|
|
604
|
+
return value;
|
|
605
|
+
}
|
|
606
|
+
function resolveElementProps(props, ctx) {
|
|
607
|
+
const resolved = {};
|
|
608
|
+
for (const [key, value] of Object.entries(props)) {
|
|
609
|
+
resolved[key] = resolvePropValue(value, ctx);
|
|
610
|
+
}
|
|
611
|
+
return resolved;
|
|
612
|
+
}
|
|
613
|
+
|
|
445
614
|
// src/actions.ts
|
|
446
615
|
var import_zod3 = require("zod");
|
|
447
616
|
var ActionConfirmSchema = import_zod3.z.object({
|
|
@@ -460,44 +629,45 @@ var ActionOnErrorSchema = import_zod3.z.union([
|
|
|
460
629
|
import_zod3.z.object({ set: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown()) }),
|
|
461
630
|
import_zod3.z.object({ action: import_zod3.z.string() })
|
|
462
631
|
]);
|
|
463
|
-
var
|
|
464
|
-
|
|
632
|
+
var ActionBindingSchema = import_zod3.z.object({
|
|
633
|
+
action: import_zod3.z.string(),
|
|
465
634
|
params: import_zod3.z.record(import_zod3.z.string(), DynamicValueSchema).optional(),
|
|
466
635
|
confirm: ActionConfirmSchema.optional(),
|
|
467
636
|
onSuccess: ActionOnSuccessSchema.optional(),
|
|
468
637
|
onError: ActionOnErrorSchema.optional()
|
|
469
638
|
});
|
|
470
|
-
|
|
639
|
+
var ActionSchema = ActionBindingSchema;
|
|
640
|
+
function resolveAction(binding, stateModel) {
|
|
471
641
|
const resolvedParams = {};
|
|
472
|
-
if (
|
|
473
|
-
for (const [key, value] of Object.entries(
|
|
474
|
-
resolvedParams[key] = resolveDynamicValue(value,
|
|
642
|
+
if (binding.params) {
|
|
643
|
+
for (const [key, value] of Object.entries(binding.params)) {
|
|
644
|
+
resolvedParams[key] = resolveDynamicValue(value, stateModel);
|
|
475
645
|
}
|
|
476
646
|
}
|
|
477
|
-
let confirm =
|
|
647
|
+
let confirm = binding.confirm;
|
|
478
648
|
if (confirm) {
|
|
479
649
|
confirm = {
|
|
480
650
|
...confirm,
|
|
481
|
-
message: interpolateString(confirm.message,
|
|
482
|
-
title: interpolateString(confirm.title,
|
|
651
|
+
message: interpolateString(confirm.message, stateModel),
|
|
652
|
+
title: interpolateString(confirm.title, stateModel)
|
|
483
653
|
};
|
|
484
654
|
}
|
|
485
655
|
return {
|
|
486
|
-
|
|
656
|
+
action: binding.action,
|
|
487
657
|
params: resolvedParams,
|
|
488
658
|
confirm,
|
|
489
|
-
onSuccess:
|
|
490
|
-
onError:
|
|
659
|
+
onSuccess: binding.onSuccess,
|
|
660
|
+
onError: binding.onError
|
|
491
661
|
};
|
|
492
662
|
}
|
|
493
|
-
function interpolateString(template,
|
|
663
|
+
function interpolateString(template, stateModel) {
|
|
494
664
|
return template.replace(/\$\{([^}]+)\}/g, (_, path) => {
|
|
495
|
-
const value = resolveDynamicValue({ path },
|
|
665
|
+
const value = resolveDynamicValue({ path }, stateModel);
|
|
496
666
|
return String(value ?? "");
|
|
497
667
|
});
|
|
498
668
|
}
|
|
499
669
|
async function executeAction(ctx) {
|
|
500
|
-
const { action: action2, handler,
|
|
670
|
+
const { action: action2, handler, setState, navigate, executeAction: executeAction2 } = ctx;
|
|
501
671
|
try {
|
|
502
672
|
await handler(action2.params);
|
|
503
673
|
if (action2.onSuccess) {
|
|
@@ -505,7 +675,7 @@ async function executeAction(ctx) {
|
|
|
505
675
|
navigate(action2.onSuccess.navigate);
|
|
506
676
|
} else if ("set" in action2.onSuccess) {
|
|
507
677
|
for (const [path, value] of Object.entries(action2.onSuccess.set)) {
|
|
508
|
-
|
|
678
|
+
setState(path, value);
|
|
509
679
|
}
|
|
510
680
|
} else if ("action" in action2.onSuccess && executeAction2) {
|
|
511
681
|
await executeAction2(action2.onSuccess.action);
|
|
@@ -516,7 +686,7 @@ async function executeAction(ctx) {
|
|
|
516
686
|
if ("set" in action2.onError) {
|
|
517
687
|
for (const [path, value] of Object.entries(action2.onError.set)) {
|
|
518
688
|
const resolvedValue = typeof value === "string" && value === "$error.message" ? error.message : value;
|
|
519
|
-
|
|
689
|
+
setState(path, resolvedValue);
|
|
520
690
|
}
|
|
521
691
|
} else if ("action" in action2.onError && executeAction2) {
|
|
522
692
|
await executeAction2(action2.onError.action);
|
|
@@ -526,25 +696,26 @@ async function executeAction(ctx) {
|
|
|
526
696
|
}
|
|
527
697
|
}
|
|
528
698
|
}
|
|
529
|
-
var
|
|
530
|
-
/** Create a simple action */
|
|
531
|
-
simple: (
|
|
532
|
-
|
|
699
|
+
var actionBinding = {
|
|
700
|
+
/** Create a simple action binding */
|
|
701
|
+
simple: (actionName, params) => ({
|
|
702
|
+
action: actionName,
|
|
533
703
|
params
|
|
534
704
|
}),
|
|
535
|
-
/** Create an action with confirmation */
|
|
536
|
-
withConfirm: (
|
|
537
|
-
|
|
705
|
+
/** Create an action binding with confirmation */
|
|
706
|
+
withConfirm: (actionName, confirm, params) => ({
|
|
707
|
+
action: actionName,
|
|
538
708
|
params,
|
|
539
709
|
confirm
|
|
540
710
|
}),
|
|
541
|
-
/** Create an action with success handler */
|
|
542
|
-
withSuccess: (
|
|
543
|
-
|
|
711
|
+
/** Create an action binding with success handler */
|
|
712
|
+
withSuccess: (actionName, onSuccess, params) => ({
|
|
713
|
+
action: actionName,
|
|
544
714
|
params,
|
|
545
715
|
onSuccess
|
|
546
716
|
})
|
|
547
717
|
};
|
|
718
|
+
var action = actionBinding;
|
|
548
719
|
|
|
549
720
|
// src/validation.ts
|
|
550
721
|
var import_zod4 = require("zod");
|
|
@@ -653,11 +824,11 @@ var builtInValidationFunctions = {
|
|
|
653
824
|
}
|
|
654
825
|
};
|
|
655
826
|
function runValidationCheck(check2, ctx) {
|
|
656
|
-
const { value,
|
|
827
|
+
const { value, stateModel, customFunctions } = ctx;
|
|
657
828
|
const resolvedArgs = {};
|
|
658
829
|
if (check2.args) {
|
|
659
830
|
for (const [key, argValue] of Object.entries(check2.args)) {
|
|
660
|
-
resolvedArgs[key] = resolveDynamicValue(argValue,
|
|
831
|
+
resolvedArgs[key] = resolveDynamicValue(argValue, stateModel);
|
|
661
832
|
}
|
|
662
833
|
}
|
|
663
834
|
const fn = builtInValidationFunctions[check2.fn] ?? customFunctions?.[check2.fn];
|
|
@@ -682,7 +853,7 @@ function runValidation(config, ctx) {
|
|
|
682
853
|
const errors = [];
|
|
683
854
|
if (config.enabled) {
|
|
684
855
|
const enabled = evaluateLogicExpression(config.enabled, {
|
|
685
|
-
|
|
856
|
+
stateModel: ctx.stateModel,
|
|
686
857
|
authState: ctx.authState
|
|
687
858
|
});
|
|
688
859
|
if (!enabled) {
|
|
@@ -749,6 +920,155 @@ var check = {
|
|
|
749
920
|
})
|
|
750
921
|
};
|
|
751
922
|
|
|
923
|
+
// src/spec-validator.ts
|
|
924
|
+
function validateSpec(spec, options = {}) {
|
|
925
|
+
const { checkOrphans = false } = options;
|
|
926
|
+
const issues = [];
|
|
927
|
+
if (!spec.root) {
|
|
928
|
+
issues.push({
|
|
929
|
+
severity: "error",
|
|
930
|
+
message: "Spec has no root element defined.",
|
|
931
|
+
code: "missing_root"
|
|
932
|
+
});
|
|
933
|
+
return { valid: false, issues };
|
|
934
|
+
}
|
|
935
|
+
if (!spec.elements[spec.root]) {
|
|
936
|
+
issues.push({
|
|
937
|
+
severity: "error",
|
|
938
|
+
message: `Root element "${spec.root}" not found in elements map.`,
|
|
939
|
+
code: "root_not_found"
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
if (Object.keys(spec.elements).length === 0) {
|
|
943
|
+
issues.push({
|
|
944
|
+
severity: "error",
|
|
945
|
+
message: "Spec has no elements.",
|
|
946
|
+
code: "empty_spec"
|
|
947
|
+
});
|
|
948
|
+
return { valid: false, issues };
|
|
949
|
+
}
|
|
950
|
+
for (const [key, element] of Object.entries(spec.elements)) {
|
|
951
|
+
if (element.children) {
|
|
952
|
+
for (const childKey of element.children) {
|
|
953
|
+
if (!spec.elements[childKey]) {
|
|
954
|
+
issues.push({
|
|
955
|
+
severity: "error",
|
|
956
|
+
message: `Element "${key}" references child "${childKey}" which does not exist in the elements map.`,
|
|
957
|
+
elementKey: key,
|
|
958
|
+
code: "missing_child"
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
const props = element.props;
|
|
964
|
+
if (props && "visible" in props && props.visible !== void 0) {
|
|
965
|
+
issues.push({
|
|
966
|
+
severity: "error",
|
|
967
|
+
message: `Element "${key}" has "visible" inside "props". It should be a top-level field on the element (sibling of type/props/children).`,
|
|
968
|
+
elementKey: key,
|
|
969
|
+
code: "visible_in_props"
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
if (props && "on" in props && props.on !== void 0) {
|
|
973
|
+
issues.push({
|
|
974
|
+
severity: "error",
|
|
975
|
+
message: `Element "${key}" has "on" inside "props". It should be a top-level field on the element (sibling of type/props/children).`,
|
|
976
|
+
elementKey: key,
|
|
977
|
+
code: "on_in_props"
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
if (props && "repeat" in props && props.repeat !== void 0) {
|
|
981
|
+
issues.push({
|
|
982
|
+
severity: "error",
|
|
983
|
+
message: `Element "${key}" has "repeat" inside "props". It should be a top-level field on the element (sibling of type/props/children).`,
|
|
984
|
+
elementKey: key,
|
|
985
|
+
code: "repeat_in_props"
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
if (checkOrphans) {
|
|
990
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
991
|
+
const walk = (key) => {
|
|
992
|
+
if (reachable.has(key)) return;
|
|
993
|
+
reachable.add(key);
|
|
994
|
+
const el = spec.elements[key];
|
|
995
|
+
if (el?.children) {
|
|
996
|
+
for (const childKey of el.children) {
|
|
997
|
+
if (spec.elements[childKey]) {
|
|
998
|
+
walk(childKey);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
if (spec.elements[spec.root]) {
|
|
1004
|
+
walk(spec.root);
|
|
1005
|
+
}
|
|
1006
|
+
for (const key of Object.keys(spec.elements)) {
|
|
1007
|
+
if (!reachable.has(key)) {
|
|
1008
|
+
issues.push({
|
|
1009
|
+
severity: "warning",
|
|
1010
|
+
message: `Element "${key}" is not reachable from root "${spec.root}".`,
|
|
1011
|
+
elementKey: key,
|
|
1012
|
+
code: "orphaned_element"
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const hasErrors = issues.some((i) => i.severity === "error");
|
|
1018
|
+
return { valid: !hasErrors, issues };
|
|
1019
|
+
}
|
|
1020
|
+
function autoFixSpec(spec) {
|
|
1021
|
+
const fixes = [];
|
|
1022
|
+
const fixedElements = {};
|
|
1023
|
+
for (const [key, element] of Object.entries(spec.elements)) {
|
|
1024
|
+
const props = element.props;
|
|
1025
|
+
let fixed = element;
|
|
1026
|
+
if (props && "visible" in props && props.visible !== void 0) {
|
|
1027
|
+
const { visible, ...restProps } = fixed.props;
|
|
1028
|
+
fixed = {
|
|
1029
|
+
...fixed,
|
|
1030
|
+
props: restProps,
|
|
1031
|
+
visible
|
|
1032
|
+
};
|
|
1033
|
+
fixes.push(`Moved "visible" from props to element level on "${key}".`);
|
|
1034
|
+
}
|
|
1035
|
+
let currentProps = fixed.props;
|
|
1036
|
+
if (currentProps && "on" in currentProps && currentProps.on !== void 0) {
|
|
1037
|
+
const { on, ...restProps } = currentProps;
|
|
1038
|
+
fixed = {
|
|
1039
|
+
...fixed,
|
|
1040
|
+
props: restProps,
|
|
1041
|
+
on
|
|
1042
|
+
};
|
|
1043
|
+
fixes.push(`Moved "on" from props to element level on "${key}".`);
|
|
1044
|
+
}
|
|
1045
|
+
currentProps = fixed.props;
|
|
1046
|
+
if (currentProps && "repeat" in currentProps && currentProps.repeat !== void 0) {
|
|
1047
|
+
const { repeat, ...restProps } = currentProps;
|
|
1048
|
+
fixed = {
|
|
1049
|
+
...fixed,
|
|
1050
|
+
props: restProps,
|
|
1051
|
+
repeat
|
|
1052
|
+
};
|
|
1053
|
+
fixes.push(`Moved "repeat" from props to element level on "${key}".`);
|
|
1054
|
+
}
|
|
1055
|
+
fixedElements[key] = fixed;
|
|
1056
|
+
}
|
|
1057
|
+
return {
|
|
1058
|
+
spec: { root: spec.root, elements: fixedElements },
|
|
1059
|
+
fixes
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
function formatSpecIssues(issues) {
|
|
1063
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
1064
|
+
if (errors.length === 0) return "";
|
|
1065
|
+
const lines = ["The generated UI spec has the following errors:"];
|
|
1066
|
+
for (const issue of errors) {
|
|
1067
|
+
lines.push(`- ${issue.message}`);
|
|
1068
|
+
}
|
|
1069
|
+
return lines.join("\n");
|
|
1070
|
+
}
|
|
1071
|
+
|
|
752
1072
|
// src/schema.ts
|
|
753
1073
|
var import_zod5 = require("zod");
|
|
754
1074
|
function createBuilder() {
|
|
@@ -773,6 +1093,7 @@ function defineSchema(builder, options) {
|
|
|
773
1093
|
return {
|
|
774
1094
|
definition,
|
|
775
1095
|
promptTemplate: options?.promptTemplate,
|
|
1096
|
+
defaultRules: options?.defaultRules,
|
|
776
1097
|
createCatalog(catalog) {
|
|
777
1098
|
return createCatalogFromSchema(this, catalog);
|
|
778
1099
|
}
|
|
@@ -928,15 +1249,95 @@ function generatePrompt(catalog, options) {
|
|
|
928
1249
|
"Output JSONL (one JSON object per line) with patches to build a UI tree."
|
|
929
1250
|
);
|
|
930
1251
|
lines.push(
|
|
931
|
-
"Each line is a JSON patch operation. Start with
|
|
1252
|
+
"Each line is a JSON patch operation. Start with /root, then stream /elements and /state patches interleaved so the UI fills in progressively as it streams."
|
|
932
1253
|
);
|
|
933
1254
|
lines.push("");
|
|
934
1255
|
lines.push("Example output (each line is a separate JSON object):");
|
|
935
1256
|
lines.push("");
|
|
936
|
-
lines.push(`{"op":"
|
|
937
|
-
{"op":"
|
|
938
|
-
{"op":"
|
|
939
|
-
{"op":"
|
|
1257
|
+
lines.push(`{"op":"add","path":"/root","value":"blog"}
|
|
1258
|
+
{"op":"add","path":"/elements/blog","value":{"type":"Stack","props":{"direction":"vertical","gap":"md"},"children":["heading","posts-grid"]}}
|
|
1259
|
+
{"op":"add","path":"/elements/heading","value":{"type":"Heading","props":{"text":"Blog","level":"h1"},"children":[]}}
|
|
1260
|
+
{"op":"add","path":"/elements/posts-grid","value":{"type":"Grid","props":{"columns":2,"gap":"md"},"repeat":{"path":"/posts","key":"id"},"children":["post-card"]}}
|
|
1261
|
+
{"op":"add","path":"/elements/post-card","value":{"type":"Card","props":{"title":{"$path":"$item/title"}},"children":["post-meta"]}}
|
|
1262
|
+
{"op":"add","path":"/elements/post-meta","value":{"type":"Text","props":{"text":{"$path":"$item/author"},"variant":"muted"},"children":[]}}
|
|
1263
|
+
{"op":"add","path":"/state/posts","value":[]}
|
|
1264
|
+
{"op":"add","path":"/state/posts/0","value":{"id":"1","title":"Getting Started","author":"Jane","date":"Jan 15"}}
|
|
1265
|
+
{"op":"add","path":"/state/posts/1","value":{"id":"2","title":"Advanced Tips","author":"Bob","date":"Feb 3"}}
|
|
1266
|
+
|
|
1267
|
+
Note: state patches appear right after the elements that use them, so the UI fills in as it streams.`);
|
|
1268
|
+
lines.push("");
|
|
1269
|
+
lines.push("INITIAL STATE:");
|
|
1270
|
+
lines.push(
|
|
1271
|
+
"Specs include a /state field to seed the state model. Components with statePath read from and write to this state, and $path expressions read from it."
|
|
1272
|
+
);
|
|
1273
|
+
lines.push(
|
|
1274
|
+
"CRITICAL: You MUST include state patches whenever your UI displays data via $path expressions, uses repeat to iterate over arrays, or uses statePath bindings. Without state, $path references resolve to nothing and repeat lists render zero items."
|
|
1275
|
+
);
|
|
1276
|
+
lines.push(
|
|
1277
|
+
"Output state patches right after the elements that reference them, so the UI fills in progressively as it streams."
|
|
1278
|
+
);
|
|
1279
|
+
lines.push(
|
|
1280
|
+
"Stream state progressively - output one patch per array item instead of one giant blob:"
|
|
1281
|
+
);
|
|
1282
|
+
lines.push(
|
|
1283
|
+
' For arrays: {"op":"add","path":"/state/posts/0","value":{"id":"1","title":"First Post",...}} then /state/posts/1, /state/posts/2, etc.'
|
|
1284
|
+
);
|
|
1285
|
+
lines.push(
|
|
1286
|
+
' For scalars: {"op":"add","path":"/state/newTodoText","value":""}'
|
|
1287
|
+
);
|
|
1288
|
+
lines.push(
|
|
1289
|
+
' Initialize the array first if needed: {"op":"add","path":"/state/posts","value":[]}'
|
|
1290
|
+
);
|
|
1291
|
+
lines.push(
|
|
1292
|
+
'When content comes from the state model, use { "$path": "/some/path" } dynamic props to display it instead of hardcoding the same value in both state and props. The state model is the single source of truth.'
|
|
1293
|
+
);
|
|
1294
|
+
lines.push(
|
|
1295
|
+
"Include realistic sample data in state. For blogs: 3-4 posts with titles, excerpts, authors, dates. For product lists: 3-5 items with names, prices, descriptions. Never leave arrays empty."
|
|
1296
|
+
);
|
|
1297
|
+
lines.push("");
|
|
1298
|
+
lines.push("DYNAMIC LISTS (repeat field):");
|
|
1299
|
+
lines.push(
|
|
1300
|
+
'Any element can have a top-level "repeat" field to render its children once per item in a state array: { "repeat": { "path": "/arrayPath", "key": "id" } }.'
|
|
1301
|
+
);
|
|
1302
|
+
lines.push(
|
|
1303
|
+
'The element itself renders once (as the container), and its children are expanded once per array item. "path" is the state array path. "key" is an optional field name on each item for stable React keys.'
|
|
1304
|
+
);
|
|
1305
|
+
lines.push(
|
|
1306
|
+
'Example: { "type": "Column", "props": { "gap": 8 }, "repeat": { "path": "/todos", "key": "id" }, "children": ["todo-item"] }'
|
|
1307
|
+
);
|
|
1308
|
+
lines.push(
|
|
1309
|
+
'Inside children of a repeated element, use "$item/field" for per-item paths: statePath:"$item/completed", { "$path": "$item/title" }. Use "$index" for the current array index.'
|
|
1310
|
+
);
|
|
1311
|
+
lines.push(
|
|
1312
|
+
"ALWAYS use the repeat field for lists backed by state arrays. NEVER hardcode individual elements for each array item."
|
|
1313
|
+
);
|
|
1314
|
+
lines.push(
|
|
1315
|
+
'IMPORTANT: "repeat" is a top-level field on the element (sibling of type/props/children), NOT inside props.'
|
|
1316
|
+
);
|
|
1317
|
+
lines.push("");
|
|
1318
|
+
lines.push("ARRAY STATE ACTIONS:");
|
|
1319
|
+
lines.push(
|
|
1320
|
+
'Use action "pushState" to append items to arrays. Params: { path: "/arrayPath", value: { ...item }, clearPath: "/inputPath" }.'
|
|
1321
|
+
);
|
|
1322
|
+
lines.push(
|
|
1323
|
+
'Values inside pushState can contain { "path": "/statePath" } references to read current state (e.g. the text from an input field).'
|
|
1324
|
+
);
|
|
1325
|
+
lines.push(
|
|
1326
|
+
'Use "$id" inside a pushState value to auto-generate a unique ID.'
|
|
1327
|
+
);
|
|
1328
|
+
lines.push(
|
|
1329
|
+
'Example: on: { "press": { "action": "pushState", "params": { "path": "/todos", "value": { "id": "$id", "title": { "path": "/newTodoText" }, "completed": false }, "clearPath": "/newTodoText" } } }'
|
|
1330
|
+
);
|
|
1331
|
+
lines.push(
|
|
1332
|
+
`Use action "removeState" to remove items from arrays by index. Params: { path: "/arrayPath", index: N }. Inside a repeated element's children, use "$index" for the current item index.`
|
|
1333
|
+
);
|
|
1334
|
+
lines.push(
|
|
1335
|
+
"For lists where users can add/remove items (todos, carts, etc.), use pushState and removeState instead of hardcoding with setState."
|
|
1336
|
+
);
|
|
1337
|
+
lines.push("");
|
|
1338
|
+
lines.push(
|
|
1339
|
+
'IMPORTANT: State paths use RFC 6901 JSON Pointer syntax (e.g. "/todos/0/title"). Do NOT use JavaScript-style dot notation (e.g. "/todos.length" is WRONG). To generate unique IDs for new items, use "$id" instead of trying to read array length.'
|
|
1340
|
+
);
|
|
940
1341
|
lines.push("");
|
|
941
1342
|
const components = catalog.data.components;
|
|
942
1343
|
if (components) {
|
|
@@ -946,8 +1347,9 @@ function generatePrompt(catalog, options) {
|
|
|
946
1347
|
const propsStr = def.props ? formatZodType(def.props) : "{}";
|
|
947
1348
|
const hasChildren = def.slots && def.slots.length > 0;
|
|
948
1349
|
const childrenStr = hasChildren ? " [accepts children]" : "";
|
|
1350
|
+
const eventsStr = def.events && def.events.length > 0 ? ` [events: ${def.events.join(", ")}]` : "";
|
|
949
1351
|
const descStr = def.description ? ` - ${def.description}` : "";
|
|
950
|
-
lines.push(`- ${name}: ${propsStr}${descStr}${childrenStr}`);
|
|
1352
|
+
lines.push(`- ${name}: ${propsStr}${descStr}${childrenStr}${eventsStr}`);
|
|
951
1353
|
}
|
|
952
1354
|
lines.push("");
|
|
953
1355
|
}
|
|
@@ -960,17 +1362,90 @@ function generatePrompt(catalog, options) {
|
|
|
960
1362
|
}
|
|
961
1363
|
lines.push("");
|
|
962
1364
|
}
|
|
1365
|
+
lines.push("EVENTS (the `on` field):");
|
|
1366
|
+
lines.push(
|
|
1367
|
+
"Elements can have an optional `on` field to bind events to actions. The `on` field is a top-level field on the element (sibling of type/props/children), NOT inside props."
|
|
1368
|
+
);
|
|
1369
|
+
lines.push(
|
|
1370
|
+
'Each key in `on` is an event name (from the component\'s supported events), and the value is an action binding: `{ "action": "<actionName>", "params": { ... } }`.'
|
|
1371
|
+
);
|
|
1372
|
+
lines.push("");
|
|
1373
|
+
lines.push("Example:");
|
|
1374
|
+
lines.push(
|
|
1375
|
+
' {"type":"Button","props":{"label":"Save"},"on":{"press":{"action":"setState","params":{"path":"/saved","value":true}}},"children":[]}'
|
|
1376
|
+
);
|
|
1377
|
+
lines.push("");
|
|
1378
|
+
lines.push(
|
|
1379
|
+
'Action params can use dynamic path references to read from state: { "path": "/statePath" }.'
|
|
1380
|
+
);
|
|
1381
|
+
lines.push(
|
|
1382
|
+
"IMPORTANT: Do NOT put action/actionParams inside props. Always use the `on` field for event bindings."
|
|
1383
|
+
);
|
|
1384
|
+
lines.push("");
|
|
1385
|
+
lines.push("VISIBILITY CONDITIONS:");
|
|
1386
|
+
lines.push(
|
|
1387
|
+
"Elements can have an optional `visible` field to conditionally show/hide based on data state. IMPORTANT: `visible` is a top-level field on the element object (sibling of type/props/children), NOT inside props."
|
|
1388
|
+
);
|
|
1389
|
+
lines.push(
|
|
1390
|
+
'Correct: {"type":"Column","props":{"gap":8},"visible":{"eq":[{"path":"/tab"},"home"]},"children":[...]}'
|
|
1391
|
+
);
|
|
1392
|
+
lines.push(
|
|
1393
|
+
'- `{ "eq": [{ "path": "/statePath" }, "value"] }` - visible when state at path equals value'
|
|
1394
|
+
);
|
|
1395
|
+
lines.push(
|
|
1396
|
+
'- `{ "neq": [{ "path": "/statePath" }, "value"] }` - visible when state at path does not equal value'
|
|
1397
|
+
);
|
|
1398
|
+
lines.push('- `{ "path": "/statePath" }` - visible when path is truthy');
|
|
1399
|
+
lines.push(
|
|
1400
|
+
'- `{ "and": [...] }`, `{ "or": [...] }`, `{ "not": {...} }` - combine conditions'
|
|
1401
|
+
);
|
|
1402
|
+
lines.push("- `true` / `false` - always visible/hidden");
|
|
1403
|
+
lines.push("");
|
|
1404
|
+
lines.push(
|
|
1405
|
+
"Use the Pressable component with on.press bound to setState to update state and drive visibility."
|
|
1406
|
+
);
|
|
1407
|
+
lines.push(
|
|
1408
|
+
'Example: A Pressable with on: { "press": { "action": "setState", "params": { "path": "/activeTab", "value": "home" } } } sets state, then a container with visible: { "eq": [{ "path": "/activeTab" }, "home"] } shows only when that tab is active.'
|
|
1409
|
+
);
|
|
1410
|
+
lines.push("");
|
|
1411
|
+
lines.push("DYNAMIC PROPS:");
|
|
1412
|
+
lines.push(
|
|
1413
|
+
"Any prop value can be a dynamic expression that resolves based on state. Two forms are supported:"
|
|
1414
|
+
);
|
|
1415
|
+
lines.push("");
|
|
1416
|
+
lines.push(
|
|
1417
|
+
'1. State binding: `{ "$path": "/statePath" }` - resolves to the value at that state path.'
|
|
1418
|
+
);
|
|
1419
|
+
lines.push(
|
|
1420
|
+
' Example: `"color": { "$path": "/theme/primary" }` reads the color from state.'
|
|
1421
|
+
);
|
|
1422
|
+
lines.push("");
|
|
1423
|
+
lines.push(
|
|
1424
|
+
'2. Conditional: `{ "$cond": <condition>, "$then": <value>, "$else": <value> }` - evaluates the condition (same syntax as visibility conditions) and picks the matching value.'
|
|
1425
|
+
);
|
|
1426
|
+
lines.push(
|
|
1427
|
+
' Example: `"color": { "$cond": { "eq": [{ "path": "/activeTab" }, "home"] }, "$then": "#007AFF", "$else": "#8E8E93" }`'
|
|
1428
|
+
);
|
|
1429
|
+
lines.push(
|
|
1430
|
+
' Example: `"name": { "$cond": { "eq": [{ "path": "/activeTab" }, "home"] }, "$then": "home", "$else": "home-outline" }`'
|
|
1431
|
+
);
|
|
1432
|
+
lines.push("");
|
|
1433
|
+
lines.push(
|
|
1434
|
+
"Use dynamic props instead of duplicating elements with opposing visible conditions when only prop values differ."
|
|
1435
|
+
);
|
|
1436
|
+
lines.push("");
|
|
963
1437
|
lines.push("RULES:");
|
|
964
1438
|
const baseRules = [
|
|
965
1439
|
"Output ONLY JSONL patches - one JSON object per line, no markdown, no code fences",
|
|
966
|
-
'First
|
|
967
|
-
'Then add each element: {"op":"
|
|
1440
|
+
'First set root: {"op":"add","path":"/root","value":"<root-key>"}',
|
|
1441
|
+
'Then add each element: {"op":"add","path":"/elements/<key>","value":{...}}',
|
|
1442
|
+
"Output /state patches right after the elements that use them, one per array item for progressive loading. REQUIRED whenever using $path, repeat, or statePath.",
|
|
968
1443
|
"ONLY use components listed above",
|
|
969
|
-
"Each element value needs:
|
|
970
|
-
"Use unique keys (e.g., 'header', 'metric-1', 'chart-revenue')"
|
|
971
|
-
"Root element's parentKey is empty string, children reference their parent's key"
|
|
1444
|
+
"Each element value needs: type, props, children (array of child keys)",
|
|
1445
|
+
"Use unique keys for the element map entries (e.g., 'header', 'metric-1', 'chart-revenue')"
|
|
972
1446
|
];
|
|
973
|
-
const
|
|
1447
|
+
const schemaRules = catalog.schema.defaultRules ?? [];
|
|
1448
|
+
const allRules = [...baseRules, ...schemaRules, ...customRules];
|
|
974
1449
|
allRules.forEach((rule, i) => {
|
|
975
1450
|
lines.push(`${i + 1}. ${rule}`);
|
|
976
1451
|
});
|
|
@@ -1108,6 +1583,56 @@ function defineCatalog(schema, catalog) {
|
|
|
1108
1583
|
return schema.createCatalog(catalog);
|
|
1109
1584
|
}
|
|
1110
1585
|
|
|
1586
|
+
// src/prompt.ts
|
|
1587
|
+
function isNonEmptySpec(spec) {
|
|
1588
|
+
if (!spec || typeof spec !== "object") return false;
|
|
1589
|
+
const s = spec;
|
|
1590
|
+
return typeof s.root === "string" && typeof s.elements === "object" && s.elements !== null && Object.keys(s.elements).length > 0;
|
|
1591
|
+
}
|
|
1592
|
+
var PATCH_INSTRUCTIONS = `IMPORTANT: The current UI is already loaded. Output ONLY the patches needed to make the requested change:
|
|
1593
|
+
- To add a new element: {"op":"add","path":"/elements/new-key","value":{...}}
|
|
1594
|
+
- To modify an existing element: {"op":"replace","path":"/elements/existing-key","value":{...}}
|
|
1595
|
+
- To remove an element: {"op":"remove","path":"/elements/old-key"}
|
|
1596
|
+
- To update the root: {"op":"replace","path":"/root","value":"new-root-key"}
|
|
1597
|
+
- To add children: update the parent element with new children array
|
|
1598
|
+
|
|
1599
|
+
DO NOT output patches for elements that don't need to change. Only output what's necessary for the requested modification.`;
|
|
1600
|
+
function buildUserPrompt(options) {
|
|
1601
|
+
const { prompt, currentSpec, state, maxPromptLength } = options;
|
|
1602
|
+
let userText = String(prompt || "");
|
|
1603
|
+
if (maxPromptLength !== void 0 && maxPromptLength > 0) {
|
|
1604
|
+
userText = userText.slice(0, maxPromptLength);
|
|
1605
|
+
}
|
|
1606
|
+
if (isNonEmptySpec(currentSpec)) {
|
|
1607
|
+
const parts2 = [];
|
|
1608
|
+
parts2.push(
|
|
1609
|
+
`CURRENT UI STATE (already loaded, DO NOT recreate existing elements):`
|
|
1610
|
+
);
|
|
1611
|
+
parts2.push(JSON.stringify(currentSpec, null, 2));
|
|
1612
|
+
parts2.push("");
|
|
1613
|
+
parts2.push(`USER REQUEST: ${userText}`);
|
|
1614
|
+
if (state && Object.keys(state).length > 0) {
|
|
1615
|
+
parts2.push("");
|
|
1616
|
+
parts2.push(`AVAILABLE STATE:
|
|
1617
|
+
${JSON.stringify(state, null, 2)}`);
|
|
1618
|
+
}
|
|
1619
|
+
parts2.push("");
|
|
1620
|
+
parts2.push(PATCH_INSTRUCTIONS);
|
|
1621
|
+
return parts2.join("\n");
|
|
1622
|
+
}
|
|
1623
|
+
const parts = [userText];
|
|
1624
|
+
if (state && Object.keys(state).length > 0) {
|
|
1625
|
+
parts.push(`
|
|
1626
|
+
AVAILABLE STATE:
|
|
1627
|
+
${JSON.stringify(state, null, 2)}`);
|
|
1628
|
+
}
|
|
1629
|
+
parts.push(
|
|
1630
|
+
`
|
|
1631
|
+
Remember: Output /root first, then interleave /elements and /state patches so the UI fills in progressively as it streams. Output each state patch right after the elements that use it, one per array item.`
|
|
1632
|
+
);
|
|
1633
|
+
return parts.join("\n");
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1111
1636
|
// src/catalog.ts
|
|
1112
1637
|
var import_zod6 = require("zod");
|
|
1113
1638
|
function createCatalog(config) {
|
|
@@ -1124,22 +1649,18 @@ function createCatalog(config) {
|
|
|
1124
1649
|
const componentSchemas = componentNames.map((componentName) => {
|
|
1125
1650
|
const def = components[componentName];
|
|
1126
1651
|
return import_zod6.z.object({
|
|
1127
|
-
key: import_zod6.z.string(),
|
|
1128
1652
|
type: import_zod6.z.literal(componentName),
|
|
1129
1653
|
props: def.props,
|
|
1130
1654
|
children: import_zod6.z.array(import_zod6.z.string()).optional(),
|
|
1131
|
-
parentKey: import_zod6.z.string().nullable().optional(),
|
|
1132
1655
|
visible: VisibilityConditionSchema.optional()
|
|
1133
1656
|
});
|
|
1134
1657
|
});
|
|
1135
1658
|
let elementSchema;
|
|
1136
1659
|
if (componentSchemas.length === 0) {
|
|
1137
1660
|
elementSchema = import_zod6.z.object({
|
|
1138
|
-
key: import_zod6.z.string(),
|
|
1139
1661
|
type: import_zod6.z.string(),
|
|
1140
1662
|
props: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.unknown()),
|
|
1141
1663
|
children: import_zod6.z.array(import_zod6.z.string()).optional(),
|
|
1142
|
-
parentKey: import_zod6.z.string().nullable().optional(),
|
|
1143
1664
|
visible: VisibilityConditionSchema.optional()
|
|
1144
1665
|
});
|
|
1145
1666
|
} else if (componentSchemas.length === 1) {
|
|
@@ -1221,7 +1742,7 @@ function generateCatalogPrompt(catalog) {
|
|
|
1221
1742
|
lines.push("");
|
|
1222
1743
|
lines.push("Components can have a `visible` property:");
|
|
1223
1744
|
lines.push("- `true` / `false` - Always visible/hidden");
|
|
1224
|
-
lines.push('- `{ "path": "/
|
|
1745
|
+
lines.push('- `{ "path": "/state/path" }` - Visible when path is truthy');
|
|
1225
1746
|
lines.push('- `{ "auth": "signedIn" }` - Visible when user is signed in');
|
|
1226
1747
|
lines.push('- `{ "and": [...] }` - All conditions must be true');
|
|
1227
1748
|
lines.push('- `{ "or": [...] }` - Any condition must be true');
|
|
@@ -1360,21 +1881,21 @@ function generateSystemPrompt(catalog, options = {}) {
|
|
|
1360
1881
|
}
|
|
1361
1882
|
lines.push("");
|
|
1362
1883
|
}
|
|
1363
|
-
lines.push("OUTPUT FORMAT (JSONL):");
|
|
1364
|
-
lines.push('{"op":"
|
|
1884
|
+
lines.push("OUTPUT FORMAT (JSONL, RFC 6902 JSON Patch):");
|
|
1885
|
+
lines.push('{"op":"add","path":"/root","value":"element-key"}');
|
|
1365
1886
|
lines.push(
|
|
1366
|
-
'{"op":"add","path":"/elements/key","value":{"
|
|
1887
|
+
'{"op":"add","path":"/elements/key","value":{"type":"...","props":{...},"children":[...]}}'
|
|
1367
1888
|
);
|
|
1368
1889
|
lines.push('{"op":"remove","path":"/elements/key"}');
|
|
1369
1890
|
lines.push("");
|
|
1370
1891
|
lines.push("RULES:");
|
|
1371
1892
|
const baseRules = [
|
|
1372
|
-
|
|
1373
|
-
|
|
1893
|
+
'First line sets /root to root element key: {"op":"add","path":"/root","value":"<key>"}',
|
|
1894
|
+
'Add elements with /elements/{key}: {"op":"add","path":"/elements/<key>","value":{...}}',
|
|
1374
1895
|
"Remove elements with op:remove - also update the parent's children array to exclude the removed key",
|
|
1375
1896
|
"Children array contains string keys, not objects",
|
|
1376
1897
|
"Parent first, then children",
|
|
1377
|
-
"Each element needs:
|
|
1898
|
+
"Each element needs: type, props",
|
|
1378
1899
|
"ONLY use props listed above - never invent new props"
|
|
1379
1900
|
];
|
|
1380
1901
|
const allRules = [...baseRules, ...customRules];
|
|
@@ -1392,6 +1913,7 @@ function generateSystemPrompt(catalog, options = {}) {
|
|
|
1392
1913
|
}
|
|
1393
1914
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1394
1915
|
0 && (module.exports = {
|
|
1916
|
+
ActionBindingSchema,
|
|
1395
1917
|
ActionConfirmSchema,
|
|
1396
1918
|
ActionOnErrorSchema,
|
|
1397
1919
|
ActionOnSuccessSchema,
|
|
@@ -1405,7 +1927,11 @@ function generateSystemPrompt(catalog, options = {}) {
|
|
|
1405
1927
|
ValidationConfigSchema,
|
|
1406
1928
|
VisibilityConditionSchema,
|
|
1407
1929
|
action,
|
|
1930
|
+
actionBinding,
|
|
1931
|
+
addByPath,
|
|
1408
1932
|
applySpecStreamPatch,
|
|
1933
|
+
autoFixSpec,
|
|
1934
|
+
buildUserPrompt,
|
|
1409
1935
|
builtInValidationFunctions,
|
|
1410
1936
|
check,
|
|
1411
1937
|
compileSpecStream,
|
|
@@ -1417,16 +1943,21 @@ function generateSystemPrompt(catalog, options = {}) {
|
|
|
1417
1943
|
evaluateVisibility,
|
|
1418
1944
|
executeAction,
|
|
1419
1945
|
findFormValue,
|
|
1946
|
+
formatSpecIssues,
|
|
1420
1947
|
generateCatalogPrompt,
|
|
1421
1948
|
generateSystemPrompt,
|
|
1422
1949
|
getByPath,
|
|
1423
1950
|
interpolateString,
|
|
1424
1951
|
parseSpecStreamLine,
|
|
1952
|
+
removeByPath,
|
|
1425
1953
|
resolveAction,
|
|
1426
1954
|
resolveDynamicValue,
|
|
1955
|
+
resolveElementProps,
|
|
1956
|
+
resolvePropValue,
|
|
1427
1957
|
runValidation,
|
|
1428
1958
|
runValidationCheck,
|
|
1429
1959
|
setByPath,
|
|
1960
|
+
validateSpec,
|
|
1430
1961
|
visibility
|
|
1431
1962
|
});
|
|
1432
1963
|
//# sourceMappingURL=index.js.map
|