@changfenhuang/dsh-genui 0.10.0 → 0.11.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +9 -1
  4. package/SKILL.md +4 -0
  5. package/lib/client.js +261 -5
  6. package/lib/index.js +14 -12
  7. package/lib/types/client/asset-loader.d.ts +2 -0
  8. package/lib/types/client/blocks/basic.d.ts +0 -5
  9. package/lib/types/client/blocks/diagram/theme.d.ts +3 -1
  10. package/lib/types/client/blocks/state.d.ts +4 -0
  11. package/lib/types/client/embed.d.ts +8 -0
  12. package/lib/types/client/inline.d.ts +3 -27
  13. package/lib/types/plugin/index.d.ts +2 -1
  14. package/package.json +63 -2
  15. package/src/client/EChartNode.tsx +3 -2
  16. package/src/client/GenuiBlock.module.css +25 -14
  17. package/src/client/GenuiBlock.tsx +24 -16
  18. package/src/client/PlotBlock.tsx +2 -1
  19. package/src/client/asset-loader.ts +7 -0
  20. package/src/client/blocks/advanced.tsx +15 -15
  21. package/src/client/blocks/basic.tsx +3 -2
  22. package/src/client/blocks/charts.tsx +18 -19
  23. package/src/client/blocks/checkbox.tsx +2 -1
  24. package/src/client/blocks/diagram/index.tsx +33 -24
  25. package/src/client/blocks/diagram/layout.ts +4 -1
  26. package/src/client/blocks/diagram/theme.ts +22 -3
  27. package/src/client/blocks/forms.tsx +15 -14
  28. package/src/client/blocks/image.tsx +2 -1
  29. package/src/client/blocks/render-node.tsx +18 -18
  30. package/src/client/blocks/state.ts +4 -0
  31. package/src/client/dom-fence.tsx +19 -10
  32. package/src/client/embed.ts +9 -0
  33. package/src/client/gallery.ts +6 -0
  34. package/src/client/guard.ts +9 -3
  35. package/src/client/inline.ts +36 -44
  36. package/src/plugin/index.ts +8 -6
  37. package/src/plugin/tool.ts +2 -2
package/lib/index.js CHANGED
@@ -1998,7 +1998,7 @@ function repairDiagram(v) {
1998
1998
  kind,
1999
1999
  nodes,
2000
2000
  edges,
2001
- zones,
2001
+ ...opt("zones", zones.length > 0 ? zones : void 0),
2002
2002
  ...opt("variant", enu(o.variant, DIAGRAM_VARIANTS)),
2003
2003
  ...opt("title", str(o.title, 256)),
2004
2004
  ...opt("theme", repairDiagramTheme(o.theme))
@@ -2110,7 +2110,7 @@ function sanitizeEChartOption(v, depth, budget) {
2110
2110
  const s = sanitizeEChartOption(v[i], depth + 1, budget);
2111
2111
  if (s !== void 0) arr.push(s);
2112
2112
  }
2113
- return arr.length > 0 ? arr : void 0;
2113
+ return arr.length > 0 || v.length === 0 ? arr : void 0;
2114
2114
  }
2115
2115
  const o = obj(v);
2116
2116
  if (o === void 0) return void 0;
@@ -2121,7 +2121,7 @@ function sanitizeEChartOption(v, depth, budget) {
2121
2121
  if (key === "tooltip" && typeof s === "object" && s !== null && !Array.isArray(s)) s.renderMode = "richText";
2122
2122
  out[key] = s;
2123
2123
  }
2124
- return Object.keys(out).length > 0 ? out : void 0;
2124
+ return Object.keys(out).length > 0 || Object.keys(o).length === 0 ? out : void 0;
2125
2125
  }
