@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
@@ -10,7 +10,13 @@
10
10
  */
11
11
 
12
12
  /** One node in the component tree. */
13
- export type GenuiNode =
13
+ /** Layout hints accepted by every node: `span` is how many columns the node
14
+ * occupies as a direct child of a `grid` (bento layouts). */
15
+ export interface GenuiLayoutHints {
16
+ span?: number
17
+ }
18
+
19
+ export type GenuiNode = (
14
20
  | GenuiText
15
21
  | GenuiRow
16
22
  | GenuiCol
@@ -26,6 +32,7 @@ export type GenuiNode =
26
32
  | GenuiVideo
27
33
  | GenuiBadge
28
34
  | GenuiStat
35
+ | GenuiHero
29
36
  | GenuiProgress
30
37
  | GenuiDivider
31
38
  | GenuiList
@@ -55,6 +62,7 @@ export type GenuiNode =
55
62
  | GenuiBreadcrumb
56
63
  | GenuiQuiz
57
64
  | GenuiDiagram
65
+ ) & GenuiLayoutHints
58
66
 
59
67
  | GenuiEChart
60
68
 
@@ -194,19 +202,41 @@ export interface GenuiBadge {
194
202
  icon?: string
195
203
  }
196
204
 
205
+ /** Cover band: the answer's focal point. One per fence — an eyebrow label, an
206
+ * oversized metric, a title and a subtitle on a soft tinted surface. */
207
+ export interface GenuiHero {
208
+ type: 'hero'
209
+ title: string
210
+ subtitle?: string
211
+ /** Oversized metric, e.g. "99.96%" / "3.2x". */
212
+ value?: string
213
+ /** Eyebrow label above the metric. */
214
+ label?: string
215
+ delta?: string
216
+ spark?: number[]
217
+ tone?: 'accent' | 'success' | 'warning' | 'danger'
218
+ }
219
+
197
220
  export interface GenuiStat {
198
221
  type: 'stat'
199
222
  label: string
200
223
  value: string
201
224
  delta?: string
225
+ /** Optional micro trend line (finite numbers, 2..60 points). */
226
+ spark?: number[]
227
+ /** `hero` renders one oversized number — use it once per fence as the anchor. */
228
+ size?: 'hero'
202
229
  }
203
230
 
204
231
  export interface GenuiProgress {
205
232
  type: 'progress'
206
- label?: string
207
- /** 0..100 */
208
233
  value: number
234
+ label?: string
209
235
  valueLabel?: string
236
+ /** `bar` (default) draws a track; `ring` draws a circular gauge. */
237
+ variant?: 'bar' | 'ring'
238
+ /** Optional target marker on the bar track (0-100). */
239
+ target?: number
210
240
  }
211
241
 
