@json-render/core 0.5.2 → 0.6.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 +91 -16
- package/dist/index.d.mts +424 -214
- package/dist/index.d.ts +424 -214
- package/dist/index.js +617 -516
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +608 -511
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -29,35 +29,39 @@ __export(index_exports, {
|
|
|
29
29
|
DynamicNumberSchema: () => DynamicNumberSchema,
|
|
30
30
|
DynamicStringSchema: () => DynamicStringSchema,
|
|
31
31
|
DynamicValueSchema: () => DynamicValueSchema,
|
|
32
|
-
|
|
32
|
+
SPEC_DATA_PART: () => SPEC_DATA_PART,
|
|
33
|
+
SPEC_DATA_PART_TYPE: () => SPEC_DATA_PART_TYPE,
|
|
33
34
|
ValidationCheckSchema: () => ValidationCheckSchema,
|
|
34
35
|
ValidationConfigSchema: () => ValidationConfigSchema,
|
|
35
36
|
VisibilityConditionSchema: () => VisibilityConditionSchema,
|
|
36
37
|
action: () => action,
|
|
37
38
|
actionBinding: () => actionBinding,
|
|
38
39
|
addByPath: () => addByPath,
|
|
40
|
+
applySpecPatch: () => applySpecPatch,
|
|
39
41
|
applySpecStreamPatch: () => applySpecStreamPatch,
|
|
40
42
|
autoFixSpec: () => autoFixSpec,
|
|
41
43
|
buildUserPrompt: () => buildUserPrompt,
|
|
42
44
|
builtInValidationFunctions: () => builtInValidationFunctions,
|
|
43
45
|
check: () => check,
|
|
44
46
|
compileSpecStream: () => compileSpecStream,
|
|
45
|
-
|
|
47
|
+
createJsonRenderTransform: () => createJsonRenderTransform,
|
|
48
|
+
createMixedStreamParser: () => createMixedStreamParser,
|
|
46
49
|
createSpecStreamCompiler: () => createSpecStreamCompiler,
|
|
47
50
|
defineCatalog: () => defineCatalog,
|
|
48
51
|
defineSchema: () => defineSchema,
|
|
49
|
-
evaluateLogicExpression: () => evaluateLogicExpression,
|
|
50
52
|
evaluateVisibility: () => evaluateVisibility,
|
|
51
53
|
executeAction: () => executeAction,
|
|
52
54
|
findFormValue: () => findFormValue,
|
|
53
55
|
formatSpecIssues: () => formatSpecIssues,
|
|
54
|
-
generateCatalogPrompt: () => generateCatalogPrompt,
|
|
55
|
-
generateSystemPrompt: () => generateSystemPrompt,
|
|
56
56
|
getByPath: () => getByPath,
|
|
57
57
|
interpolateString: () => interpolateString,
|
|
58
|
+
nestedToFlat: () => nestedToFlat,
|
|
58
59
|
parseSpecStreamLine: () => parseSpecStreamLine,
|
|
60
|
+
pipeJsonRender: () => pipeJsonRender,
|
|
59
61
|
removeByPath: () => removeByPath,
|
|
60
62
|
resolveAction: () => resolveAction,
|
|
63
|
+
resolveActionParam: () => resolveActionParam,
|
|
64
|
+
resolveBindings: () => resolveBindings,
|
|
61
65
|
resolveDynamicValue: () => resolveDynamicValue,
|
|
62
66
|
resolveElementProps: () => resolveElementProps,
|
|
63
67
|
resolvePropValue: () => resolvePropValue,
|
|
@@ -76,26 +80,26 @@ var DynamicValueSchema = import_zod.z.union([
|
|
|
76
80
|
import_zod.z.number(),
|
|
77
81
|
import_zod.z.boolean(),
|
|
78
82
|
import_zod.z.null(),
|
|
79
|
-
import_zod.z.object({
|
|
83
|
+
import_zod.z.object({ $state: import_zod.z.string() })
|
|
80
84
|
]);
|
|
81
85
|
var DynamicStringSchema = import_zod.z.union([
|
|
82
86
|
import_zod.z.string(),
|
|
83
|
-
import_zod.z.object({
|
|
87
|
+
import_zod.z.object({ $state: import_zod.z.string() })
|
|
84
88
|
]);
|
|
85
89
|
var DynamicNumberSchema = import_zod.z.union([
|
|
86
90
|
import_zod.z.number(),
|
|
87
|
-
import_zod.z.object({
|
|
91
|
+
import_zod.z.object({ $state: import_zod.z.string() })
|
|
88
92
|
]);
|
|
89
93
|
var DynamicBooleanSchema = import_zod.z.union([
|
|
90
94
|
import_zod.z.boolean(),
|
|
91
|
-
import_zod.z.object({
|
|
95
|
+
import_zod.z.object({ $state: import_zod.z.string() })
|
|
92
96
|
]);
|
|
93
97
|
function resolveDynamicValue(value, stateModel) {
|
|
94
98
|
if (value === null || value === void 0) {
|
|
95
99
|
return void 0;
|
|
96
100
|
}
|
|
97
|
-
if (typeof value === "object" && "
|
|
98
|
-
return getByPath(stateModel, value
|
|
101
|
+
if (typeof value === "object" && "$state" in value) {
|
|
102
|
+
return getByPath(stateModel, value.$state);
|
|
99
103
|
}
|
|
100
104
|
return value;
|
|
101
105
|
}
|
|
@@ -242,7 +246,7 @@ function deepEqual(a, b) {
|
|
|
242
246
|
if (aKeys.length !== bKeys.length) return false;
|
|
243
247
|
return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));
|
|
244
248
|
}
|
|
245
|
-
function findFormValue(fieldName, params,
|
|
249
|
+
function findFormValue(fieldName, params, state) {
|
|
246
250
|
if (params?.[fieldName] !== void 0) {
|
|
247
251
|
const val = params[fieldName];
|
|
248
252
|
if (typeof val !== "string" || !val.includes(".")) {
|
|
@@ -259,19 +263,15 @@ function findFormValue(fieldName, params, data) {
|
|
|
259
263
|
}
|
|
260
264
|
}
|
|
261
265
|
}
|
|
262
|
-
if (
|
|
263
|
-
for (const key of Object.keys(
|
|
266
|
+
if (state) {
|
|
267
|
+
for (const key of Object.keys(state)) {
|
|
264
268
|
if (key === fieldName || key.endsWith(`.${fieldName}`)) {
|
|
265
|
-
return
|
|
269
|
+
return state[key];
|
|
266
270
|
}
|
|
267
271
|
}
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const val = getByPath(data, path);
|
|
272
|
-
if (val !== void 0) {
|
|
273
|
-
return val;
|
|
274
|
-
}
|
|
272
|
+
const val = getByPath(state, fieldName);
|
|
273
|
+
if (val !== void 0) {
|
|
274
|
+
return val;
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
return void 0;
|
|
@@ -325,6 +325,44 @@ function applySpecStreamPatch(obj, patch) {
|
|
|
325
325
|
}
|
|
326
326
|
return obj;
|
|
327
327
|
}
|
|
328
|
+
function applySpecPatch(spec, patch) {
|
|
329
|
+
applySpecStreamPatch(spec, patch);
|
|
330
|
+
return spec;
|
|
331
|
+
}
|
|
332
|
+
function nestedToFlat(nested) {
|
|
333
|
+
const elements = {};
|
|
334
|
+
let counter = 0;
|
|
335
|
+
function walk(node) {
|
|
336
|
+
const key = `el-${counter++}`;
|
|
337
|
+
const { type, props, children: rawChildren, ...rest } = node;
|
|
338
|
+
const childKeys = [];
|
|
339
|
+
if (Array.isArray(rawChildren)) {
|
|
340
|
+
for (const child of rawChildren) {
|
|
341
|
+
if (child && typeof child === "object" && "type" in child) {
|
|
342
|
+
childKeys.push(walk(child));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const element = {
|
|
347
|
+
type: type ?? "unknown",
|
|
348
|
+
props: props ?? {},
|
|
349
|
+
children: childKeys
|
|
350
|
+
};
|
|
351
|
+
for (const [k, v] of Object.entries(rest)) {
|
|
352
|
+
if (k !== "state" && v !== void 0) {
|
|
353
|
+
element[k] = v;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
elements[key] = element;
|
|
357
|
+
return key;
|
|
358
|
+
}
|
|
359
|
+
const root = walk(nested);
|
|
360
|
+
const spec = { root, elements };
|
|
361
|
+
if (nested.state && typeof nested.state === "object" && !Array.isArray(nested.state)) {
|
|
362
|
+
spec.state = nested.state;
|
|
363
|
+
}
|
|
364
|
+
return spec;
|
|
365
|
+
}
|
|
328
366
|
function compileSpecStream(stream, initial = {}) {
|
|
329
367
|
const lines = stream.split("\n");
|
|
330
368
|
const result = { ...initial };
|
|
@@ -387,129 +425,304 @@ function createSpecStreamCompiler(initial = {}) {
|
|
|
387
425
|
}
|
|
388
426
|
};
|
|
389
427
|
}
|
|
428
|
+
function createMixedStreamParser(callbacks) {
|
|
429
|
+
let buffer = "";
|
|
430
|
+
let inSpecFence = false;
|
|
431
|
+
function processLine(line) {
|
|
432
|
+
const trimmed = line.trim();
|
|
433
|
+
if (!inSpecFence && trimmed.startsWith("```spec")) {
|
|
434
|
+
inSpecFence = true;
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (inSpecFence && trimmed === "```") {
|
|
438
|
+
inSpecFence = false;
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (!trimmed) return;
|
|
442
|
+
if (inSpecFence) {
|
|
443
|
+
const patch2 = parseSpecStreamLine(trimmed);
|
|
444
|
+
if (patch2) {
|
|
445
|
+
callbacks.onPatch(patch2);
|
|
446
|
+
}
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const patch = parseSpecStreamLine(trimmed);
|
|
450
|
+
if (patch) {
|
|
451
|
+
callbacks.onPatch(patch);
|
|
452
|
+
} else {
|
|
453
|
+
callbacks.onText(line);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
push(chunk) {
|
|
458
|
+
buffer += chunk;
|
|
459
|
+
const lines = buffer.split("\n");
|
|
460
|
+
buffer = lines.pop() || "";
|
|
461
|
+
for (const line of lines) {
|
|
462
|
+
processLine(line);
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
flush() {
|
|
466
|
+
if (buffer.trim()) {
|
|
467
|
+
processLine(buffer);
|
|
468
|
+
}
|
|
469
|
+
buffer = "";
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
var SPEC_FENCE_OPEN = "```spec";
|
|
474
|
+
var SPEC_FENCE_CLOSE = "```";
|
|
475
|
+
function createJsonRenderTransform() {
|
|
476
|
+
let lineBuffer = "";
|
|
477
|
+
let currentTextId = "";
|
|
478
|
+
let buffering = false;
|
|
479
|
+
let inSpecFence = false;
|
|
480
|
+
function emitPatch(patch, controller) {
|
|
481
|
+
controller.enqueue({
|
|
482
|
+
type: SPEC_DATA_PART_TYPE,
|
|
483
|
+
data: { type: "patch", patch }
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
function flushBuffer(controller) {
|
|
487
|
+
if (!lineBuffer) return;
|
|
488
|
+
const trimmed = lineBuffer.trim();
|
|
489
|
+
if (inSpecFence) {
|
|
490
|
+
if (trimmed) {
|
|
491
|
+
const patch = parseSpecStreamLine(trimmed);
|
|
492
|
+
if (patch) emitPatch(patch, controller);
|
|
493
|
+
}
|
|
494
|
+
lineBuffer = "";
|
|
495
|
+
buffering = false;
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (trimmed) {
|
|
499
|
+
const patch = parseSpecStreamLine(trimmed);
|
|
500
|
+
if (patch) {
|
|
501
|
+
emitPatch(patch, controller);
|
|
502
|
+
} else {
|
|
503
|
+
controller.enqueue({
|
|
504
|
+
type: "text-delta",
|
|
505
|
+
id: currentTextId,
|
|
506
|
+
delta: lineBuffer
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
} else {
|
|
510
|
+
controller.enqueue({
|
|
511
|
+
type: "text-delta",
|
|
512
|
+
id: currentTextId,
|
|
513
|
+
delta: lineBuffer
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
lineBuffer = "";
|
|
517
|
+
buffering = false;
|
|
518
|
+
}
|
|
519
|
+
function processCompleteLine(line, controller) {
|
|
520
|
+
const trimmed = line.trim();
|
|
521
|
+
if (!inSpecFence && trimmed.startsWith(SPEC_FENCE_OPEN)) {
|
|
522
|
+
inSpecFence = true;
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (inSpecFence && trimmed === SPEC_FENCE_CLOSE) {
|
|
526
|
+
inSpecFence = false;
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (inSpecFence) {
|
|
530
|
+
if (trimmed) {
|
|
531
|
+
const patch2 = parseSpecStreamLine(trimmed);
|
|
532
|
+
if (patch2) emitPatch(patch2, controller);
|
|
533
|
+
}
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (!trimmed) {
|
|
537
|
+
controller.enqueue({
|
|
538
|
+
type: "text-delta",
|
|
539
|
+
id: currentTextId,
|
|
540
|
+
delta: "\n"
|
|
541
|
+
});
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
const patch = parseSpecStreamLine(trimmed);
|
|
545
|
+
if (patch) {
|
|
546
|
+
emitPatch(patch, controller);
|
|
547
|
+
} else {
|
|
548
|
+
controller.enqueue({
|
|
549
|
+
type: "text-delta",
|
|
550
|
+
id: currentTextId,
|
|
551
|
+
delta: line + "\n"
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return new TransformStream({
|
|
556
|
+
transform(chunk, controller) {
|
|
557
|
+
switch (chunk.type) {
|
|
558
|
+
case "text-start": {
|
|
559
|
+
currentTextId = chunk.id;
|
|
560
|
+
controller.enqueue(chunk);
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
case "text-delta": {
|
|
564
|
+
const delta = chunk;
|
|
565
|
+
currentTextId = delta.id;
|
|
566
|
+
const text = delta.delta;
|
|
567
|
+
for (let i = 0; i < text.length; i++) {
|
|
568
|
+
const ch = text.charAt(i);
|
|
569
|
+
if (ch === "\n") {
|
|
570
|
+
if (buffering) {
|
|
571
|
+
processCompleteLine(lineBuffer, controller);
|
|
572
|
+
lineBuffer = "";
|
|
573
|
+
buffering = false;
|
|
574
|
+
} else {
|
|
575
|
+
if (!inSpecFence) {
|
|
576
|
+
controller.enqueue({
|
|
577
|
+
type: "text-delta",
|
|
578
|
+
id: currentTextId,
|
|
579
|
+
delta: "\n"
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
} else if (lineBuffer.length === 0 && !buffering) {
|
|
584
|
+
if (inSpecFence || ch === "{" || ch === "`") {
|
|
585
|
+
buffering = true;
|
|
586
|
+
lineBuffer += ch;
|
|
587
|
+
} else {
|
|
588
|
+
controller.enqueue({
|
|
589
|
+
type: "text-delta",
|
|
590
|
+
id: currentTextId,
|
|
591
|
+
delta: ch
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
} else if (buffering) {
|
|
595
|
+
lineBuffer += ch;
|
|
596
|
+
} else {
|
|
597
|
+
controller.enqueue({
|
|
598
|
+
type: "text-delta",
|
|
599
|
+
id: currentTextId,
|
|
600
|
+
delta: ch
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
case "text-end": {
|
|
607
|
+
flushBuffer(controller);
|
|
608
|
+
controller.enqueue(chunk);
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
default: {
|
|
612
|
+
controller.enqueue(chunk);
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
flush(controller) {
|
|
618
|
+
flushBuffer(controller);
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
var SPEC_DATA_PART = "spec";
|
|
623
|
+
var SPEC_DATA_PART_TYPE = `data-${SPEC_DATA_PART}`;
|
|
624
|
+
function pipeJsonRender(stream) {
|
|
625
|
+
return stream.pipeThrough(
|
|
626
|
+
createJsonRenderTransform()
|
|
627
|
+
);
|
|
628
|
+
}
|
|
390
629
|
|
|
391
630
|
// src/visibility.ts
|
|
392
631
|
var import_zod2 = require("zod");
|
|
393
|
-
var
|
|
632
|
+
var numericOrStateRef = import_zod2.z.union([
|
|
394
633
|
import_zod2.z.number(),
|
|
395
|
-
import_zod2.z.object({
|
|
634
|
+
import_zod2.z.object({ $state: import_zod2.z.string() })
|
|
396
635
|
]);
|
|
397
|
-
var
|
|
636
|
+
var comparisonOps = {
|
|
637
|
+
eq: import_zod2.z.unknown().optional(),
|
|
638
|
+
neq: import_zod2.z.unknown().optional(),
|
|
639
|
+
gt: numericOrStateRef.optional(),
|
|
640
|
+
gte: numericOrStateRef.optional(),
|
|
641
|
+
lt: numericOrStateRef.optional(),
|
|
642
|
+
lte: numericOrStateRef.optional(),
|
|
643
|
+
not: import_zod2.z.literal(true).optional()
|
|
644
|
+
};
|
|
645
|
+
var StateConditionSchema = import_zod2.z.object({
|
|
646
|
+
$state: import_zod2.z.string(),
|
|
647
|
+
...comparisonOps
|
|
648
|
+
});
|
|
649
|
+
var ItemConditionSchema = import_zod2.z.object({
|
|
650
|
+
$item: import_zod2.z.string(),
|
|
651
|
+
...comparisonOps
|
|
652
|
+
});
|
|
653
|
+
var IndexConditionSchema = import_zod2.z.object({
|
|
654
|
+
$index: import_zod2.z.literal(true),
|
|
655
|
+
...comparisonOps
|
|
656
|
+
});
|
|
657
|
+
var SingleConditionSchema = import_zod2.z.union([
|
|
658
|
+
StateConditionSchema,
|
|
659
|
+
ItemConditionSchema,
|
|
660
|
+
IndexConditionSchema
|
|
661
|
+
]);
|
|
662
|
+
var VisibilityConditionSchema = import_zod2.z.lazy(
|
|
398
663
|
() => import_zod2.z.union([
|
|
399
|
-
import_zod2.z.
|
|
400
|
-
|
|
401
|
-
import_zod2.z.
|
|
402
|
-
import_zod2.z.object({
|
|
403
|
-
import_zod2.z.object({
|
|
404
|
-
import_zod2.z.object({ neq: import_zod2.z.tuple([DynamicValueSchema, DynamicValueSchema]) }),
|
|
405
|
-
import_zod2.z.object({
|
|
406
|
-
gt: import_zod2.z.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema])
|
|
407
|
-
}),
|
|
408
|
-
import_zod2.z.object({
|
|
409
|
-
gte: import_zod2.z.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema])
|
|
410
|
-
}),
|
|
411
|
-
import_zod2.z.object({
|
|
412
|
-
lt: import_zod2.z.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema])
|
|
413
|
-
}),
|
|
414
|
-
import_zod2.z.object({
|
|
415
|
-
lte: import_zod2.z.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema])
|
|
416
|
-
})
|
|
664
|
+
import_zod2.z.boolean(),
|
|
665
|
+
SingleConditionSchema,
|
|
666
|
+
import_zod2.z.array(SingleConditionSchema),
|
|
667
|
+
import_zod2.z.object({ $and: import_zod2.z.array(VisibilityConditionSchema) }),
|
|
668
|
+
import_zod2.z.object({ $or: import_zod2.z.array(VisibilityConditionSchema) })
|
|
417
669
|
])
|
|
418
670
|
);
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
LogicExpressionSchema
|
|
424
|
-
]);
|
|
425
|
-
function evaluateLogicExpression(expr, ctx) {
|
|
426
|
-
const { stateModel } = ctx;
|
|
427
|
-
if ("and" in expr) {
|
|
428
|
-
return expr.and.every((subExpr) => evaluateLogicExpression(subExpr, ctx));
|
|
429
|
-
}
|
|
430
|
-
if ("or" in expr) {
|
|
431
|
-
return expr.or.some((subExpr) => evaluateLogicExpression(subExpr, ctx));
|
|
432
|
-
}
|
|
433
|
-
if ("not" in expr) {
|
|
434
|
-
return !evaluateLogicExpression(expr.not, ctx);
|
|
435
|
-
}
|
|
436
|
-
if ("path" in expr) {
|
|
437
|
-
const value = resolveDynamicValue({ path: expr.path }, stateModel);
|
|
438
|
-
return Boolean(value);
|
|
439
|
-
}
|
|
440
|
-
if ("eq" in expr) {
|
|
441
|
-
const [left, right] = expr.eq;
|
|
442
|
-
const leftValue = resolveDynamicValue(left, stateModel);
|
|
443
|
-
const rightValue = resolveDynamicValue(right, stateModel);
|
|
444
|
-
return leftValue === rightValue;
|
|
445
|
-
}
|
|
446
|
-
if ("neq" in expr) {
|
|
447
|
-
const [left, right] = expr.neq;
|
|
448
|
-
const leftValue = resolveDynamicValue(left, stateModel);
|
|
449
|
-
const rightValue = resolveDynamicValue(right, stateModel);
|
|
450
|
-
return leftValue !== rightValue;
|
|
451
|
-
}
|
|
452
|
-
if ("gt" in expr) {
|
|
453
|
-
const [left, right] = expr.gt;
|
|
454
|
-
const leftValue = resolveDynamicValue(
|
|
455
|
-
left,
|
|
456
|
-
stateModel
|
|
457
|
-
);
|
|
458
|
-
const rightValue = resolveDynamicValue(
|
|
459
|
-
right,
|
|
460
|
-
stateModel
|
|
461
|
-
);
|
|
462
|
-
if (typeof leftValue === "number" && typeof rightValue === "number") {
|
|
463
|
-
return leftValue > rightValue;
|
|
671
|
+
function resolveComparisonValue(value, ctx) {
|
|
672
|
+
if (typeof value === "object" && value !== null) {
|
|
673
|
+
if ("$state" in value && typeof value.$state === "string") {
|
|
674
|
+
return getByPath(ctx.stateModel, value.$state);
|
|
464
675
|
}
|
|
465
|
-
return false;
|
|
466
676
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
return leftValue >= rightValue;
|
|
479
|
-
}
|
|
480
|
-
return false;
|
|
677
|
+
return value;
|
|
678
|
+
}
|
|
679
|
+
function isItemCondition(cond) {
|
|
680
|
+
return "$item" in cond;
|
|
681
|
+
}
|
|
682
|
+
function isIndexCondition(cond) {
|
|
683
|
+
return "$index" in cond;
|
|
684
|
+
}
|
|
685
|
+
function resolveConditionValue(cond, ctx) {
|
|
686
|
+
if (isIndexCondition(cond)) {
|
|
687
|
+
return ctx.repeatIndex;
|
|
481
688
|
}
|
|
482
|
-
if (
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
left,
|
|
486
|
-
stateModel
|
|
487
|
-
);
|
|
488
|
-
const rightValue = resolveDynamicValue(
|
|
489
|
-
right,
|
|
490
|
-
stateModel
|
|
491
|
-
);
|
|
492
|
-
if (typeof leftValue === "number" && typeof rightValue === "number") {
|
|
493
|
-
return leftValue < rightValue;
|
|
494
|
-
}
|
|
495
|
-
return false;
|
|
689
|
+
if (isItemCondition(cond)) {
|
|
690
|
+
if (ctx.repeatItem === void 0) return void 0;
|
|
691
|
+
return cond.$item === "" ? ctx.repeatItem : getByPath(ctx.repeatItem, cond.$item);
|
|
496
692
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
);
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
693
|
+
return getByPath(ctx.stateModel, cond.$state);
|
|
694
|
+
}
|
|
695
|
+
function evaluateCondition(cond, ctx) {
|
|
696
|
+
const value = resolveConditionValue(cond, ctx);
|
|
697
|
+
let result;
|
|
698
|
+
if (cond.eq !== void 0) {
|
|
699
|
+
const rhs = resolveComparisonValue(cond.eq, ctx);
|
|
700
|
+
result = value === rhs;
|
|
701
|
+
} else if (cond.neq !== void 0) {
|
|
702
|
+
const rhs = resolveComparisonValue(cond.neq, ctx);
|
|
703
|
+
result = value !== rhs;
|
|
704
|
+
} else if (cond.gt !== void 0) {
|
|
705
|
+
const rhs = resolveComparisonValue(cond.gt, ctx);
|
|
706
|
+
result = typeof value === "number" && typeof rhs === "number" ? value > rhs : false;
|
|
707
|
+
} else if (cond.gte !== void 0) {
|
|
708
|
+
const rhs = resolveComparisonValue(cond.gte, ctx);
|
|
709
|
+
result = typeof value === "number" && typeof rhs === "number" ? value >= rhs : false;
|
|
710
|
+
} else if (cond.lt !== void 0) {
|
|
711
|
+
const rhs = resolveComparisonValue(cond.lt, ctx);
|
|
712
|
+
result = typeof value === "number" && typeof rhs === "number" ? value < rhs : false;
|
|
713
|
+
} else if (cond.lte !== void 0) {
|
|
714
|
+
const rhs = resolveComparisonValue(cond.lte, ctx);
|
|
715
|
+
result = typeof value === "number" && typeof rhs === "number" ? value <= rhs : false;
|
|
716
|
+
} else {
|
|
717
|
+
result = Boolean(value);
|
|
511
718
|
}
|
|
512
|
-
return
|
|
719
|
+
return cond.not === true ? !result : result;
|
|
720
|
+
}
|
|
721
|
+
function isAndCondition(condition) {
|
|
722
|
+
return typeof condition === "object" && condition !== null && !Array.isArray(condition) && "$and" in condition;
|
|
723
|
+
}
|
|
724
|
+
function isOrCondition(condition) {
|
|
725
|
+
return typeof condition === "object" && condition !== null && !Array.isArray(condition) && "$or" in condition;
|
|
513
726
|
}
|
|
514
727
|
function evaluateVisibility(condition, ctx) {
|
|
515
728
|
if (condition === void 0) {
|
|
@@ -518,74 +731,114 @@ function evaluateVisibility(condition, ctx) {
|
|
|
518
731
|
if (typeof condition === "boolean") {
|
|
519
732
|
return condition;
|
|
520
733
|
}
|
|
521
|
-
if (
|
|
522
|
-
|
|
523
|
-
return Boolean(value);
|
|
734
|
+
if (Array.isArray(condition)) {
|
|
735
|
+
return condition.every((c) => evaluateCondition(c, ctx));
|
|
524
736
|
}
|
|
525
|
-
if (
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if (condition.auth === "signedOut") {
|
|
531
|
-
return !isSignedIn;
|
|
532
|
-
}
|
|
533
|
-
return false;
|
|
737
|
+
if (isAndCondition(condition)) {
|
|
738
|
+
return condition.$and.every((child) => evaluateVisibility(child, ctx));
|
|
739
|
+
}
|
|
740
|
+
if (isOrCondition(condition)) {
|
|
741
|
+
return condition.$or.some((child) => evaluateVisibility(child, ctx));
|
|
534
742
|
}
|
|
535
|
-
return
|
|
743
|
+
return evaluateCondition(condition, ctx);
|
|
536
744
|
}
|
|
537
745
|
var visibility = {
|
|
538
746
|
/** Always visible */
|
|
539
747
|
always: true,
|
|
540
748
|
/** Never visible */
|
|
541
749
|
never: false,
|
|
542
|
-
/** Visible when path is truthy */
|
|
543
|
-
when: (path) => ({ path }),
|
|
544
|
-
/** Visible when
|
|
545
|
-
|
|
546
|
-
/** Visible when signed out */
|
|
547
|
-
signedOut: { auth: "signedOut" },
|
|
548
|
-
/** AND multiple conditions */
|
|
549
|
-
and: (...conditions) => ({
|
|
550
|
-
and: conditions
|
|
551
|
-
}),
|
|
552
|
-
/** OR multiple conditions */
|
|
553
|
-
or: (...conditions) => ({
|
|
554
|
-
or: conditions
|
|
555
|
-
}),
|
|
556
|
-
/** NOT a condition */
|
|
557
|
-
not: (condition) => ({ not: condition }),
|
|
750
|
+
/** Visible when state path is truthy */
|
|
751
|
+
when: (path) => ({ $state: path }),
|
|
752
|
+
/** Visible when state path is falsy */
|
|
753
|
+
unless: (path) => ({ $state: path, not: true }),
|
|
558
754
|
/** Equality check */
|
|
559
|
-
eq: (
|
|
560
|
-
|
|
755
|
+
eq: (path, value) => ({
|
|
756
|
+
$state: path,
|
|
757
|
+
eq: value
|
|
561
758
|
}),
|
|
562
759
|
/** Not equal check */
|
|
563
|
-
neq: (
|
|
564
|
-
|
|
760
|
+
neq: (path, value) => ({
|
|
761
|
+
$state: path,
|
|
762
|
+
neq: value
|
|
565
763
|
}),
|
|
566
764
|
/** Greater than */
|
|
567
|
-
gt: (
|
|
765
|
+
gt: (path, value) => ({
|
|
766
|
+
$state: path,
|
|
767
|
+
gt: value
|
|
768
|
+
}),
|
|
568
769
|
/** Greater than or equal */
|
|
569
|
-
gte: (
|
|
770
|
+
gte: (path, value) => ({
|
|
771
|
+
$state: path,
|
|
772
|
+
gte: value
|
|
773
|
+
}),
|
|
570
774
|
/** Less than */
|
|
571
|
-
lt: (
|
|
775
|
+
lt: (path, value) => ({
|
|
776
|
+
$state: path,
|
|
777
|
+
lt: value
|
|
778
|
+
}),
|
|
572
779
|
/** Less than or equal */
|
|
573
|
-
lte: (
|
|
780
|
+
lte: (path, value) => ({
|
|
781
|
+
$state: path,
|
|
782
|
+
lte: value
|
|
783
|
+
}),
|
|
784
|
+
/** AND multiple conditions */
|
|
785
|
+
and: (...conditions) => ({
|
|
786
|
+
$and: conditions
|
|
787
|
+
}),
|
|
788
|
+
/** OR multiple conditions */
|
|
789
|
+
or: (...conditions) => ({
|
|
790
|
+
$or: conditions
|
|
791
|
+
})
|
|
574
792
|
};
|
|
575
793
|
|
|
576
794
|
// src/props.ts
|
|
577
|
-
function
|
|
578
|
-
return typeof value === "object" && value !== null && "$
|
|
795
|
+
function isStateExpression(value) {
|
|
796
|
+
return typeof value === "object" && value !== null && "$state" in value && typeof value.$state === "string";
|
|
797
|
+
}
|
|
798
|
+
function isItemExpression(value) {
|
|
799
|
+
return typeof value === "object" && value !== null && "$item" in value && typeof value.$item === "string";
|
|
800
|
+
}
|
|
801
|
+
function isIndexExpression(value) {
|
|
802
|
+
return typeof value === "object" && value !== null && "$index" in value && value.$index === true;
|
|
803
|
+
}
|
|
804
|
+
function isBindStateExpression(value) {
|
|
805
|
+
return typeof value === "object" && value !== null && "$bindState" in value && typeof value.$bindState === "string";
|
|
806
|
+
}
|
|
807
|
+
function isBindItemExpression(value) {
|
|
808
|
+
return typeof value === "object" && value !== null && "$bindItem" in value && typeof value.$bindItem === "string";
|
|
579
809
|
}
|
|
580
810
|
function isCondExpression(value) {
|
|
581
811
|
return typeof value === "object" && value !== null && "$cond" in value && "$then" in value && "$else" in value;
|
|
582
812
|
}
|
|
813
|
+
function resolveBindItemPath(itemPath, ctx) {
|
|
814
|
+
if (ctx.repeatBasePath == null) {
|
|
815
|
+
console.warn(`$bindItem used outside repeat scope: "${itemPath}"`);
|
|
816
|
+
return void 0;
|
|
817
|
+
}
|
|
818
|
+
if (itemPath === "") return ctx.repeatBasePath;
|
|
819
|
+
return ctx.repeatBasePath + "/" + itemPath;
|
|
820
|
+
}
|
|
583
821
|
function resolvePropValue(value, ctx) {
|
|
584
822
|
if (value === null || value === void 0) {
|
|
585
823
|
return value;
|
|
586
824
|
}
|
|
587
|
-
if (
|
|
588
|
-
return getByPath(ctx.stateModel, value.$
|
|
825
|
+
if (isStateExpression(value)) {
|
|
826
|
+
return getByPath(ctx.stateModel, value.$state);
|
|
827
|
+
}
|
|
828
|
+
if (isItemExpression(value)) {
|
|
829
|
+
if (ctx.repeatItem === void 0) return void 0;
|
|
830
|
+
return value.$item === "" ? ctx.repeatItem : getByPath(ctx.repeatItem, value.$item);
|
|
831
|
+
}
|
|
832
|
+
if (isIndexExpression(value)) {
|
|
833
|
+
return ctx.repeatIndex;
|
|
834
|
+
}
|
|
835
|
+
if (isBindStateExpression(value)) {
|
|
836
|
+
return getByPath(ctx.stateModel, value.$bindState);
|
|
837
|
+
}
|
|
838
|
+
if (isBindItemExpression(value)) {
|
|
839
|
+
const resolvedPath = resolveBindItemPath(value.$bindItem, ctx);
|
|
840
|
+
if (resolvedPath === void 0) return void 0;
|
|
841
|
+
return getByPath(ctx.stateModel, resolvedPath);
|
|
589
842
|
}
|
|
590
843
|
if (isCondExpression(value)) {
|
|
591
844
|
const result = evaluateVisibility(value.$cond, ctx);
|
|
@@ -610,6 +863,31 @@ function resolveElementProps(props, ctx) {
|
|
|
610
863
|
}
|
|
611
864
|
return resolved;
|
|
612
865
|
}
|
|
866
|
+
function resolveBindings(props, ctx) {
|
|
867
|
+
let bindings;
|
|
868
|
+
for (const [key, value] of Object.entries(props)) {
|
|
869
|
+
if (isBindStateExpression(value)) {
|
|
870
|
+
if (!bindings) bindings = {};
|
|
871
|
+
bindings[key] = value.$bindState;
|
|
872
|
+
} else if (isBindItemExpression(value)) {
|
|
873
|
+
const resolved = resolveBindItemPath(value.$bindItem, ctx);
|
|
874
|
+
if (resolved !== void 0) {
|
|
875
|
+
if (!bindings) bindings = {};
|
|
876
|
+
bindings[key] = resolved;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return bindings;
|
|
881
|
+
}
|
|
882
|
+
function resolveActionParam(value, ctx) {
|
|
883
|
+
if (isItemExpression(value)) {
|
|
884
|
+
return resolveBindItemPath(value.$item, ctx);
|
|
885
|
+
}
|
|
886
|
+
if (isIndexExpression(value)) {
|
|
887
|
+
return ctx.repeatIndex;
|
|
888
|
+
}
|
|
889
|
+
return resolvePropValue(value, ctx);
|
|
890
|
+
}
|
|
613
891
|
|
|
614
892
|
// src/actions.ts
|
|
615
893
|
var import_zod3 = require("zod");
|
|
@@ -662,7 +940,7 @@ function resolveAction(binding, stateModel) {
|
|
|
662
940
|
}
|
|
663
941
|
function interpolateString(template, stateModel) {
|
|
664
942
|
return template.replace(/\$\{([^}]+)\}/g, (_, path) => {
|
|
665
|
-
const value = resolveDynamicValue({ path }, stateModel);
|
|
943
|
+
const value = resolveDynamicValue({ $state: path }, stateModel);
|
|
666
944
|
return String(value ?? "");
|
|
667
945
|
});
|
|
668
946
|
}
|
|
@@ -720,14 +998,14 @@ var action = actionBinding;
|
|
|
720
998
|
// src/validation.ts
|
|
721
999
|
var import_zod4 = require("zod");
|
|
722
1000
|
var ValidationCheckSchema = import_zod4.z.object({
|
|
723
|
-
|
|
1001
|
+
type: import_zod4.z.string(),
|
|
724
1002
|
args: import_zod4.z.record(import_zod4.z.string(), DynamicValueSchema).optional(),
|
|
725
1003
|
message: import_zod4.z.string()
|
|
726
1004
|
});
|
|
727
1005
|
var ValidationConfigSchema = import_zod4.z.object({
|
|
728
1006
|
checks: import_zod4.z.array(ValidationCheckSchema).optional(),
|
|
729
1007
|
validateOn: import_zod4.z.enum(["change", "blur", "submit"]).optional(),
|
|
730
|
-
enabled:
|
|
1008
|
+
enabled: VisibilityConditionSchema.optional()
|
|
731
1009
|
});
|
|
732
1010
|
var builtInValidationFunctions = {
|
|
733
1011
|
/**
|
|
@@ -831,19 +1109,19 @@ function runValidationCheck(check2, ctx) {
|
|
|
831
1109
|
resolvedArgs[key] = resolveDynamicValue(argValue, stateModel);
|
|
832
1110
|
}
|
|
833
1111
|
}
|
|
834
|
-
const
|
|
835
|
-
if (!
|
|
836
|
-
console.warn(`Unknown validation function: ${check2.
|
|
1112
|
+
const validationFn = builtInValidationFunctions[check2.type] ?? customFunctions?.[check2.type];
|
|
1113
|
+
if (!validationFn) {
|
|
1114
|
+
console.warn(`Unknown validation function: ${check2.type}`);
|
|
837
1115
|
return {
|
|
838
|
-
|
|
1116
|
+
type: check2.type,
|
|
839
1117
|
valid: true,
|
|
840
1118
|
// Don't fail on unknown functions
|
|
841
1119
|
message: check2.message
|
|
842
1120
|
};
|
|
843
1121
|
}
|
|
844
|
-
const valid =
|
|
1122
|
+
const valid = validationFn(value, resolvedArgs);
|
|
845
1123
|
return {
|
|
846
|
-
|
|
1124
|
+
type: check2.type,
|
|
847
1125
|
valid,
|
|
848
1126
|
message: check2.message
|
|
849
1127
|
};
|
|
@@ -852,9 +1130,8 @@ function runValidation(config, ctx) {
|
|
|
852
1130
|
const checks = [];
|
|
853
1131
|
const errors = [];
|
|
854
1132
|
if (config.enabled) {
|
|
855
|
-
const enabled =
|
|
856
|
-
stateModel: ctx.stateModel
|
|
857
|
-
authState: ctx.authState
|
|
1133
|
+
const enabled = evaluateVisibility(config.enabled, {
|
|
1134
|
+
stateModel: ctx.stateModel
|
|
858
1135
|
});
|
|
859
1136
|
if (!enabled) {
|
|
860
1137
|
return { valid: true, errors: [], checks: [] };
|
|
@@ -877,45 +1154,45 @@ function runValidation(config, ctx) {
|
|
|
877
1154
|
}
|
|
878
1155
|
var check = {
|
|
879
1156
|
required: (message = "This field is required") => ({
|
|
880
|
-
|
|
1157
|
+
type: "required",
|
|
881
1158
|
message
|
|
882
1159
|
}),
|
|
883
1160
|
email: (message = "Invalid email address") => ({
|
|
884
|
-
|
|
1161
|
+
type: "email",
|
|
885
1162
|
message
|
|
886
1163
|
}),
|
|
887
1164
|
minLength: (min, message) => ({
|
|
888
|
-
|
|
1165
|
+
type: "minLength",
|
|
889
1166
|
args: { min },
|
|
890
1167
|
message: message ?? `Must be at least ${min} characters`
|
|
891
1168
|
}),
|
|
892
1169
|
maxLength: (max, message) => ({
|
|
893
|
-
|
|
1170
|
+
type: "maxLength",
|
|
894
1171
|
args: { max },
|
|
895
1172
|
message: message ?? `Must be at most ${max} characters`
|
|
896
1173
|
}),
|
|
897
1174
|
pattern: (pattern, message = "Invalid format") => ({
|
|
898
|
-
|
|
1175
|
+
type: "pattern",
|
|
899
1176
|
args: { pattern },
|
|
900
1177
|
message
|
|
901
1178
|
}),
|
|
902
1179
|
min: (min, message) => ({
|
|
903
|
-
|
|
1180
|
+
type: "min",
|
|
904
1181
|
args: { min },
|
|
905
1182
|
message: message ?? `Must be at least ${min}`
|
|
906
1183
|
}),
|
|
907
1184
|
max: (max, message) => ({
|
|
908
|
-
|
|
1185
|
+
type: "max",
|
|
909
1186
|
args: { max },
|
|
910
1187
|
message: message ?? `Must be at most ${max}`
|
|
911
1188
|
}),
|
|
912
1189
|
url: (message = "Invalid URL") => ({
|
|
913
|
-
|
|
1190
|
+
type: "url",
|
|
914
1191
|
message
|
|
915
1192
|
}),
|
|
916
1193
|
matches: (otherPath, message = "Fields must match") => ({
|
|
917
|
-
|
|
918
|
-
args: { other: {
|
|
1194
|
+
type: "matches",
|
|
1195
|
+
args: { other: { $state: otherPath } },
|
|
919
1196
|
message
|
|
920
1197
|
})
|
|
921
1198
|
};
|
|
@@ -1055,7 +1332,7 @@ function autoFixSpec(spec) {
|
|
|
1055
1332
|
fixedElements[key] = fixed;
|
|
1056
1333
|
}
|
|
1057
1334
|
return {
|
|
1058
|
-
spec: { root: spec.root, elements: fixedElements },
|
|
1335
|
+
spec: { root: spec.root, elements: fixedElements, state: spec.state },
|
|
1059
1336
|
fixes
|
|
1060
1337
|
};
|
|
1061
1338
|
}
|
|
@@ -1239,15 +1516,35 @@ function generatePrompt(catalog, options) {
|
|
|
1239
1516
|
}
|
|
1240
1517
|
const {
|
|
1241
1518
|
system = "You are a UI generator that outputs JSON.",
|
|
1242
|
-
customRules = []
|
|
1519
|
+
customRules = [],
|
|
1520
|
+
mode = "generate"
|
|
1243
1521
|
} = options;
|
|
1244
1522
|
const lines = [];
|
|
1245
1523
|
lines.push(system);
|
|
1246
1524
|
lines.push("");
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1525
|
+
if (mode === "chat") {
|
|
1526
|
+
lines.push("OUTPUT FORMAT (text + JSONL, RFC 6902 JSON Patch):");
|
|
1527
|
+
lines.push(
|
|
1528
|
+
"You respond conversationally. When generating UI, first write a brief explanation (1-3 sentences), then output JSONL patch lines wrapped in a ```spec code fence."
|
|
1529
|
+
);
|
|
1530
|
+
lines.push(
|
|
1531
|
+
"The JSONL lines use RFC 6902 JSON Patch operations to build a UI tree. Always wrap them in a ```spec fence block:"
|
|
1532
|
+
);
|
|
1533
|
+
lines.push(" ```spec");
|
|
1534
|
+
lines.push(' {"op":"add","path":"/root","value":"main"}');
|
|
1535
|
+
lines.push(
|
|
1536
|
+
' {"op":"add","path":"/elements/main","value":{"type":"Card","props":{"title":"Hello"},"children":[]}}'
|
|
1537
|
+
);
|
|
1538
|
+
lines.push(" ```");
|
|
1539
|
+
lines.push(
|
|
1540
|
+
"If the user's message does not require a UI (e.g. a greeting or clarifying question), respond with text only \u2014 no JSONL."
|
|
1541
|
+
);
|
|
1542
|
+
} else {
|
|
1543
|
+
lines.push("OUTPUT FORMAT (JSONL, RFC 6902 JSON Patch):");
|
|
1544
|
+
lines.push(
|
|
1545
|
+
"Output JSONL (one JSON object per line) using RFC 6902 JSON Patch operations to build a UI tree."
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1251
1548
|
lines.push(
|
|
1252
1549
|
"Each line is a JSON patch operation (add, remove, replace). Start with /root, then stream /elements and /state patches interleaved so the UI fills in progressively as it streams."
|
|
1253
1550
|
);
|
|
@@ -1263,7 +1560,7 @@ function generatePrompt(catalog, options) {
|
|
|
1263
1560
|
const comp1Props = comp1Def ? getExampleProps(comp1Def) : {};
|
|
1264
1561
|
const comp2Props = comp2Def ? getExampleProps(comp2Def) : {};
|
|
1265
1562
|
const dynamicPropName = comp2Def?.props ? findFirstStringProp(comp2Def.props) : null;
|
|
1266
|
-
const dynamicProps = dynamicPropName ? { ...comp2Props, [dynamicPropName]: { $
|
|
1563
|
+
const dynamicProps = dynamicPropName ? { ...comp2Props, [dynamicPropName]: { $item: "title" } } : comp2Props;
|
|
1267
1564
|
const exampleOutput = [
|
|
1268
1565
|
JSON.stringify({ op: "add", path: "/root", value: "main" }),
|
|
1269
1566
|
JSON.stringify({
|
|
@@ -1286,7 +1583,7 @@ function generatePrompt(catalog, options) {
|
|
|
1286
1583
|
value: {
|
|
1287
1584
|
type: comp1,
|
|
1288
1585
|
props: comp1Props,
|
|
1289
|
-
repeat: {
|
|
1586
|
+
repeat: { statePath: "/items", key: "id" },
|
|
1290
1587
|
children: ["item"]
|
|
1291
1588
|
}
|
|
1292
1589
|
}),
|
|
@@ -1313,10 +1610,10 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|
|
1313
1610
|
lines.push("");
|
|
1314
1611
|
lines.push("INITIAL STATE:");
|
|
1315
1612
|
lines.push(
|
|
1316
|
-
"Specs include a /state field to seed the state model. Components with
|
|
1613
|
+
"Specs include a /state field to seed the state model. Components with { $bindState } or { $bindItem } read from and write to this state, and $state expressions read from it."
|
|
1317
1614
|
);
|
|
1318
1615
|
lines.push(
|
|
1319
|
-
"CRITICAL: You MUST include state patches whenever your UI displays data via $
|
|
1616
|
+
"CRITICAL: You MUST include state patches whenever your UI displays data via $state, $bindState, $bindItem, $item, or $index expressions, or uses repeat to iterate over arrays. Without state, these references resolve to nothing and repeat lists render zero items."
|
|
1320
1617
|
);
|
|
1321
1618
|
lines.push(
|
|
1322
1619
|
"Output state patches right after the elements that reference them, so the UI fills in progressively as it streams."
|
|
@@ -1334,7 +1631,7 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|
|
1334
1631
|
' Initialize the array first if needed: {"op":"add","path":"/state/posts","value":[]}'
|
|
1335
1632
|
);
|
|
1336
1633
|
lines.push(
|
|
1337
|
-
'When content comes from the state model, use { "$
|
|
1634
|
+
'When content comes from the state model, use { "$state": "/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.'
|
|
1338
1635
|
);
|
|
1339
1636
|
lines.push(
|
|
1340
1637
|
"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."
|
|
@@ -1342,16 +1639,16 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|
|
1342
1639
|
lines.push("");
|
|
1343
1640
|
lines.push("DYNAMIC LISTS (repeat field):");
|
|
1344
1641
|
lines.push(
|
|
1345
|
-
'Any element can have a top-level "repeat" field to render its children once per item in a state array: { "repeat": { "
|
|
1642
|
+
'Any element can have a top-level "repeat" field to render its children once per item in a state array: { "repeat": { "statePath": "/arrayPath", "key": "id" } }.'
|
|
1346
1643
|
);
|
|
1347
1644
|
lines.push(
|
|
1348
|
-
'The element itself renders once (as the container), and its children are expanded once per array item. "
|
|
1645
|
+
'The element itself renders once (as the container), and its children are expanded once per array item. "statePath" is the state array path. "key" is an optional field name on each item for stable React keys.'
|
|
1349
1646
|
);
|
|
1350
1647
|
lines.push(
|
|
1351
|
-
`Example: ${JSON.stringify({ type: comp1, props: comp1Props, repeat: {
|
|
1648
|
+
`Example: ${JSON.stringify({ type: comp1, props: comp1Props, repeat: { statePath: "/todos", key: "id" }, children: ["todo-item"] })}`
|
|
1352
1649
|
);
|
|
1353
1650
|
lines.push(
|
|
1354
|
-
'Inside children of a repeated element, use "$item
|
|
1651
|
+
'Inside children of a repeated element, use { "$item": "field" } to read a field from the current item, and { "$index": true } to get the current array index. For two-way binding to an item field use { "$bindItem": "completed" } on the appropriate prop.'
|
|
1355
1652
|
);
|
|
1356
1653
|
lines.push(
|
|
1357
1654
|
"ALWAYS use the repeat field for lists backed by state arrays. NEVER hardcode individual elements for each array item."
|
|
@@ -1362,19 +1659,19 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|
|
1362
1659
|
lines.push("");
|
|
1363
1660
|
lines.push("ARRAY STATE ACTIONS:");
|
|
1364
1661
|
lines.push(
|
|
1365
|
-
'Use action "pushState" to append items to arrays. Params: {
|
|
1662
|
+
'Use action "pushState" to append items to arrays. Params: { statePath: "/arrayPath", value: { ...item }, clearStatePath: "/inputPath" }.'
|
|
1366
1663
|
);
|
|
1367
1664
|
lines.push(
|
|
1368
|
-
'Values inside pushState can contain { "
|
|
1665
|
+
'Values inside pushState can contain { "$state": "/statePath" } references to read current state (e.g. the text from an input field).'
|
|
1369
1666
|
);
|
|
1370
1667
|
lines.push(
|
|
1371
1668
|
'Use "$id" inside a pushState value to auto-generate a unique ID.'
|
|
1372
1669
|
);
|
|
1373
1670
|
lines.push(
|
|
1374
|
-
'Example: on: { "press": { "action": "pushState", "params": { "
|
|
1671
|
+
'Example: on: { "press": { "action": "pushState", "params": { "statePath": "/todos", "value": { "id": "$id", "title": { "$state": "/newTodoText" }, "completed": false }, "clearStatePath": "/newTodoText" } } }'
|
|
1375
1672
|
);
|
|
1376
1673
|
lines.push(
|
|
1377
|
-
`Use action "removeState" to remove items from arrays by index. Params: {
|
|
1674
|
+
`Use action "removeState" to remove items from arrays by index. Params: { statePath: "/arrayPath", index: N }. Inside a repeated element's children, use { "$index": true } for the current item index. Action params support the same expressions as props: { "$item": "field" } resolves to the absolute state path, { "$index": true } resolves to the index number, and { "$state": "/path" } reads a value from state.`
|
|
1378
1675
|
);
|
|
1379
1676
|
lines.push(
|
|
1380
1677
|
"For lists where users can add/remove items (todos, carts, etc.), use pushState and removeState instead of hardcoding with setState."
|
|
@@ -1417,11 +1714,11 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|
|
1417
1714
|
lines.push("");
|
|
1418
1715
|
lines.push("Example:");
|
|
1419
1716
|
lines.push(
|
|
1420
|
-
` ${JSON.stringify({ type: comp1, props: comp1Props, on: { press: { action: "setState", params: {
|
|
1717
|
+
` ${JSON.stringify({ type: comp1, props: comp1Props, on: { press: { action: "setState", params: { statePath: "/saved", value: true } } }, children: [] })}`
|
|
1421
1718
|
);
|
|
1422
1719
|
lines.push("");
|
|
1423
1720
|
lines.push(
|
|
1424
|
-
'Action params can use dynamic
|
|
1721
|
+
'Action params can use dynamic references to read from state: { "$state": "/statePath" }.'
|
|
1425
1722
|
);
|
|
1426
1723
|
lines.push(
|
|
1427
1724
|
"IMPORTANT: Do NOT put action/actionParams inside props. Always use the `on` field for event bindings."
|
|
@@ -1429,20 +1726,38 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|
|
1429
1726
|
lines.push("");
|
|
1430
1727
|
lines.push("VISIBILITY CONDITIONS:");
|
|
1431
1728
|
lines.push(
|
|
1432
|
-
"Elements can have an optional `visible` field to conditionally show/hide based on
|
|
1729
|
+
"Elements can have an optional `visible` field to conditionally show/hide based on state. IMPORTANT: `visible` is a top-level field on the element object (sibling of type/props/children), NOT inside props."
|
|
1433
1730
|
);
|
|
1434
1731
|
lines.push(
|
|
1435
|
-
`Correct: ${JSON.stringify({ type: comp1, props: comp1Props, visible: {
|
|
1732
|
+
`Correct: ${JSON.stringify({ type: comp1, props: comp1Props, visible: { $state: "/activeTab", eq: "home" }, children: ["..."] })}`
|
|
1436
1733
|
);
|
|
1437
1734
|
lines.push(
|
|
1438
|
-
'- `{ "
|
|
1735
|
+
'- `{ "$state": "/path" }` - visible when state at path is truthy'
|
|
1439
1736
|
);
|
|
1440
1737
|
lines.push(
|
|
1441
|
-
'- `{ "
|
|
1738
|
+
'- `{ "$state": "/path", "not": true }` - visible when state at path is falsy'
|
|
1442
1739
|
);
|
|
1443
|
-
lines.push('- `{ "path": "/statePath" }` - visible when path is truthy');
|
|
1444
1740
|
lines.push(
|
|
1445
|
-
'- `{ "
|
|
1741
|
+
'- `{ "$state": "/path", "eq": "value" }` - visible when state equals value'
|
|
1742
|
+
);
|
|
1743
|
+
lines.push(
|
|
1744
|
+
'- `{ "$state": "/path", "neq": "value" }` - visible when state does not equal value'
|
|
1745
|
+
);
|
|
1746
|
+
lines.push(
|
|
1747
|
+
'- `{ "$state": "/path", "gt": N }` / `gte` / `lt` / `lte` - numeric comparisons'
|
|
1748
|
+
);
|
|
1749
|
+
lines.push(
|
|
1750
|
+
"- Use ONE operator per condition (eq, neq, gt, gte, lt, lte). Do not combine multiple operators."
|
|
1751
|
+
);
|
|
1752
|
+
lines.push('- Any condition can add `"not": true` to invert its result');
|
|
1753
|
+
lines.push(
|
|
1754
|
+
"- `[condition, condition]` - all conditions must be true (implicit AND)"
|
|
1755
|
+
);
|
|
1756
|
+
lines.push(
|
|
1757
|
+
'- `{ "$and": [condition, condition] }` - explicit AND (use when nesting inside $or)'
|
|
1758
|
+
);
|
|
1759
|
+
lines.push(
|
|
1760
|
+
'- `{ "$or": [condition, condition] }` - at least one must be true (OR)'
|
|
1446
1761
|
);
|
|
1447
1762
|
lines.push("- `true` / `false` - always visible/hidden");
|
|
1448
1763
|
lines.push("");
|
|
@@ -1450,41 +1765,102 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|
|
1450
1765
|
"Use a component with on.press bound to setState to update state and drive visibility."
|
|
1451
1766
|
);
|
|
1452
1767
|
lines.push(
|
|
1453
|
-
`Example: A ${comp1} with on: { "press": { "action": "setState", "params": { "
|
|
1768
|
+
`Example: A ${comp1} with on: { "press": { "action": "setState", "params": { "statePath": "/activeTab", "value": "home" } } } sets state, then a container with visible: { "$state": "/activeTab", "eq": "home" } shows only when that tab is active.`
|
|
1769
|
+
);
|
|
1770
|
+
lines.push("");
|
|
1771
|
+
lines.push(
|
|
1772
|
+
'For tab patterns where the first/default tab should be visible when no tab is selected yet, use $or to handle both cases: visible: { "$or": [{ "$state": "/activeTab", "eq": "home" }, { "$state": "/activeTab", "not": true }] }. This ensures the first tab is visible both when explicitly selected AND when /activeTab is not yet set.'
|
|
1454
1773
|
);
|
|
1455
1774
|
lines.push("");
|
|
1456
1775
|
lines.push("DYNAMIC PROPS:");
|
|
1457
1776
|
lines.push(
|
|
1458
|
-
"Any prop value can be a dynamic expression that resolves based on state.
|
|
1777
|
+
"Any prop value can be a dynamic expression that resolves based on state. Three forms are supported:"
|
|
1459
1778
|
);
|
|
1460
1779
|
lines.push("");
|
|
1461
1780
|
lines.push(
|
|
1462
|
-
'1.
|
|
1781
|
+
'1. Read-only state: `{ "$state": "/statePath" }` - resolves to the value at that state path (one-way read).'
|
|
1463
1782
|
);
|
|
1464
1783
|
lines.push(
|
|
1465
|
-
' Example: `"color": { "$
|
|
1784
|
+
' Example: `"color": { "$state": "/theme/primary" }` reads the color from state.'
|
|
1466
1785
|
);
|
|
1467
1786
|
lines.push("");
|
|
1468
1787
|
lines.push(
|
|
1469
|
-
'2.
|
|
1788
|
+
'2. Two-way binding: `{ "$bindState": "/statePath" }` - resolves to the value at the state path AND enables write-back. Use on form input props (value, checked, pressed, etc.).'
|
|
1470
1789
|
);
|
|
1471
1790
|
lines.push(
|
|
1472
|
-
' Example: `"
|
|
1791
|
+
' Example: `"value": { "$bindState": "/form/email" }` binds the input value to /form/email.'
|
|
1473
1792
|
);
|
|
1474
1793
|
lines.push(
|
|
1475
|
-
'
|
|
1794
|
+
' Inside repeat scopes: `"checked": { "$bindItem": "completed" }` binds to the current item\'s completed field.'
|
|
1476
1795
|
);
|
|
1477
1796
|
lines.push("");
|
|
1478
1797
|
lines.push(
|
|
1479
|
-
"
|
|
1798
|
+
'3. Conditional: `{ "$cond": <condition>, "$then": <value>, "$else": <value> }` - evaluates the condition (same syntax as visibility conditions) and picks the matching value.'
|
|
1799
|
+
);
|
|
1800
|
+
lines.push(
|
|
1801
|
+
' Example: `"color": { "$cond": { "$state": "/activeTab", "eq": "home" }, "$then": "#007AFF", "$else": "#8E8E93" }`'
|
|
1480
1802
|
);
|
|
1481
1803
|
lines.push("");
|
|
1804
|
+
lines.push(
|
|
1805
|
+
"Use $bindState for form inputs (text fields, checkboxes, selects, sliders, etc.) and $state for read-only data display. Inside repeat scopes, use $bindItem for form inputs bound to the current item. Use dynamic props instead of duplicating elements with opposing visible conditions when only prop values differ."
|
|
1806
|
+
);
|
|
1807
|
+
lines.push("");
|
|
1808
|
+
const hasChecksComponents = allComponents ? Object.entries(allComponents).some(([, def]) => {
|
|
1809
|
+
if (!def.props) return false;
|
|
1810
|
+
const formatted = formatZodType(def.props);
|
|
1811
|
+
return formatted.includes("checks");
|
|
1812
|
+
}) : false;
|
|
1813
|
+
if (hasChecksComponents) {
|
|
1814
|
+
lines.push("VALIDATION:");
|
|
1815
|
+
lines.push(
|
|
1816
|
+
"Form components that accept a `checks` prop support client-side validation."
|
|
1817
|
+
);
|
|
1818
|
+
lines.push(
|
|
1819
|
+
'Each check is an object: { "type": "<name>", "message": "...", "args": { ... } }'
|
|
1820
|
+
);
|
|
1821
|
+
lines.push("");
|
|
1822
|
+
lines.push("Built-in validation types:");
|
|
1823
|
+
lines.push(" - required \u2014 value must be non-empty");
|
|
1824
|
+
lines.push(" - email \u2014 valid email format");
|
|
1825
|
+
lines.push(' - minLength \u2014 minimum string length (args: { "min": N })');
|
|
1826
|
+
lines.push(' - maxLength \u2014 maximum string length (args: { "max": N })');
|
|
1827
|
+
lines.push(' - pattern \u2014 match a regex (args: { "pattern": "regex" })');
|
|
1828
|
+
lines.push(' - min \u2014 minimum numeric value (args: { "min": N })');
|
|
1829
|
+
lines.push(' - max \u2014 maximum numeric value (args: { "max": N })');
|
|
1830
|
+
lines.push(" - numeric \u2014 value must be a number");
|
|
1831
|
+
lines.push(" - url \u2014 valid URL format");
|
|
1832
|
+
lines.push(
|
|
1833
|
+
' - matches \u2014 must equal another field (args: { "other": "value" })'
|
|
1834
|
+
);
|
|
1835
|
+
lines.push("");
|
|
1836
|
+
lines.push("Example:");
|
|
1837
|
+
lines.push(
|
|
1838
|
+
' "checks": [{ "type": "required", "message": "Email is required" }, { "type": "email", "message": "Invalid email" }]'
|
|
1839
|
+
);
|
|
1840
|
+
lines.push("");
|
|
1841
|
+
lines.push(
|
|
1842
|
+
"IMPORTANT: When using checks, the component must also have a { $bindState } or { $bindItem } on its value/checked prop for two-way binding."
|
|
1843
|
+
);
|
|
1844
|
+
lines.push(
|
|
1845
|
+
"Always include validation checks on form inputs for a good user experience (e.g. required, email, minLength)."
|
|
1846
|
+
);
|
|
1847
|
+
lines.push("");
|
|
1848
|
+
}
|
|
1482
1849
|
lines.push("RULES:");
|
|
1483
|
-
const baseRules = [
|
|
1850
|
+
const baseRules = mode === "chat" ? [
|
|
1851
|
+
"When generating UI, wrap all JSONL patches in a ```spec code fence - one JSON object per line inside the fence",
|
|
1852
|
+
"Write a brief conversational response before any JSONL output",
|
|
1853
|
+
'First set root: {"op":"add","path":"/root","value":"<root-key>"}',
|
|
1854
|
+
'Then add each element: {"op":"add","path":"/elements/<key>","value":{...}}',
|
|
1855
|
+
"Output /state patches right after the elements that use them, one per array item for progressive loading. REQUIRED whenever using $state, $bindState, $bindItem, $item, $index, or repeat.",
|
|
1856
|
+
"ONLY use components listed above",
|
|
1857
|
+
"Each element value needs: type, props, children (array of child keys)",
|
|
1858
|
+
"Use unique keys for the element map entries (e.g., 'header', 'metric-1', 'chart-revenue')"
|
|
1859
|
+
] : [
|
|
1484
1860
|
"Output ONLY JSONL patches - one JSON object per line, no markdown, no code fences",
|
|
1485
1861
|
'First set root: {"op":"add","path":"/root","value":"<root-key>"}',
|
|
1486
1862
|
'Then add each element: {"op":"add","path":"/elements/<key>","value":{...}}',
|
|
1487
|
-
"Output /state patches right after the elements that use them, one per array item for progressive loading. REQUIRED whenever using $
|
|
1863
|
+
"Output /state patches right after the elements that use them, one per array item for progressive loading. REQUIRED whenever using $state, $bindState, $bindItem, $item, $index, or repeat.",
|
|
1488
1864
|
"ONLY use components listed above",
|
|
1489
1865
|
"Each element value needs: type, props, children (array of child keys)",
|
|
1490
1866
|
"Use unique keys for the element map entries (e.g., 'header', 'metric-1', 'chart-revenue')"
|
|
@@ -1772,285 +2148,6 @@ Remember: Output /root first, then interleave /elements and /state patches so th
|
|
|
1772
2148
|
);
|
|
1773
2149
|
return parts.join("\n");
|
|
1774
2150
|
}
|
|
1775
|
-
|
|
1776
|
-
// src/catalog.ts
|
|
1777
|
-
var import_zod6 = require("zod");
|
|
1778
|
-
function createCatalog(config) {
|
|
1779
|
-
const {
|
|
1780
|
-
name = "unnamed",
|
|
1781
|
-
components,
|
|
1782
|
-
actions = {},
|
|
1783
|
-
functions = {},
|
|
1784
|
-
validation = "strict"
|
|
1785
|
-
} = config;
|
|
1786
|
-
const componentNames = Object.keys(components);
|
|
1787
|
-
const actionNames = Object.keys(actions);
|
|
1788
|
-
const functionNames = Object.keys(functions);
|
|
1789
|
-
const componentSchemas = componentNames.map((componentName) => {
|
|
1790
|
-
const def = components[componentName];
|
|
1791
|
-
return import_zod6.z.object({
|
|
1792
|
-
type: import_zod6.z.literal(componentName),
|
|
1793
|
-
props: def.props,
|
|
1794
|
-
children: import_zod6.z.array(import_zod6.z.string()).optional(),
|
|
1795
|
-
visible: VisibilityConditionSchema.optional()
|
|
1796
|
-
});
|
|
1797
|
-
});
|
|
1798
|
-
let elementSchema;
|
|
1799
|
-
if (componentSchemas.length === 0) {
|
|
1800
|
-
elementSchema = import_zod6.z.object({
|
|
1801
|
-
type: import_zod6.z.string(),
|
|
1802
|
-
props: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.unknown()),
|
|
1803
|
-
children: import_zod6.z.array(import_zod6.z.string()).optional(),
|
|
1804
|
-
visible: VisibilityConditionSchema.optional()
|
|
1805
|
-
});
|
|
1806
|
-
} else if (componentSchemas.length === 1) {
|
|
1807
|
-
elementSchema = componentSchemas[0];
|
|
1808
|
-
} else {
|
|
1809
|
-
elementSchema = import_zod6.z.discriminatedUnion("type", [
|
|
1810
|
-
componentSchemas[0],
|
|
1811
|
-
componentSchemas[1],
|
|
1812
|
-
...componentSchemas.slice(2)
|
|
1813
|
-
]);
|
|
1814
|
-
}
|
|
1815
|
-
const specSchema = import_zod6.z.object({
|
|
1816
|
-
root: import_zod6.z.string(),
|
|
1817
|
-
elements: import_zod6.z.record(import_zod6.z.string(), elementSchema)
|
|
1818
|
-
});
|
|
1819
|
-
return {
|
|
1820
|
-
name,
|
|
1821
|
-
componentNames,
|
|
1822
|
-
actionNames,
|
|
1823
|
-
functionNames,
|
|
1824
|
-
validation,
|
|
1825
|
-
components,
|
|
1826
|
-
actions,
|
|
1827
|
-
functions,
|
|
1828
|
-
elementSchema,
|
|
1829
|
-
specSchema,
|
|
1830
|
-
hasComponent(type) {
|
|
1831
|
-
return type in components;
|
|
1832
|
-
},
|
|
1833
|
-
hasAction(name2) {
|
|
1834
|
-
return name2 in actions;
|
|
1835
|
-
},
|
|
1836
|
-
hasFunction(name2) {
|
|
1837
|
-
return name2 in functions;
|
|
1838
|
-
},
|
|
1839
|
-
validateElement(element) {
|
|
1840
|
-
const result = elementSchema.safeParse(element);
|
|
1841
|
-
if (result.success) {
|
|
1842
|
-
return { success: true, data: result.data };
|
|
1843
|
-
}
|
|
1844
|
-
return { success: false, error: result.error };
|
|
1845
|
-
},
|
|
1846
|
-
validateSpec(spec) {
|
|
1847
|
-
const result = specSchema.safeParse(spec);
|
|
1848
|
-
if (result.success) {
|
|
1849
|
-
return { success: true, data: result.data };
|
|
1850
|
-
}
|
|
1851
|
-
return { success: false, error: result.error };
|
|
1852
|
-
}
|
|
1853
|
-
};
|
|
1854
|
-
}
|
|
1855
|
-
function generateCatalogPrompt(catalog) {
|
|
1856
|
-
const lines = [
|
|
1857
|
-
`# ${catalog.name} Component Catalog`,
|
|
1858
|
-
"",
|
|
1859
|
-
"## Available Components",
|
|
1860
|
-
""
|
|
1861
|
-
];
|
|
1862
|
-
for (const name of catalog.componentNames) {
|
|
1863
|
-
const def = catalog.components[name];
|
|
1864
|
-
lines.push(`### ${String(name)}`);
|
|
1865
|
-
if (def.description) {
|
|
1866
|
-
lines.push(def.description);
|
|
1867
|
-
}
|
|
1868
|
-
lines.push("");
|
|
1869
|
-
}
|
|
1870
|
-
if (catalog.actionNames.length > 0) {
|
|
1871
|
-
lines.push("## Available Actions");
|
|
1872
|
-
lines.push("");
|
|
1873
|
-
for (const name of catalog.actionNames) {
|
|
1874
|
-
const def = catalog.actions[name];
|
|
1875
|
-
lines.push(
|
|
1876
|
-
`- \`${String(name)}\`${def.description ? `: ${def.description}` : ""}`
|
|
1877
|
-
);
|
|
1878
|
-
}
|
|
1879
|
-
lines.push("");
|
|
1880
|
-
}
|
|
1881
|
-
lines.push("## Visibility Conditions");
|
|
1882
|
-
lines.push("");
|
|
1883
|
-
lines.push("Components can have a `visible` property:");
|
|
1884
|
-
lines.push("- `true` / `false` - Always visible/hidden");
|
|
1885
|
-
lines.push('- `{ "path": "/state/path" }` - Visible when path is truthy');
|
|
1886
|
-
lines.push('- `{ "auth": "signedIn" }` - Visible when user is signed in');
|
|
1887
|
-
lines.push('- `{ "and": [...] }` - All conditions must be true');
|
|
1888
|
-
lines.push('- `{ "or": [...] }` - Any condition must be true');
|
|
1889
|
-
lines.push('- `{ "not": {...} }` - Negates a condition');
|
|
1890
|
-
lines.push('- `{ "eq": [a, b] }` - Equality check');
|
|
1891
|
-
lines.push("");
|
|
1892
|
-
lines.push("## Validation Functions");
|
|
1893
|
-
lines.push("");
|
|
1894
|
-
lines.push(
|
|
1895
|
-
"Built-in: `required`, `email`, `minLength`, `maxLength`, `pattern`, `min`, `max`, `url`"
|
|
1896
|
-
);
|
|
1897
|
-
if (catalog.functionNames.length > 0) {
|
|
1898
|
-
lines.push(`Custom: ${catalog.functionNames.map(String).join(", ")}`);
|
|
1899
|
-
}
|
|
1900
|
-
lines.push("");
|
|
1901
|
-
return lines.join("\n");
|
|
1902
|
-
}
|
|
1903
|
-
function formatZodType2(schema, isOptional = false) {
|
|
1904
|
-
const def = schema._def;
|
|
1905
|
-
const typeName = def.typeName ?? "";
|
|
1906
|
-
let result;
|
|
1907
|
-
switch (typeName) {
|
|
1908
|
-
case "ZodString":
|
|
1909
|
-
result = "string";
|
|
1910
|
-
break;
|
|
1911
|
-
case "ZodNumber":
|
|
1912
|
-
result = "number";
|
|
1913
|
-
break;
|
|
1914
|
-
case "ZodBoolean":
|
|
1915
|
-
result = "boolean";
|
|
1916
|
-
break;
|
|
1917
|
-
case "ZodLiteral":
|
|
1918
|
-
result = JSON.stringify(def.value);
|
|
1919
|
-
break;
|
|
1920
|
-
case "ZodEnum":
|
|
1921
|
-
result = def.values.map((v) => `"${v}"`).join("|");
|
|
1922
|
-
break;
|
|
1923
|
-
case "ZodNativeEnum":
|
|
1924
|
-
result = Object.values(def.values).map((v) => `"${v}"`).join("|");
|
|
1925
|
-
break;
|
|
1926
|
-
case "ZodArray":
|
|
1927
|
-
result = def.type ? `Array<${formatZodType2(def.type)}>` : "Array<unknown>";
|
|
1928
|
-
break;
|
|
1929
|
-
case "ZodObject": {
|
|
1930
|
-
if (!def.shape) {
|
|
1931
|
-
result = "object";
|
|
1932
|
-
break;
|
|
1933
|
-
}
|
|
1934
|
-
const shape = def.shape();
|
|
1935
|
-
const props = Object.entries(shape).map(([key, value]) => {
|
|
1936
|
-
const innerDef = value._def;
|
|
1937
|
-
const innerOptional = innerDef.typeName === "ZodOptional" || innerDef.typeName === "ZodNullable";
|
|
1938
|
-
return `${key}${innerOptional ? "?" : ""}: ${formatZodType2(value)}`;
|
|
1939
|
-
}).join(", ");
|
|
1940
|
-
result = `{ ${props} }`;
|
|
1941
|
-
break;
|
|
1942
|
-
}
|
|
1943
|
-
case "ZodOptional":
|
|
1944
|
-
return def.innerType ? formatZodType2(def.innerType, true) : "unknown?";
|
|
1945
|
-
case "ZodNullable":
|
|
1946
|
-
return def.innerType ? formatZodType2(def.innerType, true) : "unknown?";
|
|
1947
|
-
case "ZodDefault":
|
|
1948
|
-
return def.innerType ? formatZodType2(def.innerType, isOptional) : "unknown";
|
|
1949
|
-
case "ZodUnion":
|
|
1950
|
-
result = def.options ? def.options.map((opt) => formatZodType2(opt)).join("|") : "unknown";
|
|
1951
|
-
break;
|
|
1952
|
-
case "ZodNull":
|
|
1953
|
-
result = "null";
|
|
1954
|
-
break;
|
|
1955
|
-
case "ZodUndefined":
|
|
1956
|
-
result = "undefined";
|
|
1957
|
-
break;
|
|
1958
|
-
case "ZodAny":
|
|
1959
|
-
result = "any";
|
|
1960
|
-
break;
|
|
1961
|
-
case "ZodUnknown":
|
|
1962
|
-
result = "unknown";
|
|
1963
|
-
break;
|
|
1964
|
-
default:
|
|
1965
|
-
result = "unknown";
|
|
1966
|
-
}
|
|
1967
|
-
return isOptional ? `${result}?` : result;
|
|
1968
|
-
}
|
|
1969
|
-
function extractPropsFromSchema(schema) {
|
|
1970
|
-
const def = schema._def;
|
|
1971
|
-
const typeName = def.typeName ?? "";
|
|
1972
|
-
if (typeName !== "ZodObject" || !def.shape) {
|
|
1973
|
-
return [];
|
|
1974
|
-
}
|
|
1975
|
-
const shape = def.shape();
|
|
1976
|
-
return Object.entries(shape).map(([name, value]) => {
|
|
1977
|
-
const innerDef = value._def;
|
|
1978
|
-
const optional = innerDef.typeName === "ZodOptional" || innerDef.typeName === "ZodNullable";
|
|
1979
|
-
return {
|
|
1980
|
-
name,
|
|
1981
|
-
type: formatZodType2(value),
|
|
1982
|
-
optional
|
|
1983
|
-
};
|
|
1984
|
-
});
|
|
1985
|
-
}
|
|
1986
|
-
function formatPropsCompact(props) {
|
|
1987
|
-
if (props.length === 0) return "{}";
|
|
1988
|
-
const entries = props.map(
|
|
1989
|
-
(p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`
|
|
1990
|
-
);
|
|
1991
|
-
return `{ ${entries.join(", ")} }`;
|
|
1992
|
-
}
|
|
1993
|
-
function generateSystemPrompt(catalog, options = {}) {
|
|
1994
|
-
const {
|
|
1995
|
-
system = "You are a UI generator that outputs JSONL (JSON Lines) patches.",
|
|
1996
|
-
customRules = []
|
|
1997
|
-
} = options;
|
|
1998
|
-
const lines = [];
|
|
1999
|
-
lines.push(system);
|
|
2000
|
-
lines.push("");
|
|
2001
|
-
const componentCount = catalog.componentNames.length;
|
|
2002
|
-
lines.push(`AVAILABLE COMPONENTS (${componentCount}):`);
|
|
2003
|
-
lines.push("");
|
|
2004
|
-
for (const name of catalog.componentNames) {
|
|
2005
|
-
const def = catalog.components[name];
|
|
2006
|
-
const props = extractPropsFromSchema(def.props);
|
|
2007
|
-
const propsStr = formatPropsCompact(props);
|
|
2008
|
-
const hasChildrenStr = def.hasChildren ? " Has children." : "";
|
|
2009
|
-
const descStr = def.description ? ` ${def.description}` : "";
|
|
2010
|
-
lines.push(`- ${String(name)}: ${propsStr}${descStr}${hasChildrenStr}`);
|
|
2011
|
-
}
|
|
2012
|
-
lines.push("");
|
|
2013
|
-
if (catalog.actionNames.length > 0) {
|
|
2014
|
-
lines.push("AVAILABLE ACTIONS:");
|
|
2015
|
-
lines.push("");
|
|
2016
|
-
for (const name of catalog.actionNames) {
|
|
2017
|
-
const def = catalog.actions[name];
|
|
2018
|
-
lines.push(
|
|
2019
|
-
`- ${String(name)}${def.description ? `: ${def.description}` : ""}`
|
|
2020
|
-
);
|
|
2021
|
-
}
|
|
2022
|
-
lines.push("");
|
|
2023
|
-
}
|
|
2024
|
-
lines.push("OUTPUT FORMAT (JSONL, RFC 6902 JSON Patch):");
|
|
2025
|
-
lines.push('{"op":"add","path":"/root","value":"element-key"}');
|
|
2026
|
-
lines.push(
|
|
2027
|
-
'{"op":"add","path":"/elements/key","value":{"type":"...","props":{...},"children":[...]}}'
|
|
2028
|
-
);
|
|
2029
|
-
lines.push('{"op":"remove","path":"/elements/key"}');
|
|
2030
|
-
lines.push("");
|
|
2031
|
-
lines.push("RULES:");
|
|
2032
|
-
const baseRules = [
|
|
2033
|
-
'First line sets /root to root element key: {"op":"add","path":"/root","value":"<key>"}',
|
|
2034
|
-
'Add elements with /elements/{key}: {"op":"add","path":"/elements/<key>","value":{...}}',
|
|
2035
|
-
"Remove elements with op:remove - also update the parent's children array to exclude the removed key",
|
|
2036
|
-
"Children array contains string keys, not objects",
|
|
2037
|
-
"Parent first, then children",
|
|
2038
|
-
"Each element needs: type, props",
|
|
2039
|
-
"ONLY use props listed above - never invent new props"
|
|
2040
|
-
];
|
|
2041
|
-
const allRules = [...baseRules, ...customRules];
|
|
2042
|
-
allRules.forEach((rule, i) => {
|
|
2043
|
-
lines.push(`${i + 1}. ${rule}`);
|
|
2044
|
-
});
|
|
2045
|
-
lines.push("");
|
|
2046
|
-
if (catalog.functionNames.length > 0) {
|
|
2047
|
-
lines.push("CUSTOM VALIDATION FUNCTIONS:");
|
|
2048
|
-
lines.push(catalog.functionNames.map(String).join(", "));
|
|
2049
|
-
lines.push("");
|
|
2050
|
-
}
|
|
2051
|
-
lines.push("Generate JSONL:");
|
|
2052
|
-
return lines.join("\n");
|
|
2053
|
-
}
|
|
2054
2151
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2055
2152
|
0 && (module.exports = {
|
|
2056
2153
|
ActionBindingSchema,
|
|
@@ -2062,35 +2159,39 @@ function generateSystemPrompt(catalog, options = {}) {
|
|
|
2062
2159
|
DynamicNumberSchema,
|
|
2063
2160
|
DynamicStringSchema,
|
|
2064
2161
|
DynamicValueSchema,
|
|
2065
|
-
|
|
2162
|
+
SPEC_DATA_PART,
|
|
2163
|
+
SPEC_DATA_PART_TYPE,
|
|
2066
2164
|
ValidationCheckSchema,
|
|
2067
2165
|
ValidationConfigSchema,
|
|
2068
2166
|
VisibilityConditionSchema,
|
|
2069
2167
|
action,
|
|
2070
2168
|
actionBinding,
|
|
2071
2169
|
addByPath,
|
|
2170
|
+
applySpecPatch,
|
|
2072
2171
|
applySpecStreamPatch,
|
|
2073
2172
|
autoFixSpec,
|
|
2074
2173
|
buildUserPrompt,
|
|
2075
2174
|
builtInValidationFunctions,
|
|
2076
2175
|
check,
|
|
2077
2176
|
compileSpecStream,
|
|
2078
|
-
|
|
2177
|
+
createJsonRenderTransform,
|
|
2178
|
+
createMixedStreamParser,
|
|
2079
2179
|
createSpecStreamCompiler,
|
|
2080
2180
|
defineCatalog,
|
|
2081
2181
|
defineSchema,
|
|
2082
|
-
evaluateLogicExpression,
|
|
2083
2182
|
evaluateVisibility,
|
|
2084
2183
|
executeAction,
|
|
2085
2184
|
findFormValue,
|
|
2086
2185
|
formatSpecIssues,
|
|
2087
|
-
generateCatalogPrompt,
|
|
2088
|
-
generateSystemPrompt,
|
|
2089
2186
|
getByPath,
|
|
2090
2187
|
interpolateString,
|
|
2188
|
+
nestedToFlat,
|
|
2091
2189
|
parseSpecStreamLine,
|
|
2190
|
+
pipeJsonRender,
|
|
2092
2191
|
removeByPath,
|
|
2093
2192
|
resolveAction,
|
|
2193
|
+
resolveActionParam,
|
|
2194
|
+
resolveBindings,
|
|
2094
2195
|
resolveDynamicValue,
|
|
2095
2196
|
resolveElementProps,
|
|
2096
2197
|
resolvePropValue,
|