2126
2126
  /**
2127
2127
  * Deterministically repair a raw spec value into a renderable GenuiSpec.
@@ -2835,11 +2835,11 @@ function formatProcessFailure(processed) {
2835
2835
  const chartErrors = processed.errors.filter((error) => /(?:variant is unsupported|kind must be bars, line, or donut|requires data or series|(?:data|series) is required for|(?:\.data|\.series)(?:\[\d+\])?(?:\.(?:data|label|value|color))? must|series is only supported for bars)/.test(error));
2836
2836
  return chartErrors.length === 0 ? void 0 : `❌ chart 字段验证失败:\n- ${chartErrors.join("\n- ")}`;
2837
2837
  }
2838
- /** Return the legacy validation text for a process that dropped native nodes. */
2838
+ /** Report dropped components without hiding their actionable field errors. */
2839
2839
  function droppedNodeFailure(processed) {
2840
2840
  if (!processed.errors.some((error) => error.startsWith("repair dropped "))) return void 0;
2841
2841
  const dropped = processed.declaredNativeCount - processed.renderedNativeCount;
2842
- return `❌ 验证未通过:检测到声明了 ${processed.declaredNativeCount} 个组件,但仅成功解析出 ${processed.renderedNativeCount} 个(有 ${dropped} 个组件因字段格式异常被丢弃)。常见原因:table 的 columns/rows 不是二维字符串数组、tabs 的 items/content 缺失、嵌套组件字段类型不符。请修正后重新验证。`;
2842
+ return `❌ 验证未通过:检测到声明了 ${processed.declaredNativeCount} 个组件,但仅成功解析出 ${processed.renderedNativeCount} 个(有 ${dropped} 个组件因字段格式异常被丢弃)。请修正后重新验证。\n- ${processed.errors.join("\n- ")}`;
2843
2843
  }
2844
2844
  /** Tool-call title shared by the pending and completed presentations. */
