@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.
Files changed (106) hide show
  1. package/CHANGELOG.md +367 -0
  2. package/LICENSE +21 -0
  3. package/README.md +261 -0
  4. package/README.zh-CN.md +261 -0
  5. package/SKILL.md +115 -0
  6. package/cordis.patch.yml +9 -0
  7. package/demo-prompts.md +118 -0
  8. package/lib/assets/echarts.js +64 -0
  9. package/lib/assets/mermaid.js +3591 -0
  10. package/lib/assets/three.js +3844 -0
  11. package/lib/client.js +5 -0
  12. package/lib/index.js +1895 -0
  13. package/lib/invariant.js +19 -0
  14. package/lib/types/client/EChartNode.d.ts +4 -0
  15. package/lib/types/client/ErrorBoundary.d.ts +30 -0
  16. package/lib/types/client/GenuiBlock.d.ts +7 -0
  17. package/lib/types/client/PlotBlock.d.ts +44 -0
  18. package/lib/types/client/action-context.d.ts +7 -0
  19. package/lib/types/client/asset-echarts.d.ts +15 -0
  20. package/lib/types/client/asset-loader.d.ts +31 -0
  21. package/lib/types/client/asset-mermaid.d.ts +1 -0
  22. package/lib/types/client/asset-three.d.ts +1 -0
  23. package/lib/types/client/blocks/advanced.d.ts +104 -0
  24. package/lib/types/client/blocks/basic.d.ts +27 -0
  25. package/lib/types/client/blocks/charts.d.ts +38 -0
  26. package/lib/types/client/blocks/diagram/geometry.d.ts +73 -0
  27. package/lib/types/client/blocks/diagram/index.d.ts +5 -0
  28. package/lib/types/client/blocks/diagram/layout.d.ts +41 -0
  29. package/lib/types/client/blocks/diagram/theme.d.ts +35 -0
  30. package/lib/types/client/blocks/forms.d.ts +83 -0
  31. package/lib/types/client/blocks/render-node.d.ts +10 -0
  32. package/lib/types/client/blocks/state.d.ts +58 -0
  33. package/lib/types/client/dom-fence.d.ts +14 -0
  34. package/lib/types/client/echarts-lazy.d.ts +17 -0
  35. package/lib/types/client/fence-render.d.ts +58 -0
  36. package/lib/types/client/gallery.d.ts +9 -0
  37. package/lib/types/client/guard.d.ts +126 -0
  38. package/lib/types/client/index.d.ts +36 -0
  39. package/lib/types/client/interaction-store.d.ts +43 -0
  40. package/lib/types/client/mermaid-core.d.ts +8 -0
  41. package/lib/types/client/mermaid-lazy.d.ts +9 -0
  42. package/lib/types/client/mermaid-safe.d.ts +52 -0
  43. package/lib/types/client/panel-command.d.ts +29 -0
  44. package/lib/types/client/panel-store.d.ts +94 -0
  45. package/lib/types/client/panel.d.ts +15 -0
  46. package/lib/types/client/parse-partial.d.ts +37 -0
  47. package/lib/types/client/safe-math.d.ts +43 -0
  48. package/lib/types/client/scene3d-core.d.ts +21 -0
  49. package/lib/types/client/scene3d-lazy.d.ts +8 -0
  50. package/lib/types/client/spec.d.ts +610 -0
  51. package/lib/types/client/toolview.d.ts +7 -0
  52. package/lib/types/index.d.ts +1 -0
  53. package/lib/types/plugin/index.d.ts +25 -0
  54. package/lib/types/plugin/invariant.d.ts +15 -0
  55. package/lib/types/plugin/tool.d.ts +27 -0
  56. package/lib/types/shared/fence-repair.d.ts +67 -0
  57. package/package.json +121 -0
  58. package/src/client/EChartNode.tsx +242 -0
  59. package/src/client/ErrorBoundary.tsx +66 -0
  60. package/src/client/GenuiBlock.module.css +1263 -0
  61. package/src/client/GenuiBlock.tsx +166 -0
  62. package/src/client/PlotBlock.module.css +155 -0
  63. package/src/client/PlotBlock.tsx +431 -0
  64. package/src/client/action-context.ts +32 -0
  65. package/src/client/asset-echarts.ts +25 -0
  66. package/src/client/asset-loader.ts +79 -0
  67. package/src/client/asset-mermaid.ts +13 -0
  68. package/src/client/asset-three.ts +12 -0
  69. package/src/client/blocks/advanced.tsx +435 -0
  70. package/src/client/blocks/basic.tsx +116 -0
  71. package/src/client/blocks/charts.tsx +319 -0
  72. package/src/client/blocks/diagram/geometry.ts +151 -0
  73. package/src/client/blocks/diagram/index.tsx +284 -0
  74. package/src/client/blocks/diagram/layout.ts +199 -0
  75. package/src/client/blocks/diagram/theme.ts +108 -0
  76. package/src/client/blocks/forms.tsx +523 -0
  77. package/src/client/blocks/render-node.tsx +260 -0
  78. package/src/client/blocks/state.ts +61 -0
  79. package/src/client/dom-fence.tsx +641 -0
  80. package/src/client/echarts-lazy.ts +35 -0
  81. package/src/client/fence-render.tsx +230 -0
  82. package/src/client/gallery.ts +128 -0
  83. package/src/client/guard.ts +1450 -0
  84. package/src/client/index.tsx +195 -0
  85. package/src/client/interaction-store.ts +122 -0
  86. package/src/client/mermaid-core.ts +121 -0
  87. package/src/client/mermaid-lazy.ts +31 -0
  88. package/src/client/mermaid-safe.ts +122 -0
  89. package/src/client/panel-command.ts +119 -0
  90. package/src/client/panel-store.ts +471 -0
  91. package/src/client/panel.tsx +185 -0
  92. package/src/client/parse-partial.ts +162 -0
  93. package/src/client/safe-math.ts +270 -0
  94. package/src/client/scene3d-core.ts +192 -0
  95. package/src/client/scene3d-lazy.ts +26 -0
  96. package/src/client/spec.ts +767 -0
  97. package/src/client/toolview.tsx +66 -0
  98. package/src/css-modules.d.ts +5 -0
  99. package/src/index.ts +12 -0
  100. package/src/plugin/index.ts +173 -0
  101. package/src/plugin/invariant.ts +30 -0
  102. package/src/plugin/node-shims.d.ts +31 -0
  103. package/src/plugin/tool.ts +330 -0
  104. package/src/shared/fence-repair.ts +244 -0
  105. package/tsconfig.json +35 -0
  106. 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
+ }