@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.
- package/CHANGELOG.md +18 -1
- package/README.md +1 -1
- package/README.zh-CN.md +9 -1
- package/SKILL.md +4 -0
- package/lib/client.js +261 -5
- package/lib/index.js +14 -12
- package/lib/types/client/asset-loader.d.ts +2 -0
- package/lib/types/client/blocks/basic.d.ts +0 -5
- package/lib/types/client/blocks/diagram/theme.d.ts +3 -1
- package/lib/types/client/blocks/state.d.ts +4 -0
- package/lib/types/client/embed.d.ts +8 -0
- package/lib/types/client/inline.d.ts +3 -27
- package/lib/types/plugin/index.d.ts +2 -1
- package/package.json +63 -2
- package/src/client/EChartNode.tsx +3 -2
- package/src/client/GenuiBlock.module.css +25 -14
- package/src/client/GenuiBlock.tsx +24 -16
- package/src/client/PlotBlock.tsx +2 -1
- package/src/client/asset-loader.ts +7 -0
- package/src/client/blocks/advanced.tsx +15 -15
- package/src/client/blocks/basic.tsx +3 -2
- package/src/client/blocks/charts.tsx +18 -19
- package/src/client/blocks/checkbox.tsx +2 -1
- package/src/client/blocks/diagram/index.tsx +33 -24
- package/src/client/blocks/diagram/layout.ts +4 -1
- package/src/client/blocks/diagram/theme.ts +22 -3
- package/src/client/blocks/forms.tsx +15 -14
- package/src/client/blocks/image.tsx +2 -1
- package/src/client/blocks/render-node.tsx +18 -18
- package/src/client/blocks/state.ts +4 -0
- package/src/client/dom-fence.tsx +19 -10
- package/src/client/embed.ts +9 -0
- package/src/client/gallery.ts +6 -0
- package/src/client/guard.ts +9 -3
- package/src/client/inline.ts +36 -44
- package/src/plugin/index.ts +8 -6
- 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
|
-
/**
|
|
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}
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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\` 里可写
|
|
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"
|
|
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
|
-
/**
|
|
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
|
-
|
|
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\
|
|
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.
|
|
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
|
|
20
|
-
|
|
21
|
-
mode
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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 &&
|
|
225
|
+
&& prev.animateEntrance === next.animateEntrance && prev.onStateChange === next.onStateChange
|
|
226
|
+
&& specEquivalent(prev.spec, next.spec))
|
package/src/client/PlotBlock.tsx
CHANGED
|
@@ -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}`}`
|