@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,431 @@
1
+ /**
2
+ * PlotBlock: renders one or more math functions as an SVG line chart. The
3
+ * model supplies expressions (e.g. "sin(x)") which are evaluated by the
4
+ * SafeMath white-listed evaluator — never eval — and sampled into polylines.
5
+ * Pure SVG: linear x/y scales computed in place, no chart library.
6
+ *
7
+ * v2 interactivity: series may declare `params` (e.g. {a: 2} in "a*sin(x)")
8
+ * which render as live sliders under the chart — dragging re-samples the
9
+ * curve in place. The plot itself supports drag-to-pan and wheel-to-zoom.
10
+ */
11
+ import { memo, useEffect, useRef, useState, type ReactNode } from 'react'
12
+ import { sampleExpr } from './safe-math.ts'
13
+ import css from './PlotBlock.module.css'
14
+
15
+ export interface PlotSeriesParam {
16
+ name: string
17
+ value: number
18
+ min?: number | undefined
19
+ max?: number | undefined
20
+ step?: number | undefined
21
+ /** v1.5: animate this parameter from its initial value to this target. */
22
+ animateTo?: number | undefined
23
+ /** Animation duration in ms (default 4000). */
24
+ durationMs?: number | undefined
25
+ /** Loop the animation (default false). */
26
+ loop?: boolean | undefined
27
+ }
28
+
29
+ export interface PlotSeries {
30
+ /** Math expression in x. */
31
+ expr: string
32
+ /** Optional label shown in a legend row. */
33
+ label?: string | undefined
34
+ /** Stroke color; defaults to the accent token (multi-series auto-assign). */
35
+ color?: string | undefined
36
+ /** v2.9 draw shape: line (default), area (fill to baseline), scatter. */
37
+ kind?: 'line' | 'area' | 'scatter' | undefined
38
+ /** v2: adjustable parameters, one slider each, live re-render. */
39
+ params?: PlotSeriesParam[] | undefined
40
+ }
41
+
42
+ /** Categorical palette for multi-series plots: host static tokens only
43
+ * (design system v2 — same families as the chart/avatar palettes in
44
+ * GenuiBlock, no off-theme hexes). CSS custom properties resolve inside
45
+ * inline `style` on SVG strokes and legend swatches. */
46
+ export const PLOT_COLORS = [
47
+ 'var(--dsw-static-deepseek-400)',
48
+ 'var(--dsw-static-green-400)',
49
+ 'var(--dsw-static-amber-400)',
50
+ 'var(--dsw-static-red-400)',
51
+ 'var(--dsw-static-blue-450)',
52
+ 'var(--dsw-static-deepseek-450)',
53
+ 'var(--dsw-static-neutral-bluish-400)',
54
+ 'var(--dsw-static-deepseek-300)',
55
+ ]
56
+
57
+ /** Series color: explicit wins; multi-series auto-assign from the palette. */
58
+ const seriesColor = (i: number, n: number, c?: string): string | undefined =>
59
+ c ?? (n > 1 ? PLOT_COLORS[i % PLOT_COLORS.length] : undefined)
60
+
61
+ export interface PlotBlockProps {
62
+ /** Functions to draw, in draw order. */
63
+ series: PlotSeries[]
64
+ /** Horizontal range, inclusive. */
65
+ xMin?: number | undefined
66
+ xMax?: number | undefined
67
+ /** Vertical bounds; omitted = auto-fit to the sampled data. */
68
+ yMin?: number | undefined
69
+ yMax?: number | undefined
70
+ /** Chart title. */
71
+ title?: string | undefined
72
+ }
73
+
74
+ const WIDTH = 480
75
+ const HEIGHT = 220
76
+ const PAD_L = 34
77
+ const PAD_R = 10
78
+ const PAD_T = 14
79
+ const PAD_B = 24
80
+ const SAMPLES = 240
81
+
82
+ /** Auto-fit y-range from finite samples with a small margin. */
83
+ function fitY(points: ReadonlyArray<readonly [number, number]>): [number, number] {
84
+ let lo = Infinity
85
+ let hi = -Infinity
86
+ for (const [, y] of points) {
87
+ if (y < lo) lo = y
88
+ if (y > hi) hi = y
89
+ }
90
+ if (!Number.isFinite(lo) || !Number.isFinite(hi)) return [-1, 1]
91
+ if (lo === hi) { lo -= 1; hi += 1 }
92
+ const pad = (hi - lo) * 0.08
93
+ return [lo - pad, hi + pad]
94
+ }
95
+
96
+ function niceTicks(min: number, max: number, count = 5): number[] {
97
+ const span = max - min
98
+ if (!Number.isFinite(span) || span <= 0) return []
99
+ const step = Math.pow(10, Math.floor(Math.log10(span / count)))
100
+ const err = span / count / step
101
+ const mult = err >= 7.5 ? 10 : err >= 3.5 ? 5 : err >= 1.5 ? 2 : 1
102
+ const size = step * mult
103
+ const ticks: number[] = []
104
+ for (let v = Math.ceil(min / size) * size; v <= max + size * 1e-9; v += size) {
105
+ ticks.push(Math.round(v * 1e9) / 1e9)
106
+ }
107
+ return ticks
108
+ }
109
+
110
+ function formatTick(v: number): string {
111
+ if (Math.abs(v) >= 1e5 || (Math.abs(v) < 1e-3 && v !== 0)) return v.toExponential(1)
112
+ return String(Math.round(v * 100) / 100)
113
+ }
114
+
115
+ /** Polyline with manual points sync: React 18 does not reliably update the
116
+ * SVG `points` attribute when only the value changes (the element is diffed
117
+ * but the attribute write is skipped), so this wrapper writes it through a
118
+ * ref on every render. */
119
+ function Polyline({ points, className, color }: { points: string; className: string | undefined; color?: string | undefined }) {
120
+ // React 18 does not reliably update the SVG `points` attribute when the
121
+ // value changes on an existing element (the DOM write is skipped). Keying
122
+ // the element on a prefix of the points string forces a fresh element per
123
+ // distinct curve, which sidesteps the update entirely. The prefix must
124
+ // change whenever the curve changes: include the first and last point.
125
+ const key = points.length > 40 ? `${points.slice(0, 16)}|${points.slice(-16)}` : points
126
+ return (
127
+ <polyline
128
+ key={key}
129
+ points={points}
130
+ className={className}
131
+ style={color !== undefined ? { stroke: color } : undefined}
132
+ />
133
+ )
134
+ }
135
+
136
+ /** Render one plot: grid, axes, one polyline per series, legend, sliders. */
137
+ export const PlotBlock = memo(function PlotBlock({
138
+ series, xMin: propXMin = -5, xMax: propXMax = 5, yMin, yMax, title,
139
+ }: PlotBlockProps) {
140
+ // Pan/zoom view state: x range plus a LOCKED y range. The y range is fit
141
+ // ONCE from the initial parameters and then fixed, so dragging a parameter
142
+ // slider changes the curve's SHAPE (amplitude/slope) instead of rescaling
143
+ // the axis every time — the user sees the function move, not the numbers.
144
+ const [view, setView] = useState(() => {
145
+ // Fit y from the default parameter values (the same fallback the render
146
+ // path uses), so the initial axis already frames the default curve.
147
+ const defaults: Record<string, number> = {}
148
+ for (const [si, s] of series.entries()) {
149
+ for (const p of s.params ?? []) {
150
+ if (p !== null && p !== undefined) defaults[`${si}:${p.name}`] = p.value
151
+ }
152
+ }
153
+ const probe = series.map((s, si) => {
154
+ const p: Record<string, number> = {}
155
+ for (const param of s.params ?? []) {
156
+ if (param === null || param === undefined) continue
157
+ p[param.name] = defaults[`${si}:${param.name}`] ?? param.value
158
+ }
159
+ return sampleExpr(s.expr, propXMin, propXMax, SAMPLES, p)
160
+ })
161
+ const [autoLo, autoHi] = fitY(probe.flatMap(p => p))
162
+ return {
163
+ xMin: propXMin,
164
+ xMax: propXMax,
165
+ yMin: yMin ?? autoLo,
166
+ yMax: yMax ?? autoHi,
167
+ }
168
+ })
169
+ const dragRef = useRef<{ startX: number; startY: number; xMin: number; xMax: number } | null>(null)
170
+
171
+ // Parameter values keyed by series index + param name; slider drag updates.
172
+ // Deliberately starts EMPTY: the initializer must never read series props
173
+ // (a parent re-mount can rebuild them mid-interaction), so every value
174
+ // falls back to the param's declared default at render time. This makes
175
+ // remounts during slider drags crash-proof.
176
+ const [params, setParams] = useState<Record<string, number>>(() => ({}))
177
+
178
+ // Animation: pick the first parameter that declares animateTo. A play/pause
179
+ // button drives it from its current value to animateTo over durationMs
180
+ // using requestAnimationFrame; the curve moves in place (y axis is locked).
181
+ const animParam = (() => {
182
+ for (const [si, s] of series.entries()) {
183
+ for (const p of s.params ?? []) {
184
+ if (p !== null && p !== undefined && p.animateTo !== undefined) {
185
+ return { si, param: p }
186
+ }
187
+ }
188
+ }
189
+ return null
190
+ })()
191
+ const [playing, setPlaying] = useState(false)
192
+ const [animProgress, setAnimProgress] = useState(0)
193
+ const animRef = useRef<number | null>(null)
194
+
195
+ useEffect(() => {
196
+ if (!playing || animParam === null) return
197
+ const from = params[`${animParam.si}:${animParam.param.name}`] ?? animParam.param.value
198
+ const to = animParam.param.animateTo!
199
+ const duration = animParam.param.durationMs ?? 4000
200
+ const start = performance.now()
201
+ const tick = (now: number): void => {
202
+ const t = Math.min(1, (now - start) / duration)
203
+ const eased = 1 - Math.pow(1 - t, 3) // easeOutCubic
204
+ const value = from + (to - from) * eased
205
+ setParams(prev => ({ ...prev, [`${animParam.si}:${animParam.param.name}`]: value }))
206
+ setAnimProgress(t)
207
+ if (t < 1) {
208
+ animRef.current = requestAnimationFrame(tick)
209
+ } else if (animParam.param.loop === true) {
210
+ setParams(prev => ({ ...prev, [`${animParam.si}:${animParam.param.name}`]: animParam.param.value }))
211
+ setAnimProgress(0)
212
+ animRef.current = requestAnimationFrame(() => setPlaying(p => p))
213
+ } else {
214
+ setPlaying(false)
215
+ }
216
+ }
217
+ animRef.current = requestAnimationFrame(tick)
218
+ return () => { if (animRef.current !== null) cancelAnimationFrame(animRef.current) }
219
+ }, [playing, animParam?.si, animParam?.param.name, animParam?.param.animateTo])
220
+
221
+ const xMin = view.xMin
222
+ const xMax = view.xMax
223
+ const lo = view.yMin
224
+ const hi = view.yMax
225
+ const plotW = WIDTH - PAD_L - PAD_R
226
+ const plotH = HEIGHT - PAD_T - PAD_B
227
+ const fromX = (px: number): number => xMin + ((px - PAD_L) / plotW) * (xMax - xMin)
228
+ const toX = (x: number): number => PAD_L + ((x - xMin) / (xMax - xMin)) * plotW
229
+ const toY = (y: number): number => PAD_T + (1 - (y - lo) / (hi - lo)) * plotH
230
+
231
+ // Sample with current parameter values, then convert to screen coordinates
232
+ // in one pass against the LOCKED y range — the `points` string changes with
233
+ // the parameters, so the curve's shape visibly responds to the slider.
234
+ const sampled = series.map((s, si) => {
235
+ const p: Record<string, number> = {}
236
+ for (const param of s.params ?? []) {
237
+ if (param === null || param === undefined) continue
238
+ p[param.name] = params[`${si}:${param.name}`] ?? param.value
239
+ }
240
+ const pts = sampleExpr(s.expr, xMin, xMax, SAMPLES, p)
241
+ return {
242
+ series: s,
243
+ points: pts.map(([x, y]) => `${toX(x).toFixed(2)},${toY(y).toFixed(2)}`).join(' '),
244
+ }
245
+ })
246
+
247
+ const xTicks = niceTicks(xMin, xMax)
248
+ const yTicks = niceTicks(lo, hi)
249
+ const hasData = sampled.some(p => p.points.length > 1)
250
+ const hasValidRange = Number.isFinite(xMin) && Number.isFinite(xMax) && xMax > xMin && Number.isFinite(hi) && Number.isFinite(lo) && hi > lo
251
+
252
+ // v2.9 draw shapes: line (polyline), area (fill to the locked baseline),
253
+ // scatter (dots only). Colors stay on the shared categorical palette.
254
+ const renderSeries = (s: PlotSeries, i: number, points: string): ReactNode => {
255
+ const color = seriesColor(i, sampled.length, s.color) ?? 'var(--dsw-alias-state-business-primary, #4f8ef7)'
256
+ const kind = s.kind ?? 'line'
257
+ if (kind === 'scatter') {
258
+ const coords = points === '' ? [] : points.split(' ').map(pt => {
259
+ const [x, y] = pt.split(',')
260
+ return [Number(x), Number(y)] as const
261
+ })
262
+ return (
263
+ <g key={i}>
264
+ {coords.map(([cx, cy], k) => <circle key={k} cx={cx} cy={cy} r={2.6} className={css.scatterDot} style={{ fill: color }} />)}
265
+ </g>
266
+ )
267
+ }
268
+ if (kind === 'area') {
269
+ if (points === '') return <Polyline key={i} points="" className={css.line} color={color} />
270
+ const firstX = points.slice(0, points.indexOf(' ')).split(',')[0]!
271
+ const lastX = points.slice(points.lastIndexOf(' ') + 1).split(',')[0]!
272
+ const baseY = toY(lo)
273
+ const poly = `${firstX},${baseY.toFixed(2)} ${points} ${lastX},${baseY.toFixed(2)}`
274
+ return <polygon key={i} points={poly} className={css.area} style={{ fill: color }} />
275
+ }
276
+ return <Polyline key={i} points={points} className={css.line} color={color} />
277
+ }
278
+
279
+ // Drag-to-pan and wheel-to-zoom on the SVG surface. Pan/zoom move the x
280
+ // window only; the y range stays locked so parameter changes stay visible.
281
+ const onPointerDown = (e: React.PointerEvent): void => {
282
+ dragRef.current = { startX: e.clientX, startY: e.clientY, xMin, xMax }
283
+ ;(e.target as Element).setPointerCapture?.(e.pointerId)
284
+ }
285
+ const onPointerMove = (e: React.PointerEvent): void => {
286
+ const d = dragRef.current
287
+ if (d === null) return
288
+ const span = d.xMax - d.xMin
289
+ const dx = ((d.startX - e.clientX) / plotW) * span
290
+ setView(prev => ({ xMin: d.xMin + dx, xMax: d.xMax + dx, yMin: prev.yMin, yMax: prev.yMax }))
291
+ }
292
+ const onPointerUp = (): void => { dragRef.current = null }
293
+ const onWheel = (e: React.WheelEvent): void => {
294
+ const span = xMax - xMin
295
+ const factor = e.deltaY > 0 ? 1.15 : 1 / 1.15
296
+ const next = span * factor
297
+ const rect = (e.currentTarget as Element).getBoundingClientRect()
298
+ const cx = fromX(((e.clientX - rect.left) / rect.width) * WIDTH)
299
+ const left = cx - ((cx - xMin) / span) * next
300
+ setView(prev => ({ xMin: left, xMax: left + next, yMin: prev.yMin, yMax: prev.yMax }))
301
+ }
302
+
303
+ const hasParams = series.some(s => (s.params?.length ?? 0) > 0)
304
+
305
+ return (
306
+ <div className={css.block} data-genui-plot>
307
+ {title !== undefined && <div className={css.title}>{title}</div>}
308
+ {hasData && hasValidRange ? (
309
+ <svg
310
+ width="100%"
311
+ viewBox={`0 0 ${WIDTH} ${HEIGHT}`}
312
+ role="img"
313
+ aria-label={title ?? 'function plot'}
314
+ className={css.surface}
315
+ onPointerDown={onPointerDown}
316
+ onPointerMove={onPointerMove}
317
+ onPointerUp={onPointerUp}
318
+ onPointerLeave={onPointerUp}
319
+ onWheel={onWheel}
320
+ >
321
+ {/* y grid */}
322
+ {yTicks.map(t => (
323
+ <g key={`y${t}`}>
324
+ <line x1={PAD_L} x2={WIDTH - PAD_R} y1={toY(t)} y2={toY(t)} className={css.gridLine} />
325
+ <text x={PAD_L - 6} y={toY(t) + 4} className={css.tick} textAnchor="end">{formatTick(t)}</text>
326
+ </g>
327
+ ))}
328
+ {/* x grid */}
329
+ {xTicks.map(t => (
330
+ <g key={`x${t}`}>
331
+ <line x1={toX(t)} x2={toX(t)} y1={PAD_T} y2={HEIGHT - PAD_B} className={css.gridLine} />
332
+ <text x={toX(t)} y={HEIGHT - PAD_B + 14} className={css.tick} textAnchor="middle">{formatTick(t)}</text>
333
+ </g>
334
+ ))}
335
+ {/* axes */}
336
+ <line x1={PAD_L} x2={WIDTH - PAD_R} y1={HEIGHT - PAD_B} y2={HEIGHT - PAD_B} className={css.axis} />
337
+ <line x1={PAD_L} x2={PAD_L} y1={PAD_T} y2={HEIGHT - PAD_B} className={css.axis} />
338
+ {/* series */}
339
+ {sampled.map(({ series: s, points }, i) => renderSeries(s, i, points))}
340
+ </svg>
341
+ ) : (
342
+ <div className={css.empty}>
343
+ {series.map((s, i) => <div key={i} className={css.emptyRow}>{s.expr} — 无法绘制(表达式无效或范围非法)</div>)}
344
+ </div>
345
+ )}
346
+ {hasParams && (
347
+ <div className={css.sliders}>
348
+ <div className={css.slidersHead}>
349
+ <span className={css.slidersTitle}>参数调节</span>
350
+ <button
351
+ type="button"
352
+ className={css.resetBtn}
353
+ onClick={() => {
354
+ setPlaying(false)
355
+ const reset: Record<string, number> = {}
356
+ for (const [si, s] of series.entries()) {
357
+ for (const p of s.params ?? []) {
358
+ if (p !== null && p !== undefined) reset[`${si}:${p.name}`] = p.value
359
+ }
360
+ }
361
+ setParams(reset)
362
+ setAnimProgress(0)
363
+ }}
364
+ >
365
+ ↺ 重置
366
+ </button>
367
+ </div>
368
+ {series.map((s, si) => (s.params ?? []).map(p => {
369
+ if (p === null || p === undefined) return null
370
+ const key = `${si}:${p.name}`
371
+ const value = params[key] ?? p.value
372
+ return (
373
+ <label key={key} className={css.sliderRow}>
374
+ <span className={css.sliderName}>{s.label ?? s.expr} · {p.name}</span>
375
+ <input
376
+ type="range"
377
+ className={css.slider}
378
+ min={p.min ?? 0}
379
+ max={p.max ?? 10}
380
+ step={p.step ?? 0.1}
381
+ value={value}
382
+ onChange={e => {
383
+ // Read the value synchronously: React 18 may null out
384
+ // currentTarget after the handler in concurrent renders.
385
+ const next = Number(e.currentTarget.value)
386
+ setParams(prev => ({ ...prev, [key]: next }))
387
+ }}
388
+ />
389
+ <span className={css.sliderValue}>{Math.round(value * 100) / 100}</span>
390
+ </label>
391
+ )
392
+ }))}
393
+ </div>
394
+ )}
395
+ {animParam !== null && (
396
+ <div className={css.animBar}>
397
+ <button
398
+ type="button"
399
+ className={css.playBtn}
400
+ onClick={() => {
401
+ if (playing) { setPlaying(false) }
402
+ else {
403
+ // Reset to the declared start value before playing.
404
+ setParams(prev => ({ ...prev, [`${animParam.si}:${animParam.param.name}`]: animParam.param.value }))
405
+ setAnimProgress(0)
406
+ setPlaying(true)
407
+ }
408
+ }}
409
+ >
410
+ {playing ? '⏸ 暂停' : '▶ 播放动画'}
411
+ </button>
412
+ {playing && (
413
+ <div className={css.animTrack}>
414
+ <div className={css.animFill} style={{ width: `${animProgress * 100}%` }} />
415
+ </div>
416
+ )}
417
+ </div>
418
+ )}
419
+ {(series.length > 1 || hasParams) && (
420
+ <div className={css.legend}>
421
+ {series.map((s, i) => (
422
+ <span key={i} className={css.legendItem}>
423
+ <span className={css.legendSwatch} style={{ background: seriesColor(i, series.length, s.color) ?? 'var(--dsw-alias-state-business-primary, #4f8ef7)' }} />
424
+ {s.label ?? s.expr}
425
+ </span>
426
+ ))}
427
+ </div>
428
+ )}
429
+ </div>
430
+ )
431
+ })
@@ -0,0 +1,32 @@
1
+ /**
2
+ * GenUI action context, plugin-owned.
3
+ *
4
+ * Contract hosts (`dsh-host-0812-contract` and later) ship the same context
5
+ * in `@deepseek-ai/dsh-client-ui-primitives` and MarkdownText installs the
6
+ * provider around fence renders — the renderer MUST consume that exact
7
+ * context instance so the host's action wiring reaches our components.
8
+ * Pristine hosts (the stock snapshot line) export no such symbol; the DOM
9
+ * render channel then falls back to the local context and installs its own
10
+ * provider (this package wraps every tree it mounts).
11
+ *
12
+ * Absent provider = v1 behavior: interactive components are display-only
13
+ * (their `action` is ignored). The hook never throws.
14
+ */
15
+ import { createContext, useContext } from 'react'
16
+ import type { Context } from 'react'
17
+ import * as primitives from '@deepseek-ai/dsh-client-ui-primitives'
18
+
19
+ /** v2 action handler: component action + its collected data. */
20
+ export type GenuiActionHandler = (action: string, payload: Record<string, unknown>) => void
21
+
22
+ /** Local fallback context (pristine hosts). */
23
+ const localContext = createContext<GenuiActionHandler | undefined>(undefined)
24
+
25
+ /** Host-provided context when the deployment ships it, else the local one. */
26
+ export const GenuiActionContext: Context<GenuiActionHandler | undefined> =
27
+ (primitives as unknown as { GenuiActionContext?: Context<GenuiActionHandler | undefined> }).GenuiActionContext ?? localContext
28
+
29
+ /** Read the installed action handler, if any. */
30
+ export function useGenuiAction(): GenuiActionHandler | undefined {
31
+ return useContext(GenuiActionContext)
32
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * ECharts asset-bundle entry: registers the echarts engine on
3
+ * `window.__GenuiAssets__.echarts`. Built as a standalone IIFE into
4
+ * `lib/assets/echarts.js` and served by the plugin's node-half route; loaded
5
+ * on demand by echarts-lazy when a spec contains an `echart` node.
6
+ * @module @changfenhuang/dsh-genui/client/asset-echarts
7
+ */
8
+ import { init as echartsInit, type EChartsType, type EChartsCoreOption } from 'echarts'
9
+
10
+ /** The engine surface registered by the echarts asset bundle. */
11
+ export interface EChartsAssetApi {
12
+ /** Create an ECharts instance on `el`, apply `option`, return the instance. */
13
+ createChart: (el: HTMLElement, option: EChartsCoreOption, opts?: { height?: number }) => EChartsType
14
+ }
15
+
16
+ function createChart(el: HTMLElement, option: EChartsCoreOption, opts?: { height?: number }): EChartsType {
17
+ const initOpts = opts !== undefined && opts.height !== undefined ? { height: opts.height } : undefined
18
+ const instance = echartsInit(el, undefined, initOpts)
19
+ instance.setOption(option)
20
+ return instance
21
+ }
22
+
23
+ const win = globalThis as unknown as { __GenuiAssets__?: Record<string, unknown> }
24
+ const assets = win.__GenuiAssets__ ?? (win.__GenuiAssets__ = {})
25
+ assets.echarts = { createChart }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Asset-bundle loader: fetches one of the plugin's lazy engine bundles
3
+ * (`lib/assets/mermaid.js` / `lib/assets/three.js`) by script injection and
4
+ * hands back the engine surface the bundle registered on
5
+ * `window.__GenuiAssets__`.
6
+ *
7
+ * The bundles are served by the plugin's OWN node-half HTTP route
8
+ * (`/plugins/@changfenhuang/dsh-genui/assets/*`), registered through the host
9
+ * webserver service — no host source change needed, and the longest-prefix
10
+ * rule lets this route win over the generic `/plugins` bundle route. Each
11
+ * file is loaded at most once per page (promise memoized); a rejection is
12
+ * final for the page (the node's fallback UI takes over).
13
+ *
14
+ * The rev query rides the boot graph: the plugin's own row in
15
+ * `window.__DSH_BOOT__` carries the client bundle rev, so a plugin rebuild
16
+ * busts the asset cache together with the main bundle. Absent graph = no
17
+ * query; the route serves no-cache anyway.
18
+ * @module @changfenhuang/dsh-genui/client/asset-loader
19
+ */
20
+
21
+ /** The plugin's entry id — mirrors the loader entry name (package name). */
22
+ const PLUGIN_ID = '@changfenhuang/dsh-genui'
23
+
24
+ /** Assets directory served by the node-half route. */
25
+ const ASSET_DIR = `/plugins/${PLUGIN_ID}/assets`
26
+
27
+ /** Boot graph shape read from `window.__DSH_BOOT__` (subset, defensive). */
28
+ interface BootGraphLike {
29
+ entries?: Array<{ id?: string; rev?: string }>
30
+ }
31
+
32
+ /** The global the asset bundles register their engines on. */
33
+ interface AssetGlobal {
34
+ __GenuiAssets__?: Record<string, unknown>
35
+ }
36
+
37
+ /** Resolve an asset URL, appending the bundle rev for cache busting when the
38
+ * boot graph exposes it. */
39
+ export function assetUrl(file: string): string {
40
+ const graph = (window as unknown as { __DSH_BOOT__?: BootGraphLike }).__DSH_BOOT__
41
+ const rev = graph?.entries?.find(entry => entry.id === PLUGIN_ID)?.rev
42
+ return `${ASSET_DIR}/${file}${rev === undefined ? '' : `?rev=${rev}`}`
43
+ }
44
+
45
+ const pending = new Map<string, Promise<Record<string, unknown>>>()
46
+
47
+ /**
48
+ * Load one asset bundle and return the engines it registered. Memoized per
49
+ * file: repeated requests (several mermaid nodes, re-renders) share one
50
+ * script load; a failed load stays failed for the page (the component shows
51
+ * its fallback).
52
+ * @param name - 'mermaid', 'three', or 'echarts'.
53
+ * @returns the registered engine surface.
54
+ */
55
+ export function loadGenuiAsset<T>(name: 'mermaid' | 'three' | 'echarts'): Promise<T> {
56
+ const file = `${name}.js`
57
+ const existing = pending.get(file)
58
+ if (existing !== undefined) return existing as Promise<T>
59
+ const task = new Promise<T>((resolve, reject) => {
60
+ const script = document.createElement('script')
61
+ script.src = assetUrl(file)
62
+ script.async = true
63
+ script.onload = () => {
64
+ const global = (window as unknown as AssetGlobal).__GenuiAssets__ ?? {}
65
+ const api = global[name]
66
+ if (api === undefined) {
67
+ reject(new Error(`genui asset '${file}' loaded but registered no '${name}' engine`))
68
+ return
69
+ }
70
+ resolve(api as T)
71
+ }
72
+ script.onerror = () => {
73
+ reject(new Error(`genui asset '${file}' failed to load (host asset route missing?)`))
74
+ }
75
+ document.head.appendChild(script)
76
+ })
77
+ pending.set(file, task as unknown as Promise<Record<string, unknown>>)
78
+ return task
79
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Mermaid asset-bundle entry: registers the mermaid engine on
3
+ * `window.__GenuiAssets__.mermaid`. Built as a standalone IIFE into
4
+ * `lib/assets/mermaid.js` and served by the plugin's node-half route; loaded
5
+ * on demand by mermaid-lazy.
6
+ * @module @changfenhuang/dsh-genui/client/asset-mermaid
7
+ */
8
+ import { renderMermaid } from './mermaid-core.ts'
9
+ import { assertSafeSvg, repairMermaidSource } from './mermaid-safe.ts'
10
+
11
+ const win = globalThis as unknown as { __GenuiAssets__?: Record<string, unknown> }
12
+ const assets = win.__GenuiAssets__ ?? (win.__GenuiAssets__ = {})
13
+ assets.mermaid = { assertSafeSvg, repairMermaidSource, renderMermaid }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * three.js asset-bundle entry: registers the scene renderer on
3
+ * `window.__GenuiAssets__.three`. Built as a standalone IIFE into
4
+ * `lib/assets/three.js` and served by the plugin's node-half route; loaded on
5
+ * demand by scene3d-lazy.
6
+ * @module @changfenhuang/dsh-genui/client/asset-three
7
+ */
8
+ import { mountScene } from './scene3d-core.ts'
9
+
10
+ const win = globalThis as unknown as { __GenuiAssets__?: Record<string, unknown> }
11
+ const assets = win.__GenuiAssets__ ?? (win.__GenuiAssets__ = {})
12
+ assets.three = { mountScene }