Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ dsh plugin --profile web add link:$PWD

- **Answer-as-UI**: components are embedded in the reply and appear as they stream — no waiting for the whole message
- **30+ components**: cards, tables, charts, forms, tabs, accordions, file trees, timelines, diffs…
- **ECharts integration**: the `echart` node renders full ECharts charts with theme-aware colors, tooltips, and legends. Two modes: **preset shorthand** (`preset: 'bar' | 'line' | 'area' | 'pie' | 'scatter'` + `data`/`series`) for quick upgrade from the `chart` node, or **full option** (`option` field) for custom chart types, dataZoom, visualMap, and other advanced ECharts features. The echarts engine (~1 MB) is lazy-loaded on demand — the main bundle never carries it, and conversations without `echart` nodes never download it
- **Function plots**: `plot` draws curves; parameter sliders redraw in real time, with optional auto-animation

<p align="center">
Expand Down Expand Up @@ -121,19 +122,33 @@ The model outputs this fence (written for the browser — you don't need to read

What you see: two stat cards.

### ECharts example

```dsh-ui
{"title":"Q1 Revenue","items":[
{"type":"echart","title":"Monthly Revenue","preset":"bar","data":[
{"label":"Jan","value":98},
{"label":"Feb","value":112},
{"label":"Mar","value":128}
]}
]}
```

What you see: a themed bar chart with tooltips and axis labels — rendered by ECharts, lazy-loaded on demand.

## 🔧 How it works

The model writes the interface description as JSON inside a `dsh-ui` fence; the browser-side renderer (`src/client`) claims this language through the main repo's `fence-registry` interface and renders it. Components are whitelisted — the model can't smuggle in HTML/scripts; function expressions go through a standalone parser, never `eval`.

The core render package stays light (≈110 KB min / 28 KB gzip); the mermaid and three.js engines are bundled separately as on-demand assets (loaded through the plugin's self-registered HTTP routes the first time they're used), so startup only downloads the rendering core.
The core render package stays light (≈110 KB min / 28 KB gzip); the mermaid, three.js, and echarts engines are bundled separately as on-demand assets (loaded through the plugin's self-registered HTTP routes the first time they're used), so startup only downloads the rendering core.

## ❓ FAQ

- **Rendering as a code block?** Check three things: your dsh build has fence-registry (see "dual-channel rendering" at the top — builds without the extension point fall back to the DOM channel), `dsh plugin --profile web list` shows this plugin, restart + hard refresh.
- **Chat UI goes blank when rendering a dsh-ui fence?** Your dsh is too old — update dsh first, then reinstall the plugin.
- **`dsh: pnpm not found on PATH`?** Install pnpm, then **open a new terminal** and retry (`corepack enable` or `npm i -g pnpm`).
- **Stuck on git credentials / 404 during install?** The repo is public (`omdsh-dev/dsh-genui`) — the git URL above needs no login; a 404 for `@omdsh-dev/dsh-genui` means the npm package has not been published yet.
- **Installed but scene3d/mermaid don't render?** The engines (mermaid / three) are no longer inlined in client.js — they load on demand the first time they're used (`/plugins/@omdsh-dev/dsh-genui/assets/*.js`, hosted by the plugin's own HTTP routes). First restart dsh web + hard refresh (Cmd+Shift+R); still broken, remove and reinstall (`dsh plugin --profile web remove @omdsh-dev/dsh-genui`, then add again). Hosts without the asset routes degrade to source/load-error hints — update dsh.
- **Installed but scene3d/mermaid/echarts don't render?** The engines (mermaid / three / echarts) are no longer inlined in client.js — they load on demand the first time they're used (`/plugins/@omdsh-dev/dsh-genui/assets/*.js`, hosted by the plugin's own HTTP routes). First restart dsh web + hard refresh (Cmd+Shift+R); still broken, remove and reinstall (`dsh plugin --profile web remove @omdsh-dev/dsh-genui`, then add again). Hosts without the asset routes degrade to source/load-error hints — update dsh.
- **Model not outputting fences?** New sessions pick it up after a restart; or just say "output it with dsh-ui".
- **No lib/ after cloning?** Build it yourself: `pnpm install && pnpm run check`.

Expand Down
19 changes: 17 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ dsh plugin --profile web add link:$PWD

- **回答即界面**:组件嵌在回答里,边生成边出现,不用等整段写完
- **30+ 组件**:卡片、表格、图表、表单、标签页、折叠面板、文件树、时间线、diff……
- **ECharts 集成**:`echart` 节点渲染完整的 ECharts 图表,自动适配主题色、提示框和图例。两种模式:**预设简写**(`preset: 'bar' | 'line' | 'area' | 'pie' | 'scatter'` + `data`/`series`)可从 `chart` 节点快速升级;**完整选项**(`option` 字段)支持自定义图表类型、dataZoom、visualMap 等高级 ECharts 功能。echarts 引擎(~1 MB)按需懒加载——主包不含引擎,没有 `echart` 节点的对话不会下载它
- **函数图**:`plot` 画曲线,参数滑块拖动实时重绘,支持自动动画

<p align="center">
Expand Down Expand Up @@ -121,19 +122,33 @@ dsh plugin --profile web add link:$PWD

你看到的是两张统计卡片。

### ECharts 示例

```dsh-ui
{"title":"Q1 收入","items":[
{"type":"echart","title":"月度收入","preset":"bar","data":[
{"label":"1月","value":98},
{"label":"2月","value":112},
{"label":"3月","value":128}
]}
]}
```

你看到的是一张带提示框和坐标轴的主题色柱状图——由 ECharts 渲染,按需懒加载。

## 🔧 原理

模型把界面描述写成 JSON 放进 `dsh-ui` 围栏,浏览器端渲染器(`src/client`)通过主仓 `fence-registry` 接口认领这门语言并渲染。组件是白名单的,模型塞不进 HTML/脚本;函数表达式走独立解析器,不用 eval。

主渲染包保持轻量(≈110 KB min / 28 KB gzip),mermaidthree.js 引擎单独打包为按需资产(首次用到时经插件自注册的 HTTP 路由加载),启动时只下载渲染核心。
主渲染包保持轻量(≈110 KB min / 28 KB gzip),mermaidthree.js 与 echarts 引擎单独打包为按需资产(首次用到时经插件自注册的 HTTP 路由加载),启动时只下载渲染核心。

## ❓ 常见问题

- **显示成代码块?** 查三处:dsh 版本带 fence-registry(见顶部「双通道渲染」,无扩展点的构建走 DOM 通道兜底)、`dsh plugin --profile web list` 里有本插件、重启 + 硬刷新。
- **渲染 dsh-ui fence 时聊天界面白屏?** dsh 版本太旧——先更新 dsh 再重装插件。
- **`dsh: pnpm not found on PATH`?** 装 pnpm 后**新开终端**再试(`corepack enable` 或 `npm i -g pnpm`)。
- **安装时卡在 git 凭据/404?** 仓库是公开的(`omdsh-dev/dsh-genui`),上面的 git URL 无需登录;`@omdsh-dev/dsh-genui` 返回 404,表示 npm 包尚未发布。
- **装了但 scene3d/mermaid 不渲染?** 引擎(mermaid / three)不再内联进 client.js——它们在首次用到时按需加载(`/plugins/@omdsh-dev/dsh-genui/assets/*.js`,插件自带 HTTP 路由托管)。先重启 dsh web + 硬刷新(Cmd+Shift+R);仍不渲染就卸掉重装(`dsh plugin --profile web remove @omdsh-dev/dsh-genui` 后再 add)。旧版宿主缺少资产路由时会降级显示源码/加载失败提示,更新 dsh 即可。
- **装了但 scene3d/mermaid/echarts 不渲染?** 引擎(mermaid / three / echarts)不再内联进 client.js——它们在首次用到时按需加载(`/plugins/@omdsh-dev/dsh-genui/assets/*.js`,插件自带 HTTP 路由托管)。先重启 dsh web + 硬刷新(Cmd+Shift+R);仍不渲染就卸掉重装(`dsh plugin --profile web remove @omdsh-dev/dsh-genui` 后再 add)。旧版宿主缺少资产路由时会降级显示源码/加载失败提示,更新 dsh 即可。
- **模型不主动输出?** 重启后新会话生效;或直接说"用 dsh-ui 输出"。
- **clone 后没有 lib/?** `pnpm install && pnpm run check` 自己构建。

Expand Down
5 changes: 3 additions & 2 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ description: "Render structured interactive UI inline in your reply via the dsh-

布局:`text` `row` `col` `grid` `card` `divider` `spacer`
展示:`stat` `badge` `progress` `list` `table` `keyvalue` `avatar` `timeline` `file-tree` `breadcrumb` `diff` `json` `code` `callout` `steps`
图表:`chart`(bars/line/donut,可多序列)`plot`(数学函数图)
图表:`chart`(bars/line/donut,可多序列)`plot`(数学函数图)`echart`(ECharts 全功能图表)
交互:`button` `input` `select` `checkbox` `radio` `switch` `textarea` `tabs` `accordion` `copy`
高级:`mermaid`(流程图/时序/甘特等)`scene3d`(3D WebGL)`quiz`(点选判题 + 解析 + 重试)

Expand Down Expand Up @@ -45,6 +45,7 @@ description: "Render structured interactive UI inline in your reply via the dsh-
### 图表
- chart: `{"type":"chart","kind":"bars|line|donut","data":[{"label":"...","value":n,"color":"#hex?"}],"series":[...]?}` — bars 默认;line 趋势;donut 占比;series 字段 = 分组柱状图;负值数据:柱高为 0 但数值标注照显、donut 负值记 0 弧长(line 正常画负区间)
- plot: `{"type":"plot","series":[{"expr":"a*sin(b*x)","label":"...","color":"#hex?","params":[{"name":"a","value":1,"min":0,"max":5,"animateTo":3,"durationMs":4000,"loop":true},{"name":"b","value":1,"min":0.5,"max":5}]}],"xMin":-6.28,"xMax":6.28,"title":"..."}` — SVG 函数图;**series 可带 `"kind":"line|area|scatter"`**(缺省 line;area 填色到基线;scatter 散点);**params 渲染成实时滑块**(拖动即时重绘,**y 轴锁定**=只变曲线不变数轴);**animateTo 参数会显示播放按钮**(自动动画演示);SVG 可拖拽平移、滚轮缩放;表达式支持 sin/cos/tan/asin/acos/atan/sqrt/cbrt/exp/log/ln/abs/floor/ceil/round/min/max/pow,常量 pi/e/tau,变量 x(其他字母=参数)
- echart: `{"type":"echart","title":"...","height":300,"preset":"bar|line|area|pie|scatter","data":[{"label":"...","value":n}],"series":[...]?}` — **ECharts 全功能图表**,视觉效果远超 `chart`(渐变、tooltip、动画、图例交互);**preset 模式**:用和 `chart` 一样的 `data`/`series` 格式,自动构建主题化的 ECharts 配置(颜色跟随宿主主题);**full option 模式**:传 `"option":{...}` 直接写 ECharts 原生配置(支持 dataZoom/visualMap/radar/gauge/heatmap 等所有图表类型),option 中的函数会被过滤(只接受数据);推荐用 echart 替代 chart 获得更好视觉效果

### 交互
**本地优先(v2.6)**:UI 自己能做的状态变化——判卷、判题、重置、展开、选中——一律本地即时完成,**零模型往返**。action 只用于必须模型参与的事(生成新内容、执行工具、下一步建议)。**交互组件必须带 action:不带 action 的按钮渲染为禁用态,用户点不了;带 action 的按钮点击后有「已触发」本地反馈。**
Expand Down Expand Up @@ -80,7 +81,7 @@ description: "Render structured interactive UI inline in your reply via the dsh-
|---|---|
| 关键结论 / 要点罗列(≥2 条) | `list`、`keyvalue`、`callout` |
| 重点强调 / 警告 / 注意事项 | `callout`(info/success/warning/error)、`badge`、`stat` |
| 数据对比 / 趋势 / 占比 | `chart`(bars/line/donut)、`table` |
| 数据对比 / 趋势 / 占比 | `chart`(bars/line/donut)、`echart`(ECharts 全功能)、`table` |
| 关键指标数字 / 进度状态 | `stat`、`progress`、`badge` |
| 流程 / 步骤 / 阶段 / 时间线 | `steps`、`timeline`、`mermaid`(flowchart/sequence/gantt) |
| 目录 / 文件结构 / 层级关系 | `file-tree`、`mermaid`、`accordion` |
Expand Down
64 changes: 64 additions & 0 deletions lib/assets/echarts.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions lib/client.js

Large diffs are not rendered by default.

64 changes: 62 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ const GENUI_LIMITS = {
maxBreadcrumbItems: 12,
maxKeyValuePairs: 24,
/** Maximum `file-tree` nesting. */
maxTreeDepth: 6
maxTreeDepth: 6,
/** Maximum depth of an `echart` option object (prevents pathological nested
* ECharts configs from stalling the guard walk). */
maxEChartOptionDepth: 10
};
/** Is `v` one of `values`? (enum guard) */
function inEnum(v, values) {
Expand Down Expand Up @@ -151,6 +154,13 @@ const MESH_SHAPES = [
"torus"
];
const FILE_TYPES = ["file", "dir"];
const ECHART_PRESETS = [
"bar",
"line",
"area",
"pie",
"scatter"
];
/** Walk `list` with the shared node budget; drops invalid entries. */
function repairItems(list, ctx, depth) {
if (!Array.isArray(list)) return [];
Expand Down Expand Up @@ -532,6 +542,22 @@ function repairNode(value, ctx, depth) {
...opt("action", str(v.action, 200))
};
}
case "echart": {
const data = v.data !== void 0 ? repairChartData(v.data, GENUI_LIMITS.maxChartPoints) : void 0;
const series = v.series !== void 0 && Array.isArray(v.series) ? repairSeries(v.series, GENUI_LIMITS.maxPlotSeries, GENUI_LIMITS.maxChartPoints) : void 0;
const sanitized = v.option !== void 0 ? sanitizeEChartOption(v.option, 0) : void 0;
const option = sanitized === void 0 || typeof sanitized !== "object" || sanitized === null || Array.isArray(sanitized) ? void 0 : sanitized;
if (option === void 0 && data === void 0 && series === void 0) return null;
return {
type: "echart",
...opt("title", str(v.title, GENUI_LIMITS.maxString)),
...opt("height", int(v.height, 100, 800)),
...opt("preset", enu(v.preset, ECHART_PRESETS)),
...opt("data", data),
...opt("series", series),
...opt("option", option)
};
}
default: return value;
}
}
Expand Down Expand Up @@ -812,6 +838,39 @@ function repairQuizOptions(v) {
return out;
}
/**
* Sanitize an ECharts option object: depth-bounded pass-through that strips
* dangerous values (functions, `url()` in styles) but preserves the object
* shape ECharts needs. Scalars are KEPT: ECharts options are full of them,
* including inside `data` arrays (`data: [120, 150, 180]`,
* `xAxis.data: ['1月', '2月']`). Previously a scalar hit the plain-object
* gate below and returned undefined, so every primitive-valued array was
* filtered to empty and dropped — a chart with a full `option` rendered
* with empty series (blank canvas). This is a safety walk, not an ECharts
* semantic validator.
*/
function sanitizeEChartOption(v, depth) {
if (depth > GENUI_LIMITS.maxEChartOptionDepth) return void 0;
if (typeof v === "string") {
const s = v.slice(0, GENUI_LIMITS.maxString);
return s.toLowerCase().includes("url(") ? void 0 : s;
}
if (typeof v === "number" && Number.isFinite(v)) return v;
if (typeof v === "boolean") return v;
if (v === null) return null;
if (Array.isArray(v)) {
const arr = v.map((item) => sanitizeEChartOption(item, depth + 1)).filter((item) => item !== void 0);
return arr.length > 0 ? arr : void 0;
}
const o = obj(v);
if (o === void 0) return void 0;
const out = {};
for (const [key, val] of Object.entries(o)) {
const s = sanitizeEChartOption(val, depth + 1);
if (s !== void 0) out[key] = s;
}
return Object.keys(out).length > 0 ? out : void 0;
}
/**
* Deterministically repair a raw spec value into a renderable GenuiSpec.
* Returns null only when the root is not an object with an `items` array;
* every other defect is healed by dropping/clamping/truncating. Idempotent:
Expand Down Expand Up @@ -1348,6 +1407,7 @@ The spec is a white-listed component tree rendered inline where the fence sits.
- list: {"type":"list","items":["..."] or [{"title":"...","desc":"..."}]}
- table: {"type":"table","columns":["..."],"rows":[["...","..."]]} — 表头点击本地排序(升/降/还原,数值感知)
- chart: {"type":"chart","kind":"bars|line|donut","data":[{"label":"...","value":n,"color":"#hex?"}],"series":[...]?} — bars 默认;line 趋势;donut 占比;series=分组柱;负值柱高为 0 但标注照显;hover 显示精确值
- echart: {"type":"echart","preset":"bar|line|area|pie|scatter","title":"..."?,"height":n?,"data":[{"label":"...","value:n}]?,"series":[...]?} — ECharts 全功能图表(渐变/tooltip/动画/图例交互),视觉效果远超 chart;preset 模式用和 chart 一样的 data/series 格式自动构建主题化配置;也可传 "option":{...} 写原生 ECharts 配置(支持 radar/gauge/heatmap/dataZoom 等,option 中的函数会被过滤);推荐用 echart 替代 chart
- tabs: {"type":"tabs","tabs":[{"label":"...","items":[...]}]} / accordion: {"type":"accordion","items":[{"title":"...","items":[...]}]}
- avatar: {"type":"avatar","name":"..."}
- plot: {"type":"plot","series":[{"expr":"sin(x)","label":"...","kind":"line|area|scatter"?,"params":[...]?}],"xMin":-5,"xMax":5,"yMin":?,"yMax":?,"title":"..."} — SVG 函数图(可拖拽平移/滚轮缩放;params 渲染实时滑块);kind 缺省 line,area 填到基线,scatter 散点;表达式白名单 sin/cos/tan/asin/acos/atan/sqrt/cbrt/exp/log/ln/abs/floor/ceil/round/min/max/pow,常量 pi/e/tau,变量 x
Expand All @@ -1364,7 +1424,7 @@ The spec is a white-listed component tree rendered inline where the fence sits.
Rules:
- Trigger: 结构化表达优于纯文本时就主动用围栏(要点、强调、对比、流程、步骤、状态、数据、演示),纯问答与一句话不套 UI。
- 围栏放在回答中该组件该在的位置,文字前后照常流动;不要把围栏套进别的代码围栏,JSON 字符串内不放 markdown。
- Component choice (每个主题一个主组件): 结论/提醒→callout · 2–4 指标→grid+stat · 进度→progress · 多阶段→steps · 要点→list · 配置→keyvalue · 对比→table · 趋势→chart(line) · 占比→chart(donut) · 分类对比→chart(bars) · 数学曲线→plot · 事件→timeline · 分页内容→tabs · 长内容→accordion · 树→file-tree · 代码→code · 文件变更→diff · 嵌套JSON→json · 架构/流程→mermaid · 仅几何内容→scene3d · 教学→quiz · 单操作→button(action)。优先 table/chart 而非文字堆砌;同一数据不重复出现在两个组件;每次回复 3–8 个组件,拿不准就少。
- Component choice (每个主题一个主组件): 结论/提醒→callout · 2–4 指标→grid+stat · 进度→progress · 多阶段→steps · 要点→list · 配置→keyvalue · 对比→table · 趋势→echart(line) 或 chart(line) · 占比→echart(pie) 或 chart(donut) · 分类对比→echart(bar) 或 chart(bars) · 数学曲线→plot · 事件→timeline · 分页内容→tabs · 长内容→accordion · 树→file-tree · 代码→code · 文件变更→diff · 嵌套JSON→json · 架构/流程→mermaid · 仅几何内容→scene3d · 教学→quiz · 单操作→button(action)。优先 echart/table 而非文字堆砌;同一数据不重复出现在两个组件;每次回复 3–8 个组件,拿不准就少。
- 语法: 坏围栏降级为代码块,保持 JSON 严格。≥3 节点或含 table 的围栏发出前调用 validate_dsh_ui 验证,❌ 则修好再发;若 ❌ 回复里附了「已自动修复」的 JSON,照抄即可。
- 主题: 内容适配暗色;UI 主题跟随 app,不要自造。规模: ≤200 节点、嵌套≤8 层(超出被截断);3D 网格 1–5 个;plot 给合理 xMin/xMax。
- v2 actions: button/input/select/checkbox/radio/switch/slider/textarea/quiz 可带 "action":"name",交互以 [genui-action] name + 组件数据回传,届时重渲染更新 UI。可交互组件必须带 action(无 action 按钮禁用);带 action 的按钮点击有「已触发」本地反馈。
Expand Down
4 changes: 4 additions & 0 deletions lib/types/client/EChartNode.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { GenuiEChart } from './spec.ts';
export declare function EChartNode({ node }: {
node: GenuiEChart;
}): import("react").JSX.Element;
Loading