@changfenhuang/dsh-genui 0.9.9 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/SKILL.md +128 -6
- package/lib/assets/echarts-core.js +43 -0
- package/lib/assets/{echarts.js → echarts-full.js} +1 -1
- package/lib/assets/mermaid.js +1 -1
- package/lib/client.js +5 -3
- package/lib/index.js +230 -69
- package/lib/types/client/EChartNode.d.ts +9 -0
- package/lib/types/client/GenuiBlock.d.ts +3 -4
- package/lib/types/client/asset-echarts-core.d.ts +1 -0
- package/lib/types/client/asset-echarts.d.ts +3 -2
- package/lib/types/client/asset-loader.d.ts +2 -2
- package/lib/types/client/blocks/advanced.d.ts +0 -3
- package/lib/types/client/blocks/charts.d.ts +23 -12
- package/lib/types/client/blocks/state.d.ts +5 -3
- package/lib/types/client/echarts-lazy.d.ts +4 -1
- package/lib/types/client/genui-runtime/schema.d.ts +11 -1
- package/lib/types/client/inline.d.ts +29 -0
- package/lib/types/client/spec.d.ts +76 -6
- package/lib/types/plugin/index.d.ts +1 -1
- package/package.json +4 -3
- package/src/client/EChartNode.tsx +233 -19
- package/src/client/GenuiBlock.module.css +895 -145
- package/src/client/GenuiBlock.tsx +31 -23
- package/src/client/PlotBlock.module.css +17 -1
- package/src/client/PlotBlock.tsx +33 -1
- package/src/client/asset-echarts-core.ts +39 -0
- package/src/client/asset-echarts.ts +4 -3
- package/src/client/asset-loader.ts +5 -3
- package/src/client/blocks/advanced.tsx +60 -16
- package/src/client/blocks/charts.tsx +761 -144
- package/src/client/blocks/render-node.tsx +252 -11
- package/src/client/blocks/state.ts +5 -3
- package/src/client/dom-fence.tsx +60 -6
- package/src/client/echarts-lazy.ts +11 -2
- package/src/client/fence-render.tsx +6 -5
- package/src/client/gallery.ts +78 -2
- package/src/client/genui-runtime/schema.ts +45 -11
- package/src/client/guard.ts +161 -23
- package/src/client/index.tsx +14 -3
- package/src/client/inline.ts +73 -0
- package/src/client/mermaid-core.ts +42 -5
- package/src/client/spec.ts +79 -6
- package/src/plugin/index.ts +33 -45
- package/src/plugin/tool.ts +5 -1
- package/tsdown.config.ts +2 -1
|
@@ -14,4 +14,7 @@ export interface EChartsInstance {
|
|
|
14
14
|
*/
|
|
15
15
|
export declare function createChart(el: HTMLElement, option: unknown, opts?: {
|
|
16
16
|
height?: number;
|
|
17
|
-
}): Promise<EChartsInstance>;
|
|
17
|
+
}, engine?: 'core' | 'full'): Promise<EChartsInstance>;
|
|
18
|
+
/** Presets the core bundle can draw; anything else (or a raw `option`) needs
|
|
19
|
+
* the full engine. Kept next to the loader so the split stays in one place. */
|
|
20
|
+
export declare const CORE_PRESETS: ReadonlySet<string>;
|
|
@@ -57,7 +57,17 @@ export declare const DIAGRAM_NODE_TYPES: readonly ["focal", "backend", "store",
|
|
|
57
57
|
export declare const DIAGRAM_VARIANTS: readonly ["light", "dark", "editorial"];
|
|
58
58
|
export declare const DIAGRAM_EDGE_KINDS: readonly ["solid", "dashed", "accent", "link"];
|
|
59
59
|
export declare const DIAGRAM_ROUTES: readonly ["auto", "orthogonal", "straight"];
|
|
60
|
-
export declare const ECHART_PRESETS: readonly ["bar", "line", "area", "pie", "scatter"];
|
|
60
|
+
export declare const ECHART_PRESETS: readonly ["bar", "line", "area", "pie", "scatter", "radar", "gauge", "funnel", "treemap", "sankey", "graph", "heatmap", "bigline"];
|
|
61
|
+
/** Oversized single-number stat (one per fence as the visual anchor). */
|
|
62
|
+
export declare const STAT_SIZES: readonly ["hero"];
|
|
63
|
+
/** Progress shapes: a track (default) or a circular gauge. */
|
|
64
|
+
export declare const PROGRESS_VARIANTS: readonly ["bar", "ring"];
|
|
65
|
+
/** Semantic card surfaces. */
|
|
66
|
+
export declare const CARD_TONES: readonly ["info", "success", "warning", "danger"];
|
|
67
|
+
/** Hero cover tones. */
|
|
68
|
+
export declare const HERO_TONES: readonly ["accent", "success", "warning", "danger"];
|
|
69
|
+
/** Table cell renderers (`table.types`, one entry per column). */
|
|
70
|
+
export declare const TABLE_CELL_TYPES: readonly ["text", "num", "delta", "bar", "badge", "spark", "ring", "index", "group"];
|
|
61
71
|
/** Root GenUI specification metadata used by diagnostics. */
|
|
62
72
|
export declare const GENUI_SPEC_SCHEMA: ComponentSchema;
|
|
63
73
|
/** Backwards-friendly short alias for the root specification schema. */
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal inline markup for text-bearing fields.
|
|
3
|
+
*
|
|
4
|
+
* Prose-heavy answers need emphasis INSIDE a sentence: a command, a term, a key
|
|
5
|
+
* phrase, a link. Until now every field was a plain string, so emphasis meant
|
|
6
|
+
* splitting one sentence into several nodes and the paragraph read as broken.
|
|
7
|
+
*
|
|
8
|
+
* Supported (deliberately tiny, no nesting):
|
|
9
|
+
* `code` inline code chip
|
|
10
|
+
* **bold** emphasis that does not wrap or become a block
|
|
11
|
+
* ==mark== highlight
|
|
12
|
+
* [text](https://…) inline link (http/https/mailto only)
|
|
13
|
+
*
|
|
14
|
+
* Safety: this NEVER produces HTML. Each token becomes a React element, so the
|
|
15
|
+
* host's HTML parser is never involved (the whole client builds elements, the
|
|
16
|
+
* only innerHTML in the project is mermaid's sanitized SVG). An unterminated or
|
|
17
|
+
* unknown marker is rendered literally rather than erroring, and a link whose
|
|
18
|
+
* href fails {@link safeHref} degrades to its label text.
|
|
19
|
+
* @module @changfenhuang/dsh-genui/client/inline
|
|
20
|
+
*/
|
|
21
|
+
import { type ReactNode } from 'react';
|
|
22
|
+
/** True when the string contains anything the parser understands. */
|
|
23
|
+
export declare function hasInlineMarkup(text: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Render one string as React nodes with inline markup applied.
|
|
26
|
+
* @param text - the raw field value.
|
|
27
|
+
* @returns the original string when nothing matched, else the node list.
|
|
28
|
+
*/
|
|
29
|
+
export declare function renderInline(text: string): ReactNode;
|
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
* are operable, but events do NOT flow back to the model.
|
|
10
10
|
*/
|
|
11
11
|
/** One node in the component tree. */
|
|
12
|
-
|
|
12
|
+
/** Layout hints accepted by every node: `span` is how many columns the node
|
|
13
|
+
* occupies as a direct child of a `grid` (bento layouts). */
|
|
14
|
+
export interface GenuiLayoutHints {
|
|
15
|
+
span?: number;
|
|
16
|
+
}
|
|
17
|
+
export type GenuiNode = (GenuiText | GenuiRow | GenuiCol | GenuiGrid | GenuiCard | GenuiButton | GenuiInput | GenuiSelect | GenuiCheckbox | GenuiLink | GenuiImage | GenuiAudio | GenuiVideo | GenuiBadge | GenuiStat | GenuiHero | GenuiProgress | GenuiDivider | GenuiList | GenuiTable | GenuiChart | GenuiTabs | GenuiAvatar | GenuiSpacer | GenuiPlot | GenuiCallout | GenuiSteps | GenuiKeyValue | GenuiDiff | GenuiJson | GenuiCode | GenuiRadio | GenuiSubmit | GenuiSwitch | GenuiSlider | GenuiTextarea | GenuiAccordion | GenuiCopy | GenuiMermaid | GenuiScene3D | GenuiTimeline | GenuiFileTree | GenuiBreadcrumb | GenuiQuiz | GenuiDiagram) & GenuiLayoutHints | GenuiEChart;
|
|
13
18
|
export interface GenuiSpec {
|
|
14
19
|
/** Short title shown as the card banner. */
|
|
15
20
|
title?: string;
|
|
@@ -133,18 +138,39 @@ export interface GenuiBadge {
|
|
|
133
138
|
tone?: 'success' | 'warn' | 'danger' | 'accent';
|
|
134
139
|
icon?: string;
|
|
135
140
|
}
|
|
141
|
+
/** Cover band: the answer's focal point. One per fence — an eyebrow label, an
|
|
142
|
+
* oversized metric, a title and a subtitle on a soft tinted surface. */
|
|
143
|
+
export interface GenuiHero {
|
|
144
|
+
type: 'hero';
|
|
145
|
+
title: string;
|
|
146
|
+
subtitle?: string;
|
|
147
|
+
/** Oversized metric, e.g. "99.96%" / "3.2x". */
|
|
148
|
+
value?: string;
|
|
149
|
+
/** Eyebrow label above the metric. */
|
|
150
|
+
label?: string;
|
|
151
|
+
delta?: string;
|
|
152
|
+
spark?: number[];
|
|
153
|
+
tone?: 'accent' | 'success' | 'warning' | 'danger';
|
|
154
|
+
}
|
|
136
155
|
export interface GenuiStat {
|
|
137
156
|
type: 'stat';
|
|
138
157
|
label: string;
|
|
139
158
|
value: string;
|
|
140
159
|
delta?: string;
|
|
160
|
+
/** Optional micro trend line (finite numbers, 2..60 points). */
|
|
161
|
+
spark?: number[];
|
|
162
|
+
/** `hero` renders one oversized number — use it once per fence as the anchor. */
|
|
163
|
+
size?: 'hero';
|
|
141
164
|
}
|
|
142
165
|
export interface GenuiProgress {
|
|
143
166
|
type: 'progress';
|
|
144
|
-
label?: string;
|
|
145
|
-
/** 0..100 */
|
|
146
167
|
value: number;
|
|
168
|
+
label?: string;
|
|
147
169
|
valueLabel?: string;
|
|
170
|
+
/** `bar` (default) draws a track; `ring` draws a circular gauge. */
|
|
171
|
+
variant?: 'bar' | 'ring';
|
|
172
|
+
/** Optional target marker on the bar track (0-100). */
|
|
173
|
+
target?: number;
|
|
148
174
|
}
|
|
149
175
|
export interface GenuiDivider {
|
|
150
176
|
type: 'divider';
|
|
@@ -176,10 +202,16 @@ export interface GenuiGrid {
|
|
|
176
202
|
export interface GenuiCard {
|
|
177
203
|
type: 'card';
|
|
178
204
|
title?: string;
|
|
205
|
+
/** Semantic tint for the card surface (default: neutral). */
|
|
206
|
+
tone?: 'info' | 'success' | 'warning' | 'danger';
|
|
207
|
+
/** Explicit accent (hex) driving the border, title and wash. */
|
|
208
|
+
accent?: string;
|
|
179
209
|
items: GenuiNode[];
|
|
180
210
|
}
|
|
181
211
|
export interface GenuiList {
|
|
182
212
|
type: 'list';
|
|
213
|
+
/** Field id of an input/select: its value filters the items locally. */
|
|
214
|
+
filter?: string;
|
|
183
215
|
items: Array<string | {
|
|
184
216
|
title: string;
|
|
185
217
|
desc?: string;
|
|
@@ -189,7 +221,29 @@ export interface GenuiTable {
|
|
|
189
221
|
type: 'table';
|
|
190
222
|
columns: string[];
|
|
191
223
|
rows: Array<Array<string | number>>;
|
|
192
|
-
|
|
224
|
+
/** Per-column cell type; missing = auto (numeric right-align, signed delta). */
|
|
225
|
+
types?: TableCellType[];
|
|
226
|
+
/** Append a 合计 footer row (numeric columns are summed). */
|
|
227
|
+
total?: boolean;
|
|
228
|
+
/** Show 复制 Markdown / 复制 CSV chips above the table. */
|
|
229
|
+
export?: boolean;
|
|
230
|
+
/** Optional master-detail payload, positionally aligned with `rows`:
|
|
231
|
+
* `details[i]` is what row i expands into (omit / empty = not expandable). */
|
|
232
|
+
details?: Array<GenuiNode[] | null>;
|
|
233
|
+
/** Field id of an input/select: its live value filters the rows locally
|
|
234
|
+
* (substring match), so the table is searchable without a model round trip. */
|
|
235
|
+
filter?: string;
|
|
236
|
+
/** Restrict `filter` to one column index (default: every column). */
|
|
237
|
+
filterColumn?: number;
|
|
238
|
+
/** Field id of a select whose value is a column header: sorts by it locally. */
|
|
239
|
+
sortField?: string;
|
|
240
|
+
}
|
|
241
|
+
/** How a table column's cells render.
|
|
242
|
+
* - `bar` / `ring`: the cell is read as 0-100
|
|
243
|
+
* - `spark`: the cell is a number list ("3,5,4,8") drawn as a mini trend
|
|
244
|
+
* - `index`: the 1-based row number (cell content is ignored)
|
|
245
|
+
* - `delta` / `num` / `badge` / `text`: text treatments. */
|
|
246
|
+
export type TableCellType = 'text' | 'num' | 'delta' | 'bar' | 'badge' | 'spark' | 'ring' | 'index' | 'group';
|
|
193
247
|
export interface GenuiChartDatum {
|
|
194
248
|
label: string;
|
|
195
249
|
value: number;
|
|
@@ -200,12 +254,20 @@ export interface GenuiChart {
|
|
|
200
254
|
/** Chart shape: bars (default), line (trend), donut (share). */
|
|
201
255
|
kind?: 'bars' | 'line' | 'donut';
|
|
202
256
|
data: GenuiChartDatum[];
|
|
203
|
-
/** Multi-series grouped bars
|
|
257
|
+
/** Multi-series: grouped bars, or one line per entry when kind is line. */
|
|
204
258
|
series?: Array<{
|
|
205
259
|
label: string;
|
|
206
260
|
color?: string;
|
|
207
261
|
data: GenuiChartDatum[];
|
|
208
262
|
}>;
|
|
263
|
+
/** Bars only: horizontal bars (rankings, long category labels). */
|
|
264
|
+
horizontal?: boolean;
|
|
265
|
+
/** Bars + series: stack the series instead of grouping them. */
|
|
266
|
+
stacked?: boolean;
|
|
267
|
+
/** Explicit categorical palette (hex) — overrides the host theme colours. */
|
|
268
|
+
palette?: string[];
|
|
269
|
+
/** Field id of an input/select: its value filters the categories locally. */
|
|
270
|
+
filter?: string;
|
|
209
271
|
}
|
|
210
272
|
export interface GenuiTab {
|
|
211
273
|
label: string;
|
|
@@ -569,7 +631,7 @@ export interface GenuiDiagram {
|
|
|
569
631
|
}
|
|
570
632
|
/** Preset chart kinds the `echart` node can build from `data`/`series` without
|
|
571
633
|
* a full ECharts option. Each maps to a themed option template. */
|
|
572
|
-
export type EChartPreset = 'bar' | 'line' | 'area' | 'pie' | 'scatter';
|
|
634
|
+
export type EChartPreset = 'bar' | 'line' | 'area' | 'pie' | 'scatter' | 'radar' | 'gauge' | 'funnel' | 'treemap' | 'sankey' | 'graph' | 'heatmap' | 'bigline';
|
|
573
635
|
/** ECharts node: renders a full ECharts chart. Two modes:
|
|
574
636
|
*
|
|
575
637
|
* 1. **Full option** (`option` set): the model provides a standard ECharts
|
|
@@ -601,6 +663,14 @@ export interface GenuiEChart {
|
|
|
601
663
|
color?: string;
|
|
602
664
|
data: GenuiChartDatum[];
|
|
603
665
|
}>;
|
|
666
|
+
/** Node/edge data for the `sankey` and `graph` presets. */
|
|
667
|
+
links?: Array<{
|
|
668
|
+
from: string;
|
|
669
|
+
to: string;
|
|
670
|
+
value?: number;
|
|
671
|
+
}>;
|
|
672
|
+
/** Explicit categorical palette (hex) for preset mode. */
|
|
673
|
+
palette?: string[];
|
|
604
674
|
/** Full ECharts option object. When present, `preset`/`data`/`series` are
|
|
605
675
|
* ignored. This is a pass-through to `echarts.setOption`. */
|
|
606
676
|
option?: Record<string, unknown>;
|
|
@@ -13,7 +13,7 @@ import { Context } from '@deepseek-ai/cordis';
|
|
|
13
13
|
* Deliberately slim: the `genui` skill carries the full component→field
|
|
14
14
|
* mapping; this section keeps only the contract that must always be
|
|
15
15
|
* present (fence syntax, type whitelist, and critical behavioral rules). */
|
|
16
|
-
export declare const GENUI_SECTION_TEXT = "You can render interactive UI components INSIDE your reply \u2014 between paragraphs \u2014 by emitting a fenced block with the language tag `dsh-ui` containing a JSON spec:\n\n```dsh-ui\n{\"title\":\"\u53EF\u9009\u6807\u9898\",\"gap\":14,\"items\":[...]}\n```\n\nThe spec is a white-listed component tree rendered inline where the fence sits. Only these `type` values; the `genui` skill, when available, carries the full content\u2192component mapping and per-component field details:\n\n- \u5E03\u5C40: text \u00B7 row \u00B7 col \u00B7 grid \u00B7 card \u00B7 divider \u00B7 spacer\n- \u5C55\u793A: badge \u00B7 stat \u00B7 progress \u00B7 list \u00B7 table \u00B7 keyvalue \u00B7 avatar \u00B7 audio \u00B7 video \u00B7 timeline \u00B7 file-tree \u00B7 breadcrumb \u00B7 callout \u00B7 steps \u00B7 diff \u00B7 json \u00B7 code \u00B7 copy\n- \u56FE\u8868: chart {\"
|
|
16
|
+
export declare const GENUI_SECTION_TEXT = "You can render interactive UI components INSIDE your reply \u2014 between paragraphs \u2014 by emitting a fenced block with the language tag `dsh-ui` containing a JSON spec:\n\n```dsh-ui\n{\"title\":\"\u53EF\u9009\u6807\u9898\",\"gap\":14,\"items\":[...]}\n```\n\nThe spec is a white-listed component tree rendered inline where the fence sits. Only these `type` values; the `genui` skill, when available, carries the full content\u2192component mapping and per-component field details:\n\n- \u5E03\u5C40: text \u00B7 row \u00B7 col \u00B7 grid \u00B7 card \u00B7 divider \u00B7 spacer \u00B7 hero\uFF08\u5C01\u9762\u5757\uFF1A\u8D85\u5927\u6570\u5B57 + \u6807\u9898 + tone \u6E10\u53D8\u5E95\u8272\uFF0C\u4E00\u6761\u56DE\u7B54\u6700\u591A\u4E00\u4E2A\uFF09\n- \u5C55\u793A: badge \u00B7 stat \u00B7 progress \u00B7 list \u00B7 table \u00B7 keyvalue \u00B7 avatar \u00B7 audio \u00B7 video \u00B7 timeline \u00B7 file-tree \u00B7 breadcrumb \u00B7 callout \u00B7 steps \u00B7 diff \u00B7 json \u00B7 code \u00B7 copy\n- \u56FE\u8868: chart {\"kind\":\"bars|line|donut\",\"data\":[{\"label\":\"...\",\"value\":n}],\"series\":[{\"label\":\"...\",\"data\":[...]}]?,\"horizontal\":true?,\"stacked\":true?}\uFF08series\uFF1Abars \u5206\u7EC4/\u5806\u53E0 / line \u591A\u5E8F\u5217\uFF1Bhorizontal \u6A2A\u5411\u67F1\uFF09 \u00B7 echart (preset: bar/line/area/pie/scatter/radar/gauge/funnel/treemap/sankey/graph/heatmap/bigline\uFF0C\u6216 option \u76F4\u901A) \u00B7 plot (\u51FD\u6570\u56FE)\n- \u4EA4\u4E92: button \u00B7 input \u00B7 textarea \u00B7 select \u00B7 checkbox \u00B7 switch \u00B7 slider \u00B7 radio \u00B7 submit \u00B7 quiz \u00B7 link \u00B7 tabs \u00B7 accordion\n- \u9AD8\u7EA7: mermaid (flowchart/sequence/class/gantt/pie/er/state/journey) \u00B7 diagram (\u7F16\u8F91\u7EA7\u67B6\u6784/\u6D41\u7A0B\u56FE\uFF0C27 \u79CD kind) \u00B7 scene3d (3D WebGL)\n\n**\u9ED8\u8BA4\u5C31\u8BE5\u51FA UI**\uFF1A\u51FA\u73B0\u4E0B\u5217\u60C5\u51B5\u81F3\u5C11\u51FA\u4E00\u4E2A\u56F4\u680F\uFF1A\n- \u22653 \u6761\u5E76\u5217\u8981\u70B9 \u2192 `list`\uFF1B\u6570\u5B57\u5BF9\u6BD4 \u2192 `table`\uFF1B\u6307\u6807/\u8FDB\u5EA6/\u72B6\u6001 \u2192 `stat`/`progress`/`badge`\n- \u6B65\u9AA4/\u65F6\u95F4\u7EBF \u2192 `steps`/`timeline`/`mermaid`\uFF1B\u67B6\u6784/\u6D41\u7A0B \u2192 `diagram` \u6216 `mermaid`\uFF1B\u98CE\u9669/\u7ED3\u8BBA \u2192 `callout`\uFF1B\u4EE3\u7801/\u6539\u52A8 \u2192 `code`/`diff`/`json`\n- \u884C\u5185\u5BCC\u6587\u672C\uFF1A`text`/`list`/\u8868\u683C\u6587\u672C\u5217/`keyvalue`/`callout` \u91CC\u53EF\u5199 `code`\u3001**\u52A0\u7C97**\u3001==\u9AD8\u4EAE==\u3001[\u6587\u5B57](url)\uFF1A\u91CD\u70B9\u7559\u5728\u53E5\u4E2D\uFF0C\u4E0D\u5FC5\u4E3A\u4E00\u4E2A\u8BCD\u5355\u8D77\u7EC4\u4EF6\u3002\n- \u9ED8\u8BA4\u65E0\u5361 \u2260 \u5C11\u7528\u7EC4\u4EF6\uFF1A\u786C\u89E6\u53D1\u7167\u5E38\u51FA\u7EC4\u4EF6\uFF0C**\u7EC4\u4EF6\u591A\u4E0D\u662F\u95EE\u9898**\u2014\u2014\u5224\u636E\u662F\u6BCF\u4E2A\u7EC4\u4EF6\u627F\u8F7D\u4E0D\u540C\u4FE1\u606F\u3001\u6709\u7126\u70B9\u4E0E\u5C42\u6B21\u3001\u540C\u4E00\u6279\u6570\u636E\u4E0D\u91CD\u590D\u8868\u8FBE\u3002\u5361\u7247\u53EA\u7528\u4E8E\u5E76\u6392\u9879\u4E0E\u6570\u636E\u5BF9\u8C61\uFF1B\u5355\u6BB5\u6587\u5B57\u7528\u300C\u6807\u9898 + \u6B63\u6587 + \u95F4\u8DDD\u300D\u3002\n\n**\u53D1\u56DE\u7B54\u524D\u6700\u540E\u81EA\u68C0\u4E00\u6B21**\uFF1A\u8FD9\u6BB5\u5185\u5BB9\u91CC\u6709\u6CA1\u6709 \u22653 \u6761\u5E76\u5217\u8981\u70B9\u3001\u4EFB\u4F55\u5BF9\u6BD4\u3001\u4EFB\u4F55\u6570\u5B57/\u6307\u6807\u3001\u4EFB\u4F55\u6B65\u9AA4\u6216\u6D41\u7A0B\uFF1F\u6709\u5C31\u5148\u8F6C\u6210\u7EC4\u4EF6\u518D\u5F00\u53E3\u3002**\u72B6\u6001\u6C47\u62A5\u3001\u8FDB\u5EA6\u8BF4\u660E\u3001\u63D0\u4EA4\u4E0E\u6539\u52A8\u6E05\u5355\u540C\u6837\u7B97**\u2014\u2014\u4E0D\u8981\u56E0\u4E3A\u5B83\u662F\"\u8BF4\u660E\u6587\"\u5C31\u7528\u7EAF\u6587\u5B57\u5199\u3002\u8FD9\u4E00\u6761\u8E29\u8FC7\u7684\u5751\uFF1A\u8FDE\u7EED\u51E0\u6761\u6C47\u62A5\u5168\u9760\u6587\u5B57\uFF0C\u4E00\u6761\u56F4\u680F\u90FD\u6CA1\u53D1\u3002\n- \u8D8B\u52BF/\u5360\u6BD4 \u2192 `chart`\uFF08\u22648 \u70B9\uFF09\u6216 `echart`\uFF08\u591A\u5E8F\u5217/\u8981\u4EA4\u4E92\u65F6\uFF09\uFF1B\u914D\u8272\u9ED8\u8BA4\u8DDF\u968F\u4E3B\u9898\uFF0C\u53EA\u6709\u8BED\u4E49\u9700\u8981\u65F6\u624D\u7528 `palette` / `card.accent`\uFF1B\u6392\u7248\u7528 grid \u5B50\u8282\u70B9\u7684 `\"span\":2` \u8DE8\u5217\u505A\u5BBD\u7A84\u6DF7\u6392\uFF08bento\uFF09\uFF0C\u4E0D\u8981\u4E00\u5217\u65B9\u5757\u5806\u5230\u5E95\uFF1B\u6570\u636E\u591A\u65F6\u7ED9 `table`/`chart`/`list` \u914D\u4E00\u4E2A `input`(id) + `filter` \u7ED1\u5B9A\uFF0C\u8BFB\u8005\u80FD\u5C31\u5730\u7B5B\u9009\uFF0C\u4E0D\u7528\u518D\u95EE\u4E00\u904D\n\n**\u5B57\u6BB5\u901F\u67E5**\uFF08\u5B8C\u6574\u89C1 genui skill\uFF09\uFF1A`stat` `{\"label\",\"value\",\"delta\"?}` \u00B7 `table` `{\"columns\",\"rows\",\"types\"?,\"total\"?,\"details\"?,\"filter\"?,\"export\"?}` \u00B7 `callout` `{\"tone\",\"title\",\"content\"}` \u00B7 `progress` `{\"value\",\"variant\"?,\"target\"?}`\n\nRules:\n- JSON \u4E25\u683C: \u574F\u56F4\u680F\u964D\u7EA7\u4E3A\u4EE3\u7801\u5757\uFF1B\u22653 \u8282\u70B9\u6216\u542B table \u7684\u56F4\u680F\u53D1\u51FA\u524D\u8C03\u7528 validate_dsh_ui\uFF0C\u274C \u4FEE\u597D\u518D\u53D1\uFF08\u82E5\u9644\u300C\u5DF2\u81EA\u52A8\u4FEE\u590D\u300DJSON \u7167\u6284\u5373\u53EF\uFF09\u3002\n- \u89C4\u6A21: \u2264200 \u8282\u70B9\u3001\u5D4C\u5957\u22648 \u5C42\uFF08\u8D85\u51FA\u88AB\u622A\u65AD\uFF09\uFF1B\u4E00\u6761\u56DE\u7B54 3\u20138 \u4E2A\u7EC4\u4EF6\uFF0C\u4E00\u4E2A\u4E3B\u9898\u4E00\u4E2A\u4E3B\u7EC4\u4EF6\uFF1B3D mesh 1\u20135\uFF1Bplot \u7ED9\u5408\u7406 xMin/xMax\u3002\n- LOCAL-FIRST + actions: UI \u80FD\u81EA\u5DF1\u505A\u7684\u72B6\u6001\u53D8\u5316\uFF08\u5224\u5377\u3001\u5224\u9898\u3001\u91CD\u7F6E\u3001\u5C55\u5F00\u3001\u9009\u4E2D\uFF09\u5C31\u5730\u5B8C\u6210\uFF0C\u96F6\u5F80\u8FD4\uFF1Baction \u53EA\u7528\u4E8E\u5FC5\u987B\u6A21\u578B\u53C2\u4E0E\u7684\u4E8B\u3002\u4EA4\u4E92\u7EC4\u4EF6\u5E26 \"action\":\"name\"\uFF0C\u4EA4\u4E92\u4EE5 [genui-action] name + \u7EC4\u4EF6\u6570\u636E\u56DE\u4F20\uFF0C\u5C4A\u65F6\u91CD\u6E32\u67D3\u66F4\u65B0 UI\uFF1B\u65E0 action \u7684\u6309\u94AE\u7981\u7528\u3002\n- Durable state: \u4EA4\u4E92\u72B6\u6001\u6309\u300C\u4F1A\u8BDD+\u5185\u5BB9\u6307\u7EB9\u300D\u6301\u4E45\u5316\u2014\u2014\u5237\u65B0/\u91CD\u653E\u6062\u590D\uFF1B\u91CD\u6E32\u67D3\u76F8\u540C\u5185\u5BB9\u4FDD\u7559\uFF0C\u65B0\u5185\u5BB9\u91CD\u7F6E\u3002\n- \u5377\u5B50\u6A21\u5F0F: \u6BCF\u9898\u4E00\u4E2A radio\uFF08group+answer+explanation\uFF09+ \u4E00\u4E2A submit\uFF08groups \u5168\u5217\uFF09\uFF0C\u672C\u5730\u5224\u5206\u3002\n- Secrets ban: \u4E0D\u7D22\u53D6\u5BC6\u7801\u3001API Key\u3001Token\u3001\u6062\u590D\u7801\uFF1B\u9700\u8981\u65F6\u62D2\u7EDD\u5E76\u89E3\u91CA\u3002\n- Tool channel: render_ui \u5DE5\u5177\u628A\u540C\u4E00 spec \u6E32\u67D3\u4E3A\u5DE5\u5177\u884C\u5361\u7247\uFF08\u4EA4\u4ED8\u7269\u578B\u754C\u9762\u7528\uFF09\uFF1B\u56F4\u680F\u7528\u4E8E\u56DE\u7B54\u5185\u8054 UI\u3002\n- Panel: \"panel\":true \u53EA\u6E32\u67D3\u8FDB\u4F1A\u8BDD\u9762\u677F dock \u5E76\u539F\u5730\u66F4\u65B0\uFF1B\"append\":true \u8FFD\u52A0\u5408\u5E76\uFF08\u540C\u6807\u7B7E tabs \u8FFD\u52A0/\u65B0\u6807\u7B7E\u52A0\u5165/\u5C3E\u90E8\u8FFD\u52A0\uFF09\uFF1B\u4E0A\u9650 200 \u8282\u70B9/200 \u6B21\u8FFD\u52A0\uFF0C\u6EE1\u4E86\u53D1 replace \u91CD\u5EFA\u3002\u9762\u677F\u7EC4\u4EF6\u6765\u7684 [genui-action] \u53EA\u56DE\u4E00\u4E2A panel:true \u56F4\u680F + \u81F3\u591A\u4E00\u884C 10 \u5B57\u5185\u786E\u8BA4\uFF0C\u4E0D\u89E3\u91CA\u3001\u4E0D\u7528\u666E\u901A\u56F4\u680F\u3002";
|
|
17
17
|
/**
|
|
18
18
|
* Register the GenUI output-language section and the render_ui tool.
|
|
19
19
|
* @param ctx - cordis context.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@changfenhuang/dsh-genui",
|
|
3
3
|
"description": "GenUI for DeepSeek Harness: interactive UI components rendered inline in assistant replies via the ```dsh-ui fence — layout, charts, plots, forms, quizzes, mermaid, 3D scenes, and an action event loop back to the model. Ships the fence-teaching host plugin, the browser renderer (client half), and the genui skill.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.10.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/plugin/index.d.ts",
|
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
"lib/client.js",
|
|
27
27
|
"lib/assets/mermaid.js",
|
|
28
28
|
"lib/assets/three.js",
|
|
29
|
-
"lib/assets/echarts.js",
|
|
30
29
|
"lib/types/plugin/index.d.ts",
|
|
31
30
|
"lib/types/plugin/invariant.d.ts",
|
|
32
31
|
"lib/types/client/index.d.ts",
|
|
@@ -39,7 +38,9 @@
|
|
|
39
38
|
"lib",
|
|
40
39
|
"src",
|
|
41
40
|
"tsconfig.json",
|
|
42
|
-
"tsdown.config.ts"
|
|
41
|
+
"tsdown.config.ts",
|
|
42
|
+
"lib/assets/echarts-core.js",
|
|
43
|
+
"lib/assets/echarts-full.js"
|
|
43
44
|
],
|
|
44
45
|
"publishConfig": {
|
|
45
46
|
"access": "public"
|
|
@@ -13,18 +13,45 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { useEffect, useRef, useState } from 'react'
|
|
15
15
|
import css from './GenuiBlock.module.css'
|
|
16
|
-
import { createChart as lazyCreateChart, type EChartsInstance } from './echarts-lazy.ts'
|
|
16
|
+
import { CORE_PRESETS, createChart as lazyCreateChart, type EChartsInstance } from './echarts-lazy.ts'
|
|
17
17
|
import { CHART_COLORS } from './blocks/charts.tsx'
|
|
18
18
|
import type { GenuiEChart } from './spec.ts'
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
-
return
|
|
20
|
+
/** Which engine bundle this node needs (progressive disclosure). */
|
|
21
|
+
function neededEngine(node: GenuiEChart): 'core' | 'full' {
|
|
22
|
+
if (node.option !== undefined) return 'full'
|
|
23
|
+
return CORE_PRESETS.has(node.preset ?? 'bar') ? 'core' : 'full'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Categorical fallback palette. The host defines its `--dsw-static-*` tokens on
|
|
28
|
+
* `body`, not on `:root`, and ECharts renders to CANVAS (so a `var(--x)` string
|
|
29
|
+
* is meaningless to it — every colour must be resolved to a literal first).
|
|
30
|
+
* Reading only `document.documentElement` therefore returned '' for every
|
|
31
|
+
* series colour and collapsed multi-series charts to one accent hue; these
|
|
32
|
+
* fixed hues keep series distinguishable on any host.
|
|
33
|
+
*/
|
|
34
|
+
export const SERIES_FALLBACK = [
|
|
35
|
+
'#679efe', '#4ed17e', '#f5b83d', '#f2707a', '#8b7ff0', '#3fc7d4', '#b7c8fe', '#9aa3b2',
|
|
36
|
+
] as const
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Read a host theme token, resolved from the ELEMENT first (custom properties
|
|
40
|
+
* inherit, so any node inside `body` sees the host sheet) and falling back to
|
|
41
|
+
* body/root for detached renders.
|
|
42
|
+
*/
|
|
43
|
+
function readToken(name: string, fallback: string, el?: HTMLElement | null): string {
|
|
44
|
+
const hosts: Array<Element | null> = [el ?? null, typeof document === 'undefined' ? null : document.body, typeof document === 'undefined' ? null : document.documentElement]
|
|
45
|
+
for (const host of hosts) {
|
|
46
|
+
if (host === null) continue
|
|
47
|
+
const value = getComputedStyle(host).getPropertyValue(name).trim()
|
|
48
|
+
if (value !== '') return value
|
|
49
|
+
}
|
|
50
|
+
return fallback
|
|
24
51
|
}
|
|
25
52
|
|
|
26
53
|
/** Resolve the host accent and label colors for ECharts theming. */
|
|
27
|
-
function themeColors(): {
|
|
54
|
+
function themeColors(el?: HTMLElement | null): {
|
|
28
55
|
accent: string
|
|
29
56
|
labelPrimary: string
|
|
30
57
|
labelSecondary: string
|
|
@@ -33,19 +60,27 @@ function themeColors(): {
|
|
|
33
60
|
bgLayer1: string
|
|
34
61
|
} {
|
|
35
62
|
return {
|
|
36
|
-
accent: readToken('--dsw-alias-state-business-primary', '#4f8ef7'),
|
|
37
|
-
labelPrimary: readToken('--dsw-alias-label-primary', '#e6e6e6'),
|
|
38
|
-
labelSecondary: readToken('--dsw-alias-label-secondary', '#a0a0a0'),
|
|
39
|
-
labelTertiary: readToken('--dsw-alias-label-tertiary', '#6b6b6b'),
|
|
40
|
-
border: readToken('--dsw-alias-border-l1', 'rgba(255,255,255,0.12)'),
|
|
41
|
-
bgLayer1: readToken('--dsw-alias-bg-layer-1', '#1a1a1e'),
|
|
63
|
+
accent: readToken('--dsw-alias-state-business-primary', '#4f8ef7', el),
|
|
64
|
+
labelPrimary: readToken('--dsw-alias-label-primary', '#e6e6e6', el),
|
|
65
|
+
labelSecondary: readToken('--dsw-alias-label-secondary', '#a0a0a0', el),
|
|
66
|
+
labelTertiary: readToken('--dsw-alias-label-tertiary', '#6b6b6b', el),
|
|
67
|
+
border: readToken('--dsw-alias-border-l1', 'rgba(255,255,255,0.12)', el),
|
|
68
|
+
bgLayer1: readToken('--dsw-alias-bg-layer-1', '#1a1a1e', el),
|
|
42
69
|
}
|
|
43
70
|
}
|
|
44
71
|
|
|
45
|
-
/** Build a full ECharts option from a preset + the simple data/series shape.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
72
|
+
/** Build a full ECharts option from a preset + the simple data/series shape.
|
|
73
|
+
* `el` is the chart's own container: theme tokens are resolved against it so
|
|
74
|
+
* host colours are found wherever the host defines them. */
|
|
75
|
+
function presetOption(node: GenuiEChart, el?: HTMLElement | null): Record<string, unknown> {
|
|
76
|
+
const t = themeColors(el)
|
|
77
|
+
// Each palette slot carries its own fallback hue: if the host lacks the
|
|
78
|
+
// static tokens, series must still be distinguishable (the old code fell
|
|
79
|
+
// back to the accent for every slot, so every chart came out one colour).
|
|
80
|
+
const colors = node.palette !== undefined && node.palette.length > 0
|
|
81
|
+
? [...node.palette]
|
|
82
|
+
: CHART_COLORS.map((c, i) =>
|
|
83
|
+
readToken(c.replace('var(', '').replace(')', ''), SERIES_FALLBACK[i % SERIES_FALLBACK.length]!, el))
|
|
49
84
|
const data = node.data ?? []
|
|
50
85
|
const series = node.series
|
|
51
86
|
|
|
@@ -134,6 +169,185 @@ function presetOption(node: GenuiEChart): Record<string, unknown> {
|
|
|
134
169
|
legend: series !== undefined ? { bottom: 0, textStyle: { color: t.labelTertiary } } : undefined,
|
|
135
170
|
}
|
|
136
171
|
}
|
|
172
|
+
case 'radar': {
|
|
173
|
+
// Indicators come from the first series' labels; each series is one
|
|
174
|
+
// polygon. `data` alone is treated as a single unnamed series.
|
|
175
|
+
const entries = series ?? [{ label: '', data }]
|
|
176
|
+
const indicators = (entries[0]?.data ?? []).map(d => ({ name: d.label, max: undefined as number | undefined }))
|
|
177
|
+
const max = Math.max(...entries.flatMap(e => e.data.map(d => Number(d.value) || 0)), 1)
|
|
178
|
+
return {
|
|
179
|
+
...base,
|
|
180
|
+
tooltip: tt({ trigger: 'item' }),
|
|
181
|
+
legend: { bottom: 0, textStyle: { color: t.labelTertiary } },
|
|
182
|
+
radar: {
|
|
183
|
+
indicator: indicators.map(i => ({ name: i.name, max: Math.ceil(max * 1.1) })),
|
|
184
|
+
splitLine: { lineStyle: { color: t.border } },
|
|
185
|
+
splitArea: { show: false },
|
|
186
|
+
axisLine: { lineStyle: { color: t.border } },
|
|
187
|
+
axisName: { color: t.labelTertiary },
|
|
188
|
+
},
|
|
189
|
+
series: [{
|
|
190
|
+
type: 'radar',
|
|
191
|
+
symbolSize: 5,
|
|
192
|
+
areaStyle: { opacity: 0.12 },
|
|
193
|
+
data: entries.map((e, i) => ({
|
|
194
|
+
name: e.label,
|
|
195
|
+
value: e.data.map(d => Number(d.value) || 0),
|
|
196
|
+
...optItemStyleColor(e.color, i, series),
|
|
197
|
+
})),
|
|
198
|
+
}],
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
case 'gauge': {
|
|
202
|
+
// One gauge per datum (usually a single KPI).
|
|
203
|
+
return {
|
|
204
|
+
...base,
|
|
205
|
+
tooltip: tt({ trigger: 'item' }),
|
|
206
|
+
series: data.slice(0, 4).map((d, i) => ({
|
|
207
|
+
type: 'gauge',
|
|
208
|
+
startAngle: 210,
|
|
209
|
+
endAngle: -30,
|
|
210
|
+
min: 0,
|
|
211
|
+
max: Math.max(Number(d.value) || 0, 100),
|
|
212
|
+
center: data.length > 1 ? [`${(i + 0.5) * (100 / Math.min(data.length, 4))}%`, '58%'] : ['50%', '58%'],
|
|
213
|
+
radius: data.length > 1 ? '62%' : '82%',
|
|
214
|
+
progress: { show: true, width: 12 },
|
|
215
|
+
axisLine: { lineStyle: { width: 12, color: [[1, t.border]] } },
|
|
216
|
+
axisTick: { show: false },
|
|
217
|
+
splitLine: { show: false },
|
|
218
|
+
axisLabel: { show: false },
|
|
219
|
+
pointer: { show: false },
|
|
220
|
+
title: { offsetCenter: [0, '32%'], color: t.labelTertiary, fontSize: 12 },
|
|
221
|
+
detail: { valueAnimation: true, fontSize: 26, offsetCenter: [0, '2%'], formatter: '{value}', color: t.labelPrimary },
|
|
222
|
+
data: [{ value: Number(d.value) || 0, name: d.label }],
|
|
223
|
+
})),
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
case 'funnel': {
|
|
227
|
+
return {
|
|
228
|
+
...base,
|
|
229
|
+
tooltip: tt({ trigger: 'item', formatter: '{b}: {c}' }),
|
|
230
|
+
legend: { bottom: 0, textStyle: { color: t.labelTertiary } },
|
|
231
|
+
series: [{
|
|
232
|
+
type: 'funnel',
|
|
233
|
+
left: '8%',
|
|
234
|
+
width: '84%',
|
|
235
|
+
top: 16,
|
|
236
|
+
bottom: 40,
|
|
237
|
+
gap: 2,
|
|
238
|
+
label: { position: 'inside', color: '#fff', formatter: '{b} {c}' },
|
|
239
|
+
itemStyle: { borderWidth: 0 },
|
|
240
|
+
data: data.map(d => ({ name: d.label, value: Number(d.value) || 0 })),
|
|
241
|
+
}],
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
case 'treemap': {
|
|
245
|
+
return {
|
|
246
|
+
...base,
|
|
247
|
+
tooltip: tt({ trigger: 'item', formatter: '{b}: {c}' }),
|
|
248
|
+
series: [{
|
|
249
|
+
type: 'treemap',
|
|
250
|
+
roam: false,
|
|
251
|
+
nodeClick: false,
|
|
252
|
+
breadcrumb: { show: false },
|
|
253
|
+
upperLabel: { show: false },
|
|
254
|
+
label: { color: t.labelPrimary },
|
|
255
|
+
itemStyle: { borderColor: t.bgLayer1, borderWidth: 2, gapWidth: 2 },
|
|
256
|
+
data: data.map(d => ({ name: d.label, value: Number(d.value) || 0 })),
|
|
257
|
+
}],
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
case 'sankey': {
|
|
261
|
+
const links = node.links ?? []
|
|
262
|
+
const names = data.length > 0
|
|
263
|
+
? data.map(d => d.label)
|
|
264
|
+
: [...new Set(links.flatMap(l => [l.from, l.to]))]
|
|
265
|
+
return {
|
|
266
|
+
...base,
|
|
267
|
+
tooltip: tt({ trigger: 'item' }),
|
|
268
|
+
series: [{
|
|
269
|
+
type: 'sankey',
|
|
270
|
+
left: 8,
|
|
271
|
+
right: 8,
|
|
272
|
+
top: 12,
|
|
273
|
+
bottom: 12,
|
|
274
|
+
nodeGap: 12,
|
|
275
|
+
lineStyle: { color: 'gradient', curveness: 0.5, opacity: 0.32 },
|
|
276
|
+
label: { color: t.labelSecondary },
|
|
277
|
+
emphasis: { focus: 'adjacency' },
|
|
278
|
+
data: names.map(name => ({ name })),
|
|
279
|
+
links: links.map(l => ({ source: l.from, target: l.to, value: l.value ?? 1 })),
|
|
280
|
+
}],
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
case 'graph': {
|
|
284
|
+
const links = node.links ?? []
|
|
285
|
+
const names = data.length > 0
|
|
286
|
+
? data.map(d => d.label)
|
|
287
|
+
: [...new Set(links.flatMap(l => [l.from, l.to]))]
|
|
288
|
+
// Node size follows its degree, so hubs read as hubs without extra data.
|
|
289
|
+
const degree = new Map<string, number>()
|
|
290
|
+
for (const l of links) {
|
|
291
|
+
degree.set(l.from, (degree.get(l.from) ?? 0) + 1)
|
|
292
|
+
degree.set(l.to, (degree.get(l.to) ?? 0) + 1)
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
...base,
|
|
296
|
+
tooltip: tt({ trigger: 'item' }),
|
|
297
|
+
series: [{
|
|
298
|
+
type: 'graph',
|
|
299
|
+
layout: 'force',
|
|
300
|
+
roam: true,
|
|
301
|
+
label: { show: true, color: t.labelSecondary, fontSize: 11 },
|
|
302
|
+
force: { repulsion: 200, edgeLength: 80 },
|
|
303
|
+
lineStyle: { color: t.border, curveness: 0.1 },
|
|
304
|
+
emphasis: { focus: 'adjacency' },
|
|
305
|
+
data: names.map(name => ({ name, symbolSize: 16 + (degree.get(name) ?? 0) * 5 })),
|
|
306
|
+
links: links.map(l => ({ source: l.from, target: l.to })),
|
|
307
|
+
}],
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
case 'heatmap': {
|
|
311
|
+
// Rows come from `series` (one row per entry), columns from the first
|
|
312
|
+
// row's labels — the same shape the other presets use.
|
|
313
|
+
const rows = series ?? [{ label: '', data }]
|
|
314
|
+
const cols = (rows[0]?.data ?? []).map(d => d.label)
|
|
315
|
+
const values = rows.flatMap((row, y) => row.data.map((d, x) => [x, y, Number(d.value) || 0]))
|
|
316
|
+
const max = Math.max(...values.map(v => v[2] as number), 1)
|
|
317
|
+
return {
|
|
318
|
+
...base,
|
|
319
|
+
tooltip: tt({ trigger: 'item', position: 'top' }),
|
|
320
|
+
grid: { left: 64, right: 16, top: 16, bottom: 48 },
|
|
321
|
+
xAxis: { type: 'category', data: cols, splitArea: { show: false }, axisLabel: { color: t.labelTertiary }, axisLine: { lineStyle: { color: t.border } } },
|
|
322
|
+
yAxis: { type: 'category', data: rows.map(r => r.label), splitArea: { show: false }, axisLabel: { color: t.labelTertiary }, axisLine: { lineStyle: { color: t.border } } },
|
|
323
|
+
visualMap: { min: 0, max, calculable: true, orient: 'horizontal', left: 'center', bottom: 0, textStyle: { color: t.labelTertiary } },
|
|
324
|
+
series: [{ type: 'heatmap', data: values, label: { show: values.length <= 60, color: t.labelPrimary }, itemStyle: { borderColor: t.bgLayer1, borderWidth: 2 } }],
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
case 'bigline': {
|
|
328
|
+
// Long series: symbols off, area wash, inside + slider zoom.
|
|
329
|
+
return {
|
|
330
|
+
...base,
|
|
331
|
+
tooltip: tt({ trigger: 'axis' }),
|
|
332
|
+
grid: { left: 48, right: 20, top: 24, bottom: 56 },
|
|
333
|
+
dataZoom: [
|
|
334
|
+
{ type: 'inside', start: 0, end: 45 },
|
|
335
|
+
{ type: 'slider', height: 16, bottom: 8, borderColor: t.border, textStyle: { color: t.labelTertiary } },
|
|
336
|
+
],
|
|
337
|
+
xAxis: { type: 'category', boundaryGap: false, data: data.map(d => d.label), axisLabel: { color: t.labelTertiary }, axisLine: { lineStyle: { color: t.border } } },
|
|
338
|
+
yAxis: { type: 'value', axisLabel: { color: t.labelTertiary }, splitLine: { lineStyle: { color: t.border, opacity: 0.5 } } },
|
|
339
|
+
series: (series ?? [{ label: '', data }]).map((s2, i) => ({
|
|
340
|
+
name: s2.label,
|
|
341
|
+
type: 'line',
|
|
342
|
+
smooth: true,
|
|
343
|
+
showSymbol: false,
|
|
344
|
+
areaStyle: { opacity: 0.12 },
|
|
345
|
+
data: s2.data.map(d => d.value),
|
|
346
|
+
...optItemStyleColor(s2.color, i, series),
|
|
347
|
+
})),
|
|
348
|
+
legend: series !== undefined ? { bottom: 26, textStyle: { color: t.labelTertiary } } : undefined,
|
|
349
|
+
}
|
|
350
|
+
}
|
|
137
351
|
default: {
|
|
138
352
|
// 'bar' or unspecified
|
|
139
353
|
return {
|
|
@@ -171,9 +385,9 @@ export function EChartNode({ node }: { node: GenuiEChart }) {
|
|
|
171
385
|
if (el === null) return
|
|
172
386
|
|
|
173
387
|
// Full `option` wins over preset shorthand.
|
|
174
|
-
const option = node.option ?? presetOption(node)
|
|
388
|
+
const option = node.option ?? presetOption(node, el)
|
|
175
389
|
|
|
176
|
-
void lazyCreateChart(el, option, { height: node.height ?? 300 }).then((inst) => {
|
|
390
|
+
void lazyCreateChart(el, option, { height: node.height ?? 300 }, neededEngine(node)).then((inst) => {
|
|
177
391
|
if (!alive) {
|
|
178
392
|
inst.dispose()
|
|
179
393
|
return
|
|
@@ -212,7 +426,7 @@ export function EChartNode({ node }: { node: GenuiEChart }) {
|
|
|
212
426
|
// returned early when status was 'loading' and never re-run).
|
|
213
427
|
useEffect(() => {
|
|
214
428
|
if (status !== 'ready' || instanceRef.current === null) return
|
|
215
|
-
const option = node.option ?? presetOption(node)
|
|
429
|
+
const option = node.option ?? presetOption(node, ref.current)
|
|
216
430
|
instanceRef.current.setOption(option, true)
|
|
217
431
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
218
432
|
}, [node, status])
|