dsh-motion-ui 0.5.2 → 0.5.4

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/lib/protocol.js CHANGED
@@ -3,11 +3,11 @@ import { SANDBOX_LIMITS } from './sandbox.js';
3
3
  import { COMPONENTS, componentFields } from './components.js';
4
4
  import { formatNumber } from './format-number.js';
5
5
  import { expandExpressions } from './author-expression.js';
6
+ import { validIdentifier as key } from './identifiers.js';
6
7
  export const LIMITS = Object.freeze({ bytes: 131072, nodes: 160, depth: 12, rows: 500, expressions: 128, steps: 4096 });
7
8
  const own = (o, k) => Object.hasOwn(o, k);
8
9
  const fail = message => { throw new Error(`DSH UI: ${message}`); };
9
10
  const object = x => x !== null && typeof x === 'object' && !Array.isArray(x);
10
- const key = x => typeof x === 'string' && /^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(x) && !['constructor', 'prototype', '__proto__'].includes(x);
11
11
  const finite = x => typeof x === 'number' && Number.isFinite(x);
12
12
  const scalar = x => typeof x === 'string' || typeof x === 'boolean' || finite(x);
13
13
  const text = (x, at) => { if (typeof x !== 'string' || x.length > 8192) fail(`${at} must be a string of at most 8192 characters`); };
@@ -41,6 +41,10 @@ export function validateState(doc, state) {
41
41
  if (!finite(v) || v < s.min || v > s.max) fail(`state.${k} is outside its numeric range`);
42
42
  const offset = (v - s.min) / s.step;
43
43
  if (Math.abs(offset - Math.round(offset)) > 1e-6) fail(`state.${k} does not match its step`);
44
+ } else if (s.type === 'json') {
45
+ if (!object(v) && !Array.isArray(v)) fail(`state.${k} must be an array or object for type json`);
46
+ safeJson(v);
47
+ if (new TextEncoder().encode(JSON.stringify(v)).length > 16384) fail(`state.${k} JSON exceeds 16 KiB`);
44
48
  } else if (s.type === 'string') text(v, `state.${k}`);
45
49
  else if (typeof v !== 'boolean') fail(`state.${k} must be boolean`);
46
50
  if (s.options && !s.options.includes(v)) fail(`invalid option for state.${k}`);
@@ -50,7 +54,7 @@ export function validateState(doc, state) {
50
54
  }
51
55
  const ARITY = { add: [1, 8], sub: [2, 2], mul: [1, 8], div: [2, 2], min: [1, 8], max: [1, 8], round: [1, 1], abs: [1, 1], eq: [2, 2], neq: [2, 2], lt: [2, 2], lte: [2, 2], gt: [2, 2], gte: [2, 2], if: [3, 3] };
