@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/src/client/spec.ts
CHANGED
|
@@ -10,7 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
/** One node in the component tree. */
|
|
13
|
-
|
|
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
|
|
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 =
|
|
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>
|
package/src/plugin/index.ts
CHANGED
|
@@ -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
|
|
32
|
-
* the
|
|
33
|
-
*
|
|
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 {"
|
|
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
|
|
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
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
//
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
}
|
package/src/plugin/tool.ts
CHANGED
|
@@ -44,7 +44,11 @@ const RENDER_UI_PARAMETERS: Record<string, unknown> = {
|
|
|
44
44
|
properties: {
|
|
45
45
|
spec: {
|
|
46
46
|
type: 'object',
|
|
47
|
-
description:
|
|
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
|
]
|