@changfenhuang/dsh-genui 0.9.9 → 0.10.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/CHANGELOG.md +29 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/SKILL.md +128 -6
- package/lib/assets/echarts-core.js +43 -0
- package/lib/assets/{echarts.js → echarts-full.js} +1 -1
- package/lib/assets/mermaid.js +1 -1
- package/lib/client.js +5 -3
- package/lib/index.js +230 -69
- package/lib/types/client/EChartNode.d.ts +9 -0
- package/lib/types/client/GenuiBlock.d.ts +3 -4
- package/lib/types/client/asset-echarts-core.d.ts +1 -0
- package/lib/types/client/asset-echarts.d.ts +3 -2
- package/lib/types/client/asset-loader.d.ts +2 -2
- package/lib/types/client/blocks/advanced.d.ts +0 -3
- package/lib/types/client/blocks/charts.d.ts +23 -12
- package/lib/types/client/blocks/state.d.ts +5 -3
- package/lib/types/client/echarts-lazy.d.ts +4 -1
- package/lib/types/client/genui-runtime/schema.d.ts +11 -1
- package/lib/types/client/inline.d.ts +29 -0
- package/lib/types/client/spec.d.ts +76 -6
- package/lib/types/plugin/index.d.ts +1 -1
- package/package.json +4 -3
- package/src/client/EChartNode.tsx +233 -19
- package/src/client/GenuiBlock.module.css +895 -145
- package/src/client/GenuiBlock.tsx +31 -23
- package/src/client/PlotBlock.module.css +17 -1
- package/src/client/PlotBlock.tsx +33 -1
- package/src/client/asset-echarts-core.ts +39 -0
- package/src/client/asset-echarts.ts +4 -3
- package/src/client/asset-loader.ts +5 -3
- package/src/client/blocks/advanced.tsx +60 -16
- package/src/client/blocks/charts.tsx +761 -144
- package/src/client/blocks/render-node.tsx +252 -11
- package/src/client/blocks/state.ts +5 -3
- package/src/client/dom-fence.tsx +60 -6
- package/src/client/echarts-lazy.ts +11 -2
- package/src/client/fence-render.tsx +6 -5
- package/src/client/gallery.ts +78 -2
- package/src/client/genui-runtime/schema.ts +45 -11
- package/src/client/guard.ts +161 -23
- package/src/client/index.tsx +14 -3
- package/src/client/inline.ts +73 -0
- package/src/client/mermaid-core.ts +42 -5
- package/src/client/spec.ts +79 -6
- package/src/plugin/index.ts +33 -45
- package/src/plugin/tool.ts +5 -1
- package/tsdown.config.ts +2 -1
package/lib/index.js
CHANGED
|
@@ -138,7 +138,45 @@ const ECHART_PRESETS = [
|
|
|
138
138
|
"line",
|
|
139
139
|
"area",
|
|
140
140
|
"pie",
|
|
141
|
-
"scatter"
|
|
141
|
+
"scatter",
|
|
142
|
+
"radar",
|
|
143
|
+
"gauge",
|
|
144
|
+
"funnel",
|
|
145
|
+
"treemap",
|
|
146
|
+
"sankey",
|
|
147
|
+
"graph",
|
|
148
|
+
"heatmap",
|
|
149
|
+
"bigline"
|
|
150
|
+
];
|
|
151
|
+
/** Oversized single-number stat (one per fence as the visual anchor). */
|
|
152
|
+
const STAT_SIZES = ["hero"];
|
|
153
|
+
/** Progress shapes: a track (default) or a circular gauge. */
|
|
154
|
+
const PROGRESS_VARIANTS = ["bar", "ring"];
|
|
155
|
+
/** Semantic card surfaces. */
|
|
156
|
+
const CARD_TONES = [
|
|
157
|
+
"info",
|
|
158
|
+
"success",
|
|
159
|
+
"warning",
|
|
160
|
+
"danger"
|
|
161
|
+
];
|
|
162
|
+
/** Hero cover tones. */
|
|
163
|
+
const HERO_TONES = [
|
|
164
|
+
"accent",
|
|
165
|
+
"success",
|
|
166
|
+
"warning",
|
|
167
|
+
"danger"
|
|
168
|
+
];
|
|
169
|
+
/** Table cell renderers (`table.types`, one entry per column). */
|
|
170
|
+
const TABLE_CELL_TYPES = [
|
|
171
|
+
"text",
|
|
172
|
+
"num",
|
|
173
|
+
"delta",
|
|
174
|
+
"bar",
|
|
175
|
+
"badge",
|
|
176
|
+
"spark",
|
|
177
|
+
"ring",
|
|
178
|
+
"index",
|
|
179
|
+
"group"
|
|
142
180
|
];
|
|
143
181
|
const schema = (required, fields, aliases = {}, options = {}) => {
|
|
144
182
|
const optional = Object.fromEntries(Object.entries(fields).filter(([field]) => field !== "type" && !required.includes(field)));
|
|
@@ -166,7 +204,10 @@ const recordSchema = (required, fields, nested = {}, enums = {}) => ({
|
|
|
166
204
|
enums,
|
|
167
205
|
nested
|
|
168
206
|
});
|
|
169
|
-
const nodeFields = {
|
|
207
|
+
const nodeFields = {
|
|
208
|
+
type: "string",
|
|
209
|
+
span: "number"
|
|
210
|
+
};
|
|
170
211
|
const chartDatumSchema = recordSchema(["label", "value"], {
|
|
171
212
|
label: "string",
|
|
172
213
|
value: "number",
|
|
@@ -337,26 +378,25 @@ const COMPONENT_SCHEMAS = {
|
|
|
337
378
|
card: schema(["items"], {
|
|
338
379
|
...nodeFields,
|
|
339
380
|
title: "string",
|
|
340
|
-
items: "nodes"
|
|
381
|
+
items: "nodes",
|
|
382
|
+
tone: "string",
|
|
383
|
+
accent: "string"
|
|
341
384
|
}, {
|
|
342
385
|
label: "title",
|
|
343
386
|
content: "items"
|
|
344
|
-
}),
|
|
387
|
+
}, { enums: { tone: CARD_TONES } }),
|
|
345
388
|
chart: schema([], {
|
|
346
389
|
...nodeFields,
|
|
347
390
|
kind: "string",
|
|
348
391
|
data: "array",
|
|
349
|
-
series: "array"
|
|
392
|
+
series: "array",
|
|
393
|
+
horizontal: "boolean",
|
|
394
|
+
stacked: "boolean",
|
|
395
|
+
filter: "string",
|
|
396
|
+
palette: "array"
|
|
350
397
|
}, {}, {
|
|
351
398
|
oneOfRequired: [["data", "series"]],
|
|
352
399
|
conditionalRequired: [{
|
|
353
|
-
kind: "required-if",
|
|
354
|
-
when: {
|
|
355
|
-
field: "kind",
|
|
356
|
-
equals: "line"
|
|
357
|
-
},
|
|
358
|
-
required: ["data"]
|
|
359
|
-
}, {
|
|
360
400
|
kind: "required-if",
|
|
361
401
|
when: {
|
|
362
402
|
field: "kind",
|
|
@@ -426,12 +466,15 @@ const COMPONENT_SCHEMAS = {
|
|
|
426
466
|
preset: "string",
|
|
427
467
|
data: "array",
|
|
428
468
|
series: "array",
|
|
469
|
+
links: "array",
|
|
470
|
+
palette: "array",
|
|
429
471
|
option: "object"
|
|
430
472
|
}, {}, {
|
|
431
473
|
oneOfRequired: [[
|
|
432
474
|
"option",
|
|
433
475
|
"data",
|
|
434
|
-
"series"
|
|
476
|
+
"series",
|
|
477
|
+
"links"
|
|
435
478
|
]],
|
|
436
479
|
enums: { preset: ECHART_PRESETS }
|
|
437
480
|
}),
|
|
@@ -473,7 +516,8 @@ const COMPONENT_SCHEMAS = {
|
|
|
473
516
|
}),
|
|
474
517
|
list: schema(["items"], {
|
|
475
518
|
...nodeFields,
|
|
476
|
-
items: "array"
|
|
519
|
+
items: "array",
|
|
520
|
+
filter: "string"
|
|
477
521
|
}),
|
|
478
522
|
mermaid: schema(["code"], {
|
|
479
523
|
...nodeFields,
|
|
@@ -492,8 +536,10 @@ const COMPONENT_SCHEMAS = {
|
|
|
492
536
|
...nodeFields,
|
|
493
537
|
value: "number",
|
|
494
538
|
label: "string",
|
|
495
|
-
valueLabel: "string"
|
|
496
|
-
|
|
539
|
+
valueLabel: "string",
|
|
540
|
+
variant: "string",
|
|
541
|
+
target: "number"
|
|
542
|
+
}, {}, { enums: { variant: PROGRESS_VARIANTS } }),
|
|
497
543
|
quiz: schema(["question", "options"], {
|
|
498
544
|
...nodeFields,
|
|
499
545
|
question: "string",
|
|
@@ -544,12 +590,24 @@ const COMPONENT_SCHEMAS = {
|
|
|
544
590
|
id: "string"
|
|
545
591
|
}),
|
|
546
592
|
spacer: schema([], nodeFields),
|
|
593
|
+
hero: schema(["title"], {
|
|
594
|
+
...nodeFields,
|
|
595
|
+
title: "string",
|
|
596
|
+
subtitle: "string",
|
|
597
|
+
value: "string",
|
|
598
|
+
label: "string",
|
|
599
|
+
delta: "string",
|
|
600
|
+
spark: "array",
|
|
601
|
+
tone: "string"
|
|
602
|
+
}, {}, { enums: { tone: HERO_TONES } }),
|
|
547
603
|
stat: schema(["label", "value"], {
|
|
548
604
|
...nodeFields,
|
|
549
605
|
label: "string",
|
|
550
606
|
value: "string",
|
|
551
|
-
delta: "string"
|
|
552
|
-
|
|
607
|
+
delta: "string",
|
|
608
|
+
spark: "array",
|
|
609
|
+
size: "string"
|
|
610
|
+
}, {}, { enums: { size: STAT_SIZES } }),
|
|
553
611
|
steps: schema(["steps"], {
|
|
554
612
|
...nodeFields,
|
|
555
613
|
steps: "array",
|
|
@@ -571,7 +629,14 @@ const COMPONENT_SCHEMAS = {
|
|
|
571
629
|
table: schema(["columns", "rows"], {
|
|
572
630
|
...nodeFields,
|
|
573
631
|
columns: "array",
|
|
574
|
-
rows: "array"
|
|
632
|
+
rows: "array",
|
|
633
|
+
types: "array",
|
|
634
|
+
total: "boolean",
|
|
635
|
+
details: "array",
|
|
636
|
+
filter: "string",
|
|
637
|
+
filterColumn: "number",
|
|
638
|
+
sortField: "string",
|
|
639
|
+
export: "boolean"
|
|
575
640
|
}, {
|
|
576
641
|
headers: "columns",
|
|
577
642
|
data: "rows"
|
|
@@ -1026,7 +1091,41 @@ function repairItems(list, ctx, depth) {
|
|
|
1026
1091
|
}
|
|
1027
1092
|
return out;
|
|
1028
1093
|
}
|
|
1094
|
+
/** Optional explicit palette: up to 12 validated colour values. */
|
|
1095
|
+
function paletteValues(v) {
|
|
1096
|
+
if (!Array.isArray(v)) return void 0;
|
|
1097
|
+
const out = [];
|
|
1098
|
+
for (const item of v.slice(0, 12)) {
|
|
1099
|
+
const value = color(item);
|
|
1100
|
+
if (value === void 0) continue;
|
|
1101
|
+
out.push(value);
|
|
1102
|
+
}
|
|
1103
|
+
return out.length > 0 ? out : void 0;
|
|
1104
|
+
}
|
|
1105
|
+
/** Optional `stat.spark` series: finite numbers only, 2..60 points. */
|
|
1106
|
+
function sparkValues(v) {
|
|
1107
|
+
if (!Array.isArray(v)) return void 0;
|
|
1108
|
+
const out = [];
|
|
1109
|
+
for (const item of v.slice(0, 60)) {
|
|
1110
|
+
const n = typeof item === "number" ? item : Number(item);
|
|
1111
|
+
if (!Number.isFinite(n)) return void 0;
|
|
1112
|
+
out.push(n);
|
|
1113
|
+
}
|
|
1114
|
+
return out.length >= 2 ? out : void 0;
|
|
1115
|
+
}
|
|
1116
|
+
/** Layout hints are component-agnostic: `span` survives repair on ANY node
|
|
1117
|
+
* (the renderer applies it inside a grid). Kept out of the per-case switches
|
|
1118
|
+
* so a new component type cannot forget it. */
|
|
1029
1119
|
function repairNode(value, ctx, depth) {
|
|
1120
|
+
const node = repairNodeFields(value, ctx, depth);
|
|
1121
|
+
if (node === null) return null;
|
|
1122
|
+
const span = int(obj(value)?.span, 2, 12);
|
|
1123
|
+
return span === void 0 ? node : {
|
|
1124
|
+
...node,
|
|
1125
|
+
span
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
function repairNodeFields(value, ctx, depth) {
|
|
1030
1129
|
if (depth > GENUI_LIMITS.maxDepth) return null;
|
|
1031
1130
|
const v = obj(value);
|
|
1032
1131
|
if (v === void 0) return null;
|
|
@@ -1062,7 +1161,9 @@ function repairNode(value, ctx, depth) {
|
|
|
1062
1161
|
case "card": return {
|
|
1063
1162
|
type: "card",
|
|
1064
1163
|
items: repairItems(v.items, ctx, depth + 1),
|
|
1065
|
-
...opt("title", str(v.title, GENUI_LIMITS.maxString))
|
|
1164
|
+
...opt("title", str(v.title, GENUI_LIMITS.maxString)),
|
|
1165
|
+
...opt("tone", enu(v.tone, CARD_TONES)),
|
|
1166
|
+
...opt("accent", color(v.accent))
|
|
1066
1167
|
};
|
|
1067
1168
|
case "button": {
|
|
1068
1169
|
const label = str(v.label, GENUI_LIMITS.maxString);
|
|
@@ -1160,6 +1261,20 @@ function repairNode(value, ctx, depth) {
|
|
|
1160
1261
|
...opt("icon", str(v.icon, 64))
|
|
1161
1262
|
};
|
|
1162
1263
|
}
|
|
1264
|
+
case "hero": {
|
|
1265
|
+
const title = str(v.title, GENUI_LIMITS.maxString);
|
|
1266
|
+
if (title === void 0) return null;
|
|
1267
|
+
return {
|
|
1268
|
+
type: "hero",
|
|
1269
|
+
title,
|
|
1270
|
+
...opt("subtitle", str(v.subtitle, GENUI_LIMITS.maxString)),
|
|
1271
|
+
...opt("value", str(v.value, 128)),
|
|
1272
|
+
...opt("label", str(v.label, GENUI_LIMITS.maxString)),
|
|
1273
|
+
...opt("delta", str(v.delta, 64)),
|
|
1274
|
+
...opt("spark", sparkValues(v.spark)),
|
|
1275
|
+
...opt("tone", enu(v.tone, HERO_TONES))
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1163
1278
|
case "stat": {
|
|
1164
1279
|
const label = str(v.label, GENUI_LIMITS.maxString);
|
|
1165
1280
|
const value = str(v.value, 128);
|
|
@@ -1168,7 +1283,9 @@ function repairNode(value, ctx, depth) {
|
|
|
1168
1283
|
type: "stat",
|
|
1169
1284
|
label,
|
|
1170
1285
|
value,
|
|
1171
|
-
...opt("delta", str(v.delta, 64))
|
|
1286
|
+
...opt("delta", str(v.delta, 64)),
|
|
1287
|
+
...opt("spark", sparkValues(v.spark)),
|
|
1288
|
+
...opt("size", v.size === "hero" ? "hero" : void 0)
|
|
1172
1289
|
};
|
|
1173
1290
|
}
|
|
1174
1291
|
case "progress": {
|
|
@@ -1178,7 +1295,9 @@ function repairNode(value, ctx, depth) {
|
|
|
1178
1295
|
type: "progress",
|
|
1179
1296
|
value,
|
|
1180
1297
|
...opt("label", str(v.label, GENUI_LIMITS.maxString)),
|
|
1181
|
-
...opt("valueLabel", str(v.valueLabel, 64))
|
|
1298
|
+
...opt("valueLabel", str(v.valueLabel, 64)),
|
|
1299
|
+
...opt("variant", enu(v.variant, PROGRESS_VARIANTS)),
|
|
1300
|
+
...opt("target", num(v.target, 0, 100))
|
|
1182
1301
|
};
|
|
1183
1302
|
}
|
|
1184
1303
|
case "divider": return { type: "divider" };
|
|
@@ -1197,7 +1316,8 @@ function repairNode(value, ctx, depth) {
|
|
|
1197
1316
|
if (items === void 0) return null;
|
|
1198
1317
|
return {
|
|
1199
1318
|
type: "list",
|
|
1200
|
-
items
|
|
1319
|
+
items,
|
|
1320
|
+
...opt("filter", str(v.filter, 64))
|
|
1201
1321
|
};
|
|
1202
1322
|
}
|
|
1203
1323
|
case "table": {
|
|
@@ -1211,10 +1331,27 @@ function repairNode(value, ctx, depth) {
|
|
|
1211
1331
|
const columns = repairStrings(rawCols, GENUI_LIMITS.maxTableCols, 128);
|
|
1212
1332
|
const rows = repairRows(rawRows, GENUI_LIMITS.maxTableRows, GENUI_LIMITS.maxTableCols);
|
|
1213
1333
|
if (columns === void 0 || rows === void 0) return null;
|
|
1334
|
+
const rawTypes = Array.isArray(v.types) ? v.types : void 0;
|
|
1335
|
+
const types = rawTypes === void 0 ? void 0 : columns.map((_c, i) => {
|
|
1336
|
+
const raw = rawTypes[i];
|
|
1337
|
+
return typeof raw === "string" && TABLE_CELL_TYPES.includes(raw) ? raw : "text";
|
|
1338
|
+
});
|
|
1339
|
+
const rawDetails = Array.isArray(v.details) ? v.details : void 0;
|
|
1340
|
+
const details = rawDetails === void 0 ? void 0 : rows.map((_row, i) => {
|
|
1341
|
+
const entry = repairItems(rawDetails[i], ctx, depth + 1);
|
|
1342
|
+
return entry.length === 0 ? null : entry;
|
|
1343
|
+
});
|
|
1214
1344
|
return {
|
|
1215
1345
|
type: "table",
|
|
1216
1346
|
columns,
|
|
1217
|
-
rows
|
|
1347
|
+
rows,
|
|
1348
|
+
...opt("types", types),
|
|
1349
|
+
...opt("total", v.total === true ? true : void 0),
|
|
1350
|
+
...opt("export", v.export === true ? true : void 0),
|
|
1351
|
+
...opt("filter", str(v.filter, 64)),
|
|
1352
|
+
...opt("filterColumn", int(v.filterColumn, 0, GENUI_LIMITS.maxTableCols - 1)),
|
|
1353
|
+
...opt("sortField", str(v.sortField, 64)),
|
|
1354
|
+
...opt("details", details !== void 0 && details.some((d) => d !== null) ? details : void 0)
|
|
1218
1355
|
};
|
|
1219
1356
|
}
|
|
1220
1357
|
case "chart": {
|
|
@@ -1225,7 +1362,11 @@ function repairNode(value, ctx, depth) {
|
|
|
1225
1362
|
type: "chart",
|
|
1226
1363
|
data: data ?? [],
|
|
1227
1364
|
...opt("kind", enu(v.kind, CHART_KINDS)),
|
|
1228
|
-
...opt("series", series)
|
|
1365
|
+
...opt("series", series),
|
|
1366
|
+
...opt("horizontal", v.horizontal === true ? true : void 0),
|
|
1367
|
+
...opt("stacked", v.stacked === true ? true : void 0),
|
|
1368
|
+
...opt("filter", str(v.filter, 64)),
|
|
1369
|
+
...opt("palette", paletteValues(v.palette))
|
|
1229
1370
|
};
|
|
1230
1371
|
}
|
|
1231
1372
|
case "tabs": {
|
|
@@ -1439,9 +1580,20 @@ function repairNode(value, ctx, depth) {
|
|
|
1439
1580
|
case "echart": {
|
|
1440
1581
|
const data = v.data !== void 0 ? repairChartData(v.data, GENUI_LIMITS.maxChartPoints) : void 0;
|
|
1441
1582
|
const series = v.series !== void 0 && Array.isArray(v.series) ? repairSeries(v.series, GENUI_LIMITS.maxPlotSeries, GENUI_LIMITS.maxChartPoints) : void 0;
|
|
1583
|
+
const links = Array.isArray(v.links) ? v.links.slice(0, GENUI_LIMITS.maxChartPoints).flatMap((entry) => {
|
|
1584
|
+
const e = obj(entry);
|
|
1585
|
+
const from = e === void 0 ? void 0 : str(e.from, 64);
|
|
1586
|
+
const to = e === void 0 ? void 0 : str(e.to, 64);
|
|
1587
|
+
if (from === void 0 || to === void 0) return [];
|
|
1588
|
+
return [{
|
|
1589
|
+
from,
|
|
1590
|
+
to,
|
|
1591
|
+
...opt("value", num(e.value, 0, 1e9))
|
|
1592
|
+
}];
|
|
1593
|
+
}) : void 0;
|
|
1442
1594
|
const sanitized = v.option !== void 0 ? sanitizeEChartOption(v.option, 0, { count: GENUI_LIMITS.maxEChartOptionNodes }) : void 0;
|
|
1443
1595
|
const option = sanitized === void 0 || typeof sanitized !== "object" || sanitized === null || Array.isArray(sanitized) ? void 0 : sanitized;
|
|
1444
|
-
if (option === void 0 && data === void 0 && series === void 0) return null;
|
|
1596
|
+
if (option === void 0 && data === void 0 && series === void 0 && (links === void 0 || links.length === 0)) return null;
|
|
1445
1597
|
return {
|
|
1446
1598
|
type: "echart",
|
|
1447
1599
|
...opt("title", str(v.title, GENUI_LIMITS.maxString)),
|
|
@@ -1449,6 +1601,8 @@ function repairNode(value, ctx, depth) {
|
|
|
1449
1601
|
...opt("preset", enu(v.preset, ECHART_PRESETS)),
|
|
1450
1602
|
...opt("data", data),
|
|
1451
1603
|
...opt("series", series),
|
|
1604
|
+
...opt("links", links !== void 0 && links.length > 0 ? links : void 0),
|
|
1605
|
+
...opt("palette", paletteValues(v.palette)),
|
|
1452
1606
|
...opt("option", option)
|
|
1453
1607
|
};
|
|
1454
1608
|
}
|
|
@@ -2222,16 +2376,18 @@ function validateChartNode(v, at, errors) {
|
|
|
2222
2376
|
if (v.series !== void 0 && !Array.isArray(v.series)) errors.push(`${at}.series must be an array`);
|
|
2223
2377
|
if (v.kind !== void 0 && (typeof v.kind !== "string" || !CHART_KINDS.includes(v.kind))) errors.push(`${at}.kind must be bars, line, or donut`);
|
|
2224
2378
|
const kind = v.kind === void 0 ? "bars" : v.kind;
|
|
2225
|
-
|
|
2226
|
-
if (Array.isArray(v.series)) {
|
|
2227
|
-
|
|
2228
|
-
if (
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2379
|
+
const series = Array.isArray(v.series) ? v.series : void 0;
|
|
2380
|
+
if (Array.isArray(v.data) && v.data.length === 0 && (series === void 0 || series.length === 0)) errors.push(`${at}.data must not be empty`);
|
|
2381
|
+
if (series !== void 0) {
|
|
2382
|
+
if (series.length === 0) errors.push(`${at}.series must not be empty`);
|
|
2383
|
+
if (kind === "donut") errors.push(`${at}.series is only supported for bars and line`);
|
|
2384
|
+
for (let index = 0; index < series.length; index++) {
|
|
2385
|
+
const entry = obj(series[index]);
|
|
2386
|
+
if (entry !== void 0 && Array.isArray(entry.data) && entry.data.length === 0) errors.push(`${at}.series[${index}].data must not be empty`);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
if (kind === "donut" && v.data === void 0) errors.push(`${at}.data is required for donut`);
|
|
2390
|
+
if (kind === "line" && v.data === void 0 && (series === void 0 || series.length === 0)) errors.push(`${at}.data is required for line (or provide series)`);
|
|
2235
2391
|
validateChartData(v.data, `${at}.data`, errors);
|
|
2236
2392
|
validateChartSeries(v.series, `${at}.series`, errors);
|
|
2237
2393
|
}
|
|
@@ -2327,6 +2483,10 @@ function validateNode(value, depth, at, errors, walk) {
|
|
|
2327
2483
|
isStr("text");
|
|
2328
2484
|
isStr("value");
|
|
2329
2485
|
break;
|
|
2486
|
+
case "hero":
|
|
2487
|
+
if (typeof v.title !== "string") errors.push(`${at}: type 'hero' requires title (string)`);
|
|
2488
|
+
isStr("subtitle");
|
|
2489
|
+
break;
|
|
2330
2490
|
case "stat":
|
|
2331
2491
|
if (typeof v.label !== "string") errors.push(`${at}: type 'stat' requires label (string)`);
|
|
2332
2492
|
if (typeof v.value !== "string") errors.push(`${at}: type 'stat' requires value (string)`);
|
|
@@ -2349,6 +2509,8 @@ function validateNode(value, depth, at, errors, walk) {
|
|
|
2349
2509
|
case "table":
|
|
2350
2510
|
if (!Array.isArray(v.columns)) errors.push(`${at}: type 'table' requires columns (array)`);
|
|
2351
2511
|
if (!Array.isArray(v.rows)) errors.push(`${at}: type 'table' requires rows (array)`);
|
|
2512
|
+
if (v.types !== void 0 && !Array.isArray(v.types)) errors.push(`${at}.types must be an array of column cell types`);
|
|
2513
|
+
if (v.details !== void 0 && !Array.isArray(v.details)) errors.push(`${at}.details must be an array aligned with rows`);
|
|
2352
2514
|
validateTableRows(v.rows, `${at}.rows`, errors);
|
|
2353
2515
|
break;
|
|
2354
2516
|
case "chart":
|
|
@@ -2427,7 +2589,7 @@ function validateNode(value, depth, at, errors, walk) {
|
|
|
2427
2589
|
if (v.edges !== void 0 && !Array.isArray(v.edges)) errors.push(`${at}: type 'diagram' requires edges (array) when present`);
|
|
2428
2590
|
break;
|
|
2429
2591
|
case "echart":
|
|
2430
|
-
if (v.option === void 0 && v.data === void 0 && v.series === void 0) errors.push(`${at}: type 'echart' requires option, data, or
|
|
2592
|
+
if (v.option === void 0 && v.data === void 0 && v.series === void 0 && (!Array.isArray(v.links) || v.links.length === 0)) errors.push(`${at}: type 'echart' requires option, data, series, or links`);
|
|
2431
2593
|
isNum("height");
|
|
2432
2594
|
break;
|
|
2433
2595
|
default: errors.push(`${at}: unknown type '${type}' (custom renderer?)`);
|
|
@@ -2562,7 +2724,11 @@ const RENDER_UI_PARAMETERS = {
|
|
|
2562
2724
|
type: "object",
|
|
2563
2725
|
properties: { spec: {
|
|
2564
2726
|
type: "object",
|
|
2565
|
-
description:
|
|
2727
|
+
description: [
|
|
2728
|
+
"Render structured UI for the user (tool-row card). USE THIS whenever the answer contains ≥3 parallel points, a comparison, numbers/metrics, a step sequence, a flow, or a status/report — do NOT write those as markdown bullets or a markdown table.",
|
|
2729
|
+
"Same white-listed vocabulary as the ```dsh-ui fence (see the GenUI system-prompt section). Pick the fence when the UI belongs in the message body; pick this tool when the deliverable is a self-contained card.",
|
|
2730
|
+
"Deep-validated and repaired by the renderer. Pass the spec as a JSON OBJECT — never as a serialized JSON string (a string fails argument validation)."
|
|
2731
|
+
].join(" "),
|
|
2566
2732
|
properties: {
|
|
2567
2733
|
title: {
|
|
2568
2734
|
type: "string",
|
|
@@ -2880,9 +3046,9 @@ function createValidateDshUiTool() {
|
|
|
2880
3046
|
* This route serves them from the plugin's own package directory through the
|
|
2881
3047
|
* host webserver service — the longest-prefix rule lets it win over the
|
|
2882
3048
|
* generic `/plugins` bundle route, and no host source change is needed. The
|
|
2883
|
-
* service is optional at this plugin's start time
|
|
2884
|
-
* the
|
|
2885
|
-
*
|
|
3049
|
+
* service is optional at this plugin's start time, so a dependency fiber owns
|
|
3050
|
+
* the registration and follows the webserver through late binding, replacement,
|
|
3051
|
+
* and plugin reloads.
|
|
2886
3052
|
*/
|
|
2887
3053
|
/** Route prefix under /plugins; anything under it is this plugin's asset. */
|
|
2888
3054
|
const ASSET_ROUTE_PATH = "/plugins/@changfenhuang/dsh-genui/assets";
|
|
@@ -2940,16 +3106,26 @@ const GENUI_SECTION_TEXT = `You can render interactive UI components INSIDE your
|
|
|
2940
3106
|
|
|
2941
3107
|
The spec is a white-listed component tree rendered inline where the fence sits. Only these \`type\` values; the \`genui\` skill, when available, carries the full content→component mapping and per-component field details:
|
|
2942
3108
|
|
|
2943
|
-
- 布局: text · row · col · grid · card · divider · spacer
|
|
3109
|
+
- 布局: text · row · col · grid · card · divider · spacer · hero(封面块:超大数字 + 标题 + tone 渐变底色,一条回答最多一个)
|
|
2944
3110
|
- 展示: badge · stat · progress · list · table · keyvalue · avatar · audio · video · timeline · file-tree · breadcrumb · callout · steps · diff · json · code · copy
|
|
2945
|
-
- 图表: chart {"
|
|
3111
|
+
- 图表: chart {"kind":"bars|line|donut","data":[{"label":"...","value":n}],"series":[{"label":"...","data":[...]}]?,"horizontal":true?,"stacked":true?}(series:bars 分组/堆叠 / line 多序列;horizontal 横向柱) · echart (preset: bar/line/area/pie/scatter/radar/gauge/funnel/treemap/sankey/graph/heatmap/bigline,或 option 直通) · plot (函数图)
|
|
2946
3112
|
- 交互: button · input · textarea · select · checkbox · switch · slider · radio · submit · quiz · link · tabs · accordion
|
|
2947
3113
|
- 高级: mermaid (flowchart/sequence/class/gantt/pie/er/state/journey) · diagram (编辑级架构/流程图,27 种 kind) · scene3d (3D WebGL)
|
|
2948
3114
|
|
|
3115
|
+
**默认就该出 UI**:出现下列情况至少出一个围栏:
|
|
3116
|
+
- ≥3 条并列要点 → \`list\`;数字对比 → \`table\`;指标/进度/状态 → \`stat\`/\`progress\`/\`badge\`
|
|
3117
|
+
- 步骤/时间线 → \`steps\`/\`timeline\`/\`mermaid\`;架构/流程 → \`diagram\` 或 \`mermaid\`;风险/结论 → \`callout\`;代码/改动 → \`code\`/\`diff\`/\`json\`
|
|
3118
|
+
- 行内富文本:\`text\`/\`list\`/表格文本列/\`keyvalue\`/\`callout\` 里可写 \`code\`、**加粗**、==高亮==、[文字](url):重点留在句中,不必为一个词单起组件。
|
|
3119
|
+
- 默认无卡 ≠ 少用组件:硬触发照常出组件,**组件多不是问题**——判据是每个组件承载不同信息、有焦点与层次、同一批数据不重复表达。卡片只用于并排项与数据对象;单段文字用「标题 + 正文 + 间距」。
|
|
3120
|
+
|
|
3121
|
+
**发回答前最后自检一次**:这段内容里有没有 ≥3 条并列要点、任何对比、任何数字/指标、任何步骤或流程?有就先转成组件再开口。**状态汇报、进度说明、提交与改动清单同样算**——不要因为它是"说明文"就用纯文字写。这一条踩过的坑:连续几条汇报全靠文字,一条围栏都没发。
|
|
3122
|
+
- 趋势/占比 → \`chart\`(≤8 点)或 \`echart\`(多序列/要交互时);配色默认跟随主题,只有语义需要时才用 \`palette\` / \`card.accent\`;排版用 grid 子节点的 \`"span":2\` 跨列做宽窄混排(bento),不要一列方块堆到底;数据多时给 \`table\`/\`chart\`/\`list\` 配一个 \`input\`(id) + \`filter\` 绑定,读者能就地筛选,不用再问一遍
|
|
3123
|
+
|
|
3124
|
+
**字段速查**(完整见 genui skill):\`stat\` \`{"label","value","delta"?}\` · \`table\` \`{"columns","rows","types"?,"total"?,"details"?,"filter"?,"export"?}\` · \`callout\` \`{"tone","title","content"}\` · \`progress\` \`{"value","variant"?,"target"?}\`
|
|
3125
|
+
|
|
2949
3126
|
Rules:
|
|
2950
|
-
- 触发: 结构化表达优于纯文本时主动用(要点、强调、对比、流程、步骤、状态、数据、演示),纯问答与一句话不套 UI;一个主题一个主组件,每次 3–8 个组件,同一数据不重复出现。
|
|
2951
3127
|
- JSON 严格: 坏围栏降级为代码块;≥3 节点或含 table 的围栏发出前调用 validate_dsh_ui,❌ 修好再发(若附「已自动修复」JSON 照抄即可)。
|
|
2952
|
-
- 规模: ≤200 节点、嵌套≤8
|
|
3128
|
+
- 规模: ≤200 节点、嵌套≤8 层(超出被截断);一条回答 3–8 个组件,一个主题一个主组件;3D mesh 1–5;plot 给合理 xMin/xMax。
|
|
2953
3129
|
- LOCAL-FIRST + actions: UI 能自己做的状态变化(判卷、判题、重置、展开、选中)就地完成,零往返;action 只用于必须模型参与的事。交互组件带 "action":"name",交互以 [genui-action] name + 组件数据回传,届时重渲染更新 UI;无 action 的按钮禁用。
|
|
2954
3130
|
- Durable state: 交互状态按「会话+内容指纹」持久化——刷新/重放恢复;重渲染相同内容保留,新内容重置。
|
|
2955
3131
|
- 卷子模式: 每题一个 radio(group+answer+explanation)+ 一个 submit(groups 全列),本地判分。
|
|
@@ -3012,37 +3188,22 @@ function apply(ctx) {
|
|
|
3012
3188
|
order: ctx.systemPrompt.getSectionOrder("STRUCTURED_OUTPUT"),
|
|
3013
3189
|
text: GENUI_SECTION_TEXT
|
|
3014
3190
|
});
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
tools.register(createRenderUiTool());
|
|
3021
|
-
tools.register(createValidateDshUiTool());
|
|
3022
|
-
registered = true;
|
|
3023
|
-
};
|
|
3024
|
-
tryRegister(void 0);
|
|
3025
|
-
ctx.on("internal/service", (name, value) => {
|
|
3026
|
-
if (name === "tools") tryRegister(value);
|
|
3191
|
+
ctx.inject(["tools"], (toolsCtx) => {
|
|
3192
|
+
toolsCtx.effect(function* () {
|
|
3193
|
+
yield toolsCtx.tools.register(createRenderUiTool());
|
|
3194
|
+
yield toolsCtx.tools.register(createValidateDshUiTool());
|
|
3195
|
+
}, "dsh-genui: model tools");
|
|
3027
3196
|
});
|
|
3028
3197
|
ctx.inject(["skills"], (skillCtx) => {
|
|
3029
3198
|
skillCtx.skills.registerProvider(() => bundledSkillProvider());
|
|
3030
3199
|
});
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
const webServer = value ?? ctx.reflect.get("webServer", false);
|
|
3035
|
-
if (webServer === void 0) return;
|
|
3036
|
-
webServer.register({
|
|
3200
|
+
ctx.inject(["webServer"], (webCtx) => {
|
|
3201
|
+
const webServer = webCtx.reflect.get("webServer");
|
|
3202
|
+
webCtx.effect(() => webServer.register({
|
|
3037
3203
|
kind: "prefix",
|
|
3038
3204
|
path: ASSET_ROUTE_PATH,
|
|
3039
3205
|
handler: serveGenuiAsset
|
|
3040
|
-
});
|
|
3041
|
-
assetsRegistered = true;
|
|
3042
|
-
};
|
|
3043
|
-
tryRegisterAssets(void 0);
|
|
3044
|
-
ctx.on("internal/service", (name, value) => {
|
|
3045
|
-
if (name === "webServer") tryRegisterAssets(value);
|
|
3206
|
+
}), "dsh-genui: asset route");
|
|
3046
3207
|
});
|
|
3047
3208
|
}
|
|
3048
3209
|
//#endregion
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { GenuiEChart } from './spec.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Categorical fallback palette. The host defines its `--dsw-static-*` tokens on
|
|
4
|
+
* `body`, not on `:root`, and ECharts renders to CANVAS (so a `var(--x)` string
|
|
5
|
+
* is meaningless to it — every colour must be resolved to a literal first).
|
|
6
|
+
* Reading only `document.documentElement` therefore returned '' for every
|
|
7
|
+
* series colour and collapsed multi-series charts to one accent hue; these
|
|
8
|
+
* fixed hues keep series distinguishable on any host.
|
|
9
|
+
*/
|
|
10
|
+
export declare const SERIES_FALLBACK: readonly ["#679efe", "#4ed17e", "#f5b83d", "#f2707a", "#8b7ff0", "#3fc7d4", "#b7c8fe", "#9aa3b2"];
|
|
2
11
|
export declare function EChartNode({ node }: {
|
|
3
12
|
node: GenuiEChart;
|
|
4
13
|
}): import("react").JSX.Element;
|
|
@@ -2,9 +2,8 @@ import type { GenuiBlockProps } from './blocks/state.ts';
|
|
|
2
2
|
export declare const GENUI_ACTION_DEBOUNCE_MS = 300;
|
|
3
3
|
/**
|
|
4
4
|
* Render a GenUI spec as an inline block. `stateKey` is also the durable
|
|
5
|
-
* component identity
|
|
6
|
-
*
|
|
7
|
-
* key
|
|
8
|
-
* volatile instance, preserving local interaction state as the spec grows.
|
|
5
|
+
* component identity. A streaming instance adopts its first durable key so
|
|
6
|
+
* inputs and pending actions survive settling. Leaving an existing durable
|
|
7
|
+
* key remounts, keeping different blocks' interaction state isolated.
|
|
9
8
|
*/
|
|
10
9
|
export declare const GenuiBlock: import("react").NamedExoticComponent<GenuiBlockProps>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ECharts asset-bundle entry:
|
|
3
|
-
*
|
|
2
|
+
* ECharts FULL asset-bundle entry: the complete engine, fetched only when a
|
|
3
|
+
* spec needs a chart type outside the core set or ships a raw `option`.
|
|
4
|
+
* Registers on `window.__GenuiAssets__.echartsFull`. Built as a standalone IIFE into
|
|
4
5
|
* `lib/assets/echarts.js` and served by the plugin's node-half route; loaded
|
|
5
6
|
* on demand by echarts-lazy when a spec contains an `echart` node.
|
|
6
7
|
* @module @changfenhuang/dsh-genui/client/asset-echarts
|
|
@@ -25,7 +25,7 @@ export declare function assetUrl(file: string): string;
|
|
|
25
25
|
* file: repeated requests (several mermaid nodes, re-renders) share one
|
|
26
26
|
* script load; a failed load stays failed for the page (the component shows
|
|
27
27
|
* its fallback).
|
|
28
|
-
* @param name - 'mermaid', 'three', or 'echarts'.
|
|
28
|
+
* @param name - 'mermaid', 'three', 'echarts-core', or 'echarts-full'.
|
|
29
29
|
* @returns the registered engine surface.
|
|
30
30
|
*/
|
|
31
|
-
export declare function loadGenuiAsset<T>(name: 'mermaid' | 'three' | 'echarts'): Promise<T>;
|
|
31
|
+
export declare function loadGenuiAsset<T>(name: 'mermaid' | 'three' | 'echarts-core' | 'echarts-full'): Promise<T>;
|
|
@@ -75,9 +75,6 @@ export declare const Scene3DNode: import("react").NamedExoticComponent<{
|
|
|
75
75
|
export declare const TimelineNode: import("react").NamedExoticComponent<{
|
|
76
76
|
node: GenuiTimeline;
|
|
77
77
|
}>;
|
|
78
|
-
/** FileTree: indented tree of files and folders. Directory rows are LOCAL
|
|
79
|
-
* collapsible (spec.ts promised "collapsible children"; this makes it true)
|
|
80
|
-
* — click a dir to fold/unfold, default fully open. Zero model round trip. */
|
|
81
78
|
export declare const FileTreeNode: import("react").NamedExoticComponent<{
|
|
82
79
|
node: GenuiFileTree;
|
|
83
80
|
}>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
1
2
|
import type { GenuiChart, GenuiTable } from '../spec.ts';
|
|
2
3
|
export declare const CHART_COLORS: string[];
|
|
3
4
|
/**
|
|
@@ -9,30 +10,40 @@ export declare const CHART_COLORS: string[];
|
|
|
9
10
|
* (numbers first, then text).
|
|
10
11
|
*/
|
|
11
12
|
export declare function parseSortableNumber(v: unknown): number;
|
|
13
|
+
/** Markdown table text for the 复制 Markdown chip (pipes escaped). */
|
|
14
|
+
export declare function tableToMarkdown(columns: string[], rows: Array<Array<string | number>>): string;
|
|
15
|
+
/** RFC 4180 CSV text for the 复制 CSV chip. */
|
|
16
|
+
export declare function tableToCsv(rows: Array<Array<string | number>>): string;
|
|
12
17
|
export declare const TableNode: import("react").NamedExoticComponent<{
|
|
13
18
|
node: GenuiTable;
|
|
19
|
+
/** Live value of `node.filter`: substring-match rows locally. */
|
|
20
|
+
filterValue?: string | undefined;
|
|
21
|
+
/** Live value of `node.sortField`: a column header to sort by. */
|
|
22
|
+
sortValue?: string | undefined;
|
|
23
|
+
/** Renders a row's detail nodes. Supplied by render-node so the table never
|
|
24
|
+
* has to import the renderer back (import cycle). */
|
|
25
|
+
renderDetail?: ((items: NonNullable<GenuiTable["details"]>[number] & object[]) => ReactNode) | undefined;
|
|
14
26
|
}>;
|
|
15
|
-
/** Chart: bars (default), line (trend), or donut (share); multi-series bars via `series`.
|
|
16
|
-
* memoized: the spec node is a stable reference, so a keystroke in a sibling
|
|
17
|
-
* field no longer re-renders the whole chart. */
|
|
27
|
+
/** Chart: bars (default), line (trend), or donut (share); multi-series bars via `series`. */
|
|
18
28
|
export declare const ChartNode: import("react").NamedExoticComponent<{
|
|
19
29
|
chart: GenuiChart;
|
|
30
|
+
/** Live value of the control bound via `chart.filter`; keeps matching
|
|
31
|
+
* categories only (label substring), so a chart is explorable locally. */
|
|
32
|
+
filterValue?: string | undefined;
|
|
20
33
|
}>;
|
|
21
|
-
/** Bars: one column per datum (grouped bars when `series` is present).
|
|
34
|
+
/** Bars: one column per datum (grouped bars when `series` is present).
|
|
35
|
+
* Single-series bars render against a true zero line, so negative values
|
|
36
|
+
* draw downward instead of clamping to zero height. */
|
|
22
37
|
export declare const BarsNode: import("react").NamedExoticComponent<{
|
|
23
38
|
chart: GenuiChart;
|
|
24
39
|
}>;
|
|
25
|
-
/** Line: polyline
|
|
26
|
-
*
|
|
40
|
+
/** Line: responsive polyline with an area wash, nice y ticks and sampled x
|
|
41
|
+
* labels drawn in SVG at their real size. */
|
|
27
42
|
export declare const LineChartNode: import("react").NamedExoticComponent<{
|
|
28
43
|
chart: GenuiChart;
|
|
29
44
|
}>;
|
|
30
|
-
/** Donut: share of total with a center total
|
|
31
|
-
*
|
|
32
|
-
* stroke-dasharray and the browser drew the FULL circle instead). */
|
|
45
|
+
/** Donut: share of total with a center total and a legend that shows each
|
|
46
|
+
* slice's value AND percentage (the old legend was unstyled text). */
|
|
33
47
|
export declare const DonutNode: import("react").NamedExoticComponent<{
|
|
34
48
|
chart: GenuiChart;
|
|
35
49
|
}>;
|
|
36
|
-
/** Tab strip with local active-tab state. Keyboard: ArrowLeft/Right to move,
|
|
37
|
-
* Home/End to jump; ids wired via useId so `aria-controls` stays unique
|
|
38
|
-
* across fences and sessions. */
|
|
@@ -19,11 +19,13 @@ export interface GenuiBlockProps {
|
|
|
19
19
|
* v2.7: durable-state key (session + slot + content fingerprint). When set,
|
|
20
20
|
* interaction state (radio answers, checkbox groups, submit lock, field
|
|
21
21
|
* values) persists to localStorage and restores on refresh / re-render of
|
|
22
|
-
* the same content. Changing
|
|
23
|
-
*
|
|
24
|
-
* owns the matching React identity.
|
|
22
|
+
* the same content. Changing an existing key starts a fresh interaction
|
|
23
|
+
* lifetime; a streaming instance adopts its first key without losing input.
|
|
24
|
+
* GenuiBlock owns the matching React identity.
|
|
25
25
|
*/
|
|
26
26
|
stateKey?: string | undefined;
|
|
27
|
+
/** Animate newly arriving items; settled message replays opt out. */
|
|
28
|
+
animateEntrance?: boolean | undefined;
|
|
27
29
|
}
|
|
28
30
|
/** Per-question metadata registered by grouped radios for local grading. */
|
|
29
31
|
export interface QuestionMeta {
|