dsh-motion-ui 0.5.6 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/README.md +112 -4
  3. package/VERIFICATION.md +145 -0
  4. package/client.js +4869 -22533
  5. package/examples/mechanism-assembly.json +50 -0
  6. package/examples/mechanism-crank.json +103 -0
  7. package/examples/mechanism-gears.json +103 -0
  8. package/index.js +52 -15
  9. package/lib/assets/echarts-core.js +47 -0
  10. package/lib/assets/echarts-full.js +78 -0
  11. package/lib/assets/katex-fonts/KaTeX_AMS-Regular.woff2 +0 -0
  12. package/lib/assets/katex-fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  13. package/lib/assets/katex-fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  14. package/lib/assets/katex-fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  15. package/lib/assets/katex-fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  16. package/lib/assets/katex-fonts/KaTeX_Main-Bold.woff2 +0 -0
  17. package/lib/assets/katex-fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  18. package/lib/assets/katex-fonts/KaTeX_Main-Italic.woff2 +0 -0
  19. package/lib/assets/katex-fonts/KaTeX_Main-Regular.woff2 +0 -0
  20. package/lib/assets/katex-fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  21. package/lib/assets/katex-fonts/KaTeX_Math-Italic.woff2 +0 -0
  22. package/lib/assets/katex-fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  23. package/lib/assets/katex-fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  24. package/lib/assets/katex-fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  25. package/lib/assets/katex-fonts/KaTeX_Script-Regular.woff2 +0 -0
  26. package/lib/assets/katex-fonts/KaTeX_Size1-Regular.woff2 +0 -0
  27. package/lib/assets/katex-fonts/KaTeX_Size2-Regular.woff2 +0 -0
  28. package/lib/assets/katex-fonts/KaTeX_Size3-Regular.woff2 +0 -0
  29. package/lib/assets/katex-fonts/KaTeX_Size4-Regular.woff2 +0 -0
  30. package/lib/assets/katex-fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  31. package/lib/assets/katex.css +1 -0
  32. package/lib/assets/mermaid.js +3644 -0
  33. package/lib/assets/standalone-runtime.css +1 -0
  34. package/lib/assets/standalone-runtime.js +330 -0
  35. package/lib/assets/three.js +3842 -0
  36. package/lib/assets/visual.css +1 -0
  37. package/lib/assets/visual.js +416 -0
  38. package/lib/author-expression.js +3 -2
  39. package/lib/authoring-guide.js +4 -3
  40. package/lib/build-info.js +1 -1
  41. package/lib/canvas-system.js +3 -3
  42. package/lib/catalog.js +94 -92
  43. package/lib/components.js +11 -6
  44. package/lib/delivery.js +18 -0
  45. package/lib/diagnostics-route.js +6 -0
  46. package/lib/document-update.js +27 -0
  47. package/lib/export-runtime-bundle.js +1 -1
  48. package/lib/expressive-examples.js +6 -0
  49. package/lib/fence-feedback.js +61 -0
  50. package/lib/fence-route.js +14 -0
  51. package/lib/fences.js +9 -0
  52. package/lib/freeform.js +4 -3
  53. package/lib/inspection.js +3 -2
  54. package/lib/links.js +13 -0
  55. package/lib/mechanism-catalog.js +24 -0
  56. package/lib/mechanism-css.js +13 -0
  57. package/lib/mechanism-entry.js +20 -0
  58. package/lib/mechanism-geometry.js +62 -0
  59. package/lib/mechanism-render.js +24 -0
  60. package/lib/mechanism-runtime-bundle.js +2 -0
  61. package/lib/mechanism-scenes.js +20 -0
  62. package/lib/motion-library.js +18 -0
  63. package/lib/normalize-authoring.js +52 -3
  64. package/lib/normalize-visual.js +36 -0
  65. package/lib/presentation.js +1 -1
  66. package/lib/protocol.js +361 -318
  67. package/lib/sandbox.js +2 -0
  68. package/lib/scene-library.js +13 -0
  69. package/lib/store.js +17 -2
  70. package/lib/stream.js +12 -0
  71. package/lib/style-system.js +28 -0
  72. package/lib/text-document.js +25 -0
  73. package/lib/text-guide.js +13 -0
  74. package/lib/third-party/anatomy-LICENSE.txt +21 -0
  75. package/lib/third-party/anatomy-NOTICE.md +13 -0
  76. package/lib/third-party/anatomy-iso.mjs +674 -0
  77. package/lib/third-party/bundled/@braintree_sanitize-url-LICENSE +21 -0
  78. package/lib/third-party/bundled/@iconify_utils-license.txt +21 -0
  79. package/lib/third-party/bundled/@mermaid-js_parser-LICENSE +21 -0
  80. package/lib/third-party/bundled/@upsetjs_venn.js-LICENSE +22 -0
  81. package/lib/third-party/bundled/cose-base-LICENSE +21 -0
  82. package/lib/third-party/bundled/cytoscape-LICENSE +19 -0
  83. package/lib/third-party/bundled/cytoscape-cose-bilkent-LICENSE +21 -0
  84. package/lib/third-party/bundled/cytoscape-fcose-LICENSE +19 -0
  85. package/lib/third-party/bundled/cytoscape-license-update.mjs +32 -0
  86. package/lib/third-party/bundled/d3-LICENSE +13 -0
  87. package/lib/third-party/bundled/d3-array-LICENSE +27 -0
  88. package/lib/third-party/bundled/d3-axis-LICENSE +13 -0
  89. package/lib/third-party/bundled/d3-brush-LICENSE +13 -0
  90. package/lib/third-party/bundled/d3-chord-LICENSE +13 -0
  91. package/lib/third-party/bundled/d3-color-LICENSE +13 -0
  92. package/lib/third-party/bundled/d3-contour-LICENSE +13 -0
  93. package/lib/third-party/bundled/d3-delaunay-LICENSE +14 -0
  94. package/lib/third-party/bundled/d3-dispatch-LICENSE +13 -0
  95. package/lib/third-party/bundled/d3-drag-LICENSE +13 -0
  96. package/lib/third-party/bundled/d3-dsv-LICENSE +13 -0
  97. package/lib/third-party/bundled/d3-ease-LICENSE +28 -0
  98. package/lib/third-party/bundled/d3-fetch-LICENSE +13 -0
  99. package/lib/third-party/bundled/d3-force-LICENSE +13 -0
  100. package/lib/third-party/bundled/d3-format-LICENSE +13 -0
  101. package/lib/third-party/bundled/d3-geo-LICENSE +34 -0
  102. package/lib/third-party/bundled/d3-hierarchy-LICENSE +13 -0
  103. package/lib/third-party/bundled/d3-interpolate-LICENSE +13 -0
  104. package/lib/third-party/bundled/d3-path-LICENSE +27 -0
  105. package/lib/third-party/bundled/d3-polygon-LICENSE +13 -0
  106. package/lib/third-party/bundled/d3-quadtree-LICENSE +13 -0
  107. package/lib/third-party/bundled/d3-random-LICENSE +13 -0
  108. package/lib/third-party/bundled/d3-sankey-LICENSE +27 -0
  109. package/lib/third-party/bundled/d3-scale-LICENSE +13 -0
  110. package/lib/third-party/bundled/d3-scale-chromatic-LICENSE +28 -0
  111. package/lib/third-party/bundled/d3-selection-LICENSE +13 -0
  112. package/lib/third-party/bundled/d3-shape-LICENSE +13 -0
  113. package/lib/third-party/bundled/d3-time-LICENSE +13 -0
  114. package/lib/third-party/bundled/d3-time-format-LICENSE +13 -0
  115. package/lib/third-party/bundled/d3-timer-LICENSE +13 -0
  116. package/lib/third-party/bundled/d3-transition-LICENSE +13 -0
  117. package/lib/third-party/bundled/d3-zoom-LICENSE +13 -0
  118. package/lib/third-party/bundled/dagre-d3-es-LICENSE.md +23 -0
  119. package/lib/third-party/bundled/dayjs-LICENSE +21 -0
  120. package/lib/third-party/bundled/delaunator-LICENSE +15 -0
  121. package/lib/third-party/bundled/dompurify-LICENSE +202 -0
  122. package/lib/third-party/bundled/dompurify-LICENSE-MPL +373 -0
  123. package/lib/third-party/bundled/echarts-LICENSE +222 -0
  124. package/lib/third-party/bundled/echarts-NOTICE +5 -0
  125. package/lib/third-party/bundled/es-toolkit-LICENSE +21 -0
  126. package/lib/third-party/bundled/es-toolkit-NOTICE +39 -0
  127. package/lib/third-party/bundled/internmap-LICENSE +13 -0
  128. package/lib/third-party/bundled/katex-LICENSE +21 -0
  129. package/lib/third-party/bundled/khroma-license +21 -0
  130. package/lib/third-party/bundled/layout-base-LICENSE +21 -0
  131. package/lib/third-party/bundled/lodash-es-LICENSE +47 -0
  132. package/lib/third-party/bundled/marked-LICENSE.md +44 -0
  133. package/lib/third-party/bundled/mermaid-LICENSE +21 -0
  134. package/lib/third-party/bundled/react-LICENSE +21 -0
  135. package/lib/third-party/bundled/react-dom-LICENSE +21 -0
  136. package/lib/third-party/bundled/robust-predicates-LICENSE +24 -0
  137. package/lib/third-party/bundled/roughjs-LICENSE +21 -0
  138. package/lib/third-party/bundled/scheduler-LICENSE +21 -0
  139. package/lib/third-party/bundled/stylis-LICENSE +21 -0
  140. package/lib/third-party/bundled/three-LICENSE +21 -0
  141. package/lib/third-party/bundled/ts-dedent-LICENSE +21 -0
  142. package/lib/third-party/bundled/tslib-LICENSE.txt +12 -0
  143. package/lib/third-party/bundled/uuid-LICENSE.md +9 -0
  144. package/lib/third-party/bundled/zrender-LICENSE +29 -0
  145. package/lib/third-party/dsh-genui-LICENSE.txt +21 -0
  146. package/lib/third-party/dsh-genui-NOTICE.md +26 -0
  147. package/lib/third-party/echarts-LICENSE.txt +222 -0
  148. package/lib/third-party/echarts-NOTICE.txt +5 -0
  149. package/lib/third-party/katex-LICENSE.txt +21 -0
  150. package/lib/third-party/lucian-uxui-LICENSE.txt +21 -0
  151. package/lib/third-party/lucian-uxui-NOTICE.md +1 -0
  152. package/lib/third-party/mermaid-LICENSE.txt +21 -0
  153. package/lib/third-party/three-LICENSE.txt +21 -0
  154. package/lib/tool-schema.js +20 -9
  155. package/lib/typography.js +16 -0
  156. package/lib/ui-intent.js +15 -14
  157. package/lib/ui-skill.js +54 -47
  158. package/lib/visual-assets.js +7 -0
  159. package/lib/visual-defaults.js +2 -0
  160. package/lib/visual-guard.js +2 -0
  161. package/lib/visual-styles.js +142 -0
  162. package/lib/visual.js +40 -0
  163. package/package.json +20 -5
  164. package/skills/intelligent-ui/SKILL.md +22 -2
