@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/SKILL.md +128 -6
  5. package/lib/assets/echarts-core.js +43 -0
  6. package/lib/assets/{echarts.js → echarts-full.js} +1 -1
  7. package/lib/assets/mermaid.js +1 -1
  8. package/lib/client.js +5 -3
  9. package/lib/index.js +230 -69
  10. package/lib/types/client/EChartNode.d.ts +9 -0
  11. package/lib/types/client/GenuiBlock.d.ts +3 -4
  12. package/lib/types/client/asset-echarts-core.d.ts +1 -0
  13. package/lib/types/client/asset-echarts.d.ts +3 -2
  14. package/lib/types/client/asset-loader.d.ts +2 -2
  15. package/lib/types/client/blocks/advanced.d.ts +0 -3
  16. package/lib/types/client/blocks/charts.d.ts +23 -12
  17. package/lib/types/client/blocks/state.d.ts +5 -3
  18. package/lib/types/client/echarts-lazy.d.ts +4 -1
  19. package/lib/types/client/genui-runtime/schema.d.ts +11 -1
  20. package/lib/types/client/inline.d.ts +29 -0
  21. package/lib/types/client/spec.d.ts +76 -6
  22. package/lib/types/plugin/index.d.ts +1 -1
  23. package/package.json +4 -3
  24. package/src/client/EChartNode.tsx +233 -19
  25. package/src/client/GenuiBlock.module.css +895 -145
  26. package/src/client/GenuiBlock.tsx +31 -23
  27. package/src/client/PlotBlock.module.css +17 -1
  28. package/src/client/PlotBlock.tsx +33 -1
  29. package/src/client/asset-echarts-core.ts +39 -0
  30. package/src/client/asset-echarts.ts +4 -3
  31. package/src/client/asset-loader.ts +5 -3
  32. package/src/client/blocks/advanced.tsx +60 -16
  33. package/src/client/blocks/charts.tsx +761 -144
  34. package/src/client/blocks/render-node.tsx +252 -11
  35. package/src/client/blocks/state.ts +5 -3
  36. package/src/client/dom-fence.tsx +60 -6
  37. package/src/client/echarts-lazy.ts +11 -2
  38. package/src/client/fence-render.tsx +6 -5
  39. package/src/client/gallery.ts +78 -2
  40. package/src/client/genui-runtime/schema.ts +45 -11
  41. package/src/client/guard.ts +161 -23
  42. package/src/client/index.tsx +14 -3
  43. package/src/client/inline.ts +73 -0
  44. package/src/client/mermaid-core.ts +42 -5
  45. package/src/client/spec.ts +79 -6
  46. package/src/plugin/index.ts +33 -45
  47. package/src/plugin/tool.ts +5 -1
  48. 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 = { type: "string" };
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
- if (Array.isArray(v.data) && v.data.length === 0) errors.push(`${at}.data must not be empty`);
2226
- if (Array.isArray(v.series)) {
2227
- if (v.series.length === 0) errors.push(`${at}.series must not be empty`);
2228
- if (kind === "line" || kind === "donut") errors.push(`${at}.series is only supported for bars`);
2229
- for (let index = 0; index < v.series.length; index++) {
2230
- const series = obj(v.series[index]);
2231
- if (series !== void 0 && Array.isArray(series.data) && series.data.length === 0) errors.push(`${at}.series[${index}].data must not be empty`);
2232
- }
2233
- }
2234
- if ((kind === "line" || kind === "donut") && v.data === void 0) errors.push(`${at}.data is required for ${kind}`);
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 series`);
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: "GenUI component tree (white-listed vocabulary, see the dsh-ui fence section in the system prompt). 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).",
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 (same ordering reality as
2884
- * the tools registry), so registration probes immediately AND on the
2885
- * `internal/service` event, exactly like the tools registration below.
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 {"type":"chart","kind":"bars|line|donut","data":[{"label":"...","value":n,"color":"#hex?"}],"series":[...]?}(series 仅 bars;未知扩展字段可通过校验,但原生 chart repair/render 会忽略) · echart (preset|option) · plot (函数图)
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 层(超出被截断);3D mesh 1–5;plot 给合理 xMin/xMax。
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
- let registered = false;
3016
- const tryRegister = (value) => {
3017
- if (registered) return;
3018
- const tools = value ?? ctx.reflect.get("tools", false);
3019
- if (tools === void 0) return;
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
- let assetsRegistered = false;
3032
- const tryRegisterAssets = (value) => {
3033
- if (assetsRegistered) return;
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: changing it remounts the stateful implementation so
6
- * state loaded for one block can never leak into or be saved under another
7
- * key. Identity-less streaming renders deliberately share one stable
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: registers the echarts engine on
3
- * `window.__GenuiAssets__.echarts`. Built as a standalone IIFE into
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 over a fixed-height plot area with a readable Y axis —
26
- * four evenly spaced gridlines + tick labels (design system v2 skeleton). */
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. Negative values contribute
31
- * zero arc (a negative dasharray segment used to produce an invalid
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 this key also starts a fresh in-memory
23
- * interaction-state lifetime; callers only provide stateKey — GenuiBlock
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 {