2845
2845
  function cardTitle(args) {
@@ -3104,26 +3104,27 @@ const GENUI_SECTION_TEXT = `You can render interactive UI components INSIDE your
3104
3104
  {"title":"可选标题","gap":14,"items":[...]}
3105
3105
  \`\`\`
3106
3106
 
3107
- The 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→component mapping and per-component field details:
3107
+ Allowed \`type\` values; the \`genui\` skill, when available, carries the full content→component mapping and per-component field details:
3108
3108
 
3109
3109
  - 布局: text · row · col · grid · card · divider · spacer · hero(封面块:超大数字 + 标题 + tone 渐变底色,一条回答最多一个)
3110
3110
  - 展示: badge · stat · progress · list · table · keyvalue · avatar · audio · video · timeline · file-tree · breadcrumb · callout · steps · diff · json · code · copy
3111
- - 图表: chart {"kind":"bars|line|donut","data":[{"label":"...","value":n}],"series":[{"label":"...","data":[...]}]?,"horizontal":true?,"stacked":true?}(series:bars 分组/堆叠 / line 多序列;horizontal 横向柱) · echart (preset: bar/line/area/pie/scatter/radar/gauge/funnel/treemap/sankey/graph/heatmap/bigline,或 option 直通) · plot (函数图)
3111
+ - 图表: chart {"kind":"bars|line|donut","data":[{"label":"...","value":n}],"series":[{"label":"...","data":[...]}]?,"horizontal":true?,"stacked":true?}(series:bars 分组/堆叠 / line 多序列;horizontal 横向柱) · echart (preset 名或 option 直通,预设清单见 skill) · plot (函数图)
3112
3112
  - 交互: button · input · textarea · select · checkbox · switch · slider · radio · submit · quiz · link · tabs · accordion
3113
- - 高级: mermaid (flowchart/sequence/class/gantt/pie/er/state/journey) · diagram (编辑级架构/流程图,27 种 kind) · scene3d (3D WebGL)
3113
+ - 高级: mermaid (流程图/时序/类图/甘特/饼图/ER/状态/旅程,关键字见 skill) · diagram (架构/流程图) · scene3d (3D WebGL)
3114
3114
 
3115
3115
  **默认就该出 UI**:出现下列情况至少出一个围栏:
3116
3116
  - ≥3 条并列要点 → \`list\`;数字对比 → \`table\`;指标/进度/状态 → \`stat\`/\`progress\`/\`badge\`
3117
3117
  - 步骤/时间线 → \`steps\`/\`timeline\`/\`mermaid\`;架构/流程 → \`diagram\` 或 \`mermaid\`;风险/结论 → \`callout\`;代码/改动 → \`code\`/\`diff\`/\`json\`
3118
- - 行内富文本:\`text\`/\`list\`/表格文本列/\`keyvalue\`/\`callout\` 里可写 \`code\`、**加粗**、==高亮==、[文字](url):重点留在句中,不必为一个词单起组件。
3118
+ - 行内富文本:\`text\`/\`list\`/表格文本列/\`keyvalue\`/\`callout\` 里可写 $公式$、$$块公式$$、\`code\`、**加粗**、==高亮==、[文字](url):重点留在句中,不必为一个词单起组件。
3119
3119
  - 默认无卡 ≠ 少用组件:硬触发照常出组件,**组件多不是问题**——判据是每个组件承载不同信息、有焦点与层次、同一批数据不重复表达。卡片只用于并排项与数据对象;单段文字用「标题 + 正文 + 间距」。
3120
3120
 
3121
- **发回答前最后自检一次**:这段内容里有没有 ≥3 条并列要点、任何对比、任何数字/指标、任何步骤或流程?有就先转成组件再开口。**状态汇报、进度说明、提交与改动清单同样算**——不要因为它是"说明文"就用纯文字写。这一条踩过的坑:连续几条汇报全靠文字,一条围栏都没发。
3121
+ **发回答前最后自检一次**:这段内容里有没有 ≥3 条并列要点、任何对比、任何数字/指标、任何步骤或流程?有就先转成组件再开口。**状态汇报、进度说明、提交与改动清单同样算**。
3122
3122
  - 趋势/占比 → \`chart\`(≤8 点)或 \`echart\`(多序列/要交互时);配色默认跟随主题,只有语义需要时才用 \`palette\` / \`card.accent\`;排版用 grid 子节点的 \`"span":2\` 跨列做宽窄混排(bento),不要一列方块堆到底;数据多时给 \`table\`/\`chart\`/\`list\` 配一个 \`input\`(id) + \`filter\` 绑定,读者能就地筛选,不用再问一遍
3123
3123
 
3124
- **字段速查**(完整见 genui skill):\`stat\` \`{"label","value","delta"?}\` · \`table\` \`{"columns","rows","types"?,"total"?,"details"?,"filter"?,"export"?}\` · \`callout\` \`{"tone","title","content"}\` · \`progress\` \`{"value","variant"?,"target"?}\`
3124
+ **字段速查**(完整见 genui skill):\`stat\` \`{"label","value","delta"?}\` · \`table\` \`{"columns":[...],"rows":[[...]],"types"?,"total"?:true,"details"?:[[...]],"filter"?:id,"export"?:true}\` · \`callout\` \`{"tone":"info|success|warning|error","title","content"}\` · \`progress\` \`{"value":0-100,"label"?,"variant"?,"target"?}\` · \`keyvalue\` \`{"pairs":[{"key","value"}]}\` · \`steps\` \`{"steps":[{"title","desc"?}]}\`
3125
3125
 
3126
3126
  Rules:
3127
+ - Match the user’s language in prose and UI text. Chinese examples are schema examples, not a language instruction. Keep JSON keys/type values unchanged.
3127
3128
  - JSON 严格: 坏围栏降级为代码块;≥3 节点或含 table 的围栏发出前调用 validate_dsh_ui,❌ 修好再发(若附「已自动修复」JSON 照抄即可)。
3128
3129
  - 规模: ≤200 节点、嵌套≤8 层(超出被截断);一条回答 3–8 个组件,一个主题一个主组件;3D mesh 1–5;plot 给合理 xMin/xMax。
3129
3130
  - LOCAL-FIRST + actions: UI 能自己做的状态变化(判卷、判题、重置、展开、选中)就地完成,零往返;action 只用于必须模型参与的事。交互组件带 "action":"name",交互以 [genui-action] name + 组件数据回传,届时重渲染更新 UI;无 action 的按钮禁用。
@@ -3136,6 +3137,7 @@ Rules:
3136
3137
  * Register the GenUI output-language section and the render_ui tool.
3137
3138
  * @param ctx - cordis context.
3138
3139
  */
3140
+ const name = "@changfenhuang/dsh-genui";
3139
3141
  const inject = ["systemPrompt"];
3140
3142
  const BUNDLED_SKILL_RANK = 600;
3141
3143
  const BUNDLED_SKILL_PROVIDER = "dsh-genui";
@@ -3207,4 +3209,4 @@ function apply(ctx) {
3207
3209
  });
3208
3210
  }
3209
3211
  //#endregion
3210
- export { GENUI_SECTION_TEXT, apply, inject };
3212
+ export { GENUI_SECTION_TEXT, apply, inject, name };
@@ -17,6 +17,8 @@
17
17
  * query; the route serves no-cache anyway.
18
18
  * @module @changfenhuang/dsh-genui/client/asset-loader
19
19
  */
20
+ /** Set once before rendering when the host bundles engines locally. */
21
+ export declare function setGenuiAssetBase(baseURL: string): void;
20
22
  /** Resolve an asset URL, appending the bundle rev for cache busting when the
21
23
  * boot graph exposes it. */
22
24
  export declare function assetUrl(file: string): string;
@@ -1,8 +1,3 @@
1
- /**
2
- * Basic display family: avatar palette, and the local click-feedback button
3
- * (the actionable-button chip). Used by the render dispatcher.
4
- * @module @changfenhuang/dsh-genui/client/blocks/basic
5
- */
6
1
  import { type ReactNode } from 'react';
7
2
  import type { GenuiAudio, GenuiVideo } from '../spec.ts';
8
3
  export declare function avatarColor(name: string): string;
@@ -21,6 +21,8 @@ export interface DiagramPalette {
21
21
  accentTint: string;
22
22
  link: string;
23
23
  }
24
+ /** Native SVG paint values keep inherited host tokens live across theme changes. */
25
+ export declare function hostPalette(): DiagramPalette;
24
26
  /** Resolve the active palette from variant + optional theme overrides. */
25
27
  export declare function resolvePalette(variant: GenuiDiagramVariant | undefined, theme: GenuiDiagramTheme | undefined): DiagramPalette;
26
28
  /** Node treatment → { fill, stroke, dashed } per diagram-design §5. */
@@ -31,5 +33,5 @@ export declare function nodeTreatment(type: string | undefined, p: DiagramPalett
31
33
  };
32
34
  /** Edge stroke color per semantic kind. */
33
35
  export declare function edgeStroke(kind: string | undefined, p: DiagramPalette): string;
34
- /** Helper: a color at a given opacity (accepts #hex and rgba() strings). */
36
+ /** Apply opacity while retaining live host-token references. */
35
37
  export declare function inkAt(color: string, opacity: number): string;
@@ -6,6 +6,7 @@
6
6
  * @module @changfenhuang/dsh-genui/client/blocks/state
7
7
  */
8
8
  import type { GenuiSpec } from '../spec.ts';
9
+ import type { BlockInteractionState } from '../interaction-store.ts';
9
10
  export interface GenuiBlockProps {
10
11
  /** Parsed spec to render. */
11
12
  spec: GenuiSpec;
@@ -26,6 +27,9 @@ export interface GenuiBlockProps {
26
27
  stateKey?: string | undefined;
27
28
  /** Animate newly arriving items; settled message replays opt out. */
28
29
  animateEntrance?: boolean | undefined;
30
+ /** Native hosts own durable state instead of the browser store. Read once per stateKey. */
31
+ initialState?: BlockInteractionState | undefined;
32
+ onStateChange?: ((state: BlockInteractionState) => void) | undefined;
29
33
  }
30
34
  /** Per-question metadata registered by grouped radios for local grading. */
31
35
  export interface QuestionMeta {
@@ -0,0 +1,8 @@
1
+ /** Public bundler entry for hosts that embed GenUI without the DSH plugin runtime. */
2
+ export { GenuiBlock } from './GenuiBlock.tsx';
3
+ export { GenuiActionContext } from './action-context.ts';
4
+ export { ErrorBoundary } from './ErrorBoundary.tsx';
5
+ export { isRenderableProcess, processGenuiSpec } from './guard.ts';
6
+ export { setGenuiAssetBase } from './asset-loader.ts';
7
+ export type { GenuiSpec } from './spec.ts';
8
+ export type { BlockInteractionState } from './interaction-store.ts';
@@ -1,29 +1,5 @@
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
- */
1
+ /** Shared rich text for GenUI labels and content. Data values stay unchanged. */
21
2
  import { type ReactNode } from 'react';
22
- /** True when the string contains anything the parser understands. */
23
3
  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;
4
+ /** Render safe phrasing content, usable in headings, buttons and labels too. */
5
+ export declare function renderInline(text: string, allowLinks?: boolean, depth?: number): ReactNode;
@@ -13,10 +13,11 @@ 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 \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";
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\nAllowed `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 \u540D\u6216 option \u76F4\u901A\uFF0C\u9884\u8BBE\u6E05\u5355\u89C1 skill) \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 (\u6D41\u7A0B\u56FE/\u65F6\u5E8F/\u7C7B\u56FE/\u7518\u7279/\u997C\u56FE/ER/\u72B6\u6001/\u65C5\u7A0B\uFF0C\u5173\u952E\u5B57\u89C1 skill) \u00B7 diagram (\u67B6\u6784/\u6D41\u7A0B\u56FE) \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 $\u516C\u5F0F$\u3001$$\u5757\u516C\u5F0F$$\u3001`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**\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\"?:true,\"details\"?:[[...]],\"filter\"?:id,\"export\"?:true}` \u00B7 `callout` `{\"tone\":\"info|success|warning|error\",\"title\",\"content\"}` \u00B7 `progress` `{\"value\":0-100,\"label\"?,\"variant\"?,\"target\"?}` \u00B7 `keyvalue` `{\"pairs\":[{\"key\",\"value\"}]}` \u00B7 `steps` `{\"steps\":[{\"title\",\"desc\"?}]}`\n\nRules:\n- Match the user\u2019s language in prose and UI text. Chinese examples are schema examples, not a language instruction. Keep JSON keys/type values unchanged.\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.
20
20
  */