package/lib/sandbox.js CHANGED
@@ -1,3 +1,4 @@
1
+ import {MECHANISM_RUNTIME} from './mechanism-runtime-bundle.js';
1
2
  import { measureVisual } from './visual-audit.js';
2
3
  import { formatNumber } from './format-number.js';
3
4
  import { CANVAS_SYSTEM_CSS } from './canvas-system.js';
@@ -35,6 +36,7 @@ export function sandboxDocument(node, channel, nonce) {
35
36
  motion.addEventListener('change',refresh);document.addEventListener('visibilitychange',refresh);
36
37
  const formatNumber=${formatNumber.toString()};
37
38
  const api={formatNumber,get snapshot(){return structuredClone(snapshot)},subscribe(fn){if(typeof fn!=='function')throw Error('Listener required');listeners.add(fn);fn(api.snapshot);return()=>listeners.delete(fn)},setState(patch){if(!snapshot.disabled)send('patch',patch)},animate(fn){if(typeof fn!=='function')throw Error('Animation callback required');animations.add(fn);refresh();return()=>{animations.delete(fn);if(!animations.size){cancelAnimationFrame(raf);raf=0;last=0}}}};
39
+ ${node.libraries?.includes('mechanism')?MECHANISM_RUNTIME.replace(/<\/script/gi,'<\\/script')+';api.mechanism=__MotionMechanism.createMechanismAPI(api);':''}
38
40
  Object.defineProperty(window,'DSHUI',{value:Object.freeze(api),writable:false});
39
41
  for(const type of ['click','input','change','keydown','pointerdown'])document.addEventListener(type,e=>{if(snapshot.disabled){e.preventDefault();e.stopImmediatePropagation()}},true);
40
42
  addEventListener('message',event=>{
@@ -0,0 +1,13 @@
1
+ import {validateDocument} from './protocol.js';
2
+ export const SCENES=Object.freeze([
3
+ {id:'brief',name:'重点说明',description:'短解释、结论与提示',prompt:'用三条简短要点讲清楚这件事,突出最重要的一句。',spec:{title:'先交付最小结果',items:[{type:'text',size:'h2',content:'让信息一眼可读'},{type:'list',items:['先写结论,再补原因','三条要点足够时,就保持简短','复杂关系再加图解']},{type:'callout',tone:'info',content:'文字本身也可以是好用的 UI。'}]}},
4
+ {id:'comparison',name:'方案对比',description:'并排看优缺点与适用条件',prompt:'比较两个方案的成本、收益和适用条件,给出简洁建议,使用示例数据时明确标注。',spec:{title:'两种方案 · 示意',items:[{type:'grid',cols:2,items:[{type:'card',title:'轻量方案',items:[{type:'stat',label:'示例月成本',value:'¥20'},{type:'text',content:'适合快速开始,功能集中。'}]},{type:'card',title:'完整方案',items:[{type:'stat',label:'示例月成本',value:'¥60'},{type:'text',content:'适合稳定扩展,需要更多配置。'}]}]},{type:'callout',tone:'info',content:'示意数据。按实际需求选择。'}]}},
5
+ {id:'flow',name:'简易流程',description:'把一件事按顺序讲清楚',prompt:'把这件事整理成五步以内的简易流程,每步只解释一句。',spec:{title:'从准备到交付',items:[{type:'steps',steps:[{title:'明确目标',desc:'确定完成后要看到什么。'},{title:'完成最小版本',desc:'先验证核心行为。'},{title:'检查并交付',desc:'记录结果和剩余问题。'}]}]}},
6
+ {id:'dashboard',name:'数据摘要',description:'指标、图表与数据表',prompt:'将这些数据做成紧凑摘要:关键指标、小图表和明细表,不虚构数据。',spec:{title:'本周数据 · 示例',items:[{type:'stat',label:'合计',value:'30'},{type:'chart',kind:'bars',data:[{label:'A',value:12},{label:'B',value:18}]},{type:'table',columns:['项目','数量'],rows:[['A',12],['B',18]],types:['text','num'],export:true}]}},
7
+ {id:'quiz',name:'本地小测验',description:'多题统一提交、判分与解析',prompt:'出两道简单单选题,选完统一提交后本地判分,显示答案和解释,能重新作答。',spec:{title:'基础概念小测',items:[{type:'radio',group:'q1',label:'git commit 记录什么?',options:['暂存区快照','所有未暂存修改'],answer:0,explanation:'提交记录的是暂存区。'},{type:'radio',group:'q2',label:'上传提交用什么?',options:['git pull','git push'],answer:1,explanation:'push 将本地提交推送至远程。'},{type:'submit',label:'检查答案',groups:['q1','q2']}]}},
8
+ {id:'form',name:'收集需求',description:'填写后一次提交给 Agent',prompt:'先做一个简短需求表单,填写目标、选择优先级后,再一次提交给你继续处理。',spec:{title:'下一步做什么',items:[{type:'input',id:'goal',label:'目标',placeholder:'写一句想完成的事'},{type:'select',id:'priority',label:'优先级',options:['正常','优先']},{type:'submit',label:'按这些要求继续',groups:['goal','priority'],action:'根据用户填写的目标和优先级制定简短计划。'}]}},
9
+ {id:'plot',name:'函数探索',description:'参数变化即时重绘',prompt:'解释 y=a*sin(x),提供 a 的滑块并让曲线本地实时变化,说明振幅的含义。',spec:{title:'振幅与曲线',items:[{type:'plot',title:'y = a sin(x)',xMin:-6.28,xMax:6.28,series:[{label:'正弦',expr:'a*sin(x)',params:[{name:'a',value:1,min:0,max:3,step:0.1}]}]},{type:'text',content:'改变 a 调整振幅,曲线在浏览器本地重绘。'}]}},
10
+ {id:'diagram',name:'结构图解',description:'流程、架构与关联',prompt:'用简洁图解说明这些部分之间的关系,配一句总结,不要画成复杂仪表盘。',spec:{title:'输入到结果',items:[{type:'mermaid',code:'graph LR\nA[输入] --> B[处理] --> C[结果]'},{type:'text',content:'用关系表达结构,用短句补充含义。'}]}}
11
+ ]);
12
+ export function sceneCatalog(id){if(id){const scene=SCENES.find(s=>s.id===id);if(!scene)throw Error('Unknown scene; choose '+SCENES.map(s=>s.id).join(', '));return {...scene,document:validateDocument(scene.spec),guidance:'Adapt the content, apply the selected guided visual style when present, and choose one delivery channel. This is a starting example, not a fixed mandatory layout.'}}return {scenes:SCENES.map(({id,name,description})=>({id,name,description})),usage:'ui_catalog({topic:"templates",scene:"quiz"}) returns one working example. Retrieve only the closest scene; do not load every example.'}}
13
+ export const SCENE_GUIDE='For a familiar scene, ui_catalog({topic:"templates",scene:...}) returns one short working example: brief, comparison, flow, dashboard, quiz, form, plot, diagram. Choose the smallest useful composition; adapt content and the selected guided style, never invent data. Do not fetch the whole catalog for an ordinary answer.';
package/lib/store.js CHANGED
@@ -3,6 +3,7 @@ import { join, dirname } from 'node:path';
3
3
  import { randomUUID, createHash } from 'node:crypto';
4
4
  import { validateDocument, validateState, initialState } from './protocol.js';
5
5
  import { refineDocument, validateAudit } from './refinement.js';
6
+ import {updateDocument} from './document-update.js';
6
7
  export class UiStoreError extends Error {
7
8
  constructor(status, message) { super(message); this.status = status; }
8
9
  }
@@ -22,7 +23,7 @@ export class UiStore {
22
23
  finally { await unlink(temp).catch(e => { if (e.code !== 'ENOENT') throw e; }); }
23
24
  }
24
25
  async create(document, context) {
25
- const record = { version: 1, documentRevision: 0, documentId: randomUUID(), sessionId: context.sessionId, callId: context.callId, document: validateDocument(document), createdAt:new Date().toISOString() };
26
+ const record = { version: 1, documentRevision: 0, documentId: context.documentId??randomUUID(), sessionId: context.sessionId, callId: context.callId, document: validateDocument(document), createdAt:new Date().toISOString() };
26
27
  await this.atomic(this.documentPath(record.documentId), record); return record;
27
28
  }
28
29
  async readDocument(id) {
@@ -37,7 +38,8 @@ export class UiStore {
37
38
  try {
38
39
  const saved = JSON.parse(await readFile(path, 'utf8'));
39
40
  if (saved.version !== 1 || !Number.isSafeInteger(saved.revision) || saved.revision < 1) throw new Error('Invalid saved UI state');
40
- return { version: 1, revision: saved.revision, state: validateState(record.document, saved.state) };
41
+ const defaults=initialState(record.document),migrated=Object.fromEntries(Object.entries(defaults).map(([k,v])=>[k,Object.hasOwn(saved.state,k)?saved.state[k]:v]));
42
+ return { version: 1, revision: saved.revision, state: validateState(record.document, migrated) };
41
43
  } catch (e) { if (e.code !== 'ENOENT') throw e; return { version: 1, revision: 0, state: initialState(record.document) }; }
42
44
  }
43
45
  async owned(id,sessionId){const record=await this.readDocument(id);if(record.sessionId!==sessionId)throw new UiStoreError(403,'UI belongs to another conversation');return record;}
@@ -97,6 +99,19 @@ export class UiStore {
97
99
  const pending=previous.catch(()=>{}).then(async()=>{const record=await this.owned(id,sessionId);if(!Number.isSafeInteger(revision)||revision<0)throw new UiStoreError(400,'Invalid document revision');if((record.documentRevision??0)!==revision)throw new UiStoreError(409,'Document changed; inspect_ui before retrying');const next={...record,documentRevision:revision+1,document:refineDocument(record.document,changes),createdAt:new Date().toISOString()};delete next.restoredFrom;await this.archive(record);await this.atomic(path,next);return next;});
98
100
  this.locks.set(path,pending);try{return await pending}finally{if(this.locks.get(path)===pending)this.locks.delete(path)}
99
101
  }
102
+ async update(id,sessionId,revision,operationId,operation){
103
+ if(typeof operationId!=='string'||!/^[a-zA-Z0-9_-]{1,64}$/.test(operationId))throw new UiStoreError(400,'operationId requires 1..64 letters, digits, underscores or hyphens');
104
+ const path=this.documentPath(id),previous=this.locks.get(path)??Promise.resolve();
105
+ const canonical=v=>Array.isArray(v)?v.map(canonical):v&&typeof v==='object'?Object.fromEntries(Object.keys(v).sort().map(k=>[k,canonical(v[k])])):v;
106
+ const hash=createHash('sha256').update(JSON.stringify(canonical(operation))).digest('hex');
107
+ const pending=previous.catch(()=>{}).then(async()=>{
108
+ const record=await this.owned(id,sessionId),replayed=record.operations?.find(x=>x.id===operationId);
109
+ if(replayed){if(replayed.hash!==hash)throw new UiStoreError(409,'operationId already used for different content');return record}
110
+ if(!Number.isSafeInteger(revision)||revision!==(record.documentRevision??0))throw new UiStoreError(409,'Document changed; inspect_ui before updating');
111
+ const document=updateDocument(record.document,operation),next={...record,document,documentRevision:revision+1,createdAt:new Date().toISOString(),operations:[...(record.operations??[]),{id:operationId,hash}].slice(-64)};
112
+ delete next.restoredFrom;await this.archive(record);await this.atomic(path,next);return next;
113
+ });this.locks.set(path,pending);try{return await pending}finally{if(this.locks.get(path)===pending)this.locks.delete(path)}
114
+ }
100
115
  async saveAudit(id,sessionId,revision,value){
101
116
  const record=await this.owned(id,sessionId);if((record.documentRevision??0)!==revision)throw new UiStoreError(409,'Audit is for an old document revision');
102
117
  const report={...validateAudit(value),documentRevision:revision,observedAt:new Date().toISOString()};
package/lib/stream.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { LIMITS, validateDocument } from './protocol.js';
2
2
  import { toolJson } from './tool-json.js';
3
+ import {textDocument} from './text-document.js';
3
4
 
4
5
  // Parse complete JSON tokens only. Never finish a string, number or model expression.
5
6
  // Partial containers retain completed members, but only the document wrapper and
@@ -67,6 +68,8 @@ export function previewDocument(raw) {
67
68
  if (!document?.members) return null;
68
69
  if ([...root.members.keys()].some(k => k !== 'document')) return null;
69
70
  const doc = { ...document.value }, nodes = document.members.get('nodes');
71
+ const items=document.members.get('items');
72
+ if(!nodes&&items&&Array.isArray(items.value)&&items.value.length){try{return validateDocument({...doc,items:items.value})}catch{return null}}
70
73
  if (!nodes || !Array.isArray(nodes.value) || !nodes.value.length) return null;
71
74
  doc.nodes = nodes.value;
72
75
  // Only whole, valid top-level subtrees can appear. Forward references can
@@ -86,3 +89,12 @@ export function previewCanvas(raw) {
86
89
  if(html.value.length>49152||css.value.length>49152)return null;
87
90
  return {id:'canvas-preview',title:String(get('title').value).slice(0,160),fallback:String(get('fallback').value).slice(0,2000),html:html.value,css:css.value,height:Number.isFinite(get('height')?.value)?Math.max(160,Math.min(1000,get('height').value)):420};
88
91
  }
92
+
93
+ export function previewText(raw){
94
+ const root=parseToolArguments(raw),blocks=root?.members?.get('blocks');
95
+ if(!Array.isArray(blocks?.value)||!blocks.value.length)return null;
96
+ for(let n=blocks.value.length;n>0;n--){
97
+ try{return textDocument({...root.value,blocks:blocks.value.slice(0,n)})}catch{}
98
+ }
99
+ return null;
100
+ }
@@ -0,0 +1,28 @@
1
+ // Visual grammar, shared by native rendering, previews and model guidance.
2
+ // IDs are stable configuration values; no additional user switches are introduced.
3
+ export const STYLE_SYSTEM=Object.freeze({
4
+ editorial:{description:'开放正文 · 杂志层级 · 细线分组',grammar:'编辑式阅读',composition:'先给一个明确结论,再用开放正文、左右对齐比较和细线段落展开。用 column 组织文章,card 在此风格中是章节,不是封闭面板;不要给每段套框。指标嵌在解释旁,流程像文中的编号提纲。',typography:'标题有衬线阅读感,正文行距舒展;标题、正文、注释至少三个层级,强调依赖字重和位置。',surfaces:'透明正文、细分隔线、低阴影或无阴影;保持结构性留白,不加大面积渐变。',controls:'表单像文章中的设置行,标签与当前值左右对齐;按钮紧凑,代码像有边注的摘录。',avoid:'避免大彩色卡片、每个数字一个盒子、粗重边框和装饰性背景。',native:{title:27,weight:650,gap:24,radius:12,metric:32,feedback:210,gain:.25}},
5
+ soft:{description:'笔记色块 · 柔圆分组 · 舒缓节奏',grammar:'柔和彩色笔记',composition:'像清晰友好的笔记:把相关内容放在柔圆色块里,先提示主题,再给两三项要点。章节标题可以是小标签;比较项如并排便签,流程像一串有间距的笔记。',typography:'圆润标题和温和字重;正文清楚,数值与单位贴近,次要说明放在组内。',surfaces:'低饱和彩色便签、不同章节轻微交替色块、不对称的大圆角;使用实色底或少量柔和渐变。',controls:'控件内嵌在柔色分组,选择器像标签,操作有柔和回弹;避免每行都变成边框表单。',avoid:'避免工程网格、重黑海报、透明玻璃、泛白低对比度文字。',native:{title:25,weight:700,gap:20,radius:28,metric:36,feedback:390,gain:.7}},
6
+ technical:{description:'工程网格 · 数据条带 · 编号连接',grammar:'技术说明与工程图',composition:'像工程说明:按输入→机制→输出组织;优先对齐的数据条带、编号流程、表格和有意义的连接线。指标放成紧凑测量带而非一排装饰卡;比较围绕相同维度对齐。',typography:'中文正文保持正常字号,数值/单位/序号用等宽字体与 tabular-nums;标题简短精确,明确基准、单位和假设。',surfaces:'细线网格、规则的小圆角、清晰边界和表头、无浮动投影。网格仅在图解背景使用,文字区域保持干净。',controls:'标签和输入严格对齐,参数区像仪表设置行;按钮和分段选项采用规则矩形,保留键盘及全局反馈。',avoid:'避免胶囊堆叠、巨大海报数字、彩色便签、装饰箭头、靠小字硬塞信息。',native:{title:22,weight:750,gap:16,radius:10,metric:28,feedback:180,gain:.3}},
7
+ expressive:{description:'海报层级 · 非对称重点 · 醒目数字',grammar:'活力展示与重点海报',composition:'先选一个真正的视觉中心:大标题或关键数字,再用不对称分栏展开解释。主要结论占更大空间,次要比较退后;流程用醒目的序号与短句。不要把每一项都做成同等大小卡片。',typography:'短而有力的重标题、大数字、紧凑标题行距,正文仍易读。最多一个主要强调区,其余留白和细线辅助。',surfaces:'实色色块、粗细明确的边线、局部高对比强调;可用橙/珊瑚/暖黄,但不能整页争抢注意力。',controls:'主要操作醒目、次要操作轻量;表单和数据保持对齐,回弹比阅读风格明显但不空闲循环。',avoid:'避免六张等大的指标卡、满屏渐变、全屏大留白、无意义数字装饰。',native:{title:34,weight:850,gap:22,radius:20,metric:50,feedback:480,gain:1.1}},
8
+ prismframe:{description:'应用模块 · 实色控件 · 偏移底面',grammar:'明亮扁平的轻量应用',composition:'像可直接使用的轻量应用:任务标题、输入模块、结果模块、解释区职责清楚。模块使用稳定网格,操作靠近对应结果;比较、流程和数据看板各自采用合适结构,不固定成同一套卡片。',typography:'圆润清晰的系统字体、明确模块标题、中等大数字;数字绑定名称和单位,图形承担关系说明。',surfaces:'浅彩底、白色实心模块、细彩色边线和扁平偏移底面;局部多色用来区分职责,避免每块同色同形。',controls:'整齐且有面积的实色表单行、清晰状态标签,主要操作有轻微偏移底面;不是磨砂玻璃。',avoid:'避免文档式满屏细线、海报式超大字、透明层叠、视频页码和字幕、重复标题。',native:{title:26,weight:780,gap:20,radius:24,metric:36,feedback:320,gain:.65}},
9
+ aurora:{description:'产品层级 · 暖白曲面 · 局部渐彩',grammar:'柔光渐彩的精致产品界面',composition:'像精致的产品面板:暖白开放背景上安排少量实心表面,标题简洁、指标突出、控件顺手。用宽窄和疏密区分主要结果与次要说明;按钮、表单、表格、代码与文字要属于同一套语言。',typography:'近黑标题、清晰的字重差、大而利落的数字、柔和次要说明;不把文字都染成紫色。',surfaces:'暖白实色、24..30px 圆润表面、柔和投影、局部紫蓝青渐彩强调;不把整页铺成紫面板。',controls:'圆润内嵌输入、胶囊选择器、深色主操作与渐彩图标圆点;真实操作触发渐变扩散和弹性反馈。',avoid:'禁止玻璃/backdrop-filter、泛白低对比度、仅加一个演示按钮就算风格、空闲漂浮。',native:{title:28,weight:780,gap:24,radius:28,metric:42,feedback:460,gain:1}}
10
+ });
11
+ export function grammarPrompt(id){const g=STYLE_SYSTEM[id];return `视觉语言:${g.grammar}\n构图:${g.composition}\n文字层级:${g.typography}\n表面与边界:${g.surfaces}\n控件与反馈:${g.controls}\n避免:${g.avoid}\n同一任务在此风格下应具有可辨识的层级与构图,不能只换强调色。所有风格共用原生交互能力;动画只回应真实操作、支持 reduced motion。保持内容完整和 DOM 阅读顺序,不为了风格重排计算含义。用户明确指定的尺寸、颜色与布局优先。禁止玻璃材质。示例演示构图,不要求每次输出所有模块;简单需求用简短正文或一组数字,不能为了展示风格扩写内容。`;}
12
+ // Same facts, intentionally different content composition. Model examples are
13
+ // complete small documents; preview uses the same grammar without live controls.
14
+ export function grammarExample(id){
15
+ const heading={type:'heading',text:'先交付一个可用版本'},body={type:'text',text:'把目标缩小,把关键体验做好。'},note={type:'text',text:'先验证,再逐步完善。',color:'muted'};
16
+ const choice=(title,text)=>({type:'card',title,children:[{type:'text',text}]});
17
+ const choices=[choice('先做小版本','验证重点'),choice('先铺全功能','覆盖需求')];
18
+ const metrics=[{type:'metric',label:'推进阶段',value:3,unit:'步'},{type:'metric',label:'核心目标',value:1,unit:'个'}];
19
+ const flow={type:'steps',items:[{title:'明确目标',description:'先抓住重点。'},{title:'解释关系',description:'用图形说明。'},{title:'给出行动',description:'收成下一步。'}]};
20
+ let preset='brief',blocks;
21
+ if(id==='editorial'){preset='reading';blocks=[heading,body,{type:'row',columns:2,children:choices},{type:'row',columns:2,children:metrics},flow,note]}
22
+ else if(id==='soft'){blocks=[{type:'card',title:'今天的重点',children:[heading,body]},{type:'row',columns:2,children:choices},flow,{type:'row',columns:2,children:metrics},note]}
23
+ else if(id==='technical'){blocks=[heading,body,{type:'row',columns:2,children:metrics},flow,{type:'table',label:'推进方式',columns:[{key:'method',label:'方案'},{key:'focus',label:'重点'}],rows:[{method:'先做小版本',focus:'验证重点'},{method:'先铺全功能',focus:'覆盖需求'}]},note]}
24
+ else if(id==='expressive'){preset='minimal';blocks=[heading,{type:'row',columns:2,children:[{type:'column',children:[metrics[0],body]},{type:'column',children:[metrics[1],note]}]},{type:'row',columns:2,children:choices},flow]}
25
+ else if(id==='prismframe'){blocks=[heading,body,{type:'row',columns:2,children:choices},{type:'row',columns:2,children:metrics},{type:'card',title:'行动路径',children:[flow]},note]}
26
+ else{blocks=[heading,body,{type:'row',columns:2,children:metrics},{type:'row',columns:2,children:choices},flow,note]}
27
+ return {tool:'render_text',args:{title:'把复杂问题讲清楚',preset,blocks}};
28
+ }
@@ -0,0 +1,25 @@
1
+ import {validateDocument,fallbackText,LIMITS} from './protocol.js';
2
+ import {normalizeAuthoring} from './normalize-authoring.js';
3
+ // The fast path still uses the exact same validator/storage/renderer as render_ui.
4
+ export function textDocument({title='内容',preset='reading',accent,blocks}){
5
+ if(!Array.isArray(blocks)||!blocks.length)throw Error('DSH UI: blocks must be a non-empty array of text strings or registered nodes');
6
+ let encoded;try{encoded=JSON.stringify(blocks)}catch{throw Error('DSH UI: blocks must contain serializable JSON')}
7
+ if(new TextEncoder().encode(encoded).length>LIMITS.bytes)throw Error('DSH UI: blocks exceed document size limit');
8
+ const parsed=JSON.parse(encoded),pending=[[parsed,0]];while(pending.length){const[value,depth]=pending.pop();if(depth>48)throw Error('DSH UI: blocks JSON nesting is too deep');if(value&&typeof value==='object')for(const child of Object.values(value))pending.push([child,depth+1])}
9
+ let count=0;const guard=(list,depth=1)=>{if(depth>LIMITS.depth)throw Error('DSH UI: blocks exceed node depth limit');for(const n of list){if(++count>LIMITS.nodes)throw Error('DSH UI: too many blocks');if(n&&typeof n==='object'&&Array.isArray(n.children))guard(n.children,depth+1)}};guard(blocks);
10
+ blocks=normalizeAuthoring({title,fallback:title,nodes:parsed}).nodes;count=0;guard(blocks);
11
+ let serial=0;const used=new Set();
12
+ const collect=list=>list.forEach(n=>{if(n&&typeof n==='object'){if(n.id!==undefined)used.add(n.id);if(Array.isArray(n.children))collect(n.children)}});collect(blocks);
13
+ const copy=list=>list.map(block=>{
14
+ const node=typeof block==='string'?{type:'text',text:block}:structuredClone(block);
15
+ if(!node||typeof node!=='object'||Array.isArray(node))throw Error('DSH UI: each block must be text or a node object');
16
+ // Reserved generated IDs avoid clashing with the caller's optional explicit IDs.
17
+ if(node.id===undefined){do{node.id=`_text_${serial++}`}while(used.has(node.id));used.add(node.id)}
18
+ if(node.children!==undefined){if(!Array.isArray(node.children))throw Error('DSH UI: children must be an array');node.children=copy(node.children)}
19
+ return node;
20
+ });
21
+ const nodes=copy(blocks);
22
+ const doc=validateDocument({version:1,title,fallback:title,presentation:{preset,...(accent?{accent}:{})},nodes});
23
+ doc.fallback=fallbackText({...doc,title:'',fallback:''}).trim().slice(0,8192)||title;
24
+ return doc;
25
+ }
@@ -0,0 +1,13 @@
1
+ export const TEXT_GUIDE=`## 日常对话的轻量排版
2
+ 日常解释、建议、摘要、对照和资料入口可以直接用 render_text,不需要用户提醒“做 UI”,也不必写 HTML/CSS/JS。只有排版确实更易读时使用;一句话能说清的事实仍可直接回答。不要为了使用 UI 把短回答扩成看板、加无用交互或重复标题。用户要求多块内容时逐块交付,首次工具成功不代表整个任务完成;提交结论前核对是否遗漏用户指定的流程、对比、按钮或文件入口。
3
+ render_text({title?:string,preset?:string,accent?:string,blocks:[string|node,...]})。纯字符串是一段文字;blocks 中的节点可省略 id,嵌套 children 的 id 也会自动补齐。render_ui 仍要求显式 id。state/data/derived/actions 不属于 render_text;需要本地计算时用 render_ui,需要自绘动画时用 render_canvas。
4
+ 预设:reading 自然正文(无外框);brief 轻边框面板;comparison 并排比较;list 图标/标题/说明与分隔线;callout 少量强调;scorecard 标签与数值两端对齐;flow 纵向流程;minimal 最少留白。根据当前这一块内容选择,八种可在同一回复混排,不必统一用卡片。
5
+ text/heading 支持 size(12..48px 或 sm/md/lg/xl/hero)、weight(400/500/600/700/800)、color(default/muted/accent/success/warning/danger/violet 或 #RGB/#RRGGBB)、align(left/center/right)、lineHeight(1.2..2.4)。正文通常 16..18px,标题 20..26px,脚注 14px;不用过小字号或整段彩色。主题色令牌自动适应深浅主题,自选十六进制色需要兼顾两种主题的对比度。局部强调用 spans:[{text:"普通文字"},{text:"关键结论",weight:700,color:"success"}] 替代 text,不能同时提供两者。只接受声明式字段,不输出 style、className、CSS 或 HTML。
6
+ row 的 columns:1..4 可指定等宽列,gap:0..40 控制间距;column 也支持 gap。divider 可选 label。card 用于真实的信息分组;不要求每段文字包一层卡片。按钮使用 link:{type:"link",text,href:"https://...",variant:"primary"|"secondary"|"text"},本地文件用 path 替代 href。网页只接受 http/https;path 是文件路径,不能放命令、file: URL 或自定义启动协议。按钮只在用户点击后打开,文件走 DSH 原生 opener;打开文件不能伪称执行了脚本或工具。
7
+ 同一回复可以多次 render_text/render_ui/render_canvas:每块各自保存文档、参数、主题和画布;相同节点 id 可在不同文档重复,单个文档内仍须唯一。按正文讲解顺序提交,多块小而完整的 UI 比一个巨大应用更适合混排。不要为了多样式复制相同内容。inline 模式下,每次成功结果都有自己的 documentId,各自放一个 fenced dsh-ui 标记到对应段落;同一个 ID 只标记一次,不用后一个结果覆盖前一个。生成期间逐步显示经过校验的完整内容;HTML/SVG 先提供静态预览,整个回复结束后恢复参数并启用交互。优先把 title、fallback、css 放在 html 前,原生 nodes/blocks 按阅读顺序输出,使预览更早可读。不要等待预览,也不要为了预览增加工具调用。
8
+ 可直接复用的文案、提示词、邮件正文用 text 的 copyable:true,并可加 title;右上复制按钮只复制正文,spans 的样式不会混进剪贴板。代码、命令和配置用 {type:"code",lang:"bash",code:"git status",title?:string,wrap?:boolean};代码默认提供右上复制按钮,原样保留缩进、换行和反引号,只展示不执行。长行默认横向滚动,wrap:true 可换行。不必给每段普通讲解加复制框;用户需要直接使用的内容才用。不要为了可复制再把同一段内容在正文重复一遍。
9
+ 最小示例:render_text({preset:"reading",blocks:[{type:"heading",text:"先看结论"},{type:"text",spans:[{text:"优先保证 "},{text:"可读性",weight:700,color:"accent"},{text:",再补充必要细节。"}]}]})。
10
+ 对比示例:render_text({title:"两种推理方案(示意)",preset:"comparison",blocks:[{type:"row",columns:2,children:[{type:"column",children:["方案 A · 大 MoE",{type:"text",text:"30 tok/s",size:26,weight:700},"完成任务 180 秒"]},{type:"column",children:["方案 B · Dense",{type:"text",text:"10 tok/s",size:26,weight:700},"完成任务 120 秒"]}]},{type:"text",text:"示意数据,非模型实测。",size:14,color:"muted"}]})。
11
+ 入口示例:render_text({preset:"minimal",blocks:[{type:"row",children:[{type:"link",text:"查看项目",href:"https://github.com/Scorp1o117/dsh-motion-ui",variant:"primary"},{type:"link",text:"打开报告",path:"report.html",variant:"secondary"}]}]})。文件必须来自当前任务的真实产物,不能臆造路径。`;
12
+
13
+ export const TEXT_INTERFACE=`Native lightweight entry: render_text({title?,preset?,accent?,blocks:[]}). blocks are strings or native nodes with auto IDs; presets reading/brief/comparison/list/callout/scorecard/flow/minimal. Text/heading support size 12..48 or sm/md/lg/xl/hero, weight 400/500/600/700/800, color default/muted/accent/success/warning/danger/violet or hex, align left/center/right, lineHeight 1.2..2.4. Reusable text adds copyable:true and title?. Code nodes {type:"code",code:string,lang?,title?,wrap?} always have an upper-right copy button and display literal source without executing it. Inline spans:[{text,weight?,color?,size?}] replaces text. link:{text,href:http(s) URL OR path:local file,variant:primary/secondary/text} opens only on a user click through the Host. Several independent UI documents may accompany one answer, each with its own styling, state and successful documentId marker. No mandatory text preset or visual composition; use plain prose whenever it suffices.`;
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ryan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,13 @@
1
+ # Anatomy attribution
2
+
3
+ Selected isometric camera, projection, contour and fitting helpers are bundled from
4
+ Anatomy by Ryan, MIT licensed:
5
+ https://github.com/wheresryan22/anatomy
6
+
7
+ Source revision: 454a0ef801b6d9b905f08309d094491c7085b3dd
8
+ Original file: kit/iso-kit.mjs (retained as anatomy-iso.mjs).
9
+ License: anatomy-LICENSE.txt. Copyright (c) 2026 Ryan.
10
+
11
+ Motion UI's scene validation, renderer, state bridge, controls, simplified mechanism
12
+ models and examples are original integration code. This is a compact explanatory
13
+ renderer, not Anatomy's full collision/audit system or its beta 3D/WebGL runtime.