@json-render/core 0.5.2 → 0.6.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 +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.mjs
CHANGED
|
@@ -5,26 +5,26 @@ var DynamicValueSchema = z.union([
|
|
|
5
5
|
z.number(),
|
|
6
6
|
z.boolean(),
|
|
7
7
|
z.null(),
|
|
8
|
-
z.object({
|
|
8
|
+
z.object({ $state: z.string() })
|
|
9
9
|
]);
|
|
10
10
|
var DynamicStringSchema = z.union([
|
|
11
11
|
z.string(),
|
|
12
|
-
z.object({
|
|
12
|
+
z.object({ $state: z.string() })
|
|
13
13
|
]);
|
|
14
14
|
var DynamicNumberSchema = z.union([
|
|
15
15
|
z.number(),
|
|
16
|
-
z.object({
|
|
16
|
+
z.object({ $state: z.string() })
|
|
17
17
|
]);
|
|
18
18
|
var DynamicBooleanSchema = z.union([
|
|
19
19
|
z.boolean(),
|
|
20
|
-
z.object({
|
|
20
|
+
z.object({ $state: z.string() })
|
|
21
21
|
]);
|
|
22
22
|
function resolveDynamicValue(value, stateModel) {
|
|
23
23
|
if (value === null || value === void 0) {
|
|
24
24
|
return void 0;
|
|
25
25
|
}
|
|
26
|
-
if (typeof value === "object" && "
|
|
27
|
-
return getByPath(stateModel, value
|
|
26
|
+
if (typeof value === "object" && "$state" in value) {
|
|
27
|
+
return getByPath(stateModel, value.$state);
|
|
28
28
|
}
|
|
29
29
|
return value;
|
|
30
30
|
}
|
|
@@ -171,7 +171,7 @@ function deepEqual(a, b) {
|
|
|
171
171
|
if (aKeys.length !== bKeys.length) return false;
|
|
172
172
|
return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));
|
|
173
173
|
}
|
|
174
|
-
function findFormValue(fieldName, params,
|
|
174
|
+
function findFormValue(fieldName, params, state) {
|
|
175
175
|
if (params?.[fieldName] !== void 0) {
|
|
176
176
|
const val = params[fieldName];
|
|
177
177
|
if (typeof val !== "string" || !val.includes(".")) {
|
|
@@ -188,19 +188,15 @@ function findFormValue(fieldName, params, data) {
|
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
|
-
if (
|
|
192
|
-
for (const key of Object.keys(
|
|
191
|
+
if (state) {
|
|
192
|
+
for (const key of Object.keys(state)) {
|
|
193
193
|
if (key === fieldName || key.endsWith(`.${fieldName}`)) {
|
|
194
|
-
return
|
|
194
|
+
return state[key];
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const val = getByPath(data, path);
|
|
201
|
-
if (val !== void 0) {
|
|
202
|
-
return val;
|
|
203
|
-
}
|
|
197
|
+
const val = getByPath(state, fieldName);
|
|
198
|
+
if (val !== void 0) {
|
|
199
|
+
return val;
|
|
204
200
|
}
|
|
205
201
|
}
|
|
206
202
|
return void 0;
|
|
@@ -254,6 +250,44 @@ function applySpecStreamPatch(obj, patch) {
|
|
|
254
250
|
}
|
|
255
251
|
return obj;
|
|
256
252
|
}
|
|
253
|
+
function applySpecPatch(spec, patch) {
|
|
254
|
+
applySpecStreamPatch(spec, patch);
|
|
255
|
+
return spec;
|
|
256
|
+
}
|
|
257
|
+
function nestedToFlat(nested) {
|
|
258
|
+
const elements = {};
|
|
259
|
+
let counter = 0;
|
|
260
|
+
function walk(node) {
|
|
261
|
+
const key = `el-${counter++}`;
|
|
262
|
+
const { type, props, children: rawChildren, ...rest } = node;
|
|
263
|
+
const childKeys = [];
|
|
264
|
+
if (Array.isArray(rawChildren)) {
|
|
265
|
+
for (const child of rawChildren) {
|
|
266
|
+
if (child && typeof child === "object" && "type" in child) {
|
|
267
|
+
childKeys.push(walk(child));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const element = {
|
|
272
|
+
type: type ?? "unknown",
|
|
273
|
+
props: props ?? {},
|
|
274
|
+
children: childKeys
|
|
275
|
+
};
|
|
276
|
+
for (const [k, v] of Object.entries(rest)) {
|
|
277
|
+
if (k !== "state" && v !== void 0) {
|
|
278
|
+
element[k] = v;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
elements[key] = element;
|
|
282
|
+
return key;
|
|
283
|
+
}
|
|
284
|
+
const root = walk(nested);
|
|
285
|
+
const spec = { root, elements };
|
|
286
|
+
if (nested.state && typeof nested.state === "object" && !Array.isArray(nested.state)) {
|
|
287
|
+
spec.state = nested.state;
|
|
288
|
+
}
|
|
289
|
+
return spec;
|
|
290
|
+
}
|
|
257
291
|
function compileSpecStream(stream, initial = {}) {
|
|
258
292
|
const lines = stream.split("\n");
|
|
259
293
|
const result = { ...initial };
|
|
@@ -316,129 +350,304 @@ function createSpecStreamCompiler(initial = {}) {
|
|
|
316
350
|
}
|
|
317
351
|
};
|
|
318
352
|
}
|
|
353
|
+
function createMixedStreamParser(callbacks) {
|
|
354
|
+
let buffer = "";
|
|
355
|
+
let inSpecFence = false;
|
|
356
|
+
function processLine(line) {
|
|
357
|
+
const trimmed = line.trim();
|
|
358
|
+
if (!inSpecFence && trimmed.startsWith("```spec")) {
|
|
359
|
+
inSpecFence = true;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (inSpecFence && trimmed === "```") {
|
|
363
|
+
inSpecFence = false;
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (!trimmed) return;
|
|
367
|
+
if (inSpecFence) {
|
|
368
|
+
const patch2 = parseSpecStreamLine(trimmed);
|
|
369
|
+
if (patch2) {
|
|
370
|
+
callbacks.onPatch(patch2);
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const patch = parseSpecStreamLine(trimmed);
|
|
375
|
+
if (patch) {
|
|
376
|
+
callbacks.onPatch(patch);
|
|
377
|
+
} else {
|
|
378
|
+
callbacks.onText(line);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
push(chunk) {
|
|
383
|
+
buffer += chunk;
|
|
384
|
+
const lines = buffer.split("\n");
|
|
385
|
+
buffer = lines.pop() || "";
|
|
386
|
+
for (const line of lines) {
|
|
387
|
+
processLine(line);
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
flush() {
|
|
391
|
+
if (buffer.trim()) {
|
|
392
|
+
processLine(buffer);
|
|
393
|
+
}
|
|
394
|
+
buffer = "";
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
var SPEC_FENCE_OPEN = "```spec";
|
|
399
|
+
var SPEC_FENCE_CLOSE = "```";
|
|
400
|
+
function createJsonRenderTransform() {
|
|
401
|
+
let lineBuffer = "";
|
|
402
|
+
let currentTextId = "";
|
|
403
|
+
let buffering = false;
|
|
404
|
+
let inSpecFence = false;
|
|
405
|
+
function emitPatch(patch, controller) {
|
|
406
|
+
controller.enqueue({
|
|
407
|
+
type: SPEC_DATA_PART_TYPE,
|
|
408
|
+
data: { type: "patch", patch }
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
function flushBuffer(controller) {
|
|
412
|
+
if (!lineBuffer) return;
|
|
413
|
+
const trimmed = lineBuffer.trim();
|
|
414
|
+
if (inSpecFence) {
|
|
415
|
+
if (trimmed) {
|
|
416
|
+
const patch = parseSpecStreamLine(trimmed);
|
|
417
|
+
if (patch) emitPatch(patch, controller);
|
|
418
|
+
}
|
|
419
|
+
lineBuffer = "";
|
|
420
|
+
buffering = false;
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (trimmed) {
|
|
424
|
+
const patch = parseSpecStreamLine(trimmed);
|
|
425
|
+
if (patch) {
|
|
426
|
+
emitPatch(patch, controller);
|
|
427
|
+
} else {
|
|
428
|
+
controller.enqueue({
|
|
429
|
+
type: "text-delta",
|
|
430
|
+
id: currentTextId,
|
|
431
|
+
delta: lineBuffer
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
controller.enqueue({
|
|
436
|
+
type: "text-delta",
|
|
437
|
+
id: currentTextId,
|
|
438
|
+
delta: lineBuffer
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
lineBuffer = "";
|
|
442
|
+
buffering = false;
|
|
443
|
+
}
|
|
444
|
+
function processCompleteLine(line, controller) {
|
|
445
|
+
const trimmed = line.trim();
|
|
446
|
+
if (!inSpecFence && trimmed.startsWith(SPEC_FENCE_OPEN)) {
|
|
447
|
+
inSpecFence = true;
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (inSpecFence && trimmed === SPEC_FENCE_CLOSE) {
|
|
451
|
+
inSpecFence = false;
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (inSpecFence) {
|
|
455
|
+
if (trimmed) {
|
|
456
|
+
const patch2 = parseSpecStreamLine(trimmed);
|
|
457
|
+
if (patch2) emitPatch(patch2, controller);
|
|
458
|
+
}
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (!trimmed) {
|
|
462
|
+
controller.enqueue({
|
|
463
|
+
type: "text-delta",
|
|
464
|
+
id: currentTextId,
|
|
465
|
+
delta: "\n"
|
|
466
|
+
});
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const patch = parseSpecStreamLine(trimmed);
|
|
470
|
+
if (patch) {
|
|
471
|
+
emitPatch(patch, controller);
|
|
472
|
+
} else {
|
|
473
|
+
controller.enqueue({
|
|
474
|
+
type: "text-delta",
|
|
475
|
+
id: currentTextId,
|
|
476
|
+
delta: line + "\n"
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return new TransformStream({
|
|
481
|
+
transform(chunk, controller) {
|
|
482
|
+
switch (chunk.type) {
|
|
483
|
+
case "text-start": {
|
|
484
|
+
currentTextId = chunk.id;
|
|
485
|
+
controller.enqueue(chunk);
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
case "text-delta": {
|
|
489
|
+
const delta = chunk;
|
|
490
|
+
currentTextId = delta.id;
|
|
491
|
+
const text = delta.delta;
|
|
492
|
+
for (let i = 0; i < text.length; i++) {
|
|
493
|
+
const ch = text.charAt(i);
|
|
494
|
+
if (ch === "\n") {
|
|
495
|
+
if (buffering) {
|
|
496
|
+
processCompleteLine(lineBuffer, controller);
|
|
497
|
+
lineBuffer = "";
|
|
498
|
+
buffering = false;
|
|
499
|
+
} else {
|
|
500
|
+
if (!inSpecFence) {
|
|
501
|
+
controller.enqueue({
|
|
502
|
+
type: "text-delta",
|
|
503
|
+
id: currentTextId,
|
|
504
|
+
delta: "\n"
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
} else if (lineBuffer.length === 0 && !buffering) {
|
|
509
|
+
if (inSpecFence || ch === "{" || ch === "`") {
|
|
510
|
+
buffering = true;
|
|
511
|
+
lineBuffer += ch;
|
|
512
|
+
} else {
|
|
513
|
+
controller.enqueue({
|
|
514
|
+
type: "text-delta",
|
|
515
|
+
id: currentTextId,
|
|
516
|
+
delta: ch
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
} else if (buffering) {
|
|
520
|
+
lineBuffer += ch;
|
|
521
|
+
} else {
|
|
522
|
+
controller.enqueue({
|
|
523
|
+
type: "text-delta",
|
|
524
|
+
id: currentTextId,
|
|
525
|
+
delta: ch
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
case "text-end": {
|
|
532
|
+
flushBuffer(controller);
|
|
533
|
+
controller.enqueue(chunk);
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
default: {
|
|
537
|
+
controller.enqueue(chunk);
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
flush(controller) {
|
|
543
|
+
flushBuffer(controller);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
var SPEC_DATA_PART = "spec";
|
|
548
|
+
var SPEC_DATA_PART_TYPE = `data-${SPEC_DATA_PART}`;
|
|
549
|
+
function pipeJsonRender(stream) {
|
|
550
|
+
return stream.pipeThrough(
|
|
551
|
+
createJsonRenderTransform()
|
|
552
|
+
);
|
|
553
|
+
}
|
|
319
554
|
|
|
320
555
|
// src/visibility.ts
|
|
321
556
|
import { z as z2 } from "zod";
|
|
322
|
-
var
|
|
557
|
+
var numericOrStateRef = z2.union([
|
|
323
558
|
z2.number(),
|
|
324
|
-
z2.object({
|
|
559
|
+
z2.object({ $state: z2.string() })
|
|
325
560
|
]);
|
|
326
|
-
var
|
|
561
|
+
var comparisonOps = {
|
|
562
|
+
eq: z2.unknown().optional(),
|
|
563
|
+
neq: z2.unknown().optional(),
|
|
564
|
+
gt: numericOrStateRef.optional(),
|
|
565
|
+
gte: numericOrStateRef.optional(),
|
|
566
|
+
lt: numericOrStateRef.optional(),
|
|
567
|
+
lte: numericOrStateRef.optional(),
|
|
568
|
+
not: z2.literal(true).optional()
|
|
569
|
+
};
|
|
570
|
+
var StateConditionSchema = z2.object({
|
|
571
|
+
$state: z2.string(),
|
|
572
|
+
...comparisonOps
|
|
573
|
+
});
|
|
574
|
+
var ItemConditionSchema = z2.object({
|
|
575
|
+
$item: z2.string(),
|
|
576
|
+
...comparisonOps
|
|
577
|
+
});
|
|
578
|
+
var IndexConditionSchema = z2.object({
|
|
579
|
+
$index: z2.literal(true),
|
|
580
|
+
...comparisonOps
|
|
581
|
+
});
|
|
582
|
+
var SingleConditionSchema = z2.union([
|
|
583
|
+
StateConditionSchema,
|
|
584
|
+
ItemConditionSchema,
|
|
585
|
+
IndexConditionSchema
|
|
586
|
+
]);
|
|
587
|
+
var VisibilityConditionSchema = z2.lazy(
|
|
327
588
|
() => z2.union([
|
|
328
|
-
z2.
|
|
329
|
-
|
|
330
|
-
z2.
|
|
331
|
-
z2.object({
|
|
332
|
-
z2.object({
|
|
333
|
-
z2.object({ neq: z2.tuple([DynamicValueSchema, DynamicValueSchema]) }),
|
|
334
|
-
z2.object({
|
|
335
|
-
gt: z2.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema])
|
|
336
|
-
}),
|
|
337
|
-
z2.object({
|
|
338
|
-
gte: z2.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema])
|
|
339
|
-
}),
|
|
340
|
-
z2.object({
|
|
341
|
-
lt: z2.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema])
|
|
342
|
-
}),
|
|
343
|
-
z2.object({
|
|
344
|
-
lte: z2.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema])
|
|
345
|
-
})
|
|
589
|
+
z2.boolean(),
|
|
590
|
+
SingleConditionSchema,
|
|
591
|
+
z2.array(SingleConditionSchema),
|
|
592
|
+
z2.object({ $and: z2.array(VisibilityConditionSchema) }),
|
|
593
|
+
z2.object({ $or: z2.array(VisibilityConditionSchema) })
|
|
346
594
|
])
|
|
347
595
|
);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
LogicExpressionSchema
|
|
353
|
-
]);
|
|
354
|
-
function evaluateLogicExpression(expr, ctx) {
|
|
355
|
-
const { stateModel } = ctx;
|
|
356
|
-
if ("and" in expr) {
|
|
357
|
-
return expr.and.every((subExpr) => evaluateLogicExpression(subExpr, ctx));
|
|
358
|
-
}
|
|
359
|
-
if ("or" in expr) {
|
|
360
|
-
return expr.or.some((subExpr) => evaluateLogicExpression(subExpr, ctx));
|
|
361
|
-
}
|
|
362
|
-
if ("not" in expr) {
|
|
363
|
-
return !evaluateLogicExpression(expr.not, ctx);
|
|
364
|
-
}
|
|
365
|
-
if ("path" in expr) {
|
|
366
|
-
const value = resolveDynamicValue({ path: expr.path }, stateModel);
|
|
367
|
-
return Boolean(value);
|
|
368
|
-
}
|
|
369
|
-
if ("eq" in expr) {
|
|
370
|
-
const [left, right] = expr.eq;
|
|
371
|
-
const leftValue = resolveDynamicValue(left, stateModel);
|
|
372
|
-
const rightValue = resolveDynamicValue(right, stateModel);
|
|
373
|
-
return leftValue === rightValue;
|
|
374
|
-
}
|
|
375
|
-
if ("neq" in expr) {
|
|
376
|
-
const [left, right] = expr.neq;
|
|
377
|
-
const leftValue = resolveDynamicValue(left, stateModel);
|
|
378
|
-
const rightValue = resolveDynamicValue(right, stateModel);
|
|
379
|
-
return leftValue !== rightValue;
|
|
380
|
-
}
|
|
381
|
-
if ("gt" in expr) {
|
|
382
|
-
const [left, right] = expr.gt;
|
|
383
|
-
const leftValue = resolveDynamicValue(
|
|
384
|
-
left,
|
|
385
|
-
stateModel
|
|
386
|
-
);
|
|
387
|
-
const rightValue = resolveDynamicValue(
|
|
388
|
-
right,
|
|
389
|
-
stateModel
|
|
390
|
-
);
|
|
391
|
-
if (typeof leftValue === "number" && typeof rightValue === "number") {
|
|
392
|
-
return leftValue > rightValue;
|
|
596
|
+
function resolveComparisonValue(value, ctx) {
|
|
597
|
+
if (typeof value === "object" && value !== null) {
|
|
598
|
+
if ("$state" in value && typeof value.$state === "string") {
|
|
599
|
+
return getByPath(ctx.stateModel, value.$state);
|
|
393
600
|
}
|
|
394
|
-
return false;
|
|
395
601
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
return leftValue >= rightValue;
|
|
408
|
-
}
|
|
409
|
-
return false;
|
|
602
|
+
return value;
|
|
603
|
+
}
|
|
604
|
+
function isItemCondition(cond) {
|
|
605
|
+
return "$item" in cond;
|
|
606
|
+
}
|
|
607
|
+
function isIndexCondition(cond) {
|
|
608
|
+
return "$index" in cond;
|
|
609
|
+
}
|
|
610
|
+
function resolveConditionValue(cond, ctx) {
|
|
611
|
+
if (isIndexCondition(cond)) {
|
|
612
|
+
return ctx.repeatIndex;
|
|
410
613
|
}
|
|
411
|
-
if (
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
left,
|
|
415
|
-
stateModel
|
|
416
|
-
);
|
|
417
|
-
const rightValue = resolveDynamicValue(
|
|
418
|
-
right,
|
|
419
|
-
stateModel
|
|
420
|
-
);
|
|
421
|
-
if (typeof leftValue === "number" && typeof rightValue === "number") {
|
|
422
|
-
return leftValue < rightValue;
|
|
423
|
-
}
|
|
424
|
-
return false;
|
|
614
|
+
if (isItemCondition(cond)) {
|
|
615
|
+
if (ctx.repeatItem === void 0) return void 0;
|
|
616
|
+
return cond.$item === "" ? ctx.repeatItem : getByPath(ctx.repeatItem, cond.$item);
|
|
425
617
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
);
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
618
|
+
return getByPath(ctx.stateModel, cond.$state);
|
|
619
|
+
}
|
|
620
|
+
function evaluateCondition(cond, ctx) {
|
|
621
|
+
const value = resolveConditionValue(cond, ctx);
|
|
622
|
+
let result;
|
|
623
|
+
if (cond.eq !== void 0) {
|
|
624
|
+
const rhs = resolveComparisonValue(cond.eq, ctx);
|
|
625
|
+
result = value === rhs;
|
|
626
|
+
} else if (cond.neq !== void 0) {
|
|
627
|
+
const rhs = resolveComparisonValue(cond.neq, ctx);
|
|
628
|
+
result = value !== rhs;
|
|
629
|
+
} else if (cond.gt !== void 0) {
|
|
630
|
+
const rhs = resolveComparisonValue(cond.gt, ctx);
|
|
631
|
+
result = typeof value === "number" && typeof rhs === "number" ? value > rhs : false;
|
|
632
|
+
} else if (cond.gte !== void 0) {
|
|
633
|
+
const rhs = resolveComparisonValue(cond.gte, ctx);
|
|
634
|
+
result = typeof value === "number" && typeof rhs === "number" ? value >= rhs : false;
|
|
635
|
+
} else if (cond.lt !== void 0) {
|
|
636
|
+
const rhs = resolveComparisonValue(cond.lt, ctx);
|
|
637
|
+
result = typeof value === "number" && typeof rhs === "number" ? value < rhs : false;
|
|
638
|
+
} else if (cond.lte !== void 0) {
|
|
639
|
+
const rhs = resolveComparisonValue(cond.lte, ctx);
|
|
640
|
+
result = typeof value === "number" && typeof rhs === "number" ? value <= rhs : false;
|
|
641
|
+
} else {
|
|
642
|
+
result = Boolean(value);
|
|
440
643
|
}
|
|
441
|
-
return
|
|
644
|
+
return cond.not === true ? !result : result;
|
|
645
|
+
}
|
|
646
|
+
function isAndCondition(condition) {
|
|
647
|
+
return typeof condition === "object" && condition !== null && !Array.isArray(condition) && "$and" in condition;
|
|
648
|
+
}
|
|
649
|
+
function isOrCondition(condition) {
|
|
650
|
+
return typeof condition === "object" && condition !== null && !Array.isArray(condition) && "$or" in condition;
|
|
442
651
|
}
|
|
443
652
|
function evaluateVisibility(condition, ctx) {
|
|
444
653
|
if (condition === void 0) {
|
|
@@ -447,74 +656,114 @@ function evaluateVisibility(condition, ctx) {
|
|
|
447
656
|
if (typeof condition === "boolean") {
|
|
448
657
|
return condition;
|
|
449
658
|
}
|
|
450
|
-
if (
|
|
451
|
-
|
|
452
|
-
return Boolean(value);
|
|
659
|
+
if (Array.isArray(condition)) {
|
|
660
|
+
return condition.every((c) => evaluateCondition(c, ctx));
|
|
453
661
|
}
|
|
454
|
-
if (
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if (condition.auth === "signedOut") {
|
|
460
|
-
return !isSignedIn;
|
|
461
|
-
}
|
|
462
|
-
return false;
|
|
662
|
+
if (isAndCondition(condition)) {
|
|
663
|
+
return condition.$and.every((child) => evaluateVisibility(child, ctx));
|
|
664
|
+
}
|
|
665
|
+
if (isOrCondition(condition)) {
|
|
666
|
+
return condition.$or.some((child) => evaluateVisibility(child, ctx));
|
|
463
667
|
}
|
|
464
|
-
return
|
|
668
|
+
return evaluateCondition(condition, ctx);
|
|
465
669
|
}
|
|
466
670
|
var visibility = {
|
|
467
671
|
/** Always visible */
|
|
468
672
|
always: true,
|
|
469
673
|
/** Never visible */
|
|
470
674
|
never: false,
|
|
471
|
-
/** Visible when path is truthy */
|
|
472
|
-
when: (path) => ({ path }),
|
|
473
|
-
/** Visible when
|
|
474
|
-
|
|
475
|
-
/** Visible when signed out */
|
|
476
|
-
signedOut: { auth: "signedOut" },
|
|
477
|
-
/** AND multiple conditions */
|
|
478
|
-
and: (...conditions) => ({
|
|
479
|
-
and: conditions
|
|
480
|
-
}),
|
|
481
|
-
/** OR multiple conditions */
|
|
482
|
-
or: (...conditions) => ({
|
|
483
|
-
or: conditions
|
|
484
|
-
}),
|
|
485
|
-
/** NOT a condition */
|
|
486
|
-
not: (condition) => ({ not: condition }),
|
|
675
|
+
/** Visible when state path is truthy */
|
|
676
|
+
when: (path) => ({ $state: path }),
|
|
677
|
+
/** Visible when state path is falsy */
|
|
678
|
+
unless: (path) => ({ $state: path, not: true }),
|
|
487
679
|
/** Equality check */
|
|
488
|
-
eq: (
|
|
489
|
-
|
|
680
|
+
eq: (path, value) => ({
|
|
681
|
+
$state: path,
|
|
682
|
+
eq: value
|
|
490
683
|
}),
|
|
491
684
|
/** Not equal check */
|
|
492
|
-
neq: (
|
|
493
|
-
|
|
685
|
+
neq: (path, value) => ({
|
|
686
|
+
$state: path,
|
|
687
|
+
neq: value
|
|
494
688
|
}),
|
|
495
689
|
/** Greater than */
|
|
496
|
-
gt: (
|
|
690
|
+
gt: (path, value) => ({
|
|
691
|
+
$state: path,
|
|
692
|
+
gt: value
|
|
693
|
+
}),
|
|
497
694
|
/** Greater than or equal */
|
|
498
|
-
gte: (
|
|
695
|
+
gte: (path, value) => ({
|
|
696
|
+
$state: path,
|
|
697
|
+
gte: value
|
|
698
|
+
}),
|
|
499
699
|
/** Less than */
|
|
500
|
-
lt: (
|
|
700
|
+
lt: (path, value) => ({
|
|
701
|
+
$state: path,
|
|
702
|
+
lt: value
|
|
703
|
+
}),
|
|
501
704
|
/** Less than or equal */
|
|
502
|
-
lte: (
|
|
705
|
+
lte: (path, value) => ({
|
|
706
|
+
$state: path,
|
|
707
|
+
lte: value
|
|
708
|
+
}),
|
|
709
|
+
/** AND multiple conditions */
|
|
710
|
+
and: (...conditions) => ({
|
|
711
|
+
$and: conditions
|
|
712
|
+
}),
|
|
713
|
+
/** OR multiple conditions */
|
|
714
|
+
or: (...conditions) => ({
|
|
715
|
+
$or: conditions
|
|
716
|
+
})
|
|
503
717
|
};
|
|
504
718
|
|
|
505
719
|
// src/props.ts
|
|
506
|
-
function
|
|
507
|
-
return typeof value === "object" && value !== null && "$
|
|
720
|
+
function isStateExpression(value) {
|
|
721
|
+
return typeof value === "object" && value !== null && "$state" in value && typeof value.$state === "string";
|
|
722
|
+
}
|
|
723
|
+
function isItemExpression(value) {
|
|
724
|
+
return typeof value === "object" && value !== null && "$item" in value && typeof value.$item === "string";
|
|
725
|
+
}
|
|
726
|
+
function isIndexExpression(value) {
|
|
727
|
+
return typeof value === "object" && value !== null && "$index" in value && value.$index === true;
|
|
728
|
+
}
|
|
729
|
+
function isBindStateExpression(value) {
|
|
730
|
+
return typeof value === "object" && value !== null && "$bindState" in value && typeof value.$bindState === "string";
|
|
731
|
+
}
|
|
732
|
+
function isBindItemExpression(value) {
|
|
733
|
+
return typeof value === "object" && value !== null && "$bindItem" in value && typeof value.$bindItem === "string";
|
|
508
734
|
}
|
|
509
735
|
function isCondExpression(value) {
|
|
510
736
|
return typeof value === "object" && value !== null && "$cond" in value && "$then" in value && "$else" in value;
|
|
511
737
|
}
|
|
738
|
+
function resolveBindItemPath(itemPath, ctx) {
|
|
739
|
+
if (ctx.repeatBasePath == null) {
|
|
740
|
+
console.warn(`$bindItem used outside repeat scope: "${itemPath}"`);
|
|
741
|
+
return void 0;
|
|
742
|
+
}
|
|
743
|
+
if (itemPath === "") return ctx.repeatBasePath;
|
|
744
|
+
return ctx.repeatBasePath + "/" + itemPath;
|
|
745
|
+
}
|
|
512
746
|
function resolvePropValue(value, ctx) {
|
|
513
747
|
if (value === null || value === void 0) {
|
|
514
748
|
return value;
|
|
515
749
|
}
|
|
516
|
-
if (
|
|
517
|
-
return getByPath(ctx.stateModel, value.$
|
|
750
|
+
if (isStateExpression(value)) {
|
|
751
|
+
return getByPath(ctx.stateModel, value.$state);
|
|
752
|
+
}
|
|
753
|
+
if (isItemExpression(value)) {
|
|
754
|
+
if (ctx.repeatItem === void 0) return void 0;
|
|
755
|
+
return value.$item === "" ? ctx.repeatItem : getByPath(ctx.repeatItem, value.$item);
|
|
756
|
+
}
|
|
757
|
+
if (isIndexExpression(value)) {
|
|
758
|
+
return ctx.repeatIndex;
|
|
759
|
+
}
|
|
760
|
+
if (isBindStateExpression(value)) {
|
|
761
|
+
return getByPath(ctx.stateModel, value.$bindState);
|
|
762
|
+
}
|
|
763
|
+
if (isBindItemExpression(value)) {
|
|
764
|
+
const resolvedPath = resolveBindItemPath(value.$bindItem, ctx);
|
|
765
|
+
if (resolvedPath === void 0) return void 0;
|
|
766
|
+
return getByPath(ctx.stateModel, resolvedPath);
|
|
518
767
|
}
|
|
519
768
|
if (isCondExpression(value)) {
|
|
520
769
|
const result = evaluateVisibility(value.$cond, ctx);
|
|
@@ -539,6 +788,31 @@ function resolveElementProps(props, ctx) {
|
|
|
539
788
|
}
|
|
540
789
|
return resolved;
|
|
541
790
|
}
|
|
791
|
+
function resolveBindings(props, ctx) {
|
|
792
|
+
let bindings;
|
|
793
|
+
for (const [key, value] of Object.entries(props)) {
|
|
794
|
+
if (isBindStateExpression(value)) {
|
|
795
|
+
if (!bindings) bindings = {};
|
|
796
|
+
bindings[key] = value.$bindState;
|
|
797
|
+
} else if (isBindItemExpression(value)) {
|
|
798
|
+
const resolved = resolveBindItemPath(value.$bindItem, ctx);
|
|
799
|
+
if (resolved !== void 0) {
|
|
800
|
+
if (!bindings) bindings = {};
|
|
801
|
+
bindings[key] = resolved;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return bindings;
|
|
806
|
+
}
|
|
807
|
+
function resolveActionParam(value, ctx) {
|
|
808
|
+
if (isItemExpression(value)) {
|
|
809
|
+
return resolveBindItemPath(value.$item, ctx);
|
|
810
|
+
}
|
|
811
|
+
if (isIndexExpression(value)) {
|
|
812
|
+
return ctx.repeatIndex;
|
|
813
|
+
}
|
|
814
|
+
return resolvePropValue(value, ctx);
|
|
815
|
+
}
|
|
542
816
|
|
|
543
817
|
// src/actions.ts
|
|
544
818
|
import { z as z3 } from "zod";
|
|
@@ -591,7 +865,7 @@ function resolveAction(binding, stateModel) {
|
|
|
591
865
|
}
|
|
592
866
|
function interpolateString(template, stateModel) {
|
|
593
867
|
return template.replace(/\$\{([^}]+)\}/g, (_, path) => {
|
|
594
|
-
const value = resolveDynamicValue({ path }, stateModel);
|
|
868
|
+
const value = resolveDynamicValue({ $state: path }, stateModel);
|
|
595
869
|
return String(value ?? "");
|
|
596
870
|
});
|
|
597
871
|
}
|
|
@@ -649,14 +923,14 @@ var action = actionBinding;
|
|
|
649
923
|
// src/validation.ts
|
|
650
924
|
import { z as z4 } from "zod";
|
|
651
925
|
var ValidationCheckSchema = z4.object({
|
|
652
|
-
|
|
926
|
+
type: z4.string(),
|
|
653
927
|
args: z4.record(z4.string(), DynamicValueSchema).optional(),
|
|
654
928
|
message: z4.string()
|
|
655
929
|
});
|
|
656
930
|
var ValidationConfigSchema = z4.object({
|
|
657
931
|
checks: z4.array(ValidationCheckSchema).optional(),
|
|
658
932
|
validateOn: z4.enum(["change", "blur", "submit"]).optional(),
|
|
659
|
-
enabled:
|
|
933
|
+
enabled: VisibilityConditionSchema.optional()
|
|
660
934
|
});
|
|
661
935
|
var builtInValidationFunctions = {
|
|
662
936
|
/**
|
|
@@ -760,19 +1034,19 @@ function runValidationCheck(check2, ctx) {
|
|
|
760
1034
|
resolvedArgs[key] = resolveDynamicValue(argValue, stateModel);
|
|
761
1035
|
}
|
|
762
1036
|
}
|
|
763
|
-
const
|
|
764
|
-
if (!
|
|
765
|
-
console.warn(`Unknown validation function: ${check2.
|
|
1037
|
+
const validationFn = builtInValidationFunctions[check2.type] ?? customFunctions?.[check2.type];
|
|
1038
|
+
if (!validationFn) {
|
|
1039
|
+
console.warn(`Unknown validation function: ${check2.type}`);
|
|
766
1040
|
return {
|
|
767
|
-
|
|
1041
|
+
type: check2.type,
|
|
768
1042
|
valid: true,
|
|
769
1043
|
// Don't fail on unknown functions
|
|
770
1044
|
message: check2.message
|
|
771
1045
|
};
|
|
772
1046
|
}
|
|
773
|
-
const valid =
|
|
1047
|
+
const valid = validationFn(value, resolvedArgs);
|
|
774
1048
|
return {
|
|
775
|
-
|
|
1049
|
+
type: check2.type,
|
|
776
1050
|
valid,
|
|
777
1051
|
message: check2.message
|
|
778
1052
|
};
|
|
@@ -781,9 +1055,8 @@ function runValidation(config, ctx) {
|
|
|
781
1055
|
const checks = [];
|
|
782
1056
|
const errors = [];
|
|
783
1057
|
if (config.enabled) {
|
|
784
|
-
const enabled =
|
|
785
|
-
stateModel: ctx.stateModel
|
|
786
|
-
authState: ctx.authState
|
|
1058
|
+
const enabled = evaluateVisibility(config.enabled, {
|
|
1059
|
+
stateModel: ctx.stateModel
|
|
787
1060
|
});
|
|
788
1061
|
if (!enabled) {
|
|
789
1062
|
return { valid: true, errors: [], checks: [] };
|
|
@@ -806,45 +1079,45 @@ function runValidation(config, ctx) {
|
|
|
806
1079
|
}
|
|
807
1080
|
var check = {
|
|
808
1081
|
required: (message = "This field is required") => ({
|
|
809
|
-
|
|
1082
|
+
type: "required",
|
|
810
1083
|
message
|
|
811
1084
|
}),
|
|
812
1085
|
email: (message = "Invalid email address") => ({
|
|
813
|
-
|
|
1086
|
+
type: "email",
|
|
814
1087
|
message
|
|
815
1088
|
}),
|
|
816
1089
|
minLength: (min, message) => ({
|
|
817
|
-
|
|
1090
|
+
type: "minLength",
|
|
818
1091
|
args: { min },
|
|
819
1092
|
message: message ?? `Must be at least ${min} characters`
|
|
820
1093
|
}),
|
|
821
1094
|
maxLength: (max, message) => ({
|
|
822
|
-
|
|
1095
|
+
type: "maxLength",
|
|
823
1096
|
args: { max },
|
|
824
1097
|
message: message ?? `Must be at most ${max} characters`
|
|
825
1098
|
}),
|
|
826
1099
|
pattern: (pattern, message = "Invalid format") => ({
|
|
827
|
-
|
|
1100
|
+
type: "pattern",
|
|
828
1101
|
args: { pattern },
|
|
829
1102
|
message
|
|
830
1103
|
}),
|
|
831
1104
|
min: (min, message) => ({
|
|
832
|
-
|
|
1105
|
+
type: "min",
|
|
833
1106
|
args: { min },
|
|
834
1107
|
message: message ?? `Must be at least ${min}`
|
|
835
1108
|
}),
|
|
836
1109
|
max: (max, message) => ({
|
|
837
|
-
|
|
1110
|
+
type: "max",
|
|
838
1111
|
args: { max },
|
|
839
1112
|
message: message ?? `Must be at most ${max}`
|
|
840
1113
|
}),
|
|
841
1114
|
url: (message = "Invalid URL") => ({
|
|
842
|
-
|
|
1115
|
+
type: "url",
|
|
843
1116
|
message
|
|
844
1117
|
}),
|
|
845
1118
|
matches: (otherPath, message = "Fields must match") => ({
|
|
846
|
-
|
|
847
|
-
args: { other: {
|
|
1119
|
+
type: "matches",
|
|
1120
|
+
args: { other: { $state: otherPath } },
|
|
848
1121
|
message
|
|
849
1122
|
})
|
|
850
1123
|
};
|
|
@@ -984,7 +1257,7 @@ function autoFixSpec(spec) {
|
|
|
984
1257
|
fixedElements[key] = fixed;
|
|
985
1258
|
}
|
|
986
1259
|
return {
|
|
987
|
-
spec: { root: spec.root, elements: fixedElements },
|
|
1260
|
+
spec: { root: spec.root, elements: fixedElements, state: spec.state },
|
|
988
1261
|
fixes
|
|
989
1262
|
};
|
|
990
1263
|
}
|
|
@@ -1168,15 +1441,35 @@ function generatePrompt(catalog, options) {
|
|
|
1168
1441
|
}
|
|
1169
1442
|
const {
|
|
1170
1443
|
system = "You are a UI generator that outputs JSON.",
|
|
1171
|
-
customRules = []
|
|
1444
|
+
customRules = [],
|
|
1445
|
+
mode = "generate"
|
|
1172
1446
|
} = options;
|
|
1173
1447
|
const lines = [];
|
|
1174
1448
|
lines.push(system);
|
|
1175
1449
|
lines.push("");
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1450
|
+
if (mode === "chat") {
|
|
1451
|
+
lines.push("OUTPUT FORMAT (text + JSONL, RFC 6902 JSON Patch):");
|
|
1452
|
+
lines.push(
|
|
1453
|
+
"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."
|
|
1454
|
+
);
|
|
1455
|
+
lines.push(
|
|
1456
|
+
"The JSONL lines use RFC 6902 JSON Patch operations to build a UI tree. Always wrap them in a ```spec fence block:"
|
|
1457
|
+
);
|
|
1458
|
+
lines.push(" ```spec");
|
|
1459
|
+
lines.push(' {"op":"add","path":"/root","value":"main"}');
|
|
1460
|
+
lines.push(
|
|
1461
|
+
' {"op":"add","path":"/elements/main","value":{"type":"Card","props":{"title":"Hello"},"children":[]}}'
|
|
1462
|
+
);
|
|
1463
|
+
lines.push(" ```");
|
|
1464
|
+
lines.push(
|
|
1465
|
+
"If the user's message does not require a UI (e.g. a greeting or clarifying question), respond with text only \u2014 no JSONL."
|
|
1466
|
+
);
|
|
1467
|
+
} else {
|
|
1468
|
+
lines.push("OUTPUT FORMAT (JSONL, RFC 6902 JSON Patch):");
|
|
1469
|
+
lines.push(
|
|
1470
|
+
"Output JSONL (one JSON object per line) using RFC 6902 JSON Patch operations to build a UI tree."
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1180
1473
|
lines.push(
|
|
1181
1474
|
"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."
|
|
1182
1475
|
);
|
|
@@ -1192,7 +1485,7 @@ function generatePrompt(catalog, options) {
|
|
|
1192
1485
|
const comp1Props = comp1Def ? getExampleProps(comp1Def) : {};
|
|
1193
1486
|
const comp2Props = comp2Def ? getExampleProps(comp2Def) : {};
|
|
1194
1487
|
const dynamicPropName = comp2Def?.props ? findFirstStringProp(comp2Def.props) : null;
|
|
1195
|
-
const dynamicProps = dynamicPropName ? { ...comp2Props, [dynamicPropName]: { $
|
|
1488
|
+
const dynamicProps = dynamicPropName ? { ...comp2Props, [dynamicPropName]: { $item: "title" } } : comp2Props;
|
|
1196
1489
|
const exampleOutput = [
|
|
1197
1490
|
JSON.stringify({ op: "add", path: "/root", value: "main" }),
|
|
1198
1491
|
JSON.stringify({
|
|
@@ -1215,7 +1508,7 @@ function generatePrompt(catalog, options) {
|
|
|
1215
1508
|
value: {
|
|
1216
1509
|
type: comp1,
|
|
1217
1510
|
props: comp1Props,
|
|
1218
|
-
repeat: {
|
|
1511
|
+
repeat: { statePath: "/items", key: "id" },
|
|
1219
1512
|
children: ["item"]
|
|
1220
1513
|
}
|
|
1221
1514
|
}),
|
|
@@ -1242,10 +1535,10 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|
|
1242
1535
|
lines.push("");
|
|
1243
1536
|
lines.push("INITIAL STATE:");
|
|
1244
1537
|
lines.push(
|
|
1245
|
-
"Specs include a /state field to seed the state model. Components with
|
|
1538
|
+
"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."
|
|
1246
1539
|
);
|
|
1247
1540
|
lines.push(
|
|
1248
|
-
"CRITICAL: You MUST include state patches whenever your UI displays data via $
|
|
1541
|
+
"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."
|
|
1249
1542
|
);
|
|
1250
1543
|
lines.push(
|
|
1251
1544
|
"Output state patches right after the elements that reference them, so the UI fills in progressively as it streams."
|
|
@@ -1263,7 +1556,7 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|
|
1263
1556
|
' Initialize the array first if needed: {"op":"add","path":"/state/posts","value":[]}'
|
|
1264
1557
|
);
|
|
1265
1558
|
lines.push(
|
|
1266
|
-
'When content comes from the state model, use { "$
|
|
1559
|
+
'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.'
|
|
1267
1560
|
);
|
|
1268
1561
|
lines.push(
|
|
1269
1562
|
"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."
|
|
@@ -1271,16 +1564,16 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|
|
1271
1564
|
lines.push("");
|
|
1272
1565
|
lines.push("DYNAMIC LISTS (repeat field):");
|
|
1273
1566
|
lines.push(
|
|
1274
|
-
'Any element can have a top-level "repeat" field to render its children once per item in a state array: { "repeat": { "
|
|
1567
|
+
'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" } }.'
|
|
1275
1568
|
);
|
|
1276
1569
|
lines.push(
|
|
1277
|
-
'The element itself renders once (as the container), and its children are expanded once per array item. "
|
|
1570
|
+
'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.'
|
|
1278
1571
|
);
|
|
1279
1572
|
lines.push(
|
|
1280
|
-
`Example: ${JSON.stringify({ type: comp1, props: comp1Props, repeat: {
|
|
1573
|
+
`Example: ${JSON.stringify({ type: comp1, props: comp1Props, repeat: { statePath: "/todos", key: "id" }, children: ["todo-item"] })}`
|
|
1281
1574
|
);
|
|
1282
1575
|
lines.push(
|
|
1283
|
-
'Inside children of a repeated element, use "$item
|
|
1576
|
+
'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.'
|
|
1284
1577
|
);
|
|
1285
1578
|
lines.push(
|
|
1286
1579
|
"ALWAYS use the repeat field for lists backed by state arrays. NEVER hardcode individual elements for each array item."
|
|
@@ -1291,19 +1584,19 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|
|
1291
1584
|
lines.push("");
|
|
1292
1585
|
lines.push("ARRAY STATE ACTIONS:");
|
|
1293
1586
|
lines.push(
|
|
1294
|
-
'Use action "pushState" to append items to arrays. Params: {
|
|
1587
|
+
'Use action "pushState" to append items to arrays. Params: { statePath: "/arrayPath", value: { ...item }, clearStatePath: "/inputPath" }.'
|
|
1295
1588
|
);
|
|
1296
1589
|
lines.push(
|
|
1297
|
-
'Values inside pushState can contain { "
|
|
1590
|
+
'Values inside pushState can contain { "$state": "/statePath" } references to read current state (e.g. the text from an input field).'
|
|
1298
1591
|
);
|
|
1299
1592
|
lines.push(
|
|
1300
1593
|
'Use "$id" inside a pushState value to auto-generate a unique ID.'
|
|
1301
1594
|
);
|
|
1302
1595
|
lines.push(
|
|
1303
|
-
'Example: on: { "press": { "action": "pushState", "params": { "
|
|
1596
|
+
'Example: on: { "press": { "action": "pushState", "params": { "statePath": "/todos", "value": { "id": "$id", "title": { "$state": "/newTodoText" }, "completed": false }, "clearStatePath": "/newTodoText" } } }'
|
|
1304
1597
|
);
|
|
1305
1598
|
lines.push(
|
|
1306
|
-
`Use action "removeState" to remove items from arrays by index. Params: {
|
|
1599
|
+
`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.`
|
|
1307
1600
|
);
|
|
1308
1601
|
lines.push(
|
|
1309
1602
|
"For lists where users can add/remove items (todos, carts, etc.), use pushState and removeState instead of hardcoding with setState."
|
|
@@ -1346,11 +1639,11 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|
|
1346
1639
|
lines.push("");
|
|
1347
1640
|
lines.push("Example:");
|
|
1348
1641
|
lines.push(
|
|
1349
|
-
` ${JSON.stringify({ type: comp1, props: comp1Props, on: { press: { action: "setState", params: {
|
|
1642
|
+
` ${JSON.stringify({ type: comp1, props: comp1Props, on: { press: { action: "setState", params: { statePath: "/saved", value: true } } }, children: [] })}`
|
|
1350
1643
|
);
|
|
1351
1644
|
lines.push("");
|
|
1352
1645
|
lines.push(
|
|
1353
|
-
'Action params can use dynamic
|
|
1646
|
+
'Action params can use dynamic references to read from state: { "$state": "/statePath" }.'
|
|
1354
1647
|
);
|
|
1355
1648
|
lines.push(
|
|
1356
1649
|
"IMPORTANT: Do NOT put action/actionParams inside props. Always use the `on` field for event bindings."
|
|
@@ -1358,20 +1651,38 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|
|
1358
1651
|
lines.push("");
|
|
1359
1652
|
lines.push("VISIBILITY CONDITIONS:");
|
|
1360
1653
|
lines.push(
|
|
1361
|
-
"Elements can have an optional `visible` field to conditionally show/hide based on
|
|
1654
|
+
"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."
|
|
1362
1655
|
);
|
|
1363
1656
|
lines.push(
|
|
1364
|
-
`Correct: ${JSON.stringify({ type: comp1, props: comp1Props, visible: {
|
|
1657
|
+
`Correct: ${JSON.stringify({ type: comp1, props: comp1Props, visible: { $state: "/activeTab", eq: "home" }, children: ["..."] })}`
|
|
1365
1658
|
);
|
|
1366
1659
|
lines.push(
|
|
1367
|
-
'- `{ "
|
|
1660
|
+
'- `{ "$state": "/path" }` - visible when state at path is truthy'
|
|
1368
1661
|
);
|
|
1369
1662
|
lines.push(
|
|
1370
|
-
'- `{ "
|
|
1663
|
+
'- `{ "$state": "/path", "not": true }` - visible when state at path is falsy'
|
|
1371
1664
|
);
|
|
1372
|
-
lines.push('- `{ "path": "/statePath" }` - visible when path is truthy');
|
|
1373
1665
|
lines.push(
|
|
1374
|
-
'- `{ "
|
|
1666
|
+
'- `{ "$state": "/path", "eq": "value" }` - visible when state equals value'
|
|
1667
|
+
);
|
|
1668
|
+
lines.push(
|
|
1669
|
+
'- `{ "$state": "/path", "neq": "value" }` - visible when state does not equal value'
|
|
1670
|
+
);
|
|
1671
|
+
lines.push(
|
|
1672
|
+
'- `{ "$state": "/path", "gt": N }` / `gte` / `lt` / `lte` - numeric comparisons'
|
|
1673
|
+
);
|
|
1674
|
+
lines.push(
|
|
1675
|
+
"- Use ONE operator per condition (eq, neq, gt, gte, lt, lte). Do not combine multiple operators."
|
|
1676
|
+
);
|
|
1677
|
+
lines.push('- Any condition can add `"not": true` to invert its result');
|
|
1678
|
+
lines.push(
|
|
1679
|
+
"- `[condition, condition]` - all conditions must be true (implicit AND)"
|
|
1680
|
+
);
|
|
1681
|
+
lines.push(
|
|
1682
|
+
'- `{ "$and": [condition, condition] }` - explicit AND (use when nesting inside $or)'
|
|
1683
|
+
);
|
|
1684
|
+
lines.push(
|
|
1685
|
+
'- `{ "$or": [condition, condition] }` - at least one must be true (OR)'
|
|
1375
1686
|
);
|
|
1376
1687
|
lines.push("- `true` / `false` - always visible/hidden");
|
|
1377
1688
|
lines.push("");
|
|
@@ -1379,41 +1690,102 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|
|
1379
1690
|
"Use a component with on.press bound to setState to update state and drive visibility."
|
|
1380
1691
|
);
|
|
1381
1692
|
lines.push(
|
|
1382
|
-
`Example: A ${comp1} with on: { "press": { "action": "setState", "params": { "
|
|
1693
|
+
`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.`
|
|
1694
|
+
);
|
|
1695
|
+
lines.push("");
|
|
1696
|
+
lines.push(
|
|
1697
|
+
'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.'
|
|
1383
1698
|
);
|
|
1384
1699
|
lines.push("");
|
|
1385
1700
|
lines.push("DYNAMIC PROPS:");
|
|
1386
1701
|
lines.push(
|
|
1387
|
-
"Any prop value can be a dynamic expression that resolves based on state.
|
|
1702
|
+
"Any prop value can be a dynamic expression that resolves based on state. Three forms are supported:"
|
|
1388
1703
|
);
|
|
1389
1704
|
lines.push("");
|
|
1390
1705
|
lines.push(
|
|
1391
|
-
'1.
|
|
1706
|
+
'1. Read-only state: `{ "$state": "/statePath" }` - resolves to the value at that state path (one-way read).'
|
|
1392
1707
|
);
|
|
1393
1708
|
lines.push(
|
|
1394
|
-
' Example: `"color": { "$
|
|
1709
|
+
' Example: `"color": { "$state": "/theme/primary" }` reads the color from state.'
|
|
1395
1710
|
);
|
|
1396
1711
|
lines.push("");
|
|
1397
1712
|
lines.push(
|
|
1398
|
-
'2.
|
|
1713
|
+
'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.).'
|
|
1399
1714
|
);
|
|
1400
1715
|
lines.push(
|
|
1401
|
-
' Example: `"
|
|
1716
|
+
' Example: `"value": { "$bindState": "/form/email" }` binds the input value to /form/email.'
|
|
1402
1717
|
);
|
|
1403
1718
|
lines.push(
|
|
1404
|
-
'
|
|
1719
|
+
' Inside repeat scopes: `"checked": { "$bindItem": "completed" }` binds to the current item\'s completed field.'
|
|
1405
1720
|
);
|
|
1406
1721
|
lines.push("");
|
|
1407
1722
|
lines.push(
|
|
1408
|
-
"
|
|
1723
|
+
'3. Conditional: `{ "$cond": <condition>, "$then": <value>, "$else": <value> }` - evaluates the condition (same syntax as visibility conditions) and picks the matching value.'
|
|
1724
|
+
);
|
|
1725
|
+
lines.push(
|
|
1726
|
+
' Example: `"color": { "$cond": { "$state": "/activeTab", "eq": "home" }, "$then": "#007AFF", "$else": "#8E8E93" }`'
|
|
1409
1727
|
);
|
|
1410
1728
|
lines.push("");
|
|
1729
|
+
lines.push(
|
|
1730
|
+
"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."
|
|
1731
|
+
);
|
|
1732
|
+
lines.push("");
|
|
1733
|
+
const hasChecksComponents = allComponents ? Object.entries(allComponents).some(([, def]) => {
|
|
1734
|
+
if (!def.props) return false;
|
|
1735
|
+
const formatted = formatZodType(def.props);
|
|
1736
|
+
return formatted.includes("checks");
|
|
1737
|
+
}) : false;
|
|
1738
|
+
if (hasChecksComponents) {
|
|
1739
|
+
lines.push("VALIDATION:");
|
|
1740
|
+
lines.push(
|
|
1741
|
+
"Form components that accept a `checks` prop support client-side validation."
|
|
1742
|
+
);
|
|
1743
|
+
lines.push(
|
|
1744
|
+
'Each check is an object: { "type": "<name>", "message": "...", "args": { ... } }'
|
|
1745
|
+
);
|
|
1746
|
+
lines.push("");
|
|
1747
|
+
lines.push("Built-in validation types:");
|
|
1748
|
+
lines.push(" - required \u2014 value must be non-empty");
|
|
1749
|
+
lines.push(" - email \u2014 valid email format");
|
|
1750
|
+
lines.push(' - minLength \u2014 minimum string length (args: { "min": N })');
|
|
1751
|
+
lines.push(' - maxLength \u2014 maximum string length (args: { "max": N })');
|
|
1752
|
+
lines.push(' - pattern \u2014 match a regex (args: { "pattern": "regex" })');
|
|
1753
|
+
lines.push(' - min \u2014 minimum numeric value (args: { "min": N })');
|
|
1754
|
+
lines.push(' - max \u2014 maximum numeric value (args: { "max": N })');
|
|
1755
|
+
lines.push(" - numeric \u2014 value must be a number");
|
|
1756
|
+
lines.push(" - url \u2014 valid URL format");
|
|
1757
|
+
lines.push(
|
|
1758
|
+
' - matches \u2014 must equal another field (args: { "other": "value" })'
|
|
1759
|
+
);
|
|
1760
|
+
lines.push("");
|
|
1761
|
+
lines.push("Example:");
|
|
1762
|
+
lines.push(
|
|
1763
|
+
' "checks": [{ "type": "required", "message": "Email is required" }, { "type": "email", "message": "Invalid email" }]'
|
|
1764
|
+
);
|
|
1765
|
+
lines.push("");
|
|
1766
|
+
lines.push(
|
|
1767
|
+
"IMPORTANT: When using checks, the component must also have a { $bindState } or { $bindItem } on its value/checked prop for two-way binding."
|
|
1768
|
+
);
|
|
1769
|
+
lines.push(
|
|
1770
|
+
"Always include validation checks on form inputs for a good user experience (e.g. required, email, minLength)."
|
|
1771
|
+
);
|
|
1772
|
+
lines.push("");
|
|
1773
|
+
}
|
|
1411
1774
|
lines.push("RULES:");
|
|
1412
|
-
const baseRules = [
|
|
1775
|
+
const baseRules = mode === "chat" ? [
|
|
1776
|
+
"When generating UI, wrap all JSONL patches in a ```spec code fence - one JSON object per line inside the fence",
|
|
1777
|
+
"Write a brief conversational response before any JSONL output",
|
|
1778
|
+
'First set root: {"op":"add","path":"/root","value":"<root-key>"}',
|
|
1779
|
+
'Then add each element: {"op":"add","path":"/elements/<key>","value":{...}}',
|
|
1780
|
+
"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.",
|
|
1781
|
+
"ONLY use components listed above",
|
|
1782
|
+
"Each element value needs: type, props, children (array of child keys)",
|
|
1783
|
+
"Use unique keys for the element map entries (e.g., 'header', 'metric-1', 'chart-revenue')"
|
|
1784
|
+
] : [
|
|
1413
1785
|
"Output ONLY JSONL patches - one JSON object per line, no markdown, no code fences",
|
|
1414
1786
|
'First set root: {"op":"add","path":"/root","value":"<root-key>"}',
|
|
1415
1787
|
'Then add each element: {"op":"add","path":"/elements/<key>","value":{...}}',
|
|
1416
|
-
"Output /state patches right after the elements that use them, one per array item for progressive loading. REQUIRED whenever using $
|
|
1788
|
+
"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.",
|
|
1417
1789
|
"ONLY use components listed above",
|
|
1418
1790
|
"Each element value needs: type, props, children (array of child keys)",
|
|
1419
1791
|
"Use unique keys for the element map entries (e.g., 'header', 'metric-1', 'chart-revenue')"
|
|
@@ -1701,285 +2073,6 @@ Remember: Output /root first, then interleave /elements and /state patches so th
|
|
|
1701
2073
|
);
|
|
1702
2074
|
return parts.join("\n");
|
|
1703
2075
|
}
|
|
1704
|
-
|
|
1705
|
-
// src/catalog.ts
|
|
1706
|
-
import { z as z6 } from "zod";
|
|
1707
|
-
function createCatalog(config) {
|
|
1708
|
-
const {
|
|
1709
|
-
name = "unnamed",
|
|
1710
|
-
components,
|
|
1711
|
-
actions = {},
|
|
1712
|
-
functions = {},
|
|
1713
|
-
validation = "strict"
|
|
1714
|
-
} = config;
|
|
1715
|
-
const componentNames = Object.keys(components);
|
|
1716
|
-
const actionNames = Object.keys(actions);
|
|
1717
|
-
const functionNames = Object.keys(functions);
|
|
1718
|
-
const componentSchemas = componentNames.map((componentName) => {
|
|
1719
|
-
const def = components[componentName];
|
|
1720
|
-
return z6.object({
|
|
1721
|
-
type: z6.literal(componentName),
|
|
1722
|
-
props: def.props,
|
|
1723
|
-
children: z6.array(z6.string()).optional(),
|
|
1724
|
-
visible: VisibilityConditionSchema.optional()
|
|
1725
|
-
});
|
|
1726
|
-
});
|
|
1727
|
-
let elementSchema;
|
|
1728
|
-
if (componentSchemas.length === 0) {
|
|
1729
|
-
elementSchema = z6.object({
|
|
1730
|
-
type: z6.string(),
|
|
1731
|
-
props: z6.record(z6.string(), z6.unknown()),
|
|
1732
|
-
children: z6.array(z6.string()).optional(),
|
|
1733
|
-
visible: VisibilityConditionSchema.optional()
|
|
1734
|
-
});
|
|
1735
|
-
} else if (componentSchemas.length === 1) {
|
|
1736
|
-
elementSchema = componentSchemas[0];
|
|
1737
|
-
} else {
|
|
1738
|
-
elementSchema = z6.discriminatedUnion("type", [
|
|
1739
|
-
componentSchemas[0],
|
|
1740
|
-
componentSchemas[1],
|
|
1741
|
-
...componentSchemas.slice(2)
|
|
1742
|
-
]);
|
|
1743
|
-
}
|
|
1744
|
-
const specSchema = z6.object({
|
|
1745
|
-
root: z6.string(),
|
|
1746
|
-
elements: z6.record(z6.string(), elementSchema)
|
|
1747
|
-
});
|
|
1748
|
-
return {
|
|
1749
|
-
name,
|
|
1750
|
-
componentNames,
|
|
1751
|
-
actionNames,
|
|
1752
|
-
functionNames,
|
|
1753
|
-
validation,
|
|
1754
|
-
components,
|
|
1755
|
-
actions,
|
|
1756
|
-
functions,
|
|
1757
|
-
elementSchema,
|
|
1758
|
-
specSchema,
|
|
1759
|
-
hasComponent(type) {
|
|
1760
|
-
return type in components;
|
|
1761
|
-
},
|
|
1762
|
-
hasAction(name2) {
|
|
1763
|
-
return name2 in actions;
|
|
1764
|
-
},
|
|
1765
|
-
hasFunction(name2) {
|
|
1766
|
-
return name2 in functions;
|
|
1767
|
-
},
|
|
1768
|
-
validateElement(element) {
|
|
1769
|
-
const result = elementSchema.safeParse(element);
|
|
1770
|
-
if (result.success) {
|
|
1771
|
-
return { success: true, data: result.data };
|
|
1772
|
-
}
|
|
1773
|
-
return { success: false, error: result.error };
|
|
1774
|
-
},
|
|
1775
|
-
validateSpec(spec) {
|
|
1776
|
-
const result = specSchema.safeParse(spec);
|
|
1777
|
-
if (result.success) {
|
|
1778
|
-
return { success: true, data: result.data };
|
|
1779
|
-
}
|
|
1780
|
-
return { success: false, error: result.error };
|
|
1781
|
-
}
|
|
1782
|
-
};
|
|
1783
|
-
}
|
|
1784
|
-
function generateCatalogPrompt(catalog) {
|
|
1785
|
-
const lines = [
|
|
1786
|
-
`# ${catalog.name} Component Catalog`,
|
|
1787
|
-
"",
|
|
1788
|
-
"## Available Components",
|
|
1789
|
-
""
|
|
1790
|
-
];
|
|
1791
|
-
for (const name of catalog.componentNames) {
|
|
1792
|
-
const def = catalog.components[name];
|
|
1793
|
-
lines.push(`### ${String(name)}`);
|
|
1794
|
-
if (def.description) {
|
|
1795
|
-
lines.push(def.description);
|
|
1796
|
-
}
|
|
1797
|
-
lines.push("");
|
|
1798
|
-
}
|
|
1799
|
-
if (catalog.actionNames.length > 0) {
|
|
1800
|
-
lines.push("## Available Actions");
|
|
1801
|
-
lines.push("");
|
|
1802
|
-
for (const name of catalog.actionNames) {
|
|
1803
|
-
const def = catalog.actions[name];
|
|
1804
|
-
lines.push(
|
|
1805
|
-
`- \`${String(name)}\`${def.description ? `: ${def.description}` : ""}`
|
|
1806
|
-
);
|
|
1807
|
-
}
|
|
1808
|
-
lines.push("");
|
|
1809
|
-
}
|
|
1810
|
-
lines.push("## Visibility Conditions");
|
|
1811
|
-
lines.push("");
|
|
1812
|
-
lines.push("Components can have a `visible` property:");
|
|
1813
|
-
lines.push("- `true` / `false` - Always visible/hidden");
|
|
1814
|
-
lines.push('- `{ "path": "/state/path" }` - Visible when path is truthy');
|
|
1815
|
-
lines.push('- `{ "auth": "signedIn" }` - Visible when user is signed in');
|
|
1816
|
-
lines.push('- `{ "and": [...] }` - All conditions must be true');
|
|
1817
|
-
lines.push('- `{ "or": [...] }` - Any condition must be true');
|
|
1818
|
-
lines.push('- `{ "not": {...} }` - Negates a condition');
|
|
1819
|
-
lines.push('- `{ "eq": [a, b] }` - Equality check');
|
|
1820
|
-
lines.push("");
|
|
1821
|
-
lines.push("## Validation Functions");
|
|
1822
|
-
lines.push("");
|
|
1823
|
-
lines.push(
|
|
1824
|
-
"Built-in: `required`, `email`, `minLength`, `maxLength`, `pattern`, `min`, `max`, `url`"
|
|
1825
|
-
);
|
|
1826
|
-
if (catalog.functionNames.length > 0) {
|
|
1827
|
-
lines.push(`Custom: ${catalog.functionNames.map(String).join(", ")}`);
|
|
1828
|
-
}
|
|
1829
|
-
lines.push("");
|
|
1830
|
-
return lines.join("\n");
|
|
1831
|
-
}
|
|
1832
|
-
function formatZodType2(schema, isOptional = false) {
|
|
1833
|
-
const def = schema._def;
|
|
1834
|
-
const typeName = def.typeName ?? "";
|
|
1835
|
-
let result;
|
|
1836
|
-
switch (typeName) {
|
|
1837
|
-
case "ZodString":
|
|
1838
|
-
result = "string";
|
|
1839
|
-
break;
|
|
1840
|
-
case "ZodNumber":
|
|
1841
|
-
result = "number";
|
|
1842
|
-
break;
|
|
1843
|
-
case "ZodBoolean":
|
|
1844
|
-
result = "boolean";
|
|
1845
|
-
break;
|
|
1846
|
-
case "ZodLiteral":
|
|
1847
|
-
result = JSON.stringify(def.value);
|
|
1848
|
-
break;
|
|
1849
|
-
case "ZodEnum":
|
|
1850
|
-
result = def.values.map((v) => `"${v}"`).join("|");
|
|
1851
|
-
break;
|
|
1852
|
-
case "ZodNativeEnum":
|
|
1853
|
-
result = Object.values(def.values).map((v) => `"${v}"`).join("|");
|
|
1854
|
-
break;
|
|
1855
|
-
case "ZodArray":
|
|
1856
|
-
result = def.type ? `Array<${formatZodType2(def.type)}>` : "Array<unknown>";
|
|
1857
|
-
break;
|
|
1858
|
-
case "ZodObject": {
|
|
1859
|
-
if (!def.shape) {
|
|
1860
|
-
result = "object";
|
|
1861
|
-
break;
|
|
1862
|
-
}
|
|
1863
|
-
const shape = def.shape();
|
|
1864
|
-
const props = Object.entries(shape).map(([key, value]) => {
|
|
1865
|
-
const innerDef = value._def;
|
|
1866
|
-
const innerOptional = innerDef.typeName === "ZodOptional" || innerDef.typeName === "ZodNullable";
|
|
1867
|
-
return `${key}${innerOptional ? "?" : ""}: ${formatZodType2(value)}`;
|
|
1868
|
-
}).join(", ");
|
|
1869
|
-
result = `{ ${props} }`;
|
|
1870
|
-
break;
|
|
1871
|
-
}
|
|
1872
|
-
case "ZodOptional":
|
|
1873
|
-
return def.innerType ? formatZodType2(def.innerType, true) : "unknown?";
|
|
1874
|
-
case "ZodNullable":
|
|
1875
|
-
return def.innerType ? formatZodType2(def.innerType, true) : "unknown?";
|
|
1876
|
-
case "ZodDefault":
|
|
1877
|
-
return def.innerType ? formatZodType2(def.innerType, isOptional) : "unknown";
|
|
1878
|
-
case "ZodUnion":
|
|
1879
|
-
result = def.options ? def.options.map((opt) => formatZodType2(opt)).join("|") : "unknown";
|
|
1880
|
-
break;
|
|
1881
|
-
case "ZodNull":
|
|
1882
|
-
result = "null";
|
|
1883
|
-
break;
|
|
1884
|
-
case "ZodUndefined":
|
|
1885
|
-
result = "undefined";
|
|
1886
|
-
break;
|
|
1887
|
-
case "ZodAny":
|
|
1888
|
-
result = "any";
|
|
1889
|
-
break;
|
|
1890
|
-
case "ZodUnknown":
|
|
1891
|
-
result = "unknown";
|
|
1892
|
-
break;
|
|
1893
|
-
default:
|
|
1894
|
-
result = "unknown";
|
|
1895
|
-
}
|
|
1896
|
-
return isOptional ? `${result}?` : result;
|
|
1897
|
-
}
|
|
1898
|
-
function extractPropsFromSchema(schema) {
|
|
1899
|
-
const def = schema._def;
|
|
1900
|
-
const typeName = def.typeName ?? "";
|
|
1901
|
-
if (typeName !== "ZodObject" || !def.shape) {
|
|
1902
|
-
return [];
|
|
1903
|
-
}
|
|
1904
|
-
const shape = def.shape();
|
|
1905
|
-
return Object.entries(shape).map(([name, value]) => {
|
|
1906
|
-
const innerDef = value._def;
|
|
1907
|
-
const optional = innerDef.typeName === "ZodOptional" || innerDef.typeName === "ZodNullable";
|
|
1908
|
-
return {
|
|
1909
|
-
name,
|
|
1910
|
-
type: formatZodType2(value),
|
|
1911
|
-
optional
|
|
1912
|
-
};
|
|
1913
|
-
});
|
|
1914
|
-
}
|
|
1915
|
-
function formatPropsCompact(props) {
|
|
1916
|
-
if (props.length === 0) return "{}";
|
|
1917
|
-
const entries = props.map(
|
|
1918
|
-
(p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`
|
|
1919
|
-
);
|
|
1920
|
-
return `{ ${entries.join(", ")} }`;
|
|
1921
|
-
}
|
|
1922
|
-
function generateSystemPrompt(catalog, options = {}) {
|
|
1923
|
-
const {
|
|
1924
|
-
system = "You are a UI generator that outputs JSONL (JSON Lines) patches.",
|
|
1925
|
-
customRules = []
|
|
1926
|
-
} = options;
|
|
1927
|
-
const lines = [];
|
|
1928
|
-
lines.push(system);
|
|
1929
|
-
lines.push("");
|
|
1930
|
-
const componentCount = catalog.componentNames.length;
|
|
1931
|
-
lines.push(`AVAILABLE COMPONENTS (${componentCount}):`);
|
|
1932
|
-
lines.push("");
|
|
1933
|
-
for (const name of catalog.componentNames) {
|
|
1934
|
-
const def = catalog.components[name];
|
|
1935
|
-
const props = extractPropsFromSchema(def.props);
|
|
1936
|
-
const propsStr = formatPropsCompact(props);
|
|
1937
|
-
const hasChildrenStr = def.hasChildren ? " Has children." : "";
|
|
1938
|
-
const descStr = def.description ? ` ${def.description}` : "";
|
|
1939
|
-
lines.push(`- ${String(name)}: ${propsStr}${descStr}${hasChildrenStr}`);
|
|
1940
|
-
}
|
|
1941
|
-
lines.push("");
|
|
1942
|
-
if (catalog.actionNames.length > 0) {
|
|
1943
|
-
lines.push("AVAILABLE ACTIONS:");
|
|
1944
|
-
lines.push("");
|
|
1945
|
-
for (const name of catalog.actionNames) {
|
|
1946
|
-
const def = catalog.actions[name];
|
|
1947
|
-
lines.push(
|
|
1948
|
-
`- ${String(name)}${def.description ? `: ${def.description}` : ""}`
|
|
1949
|
-
);
|
|
1950
|
-
}
|
|
1951
|
-
lines.push("");
|
|
1952
|
-
}
|
|
1953
|
-
lines.push("OUTPUT FORMAT (JSONL, RFC 6902 JSON Patch):");
|
|
1954
|
-
lines.push('{"op":"add","path":"/root","value":"element-key"}');
|
|
1955
|
-
lines.push(
|
|
1956
|
-
'{"op":"add","path":"/elements/key","value":{"type":"...","props":{...},"children":[...]}}'
|
|
1957
|
-
);
|
|
1958
|
-
lines.push('{"op":"remove","path":"/elements/key"}');
|
|
1959
|
-
lines.push("");
|
|
1960
|
-
lines.push("RULES:");
|
|
1961
|
-
const baseRules = [
|
|
1962
|
-
'First line sets /root to root element key: {"op":"add","path":"/root","value":"<key>"}',
|
|
1963
|
-
'Add elements with /elements/{key}: {"op":"add","path":"/elements/<key>","value":{...}}',
|
|
1964
|
-
"Remove elements with op:remove - also update the parent's children array to exclude the removed key",
|
|
1965
|
-
"Children array contains string keys, not objects",
|
|
1966
|
-
"Parent first, then children",
|
|
1967
|
-
"Each element needs: type, props",
|
|
1968
|
-
"ONLY use props listed above - never invent new props"
|
|
1969
|
-
];
|
|
1970
|
-
const allRules = [...baseRules, ...customRules];
|
|
1971
|
-
allRules.forEach((rule, i) => {
|
|
1972
|
-
lines.push(`${i + 1}. ${rule}`);
|
|
1973
|
-
});
|
|
1974
|
-
lines.push("");
|
|
1975
|
-
if (catalog.functionNames.length > 0) {
|
|
1976
|
-
lines.push("CUSTOM VALIDATION FUNCTIONS:");
|
|
1977
|
-
lines.push(catalog.functionNames.map(String).join(", "));
|
|
1978
|
-
lines.push("");
|
|
1979
|
-
}
|
|
1980
|
-
lines.push("Generate JSONL:");
|
|
1981
|
-
return lines.join("\n");
|
|
1982
|
-
}
|
|
1983
2076
|
export {
|
|
1984
2077
|
ActionBindingSchema,
|
|
1985
2078
|
ActionConfirmSchema,
|
|
@@ -1990,35 +2083,39 @@ export {
|
|
|
1990
2083
|
DynamicNumberSchema,
|
|
1991
2084
|
DynamicStringSchema,
|
|
1992
2085
|
DynamicValueSchema,
|
|
1993
|
-
|
|
2086
|
+
SPEC_DATA_PART,
|
|
2087
|
+
SPEC_DATA_PART_TYPE,
|
|
1994
2088
|
ValidationCheckSchema,
|
|
1995
2089
|
ValidationConfigSchema,
|
|
1996
2090
|
VisibilityConditionSchema,
|
|
1997
2091
|
action,
|
|
1998
2092
|
actionBinding,
|
|
1999
2093
|
addByPath,
|
|
2094
|
+
applySpecPatch,
|
|
2000
2095
|
applySpecStreamPatch,
|
|
2001
2096
|
autoFixSpec,
|
|
2002
2097
|
buildUserPrompt,
|
|
2003
2098
|
builtInValidationFunctions,
|
|
2004
2099
|
check,
|
|
2005
2100
|
compileSpecStream,
|
|
2006
|
-
|
|
2101
|
+
createJsonRenderTransform,
|
|
2102
|
+
createMixedStreamParser,
|
|
2007
2103
|
createSpecStreamCompiler,
|
|
2008
2104
|
defineCatalog,
|
|
2009
2105
|
defineSchema,
|
|
2010
|
-
evaluateLogicExpression,
|
|
2011
2106
|
evaluateVisibility,
|
|
2012
2107
|
executeAction,
|
|
2013
2108
|
findFormValue,
|
|
2014
2109
|
formatSpecIssues,
|
|
2015
|
-
generateCatalogPrompt,
|
|
2016
|
-
generateSystemPrompt,
|
|
2017
2110
|
getByPath,
|
|
2018
2111
|
interpolateString,
|
|
2112
|
+
nestedToFlat,
|
|
2019
2113
|
parseSpecStreamLine,
|
|
2114
|
+
pipeJsonRender,
|
|
2020
2115
|
removeByPath,
|
|
2021
2116
|
resolveAction,
|
|
2117
|
+
resolveActionParam,
|
|
2118
|
+
resolveBindings,
|
|
2022
2119
|
resolveDynamicValue,
|
|
2023
2120
|
resolveElementProps,
|
|
2024
2121
|
resolvePropValue,
|