@changfenhuang/dsh-genui 0.9.1
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 +367 -0
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/README.zh-CN.md +261 -0
- package/SKILL.md +115 -0
- package/cordis.patch.yml +9 -0
- package/demo-prompts.md +118 -0
- package/lib/assets/echarts.js +64 -0
- package/lib/assets/mermaid.js +3591 -0
- package/lib/assets/three.js +3844 -0
- package/lib/client.js +5 -0
- package/lib/index.js +1895 -0
- package/lib/invariant.js +19 -0
- package/lib/types/client/EChartNode.d.ts +4 -0
- package/lib/types/client/ErrorBoundary.d.ts +30 -0
- package/lib/types/client/GenuiBlock.d.ts +7 -0
- package/lib/types/client/PlotBlock.d.ts +44 -0
- package/lib/types/client/action-context.d.ts +7 -0
- package/lib/types/client/asset-echarts.d.ts +15 -0
- package/lib/types/client/asset-loader.d.ts +31 -0
- package/lib/types/client/asset-mermaid.d.ts +1 -0
- package/lib/types/client/asset-three.d.ts +1 -0
- package/lib/types/client/blocks/advanced.d.ts +104 -0
- package/lib/types/client/blocks/basic.d.ts +27 -0
- package/lib/types/client/blocks/charts.d.ts +38 -0
- package/lib/types/client/blocks/diagram/geometry.d.ts +73 -0
- package/lib/types/client/blocks/diagram/index.d.ts +5 -0
- package/lib/types/client/blocks/diagram/layout.d.ts +41 -0
- package/lib/types/client/blocks/diagram/theme.d.ts +35 -0
- package/lib/types/client/blocks/forms.d.ts +83 -0
- package/lib/types/client/blocks/render-node.d.ts +10 -0
- package/lib/types/client/blocks/state.d.ts +58 -0
- package/lib/types/client/dom-fence.d.ts +14 -0
- package/lib/types/client/echarts-lazy.d.ts +17 -0
- package/lib/types/client/fence-render.d.ts +58 -0
- package/lib/types/client/gallery.d.ts +9 -0
- package/lib/types/client/guard.d.ts +126 -0
- package/lib/types/client/index.d.ts +36 -0
- package/lib/types/client/interaction-store.d.ts +43 -0
- package/lib/types/client/mermaid-core.d.ts +8 -0
- package/lib/types/client/mermaid-lazy.d.ts +9 -0
- package/lib/types/client/mermaid-safe.d.ts +52 -0
- package/lib/types/client/panel-command.d.ts +29 -0
- package/lib/types/client/panel-store.d.ts +94 -0
- package/lib/types/client/panel.d.ts +15 -0
- package/lib/types/client/parse-partial.d.ts +37 -0
- package/lib/types/client/safe-math.d.ts +43 -0
- package/lib/types/client/scene3d-core.d.ts +21 -0
- package/lib/types/client/scene3d-lazy.d.ts +8 -0
- package/lib/types/client/spec.d.ts +610 -0
- package/lib/types/client/toolview.d.ts +7 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/plugin/index.d.ts +25 -0
- package/lib/types/plugin/invariant.d.ts +15 -0
- package/lib/types/plugin/tool.d.ts +27 -0
- package/lib/types/shared/fence-repair.d.ts +67 -0
- package/package.json +121 -0
- package/src/client/EChartNode.tsx +242 -0
- package/src/client/ErrorBoundary.tsx +66 -0
- package/src/client/GenuiBlock.module.css +1263 -0
- package/src/client/GenuiBlock.tsx +166 -0
- package/src/client/PlotBlock.module.css +155 -0
- package/src/client/PlotBlock.tsx +431 -0
- package/src/client/action-context.ts +32 -0
- package/src/client/asset-echarts.ts +25 -0
- package/src/client/asset-loader.ts +79 -0
- package/src/client/asset-mermaid.ts +13 -0
- package/src/client/asset-three.ts +12 -0
- package/src/client/blocks/advanced.tsx +435 -0
- package/src/client/blocks/basic.tsx +116 -0
- package/src/client/blocks/charts.tsx +319 -0
- package/src/client/blocks/diagram/geometry.ts +151 -0
- package/src/client/blocks/diagram/index.tsx +284 -0
- package/src/client/blocks/diagram/layout.ts +199 -0
- package/src/client/blocks/diagram/theme.ts +108 -0
- package/src/client/blocks/forms.tsx +523 -0
- package/src/client/blocks/render-node.tsx +260 -0
- package/src/client/blocks/state.ts +61 -0
- package/src/client/dom-fence.tsx +641 -0
- package/src/client/echarts-lazy.ts +35 -0
- package/src/client/fence-render.tsx +230 -0
- package/src/client/gallery.ts +128 -0
- package/src/client/guard.ts +1450 -0
- package/src/client/index.tsx +195 -0
- package/src/client/interaction-store.ts +122 -0
- package/src/client/mermaid-core.ts +121 -0
- package/src/client/mermaid-lazy.ts +31 -0
- package/src/client/mermaid-safe.ts +122 -0
- package/src/client/panel-command.ts +119 -0
- package/src/client/panel-store.ts +471 -0
- package/src/client/panel.tsx +185 -0
- package/src/client/parse-partial.ts +162 -0
- package/src/client/safe-math.ts +270 -0
- package/src/client/scene3d-core.ts +192 -0
- package/src/client/scene3d-lazy.ts +26 -0
- package/src/client/spec.ts +767 -0
- package/src/client/toolview.tsx +66 -0
- package/src/css-modules.d.ts +5 -0
- package/src/index.ts +12 -0
- package/src/plugin/index.ts +173 -0
- package/src/plugin/invariant.ts +30 -0
- package/src/plugin/node-shims.d.ts +31 -0
- package/src/plugin/tool.ts +330 -0
- package/src/shared/fence-repair.ts +244 -0
- package/tsconfig.json +35 -0
- package/tsdown.config.ts +193 -0
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GenUI spec language: the declarative component tree a model emits inside a
|
|
3
|
+
* ```dsh-ui fence in its reply, which GenuiBlock renders as real interactive
|
|
4
|
+
* UI inline in the conversation. The vocabulary is a white list — the renderer
|
|
5
|
+
* maps each node to DOM directly, with no arbitrary-HTML path (same
|
|
6
|
+
* untrusted-output stance as MarkdownText).
|
|
7
|
+
*
|
|
8
|
+
* v1 interactivity is client-side only: buttons, tabs, checkboxes, and inputs
|
|
9
|
+
* are operable, but events do NOT flow back to the model.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** One node in the component tree. */
|
|
13
|
+
export type GenuiNode =
|
|
14
|
+
| GenuiText
|
|
15
|
+
| GenuiRow
|
|
16
|
+
| GenuiCol
|
|
17
|
+
| GenuiGrid
|
|
18
|
+
| GenuiCard
|
|
19
|
+
| GenuiButton
|
|
20
|
+
| GenuiInput
|
|
21
|
+
| GenuiSelect
|
|
22
|
+
| GenuiCheckbox
|
|
23
|
+
| GenuiLink
|
|
24
|
+
| GenuiAudio
|
|
25
|
+
| GenuiVideo
|
|
26
|
+
| GenuiBadge
|
|
27
|
+
| GenuiStat
|
|
28
|
+
| GenuiProgress
|
|
29
|
+
| GenuiDivider
|
|
30
|
+
| GenuiList
|
|
31
|
+
| GenuiTable
|
|
32
|
+
| GenuiChart
|
|
33
|
+
| GenuiTabs
|
|
34
|
+
| GenuiAvatar
|
|
35
|
+
| GenuiSpacer
|
|
36
|
+
| GenuiPlot
|
|
37
|
+
| GenuiCallout
|
|
38
|
+
| GenuiSteps
|
|
39
|
+
| GenuiKeyValue
|
|
40
|
+
| GenuiDiff
|
|
41
|
+
| GenuiJson
|
|
42
|
+
| GenuiCode
|
|
43
|
+
| GenuiRadio
|
|
44
|
+
| GenuiSubmit
|
|
45
|
+
| GenuiSwitch
|
|
46
|
+
| GenuiSlider
|
|
47
|
+
| GenuiTextarea
|
|
48
|
+
| GenuiAccordion
|
|
49
|
+
| GenuiCopy
|
|
50
|
+
| GenuiMermaid
|
|
51
|
+
| GenuiScene3D
|
|
52
|
+
| GenuiTimeline
|
|
53
|
+
| GenuiFileTree
|
|
54
|
+
| GenuiBreadcrumb
|
|
55
|
+
| GenuiQuiz
|
|
56
|
+
| GenuiDiagram
|
|
57
|
+
|
|
58
|
+
| GenuiEChart
|
|
59
|
+
|
|
60
|
+
export interface GenuiSpec {
|
|
61
|
+
/** Short title shown as the card banner. */
|
|
62
|
+
title?: string
|
|
63
|
+
/** Vertical gap between root items in px. */
|
|
64
|
+
gap?: number
|
|
65
|
+
/** Panel-only flag: `true` routes the fence to the session panel dock
|
|
66
|
+
* instead of the message flow (rendered by the same block, updated in
|
|
67
|
+
* place on every publish). */
|
|
68
|
+
panel?: boolean
|
|
69
|
+
/** Panel APPEND flag (meaningful only with `panel: true`): instead of
|
|
70
|
+
* replacing the session panel, merge this spec INTO the existing one —
|
|
71
|
+
* same-labelled tabs get their items appended, new tabs are added, plain
|
|
72
|
+
* item lists are appended to the tail. Lets the model grow a panel
|
|
73
|
+
* incrementally without resending (or truncating) the accumulated content.
|
|
74
|
+
* Applies only when the fence body is fully complete, so a streaming
|
|
75
|
+
* partial parse never double-merges. */
|
|
76
|
+
append?: boolean
|
|
77
|
+
/** Root component list. */
|
|
78
|
+
items: GenuiNode[]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* ---------------- leaf nodes ---------------- */
|
|
82
|
+
|
|
83
|
+
export interface GenuiText {
|
|
84
|
+
type: 'text'
|
|
85
|
+
size?: 'h1' | 'h2' | 'h3' | 'body' | 'muted' | 'caption'
|
|
86
|
+
content: string
|
|
87
|
+
center?: boolean
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface GenuiButton {
|
|
91
|
+
type: 'button'
|
|
92
|
+
label: string
|
|
93
|
+
tone?: 'primary' | 'danger' | 'success' | 'ghost'
|
|
94
|
+
full?: boolean
|
|
95
|
+
small?: boolean
|
|
96
|
+
icon?: string
|
|
97
|
+
/** v2: when set, clicking sends this action back to the model. */
|
|
98
|
+
action?: string
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface GenuiInput {
|
|
102
|
+
type: 'input'
|
|
103
|
+
label?: string
|
|
104
|
+
placeholder?: string
|
|
105
|
+
value?: string
|
|
106
|
+
inputType?: 'text' | 'email' | 'password'
|
|
107
|
+
/** v2: when set, interaction sends this action back to the model. */
|
|
108
|
+
action?: string
|
|
109
|
+
/**
|
|
110
|
+
* v2.7: stable field id — the value is persisted across refresh/re-render
|
|
111
|
+
* and collected by a sibling `submit` node (`fields: {id: value}`).
|
|
112
|
+
*/
|
|
113
|
+
id?: string
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface GenuiSelect {
|
|
117
|
+
type: 'select'
|
|
118
|
+
label?: string
|
|
119
|
+
options: string[]
|
|
120
|
+
/** v2: when set, interaction sends this action back to the model. */
|
|
121
|
+
action?: string
|
|
122
|
+
/**
|
|
123
|
+
* v2.8: default-selected option index (a placeholder option shows instead
|
|
124
|
+
* when absent, so nothing is silently pre-registered).
|
|
125
|
+
*/
|
|
126
|
+
selected?: number
|
|
127
|
+
/**
|
|
128
|
+
* v2.8: stable field id — the chosen option persists across refresh and is
|
|
129
|
+
* collected by a sibling `submit` node (`fields: {id: value}`), like
|
|
130
|
+
* input/textarea ids.
|
|
131
|
+
*/
|
|
132
|
+
id?: string
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface GenuiCheckbox {
|
|
136
|
+
type: 'checkbox'
|
|
137
|
+
label: string
|
|
138
|
+
checked?: boolean
|
|
139
|
+
/** v2: when set, interaction sends this action back to the model. */
|
|
140
|
+
action?: string
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface GenuiLink {
|
|
144
|
+
type: 'link'
|
|
145
|
+
label: string
|
|
146
|
+
/**
|
|
147
|
+
* Optional http(s)/mailto target. With `href` the node renders a REAL
|
|
148
|
+
* anchor (target=_blank + noopener); without it the node renders as plain
|
|
149
|
+
* styled text — never a dead clickable-looking control.
|
|
150
|
+
*/
|
|
151
|
+
href?: string
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** User-controlled audio from a browser-reachable URL. */
|
|
155
|
+
export interface GenuiAudio {
|
|
156
|
+
type: 'audio'
|
|
157
|
+
src: string
|
|
158
|
+
/** Visible caption and accessible player name. */
|
|
159
|
+
alt?: string
|
|
160
|
+
loop?: boolean
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** User-controlled video from a browser-reachable URL. */
|
|
164
|
+
export interface GenuiVideo {
|
|
165
|
+
type: 'video'
|
|
166
|
+
src: string
|
|
167
|
+
/** Visible caption and accessible player name. */
|
|
168
|
+
alt?: string
|
|
169
|
+
poster?: string
|
|
170
|
+
loop?: boolean
|
|
171
|
+
muted?: boolean
|
|
172
|
+
aspectRatio?: '16:9' | '4:3' | '1:1' | '9:16'
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface GenuiBadge {
|
|
176
|
+
type: 'badge'
|
|
177
|
+
label: string
|
|
178
|
+
tone?: 'success' | 'warn' | 'danger' | 'accent'
|
|
179
|
+
icon?: string
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface GenuiStat {
|
|
183
|
+
type: 'stat'
|
|
184
|
+
label: string
|
|
185
|
+
value: string
|
|
186
|
+
delta?: string
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface GenuiProgress {
|
|
190
|
+
type: 'progress'
|
|
191
|
+
label?: string
|
|
192
|
+
/** 0..100 */
|
|
193
|
+
value: number
|
|
194
|
+
valueLabel?: string
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface GenuiDivider {
|
|
198
|
+
type: 'divider'
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface GenuiAvatar {
|
|
202
|
+
type: 'avatar'
|
|
203
|
+
name: string
|
|
204
|
+
color?: string
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface GenuiSpacer {
|
|
208
|
+
type: 'spacer'
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* ---------------- container nodes ---------------- */
|
|
212
|
+
|
|
213
|
+
export interface GenuiRow {
|
|
214
|
+
type: 'row'
|
|
215
|
+
items: GenuiNode[]
|
|
216
|
+
wrap?: boolean
|
|
217
|
+
spacer?: boolean
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface GenuiCol {
|
|
221
|
+
type: 'col'
|
|
222
|
+
items: GenuiNode[]
|
|
223
|
+
gap?: number
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface GenuiGrid {
|
|
227
|
+
type: 'grid'
|
|
228
|
+
cols: number
|
|
229
|
+
items: GenuiNode[]
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface GenuiCard {
|
|
233
|
+
type: 'card'
|
|
234
|
+
title?: string
|
|
235
|
+
items: GenuiNode[]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export interface GenuiList {
|
|
239
|
+
type: 'list'
|
|
240
|
+
items: Array<string | { title: string; desc?: string } | GenuiNode>
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface GenuiTable {
|
|
244
|
+
type: 'table'
|
|
245
|
+
columns: string[]
|
|
246
|
+
rows: Array<Array<string | number>>
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface GenuiChartDatum {
|
|
250
|
+
label: string
|
|
251
|
+
value: number
|
|
252
|
+
color?: string
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export interface GenuiChart {
|
|
256
|
+
type: 'chart'
|
|
257
|
+
/** Chart shape: bars (default), line (trend), donut (share). */
|
|
258
|
+
kind?: 'bars' | 'line' | 'donut'
|
|
259
|
+
data: GenuiChartDatum[]
|
|
260
|
+
/** Multi-series grouped bars: one series of data per entry. */
|
|
261
|
+
series?: Array<{ label: string; color?: string; data: GenuiChartDatum[] }>
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface GenuiTab {
|
|
265
|
+
label: string
|
|
266
|
+
items: GenuiNode[]
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export interface GenuiTabs {
|
|
270
|
+
type: 'tabs'
|
|
271
|
+
tabs: GenuiTab[]
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* ---------------- v1.1 additions: plot / callout / steps / keyvalue / 收编 ---------------- */
|
|
275
|
+
|
|
276
|
+
export interface GenuiPlotSeries {
|
|
277
|
+
/** Math expression in x (safe evaluator: sin/cos/tan/pow/sqrt/log/exp/abs/floor/ceil/round/min/max/pi/e). */
|
|
278
|
+
expr: string
|
|
279
|
+
/** Legend label; defaults to the expression. */
|
|
280
|
+
label?: string
|
|
281
|
+
/** Stroke color. */
|
|
282
|
+
color?: string
|
|
283
|
+
/** v2.9 draw shape: line (default), area (fill to the baseline), scatter
|
|
284
|
+
* (dots only). */
|
|
285
|
+
kind?: 'line' | 'area' | 'scatter'
|
|
286
|
+
/** v2: adjustable parameters (e.g. {a: 2} in "a*sin(x)") — one slider each, live re-render. */
|
|
287
|
+
params?: Array<{
|
|
288
|
+
name: string
|
|
289
|
+
value: number
|
|
290
|
+
min?: number
|
|
291
|
+
max?: number
|
|
292
|
+
step?: number
|
|
293
|
+
/** v1.5: animate this parameter (auto-play) from `value` to `animateTo`. */
|
|
294
|
+
animateTo?: number
|
|
295
|
+
/** Animation duration in ms (default 4000). */
|
|
296
|
+
durationMs?: number
|
|
297
|
+
/** Loop the animation (default false). */
|
|
298
|
+
loop?: boolean
|
|
299
|
+
}>
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export interface GenuiPlot {
|
|
303
|
+
type: 'plot'
|
|
304
|
+
/** Functions to draw, in draw order. */
|
|
305
|
+
series: GenuiPlotSeries[]
|
|
306
|
+
xMin?: number
|
|
307
|
+
xMax?: number
|
|
308
|
+
yMin?: number
|
|
309
|
+
yMax?: number
|
|
310
|
+
title?: string
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface GenuiCallout {
|
|
314
|
+
type: 'callout'
|
|
315
|
+
tone?: 'info' | 'success' | 'warning' | 'error'
|
|
316
|
+
/** Short heading above the body. */
|
|
317
|
+
title?: string
|
|
318
|
+
content: string
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export interface GenuiStep {
|
|
322
|
+
title: string
|
|
323
|
+
desc?: string
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export interface GenuiSteps {
|
|
327
|
+
type: 'steps'
|
|
328
|
+
/** Completed steps are rendered in the accent color, the rest muted. */
|
|
329
|
+
current?: number
|
|
330
|
+
steps: GenuiStep[]
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export interface GenuiKeyValue {
|
|
334
|
+
type: 'keyvalue'
|
|
335
|
+
pairs: Array<{ key: string; value: string }>
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** 收编 dsh DiffBlock: file mutations as an inline diff. */
|
|
339
|
+
export interface GenuiDiff {
|
|
340
|
+
type: 'diff'
|
|
341
|
+
diffs: Array<{
|
|
342
|
+
path: string
|
|
343
|
+
oldText: string | null
|
|
344
|
+
newText: string
|
|
345
|
+
}>
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** 收编 dsh JsonTree: a structured JSON value for inspection. */
|
|
349
|
+
export interface GenuiJson {
|
|
350
|
+
type: 'json'
|
|
351
|
+
/** Any JSON value to render as a tree. */
|
|
352
|
+
value: unknown
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** 收编 dsh CodeBlock: syntax-highlighted code with an explicit language. */
|
|
356
|
+
export interface GenuiCode {
|
|
357
|
+
type: 'code'
|
|
358
|
+
lang?: string
|
|
359
|
+
code: string
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/* ---------------- v1.2: richer interactivity ---------------- */
|
|
363
|
+
|
|
364
|
+
export interface GenuiRadio {
|
|
365
|
+
type: 'radio'
|
|
366
|
+
label?: string
|
|
367
|
+
options: string[]
|
|
368
|
+
/** Default-selected option index. */
|
|
369
|
+
selected?: number
|
|
370
|
+
/** v2: when set, interaction sends this action back to the model. */
|
|
371
|
+
action?: string
|
|
372
|
+
/**
|
|
373
|
+
* v2.5: aggregation group name. When set, the selection is recorded into
|
|
374
|
+
* the block-wide answers registry instead of firing a per-click action — a
|
|
375
|
+
* sibling `submit` node then collects ALL groups ("交卷" pattern). Without
|
|
376
|
+
* `group`, the legacy per-click behavior applies.
|
|
377
|
+
*/
|
|
378
|
+
group?: string
|
|
379
|
+
/**
|
|
380
|
+
* v2.6: correct option for LOCAL grading — the option index (number) or
|
|
381
|
+
* its label (string). When the group's radio carries this, the `submit`
|
|
382
|
+
* button grades IN PLACE on click (score + per-question right/wrong +
|
|
383
|
+
* explanations, zero model round trip).
|
|
384
|
+
*/
|
|
385
|
+
answer?: number | string
|
|
386
|
+
/** v2.6: per-question explanation shown after local grading. */
|
|
387
|
+
explanation?: string
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Submit node: collects the answers of sibling `radio` groups in this block.
|
|
391
|
+
* LOCAL-FIRST: when the questions carry `answer` data the click grades IN
|
|
392
|
+
* PLACE — score, per-question right/wrong, explanations — with no model
|
|
393
|
+
* round trip, and locks the questions until "重新作答" resets them. Only when
|
|
394
|
+
* NO question carries answers does it fall back to firing ONE action
|
|
395
|
+
* (`{type:'submit', answers, total, answered}`). */
|
|
396
|
+
export interface GenuiSubmit {
|
|
397
|
+
type: 'submit'
|
|
398
|
+
label: string
|
|
399
|
+
/**
|
|
400
|
+
* Optional action name. Local-first: when ANY question in scope carries
|
|
401
|
+
* `answer` data the click grades IN PLACE with zero model round trip, so
|
|
402
|
+
* no action is needed — the spec stays valid without one. Only when NO
|
|
403
|
+
* question has local answers does the submit need an action to collect
|
|
404
|
+
* `{type:'submit', answers, total, answered}`; without one the button
|
|
405
|
+
* renders disabled (honest affordance). Also fired as the reset hook if
|
|
406
|
+
* `resetAction` is absent.
|
|
407
|
+
*/
|
|
408
|
+
action?: string
|
|
409
|
+
/**
|
|
410
|
+
* v2.6: optional action fired when the user clicks "重新作答" after a
|
|
411
|
+
* local grading (e.g. to tell the model the paper was redone). Absent =
|
|
412
|
+
* reset stays fully local.
|
|
413
|
+
*/
|
|
414
|
+
resetAction?: string
|
|
415
|
+
/**
|
|
416
|
+
* Optional explicit group list to wait for; when absent the submit enables
|
|
417
|
+
* once at least one grouped radio has an answer. When present it stays
|
|
418
|
+
* disabled until EVERY listed group has a recorded answer (the hint shows
|
|
419
|
+
* the progress).
|
|
420
|
+
*/
|
|
421
|
+
groups?: string[]
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export interface GenuiSwitch {
|
|
425
|
+
type: 'switch'
|
|
426
|
+
label: string
|
|
427
|
+
checked?: boolean
|
|
428
|
+
/** v2: when set, interaction sends this action back to the model. */
|
|
429
|
+
action?: string
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Slider: a range input for numeric form values (v2.9). The current value
|
|
434
|
+
* shows next to the track; dragging fires the action (debounced by the
|
|
435
|
+
* block) and, with an `id`, persists across refresh and joins the sibling
|
|
436
|
+
* submit's `fields` collection (the value is the numeric string).
|
|
437
|
+
*/
|
|
438
|
+
export interface GenuiSlider {
|
|
439
|
+
type: 'slider'
|
|
440
|
+
label?: string
|
|
441
|
+
min?: number
|
|
442
|
+
max?: number
|
|
443
|
+
step?: number
|
|
444
|
+
/** Default value; defaults to `min` (or 0). */
|
|
445
|
+
value?: number
|
|
446
|
+
/** v2: when set, interaction sends this action back to the model. */
|
|
447
|
+
action?: string
|
|
448
|
+
/** v2.9: stable field id — durable value + submit collection. */
|
|
449
|
+
id?: string
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export interface GenuiTextarea {
|
|
453
|
+
type: 'textarea'
|
|
454
|
+
label?: string
|
|
455
|
+
placeholder?: string
|
|
456
|
+
rows?: number
|
|
457
|
+
value?: string
|
|
458
|
+
/** v2.5: when set, blurring the field sends this action with the value. */
|
|
459
|
+
action?: string
|
|
460
|
+
/**
|
|
461
|
+
* v2.7: stable field id — the value is persisted across refresh/re-render
|
|
462
|
+
* and collected by a sibling `submit` node (`fields: {id: value}`).
|
|
463
|
+
*/
|
|
464
|
+
id?: string
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export interface GenuiAccordionItem {
|
|
468
|
+
title: string
|
|
469
|
+
items: GenuiNode[]
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export interface GenuiAccordion {
|
|
473
|
+
type: 'accordion'
|
|
474
|
+
items: GenuiAccordionItem[]
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export interface GenuiCopy {
|
|
478
|
+
type: 'copy'
|
|
479
|
+
label?: string
|
|
480
|
+
text: string
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/* ---------------- v1.3: advanced ---------------- */
|
|
484
|
+
|
|
485
|
+
/** Mermaid diagram: flowchart/sequence/class/gantt source rendered lazily. */
|
|
486
|
+
export interface GenuiMermaid {
|
|
487
|
+
type: 'mermaid'
|
|
488
|
+
/** Mermaid diagram source (whitelisted diagram kinds at render time). */
|
|
489
|
+
code: string
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** One mesh in a 3D scene. */
|
|
493
|
+
export interface GenuiMesh {
|
|
494
|
+
/** Primitive geometry, white-listed. */
|
|
495
|
+
shape: 'box' | 'sphere' | 'cone' | 'cylinder' | 'torus'
|
|
496
|
+
/** Mesh color. */
|
|
497
|
+
color?: string
|
|
498
|
+
/** [x, y, z] position. */
|
|
499
|
+
position?: [number, number, number]
|
|
500
|
+
/** [x, y, z] rotation in radians. */
|
|
501
|
+
rotation?: [number, number, number]
|
|
502
|
+
/** Uniform scale, or [sx, sy, sz]. */
|
|
503
|
+
scale?: number | [number, number, number]
|
|
504
|
+
/** Box size [w, h, d] or sphere/cone/cylinder radius. */
|
|
505
|
+
size?: number | [number, number, number]
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** 3D scene rendered with three.js (lazily imported). */
|
|
509
|
+
export interface GenuiScene3D {
|
|
510
|
+
type: 'scene3d'
|
|
511
|
+
title?: string
|
|
512
|
+
meshes: GenuiMesh[]
|
|
513
|
+
/** Ambient light intensity (0..2). */
|
|
514
|
+
ambient?: number
|
|
515
|
+
/** Background color; default transparent. */
|
|
516
|
+
background?: string
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/* ---------------- v1.4: more content structure ---------------- */
|
|
520
|
+
|
|
521
|
+
export interface GenuiTimelineItem {
|
|
522
|
+
/** Title of the event. */
|
|
523
|
+
title: string
|
|
524
|
+
/** Secondary description. */
|
|
525
|
+
desc?: string
|
|
526
|
+
/** Optional timestamp label. */
|
|
527
|
+
time?: string
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export interface GenuiTimeline {
|
|
531
|
+
type: 'timeline'
|
|
532
|
+
items: GenuiTimelineItem[]
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export interface GenuiFileTreeNode {
|
|
536
|
+
/** File or directory name. */
|
|
537
|
+
name: string
|
|
538
|
+
/** Directory when true (collapsible children). */
|
|
539
|
+
type?: 'file' | 'dir'
|
|
540
|
+
/** Child nodes (dirs only). */
|
|
541
|
+
children?: GenuiFileTreeNode[]
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export interface GenuiFileTree {
|
|
545
|
+
type: 'file-tree'
|
|
546
|
+
items: GenuiFileTreeNode[]
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export interface GenuiBreadcrumb {
|
|
550
|
+
type: 'breadcrumb'
|
|
551
|
+
items: string[]
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/* ---------------- v1.5: teaching ---------------- */
|
|
555
|
+
|
|
556
|
+
export interface GenuiQuizOption {
|
|
557
|
+
label: string
|
|
558
|
+
/** True when this option is the correct answer. */
|
|
559
|
+
correct?: boolean
|
|
560
|
+
/** Per-option feedback shown after selection. */
|
|
561
|
+
feedback?: string
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export interface GenuiQuiz {
|
|
565
|
+
type: 'quiz'
|
|
566
|
+
/** The question text. */
|
|
567
|
+
question: string
|
|
568
|
+
/** Answer options; exactly one should be correct. */
|
|
569
|
+
options: GenuiQuizOption[]
|
|
570
|
+
/** Shown after answering (either way). */
|
|
571
|
+
explanation?: string
|
|
572
|
+
/** Reset the quiz when this value changes (e.g. a question id). */
|
|
573
|
+
id?: string
|
|
574
|
+
/** v2.5: when set, answering sends this action with the chosen answer
|
|
575
|
+
* (`{type:'quiz', question, answer, correct}`) so the model can collect
|
|
576
|
+
* or grade it. */
|
|
577
|
+
action?: string
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/* ---------------- v2.10: editorial diagram (diagram-design port) ---------------- */
|
|
581
|
+
|
|
582
|
+
/** Editorial diagram kinds, ported from cathrynlavery/diagram-design (27 types). */
|
|
583
|
+
export const DIAGRAM_KINDS = [
|
|
584
|
+
'architecture', 'it-state', 'flowchart', 'sequence', 'state', 'er', 'timeline',
|
|
585
|
+
'swimlane', 'quadrant', 'radar', 'loop', 'nested', 'tree', 'org-chart', 'layers',
|
|
586
|
+
'venn', 'pyramid', 'bar', 'line', 'gantt', 'scatter', 'high-level', 'process',
|
|
587
|
+
'medallion', 'data-flow', 'dp-integration', 'dp-security-matrix',
|
|
588
|
+
] as const
|
|
589
|
+
export type GenuiDiagramKind = typeof DIAGRAM_KINDS[number]
|
|
590
|
+
|
|
591
|
+
/** Diagram surface variant: light / dark follow the host theme; editorial is the full skin. */
|
|
592
|
+
export type GenuiDiagramVariant = 'light' | 'dark' | 'editorial'
|
|
593
|
+
|
|
594
|
+
/** Node treatment classes (map to the diagram-design semantic fills/strokes). */
|
|
595
|
+
export type GenuiDiagramNodeType =
|
|
596
|
+
| 'focal' | 'backend' | 'store' | 'external' | 'input' | 'optional' | 'security'
|
|
597
|
+
|
|
598
|
+
/** One positioned node in a coordinate-mode diagram. */
|
|
599
|
+
export interface GenuiDiagramNode {
|
|
600
|
+
/** Stable node id referenced by edges. */
|
|
601
|
+
id: string
|
|
602
|
+
/** Human-readable label (Geist sans in the renderer). */
|
|
603
|
+
label: string
|
|
604
|
+
/** Optional technical sublabel (Geist mono in the renderer). */
|
|
605
|
+
sub?: string
|
|
606
|
+
/** Semantic treatment; `focal` is budgeted (max 2 per diagram). */
|
|
607
|
+
type?: GenuiDiagramNodeType
|
|
608
|
+
/** Position / size (coordinate-mode kinds only). 4px grid is enforced at render. */
|
|
609
|
+
x?: number
|
|
610
|
+
y?: number
|
|
611
|
+
w?: number
|
|
612
|
+
h?: number
|
|
613
|
+
/** Optional rectangular type tag (e.g. `API`). */
|
|
614
|
+
tag?: string
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** One connector between two nodes. */
|
|
618
|
+
export interface GenuiDiagramEdge {
|
|
619
|
+
/** Source node id. */
|
|
620
|
+
from: string
|
|
621
|
+
/** Target node id. */
|
|
622
|
+
to: string
|
|
623
|
+
/** Optional edge label (all-caps, <=14 chars in the renderer). */
|
|
624
|
+
label?: string
|
|
625
|
+
/** Stroke semantics: solid (default), dashed (optional/async), accent, link (HTTP/API). */
|
|
626
|
+
kind?: 'solid' | 'dashed' | 'accent' | 'link'
|
|
627
|
+
/** Route hint; `auto` (default) lets the renderer choose orthogonal routing. */
|
|
628
|
+
route?: 'auto' | 'orthogonal' | 'straight'
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/** Optional brand override; every key maps to a diagram-design semantic token. */
|
|
632
|
+
export interface GenuiDiagramTheme {
|
|
633
|
+
paper?: string
|
|
634
|
+
'paper-2'?: string
|
|
635
|
+
ink?: string
|
|
636
|
+
muted?: string
|
|
637
|
+
soft?: string
|
|
638
|
+
rule?: string
|
|
639
|
+
accent?: string
|
|
640
|
+
'accent-tint'?: string
|
|
641
|
+
link?: string
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** Zone container: a labelled region grouping related nodes (max 3). */
|
|
645
|
+
export interface GenuiDiagramZone {
|
|
646
|
+
/** Zone label (mono uppercase eyebrow in the renderer). */
|
|
647
|
+
label: string
|
|
648
|
+
/** Bounding box in the same canvas space as the nodes. */
|
|
649
|
+
x?: number
|
|
650
|
+
y?: number
|
|
651
|
+
w?: number
|
|
652
|
+
h?: number
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Editorial diagram (diagram-design port): a white-listed declarative diagram
|
|
657
|
+
* rendered as inline SVG by the browser. `kind` selects the layout grammar
|
|
658
|
+
* (coordinate kinds place nodes via x/y; rule kinds auto-layout from data),
|
|
659
|
+
* `variant` selects the skin, and `theme` overrides semantic tokens. The
|
|
660
|
+
* renderer enforces the editorial constraints — orthogonal connectors, 4px
|
|
661
|
+
* grid, complexity budget, focal accent budget — so the model cannot emit
|
|
662
|
+
* "AI slop" schematics.
|
|
663
|
+
*/
|
|
664
|
+
export interface GenuiDiagram {
|
|
665
|
+
type: 'diagram'
|
|
666
|
+
/** The 27-type editorial vocabulary. */
|
|
667
|
+
kind: GenuiDiagramKind
|
|
668
|
+
/** Surface skin; default follows the host theme. */
|
|
669
|
+
variant?: GenuiDiagramVariant
|
|
670
|
+
/** Optional diagram title (Instrument Serif in the renderer). */
|
|
671
|
+
title?: string
|
|
672
|
+
/** Nodes (coordinate kinds use x/y/w/h; rule kinds may omit positions). */
|
|
673
|
+
nodes: GenuiDiagramNode[]
|
|
674
|
+
/** Connectors. */
|
|
675
|
+
edges?: GenuiDiagramEdge[]
|
|
676
|
+
/** Optional labelled regions grouping nodes (max 3). */
|
|
677
|
+
zones?: GenuiDiagramZone[]
|
|
678
|
+
/** Optional semantic-token overrides. */
|
|
679
|
+
theme?: GenuiDiagramTheme
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/* ---------------- v1.6: ECharts ---------------- */
|
|
683
|
+
|
|
684
|
+
/** Preset chart kinds the `echart` node can build from `data`/`series` without
|
|
685
|
+
* a full ECharts option. Each maps to a themed option template. */
|
|
686
|
+
export type EChartPreset = 'bar' | 'line' | 'area' | 'pie' | 'scatter'
|
|
687
|
+
|
|
688
|
+
/** ECharts node: renders a full ECharts chart. Two modes:
|
|
689
|
+
*
|
|
690
|
+
* 1. **Full option** (`option` set): the model provides a standard ECharts
|
|
691
|
+
* `EChartsCoreOption` object directly. This is the escape hatch for
|
|
692
|
+
* custom chart types, complex series, or advanced features (dataZoom,
|
|
693
|
+
* visualMap, etc.).
|
|
694
|
+
* 2. **Preset shorthand** (`preset` + `data`/`series`): the model provides
|
|
695
|
+
* the same simple `data`/`series` shape as the `chart` node, and the
|
|
696
|
+
* component builds a themed ECharts option automatically. This is the
|
|
697
|
+
* easy upgrade path: change `type: 'chart'` to `type: 'echart'` and add
|
|
698
|
+
* `preset`.
|
|
699
|
+
*
|
|
700
|
+
* The echarts engine is lazy-loaded (lib/assets/echarts.js) only when an
|
|
701
|
+
* `echart` node appears in a spec. */
|
|
702
|
+
export interface GenuiEChart {
|
|
703
|
+
type: 'echart'
|
|
704
|
+
/** Optional title shown above the chart. */
|
|
705
|
+
title?: string
|
|
706
|
+
/** Chart height in pixels (default 300). */
|
|
707
|
+
height?: number
|
|
708
|
+
/** Preset: builds the ECharts option from `data`/`series` when `option`
|
|
709
|
+
* is absent. */
|
|
710
|
+
preset?: EChartPreset
|
|
711
|
+
/** Simple data for preset mode (same shape as `chart.data`). */
|
|
712
|
+
data?: GenuiChartDatum[]
|
|
713
|
+
/** Multi-series for preset mode (same shape as `chart.series`). */
|
|
714
|
+
series?: Array<{ label: string; color?: string; data: GenuiChartDatum[] }>
|
|
715
|
+
/** Full ECharts option object. When present, `preset`/`data`/`series` are
|
|
716
|
+
* ignored. This is a pass-through to `echarts.setOption`. */
|
|
717
|
+
option?: Record<string, unknown>
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** Parse the raw fence body as a GenuiSpec, or null when it is not one. */
|
|
721
|
+
export function parseGenuiSpec(raw: string): GenuiSpec | null {
|
|
722
|
+
const trimmed = raw.trim()
|
|
723
|
+
if (trimmed === '') return null
|
|
724
|
+
let value: unknown
|
|
725
|
+
try {
|
|
726
|
+
value = JSON.parse(trimmed)
|
|
727
|
+
} catch {
|
|
728
|
+
return null
|
|
729
|
+
}
|
|
730
|
+
if (isGenuiSpec(value)) return value
|
|
731
|
+
// Single-component roots are part of the documented fence vocabulary
|
|
732
|
+
// (e.g. {"type":"callout","tone":"info","title":"…","content":"…"} as the
|
|
733
|
+
// whole body) — wrap them into a col so the items-gated pipeline renders
|
|
734
|
+
// them. panel/append hoist onto the wrapper so panel routing keeps working.
|
|
735
|
+
return wrapSingleComponentRoot(value)
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Wrap a bare component object into a col root. Returns null when `value` is
|
|
740
|
+
* not component-shaped (no usable `type`). `panel`/`append` live on the root
|
|
741
|
+
* spec, so they are hoisted onto the wrapper.
|
|
742
|
+
*/
|
|
743
|
+
export function wrapSingleComponentRoot(value: unknown): GenuiSpec | null {
|
|
744
|
+
if (typeof value !== 'object' || value === null) return null
|
|
745
|
+
const v = value as { type?: unknown; panel?: unknown; append?: unknown }
|
|
746
|
+
if (typeof v.type !== 'string' || v.type === '') return null
|
|
747
|
+
// GenuiCol is structurally a GenuiSpec (items + optional gap); panel and
|
|
748
|
+
// append are GenuiSpec-only root flags, added after construction.
|
|
749
|
+
const wrapped: GenuiCol = {
|
|
750
|
+
type: 'col',
|
|
751
|
+
items: [value as GenuiNode],
|
|
752
|
+
}
|
|
753
|
+
const root: GenuiSpec = wrapped
|
|
754
|
+
if (v.panel === true) root.panel = true
|
|
755
|
+
if (v.append === true) root.append = true
|
|
756
|
+
return root
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/** Basic structural guard: is this object a valid GenuiSpec? */
|
|
760
|
+
export function isGenuiSpec(value: unknown): value is GenuiSpec {
|
|
761
|
+
if (typeof value !== 'object' || value === null) return false
|
|
762
|
+
const v = value as { items?: unknown; title?: unknown; gap?: unknown }
|
|
763
|
+
if (!Array.isArray(v.items)) return false
|
|
764
|
+
if (v.title !== undefined && typeof v.title !== 'string') return false
|
|
765
|
+
if (v.gap !== undefined && typeof v.gap !== 'number') return false
|
|
766
|
+
return true
|
|
767
|
+
}
|