21
+ export declare const name = "@changfenhuang/dsh-genui";
21
22
  export declare const inject: string[];
22
23
  export declare function apply(ctx: Context): void;
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.10.0",
4
+ "version": "0.11.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/plugin/index.d.ts",
@@ -18,7 +18,13 @@
18
18
  "types": "./lib/types/client/index.d.ts",
19
19
  "default": "./lib/client.js"
20
20
  },
21
- "./package.json": "./package.json"
21
+ "./package.json": "./package.json",
22
+ "./embed": "./src/client/embed.ts",
23
+ "./assets/mermaid": "./src/client/asset-mermaid.ts",
24
+ "./assets/three": "./src/client/asset-three.ts",
25
+ "./assets/echarts-core": "./src/client/asset-echarts-core.ts",
26
+ "./assets/echarts": "./src/client/asset-echarts.ts",
27
+ "./skill": "./SKILL.md"
22
28
  },
23
29
  "files": [
24
30
  "lib/index.js",
@@ -62,6 +68,7 @@
62
68
  "ui"
63
69
  ],
64
70
  "dependencies": {
71
+ "katex": "0.16.47",
65
72
  "react": "^18.3.1"
66
73
  },
67
74
  "peerDependencies": {
@@ -144,6 +151,7 @@
144
151
  "@deepseek-ai/dsh-workspace": "0.1.2-rc.1",
145
152
  "@testing-library/dom": "^10.4.1",
146
153
  "@testing-library/react": "^16.0.0",
154
+ "@types/katex": "0.16.8",
147
155
  "@types/node": "^22.19.0",
148
156
  "@types/react": "^18.0.0",
149
157
  "@types/react-dom": "^18.0.0",
@@ -180,5 +188,58 @@
180
188
  "engines": {
181
189
  "node": "^22.19.0 || >=24.0.0",
182
190
  "pnpm": ">=11.7.0 <12"
191
+ },
192
+ "peerDependenciesMeta": {
193
+ "@deepseek-ai/cordis": {
194
+ "optional": true
195
+ },
196
+ "@deepseek-ai/dsh-api-session-controller": {
197
+ "optional": true
198
+ },
199
+ "@deepseek-ai/dsh-client-store": {
200
+ "optional": true
201
+ },
202
+ "@deepseek-ai/dsh-client-ui-chat": {
203
+ "optional": true
204
+ },
205
+ "@deepseek-ai/dsh-client-ui-conversation": {
206
+ "optional": true
207
+ },
208
+ "@deepseek-ai/dsh-client-ui-input-trigger": {
209
+ "optional": true
210
+ },
211
+ "@deepseek-ai/dsh-client-ui-renderer": {
212
+ "optional": true
213
+ },
214
+ "@deepseek-ai/dsh-client-ui-session": {
215
+ "optional": true
216
+ },
217
+ "@deepseek-ai/dsh-client-ui-slots": {
218
+ "optional": true
219
+ },
220
+ "@deepseek-ai/dsh-client-ui-tool": {
221
+ "optional": true
222
+ },
223
+ "@deepseek-ai/dsh-invariants": {
224
+ "optional": true
225
+ },
226
+ "@deepseek-ai/dsh-llm": {
227
+ "optional": true
228
+ },
229
+ "@deepseek-ai/dsh-session": {
230
+ "optional": true
231
+ },
232
+ "@deepseek-ai/dsh-skill": {
233
+ "optional": true
234
+ },
235
+ "@deepseek-ai/dsh-system-prompt": {
236
+ "optional": true
237
+ },
238
+ "@deepseek-ai/dsh-tools": {
239
+ "optional": true
240
+ },
241
+ "@deepseek-ai/dsh-util-values": {
242
+ "optional": true
243
+ }
183
244
  }
184
245
  }