52
56
  function reference(doc, ref) {
53
- if (typeof ref !== 'string' || !/^(state|data|derived)\.[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(ref)) fail('invalid reference');
57
+ if (typeof ref !== 'string' || !['state','data','derived'].includes(ref.split('.')[0]) || ref.split('.').length !== 2 || !key(ref.split('.')[1])) fail(`invalid reference ${JSON.stringify(ref)}; use state.key, data.key or derived.key with a letter/underscore identifier`);
54
58
  const [group, k] = ref.split('.');
55
59
  if (!own(doc[group], k)) fail(`unknown reference ${ref}`);
56
60
  return [group, k];
@@ -118,7 +122,7 @@ function checkViewValues(doc, evaluated) {
118
122
  if (n.type === 'chart' && rows.some(r => !own(r, n.labelKey) || !scalar(r[n.labelKey]) || !own(r, n.valueKey) || !finite(r[n.valueKey]))) fail('chart requires labels and finite numeric values');
119
123
  if (n.type === 'chart' && n.xScale === 'linear' && rows.some(r => !finite(r[n.labelKey]))) fail('linear chart requires numeric x values');
120
124
  }
121
- if (n.type === 'metric' && !finite(evaluated.resolveValue(n.value))) fail('metric requires a numeric value');
125
+ if (n.type === 'metric' && !scalar(evaluated.resolveValue(n.value))) fail(`node.${n.id}.value must resolve to a number, string or boolean; use table for rows or a canvas for structured data`);
122
126
  if (n.type === 'progress') {
123
127
  const v = evaluated.resolveValue(n.value), max = evaluated.resolveValue(n.max);
124
128
  if (!finite(v) || !finite(max) || max <= 0 || v < 0 || v > max) fail('progress requires 0 <= value <= positive max');
@@ -144,11 +148,14 @@ export function validateDocument(input) {
144
148
  for (const group of ['state', 'data', 'derived', 'actions']) {
145
149
  doc[group] ??= {};
146
150
  if (!object(doc[group]) || Object.keys(doc[group]).length > 64) fail(`invalid ${group}`);
147
- for (const k of Object.keys(doc[group])) if (!key(k)) fail(`invalid key in ${group}`);
151
+ for (const k of Object.keys(doc[group])) if (!key(k)) fail(`invalid key in ${group}: ${JSON.stringify(k)}; use 1..64 letters, numbers or underscores, starting with a letter/underscore (Chinese letters supported); no spaces, dots or hyphens. Put display labels in label/text`);
148
152
  }
149
153
  for (const [k, s] of Object.entries(doc.state)) {
150
154
  fields(s, ['type', 'default', 'min', 'max', 'step', 'options'], `state.${k}`);
151
- if (!['number', 'string', 'boolean'].includes(s.type)) fail(`unsupported state type ${k}`);
155
+ // Preserve structured checklist data instead of coercing it to a string.
156
+ if (s.type === 'string' && (Array.isArray(s.default) || object(s.default)) && !own(s,'options')) s.type = 'json';
157
+ if (!['number', 'string', 'boolean', 'json'].includes(s.type)) fail(`unsupported state type ${k}; use number, string, boolean or json (arrays/objects)`);
158
+ if (s.type === 'json' && own(s,'options')) fail(`state.${k}: json state does not support select options; use scalar options or a canvas`);
152
159
  if (s.type === 'number') {
153
160
  if (![s.min, s.max, s.step].every(finite) || s.min >= s.max || s.step <= 0) fail(`invalid numeric limits for ${k}`);
154
161
  if ((s.max - s.min) / s.step > 1e9) fail(`numeric step too small for ${k}`);
@@ -168,7 +175,7 @@ export function validateDocument(input) {
168
175
  const [, k] = reference(doc, e.map.source), rows = doc.data[k];
169
176
  if (!Array.isArray(rows) || rows.length > LIMITS.rows || !rows.every(object)) fail('map source must be object rows');
170
177
  if (!object(e.map.fields) || !Object.keys(e.map.fields).length || Object.keys(e.map.fields).length > 16) fail('map requires 1..16 fields');
171
- for (const [k, value] of Object.entries(e.map.fields)) { if (!key(k)) fail(`invalid map field ${JSON.stringify(k).slice(0,100)}; use an ASCII identifier matching [a-zA-Z][a-zA-Z0-9_]{0,63}, such as powerW. Put Chinese display text in column/chart labels, not internal field keys`); validateExpression(value, depth + 1, rows); }
178
+ for (const [k, value] of Object.entries(e.map.fields)) { if (!key(k)) fail(`invalid map field ${JSON.stringify(k).slice(0,100)}; use a letter/underscore identifier such as powerW or 功率, without spaces, dots or hyphens`); validateExpression(value, depth + 1, rows); }
172
179
  return;
173
180
  }
174
181
  if (own(e, 'const')) { fields(e, ['const'], 'expression'); if (!scalar(e.const)) fail('constant must be scalar'); return; }
@@ -208,6 +215,7 @@ export function validateDocument(input) {
208
215
  if (++count > LIMITS.nodes) fail('too many nodes');
209
216
  if (!object(n) || !key(n.id) || ids.has(n.id)) fail('node ids must be unique identifiers'); ids.add(n.id);
210
217
  if (!Object.hasOwn(COMPONENTS, n.type)) fail('unsupported component ' + n.type);
218
+ if (['table','chart'].includes(n.type) && typeof n.rows === 'string' && /^(data|derived)\./.test(n.rows)) n.rows={ref:n.rows};
211
219
  switch (n.type) {
212
220
  case 'sandbox': {
213
221
 
@@ -240,17 +248,19 @@ export function validateDocument(input) {
240
248
  const [, k] = reference(doc, n.bind), spec = doc.state[k];
241
249
  if (n.type === 'slider' && spec.type !== 'number') fail('slider requires numeric state');
242
250
  if (n.type === 'toggle' && spec.type !== 'boolean') fail('toggle requires boolean state');
251
+ if (['input','select'].includes(n.type) && spec.type === 'json') fail(`node.${n.id}: structured state requires a sandbox with DSHUI.setState; use separate boolean states and toggle nodes for a native checklist`);
243
252
  if (n.type === 'input' && spec.type === 'boolean') fail('use toggle for boolean state');
244
253
  if (n.type === 'select' && !spec.options) fail('select requires options'); break;
245
254
  }
246
255
  case 'button': text(n.text, 'button text'); if (!own(doc.actions, n.action)) fail('unknown action'); break;
247
256
  case 'table': {
248
- value(n.rows, 'table rows');
257
+ if (n.label !== undefined) text(n.label, 'table label');
258
+ if (!Array.isArray(n.rows)) value(n.rows, 'table rows');
249
259
  if (!Array.isArray(n.columns) || !n.columns.length || n.columns.length > 16) fail('invalid table columns');
250
260
  const cols = new Set();
251
261
  for (const c of n.columns) { fields(c, ['key', 'label', 'unit'], 'column'); if (!key(c.key) || cols.has(c.key)) fail('invalid or duplicate column key'); cols.add(c.key); text(c.label, 'column label'); if (c.unit !== undefined) text(c.unit, 'column unit'); } break;
252
262
  }
253
- case 'chart': value(n.rows, 'chart rows'); if (!['bar', 'line'].includes(n.kind) || !key(n.labelKey) || !key(n.valueKey)) fail('invalid chart'); text(n.label, 'chart label'); if (n.unit !== undefined) text(n.unit, 'chart unit'); if (n.xScale !== undefined && (!['category', 'linear'].includes(n.xScale) || n.xScale === 'linear' && n.kind !== 'line')) fail('invalid xScale'); break;
263
+ case 'chart': if (!Array.isArray(n.rows)) value(n.rows, 'chart rows'); if (!['bar', 'line'].includes(n.kind) || !key(n.labelKey) || !key(n.valueKey)) fail('invalid chart'); text(n.label, 'chart label'); if (n.unit !== undefined) text(n.unit, 'chart unit'); if (n.xScale !== undefined && (!['category', 'linear'].includes(n.xScale) || n.xScale === 'linear' && n.kind !== 'line')) fail('invalid xScale'); break;
254
264
  default: fail(`unsupported component ${n.type}`);
255
265
  }
256
266
  if (['heading', 'text', 'card', 'metric', 'button', 'badge', 'progress', 'details'].includes(n.type)) {
@@ -283,8 +293,8 @@ export function fallbackText(doc) {
283
293
  if ((n.type === 'card' || n.type === 'details') && n.title) lines.push(n.title);
284
294
  if (n.type === 'progress') lines.push(`${n.label}: ${resolveValue(n.value)} / ${resolveValue(n.max)} ${n.unit ?? ''}`);
285
295
  if (n.type === 'steps') for (const item of n.items) lines.push(`[${item.status}] ${item.title}${item.description ? ': ' + item.description : ''}`);
286
- if (n.type === 'metric') lines.push(`${n.label}: ${formatNumber(resolveValue(n.value),n.decimals ?? 2)} ${n.unit ?? ''}`);
287
- if (n.type === 'table') { lines.push(n.columns.map(c => c.label + (c.unit ? ` (${c.unit})` : '')).join(' | ')); for (const row of resolveValue(n.rows)) lines.push(n.columns.map(c => Object.hasOwn(row, c.key) ? String(row[c.key]) : '—').join(' | ')); }
296
+ if (n.type === 'metric') { const v=resolveValue(n.value); lines.push(`${n.label}: ${typeof v === 'number' ? formatNumber(v,n.decimals ?? 2) : String(v)} ${n.unit ?? ''}`); }
297
+ if (n.type === 'table') { if(n.label)lines.push(n.label); lines.push(n.columns.map(c => c.label + (c.unit ? ` (${c.unit})` : '')).join(' | ')); for (const row of resolveValue(n.rows)) lines.push(n.columns.map(c => Object.hasOwn(row, c.key) ? String(row[c.key]) : '—').join(' | ')); }
288
298
  if (n.type === 'chart') { lines.push(n.label + (n.unit ? ` (${n.unit})` : '')); for (const r of resolveValue(n.rows)) lines.push(`${r[n.labelKey]}: ${r[n.valueKey]}`); }
289
299
  if (n.children) walk(n.children);
290
300
  } }
@@ -0,0 +1,8 @@
1
+ // Small authored starting points. Static documents need no state or script.
2
+ export const QUICK_GUIDE=`轻量可视化优先:先用一幅静态图、短流程、对齐对比或少量指标讲清楚;只有参数变化/选择本身有价值才加控件。默认不写 script、不建 state、不加动画。布局用宿主组件或已有 ui-* 类,不自建主题和绘图引擎。流程 3–5 步,架构 3–6 节点,比较 2–3 方案,每项 1–2 句;这些是起点,不是截断用户必要内容的硬限制。中性背景、细边框、一种强调色、清楚的标题与直接标注;不用阴影、发光或嵌套卡片装饰。复杂细节按用户后续问题扩展。`;
3
+ export const QUICK_EXAMPLES=[
4
+ {version:1,title:'发布流程',fallback:'建议流程:开发完成后先测试,通过后发布;这是规划,尚未执行。',presentation:{layout:'guide',density:'compact',accent:'teal'},nodes:[{id:'flow',type:'steps',items:[{title:'开发',description:'完成核心功能',status:'pending'},{title:'测试',description:'检查功能与边界',status:'pending'},{title:'发布',description:'交付并观察反馈',status:'pending'}]}]},
5
+ {version:1,title:'两种协作方式',fallback:'示例:表格便于记录,任务看板便于追踪流转;按工作方式选择。',presentation:{layout:'comparison',density:'compact',accent:'blue'},nodes:[{id:'choices',type:'row',children:[{id:'sheet',type:'card',title:'表格',children:[{id:'sheetText',type:'text',text:'适合清单与结构化记录。多人更新时需要约定字段。'}]},{id:'board',type:'card',title:'任务看板',children:[{id:'boardText',type:'text',text:'适合追踪任务阶段。复杂依赖需补充说明。'}]}]}]},
6
+ {version:1,title:'请求怎样到达数据',fallback:'简化架构:浏览器将请求交给服务,服务访问数据库后返回结果。',nodes:[{id:'diagram',type:'sandbox',title:'三层架构',height:180,html:'<div class="ui-flow" aria-label="浏览器到服务到数据库"><div class="ui-step"><strong>浏览器</strong><span>发起请求</span></div><span class="ui-arrow" aria-hidden="true">→</span><div class="ui-step"><strong>服务</strong><span>处理业务</span></div><span class="ui-arrow" aria-hidden="true">→</span><div class="ui-step"><strong>数据库</strong><span>保存与查询</span></div></div>',fallback:'浏览器 → 服务 → 数据库。简化架构示意。'}]},
7
+ {version:1,title:'本周概览',fallback:'示例数据:本周新增任务 12 项,已完成 9 项。',presentation:{layout:'dashboard',density:'compact',accent:'teal'},nodes:[{id:'metrics',type:'row',children:[{id:'new',type:'metric',label:'新增任务',value:12,unit:'项',decimals:0},{id:'done',type:'metric',label:'已完成',value:9,unit:'项',decimals:0}]},{id:'note',type:'text',text:'示例数据,仅展示看板排版。'}]}
8
+ ];
package/lib/stream.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { LIMITS, validateDocument } from './protocol.js';
2
+ import { toolJson } from './tool-json.js';
2
3
 
3
4
  // Parse complete JSON tokens only. Never finish a string, number or model expression.
4
5
  // Partial containers retain completed members, but only the document wrapper and
@@ -62,7 +63,7 @@ export function previewDocument(raw) {
62
63
  try {
63
64
  const root=parseToolArguments(raw);if(!root)return null;
64
65
  const document = root.members?.get('document');
65
- if(document?.complete&&typeof document.value==='string'&&root.members.size===1)return validateDocument(JSON.parse(document.value));
66
+ if(document?.complete&&typeof document.value==='string'&&root.members.size===1)return validateDocument(toolJson(document.value,'document'));
66
67
  if (!document?.members) return null;
67
68
  if ([...root.members.keys()].some(k => k !== 'document')) return null;
68
69
  const doc = { ...document.value }, nodes = document.members.get('nodes');
package/lib/tool-json.js CHANGED
@@ -1,7 +1,26 @@
1
1
  // DSH adapters may represent `type: json` fields as JSON strings.
2
2
  // Decode only the tool field, once; strict protocol validation still applies.
3
+ function clean(source) {
4
+ let input=source.trim().replace(/^\uFEFF/, '');
5
+ const fence=input.match(/^```(?:json)?\s*\n([\s\S]*?)\n```$/i);
6
+ if(fence)input=fence[1];
7
+ let output='',quoted=false,escaped=false;
8
+ for(let i=0;i<input.length;i++){
9
+ const c=input[i];
10
+ if(quoted){output+=c;if(escaped)escaped=false;else if(c==='\\')escaped=true;else if(c==='"')quoted=false;continue;}
11
+ if(c==='"')quoted=true;
12
+ if(c===',' && /^\s*[}\]]/.test(input.slice(i+1)))continue;
13
+ output+=c;
14
+ }
15
+ return output;
16
+ }
3
17
  export function toolJson(value, field, maxBytes=131072){
4
18
  if(typeof value!=='string')return value;
5
- if(Buffer.byteLength(value,'utf8')>maxBytes)throw new Error(`DSH UI: ${field} JSON exceeds size limit`);
6
- try{return JSON.parse(value)}catch{throw new Error(`DSH UI: ${field} must contain valid JSON. Fix quoting/escaping. For a standalone HTML/SVG canvas, render_canvas accepts separate html/css/script fields.`)}
19
+ if(new TextEncoder().encode(value).length>maxBytes)throw new Error(`DSH UI: ${field} JSON exceeds size limit`);
20
+ const source=clean(value);
21
+ try{return JSON.parse(source)}catch(error){
22
+ const position=error.message.match(/position (\d+)/)?.[1];
23
+ const location=position===undefined?'':` at line ${source.slice(0,Number(position)).split('\n').length}, column ${Number(position)-source.lastIndexOf('\n',Number(position)-1)}`;
24
+ throw new Error(`DSH UI: ${field} must contain valid JSON${location}. Pass an object directly, not a manually escaped JSON string. Do not use single-quoted keys or raw newlines inside strings. For HTML/SVG use render_canvas with separate html/css/script fields. No partial document was saved.`);
25
+ }
7
26
  }
@@ -0,0 +1,29 @@
1
+ import {COMPONENTS} from './components.js';
2
+ // Expose the document skeleton to native function calling, not just prose.
3
+ // Per-component and expression checks remain in the shared runtime validator.
4
+ const dictionary=description=>({type:'object',additionalProperties:true,description});
5
+ export const DOCUMENT_PARAMETER={
6
+ required:true,
7
+ description:'Pass the document as an OBJECT. Do not manually escape/serialize it. For HTML/SVG source prefer render_canvas.',
8
+ type:'object',additionalProperties:false,
9
+ properties:{
10
+ version:{type:'number',const:1,required:true},
11
+ title:{type:'string',required:true},
12
+ fallback:{type:'string',required:true,description:'Useful plain-text explanation if the UI is unavailable.'},
13
+ nodes:{type:'array',required:true,items:{type:'object',additionalProperties:true,properties:{
14
+ id:{type:'string',required:true,description:'Unique letter/underscore identifier; Chinese supported, max64.'},
15
+ type:{type:'string',required:true,enum:Object.keys(COMPONENTS)},
16
+ label:{type:'string'},title:{type:'string'},
17
+ text:{type:'json',description:'Literal scalar or {ref}/{expr}.'},
18
+ value:{type:'json',description:'metric: number or display string/boolean, or {ref}/{expr}. progress: numeric.'},
19
+ rows:{type:'json',description:'table/chart: inline array of objects, {ref:"data.rows"}/{ref:"derived.rows"}, or scoped reference string.'},
20
+ columns:{type:'array',items:{type:'object',additionalProperties:false,properties:{key:{type:'string',required:true},label:{type:'string',required:true},unit:{type:'string'}}}},
21
+ bind:{type:'string',description:'Controls bind state.key.'},children:{type:'array',items:{type:'json'}}
22
+ }}},
23
+ state:dictionary('key -> {type:number/string/boolean/json,default,...}. Numeric state needs min,max,step. json is bounded array/object state for canvas controls.'),
24
+ data:dictionary('Static data; Chinese identifier keys supported. Omit if rows are inline.'),
25
+ derived:dictionary('Local expressions or maps. Omit when unused.'),
26
+ actions:dictionary('Declared agent.followup actions. Omit when unused.'),
27
+ presentation:dictionary('Optional layout/accent/density from registered tokens.')
28
+ }
29
+ };
package/lib/ui-intent.js CHANGED
@@ -4,9 +4,9 @@ export const UI_INTENT_POLICY = `## 表达选择与首次交付
4
4
  无需等待用户说“UI”“可视化”或指定工具。先用一个简短判断选择表达方式,然后直接回答或提交界面,不向用户复述设计推理:
5
5
  - 用户明确要求交互界面、图解或动画:交付对应 UI;已要求纯文字、不要界面、只给代码时遵从用户。
6
6
  - 用户没提 UI,但问题的关键是参数变化如何影响结果、多个方案的权衡、时间趋势、空间结构或流程关系:当图形或操作比文字明显更易懂时主动使用 UI。不能只因出现数字、多个条目或技术名词就触发。
7
- - 简单事实、单次数值换算、短定义、普通代码排错:直接文字。一个静态小表格足够时不必创建交互应用。不确定有无收益时选更简单的表达,不询问“要不要我做个 UI”。
7
+ - 简单事实、单次数值换算、短定义、普通代码排错:直接文字。一个静态小表格足够时不必创建交互应用;静态图解、方案对比、流程和看板也都是 UI 的正常用途。不确定有无收益时选更简单的表达,不询问“要不要我做个 UI”。
8
8
  对照:问“复利是什么,简短解释”→文字;问“5万元按3%、5%、7%存10年,为什么后期差距更大”→可主动给增长曲线与年数调整,配准确数值和简短解释;问“解释七速自行车各档如何影响踏频和行进”→可主动给传动示意与档位选择;问“1 GiB是多少字节”→文字。例子只指导时机,不规定构图或配色。
9
- 首次交付只解决一个核心关系。立即选择最小完整方案并调用工具,不穷举设计选项,不先写长篇视觉规划,不等待“完美”再提交。默认一幅主图、1至3个必要控件、清楚的初始结果与说明;用户的必要要求不能省略。可以少而精,不能用漂亮静态壳冒充可交互成品。
9
+ 首次交付只解决一个核心关系。立即选择最小完整方案并调用工具,不穷举设计选项,不先写长篇视觉规划,不等待“完美”再提交。默认一幅静态主图、一个短流程、一个对齐对比或一个简洁看板即可;没有明确交互收益时不建 state、不写 script、不加控件和动画。静态可视化本身就是完整交付;只有用户需要操作时才保证对应交互。用户的必要要求不能省略。
10
10
  仅数值试算优先原生组件;计算超出 derived 白名单(例如幂、三角函数)时不要臆造算子或堆复杂表达式,改用小画布中的单一计算函数同步图与数值。结构或运动讲解选择一块自由画布,直接使用 render_canvas 的独立源码字段。避免首次创建多页应用、自制组件库或庞大绘图引擎。除非用户或宿主另有要求,对话内的简短交互讲解无需建立项目、编辑文件或制定任务清单。工具提交前检查绑定、初始图可见、改变参数有反馈、标签/单位和 fallback;不为简单任务反复读全部目录。
11
11
  必要的布局与排版从首次提交就做好:DSH 以 PC 横屏为主要使用场景,优先保证 1365px/1920px 桌面视口中实际聊天区域的构图与可读性,不假设画布等于整个浏览器宽度。主体突出、文字可读、控件有标签,装饰不挤占内容;手机适配是补充,不为窄屏牺牲桌面主体。Canvas 2D 按实际 CSS 像素布局,不整体缩放到文字难辨;动态数值和关键标签可放 HTML,绘图高度匹配内容,避免大块空白。
12
12
  首屏应能看见主要关系、关键操作及当前结果。用户问“为什么变化不同”,优先用能对照变化的图形;发光、粒子或装饰不能代替趋势或关系证据。主图紧邻控件,推导、完整表格和次要说明随后呈现;同一数值不在指标、图注、卡片和正文反复展示。不要把所有可用组件都用一遍,也不为减少滚动而缩小字。比较不同单位的量,分别标轴或明确使用相对同一基准的无量纲倍数,不能把原始数值放在同一未说明尺度上暗示快慢。这些是信息优先级要求,具体图形、构图与风格仍按问题自行设计。
package/lib/ui-skill.js CHANGED
@@ -4,6 +4,7 @@ import { CANVAS_DESIGN_GUIDE } from './canvas-design.js';
4
4
  import { FREE_UI_CONTRACT } from './freeform.js';
5
5
  import { UI_INTENT_POLICY } from './ui-intent.js';
6
6
  import { CANVAS_SYSTEM_GUIDE } from './canvas-system.js';
7
+ import {QUICK_EXAMPLES,QUICK_GUIDE} from './quick-examples.js';
7
8
 
8
9
  // Authored examples are deliberately small enough to be present in every model's context.
9
10
  export const SKILL_EXAMPLES = [
@@ -38,7 +39,7 @@ const skill = readFileSync(new URL('../skills/intelligent-ui/SKILL.md', import.m
38
39
  export const UI_SKILL_SECTIONS = Object.freeze([
39
40
  { name: 'generative-ui:design-skill', order: 850, interpolate: false, text: UI_INTENT_POLICY + '\n\n' + skill + '\n\nCanvas composition details:\n' + CANVAS_DESIGN_GUIDE + '\n\nConstruction handbook:\n' + CANVAS_SYSTEM_GUIDE },
40
41
  { name: 'generative-ui:protocol', order: 851, interpolate: false, text: PROTOCOL_GUIDE },
41
- { name: 'generative-ui:worked-examples', order: 852, interpolate: false, text: 'These two valid render_ui document examples teach composition and exact syntax. Adapt the idea to the user; do not copy their demo claims as real data. Submit as render_ui({document: ...}).\n' + SKILL_EXAMPLES.map(doc => JSON.stringify(doc)).join('\n') }
42
+ { name: 'generative-ui:worked-examples', order: 852, interpolate: false, text: QUICK_GUIDE+'\nThese valid render_ui examples start with static flow and comparison; the last adds a control only for a calculation. Adapt to the user, not their demo claims. Submit as render_ui({document: ...}). Animation examples remain available via ui_catalog({topic:"canvas"}).\n' + [QUICK_EXAMPLES[0],QUICK_EXAMPLES[1],SKILL_EXAMPLES[0]].map(doc => JSON.stringify(doc)).join('\n') }
42
43
  ]);
43
44
  export function promptSections(mode='guided'){
44
45
  if(!['guided','free'].includes(mode))throw new Error('DSH UI: designMode must be guided or free');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-motion-ui",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Interactive UI, HTML/SVG animation and local calculations for DeepSeek Harness Web, with injected design guidance.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -10,7 +10,7 @@
10
10
 
11
11
  按问题选择构图,而不是每次复制仪表盘:
12
12
 
13
- | 任务 | 适合的构图 | 核心交互 |
13
+ | 任务 | 适合的构图 | 可选交互(并非必需) |
14
14
  | --- | --- | --- |
15
15
  | 机械/物体拆解 | 大幅 SVG 主体 + 部件说明,exploratory 处理 | 一个展开滑块 + 部件选择 |
16
16
  | 原理/参数实验 | 图形占主要空间 + 紧凑参数区,workbench | 参数改变,图与解释同步 |
@@ -21,6 +21,12 @@
21
21
 
22
22
  exploratory/analytical/editorial/technical 是构图思想,不是合法 layout 枚举;只能使用协议声明的 layout。状态面板仅在确有状态信息时使用 status。
23
23
 
24
+ ## 轻量图解优先
25
+
26
+ 默认先做静态可视化,不强行交互。一幅小图、3–5 步流程、2–3 方案的对齐对比或少量指标已经可以是完整作品。只有变化、选择或运动确实帮助理解时才添加 state、控件和 script;复杂交互与动画按需求启用,不能成为所有场景的默认。
27
+
28
+ 图解采用编辑式排版:中性底色、细线边框、少量强调色、直接标注、清晰层级。不叠阴影、发光或多重卡片。流程/架构用简单节点与有意义的箭头;可直接使用宿主提供的 ui-flow/ui-step/ui-arrow/ui-callout 类,通常无需自写 CSS 和 script。ui_catalog({topic:"quick"}) 提供流程、对比、架构和小看板四个短例子;优先参考已注入的静态例子,不反复查询目录。先提交足够讲清关系的小作品,按用户后续需求扩展。
29
+
24
30
  ## 视觉设计技能
25
31
 
26
32
  - 用大小、对齐、留白建立层级。让图形或证据成为主体;不把每段文字套进卡片,不堆 KPI,不重复标题。
@@ -34,7 +40,7 @@ exploratory/analytical/editorial/technical 是构图思想,不是合法 layout
34
40
 
35
41
  ## 正确生成的步骤
36
42
 
37
- 1. 确定问题、一个主要交互和可见反馈,不展开冗长设计复盘。
43
+ 1. 确定问题与核心可视化;仅有必要时加入一个主要交互和可见反馈,不展开冗长设计复盘。
38
44
  2. 优先使用下面已注入的协议和小例子。复杂功能或不确定字段,再调用 ui_catalog({topic:"canvas"|"data"|"scenes"});不必每次获取 all,也不必重复查询相同目录。
39
45
  3. 标准数据、控件使用原生组件;自由形状、SVG、HTML/CSS/JavaScript 动画使用 sandbox。它们可以混排,别因为固定组件不够就退回静态图片。
40
46
  4. 原生组件混排使用完整 render_ui({document:...}):先写元数据、state/data/derived/actions,再写 nodes。单独一块自由画布可用 render_canvas({title,fallback,html,css,script,state?...}),源码字段直接传字符串,避免把整份程序再嵌套进 JSON 文档字符串;该入口的画布节点 ID 固定为 canvas。不得在 PTC/run_code 中套用这两个工具。