dsh-motion-ui 0.0.0-stage → 0.5.2
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 +16 -0
- package/DESIGN.zh-CN.md +56 -0
- package/LICENSE +21 -0
- package/README.md +162 -2
- package/RELEASE-PLAN.zh-CN.md +52 -0
- package/VERIFICATION.md +133 -0
- package/client.js +23645 -0
- package/cordis.patch.yml +3 -0
- package/examples/agent-architecture.json +48 -0
- package/examples/bicycle-explorer.json +108 -0
- package/examples/calculator.json +94 -0
- package/examples/comparison.json +59 -0
- package/examples/conditional-budget.json +148 -0
- package/examples/live-memory-curve.json +251 -0
- package/examples/scene-comparison.json +86 -0
- package/examples/scene-dashboard.json +92 -0
- package/examples/scene-guide.json +56 -0
- package/examples/scene-report.json +61 -0
- package/examples/scene-status.json +70 -0
- package/examples/scene-workbench.json +105 -0
- package/examples/sine-motion.json +49 -0
- package/examples/speed-memory-scatter.json +113 -0
- package/index.js +69 -0
- package/lib/author-expression.js +60 -0
- package/lib/build-info.js +2 -0
- package/lib/canvas-design.js +10 -0
- package/lib/canvas-example.js +46 -0
- package/lib/canvas-export.js +9 -0
- package/lib/canvas-preview.js +31 -0
- package/lib/canvas-system.js +14 -0
- package/lib/catalog.js +92 -0
- package/lib/components.js +28 -0
- package/lib/export-runtime-bundle.js +2 -0
- package/lib/format-number.js +7 -0
- package/lib/freeform.js +13 -0
- package/lib/inspection.js +13 -0
- package/lib/presentation.js +6 -0
- package/lib/protocol.js +295 -0
- package/lib/refinement.js +31 -0
- package/lib/sandbox.js +52 -0
- package/lib/scenes.js +50 -0
- package/lib/store.js +176 -0
- package/lib/stream.js +87 -0
- package/lib/tool-json.js +7 -0
- package/lib/ui-intent.js +14 -0
- package/lib/ui-skill.js +46 -0
- package/lib/visual-audit.js +18 -0
- package/lib/visual-examples.js +28 -0
- package/package.json +75 -4
- package/skills/intelligent-ui/SKILL.md +86 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"title": "参数工作台 · 下载时间估算",
|
|
4
|
+
"fallback": "这是布局与交互示例,数据为演示数据;不代表实际测试、任务进度或推荐结论。",
|
|
5
|
+
"presentation": {
|
|
6
|
+
"layout": "workbench",
|
|
7
|
+
"accent": "teal"
|
|
8
|
+
},
|
|
9
|
+
"nodes": [
|
|
10
|
+
{
|
|
11
|
+
"id": "assumption",
|
|
12
|
+
"type": "text",
|
|
13
|
+
"text": "理想持续速度估算,不含协议开销、限速与波动。",
|
|
14
|
+
"tone": "warning"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "parameters",
|
|
18
|
+
"type": "card",
|
|
19
|
+
"title": "输入参数",
|
|
20
|
+
"children": [
|
|
21
|
+
{
|
|
22
|
+
"id": "size",
|
|
23
|
+
"type": "input",
|
|
24
|
+
"label": "文件大小 GiB",
|
|
25
|
+
"bind": "state.size"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "speed",
|
|
29
|
+
"type": "slider",
|
|
30
|
+
"label": "持续速度 MiB/s",
|
|
31
|
+
"bind": "state.speed"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"surface": "outline"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "time",
|
|
38
|
+
"type": "metric",
|
|
39
|
+
"label": "理论用时",
|
|
40
|
+
"value": {
|
|
41
|
+
"ref": "derived.minutes"
|
|
42
|
+
},
|
|
43
|
+
"unit": "分钟",
|
|
44
|
+
"decimals": 1,
|
|
45
|
+
"icon": "gauge"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "detail",
|
|
49
|
+
"type": "details",
|
|
50
|
+
"title": "计算方式",
|
|
51
|
+
"children": [
|
|
52
|
+
{
|
|
53
|
+
"id": "formula",
|
|
54
|
+
"type": "text",
|
|
55
|
+
"text": "分钟 = GiB × 1024 ÷ MiB/s ÷ 60。速度需使用实际持续传输速度。"
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"state": {
|
|
61
|
+
"size": {
|
|
62
|
+
"type": "number",
|
|
63
|
+
"default": 24,
|
|
64
|
+
"min": 1,
|
|
65
|
+
"max": 500,
|
|
66
|
+
"step": 1
|
|
67
|
+
},
|
|
68
|
+
"speed": {
|
|
69
|
+
"type": "number",
|
|
70
|
+
"default": 80,
|
|
71
|
+
"min": 5,
|
|
72
|
+
"max": 200,
|
|
73
|
+
"step": 5
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"derived": {
|
|
77
|
+
"minutes": {
|
|
78
|
+
"op": "div",
|
|
79
|
+
"args": [
|
|
80
|
+
{
|
|
81
|
+
"op": "mul",
|
|
82
|
+
"args": [
|
|
83
|
+
{
|
|
84
|
+
"ref": "state.size"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"const": 1024
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"op": "mul",
|
|
93
|
+
"args": [
|
|
94
|
+
{
|
|
95
|
+
"ref": "state.speed"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"const": 60
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"title": "正弦运动 · 参数实验",
|
|
4
|
+
"fallback": "数学动画示意:位置 x = A sin(2πft)。振幅 A 与频率 f 可调;不是弹簧物理仿真。刷新保留参数,动画时间重新开始。",
|
|
5
|
+
"state": {
|
|
6
|
+
"amplitude": {
|
|
7
|
+
"type": "number",
|
|
8
|
+
"default": 90,
|
|
9
|
+
"min": 10,
|
|
10
|
+
"max": 150,
|
|
11
|
+
"step": 10
|
|
12
|
+
},
|
|
13
|
+
"frequency": {
|
|
14
|
+
"type": "number",
|
|
15
|
+
"default": 1,
|
|
16
|
+
"min": 0.25,
|
|
17
|
+
"max": 2,
|
|
18
|
+
"step": 0.25
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"nodes": [
|
|
22
|
+
{
|
|
23
|
+
"id": "wave",
|
|
24
|
+
"type": "sandbox",
|
|
25
|
+
"title": "可调正弦运动",
|
|
26
|
+
"height": 260,
|
|
27
|
+
"fallback": "圆点沿水平轴做正弦运动。振幅决定最大偏移,频率决定每秒往复次数;减少动态效果开启时显示静止示意。",
|
|
28
|
+
"stateKeys": [
|
|
29
|
+
"amplitude",
|
|
30
|
+
"frequency"
|
|
31
|
+
],
|
|
32
|
+
"html": "<div class=\"lab\"><p>POSITION / TIME</p><svg viewBox=\"0 0 500 150\" role=\"img\" aria-label=\"圆点沿水平轴正弦运动\"><path d=\"M50 75H450\"/><path d=\"M250 60V90\"/><circle id=\"mass\" cx=\"250\" cy=\"75\" r=\"18\"/></svg><output id=\"equation\"></output></div>",
|
|
33
|
+
"css": ".lab{padding:20px;background:var(--canvas-soft);border-radius:14px}.lab p{font-size:10px;letter-spacing:2px;color:var(--canvas-muted);margin:0}.lab svg{width:100%;display:block}.lab path{stroke:var(--canvas-line);fill:none}.lab circle{fill:var(--canvas-accent)}.lab output{font-size:13px;font-variant-numeric:tabular-nums}",
|
|
34
|
+
"script": "let s=DSHUI.snapshot;DSHUI.subscribe(v=>{s=v;document.getElementById('equation').textContent='x = '+s.state.amplitude+' sin(2π × '+s.state.frequency+' × t)'});DSHUI.animate(({time,reducedMotion})=>{const x=reducedMotion?0:s.state.amplitude*Math.sin(2*Math.PI*s.state.frequency*time);document.getElementById('mass').setAttribute('cx',250+x)});"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "amplitude",
|
|
38
|
+
"type": "slider",
|
|
39
|
+
"label": "振幅 A(示意坐标单位)",
|
|
40
|
+
"bind": "state.amplitude"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "frequency",
|
|
44
|
+
"type": "slider",
|
|
45
|
+
"label": "频率 f(Hz)",
|
|
46
|
+
"bind": "state.frequency"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"title": "看清速度与内存的取舍",
|
|
4
|
+
"presentation": {
|
|
5
|
+
"layout": "dashboard",
|
|
6
|
+
"accent": "teal"
|
|
7
|
+
},
|
|
8
|
+
"fallback": "全部为演示数据,不能据此推荐真实模型。方案 A:12 GiB、35 tokens/s;B:20 GiB、60 tokens/s;C:28 GiB、45 tokens/s。倍率只做假设计算,内存固定。",
|
|
9
|
+
"state": {
|
|
10
|
+
"multiplier": {
|
|
11
|
+
"type": "number",
|
|
12
|
+
"default": 1,
|
|
13
|
+
"min": 1,
|
|
14
|
+
"max": 3,
|
|
15
|
+
"step": 0.25
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"data": {
|
|
19
|
+
"samples": [
|
|
20
|
+
{
|
|
21
|
+
"name": "A",
|
|
22
|
+
"memory": 12,
|
|
23
|
+
"speed": 35
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "B",
|
|
27
|
+
"memory": 20,
|
|
28
|
+
"speed": 60
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "C",
|
|
32
|
+
"memory": 28,
|
|
33
|
+
"speed": 45
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"derived": {
|
|
38
|
+
"series": {
|
|
39
|
+
"map": {
|
|
40
|
+
"source": "data.samples",
|
|
41
|
+
"fields": {
|
|
42
|
+
"name": {
|
|
43
|
+
"ref": "item.name"
|
|
44
|
+
},
|
|
45
|
+
"memory": {
|
|
46
|
+
"ref": "item.memory"
|
|
47
|
+
},
|
|
48
|
+
"speed": {
|
|
49
|
+
"op": "mul",
|
|
50
|
+
"args": [
|
|
51
|
+
{
|
|
52
|
+
"ref": "item.speed"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"ref": "state.multiplier"
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"nodes": [
|
|
64
|
+
{
|
|
65
|
+
"id": "notice",
|
|
66
|
+
"type": "text",
|
|
67
|
+
"tone": "warning",
|
|
68
|
+
"text": "演示数据,不是模型实测。假设吞吐倍率不代表真实优化收益。"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"id": "scatter",
|
|
72
|
+
"type": "sandbox",
|
|
73
|
+
"title": "内存与速度散点图",
|
|
74
|
+
"height": 330,
|
|
75
|
+
"derivedKeys": [
|
|
76
|
+
"series"
|
|
77
|
+
],
|
|
78
|
+
"fallback": "横轴为演示内存 GiB,纵轴为演示速度 tokens/s。三个方案各一个点,右侧表格提供当前精确数值;倍率提高时速度按同一公式变化。",
|
|
79
|
+
"html": "<div class=\"plot\"><div class=\"caption\"><strong>空间与速度</strong><span>假设数据 · 同一计算结果</span></div><div class=\"scatter-scroll\" tabindex=\"0\" role=\"region\" aria-label=\"散点图,窄屏可横向滚动\"><svg viewBox=\"0 0 600 300\" role=\"img\" aria-label=\"演示内存与速度散点图,当前精确值见下方表格\"><g id=\"axes\"></g><g id=\"points\"></g><text class=\"axis-label\" x=\"330\" y=\"292\" text-anchor=\"middle\">演示内存 / GiB</text><text class=\"axis-label\" x=\"18\" y=\"145\" transform=\"rotate(-90 18 145)\" text-anchor=\"middle\">演示速度 / tokens/s</text></svg></div></div>",
|
|
80
|
+
"css": ".plot{padding:12px 0}.scatter-scroll{max-width:100%;overflow-x:auto}.scatter-scroll svg{min-width:520px}.caption{display:flex;align-items:baseline;justify-content:space-between;gap:12px;flex-wrap:wrap}.caption strong{font-size:16px}.caption span{font-size:11px;color:var(--canvas-muted)}svg{display:block;width:100%;margin-top:8px}.grid{stroke:var(--canvas-line);stroke-dasharray:3 5}.tick,.axis-label{fill:var(--canvas-muted);font-size:12px}.value{fill:var(--canvas-ink);font-size:13px;font-variant-numeric:tabular-nums}.dot{fill:var(--canvas-accent);stroke:var(--canvas-bg);stroke-width:2}",
|
|
81
|
+
"script": "const NS='http://www.w3.org/2000/svg',axes=document.getElementById('axes'),points=document.getElementById('points');const add=(tag,attrs,text,parent)=>{const el=document.createElementNS(NS,tag);for(const[k,v]of Object.entries(attrs))el.setAttribute(k,v);if(text!==undefined)el.textContent=text;parent.append(el);return el};for(const value of[0,60,120,180]){const y=250-value/180*216;add('line',{x1:65,x2:570,y1:y,y2:y,class:'grid'},undefined,axes);add('text',{x:54,y:y+4,'text-anchor':'end',class:'tick'},value,axes)}for(const value of[0,8,16,24,32])add('text',{x:65+value/32*505,y:272,'text-anchor':'middle',class:'tick'},value,axes);DSHUI.subscribe(s=>{points.replaceChildren();for(const row of s.derived.series){const x=65+row.memory/32*505,y=250-row.speed/180*216;const circle=add('circle',{cx:x,cy:y,r:7,class:'dot'},undefined,points);add('title',{},row.name+': '+row.memory+' GiB, '+row.speed+' tokens/s',circle);add('text',{x:x+11,y:y-11,class:'value'},row.name+' · '+row.speed,points)}});"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": "multiplier",
|
|
85
|
+
"type": "slider",
|
|
86
|
+
"label": "假设吞吐倍率(内存固定)",
|
|
87
|
+
"bind": "state.multiplier"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"id": "values",
|
|
91
|
+
"type": "table",
|
|
92
|
+
"rows": {
|
|
93
|
+
"ref": "derived.series"
|
|
94
|
+
},
|
|
95
|
+
"columns": [
|
|
96
|
+
{
|
|
97
|
+
"key": "name",
|
|
98
|
+
"label": "演示方案"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"key": "memory",
|
|
102
|
+
"label": "演示内存",
|
|
103
|
+
"unit": "GiB"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"key": "speed",
|
|
107
|
+
"label": "假设速度",
|
|
108
|
+
"unit": "tokens/s"
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
3
|
+
import { validateDocument, fallbackText } from './lib/protocol.js';
|
|
4
|
+
import { UiStore, stateRoute, documentRoute, historyRoute } from './lib/store.js';
|
|
5
|
+
import { evaluate } from './lib/protocol.js';
|
|
6
|
+
import { componentCatalog } from './lib/components.js';
|
|
7
|
+
import { CANVAS_DESIGN_GUIDE } from './lib/canvas-design.js';
|
|
8
|
+
import { architectureExample, scatterExample } from './lib/visual-examples.js';
|
|
9
|
+
import { BUILD_INFO } from './lib/build-info.js';
|
|
10
|
+
import { UI_SKILL_SECTIONS, promptSections } from './lib/ui-skill.js';
|
|
11
|
+
import { canvasDocument } from './lib/freeform.js';
|
|
12
|
+
import z from '@deepseek-ai/schemastery';
|
|
13
|
+
import { toolJson } from './lib/tool-json.js';
|
|
14
|
+
import { inspectRecord } from './lib/inspection.js';
|
|
15
|
+
import { PROTOCOL_GUIDE, calculatorExample, comparisonExample, budgetExample, curveExample, sceneExamples, bicycleExample, oscillatorExample } from './lib/catalog.js';
|
|
16
|
+
|
|
17
|
+
export const name = 'motion-ui';
|
|
18
|
+
export const inject = ['tools', 'connection', 'systemPrompt'];
|
|
19
|
+
export const Config=z.object({designMode:z.union(['guided','free']).default('guided').description('guided:设计技能与示例引导;free:只注入画布接口与交互约束,由模型自主设计。'),displayMode:z.union(['tail','inline','sidebar']).default('tail').description('tail:回答下方;inline:正文标记位置;sidebar:PC 侧栏。')}).volatile();
|
|
20
|
+
export function apply(ctx,config={}) {
|
|
21
|
+
const store = new UiStore(dshHomePath('plugins', 'dsh-generative-ui'));
|
|
22
|
+
const settings=()=>typeof config.get==='function'?config.get():config;
|
|
23
|
+
const placement=value=>settings().displayMode==='inline'?`\nPlace this exact fenced marker in your answer at the intended UI position:\n\`\`\`dsh-ui\n${value.documentId}\n\`\`\``:'';
|
|
24
|
+
ctx.effect(() => ctx.tools.register(defineTool({
|
|
25
|
+
name: 'render_ui',
|
|
26
|
+
description: 'Render an interactive DSH UI v1 document in this conversation. Proactively use when visual relationships or changing parameters clearly explain the question better, even without an explicit UI request. Ordinary facts and short explanations stay text. Deliver the smallest complete useful UI first. ui_catalog supplies optional details. Direct native calls only; PTC returns text. Calculations stay local. document requires version:1,title,fallback,nodes. Repair errors at most twice or simplify/answer in text.',
|
|
27
|
+
parameters: { document: { type: 'json', required: true, description: 'DSH UI v1 document. Follow the injected protocol and worked examples; ui_catalog supplies additional schema/examples when needed.' } },
|
|
28
|
+
output: {
|
|
29
|
+
schema: { type: 'json' },
|
|
30
|
+
render: (_args, value) => [{ type: 'text', text: fallbackText(value.document)+`\nUI documentId=${value.documentId}; documentRevision=${value.documentRevision??0}. Use inspect_ui before refinements.`+placement(value) }],
|
|
31
|
+
presentationMeta: (_args, value) => ({ dshGenerativeUi: value })
|
|
32
|
+
},
|
|
33
|
+
async execute({ document }, exec) {
|
|
34
|
+
exec.signal.throwIfAborted();
|
|
35
|
+
const validated = validateDocument(toolJson(document,'document')); fallbackText(validated);
|
|
36
|
+
const record = await store.create(validated, { sessionId: exec.agent?.id ?? 'unscoped', callId: exec.callId });
|
|
37
|
+
exec.signal.throwIfAborted(); return record;
|
|
38
|
+
}
|
|
39
|
+
})));
|
|
40
|
+
ctx.effect(()=>ctx.tools.register(defineTool({
|
|
41
|
+
name:'render_canvas',description:'Create a freeform interactive HTML/SVG/CSS/JavaScript canvas in the conversation. You design all content, controls, layout and motion. No fixed UI composition. Same persistence, isolation and inspect/patch support as render_ui. Use the injected canvas contract; no external resources or host APIs. Direct native calls only.',
|
|
42
|
+
parameters:{title:{type:'string',required:true},fallback:{type:'string',required:true},state:{type:'json'},data:{type:'json'},derived:{type:'json'},height:{type:'number'},css:{type:'string',description:'Emit before html for styled static streaming preview; use an empty string when no custom styles are needed.'},html:{type:'string',required:true},script:{type:'string'}},
|
|
43
|
+
output:{schema:{type:'json'},render:(_a,v)=>[{type:'text',text:fallbackText(v.document)+`\nUI documentId=${v.documentId}; documentRevision=${v.documentRevision??0}; canvas nodeId=canvas.`+placement(v)}],presentationMeta:(_a,v)=>({dshGenerativeUi:v})},
|
|
44
|
+
async execute(args,exec){exec.signal.throwIfAborted();const record=await store.create(canvasDocument(args),{sessionId:exec.agent?.id??'unscoped',callId:exec.callId});exec.signal.throwIfAborted();return record;}
|
|
45
|
+
})));
|
|
46
|
+
ctx.effect(() => ctx.tools.register(defineTool({
|
|
47
|
+
name: 'ui_catalog', description: 'Read the DSH UI protocol and registered components. Optional topic: overview (default, compact examples), canvas (freeform design guide and four visual examples), data, scenes, all (fourteen examples). Prefer targeted topics to avoid unnecessary context. Read-only; omit for ordinary text answers.',
|
|
48
|
+
parameters: { topic: { type: 'string', enum: ['overview', 'canvas', 'data', 'scenes', 'all'], description: 'overview, canvas, data, scenes or all; default overview' } }, output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }] },
|
|
49
|
+
isConcurrencySafe: () => true,
|
|
50
|
+
async execute({ topic = 'overview' } = {}) {
|
|
51
|
+
const groups = { overview: [calculatorExample, oscillatorExample], canvas: [bicycleExample, oscillatorExample, architectureExample, scatterExample], data: [calculatorExample, comparisonExample, budgetExample, curveExample, scatterExample], scenes: sceneExamples, all: [calculatorExample, comparisonExample, budgetExample, curveExample, ...sceneExamples, bicycleExample, oscillatorExample, architectureExample, scatterExample] };
|
|
52
|
+
if (!Object.hasOwn(groups, topic)) throw new Error('Unknown ui_catalog topic; use overview, canvas, data, scenes or all');
|
|
53
|
+
return { build: BUILD_INFO, topic, protocol: PROTOCOL_GUIDE, components: componentCatalog(), design: topic === 'canvas' || topic === 'all' ? CANVAS_DESIGN_GUIDE : 'Choose composition by the question: editorial reading, technical relationships, analytical data, exploratory objects, aligned comparisons or ordered instruction. Keep theme, accessible labels and readable Chinese. Use topic=canvas for the full visual design guide.', examples: groups[topic] };
|
|
54
|
+
}
|
|
55
|
+
})));
|
|
56
|
+
ctx.effect(() => ctx.connection.fetch.register({ path: '/api/plugins/dsh-generative-ui/state', methods: ['GET', 'POST'], requestBody: 'buffered', fetch: stateRoute(store) }));
|
|
57
|
+
ctx.effect(() => ctx.connection.fetch.register({ path: '/api/plugins/dsh-generative-ui/document', methods: ['GET', 'POST'], requestBody: 'buffered', fetch: documentRoute(store) }));
|
|
58
|
+
ctx.effect(() => ctx.connection.fetch.register({ path: '/api/plugins/dsh-generative-ui/history', methods: ['GET', 'POST'], requestBody: 'buffered', fetch: historyRoute(store) }));
|
|
59
|
+
ctx.effect(()=>ctx.tools.register(defineTool({
|
|
60
|
+
name:'inspect_ui',description:'Read the current UI revision, saved state/derived and heuristic visual observations. Default summary omits source. Use detail=nodes with nodeIds to read exact nodes before code edits, or full only when needed. No report means unobserved, not a pass. Do not poll empty reports; automatic repair is at most two rounds per user request.',
|
|
61
|
+
parameters:{documentId:{type:'string',required:true},detail:{type:'string',enum:['summary','nodes','full']},nodeIds:{type:'json',description:'For nodes detail: up to 16 existing node IDs.'}},
|
|
62
|
+
output:{schema:{type:'json'},render:(_args,value)=>[{type:'text',text:JSON.stringify(value)}]},isConcurrencySafe:()=>true,
|
|
63
|
+
async execute({documentId,detail='summary',nodeIds=[]},exec){const record=await store.owned(documentId,exec.agent?.id);const saved=await store.readState(documentId,record.sessionId);return inspectRecord(record,saved.state,evaluate(record.document,saved.state).derived,await store.audits(documentId),{detail,nodeIds:toolJson(nodeIds,'nodeIds')});}
|
|
64
|
+
})));
|
|
65
|
+
ctx.effect(()=>ctx.tools.register(defineTool({name:'patch_ui',description:'Refine existing UI nodes without changing their IDs, structure, input bindings or actions. First inspect_ui for current documentRevision. changes is [{id,props}], with registered properties only. Replacing canvas html/css/script restarts that canvas; saved parameters and unaffected live nodes remain. Revision conflicts require inspect and retry. No partial scripts execute.',parameters:{documentId:{type:'string',required:true},documentRevision:{type:'number',required:true},changes:{type:'json',required:true}},output:{schema:{type:'json'},render:(_a,v)=>[{type:'text',text:`Updated ${v.document.title}; documentId=${v.documentId}; documentRevision=${v.documentRevision}. Saved parameters retained.`}],presentationMeta:(_a,v)=>({dshGenerativeUiPatch:v})},async execute({documentId,documentRevision,changes},exec){exec.signal.throwIfAborted();return store.refine(documentId,exec.agent?.id,documentRevision,toolJson(changes,'changes'));}})));
|
|
66
|
+
for (const [i,section] of UI_SKILL_SECTIONS.entries())ctx.effect(()=>ctx.systemPrompt.section({...section,text:()=>promptSections(settings().designMode??'guided')[i].text+(i===0&&settings().displayMode==='inline'?'\nPresentation mode: inline. After a successful render_ui/render_canvas, put a fenced dsh-ui block containing only its documentId in your answer at the best explanatory position. Never invent an ID or put source inside the marker. If omitted, the interface remains below the answer.':'')}));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// A small authoring language compiled to the existing interpreter; never eval.
|
|
2
|
+
export function compileExpression(source) {
|
|
3
|
+
const fail = () => { throw new Error('DSH UI: invalid formula; use state.x, data.x, derived.x, item.x, numbers, strings, + - * /, comparisons, ?: and min/max/round/abs'); };
|
|
4
|
+
if (typeof source !== 'string' || !source.trim() || source.length > 2048) fail();
|
|
5
|
+
const tokens = []; let offset = 0;
|
|
6
|
+
const token = /\s*(?:(\d+(?:\.\d*)?|\.\d+)([eE][+-]?\d+)?|("(?:[^"\\]|\\.)*")|([a-zA-Z][a-zA-Z0-9_]*(?:\.[a-zA-Z][a-zA-Z0-9_]*)?)|(===|!==|<=|>=|==|!=|[+*/()?,:<>-]))/y;
|
|
7
|
+
while (offset < source.length) {
|
|
8
|
+
if (!source.slice(offset).trim()) break;
|
|
9
|
+
token.lastIndex = offset; const m = token.exec(source); if (!m) fail();
|
|
10
|
+
offset = token.lastIndex;
|
|
11
|
+
tokens.push(m[1] ? { number: Number(m[1] + (m[2] ?? '')) } : m[3] ? { string: JSON.parse(m[3]) } : m[4] ?? m[5]);
|
|
12
|
+
if (tokens.length > 256) fail();
|
|
13
|
+
}
|
|
14
|
+
let pos = 0;
|
|
15
|
+
const operators = { '==': ['eq', 1], '===': ['eq', 1], '!=': ['neq', 1], '!==': ['neq', 1], '<': ['lt', 2], '<=': ['lte', 2], '>': ['gt', 2], '>=': ['gte', 2], '+': ['add', 3], '-': ['sub', 3], '*': ['mul', 4], '/': ['div', 4] };
|
|
16
|
+
const take = t => { if (tokens[pos++] !== t) fail(); };
|
|
17
|
+
function parse(min = 0, depth = 0) {
|
|
18
|
+
if (depth > 12) fail();
|
|
19
|
+
const t = tokens[pos++]; let out;
|
|
20
|
+
if (t && typeof t === 'object') out = { const: Object.hasOwn(t, 'number') ? t.number : t.string };
|
|
21
|
+
else if (t === 'true' || t === 'false') out = { const: t === 'true' };
|
|
22
|
+
else if (t === '(') { out = parse(0, depth + 1); take(')'); }
|
|
23
|
+
else if (t === '-' || t === '+') { const arg = parse(5, depth + 1); out = t === '-' ? { op: 'sub', args: [{ const: 0 }, arg] } : arg; }
|
|
24
|
+
else if (typeof t === 'string' && /^(state|data|derived|item)\.[a-zA-Z][a-zA-Z0-9_]*$/.test(t)) out = { ref: t };
|
|
25
|
+
else if (['min', 'max', 'round', 'abs'].includes(t)) {
|
|
26
|
+
take('('); const args = [parse(0, depth + 1)];
|
|
27
|
+
while (tokens[pos] === ',') { pos++; args.push(parse(0, depth + 1)); if (args.length > 8) fail(); }
|
|
28
|
+
take(')'); out = { op: t, args };
|
|
29
|
+
} else fail();
|
|
30
|
+
while (typeof tokens[pos]==='string' && Object.hasOwn(operators,tokens[pos]) && operators[tokens[pos]][1] >= min) {
|
|
31
|
+
const [op, precedence] = operators[tokens[pos++]];
|
|
32
|
+
out = { op, args: [out, parse(precedence + 1, depth + 1)] };
|
|
33
|
+
}
|
|
34
|
+
if (min === 0 && tokens[pos] === '?') {
|
|
35
|
+
pos++; const yes = parse(0, depth + 1); take(':'); out = { op: 'if', args: [out, yes, parse(0, depth + 1)] };
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
const result = parse(); if (pos !== tokens.length) fail(); return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function expandExpressions(doc) {
|
|
43
|
+
const expression = e => {
|
|
44
|
+
if (!e || typeof e !== 'object' || Array.isArray(e)) return e;
|
|
45
|
+
if (Object.hasOwn(e, 'expr')) {
|
|
46
|
+
if (Object.keys(e).length !== 1) throw new Error('DSH UI: expr must be the only formula field');
|
|
47
|
+
return compileExpression(e.expr);
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(e.args)) e.args = e.args.map(expression);
|
|
50
|
+
if (e.map?.fields) for (const k of Object.keys(e.map.fields)) e.map.fields[k] = expression(e.map.fields[k]);
|
|
51
|
+
return e;
|
|
52
|
+
};
|
|
53
|
+
if (doc.derived && typeof doc.derived === 'object') for (const k of Object.keys(doc.derived)) doc.derived[k] = expression(doc.derived[k]);
|
|
54
|
+
const walk = nodes => { if (!Array.isArray(nodes)) return; for (const node of nodes) {
|
|
55
|
+
if (!node || typeof node !== 'object') continue;
|
|
56
|
+
for (const field of ['text', 'value', 'max', 'rows']) if (Object.hasOwn(node, field)) node[field] = expression(node[field]);
|
|
57
|
+
walk(node.children);
|
|
58
|
+
} };
|
|
59
|
+
walk(doc.nodes); return doc;
|
|
60
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const CANVAS_DESIGN_GUIDE = `Design a useful interactive explanation, not a decorated tool card. Choose a visual plan before writing code; examples illustrate capabilities, not templates to copy.
|
|
2
|
+
1. Identify the user's question, the visual relationship and the one primary interaction. Reserve the largest region for the evidence or object being explored. Use a heading, a compact explanation and purposeful supporting details; don't repeat titles or put every paragraph inside another card.
|
|
3
|
+
2. Choose composition by task: mechanical explorer -> large SVG subject, continuous disassembly slider and part focus; parameter experiment -> visualization plus compact controls and exact values; architecture/process -> role-based nodes and clear labelled relationships; data analysis -> chart and exact table with units/source; comparison -> aligned criteria with one meaningful focus; reading/guide -> editorial text with a single supporting illustration. Combine native components and canvases where useful.
|
|
4
|
+
3. Choose visual treatment by content: editorial (reading, minimal borders), technical (geometry, restrained grid, annotations), analytical (quiet surface, aligned numerals), exploratory (large interactive object), comparative (aligned criteria), instructional (ordered explanation). Variation must change hierarchy and composition, not randomly swap accent colors. Keep host theme and Chinese readability. Limit accent areas; semantic warning/success colors describe established meaning, not decoration. Check foreground/background contrast in both themes; pale dark-theme accents need dark foregrounds.
|
|
5
|
+
4. Diagram grammar: distinguish hierarchy, flow, sequence, dependency and spatial assembly. Give each node one clear idea. Reserve space for labels before routing lines; avoid crossing text, ambiguity and clipped arrows. Prefer a focused overview plus details over dense spaghetti. Don't force every relation into identical cards. SVG labels and units must remain readable on mobile; use viewBox and responsive width. A diagram's numbers must not imply engineering or physical accuracy unless established by evidence.
|
|
6
|
+
5. Interaction: show an informative initial state; updates need visible feedback and matching explanation. Important inputs use document.state. Read the shared DSHUI snapshot instead of independently recalculating a host-derived result. Use DSHUI.subscribe to drive views, not to echo the same state back. Prefer a few labelled controls. Don't duplicate every native input inside the canvas. Make SVG hotspots keyboard-operable or provide equivalent labelled buttons.
|
|
7
|
+
6. Motion: animate the explanation, not every decoration. Prefer DSHUI.animate(({time,delta,reducedMotion,visible})=>{...}) and retain its cleanup callback. time/delta are seconds; integrate delta for pause/resume without jumps. The helper pauses offscreen/background, and supplies a static delta=0 frame for reduced motion. Keep reading and controls usable without animation; never save each animation frame to host state. Raw requestAnimationFrame remains available, but you own its lifecycle and reduced-motion handling.
|
|
8
|
+
7. Before submission mentally check: PC landscape at 1920px/1365px viewport and the actual narrower chat column first, then 768px/390px; light/dark contrast; long Chinese labels; no overflow or clipped annotations; keyboard labels; static/reduced-motion state; empty or zero values; truthful data provenance; useful canvas and document fallback. SVG viewBox scaling shrinks fonts too: verify labels are at least 12px at their actual rendered size, not merely in SVG units. On narrow displays move important labels/values into responsive HTML captions or native text rather than shrinking a desktop diagram. Avoid giant fixed minimum widths and viewport-height loops: the host measures content height. External fonts/images/imports are unavailable; use inline SVG and system fonts.
|
|
9
|
+
8. Keep the explanatory evidence, its control and the changing result together near the start. For a comparison of changes, show their relationship before optional decorative motion or lengthy derivations. Distinct units require distinct labelled axes or explicitly normalized ratios against the same baseline. Preserve meaningful reference points and units while varying parameters. Use transparent fills with explicit alpha (for SVG, fill-opacity/stop-opacity); do not assume a hex-only color helper also accepts rgb() or theme variables. Treat a clean first viewport as an information hierarchy goal, not a fixed height or a reason to shrink text.
|
|
10
|
+
Only use visual complexity that improves the answer. Standard prose remains appropriate for simple facts. Never claim tool measurements, source verification or completed actions from animation alone.`;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const bicycleExample = {
|
|
2
|
+
version: 1, title: '七速自行车 · 拆解与探索',
|
|
3
|
+
fallback: '交互式结构示意,不按工程比例绘制。拖动拆解程度,观察车架、车轮、传动、刹车和操控系统;选择部件查看职责。七速表示后部有七个可选齿比,此图未精确绘制七片飞轮或真实走线。',
|
|
4
|
+
presentation: { layout: 'guide', accent: 'teal' },
|
|
5
|
+
state: {
|
|
6
|
+
explode: { type: 'number', default: 0, min: 0, max: 100, step: 1 },
|
|
7
|
+
focus: { type: 'string', default: 'all', options: ['all', 'frame', 'wheels', 'drive', 'brakes', 'cockpit'] },
|
|
8
|
+
playing: { type: 'boolean', default: false }
|
|
9
|
+
},
|
|
10
|
+
derived: { phase: { op: 'if', args: [{ op: 'gt', args: [{ ref: 'state.explode' }, { const: 50 }] }, { const: '展开观察部件如何配合' }, { const: '从整车出发理解五个系统' }] } },
|
|
11
|
+
nodes: [
|
|
12
|
+
{ id: 'intro', type: 'text', text: '一辆车,五个协作系统。拖动滑块拆解结构,点击部件名称聚焦探索。示意图,不是工程装配图。' },
|
|
13
|
+
{ id: 'bike', type: 'sandbox', title: '自行车交互结构示意', height: 560,
|
|
14
|
+
fallback: '车架连接各系统并承载骑手;车轮支撑与滚动;传动将踩踏转为后轮扭矩;刹车通过摩擦减速;操控系统连接转向与骑手操作。动画仅展示运动关系,不模拟真实物理。',
|
|
15
|
+
stateKeys: ['explode', 'focus', 'playing'], derivedKeys: ['phase'],
|
|
16
|
+
html: `<div class="demo"><div class="eyebrow"><span>MECHANICAL EXPLORER</span><span>01 / BICYCLE</span></div><div class="stage"><svg viewBox="0 0 720 360" role="img" aria-labelledby="bikeTitle bikeDesc"><title id="bikeTitle">自行车拆解示意</title><desc id="bikeDesc">拖动下方拆解滑块,或选择部件名称。每个系统的职责在图下解释。</desc><defs><pattern id="grid" width="24" height="24" patternUnits="userSpaceOnUse"><circle cx="1" cy="1" r=".8" fill="currentColor" opacity=".12"/></pattern></defs><rect width="720" height="360" fill="url(#grid)"/><path class="ground" d="M70 323H650"/>
|
|
17
|
+
<g data-part="wheels"><g id="rearWheel"><circle cx="180" cy="250" r="69" class="tyre"/><circle cx="180" cy="250" r="61" class="rim"/><g class="spokes" data-wheel="rear"></g><circle cx="180" cy="250" r="6" class="hub"/></g><g id="frontWheel"><circle cx="540" cy="250" r="69" class="tyre"/><circle cx="540" cy="250" r="61" class="rim"/><g class="spokes" data-wheel="front"></g><circle cx="540" cy="250" r="6" class="hub"/></g></g>
|
|
18
|
+
<g data-part="frame" class="frame"><path d="M180 250L285 130L340 250Z M285 130L455 115L340 250 M455 115L469 150 M285 130L273 98"/><path d="M253 96L282 96" class="saddle"/></g>
|
|
19
|
+
<g data-part="drive" class="drive"><path d="M180 241L340 231A20 20 0 0 1 340 269L180 259Z" class="chain"/><circle cx="340" cy="250" r="18"/><circle cx="180" cy="250" r="13"/><path d="M340 250L365 276H380 M340 250L322 224H307"/><path d="M173 265L170 283L184 291"/></g>
|
|
20
|
+
<g data-part="cockpit" class="cockpit"><path d="M460 132L486 192L540 250 M454 111L446 85L476 76 M476 76L500 64L524 70"/><path d="M500 64L503 80" class="lever"/></g>
|
|
21
|
+
<g data-part="brakes" class="brakes"><path d="M159 181L171 175L183 181 M520 181L531 175L544 181"/><path d="M503 80Q540 110 531 175 M492 75Q335 25 171 175" class="cable"/></g>
|
|
22
|
+
<g class="partlabels"><text x="96" y="339">滚动</text><text x="310" y="38">承载 · 转向 · 传动</text><text x="568" y="339">协作</text></g></svg><span class="stage-note">结构与运动关系示意 · 非工程比例</span></div><div class="partnav" role="group" aria-label="选择系统"></div><div class="explanation" aria-live="polite"><span class="index">01—05</span><div><h2 id="partTitle">五个相互连接的系统</h2><p id="partText"></p></div></div><div class="foot"><span id="phase"></span><button id="play" type="button">播放轮转</button></div></div>`,
|
|
23
|
+
css: `.demo{padding:12px 2px;color:var(--canvas-ink)}.eyebrow,.foot{display:flex;justify-content:space-between;gap:12px;align-items:center;color:var(--canvas-muted);font-size:10px;letter-spacing:1.2px}.stage{position:relative;border-radius:16px;margin:18px 0;background:var(--canvas-soft);overflow:hidden}.stage svg{display:block;width:100%;color:var(--canvas-muted)}.ground{stroke:var(--canvas-line);fill:none;stroke-dasharray:3 5}.stage-note{position:absolute;bottom:10px;left:16px;font-size:10px;color:var(--canvas-muted)}[data-part]{transform-origin:center;transition:opacity .18s}.tyre{stroke:var(--canvas-ink);stroke-width:7;fill:none}.rim,.spokes,.hub{stroke:var(--canvas-muted);fill:none;stroke-width:1.2}.frame{stroke:var(--canvas-accent);stroke-width:9;stroke-linejoin:round;stroke-linecap:round;fill:none}.frame .saddle{stroke-width:11}.drive,.cockpit,.brakes{fill:none;stroke:var(--canvas-ink);stroke-width:4;stroke-linecap:round;stroke-linejoin:round}.chain,.cable{stroke-width:1.5}.cockpit{stroke-width:7}.lever{stroke-width:3}.brakes{stroke:var(--canvas-accent);stroke-width:4}.partlabels{font-size:10px;fill:var(--canvas-muted);letter-spacing:2px}.partnav{display:flex;gap:7px;flex-wrap:wrap}.partnav button,#play{border:1px solid var(--canvas-line);background:transparent;color:var(--canvas-muted);padding:5px 12px;border-radius:99px;cursor:pointer;font-size:12px}.partnav button[aria-pressed=true]{background:var(--canvas-accent);border-color:var(--canvas-accent);color:white}.explanation{display:flex;gap:18px;padding:23px 0 18px;min-height:114px}.index{font-size:11px;color:var(--canvas-accent);padding-top:5px;white-space:nowrap}.explanation h2{margin:0 0 7px;font-size:18px;font-weight:600}.explanation p{margin:0;color:var(--canvas-muted);font-size:13px;line-height:1.8}.foot{border-top:1px solid var(--canvas-line);padding-top:12px;font-size:11px;letter-spacing:0}@media(max-width:420px){.eyebrow{font-size:9px}.stage{margin:13px 0}.stage-note{font-size:8px;bottom:5px}.explanation{padding-top:18px}.explanation h2{font-size:16px}.partnav{gap:5px}.partnav button{padding:5px 9px}.foot{flex-wrap:wrap}}`,
|
|
24
|
+
script: `const parts={all:['全部','五个相互连接的系统','车架承载骑手,车轮负责滚动,传动把踩踏转为后轮扭矩。选择一个系统,观察它如何与整车配合。'],frame:['车架','车架:让各系统成为整车','三角形结构连接车轮、坐垫和转向部件,承受并传递骑手与路面的载荷。图中的位移只是拆解展示。'],wheels:['车轮','车轮:支撑与滚动','轮胎接触地面,轮圈与辐条连接轮毂。前轮随车把转向,后轮接受传动扭矩。'],drive:['传动','传动:把踩踏变为前进','曲柄通过链条驱动后轮。七个可选齿比改变踩踏阻力与每圈前进距离;本示意不精确绘制七片飞轮。'],brakes:['刹车','刹车:用摩擦控制速度','刹把经拉线控制制动机构。这里用简化的轮圈刹车表示,实际走线与装配位置因车型而异。'],cockpit:['操控','操控:把意图传给车辆','车把、把立和前叉组成转向链路,骑手通过车把和刹把控制方向与速度。']};
|
|
25
|
+
const nav=document.querySelector('.partnav');for(const [key,p]of Object.entries(parts)){const b=document.createElement('button');b.type='button';b.textContent=p[0];b.dataset.focus=key;b.onclick=()=>DSHUI.setState({focus:key});nav.append(b)}
|
|
26
|
+
for(const [key,x]of [['rear',180],['front',540]]){const g=document.querySelector('[data-wheel='+key+']');for(let i=0;i<16;i++){const a=i*Math.PI/8,l=document.createElementNS('http://www.w3.org/2000/svg','line');for(const [k,v]of Object.entries({x1:x,y1:250,x2:x+60*Math.cos(a),y2:250+60*Math.sin(a)}))l.setAttribute(k,v);g.append(l)}}
|
|
27
|
+
let current=DSHUI.snapshot,angle=0;
|
|
28
|
+
DSHUI.subscribe(s=>{current=s;const e=s.state.explode/100,f=s.state.focus;for(const g of document.querySelectorAll('[data-part]')){const p=g.dataset.part;g.style.opacity=f==='all'||f===p?'1':'.16';if(p==='wheels'){document.getElementById('rearWheel').setAttribute('transform','translate('+(-48*e)+' '+(15*e)+')');document.getElementById('frontWheel').setAttribute('transform','translate('+(48*e)+' '+(15*e)+')')}else{const v={frame:[0,-18],drive:[0,52],brakes:[18,-45],cockpit:[22,-55]}[p];g.setAttribute('transform','translate('+(v[0]*e)+' '+(v[1]*e)+')')}}for(const b of nav.children){b.setAttribute('aria-pressed',String(b.dataset.focus===f));b.disabled=s.disabled}document.getElementById('partTitle').textContent=parts[f][1];document.getElementById('partText').textContent=parts[f][2];document.getElementById('phase').textContent=s.derived.phase;document.getElementById('play').textContent=s.state.playing?'暂停轮转':'播放轮转';document.getElementById('play').disabled=s.disabled});
|
|
29
|
+
document.getElementById('play').onclick=()=>DSHUI.setState({playing:!current.state.playing});
|
|
30
|
+
DSHUI.animate(({delta})=>{if(current.state.playing&&!current.disabled){angle+=delta*90;for(const [key,x]of [['rear',180],['front',540]])document.querySelector('[data-wheel='+key+']').setAttribute('transform','rotate('+angle+' '+x+' 250)')}});`
|
|
31
|
+
},
|
|
32
|
+
{ id: 'explodeControl', type: 'slider', label: '拆解程度 %', bind: 'state.explode' },
|
|
33
|
+
{ id: 'focusControl', type: 'select', label: '聚焦系统(与图内按钮同步)', bind: 'state.focus' },
|
|
34
|
+
{ id: 'rotation', type: 'toggle', label: '播放示意轮转(非物理仿真)', bind: 'state.playing' },
|
|
35
|
+
{ id: 'phaseText', type: 'text', text: { ref: 'derived.phase' } }
|
|
36
|
+
]
|
|
37
|
+
};
|
|
38
|
+
export const oscillatorExample = {
|
|
39
|
+
version: 1, title: '正弦运动 · 参数实验', fallback: '数学动画示意:位置 x = A sin(2πft)。振幅 A 与频率 f 可调;不是弹簧物理仿真。刷新保留参数,动画时间重新开始。',
|
|
40
|
+
state: { amplitude: { type: 'number', default: 90, min: 10, max: 150, step: 10 }, frequency: { type: 'number', default: 1, min: 0.25, max: 2, step: 0.25 } },
|
|
41
|
+
nodes: [{ id: 'wave', type: 'sandbox', title: '可调正弦运动', height: 260, fallback: '圆点沿水平轴做正弦运动。振幅决定最大偏移,频率决定每秒往复次数;减少动态效果开启时显示静止示意。', stateKeys: ['amplitude', 'frequency'],
|
|
42
|
+
html: '<div class="lab"><p>POSITION / TIME</p><svg viewBox="0 0 500 150" role="img" aria-label="圆点沿水平轴正弦运动"><path d="M50 75H450"/><path d="M250 60V90"/><circle id="mass" cx="250" cy="75" r="18"/></svg><output id="equation"></output></div>',
|
|
43
|
+
css: '.lab{padding:20px;background:var(--canvas-soft);border-radius:14px}.lab p{font-size:10px;letter-spacing:2px;color:var(--canvas-muted);margin:0}.lab svg{width:100%;display:block}.lab path{stroke:var(--canvas-line);fill:none}.lab circle{fill:var(--canvas-accent)}.lab output{font-size:13px;font-variant-numeric:tabular-nums}',
|
|
44
|
+
script: "let s=DSHUI.snapshot;DSHUI.subscribe(v=>{s=v;document.getElementById('equation').textContent='x = '+s.state.amplitude+' sin(2π × '+s.state.frequency+' × t)'});DSHUI.animate(({time,reducedMotion})=>{const x=reducedMotion?0:s.state.amplitude*Math.sin(2*Math.PI*s.state.frequency*time);document.getElementById('mass').setAttribute('cx',250+x)});"
|
|
45
|
+
}, { id: 'amplitude', type: 'slider', label: '振幅 A(示意坐标单位)', bind: 'state.amplitude' }, { id: 'frequency', type: 'slider', label: '频率 f(Hz)', bind: 'state.frequency' }]
|
|
46
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import{EXPORT_RUNTIME}from'./export-runtime-bundle.js';
|
|
2
|
+
import{validateState}from'./protocol.js';
|
|
3
|
+
export function standaloneCanvas(document,node,state,theme={}){
|
|
4
|
+
validateState(document,state);
|
|
5
|
+
const token=Array.from(crypto.getRandomValues(new Uint8Array(16)),b=>b.toString(16).padStart(2,'0')).join('');
|
|
6
|
+
const json=JSON.stringify({document,nodeId:node.id,state,theme,nonce:token}).replace(/</g,'\\u003c');
|
|
7
|
+
const escape=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
8
|
+
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'none';script-src 'nonce-${token}';style-src 'unsafe-inline';frame-src 'self';base-uri 'none';form-action 'none'"><title>${escape(node.title)}</title><style>body{margin:0;padding:24px;background:${theme.scheme==='dark'?'#161b23':'#f4f6fa'};color:${theme.scheme==='dark'?'#e6edf6':'#182537'};font:14px/1.6 system-ui,sans-serif}main{max-width:1100px;margin:auto}h1{font-size:22px}p{overflow-wrap:anywhere}#surface{background:${theme.scheme==='dark'?'#161b23':'#fff'};border-radius:12px;overflow:hidden}#status{min-height:1.6em}</style></head><body><main><h1>${escape(node.title)}</h1><p>DSH Motion UI · 独立副本。包含导出时参数,本地交互不会保存到 DSH,也不会调用 Agent。</p><div id="surface"></div><p id="status" role="status"></p><details><summary>文字说明</summary><p>${escape(node.fallback)}</p></details></main><script type="application/json" id="dsh-ui-export">${json}</script><script nonce="${token}">${EXPORT_RUNTIME.replace(/<\/script/gi,'<\\/script')}</script></body></html>`;
|
|
9
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { CANVAS_SYSTEM_CSS } from './canvas-system.js';
|
|
2
|
+
// Executed only as trusted bootstrap code inside an opaque-origin preview frame.
|
|
3
|
+
export function updatePreview(html) {
|
|
4
|
+
const template=document.createElement('template');template.innerHTML=html;
|
|
5
|
+
const tags=new Set('div main section article header footer aside nav p span strong em b i small br hr h1 h2 h3 h4 ul ol li dl dt dd figure figcaption table caption thead tbody tfoot tr th td button label input output select option textarea svg g defs marker path rect circle ellipse line polyline polygon text tspan title desc linearGradient radialGradient stop clipPath use'.toLowerCase().split(' '));
|
|
6
|
+
for(const el of [...template.content.querySelectorAll('*')]){
|
|
7
|
+
if(!tags.has(el.localName.toLowerCase())){el.remove();continue;}
|
|
8
|
+
for(const attr of [...el.attributes]){
|
|
9
|
+
const name=attr.name.toLowerCase();
|
|
10
|
+
if(!/^(id|class|style|role|tabindex|type|value|min|max|step|for|disabled|checked|selected|colspan|rowspan|scope|viewbox|width|height|x|y|x1|x2|y1|y2|cx|cy|r|rx|ry|d|points|fill|fill-opacity|stroke|stroke-width|stroke-dasharray|stroke-linecap|stroke-linejoin|opacity|transform|text-anchor|dominant-baseline|font-size|font-weight|marker-end|marker-start|markerwidth|markerheight|refx|refy|orient|offset|stop-color|stop-opacity|clip-path|gradientunits|gradienttransform|preserveaspectratio|aria-[a-z-]+|data-[a-z-]+)$/.test(name))el.removeAttribute(attr.name);
|
|
11
|
+
}
|
|
12
|
+
if(el.matches('button,input,select,textarea'))el.setAttribute('disabled','');
|
|
13
|
+
}
|
|
14
|
+
function morph(parent,next){
|
|
15
|
+
const desired=[...next.childNodes];
|
|
16
|
+
desired.forEach((node,i)=>{
|
|
17
|
+
const old=parent.childNodes[i];
|
|
18
|
+
if(!old){parent.append(node.cloneNode(true));return;}
|
|
19
|
+
if(old.nodeType!==node.nodeType||old.nodeName!==node.nodeName){old.replaceWith(node.cloneNode(true));return;}
|
|
20
|
+
if(node.nodeType!==1){if(old.textContent!==node.textContent)old.textContent=node.textContent;return;}
|
|
21
|
+
for(const attr of [...old.attributes])if(!node.hasAttribute(attr.name))old.removeAttribute(attr.name);
|
|
22
|
+
for(const attr of [...node.attributes])if(old.getAttribute(attr.name)!==attr.value)old.setAttribute(attr.name,attr.value);
|
|
23
|
+
morph(old,node);
|
|
24
|
+
});
|
|
25
|
+
while(parent.childNodes.length>desired.length)parent.lastChild.remove();
|
|
26
|
+
}
|
|
27
|
+
morph(document.getElementById('canvas'),template.content);
|
|
28
|
+
}
|
|
29
|
+
export function previewFrameDocument(channel,nonce){
|
|
30
|
+
return `<!doctype html><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none';script-src 'nonce-${nonce}';style-src 'unsafe-inline';img-src data:;base-uri 'none';form-action 'none';frame-src 'none'"><style>${CANVAS_SYSTEM_CSS}</style><style id="custom"></style><style>#canvas,#canvas *,#canvas *::before,#canvas *::after{animation:none!important;transition:none!important}html,body{overflow:hidden}</style><main id="canvas" inert></main><script nonce="${nonce}">const update=${updatePreview.toString()};addEventListener('message',e=>{if(e.source!==parent||e.data?.channel!==${JSON.stringify(channel)})return;document.getElementById('custom').textContent=e.data.css;for(const [k,v] of Object.entries(e.data.theme||{}))document.documentElement.style.setProperty('--canvas-'+k,v);document.documentElement.dataset.canvasScheme=e.data.theme?.scheme||'light';update(e.data.html)});</script>`;
|
|
31
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Optional primitives for model-authored surfaces. Host theme remains authoritative.
|
|
2
|
+
export const CANVAS_SYSTEM_CSS = `
|
|
3
|
+
:root{--canvas-radius:12px;--canvas-gap:16px;--canvas-positive:#167456;--canvas-negative:#b13b3b;--canvas-warning:#875b15}
|
|
4
|
+
:root[data-canvas-scheme=dark]{--canvas-positive:#78d9b4;--canvas-negative:#ffaaaa;--canvas-warning:#edc87e}
|
|
5
|
+
html,body{margin:0;min-width:0;background:var(--canvas-bg,#fff);color:var(--canvas-ink,#182537);font:14px/1.6 system-ui,"Microsoft YaHei",sans-serif}*{box-sizing:border-box}svg{max-width:100%;height:auto}button,input,select,textarea{font:inherit;color:inherit}button,input,select,textarea{border:1px solid var(--canvas-line,#ced4df);border-radius:8px;background:var(--canvas-bg,#fff);padding:8px 12px;max-width:100%}button{cursor:pointer}button:disabled{cursor:default;opacity:.55}input[type=range]{padding:0;accent-color:var(--canvas-accent,#3e60d3)}input[type=checkbox],input[type=radio]{accent-color:var(--canvas-accent,#3e60d3)}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--canvas-accent,#3e60d3);outline-offset:3px}
|
|
6
|
+
.ui-stack{display:grid;gap:var(--canvas-gap)}.ui-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:var(--canvas-gap)}.ui-surface{padding:20px;border:1px solid var(--canvas-line,#ced4df);border-radius:var(--canvas-radius);background:var(--canvas-soft,#f3f5f8)}.ui-title{margin:0;font-size:clamp(20px,3vw,28px);line-height:1.3;letter-spacing:-.02em}.ui-caption{color:var(--canvas-muted,#526176);margin:8px 0;font-size:14px}.ui-value{font-size:clamp(24px,4vw,36px);font-variant-numeric:tabular-nums;line-height:1.2}.ui-controls{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.ui-field{display:grid;gap:6px;min-width:0}.ui-legend{display:flex;gap:16px;flex-wrap:wrap;font-size:13px}.ui-status{min-height:1.6em}.ui-error{color:var(--canvas-negative)}.ui-primary{background:var(--canvas-accent,#3e60d3);color:var(--canvas-accent-ink,#fff);border-color:transparent}
|
|
7
|
+
svg .ui-node{fill:var(--canvas-soft,#f3f5f8);stroke:var(--canvas-line,#ced4df);stroke-width:1}svg .ui-edge{fill:none;stroke:var(--canvas-muted,#526176);stroke-width:1.5}svg .ui-label{fill:var(--canvas-ink,#182537);font:500 14px system-ui,sans-serif}svg .ui-note{fill:var(--canvas-muted,#526176);font:400 13px system-ui,sans-serif}svg .ui-leader{fill:none;stroke:var(--canvas-muted,#526176);stroke-dasharray:3 4}svg .ui-accent{fill:color-mix(in srgb,var(--canvas-accent,#3e60d3) 12%,var(--canvas-bg,#fff));stroke:var(--canvas-accent,#3e60d3)}
|
|
8
|
+
@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}`;
|
|
9
|
+
|
|
10
|
+
export const CANVAS_SYSTEM_GUIDE = `The canvas includes optional .ui-stack, .ui-grid, .ui-surface, .ui-title, .ui-caption, .ui-value, .ui-controls, .ui-field, .ui-legend, .ui-status, .ui-error and .ui-primary classes. SVG helpers: .ui-node (shape), .ui-edge (unfilled connector), .ui-label (main text), .ui-note (annotation), .ui-leader (dashed annotation line), .ui-accent (theme-aware accent shape). Use them as building blocks, or override with your own composition; they do not prescribe cards or a fixed layout. Theme variables --canvas-bg/soft/ink/muted/line/accent follow DSH. Define --canvas-accent-ink yourself if overriding the accent with a pale color.
|
|
11
|
+
SVG construction: reserve label space first; center box labels with text-anchor=middle and dominant-baseline=central. Measure Chinese characters at roughly one font-size each, Latin letters about .6 font-size; mixed text needs combined width plus 24px padding on each side. These are estimates, not browser measurements. Give same-role nodes aligned dimensions. Route connectors around text and unrelated boxes, with explicit fill=none and visible arrowheads. Compute viewBox bounds from the furthest shape, label and arrow plus padding. A 680-unit diagram shown at 340px halves its fonts: reflow or move captions into HTML instead of shrinking important labels. Keep PC chat-column labels readable at actual rendered size. Use overview + focus detail for dense systems. Do not force this grammar on spatial/mechanical drawings.
|
|
12
|
+
For calculators validate blanks, finite values, ranges and zero denominators; show an actionable error, never a stale result as valid. Use one state source for filtering/sorting/reset and one animation loop with cleanup. A reset restores inputs and outputs together. For explanation show relationships and one useful variable; for comparison expose criteria/units/assumptions; for a tool provide working inputs, calculation, outputs and reset.
|
|
13
|
+
For fastest render_canvas preview emit title, fallback, state/data/derived if needed, height, css, html, script in that order. Keep CSS small; put a useful static initial view in html. Preview is non-interactive and scripts only run after successful commit and state restoration. Source order is a delivery optimization, not a thinking limit. Do not claim a preview is a working or saved result.
|
|
14
|
+
Small building example (adapt relationships and dimensions to the actual task, not a compulsory template): <svg viewBox="0 0 420 120" role="img" aria-label="输入转为输出"><path class="ui-edge" d="M160 60H260"/><path class="ui-edge" d="M250 54L260 60L250 66"/><rect class="ui-node" x="20" y="36" width="140" height="48" rx="8"/><text class="ui-label" x="90" y="60" text-anchor="middle" dominant-baseline="central">输入</text><rect class="ui-node ui-accent" x="260" y="36" width="140" height="48" rx="8"/><text class="ui-label" x="330" y="60" text-anchor="middle" dominant-baseline="central">输出</text></svg>. Use SVG label text only; explanatory paragraphs belong in responsive HTML. For a reset button connect addEventListener('click',()=>DSHUI.setState({yourDeclaredKey:yourDefault})); render results from DSHUI.subscribe(s=>...) rather than independently duplicating host-derived formulas.`;
|