@@ -11,6 +11,7 @@
11
11
  * `type` to `echart` and adding `preset`.
12
12
  * @module @changfenhuang/dsh-genui/client/EChartNode
13
13
  */
14
+ import { renderInline } from './inline.ts'
14
15
  import { useEffect, useRef, useState } from 'react'
15
16
  import css from './GenuiBlock.module.css'
16
17
  import { CORE_PRESETS, createChart as lazyCreateChart, type EChartsInstance } from './echarts-lazy.ts'
@@ -435,14 +436,14 @@ export function EChartNode({ node }: { node: GenuiEChart }) {
435
436
  return (
436
437
  <div className={css.echartFallback} data-genui-echart>
437
438
  <div className={css.echartErr}>ECharts 渲染失败</div>
438
- {node.title !== undefined && <div className={css.echartHint}>{node.title}</div>}
439
+ {node.title !== undefined && <div className={css.echartHint}>{renderInline(node.title)}</div>}
439
440
  </div>
440
441
  )
441
442
  }
442
443
 
443
444
  return (
444
445
  <div className={css.echartWrap} data-genui-echart>
445
- {node.title !== undefined && <div className={css.echartTitle}>{node.title}</div>}
446
+ {node.title !== undefined && <div className={css.echartTitle}>{renderInline(node.title)}</div>}
446
447
  <div
447
448
  ref={ref}
448
449
  className={css.echartCanvas}
@@ -16,9 +16,9 @@
16
16
  literals here are only last-resort fallbacks for hosts without the token
17
17
  sheets. */
18
18
 
19
- /* Dark theme: the layer step already lifts the card off the canvas, so the
20
- extra label tint stays a small overlay (4%) instead of the 10% that light
21
- mode needs to clear the 4% lightness-step minimum. */
19
+ /* Dark theme: the page is near-black, so the card is lifted by the host's own
20
+ layer step (layer-2) plus a small 4% overlay of the theme's label colour.
21
+ Light mode uses no tint at all — see the token block below for why. */
22
22
  body[data-ds-dark-theme] .block,
23
23
  body[data-ds-dark-theme] .panel {
24
24
  --dsl-g-surface: color-mix(in srgb, var(--dsw-alias-label-primary) 4%,
@@ -70,17 +70,24 @@ body[data-ds-dark-theme] .panel {
70
70
  --dsl-g-tint: 8%;
71
71
  --dsl-g-tint-strong: 14%;
72
72
  --dsl-g-font-mono: var(--ds-font-family-code, ui-monospace, 'SF Mono', monospace);
73
- /* Elevation, theme-agnostic: the light theme maps EVERY bg layer to white,
74
- so a card there is white-on-white and a border alone cannot carry the
75
- hierarchy. Both a tint of the theme's own label colour and the surface
76
- border are derived from tokens that exist in both themes:
77
- light: label ≈ near-black → tint darkens the white card to ~#f5f5f5
78
- dark : label ≈ near-white → tint lifts the card off the page */
79
- /* 4% / 14% measured as 255→246 and read as "no difference" in review: the
80
- tint now sits clearly above the perception floor. Light: page 255 → card
81
- ~232, border ~190. Dark: page 21 → card ~62 (an unmistakable elevation). */
82
- --dsl-g-surface: color-mix(in srgb, var(--dsw-alias-label-primary) 10%,
83
- var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1, var(--dsw-alias-markdown-code-block, transparent))));
73
+ /* Elevation is theme-specific, because the two themes lift a card off the
74
+ page by OPPOSITE means:
75
+ light: the host maps every bg layer to white, so there is no layer step
76
+ to take. The card is WHITE, and the lift is carried by the 1px
77
+ surface border plus the shadow — the shadow branch of the
78
+ documented rule "adjacent light surfaces need a >=4% lightness
79
+ step OR a shadow of at least 0 1px 3px rgba(0,0,0,0.10)".
80
+ dark : the page is near-black and drop shadows are nearly invisible
81
+ there, so the layer step + a 4% label overlay do the lifting
82
+ (see the dark override above).
83
+ Do NOT darken the light surface to make it "visible": a card below the
84
+ page's lightness reads as recessed/disabled, not elevated. 0.10.0 shipped
85
+ exactly that — a 10% label tint (= #e7e7e8, page 255) reported in issue
86
+ #159 as "every card is grey". The 1px border keeps its 12% weight and is
87
+ drawn over the surface, so on white it resolves to a quiet #e2e2e3
88
+ hairline instead of 0.10.0's #cdcdce. */
89
+ --dsl-g-surface: var(--dsw-alias-bg-layer-2,
90
+ var(--dsw-alias-bg-layer-1, var(--dsw-alias-markdown-code-block, #fff)));
84
91
  /* Borders separate, shadows lift (design standard: "shadows over borders"):
85
92
  the outline stays quiet and the layered shadow carries the depth, with the
86
93
  first layer at the documented minimum `0 1px 3px rgba(0,0,0,0.10)`. */
@@ -2146,3 +2153,7 @@ button.ftNameBtn[aria-expanded] { cursor: pointer; }
2146
2153
  .panelResizeHandleActive {
2147
2154
  background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #4f8ef7) 35%, transparent);
2148
2155
  }
2156
+
2157
+ /* Formula markup remains phrasing content inside labels and buttons. */
2158
+ .inlineMath { display: inline; }
2159
+ .inlineMath [class~="katex-display"] { max-width: 100%; overflow-x: auto; overflow-y: hidden; }
@@ -6,6 +6,7 @@
6
6
  * durable localStorage persistence, action debounce); the per-family
7
7
  * components live in src/client/blocks/*.
8
8
  */
9
+ import { renderInline } from './inline.ts'
9
10
  import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
10
11
  import { useGenuiAction } from './action-context.ts'
11
12
  import css from './GenuiBlock.module.css'
@@ -69,14 +70,17 @@ function specEquivalent(a: GenuiSpec, b: GenuiSpec): boolean {
69
70
 
70
71
  /** Stateful implementation. Streaming state adopts its first durable key
71
72
  * when the reply settles; switching an existing durable key starts fresh. */
72
- function GenuiBlockInstance({ spec, stateKey, animateEntrance = true }: GenuiBlockProps) {
73
+ function GenuiBlockInstance({ spec, stateKey, animateEntrance = true, initialState, onStateChange }: GenuiBlockProps) {
73
74
  const gap = spec.gap ?? 16
74
75
  const onAction = useDebouncedAction(useGenuiAction())
76
+ const stateChangeRef = useRef(onStateChange)
77
+ stateChangeRef.current = onStateChange
78
+ const savesExternally = onStateChange !== undefined
75
79
  // Grouped radios and grouped checkboxes record their local selections here;
76
80
  // `submit` either grades radio-only papers locally or aggregates all form
77
81
  // state into one action. Block-local state survives streaming/panel
78
82
  // re-renders, and with a stateKey it also survives refresh/reopen.
79
- const [persisted] = useState(() => (stateKey === undefined ? null : loadBlockState(stateKey)))
83
+ const [persisted] = useState(() => initialState ?? (onStateChange !== undefined || stateKey === undefined ? null : loadBlockState(stateKey)))
80
84
  const [answers, setAnswers] = useState<Record<string, string>>(persisted?.answers ?? {})
81
85
  const [multiAnswers, setMultiAnswers] = useState<Record<string, string[]>>(persisted?.multiAnswers ?? {})
82
86
  const [fields, setFields] = useState<Record<string, string>>(persisted?.fields ?? {})
@@ -151,20 +155,23 @@ function GenuiBlockInstance({ spec, stateKey, animateEntrance = true }: GenuiBlo
151
155
  // Durable save (debounced 300ms — typing in a field fires per keystroke).
152
156
  // Secret field values are stripped before writing: passwords never persist.
153
157
  useEffect(() => {
158
+ const safeFields = Object.fromEntries(
159
+ Object.entries(fields).filter(([id]) => !secretFields.has(id)),
160
+ )
161
+ const state = {
162
+ answers,
163
+ ...(Object.keys(multiAnswers).length > 0 ? { multiAnswers } : {}),
164
+ locked,
165
+ ...(Object.keys(safeFields).length > 0 ? { fields: safeFields } : {}),
166
+ }
167
+ if (savesExternally) {
168
+ stateChangeRef.current?.(state)
169
+ return
170
+ }
154
171
  if (stateKey === undefined) return
155
- const timer = setTimeout(() => {
156
- const safeFields = Object.fromEntries(
157
- Object.entries(fields).filter(([id]) => !secretFields.has(id)),
158
- )
159
- saveBlockState(stateKey, {
160
- answers,
161
- ...(Object.keys(multiAnswers).length > 0 ? { multiAnswers } : {}),
162
- locked,
163
- ...(Object.keys(safeFields).length > 0 ? { fields: safeFields } : {}),
164
- })
165
- }, 300)
172
+ const timer = setTimeout(() => saveBlockState(stateKey, state), 300)
166
173
  return () => clearTimeout(timer)
167
- }, [stateKey, answers, multiAnswers, locked, fields, secretFields])
174
+ }, [stateKey, answers, multiAnswers, locked, fields, secretFields, savesExternally])
168
175
  // Achievement telemetry (0.9.5): the store dedupes by spec fingerprint, so
169
176
  // streaming re-renders and replays count once per distinct content.
170
177
  useEffect(() => {
@@ -172,7 +179,7 @@ function GenuiBlockInstance({ spec, stateKey, animateEntrance = true }: GenuiBlo
172
179
  }, [spec])
173
180
  return (
174
181
  <div className={css.block} data-genui>
175
- {spec.title !== undefined && <div className={css.banner}>{spec.title}</div>}
182
+ {spec.title !== undefined && <div className={css.banner}>{renderInline(spec.title)}</div>}
176
183
  <div className={css.col} style={{ gap: `${gap}px` }}>
177
184
  {spec.items.map((c, i) => (
178
185
  // Staggered reveal: each root item fades/slides in after its
@@ -215,4 +222,5 @@ export const GenuiBlock = memo(function GenuiBlock(props: GenuiBlockProps) {
215
222
  }
216
223
  return <GenuiBlockInstance key={identity.generation} {...props} />
217
224
  }, (prev, next) => prev.stateKey === next.stateKey
218
- && prev.animateEntrance === next.animateEntrance && specEquivalent(prev.spec, next.spec))
225
+ && prev.animateEntrance === next.animateEntrance && prev.onStateChange === next.onStateChange
226
+ && specEquivalent(prev.spec, next.spec))
@@ -8,6 +8,7 @@
8
8
  * which render as live sliders under the chart — dragging re-samples the
9
9
  * curve in place. The plot itself supports drag-to-pan and wheel-to-zoom.
10
10
  */
11
+ import { renderInline } from './inline.ts'
11
12
  import { memo, useEffect, useRef, useState, type ReactNode } from 'react'
12
13
  import { sampleExpr } from './safe-math.ts'
13
14
  import css from './PlotBlock.module.css'
@@ -352,7 +353,7 @@ export const PlotBlock = memo(function PlotBlock({
352
353
 
353
354
  return (
354
355
  <div className={css.block} data-genui-plot>
355
- {title !== undefined && <div className={css.title}>{title}</div>}
356
+ {title !== undefined && <div className={css.title}>{renderInline(title)}</div>}
356
357
  {hasData && hasValidRange && (
357
358
  <div className={css.plotTools}>
358
359
  <span className={css.plotHint}>
@@ -23,6 +23,12 @@ const PLUGIN_ID = '@changfenhuang/dsh-genui'
23
23
 
24
24
  /** Assets directory served by the node-half route. */
25
25
  const ASSET_DIR = `/plugins/${PLUGIN_ID}/assets`
26
+ let embeddedAssetBase: string | undefined
27
+
28
+ /** Set once before rendering when the host bundles engines locally. */
29
+ export function setGenuiAssetBase(baseURL: string): void {
30
+ embeddedAssetBase = new URL(baseURL, document.baseURI).href.replace(/\/?$/, '/')
31
+ }
26
32
 
27
33
  /** Boot graph shape read from `window.__DSH_BOOT__` (subset, defensive). */
28
34
  interface BootGraphLike {
@@ -37,6 +43,7 @@ interface AssetGlobal {
37
43
  /** Resolve an asset URL, appending the bundle rev for cache busting when the
38
44
  * boot graph exposes it. */
39
45
  export function assetUrl(file: string): string {
46
+ if (embeddedAssetBase !== undefined) return new URL(file, embeddedAssetBase).href
40
47
  const graph = (window as unknown as { __DSH_BOOT__?: BootGraphLike }).__DSH_BOOT__
41
48
  const rev = graph?.entries?.find(entry => entry.id === PLUGIN_ID)?.rev
42
49
  return `${ASSET_DIR}/${file}${rev === undefined ? '' : `?rev=${rev}`}`