212
242
  export interface GenuiDivider {
@@ -247,11 +277,17 @@ export interface GenuiGrid {
247
277
  export interface GenuiCard {
248
278
  type: 'card'
249
279
  title?: string
280
+ /** Semantic tint for the card surface (default: neutral). */
281
+ tone?: 'info' | 'success' | 'warning' | 'danger'
282
+ /** Explicit accent (hex) driving the border, title and wash. */
283
+ accent?: string
250
284
  items: GenuiNode[]
251
285
  }
252
286
 
253
287
  export interface GenuiList {
254
288
  type: 'list'
289
+ /** Field id of an input/select: its value filters the items locally. */
290
+ filter?: string
255
291
  items: Array<string | { title: string; desc?: string } | GenuiNode>
256
292
  }
257
293
 
@@ -259,7 +295,30 @@ export interface GenuiTable {
259
295
  type: 'table'
260
296
  columns: string[]
261
297
  rows: Array<Array<string | number>>
262
- }
298
+ /** Per-column cell type; missing = auto (numeric right-align, signed delta). */
299
+ types?: TableCellType[]
300
+ /** Append a 合计 footer row (numeric columns are summed). */
301
+ total?: boolean
302
+ /** Show 复制 Markdown / 复制 CSV chips above the table. */
303
+ export?: boolean
304
+ /** Optional master-detail payload, positionally aligned with `rows`:
305
+ * `details[i]` is what row i expands into (omit / empty = not expandable). */
306
+ details?: Array<GenuiNode[] | null>
307
+ /** Field id of an input/select: its live value filters the rows locally
308
+ * (substring match), so the table is searchable without a model round trip. */
309
+ filter?: string
310
+ /** Restrict `filter` to one column index (default: every column). */
311
+ filterColumn?: number
312
+ /** Field id of a select whose value is a column header: sorts by it locally. */
313
+ sortField?: string
314
+ }
315
+
316
+ /** How a table column's cells render.
317
+ * - `bar` / `ring`: the cell is read as 0-100
318
+ * - `spark`: the cell is a number list ("3,5,4,8") drawn as a mini trend
319
+ * - `index`: the 1-based row number (cell content is ignored)
320
+ * - `delta` / `num` / `badge` / `text`: text treatments. */
321
+ export type TableCellType = 'text' | 'num' | 'delta' | 'bar' | 'badge' | 'spark' | 'ring' | 'index' | 'group'
263
322
 
264
323
  export interface GenuiChartDatum {
265
324
  label: string
@@ -272,8 +331,16 @@ export interface GenuiChart {
272
331
  /** Chart shape: bars (default), line (trend), donut (share). */
273
332
  kind?: 'bars' | 'line' | 'donut'
274
333
  data: GenuiChartDatum[]
275
- /** Multi-series grouped bars: one series of data per entry. */
334
+ /** Multi-series: grouped bars, or one line per entry when kind is line. */
276
335
  series?: Array<{ label: string; color?: string; data: GenuiChartDatum[] }>
336
+ /** Bars only: horizontal bars (rankings, long category labels). */
337
+ horizontal?: boolean
338
+ /** Bars + series: stack the series instead of grouping them. */
339
+ stacked?: boolean
340
+ /** Explicit categorical palette (hex) — overrides the host theme colours. */
341
+ palette?: string[]
342
+ /** Field id of an input/select: its value filters the categories locally. */
343
+ filter?: string
277
344
  }
278
345
 
279
346
  export interface GenuiTab {
@@ -691,7 +758,9 @@ export interface GenuiDiagram {
691
758
 
692
759
  /** Preset chart kinds the `echart` node can build from `data`/`series` without
693
760
  * a full ECharts option. Each maps to a themed option template. */
694
- export type EChartPreset = 'bar' | 'line' | 'area' | 'pie' | 'scatter'
761
+ export type EChartPreset =
762
+ | 'bar' | 'line' | 'area' | 'pie' | 'scatter'
763
+ | 'radar' | 'gauge' | 'funnel' | 'treemap' | 'sankey' | 'graph' | 'heatmap' | 'bigline'
695
764
 
696
765
  /** ECharts node: renders a full ECharts chart. Two modes:
697
766
  *
@@ -720,6 +789,10 @@ export interface GenuiEChart {
720
789
  data?: GenuiChartDatum[]
721
790
  /** Multi-series for preset mode (same shape as `chart.series`). */
722
791
  series?: Array<{ label: string; color?: string; data: GenuiChartDatum[] }>
792
+ /** Node/edge data for the `sankey` and `graph` presets. */
793
+ links?: Array<{ from: string; to: string; value?: number }>
794
+ /** Explicit categorical palette (hex) for preset mode. */
795
+ palette?: string[]
723
796
  /** Full ECharts option object. When present, `preset`/`data`/`series` are
724
797
  * ignored. This is a pass-through to `echarts.setOption`. */
725
798
  option?: Record<string, unknown>
@@ -28,9 +28,9 @@ import { createRenderUiTool, createValidateDshUiTool } from './tool.ts'
28
28
  * This route serves them from the plugin's own package directory through the
29
29
  * host webserver service — the longest-prefix rule lets it win over the
30
30
  * generic `/plugins` bundle route, and no host source change is needed. The
31
- * service is optional at this plugin's start time (same ordering reality as
32
- * the tools registry), so registration probes immediately AND on the
33
- * `internal/service` event, exactly like the tools registration below.
31
+ * service is optional at this plugin's start time, so a dependency fiber owns
32
+ * the registration and follows the webserver through late binding, replacement,
33
+ * and plugin reloads.
34
34
  */
35
35
 
36
36
  /** Route prefix under /plugins; anything under it is this plugin's asset. */
@@ -94,16 +94,26 @@ export const GENUI_SECTION_TEXT = `You can render interactive UI components INSI
94
94
 
95
95
  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:
96
96
 
97
- - 布局: text · row · col · grid · card · divider · spacer
97
+ - 布局: text · row · col · grid · card · divider · spacer · hero(封面块:超大数字 + 标题 + tone 渐变底色,一条回答最多一个)
98
98
  - 展示: badge · stat · progress · list · table · keyvalue · avatar · audio · video · timeline · file-tree · breadcrumb · callout · steps · diff · json · code · copy
99
- - 图表: chart {"type":"chart","kind":"bars|line|donut","data":[{"label":"...","value":n,"color":"#hex?"}],"series":[...]?}(series 仅 bars;未知扩展字段可通过校验,但原生 chart repair/render 会忽略) · echart (preset|option) · plot (函数图)
99
+ - 图表: 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 (函数图)
100
100
  - 交互: button · input · textarea · select · checkbox · switch · slider · radio · submit · quiz · link · tabs · accordion
101
101
  - 高级: mermaid (flowchart/sequence/class/gantt/pie/er/state/journey) · diagram (编辑级架构/流程图,27 种 kind) · scene3d (3D WebGL)
102
102
 
103
+ **默认就该出 UI**:出现下列情况至少出一个围栏:
104
+ - ≥3 条并列要点 → \`list\`;数字对比 → \`table\`;指标/进度/状态 → \`stat\`/\`progress\`/\`badge\`
105
+ - 步骤/时间线 → \`steps\`/\`timeline\`/\`mermaid\`;架构/流程 → \`diagram\` 或 \`mermaid\`;风险/结论 → \`callout\`;代码/改动 → \`code\`/\`diff\`/\`json\`
106
+ - 行内富文本:\`text\`/\`list\`/表格文本列/\`keyvalue\`/\`callout\` 里可写 \`code\`、**加粗**、==高亮==、[文字](url):重点留在句中,不必为一个词单起组件。
107
+ - 默认无卡 ≠ 少用组件:硬触发照常出组件,**组件多不是问题**——判据是每个组件承载不同信息、有焦点与层次、同一批数据不重复表达。卡片只用于并排项与数据对象;单段文字用「标题 + 正文 + 间距」。
108
+
109
+ **发回答前最后自检一次**:这段内容里有没有 ≥3 条并列要点、任何对比、任何数字/指标、任何步骤或流程?有就先转成组件再开口。**状态汇报、进度说明、提交与改动清单同样算**——不要因为它是"说明文"就用纯文字写。这一条踩过的坑:连续几条汇报全靠文字,一条围栏都没发。
110
+ - 趋势/占比 → \`chart\`(≤8 点)或 \`echart\`(多序列/要交互时);配色默认跟随主题,只有语义需要时才用 \`palette\` / \`card.accent\`;排版用 grid 子节点的 \`"span":2\` 跨列做宽窄混排(bento),不要一列方块堆到底;数据多时给 \`table\`/\`chart\`/\`list\` 配一个 \`input\`(id) + \`filter\` 绑定,读者能就地筛选,不用再问一遍
111
+
112
+ **字段速查**(完整见 genui skill):\`stat\` \`{"label","value","delta"?}\` · \`table\` \`{"columns","rows","types"?,"total"?,"details"?,"filter"?,"export"?}\` · \`callout\` \`{"tone","title","content"}\` · \`progress\` \`{"value","variant"?,"target"?}\`
113
+
103
114
  Rules:
104
- - 触发: 结构化表达优于纯文本时主动用(要点、强调、对比、流程、步骤、状态、数据、演示),纯问答与一句话不套 UI;一个主题一个主组件,每次 3–8 个组件,同一数据不重复出现。
105
115
  - JSON 严格: 坏围栏降级为代码块;≥3 节点或含 table 的围栏发出前调用 validate_dsh_ui,❌ 修好再发(若附「已自动修复」JSON 照抄即可)。
106
- - 规模: ≤200 节点、嵌套≤8 层(超出被截断);3D mesh 1–5;plot 给合理 xMin/xMax。
116
+ - 规模: ≤200 节点、嵌套≤8 层(超出被截断);一条回答 3–8 个组件,一个主题一个主组件;3D mesh 1–5;plot 给合理 xMin/xMax。
107
117
  - LOCAL-FIRST + actions: UI 能自己做的状态变化(判卷、判题、重置、展开、选中)就地完成,零往返;action 只用于必须模型参与的事。交互组件带 "action":"name",交互以 [genui-action] name + 组件数据回传,届时重渲染更新 UI;无 action 的按钮禁用。
108
118
  - Durable state: 交互状态按「会话+内容指纹」持久化——刷新/重放恢复;重渲染相同内容保留,新内容重置。
109
119
  - 卷子模式: 每题一个 radio(group+answer+explanation)+ 一个 submit(groups 全列),本地判分。
@@ -167,49 +177,27 @@ export function apply(ctx: Context): void {
167
177
  order: ctx.systemPrompt.getSectionOrder('STRUCTURED_OUTPUT'),
168
178
  text: GENUI_SECTION_TEXT,
169
179
  })
170
- // The tools service is optional: hosts without tool access (or minimal
171
- // compositions) keep the fence channel; only when the registry exists does
172
- // the render_ui tool join the model's tool set. `reflect.get(name, false)`
173
- // is cordis's non-throwing optional service lookup (the proxy's own trap
174
- // uses it) — property access without inject would throw instead.
175
- //
176
- // Start-up ordering: this plugin injects only `systemPrompt`, so cordis
177
- // starts it EARLY — before the tools provider (which injects deeper
178
- // dependencies) has bound its service. A one-shot probe at apply time
179
- // therefore misses the registry on real hosts (the fence section lands,
180
- // the tool never registers). Fix: probe immediately AND subscribe to
181
- // `internal/service` (emitted by cordis on every service binding), so the
182
- // registration lands the moment `tools` appears, whatever the order.
183
- let registered = false
184
- const tryRegister = (value: { register(tool: unknown): unknown } | undefined): void => {
185
- if (registered) return
186
- const tools = value ?? ctx.reflect.get('tools', false) as { register(tool: unknown): unknown } | undefined
187
- if (tools === undefined) return
188
- tools.register(createRenderUiTool())
189
- tools.register(createValidateDshUiTool())
190
- registered = true
191
- }
192
- tryRegister(undefined)
193
- ctx.on('internal/service', (name: string, value: unknown) => {
194
- if (name === 'tools') tryRegister(value as { register(tool: unknown): unknown })
180
+ // Hosts without tool access keep the fence channel. The dependency fiber
181
+ // starts whenever tools becomes available and unloads its registrations
182
+ // before either the service or this plugin is replaced.
183
+ ctx.inject(['tools'], (toolsCtx) => {
184
+ toolsCtx.effect(function* () {
185
+ yield toolsCtx.tools.register(createRenderUiTool())
186
+ yield toolsCtx.tools.register(createValidateDshUiTool())
187
+ }, 'dsh-genui: model tools')
195
188
  })
196
189
 
197
190
  ctx.inject(['skills'], (skillCtx) => {
198
191
  skillCtx.skills.registerProvider(() => bundledSkillProvider())
199
192
  })
200
193
 
201
- // Lazy-engine asset route: same optional-probe pattern as the tools
202
- // registry — the webserver service may bind after this plugin starts.
203
- let assetsRegistered = false
204
- const tryRegisterAssets = (value: { register(route: unknown): unknown } | undefined): void => {
205
- if (assetsRegistered) return
206
- const webServer = value ?? ctx.reflect.get('webServer', false) as { register(route: unknown): unknown } | undefined
207
- if (webServer === undefined) return
208
- webServer.register({ kind: 'prefix', path: ASSET_ROUTE_PATH, handler: serveGenuiAsset })
209
- assetsRegistered = true
210
- }
211
- tryRegisterAssets(undefined)
212
- ctx.on('internal/service', (name: string, value: unknown) => {
213
- if (name === 'webServer') tryRegisterAssets(value as { register(route: unknown): unknown })
194
+ // webServer.register returns a raw disposer, so an explicit effect binds the
195
+ // route to the dependency fiber instead of leaving it in the host route table.
196
+ ctx.inject(['webServer'], (webCtx) => {
197
+ const webServer = webCtx.reflect.get('webServer') as { register(route: unknown): () => void }
198
+ webCtx.effect(
199
+ () => webServer.register({ kind: 'prefix', path: ASSET_ROUTE_PATH, handler: serveGenuiAsset }),
200
+ 'dsh-genui: asset route',
201
+ )
214
202
  })
215
203
  }
@@ -44,7 +44,11 @@ const RENDER_UI_PARAMETERS: Record<string, unknown> = {
44
44
  properties: {
45
45
  spec: {
46
46
  type: 'object',
47
- 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).',
47
+ description: [
48
+ '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.',
49
+ '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.',
50
+ '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).',
51
+ ].join(' '),
48
52
  // Structural hints for the tool-call bridge. A bare `object` here was
49
53
  // observed to make some bridge layers stringify the whole spec into an
50
54
  // OpenAI-style `{ arguments: "<JSON>" }` wrapper (and to corrupt long
package/tsdown.config.ts CHANGED
@@ -189,5 +189,6 @@ export default [
189
189
  clientConfig,
190
190
  assetConfig('mermaid', 'src/client/asset-mermaid.ts'),
191
191
  assetConfig('three', 'src/client/asset-three.ts'),
192
- assetConfig('echarts', 'src/client/asset-echarts.ts'),
192
+ assetConfig('echarts-core', 'src/client/asset-echarts-core.ts'),
193
+ assetConfig('echarts-full', 'src/client/asset-echarts.ts'),
193
194
  ]