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
@@ -1,7 +1,7 @@
1
1
  // A small authoring language compiled to the existing interpreter; never eval.
2
2
  import { IDENTIFIER_SOURCE, validIdentifier } from './identifiers.js';
3
3
  export function compileExpression(source) {
4
- 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
+ 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/ceil/floor/abs'); };
5
5
  if (typeof source !== 'string' || !source.trim() || source.length > 2048) fail();
6
6
  const tokens = []; let offset = 0;
7
7
  const token = new RegExp(String.raw`\s*(?:(\d+(?:\.\d*)?|\.\d+)([eE][+-]?\d+)?|("(?:[^"\\]|\\.)*")|(${IDENTIFIER_SOURCE}(?:\.${IDENTIFIER_SOURCE})?)|(===|!==|<=|>=|==|!=|[+*/()?,:<>-]))`, 'uy');
@@ -23,7 +23,7 @@ export function compileExpression(source) {
23
23
  else if (t === '(') { out = parse(0, depth + 1); take(')'); }
24
24
  else if (t === '-' || t === '+') { const arg = parse(5, depth + 1); out = t === '-' ? { op: 'sub', args: [{ const: 0 }, arg] } : arg; }
25
25
  else if (typeof t === 'string' && t.split('.').length === 2 && ['state','data','derived','item'].includes(t.split('.')[0]) && validIdentifier(t.split('.')[1])) out = { ref: t };
26
- else if (['min', 'max', 'round', 'abs'].includes(t)) {
26
+ else if (['min', 'max', 'round', 'ceil', 'floor', 'abs'].includes(t)) {
27
27
  take('('); const args = [parse(0, depth + 1)];
28
28
  while (tokens[pos] === ',') { pos++; args.push(parse(0, depth + 1)); if (args.length > 8) fail(); }
29
29
  take(')'); out = { op: t, args };
@@ -55,6 +55,7 @@ export function expandExpressions(doc) {
55
55
  const walk = nodes => { if (!Array.isArray(nodes)) return; for (const node of nodes) {
56
56
  if (!node || typeof node !== 'object') continue;
57
57
  for (const field of ['text', 'value', 'max', 'rows']) if (Object.hasOwn(node, field)) node[field] = expression(node[field]);
58
+ if (['table','chart'].includes(node.type)&&Array.isArray(node.rows)) for(const row of node.rows) if(row&&typeof row==='object'&&!Array.isArray(row)) for(const k of Object.keys(row)) row[k]=expression(row[k]);
58
59
  walk(node.children);
59
60
  } };
60
61
  walk(doc.nodes); return doc;
@@ -1,15 +1,16 @@
1
1
  // Keep this short and early in the injected contract, including free mode.
2
2
  export const AUTHORING_GUIDE = `Authoring fast path:
3
- - UI presentation waits for the whole assistant turn to end, including later reasoning/text. A successful render call saves the UI but does not mean it is already visible. Tool argument streaming does not preview UI. Text/reasoning stream normally; continue the user's task without waiting for the display or polling it.
3
+ - Complete validated components may preview progressively as read-only UI during tool argument streaming. Controls and scripts activate only when the whole assistant turn ends, including later reasoning/text. A successful render call saves the document; continue the user's task without waiting for the display or polling it.
4
4
  - JSON object field order never matters. Every node, including each sandbox, must include a unique id. sandbox title/fallback default to the document's only when omitted; do not send null or empty values.
5
5
  - Declaring state does NOT create controls. For each parameter the user should edit, add a native slider/input/select/toggle bound to state.key, or actual canvas HTML controls wired to DSHUI.setState with that key in stateKeys. A canvas subscribe callback only updates display; it does not create an input. Read-only state and static UI are valid; do not invent controls where the task does not need them. Check intended controls exist before calling the tool.
6
6
  - Use render_ui/render_canvas directly for UI construction. This contract and ui_catalog are the protocol reference; installation source inspection is not a prerequisite. For unclear component/formula details consult ui_catalog, rather than grep/glob/shell searches of DSH checkouts or app.asar. File/search tools needed for the user's actual task or data remain appropriate.
7
- - Formula round(x) takes ONE argument and uses JavaScript Math.round: nearest integer, ties toward positive infinity (round(2.5)=3, round(-2.5)=-2). It is not ceil/floor, which are not registered native formula functions. For ceiling/floor use Math.ceil/Math.floor in a canvas subscribed to declared shared state. Display decimals formats numbers without changing calculation precision.
7
+ - Formula round(x) takes ONE argument and uses JavaScript Math.round: nearest integer, ties toward positive infinity (round(2.5)=3, round(-2.5)=-2). Native ceil(x) rounds up and floor(x) rounds down, each with ONE argument. Use {expr:"ceil(state.people / 2)"} for room counts directly, without a canvas or lookup chain. Display decimals formats numbers without changing calculation precision.
8
8
  - Pass tool JSON fields as objects, not hand-escaped JSON strings. For custom HTML/SVG use render_canvas({title,fallback,html,css,script}) to avoid nesting source in document JSON.
9
9
  - Start with the smallest useful static UI. State/derived/actions are optional. A table can put its object rows directly in rows; reuse data/derived references only when useful. Example: {id:"t",type:"table",label:"方案比较",columns:[{key:"方案",label:"方案"}],rows:[{"方案":"A"}]}.
10
10
  - For a short ordered process, use steps with a DIRECT items array: {id:"flow",type:"steps",items:[{title:"整理文件",description:"保留源码和说明,排除密钥。"},{title:"提交代码",description:"检查变更后提交。"}]}. Do not wrap items in {item:[...]}. status is optional: omit it for explanatory guides; use pending/active/done/error only for known task states. Keep one sentence per requested step; avoid extra command disclosures unless asked.
11
+ - A short dynamic chart/table can use inline formula cells: rows:[{label:"餐饮",value:{ref:"derived.food"}},{label:"住宿",value:{expr:"ceil(state.people / 2) * state.room"}}]. Chart labelKey:"label",valueKey:"value". No map/if chain is required for a few categories. Submit complete nodes/state/derived together on every render_ui retry; it creates a document, not a partial patch.
11
12
  - Numeric controls need complete typed state: state:{people:{type:"number",default:2,min:1,max:12,step:1},days:{type:"number",default:5,min:2,max:15,step:1}}. Use numbers without quotes or units. Prefer derived:{food:{expr:"state.people * state.days * 100"}} to a deep op/args tree. children, options, columns and expression args are direct arrays. The host normalizes plain numeric strings only in explicitly numeric fields and exact {item:[...]} wrappers only at declared array positions; ordinary text/data retain their types.
12
- - Host defaults use rounded surfaces and soft theme-aware gradients. For guided canvas designs prefer 18..24px surface radii and subtle gradient fills with legible text. Let content determine height: no empty spacer sections, min-height:100vh or large fixed min-heights. A canvas auto-fits its rendered content after commit, including shrinking after filtering; height is only the initial placeholder. Use SVG viewBox bounds close to actual graphics plus reasonable padding.
13
+ - Host defaults use readable editorial typography, quiet backgrounds, light borders and restrained accents. For guided canvas designs prefer 12..18px radii and flat surfaces; gradients are optional and must not replace information hierarchy. Let content determine height: no empty spacer sections, min-height:100vh or large fixed min-heights. A canvas auto-fits its rendered content after commit, including shrinking after filtering; height is only the initial placeholder. Use SVG viewBox bounds close to actual graphics plus reasonable padding.
13
14
  - metric.value accepts a finite number or display string/boolean. Numbers use decimals and unit; strings display verbatim ("约 12 GiB", "待确认"). Calculations and chart series still require real numbers, never formatted strings.
14
15
  - IDs, data/state/derived keys, column keys and formula references support Chinese letters and underscores. Use 1..64 letters/numbers/underscores, start with a letter/underscore, no spaces/dots/hyphens; never constructor/prototype/__proto__. Labels/text can be natural language.
15
16
  - Persist a canvas checklist/list/object with state:{checklist:{type:"json",default:[]}} and DSHUI.setState({checklist:nextArray}). json state allows bounded arrays/objects, no options; native input/select cannot edit it. A native checklist uses separate boolean states and toggle nodes. Static lists belong in nodes/data, not state. String state with an array/object default and no options is canonicalized to json without losing its structure.
package/lib/build-info.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by npm run build.
2
- export const BUILD_INFO = Object.freeze({"version":"0.5.6","id":"0.5.6+1a92fbfce0c2","sourceHash":"1a92fbfce0c23812d59332a0de554fd7fd914d44fd715ab8e056ca0f19193bcf"});
2
+ export const BUILD_INFO = Object.freeze({"version":"0.7.0","id":"0.7.0+6adc22f63978","sourceHash":"6adc22f639783dee4cfb7400828727748c020f09b8e912e4500dde67fc871562"});
@@ -1,12 +1,12 @@
1
1
  // Optional primitives for model-authored surfaces. Host theme remains authoritative.
2
2
  export const CANVAS_SYSTEM_CSS = `
3
- :root{--canvas-radius:20px;--canvas-gap:16px;--canvas-positive:#167456;--canvas-negative:#b13b3b;--canvas-warning:#875b15}
3
+ :root{--canvas-radius:16px;--canvas-gap:16px;--canvas-positive:#167456;--canvas-negative:#b13b3b;--canvas-warning:#875b15}
4
4
  :root[data-canvas-scheme=dark]{--canvas-positive:#78d9b4;--canvas-negative:#ffaaaa;--canvas-warning:#edc87e}
5
5
  #canvas{display:flow-root}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
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
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
8
  .ui-flow{display:flex;align-items:center;justify-content:center;gap:16px;padding:24px 8px;flex-wrap:wrap}.ui-step{display:grid;gap:6px;min-width:130px;max-width:240px;padding:16px 20px;border:1px solid var(--canvas-line);border-radius:6px;background:var(--canvas-bg)}.ui-step strong{font-size:16px;font-weight:600}.ui-step span{font-size:14px;color:var(--canvas-muted)}.ui-arrow{color:var(--canvas-muted);font-size:22px}.ui-callout{border-inline-start:3px solid var(--canvas-accent);padding:8px 16px;color:var(--canvas-ink)}
9
- .ui-surface,.ui-step{border-radius:var(--canvas-radius);background:linear-gradient(135deg,color-mix(in srgb,var(--canvas-accent,#3e60d3) 9%,var(--canvas-bg,#fff)),var(--canvas-soft,#f3f5f8))}button,input:not([type=checkbox]):not([type=radio]):not([type=range]),select,textarea{border-radius:14px}.ui-primary{background:linear-gradient(120deg,var(--canvas-accent,#3e60d3),color-mix(in srgb,var(--canvas-accent,#3e60d3) 85%,#152647))}.ui-callout{border-radius:14px;background:linear-gradient(120deg,color-mix(in srgb,var(--canvas-accent,#3e60d3) 7%,var(--canvas-bg,#fff)),var(--canvas-bg,#fff))}
9
+ .ui-surface,.ui-step{border-radius:var(--canvas-radius);background:var(--canvas-bg,#fff)}button,input:not([type=checkbox]):not([type=radio]):not([type=range]),select,textarea{border-radius:12px}button{border-radius:999px}.ui-primary{background:var(--canvas-ink,#182537);color:var(--canvas-bg,#fff)}.ui-callout{border-radius:0 12px 12px 0;background:color-mix(in srgb,var(--canvas-accent,#3e60d3) 5%,var(--canvas-bg,#fff))}
10
10
  @media(max-width:480px){.ui-flow{flex-direction:column;padding:16px 8px;gap:12px}.ui-step{width:100%;max-width:none}.ui-arrow{transform:rotate(90deg)}}
11
11
  @media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}`;
12
12
 
@@ -14,5 +14,5 @@ export const CANVAS_SYSTEM_GUIDE = `The canvas includes optional .ui-stack, .ui-
14
14
  For a small static diagram, .ui-flow arranges .ui-step nodes separated by .ui-arrow; each step uses strong + span. .ui-callout emphasizes one sentence. These work with no custom CSS or JavaScript and adapt to narrow screens.
15
15
  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.
16
16
  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.
17
- 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.
17
+ Keep CSS small and put a useful initial view in html. Source field order is arbitrary. UI is shown only after the assistant turn ends and scripts activate after state restoration; there is no live tool-time preview. Do not claim a saved document has been visually checked without evidence.
18
18
  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.`;
package/lib/catalog.js CHANGED
@@ -1,93 +1,95 @@
1
- import { UI_ICONS, UI_TONES } from './presentation.js';
2
- import { componentGuide } from './components.js';
3
- import { AUTHORING_GUIDE } from './authoring-guide.js';
4
- export { sceneExamples } from './scenes.js';
5
- export { bicycleExample, oscillatorExample } from './canvas-example.js';
6
- export const PROTOCOL_GUIDE = AUTHORING_GUIDE+`\n\nDSH UI v1. Call render_ui({document: ...}) directly when visualization helps. Do not call it inside run_code/PTC in this version.
7
- Formula shorthand: {expr:"state.V * state.V / state.R"} compiles to the same validated interpreter. Supports explicit state.x/data.x/derived.x/item.x references, + - * /, == != < <= > >=, parentheses, condition ? yes : no, min/max/round/abs, double-quoted strings and booleans. Use it in derived, node text/value/max/rows or map.fields. No arbitrary functions, property chains, statements or side effects; complex animation belongs in render_canvas. Example derived:{power:{expr:"state.V * state.V / state.R"}}, metric.value:{ref:"derived.power"}.
8
- Document: {version:1,title:string,fallback:string,presentation?:{layout,accent,density},state?:{},data?:{},derived?:{},nodes:[],actions?:{}}.
9
- Scene layouts: dashboard (metrics/trends), workbench (parameter controls/results), comparison (parallel choice cards), guide (ordered instructions), status (observed task snapshot), report (reading and evidence). Accent: blue/teal/violet/amber/rose; density: comfortable/compact. Omit to keep the standard design. Choose based on the task, not at random. Card surface: outline/soft/featured. Featured indicates discussion focus, not an automatic recommendation.
10
- Additional nodes: badge {id,type,text,icon?,tone?}; progress {id,type,label,value,max,unit?,icon?,tone?} where values may be bindings and 0 <= value <= positive max; steps {id,type,items:[{title,description?,status?:"pending"|"active"|"done"|"error"}]} (direct array of 1..12 items, not {item:[...]}; omit status for static guides); details {id,type,title,children,open?:boolean,icon?,tone?}. Progress and steps are declared snapshots, not a live host task subscription. Only claim real progress/completion from actual tool evidence; mark demo data visibly. Details keeps the content available in text fallback.
11
- Streaming: use ONE normal native render_ui call. Write version,title,fallback,presentation,state,data,derived,actions BEFORE nodes where possible. The client previews completed, validated top-level subtrees as arguments stream; incomplete nodes/strings/calculations wait. Preview controls are disabled and never save state or send actions. Only a successful Host result activates the durable UI. No separate ui.begin/patch calls are required. A cancelled/failed generation has no interactive document. Prefer several small meaningful top-level sections over one enormous wrapping card.
12
- All IDs and dictionary keys: Unicode letters (including Chinese) or underscore, followed by letters/marks/digits/underscore, max64; unique node IDs. Standard components reject HTML, JS, URLs, styles and callbacks. The sandbox component below is the explicit exception; unlisted fields are always rejected.
13
- Freeform canvas: {id,type:"sandbox",title,html,css?:string,script?:string,height?:160..1000,fallback,stateKeys?:string[],dataKeys?:string[],derivedKeys?:string[]}. Use it for custom SVG diagrams, animation, simulations and bespoke interfaces that fixed components cannot express. HTML is a body fragment. Put CSS in css and ALL JavaScript in script (not inline handlers, script tags or remote imports). Inline SVG is supported. Each source field allows up to 49152 characters; the total document remains 128KiB. The iframe has no same-origin privilege, remote resources/network APIs are blocked by CSP, and no host tools are exposed. Never put credentials or private unrelated data in a canvas. It is browser origin isolation, not a CPU/memory hard sandbox.
14
- Canvas API: window.DSHUI.snapshot -> {state,data,derived,disabled,theme,visible}; DSHUI.animate(fn) returns cleanup and calls fn({time,delta,reducedMotion,visible}), time/delta in seconds, delta capped at 0.05. It pauses offscreen/background and supplies a static delta=0 callback for reduced motion; integrate delta for pause/resume without jumps. DSHUI.subscribe(snapshot=>{...}) immediately supplies current selected values and returns unsubscribe; DSHUI.setState({key:value,...}) requests an atomic Host-validated update of declared stateKeys. Use bare dictionary keys in stateKeys/dataKeys/derivedKeys. Only declared keys are exposed; state types/ranges/options remain enforced. Subscribe to drive DOM/SVG from host state; do not send the same state back from subscribe (feedback loop). Native controls and canvas share the saved state. Local animation may use requestAnimationFrame without saving every frame. No Agent/tool action API in this release. theme exposes bg,soft,ink,muted,line,accent,scheme; CSS variables --canvas-bg/soft/ink/muted/line/accent update automatically. Honor reduced motion, provide accessible labels and responsive layout. Canvas scripts activate only after successful tool commit and state restoration; previews show a placeholder. Use a complete useful fallback per canvas. Code or reset changes restart the frame, state changes do not. All transient internal animation state is reset on reload; declare important parameters in document.state.
15
- state: key -> {type:"number",default,min,max,step} or {type:"string",default,options?:scalar[]} or {type:"boolean",default} or {type:"json",default:array/object} (canvas structured state, max16KiB per value). select requires scalar options. Numeric defaults follow step from min.
16
- derived: key -> expression: {const:scalar}, {ref:"state.key"|"data.key"|"derived.key"}, or {op,args:expression[]}. Allowed ops add,mul,min,max (1..8 args); sub,div,eq,neq,lt,lte,gt,gte (2); round,abs (1); if (3, boolean condition). No cycles. Arithmetic requires finite numbers: add does NOT concatenate strings. For formatted labels use a metric's label/unit/decimals, or format text inside a sandbox; omit unused derived fields. if can return strings: {op:"if",args:[{op:"gt",args:[{ref:"derived.weightGiB"},{const:20}]},{const:"超过理论预算"},{const:"在理论预算内"}]} -> bind a text node to this derived result. No separate conditional-rendering field is needed.
17
- Text/metric values are literal scalars or {ref:...}. Table/chart rows accept inline arrays of objects, {ref:"data.rows"}, {ref:"derived.rows"}, or the shorthand string "data.rows"/"derived.rows". Table label is an optional caption. Static table example: data:{rows:[{name:"A",note:"示例"}]}, node:{id:"t",type:"table",rows:{ref:"data.rows"},columns:[{key:"name",label:"方案"},{key:"note",label:"说明"}]}. Dynamic rows: a derived root may be {map:{source:"data.samples",fields:{contextK:{op:"mul",args:[{ref:"state.contextK"},{ref:"item.fraction"}]},value:{ref:"derived.total"}}}}. source must be the string "data.samples", not an inline array; declare those object rows in document.data.samples. Internal field keys support Chinese identifiers such as 功率 and ASCII identifiers such as powerW; spaces/dots/hyphens belong in display labels. item.key is available only inside map fields. Each field is a normal scalar expression. No nested maps/callbacks; 1..16 fields, max500 generated rows across all maps; normal expression and calculation budgets apply. Derived output max128KiB. DSHUI.formatNumber(value, decimals=2) formats finite numbers with 0..6 decimal places and comma grouping, exactly matching native metrics and fallback; use it for shared readouts instead of mixing toFixed rounding. Use this for live charts/tables, never model calls on slider changes.
18
- Registered component fields (some optional; required values and types follow the rules above/below): ${componentGuide()}. Controls bind "state.key"; slider numeric, toggle boolean, select requires state options. Metric decimals 0..6. Table columns: [{key,label,unit?}]. Chart kind bar/line, finite numeric values, rows as a data/derived binding; line xScale linear positions numeric labelKey samples by their actual coordinate, category uses equal spacing. Use ascending samples for a curve.
19
- actions: key -> {type:"agent.followup",intent:string,stateKeys:string[]}. Button submits current selected parameters as a new conversation request; it does not perform a host action itself. All host actions still use ordinary DSH permissions.
20
- Presentation: heading/text/card/metric accept optional tone (${UI_TONES.join(', ')}); heading/text/card/metric/button accept optional icon (${UI_ICONS.join(', ')}). Fixed tokens only, no SVG/HTML/CSS/color strings. Defaults already include icons and themed spacing. Use accent for primary information, warning for assumptions/caveats, success/danger only for explicitly established facts. Tones are static, not conditional; use derived string text to report changing verdicts. Use row for 2-3 related metrics, card for a meaningful section, avoid repeated headings and deep decorative nesting. Do not use color as the only explanation.
21
- Always include a useful fallback, units, assumptions and data provenance in visible text. No invented measurements. Use local derived values rather than requesting inference on slider changes.
22
- Limits: 128KiB document, 160 nodes, 12 node nesting levels, 64 state/derived/data/action keys, 128 expression nodes, 4096 calculation steps, 8192 characters/string. Charts include an accessible data table. Unknown fields/components are rejected.`;
23
- export const calculatorExample = {
24
- version: 1, title: '理论权重体积计算器',
25
- fallback: '27B 参数、4 bit 时纯权重理论体积约 12.57 GiB。这是理论估算,不是实测文件大小或运行显存;不含量化元数据、混合精度层、KV cache 和运行缓冲区。',
26
- state: { parametersB: { type: 'number', default: 27, min: 1, max: 200, step: 1 }, bits: { type: 'number', default: 4, min: 2, max: 16, step: 1 } },
27
- data: {},
28
- derived: { weightGiB: { op: 'div', args: [{ op: 'mul', args: [{ ref: 'state.parametersB' }, { const: 1000000000 }, { ref: 'state.bits' }] }, { const: 8589934592 }] } },
29
- nodes: [
30
- { id: 'parameters', type: 'input', label: '参数量 B(十亿)', bind: 'state.parametersB' },
31
- { id: 'bits', type: 'slider', label: '假设有效位宽 bit', bind: 'state.bits' },
32
- { id: 'weight', type: 'metric', label: '纯权重理论体积', value: { ref: 'derived.weightGiB' }, unit: 'GiB', decimals: 2, icon: 'database', tone: 'accent' },
33
- { id: 'note', type: 'text', text: '理论估算,不是实际 GGUF 大小或推理显存。', tone: 'warning' },
34
- { id: 'followup', type: 'button', text: '按当前参数继续分析', action: 'analyze' }
35
- ],
36
- actions: { analyze: { type: 'agent.followup', intent: '分析当前参数下的量化选择,区分理论权重体积与真实运行开销;缺少信息时说明不确定性。', stateKeys: ['parametersB', 'bits'] } }
37
- };
38
- export const comparisonExample = {
39
- version: 1, title: '示例数据比较', fallback: '仅用于演示:A 为 12,B 为 18,C 为 15;不是实测性能。',
40
- data: { results: [{ name: 'A', value: 12 }, { name: 'B', value: 18 }, { name: 'C', value: 15 }] },
41
- nodes: [
42
- { id: 'note', type: 'text', text: '以下为演示数据,不是实测性能。', tone: 'warning' },
43
- { id: 'chart', type: 'chart', kind: 'bar', rows: { ref: 'data.results' }, labelKey: 'name', valueKey: 'value', label: '示例分数', unit: '分' },
44
- { id: 'table', type: 'table', rows: { ref: 'data.results' }, columns: [{ key: 'name', label: '方案' }, { key: 'value', label: '示例分数', unit: '分' }] }
45
- ]
46
- };
47
- export const budgetExample = {
48
- ...calculatorExample, title: '理论权重预算比较',
49
- fallback: '这是纯权重理论预算比较,不包含实际推理运行开销。默认 27B、4 bit、20 GiB 理论预算,权重约 12.57 GiB。',
50
- state: { ...calculatorExample.state, budgetGiB: { type: 'number', default: 20, min: 1, max: 96, step: 1 } },
51
- derived: { ...calculatorExample.derived,
52
- verdict: { op: 'if', args: [{ op: 'gt', args: [{ ref: 'derived.weightGiB' }, { ref: 'state.budgetGiB' }] }, { const: '超过纯权重理论预算;请降低位宽或提高预算。' }, { const: '在纯权重理论预算内;仍需另计运行开销。' }] }
53
- },
54
- nodes: [
55
- { id: 'assumptions', type: 'text', tone: 'warning', text: '以下均为理论估算,不能直接据此判断模型能否在显卡上运行。' },
56
- { id: 'settings', type: 'card', title: '调整参数', icon: 'sliders', children: [calculatorExample.nodes[0], calculatorExample.nodes[1], { id: 'budget', type: 'slider', label: '纯权重理论预算 GiB', bind: 'state.budgetGiB' }] },
57
- { id: 'results', type: 'row', children: [calculatorExample.nodes[2], { id: 'budgetMetric', type: 'metric', label: '理论预算', icon: 'target', tone: 'neutral', value: { ref: 'state.budgetGiB' }, unit: 'GiB', decimals: 0 }] },
58
- { id: 'verdictText', type: 'text', icon: 'info', text: { ref: 'derived.verdict' } }
59
- ], actions: {}
60
- };
61
- export const curveExample = {
62
- version: 1, title: '上下文与理论内存曲线',
63
- fallback: '理论估算,只计算纯权重与 KV cache,不包含运行缓冲、量化元数据或多卡分配。默认 27B、4.25 bpw、32K 上下文、48 KiB/token KV,权重约 13.36 GiB,权重加 KV 约 14.86 GiB。',
64
- state: {
65
- parametersB: { type: 'number', default: 27, min: 1, max: 200, step: 1 },
66
- bits: { type: 'number', default: 4.25, min: 2, max: 8, step: 0.25 },
67
- contextK: { type: 'number', default: 32, min: 8, max: 128, step: 8 },
68
- kvKib: { type: 'number', default: 48, min: 8, max: 192, step: 8 }
69
- },
70
- data: { samples: [{ fraction: 0.25 }, { fraction: 0.5 }, { fraction: 0.75 }, { fraction: 1 }] },
71
- derived: {
72
- weightGiB: calculatorExample.derived.weightGiB,
73
- totalGiB: { op: 'add', args: [{ ref: 'derived.weightGiB' }, { op: 'div', args: [{ op: 'mul', args: [{ ref: 'state.contextK' }, { ref: 'state.kvKib' }] }, { const: 1024 }] }] },
74
- curve: { map: { source: 'data.samples', fields: {
75
- contextK: { op: 'mul', args: [{ ref: 'state.contextK' }, { ref: 'item.fraction' }] },
76
- totalGiB: { op: 'add', args: [{ ref: 'derived.weightGiB' }, { op: 'div', args: [{ op: 'mul', args: [{ ref: 'state.contextK' }, { ref: 'item.fraction' }, { ref: 'state.kvKib' }] }, { const: 1024 }] }] }
77
- } } }
78
- },
79
- nodes: [
80
- { id: 'note', type: 'text', tone: 'warning', text: '理论值,不是实测推理显存。仅计纯权重 + KV cache,不含运行缓冲、量化元数据或多卡分配。' },
81
- { id: 'controls', type: 'card', title: '参数调整', icon: 'sliders', children: [
82
- calculatorExample.nodes[0], calculatorExample.nodes[1],
83
- { id: 'context', type: 'slider', label: '最大上下文 K tokens', bind: 'state.contextK' },
84
- { id: 'kv', type: 'slider', label: '每 token KV 开销 KiB', bind: 'state.kvKib' }
85
- ] },
86
- { id: 'metric', type: 'metric', label: '最大上下文下的理论权重 + KV', icon: 'database', value: { ref: 'derived.totalGiB' }, unit: 'GiB', decimals: 2 },
87
- { id: 'curveChart', type: 'chart', kind: 'line', xScale: 'linear', label: '上下文 K tokens → 权重 + KV', unit: 'GiB', rows: { ref: 'derived.curve' }, labelKey: 'contextK', valueKey: 'totalGiB' },
88
- { id: 'curveTable', type: 'table', rows: { ref: 'derived.curve' }, columns: [{ key: 'contextK', label: '上下文', unit: 'K tokens' }, { key: 'totalGiB', label: '理论内存', unit: 'GiB' }] },
89
- { id: 'followup', type: 'button', text: '按当前曲线继续分析', action: 'analyze' }
90
- ],
91
- actions: { analyze: { type: 'agent.followup', intent: '依据当前参数与 derived.curve 理论曲线分析上下文和内存取舍,说明尚未计入的实际运行开销。', stateKeys: ['parametersB', 'bits', 'contextK', 'kvKib'] } }
92
- };
1
+ import { UI_ICONS, UI_TONES } from './presentation.js';
2
+ import { componentGuide } from './components.js';
3
+ import { AUTHORING_GUIDE } from './authoring-guide.js';
4
+ import {VISUAL_GUIDE} from './visual.js';
5
+ export { sceneExamples } from './scenes.js';
6
+ export { bicycleExample, oscillatorExample } from './canvas-example.js';
7
+ export const PROTOCOL_GUIDE = VISUAL_GUIDE+'\n\n'+AUTHORING_GUIDE+`\n\nDSH UI v1. Call render_ui({document: ...}) directly when visualization helps. Do not call it inside run_code/PTC in this version.
8
+ Formula shorthand: {expr:"state.V * state.V / state.R"} compiles to the same validated interpreter. Supports explicit state.x/data.x/derived.x/item.x references, + - * /, == != < <= > >=, parentheses, condition ? yes : no, min/max/round/ceil/floor/abs, double-quoted strings and booleans. Use it in derived, node text/value/max/rows or map.fields. No arbitrary functions, property chains, statements or side effects; complex animation belongs in render_canvas. Example derived:{power:{expr:"state.V * state.V / state.R"}}, metric.value:{ref:"derived.power"}.
9
+ Document: {version:1,title:string,fallback:string,presentation?:{layout,accent,density,preset},state?:{},data?:{},derived?:{},nodes:[],actions?:{}}.
10
+ Scene layouts: dashboard (metrics/trends), workbench (parameter controls/results), comparison (parallel choice cards), guide (ordered instructions), status (observed task snapshot), report (reading and evidence). Accent: blue/teal/violet/amber/rose; density: comfortable/compact. Omit to keep the standard design. Choose based on the task, not at random. Card surface: outline/soft/featured. Featured indicates discussion focus, not an automatic recommendation.
11
+ Additional nodes: badge {id,type,text,icon?,tone?}; progress {id,type,label,value,max,unit?,icon?,tone?} where values may be bindings and 0 <= value <= positive max; steps {id,type,items:[{title,description?,status?:"pending"|"active"|"done"|"error"}]} (direct array of 1..12 items, not {item:[...]}; omit status for static guides); details {id,type,title,children,open?:boolean,icon?,tone?}. Progress and steps are declared snapshots, not a live host task subscription. Only claim real progress/completion from actual tool evidence; mark demo data visibly. Details keeps the content available in text fallback.
12
+ Display lifecycle: submit native render_text/render_ui/render_canvas directly. Several independent UI documents may be created in one answer. Complete validated native blocks appear progressively as read-only previews. Keep previews visible through later reasoning and text; durable turn/end activates controls and scripts. Incomplete/failed tools never become interactive. Use small meaningful documents; no separate ui.begin/append calls.
13
+ All IDs and dictionary keys: Unicode letters (including Chinese) or underscore, followed by letters/marks/digits/underscore, max64; unique node IDs. Standard components reject HTML, JS, arbitrary styles and callbacks. link/button href explicitly supports http/https URLs; path uses the host file opener on user click. The sandbox component below is the explicit exception; unlisted fields are always rejected.
14
+ Freeform canvas: {id,type:"sandbox",title,html,css?:string,script?:string,height?:160..1000,fallback,stateKeys?:string[],dataKeys?:string[],derivedKeys?:string[],libraries?:["mechanism"]}. For internal assemblies and mechanical relations, ui_catalog topic=mechanism provides an opt-in local drawing/kinematics helper and three complete examples (assembly/gears/crank). Use it for custom SVG diagrams, animation, simulations and bespoke interfaces that fixed components cannot express. HTML is a body fragment. Put CSS in css and ALL JavaScript in script (not inline handlers, script tags or remote imports). Inline SVG is supported. Each source field allows up to 49152 characters; the total document remains 128KiB. The iframe has no same-origin privilege, remote resources/network APIs are blocked by CSP, and no host tools are exposed. Never put credentials or private unrelated data in a canvas. It is browser origin isolation, not a CPU/memory hard sandbox.
15
+ Canvas API: window.DSHUI.snapshot -> {state,data,derived,disabled,theme,visible}; DSHUI.animate(fn) returns cleanup and calls fn({time,delta,reducedMotion,visible}), time/delta in seconds, delta capped at 0.05. It pauses offscreen/background and supplies a static delta=0 callback for reduced motion; integrate delta for pause/resume without jumps. DSHUI.subscribe(snapshot=>{...}) immediately supplies current selected values and returns unsubscribe; DSHUI.setState({key:value,...}) requests an atomic Host-validated update of declared stateKeys. Use bare dictionary keys in stateKeys/dataKeys/derivedKeys. Only declared keys are exposed; state types/ranges/options remain enforced. Subscribe to drive DOM/SVG from host state; do not send the same state back from subscribe (feedback loop). Native controls and canvas share the saved state. Local animation may use requestAnimationFrame without saving every frame. No Agent/tool action API in this release. theme exposes bg,soft,ink,muted,line,accent,scheme; CSS variables --canvas-bg/soft/ink/muted/line/accent update automatically. Honor reduced motion, provide accessible labels and responsive layout. Canvas scripts activate only after successful tool commit and state restoration; previews show inert completed HTML/CSS. Use a complete useful fallback per canvas. Code or reset changes restart the frame, state changes do not. All transient internal animation state is reset on reload; declare important parameters in document.state.
16
+ state: key -> {type:"number",default,min,max,step} or {type:"string",default,options?:scalar[]} or {type:"boolean",default} or {type:"json",default:array/object} (canvas structured state, max16KiB per value). select requires scalar options. Numeric defaults follow step from min.
17
+ derived: key -> expression: {const:scalar}, {ref:"state.key"|"data.key"|"derived.key"}, or {op,args:expression[]}. Allowed ops add,mul,min,max (1..8 args); sub,div,eq,neq,lt,lte,gt,gte (2); round,ceil,floor,abs (1); if (3, boolean condition). No cycles. Arithmetic requires finite numbers: add does NOT concatenate strings. For formatted labels use a metric's label/unit/decimals, or format text inside a sandbox; omit unused derived fields. if can return strings: {op:"if",args:[{op:"gt",args:[{ref:"derived.weightGiB"},{const:20}]},{const:"超过理论预算"},{const:"在理论预算内"}]} -> bind a text node to this derived result. No separate conditional-rendering field is needed.
18
+ Text/metric values are literal scalars or {ref:...}. Table/chart rows accept inline arrays of objects, {ref:"data.rows"}, {ref:"derived.rows"}, or the shorthand string "data.rows"/"derived.rows". Table label is an optional caption. Static table example: data:{rows:[{name:"A",note:"示例"}]}, node:{id:"t",type:"table",rows:{ref:"data.rows"},columns:[{key:"name",label:"方案"},{key:"note",label:"说明"}]}. For a short live breakdown prefer inline formula cells: rows:[{label:"餐饮",value:{ref:"derived.mealTotal"}},{label:"住宿",value:{expr:"ceil(state.people / 2) * state.room"}}], labelKey:"label",valueKey:"value". These cells recalculate locally; data remains literal and never executes formulas. Dynamic rows for many samples: a derived root may be {map:{source:"data.samples",fields:{contextK:{op:"mul",args:[{ref:"state.contextK"},{ref:"item.fraction"}]},value:{ref:"derived.total"}}}}. source must be the string "data.samples", not an inline array; declare those object rows in document.data.samples. Internal field keys support Chinese identifiers such as 功率 and ASCII identifiers such as powerW; spaces/dots/hyphens belong in display labels. item.key is available only inside map fields. Each field is a normal scalar expression. No nested maps/callbacks; 1..16 fields, max500 generated rows across all maps; normal expression and calculation budgets apply. Derived output max128KiB. DSHUI.formatNumber(value, decimals=2) formats finite numbers with 0..6 decimal places and comma grouping, exactly matching native metrics and fallback; use it for shared readouts instead of mixing toFixed rounding. Use this for live charts/tables, never model calls on slider changes.
19
+ Registered component fields (some optional; required values and types follow the rules above/below): ${componentGuide()}. Controls bind "state.key"; slider numeric, toggle boolean, select requires state options. Metric decimals 0..6. Table columns: [{key,label,unit?}]. Chart kind bar/line, finite numeric values, rows as a data/derived binding; line xScale linear positions numeric labelKey samples by their actual coordinate, category uses equal spacing. Use ascending samples for a curve.
20
+ actions: key -> {type:"agent.followup",intent:string,stateKeys:string[]}. Button submits current selected parameters as a new conversation request; it does not perform a host action itself. All host actions still use ordinary DSH permissions.
21
+ Presentation: heading/text/card/metric accept optional tone (${UI_TONES.join(', ')}); heading/text/card/metric/button accept optional icon (${UI_ICONS.join(', ')}). Fixed tokens only, no SVG/HTML/CSS/color strings. Defaults already include icons and themed spacing. Use accent for primary information, warning for assumptions/caveats, success/danger only for explicitly established facts. Tones are static, not conditional; use derived string text to report changing verdicts. Use row for 2-3 related metrics, card for a meaningful section, avoid repeated headings and deep decorative nesting. Do not use color as the only explanation.
22
+ Always include a useful fallback, units, assumptions and data provenance in visible text. No invented measurements. Use local derived values rather than requesting inference on slider changes.
23
+ Limits: 128KiB document, 160 nodes, 12 node nesting levels, 64 state/derived/data/action keys, 128 expression nodes, 4096 calculation steps, 8192 characters/string. Charts include an accessible data table. Unknown fields/components are rejected.`;
24
+ export const calculatorExample = {
25
+ version: 1, title: '理论权重体积计算器',
26
+ fallback: '27B 参数、4 bit 时纯权重理论体积约 12.57 GiB。这是理论估算,不是实测文件大小或运行显存;不含量化元数据、混合精度层、KV cache 和运行缓冲区。',
27
+ state: { parametersB: { type: 'number', default: 27, min: 1, max: 200, step: 1 }, bits: { type: 'number', default: 4, min: 2, max: 16, step: 1 } },
28
+ data: {},
29
+ derived: { weightGiB: { op: 'div', args: [{ op: 'mul', args: [{ ref: 'state.parametersB' }, { const: 1000000000 }, { ref: 'state.bits' }] }, { const: 8589934592 }] } },
30
+ nodes: [
31
+ { id: 'parameters', type: 'input', label: '参数量 B(十亿)', bind: 'state.parametersB' },
32
+ { id: 'bits', type: 'slider', label: '假设有效位宽 bit', bind: 'state.bits' },
33
+ { id: 'weight', type: 'metric', label: '纯权重理论体积', value: { ref: 'derived.weightGiB' }, unit: 'GiB', decimals: 2, icon: 'database', tone: 'accent' },
34
+ { id: 'note', type: 'text', text: '理论估算,不是实际 GGUF 大小或推理显存。', tone: 'warning' },
35
+ { id: 'followup', type: 'button', text: '按当前参数继续分析', action: 'analyze' }
36
+ ],
37
+ actions: { analyze: { type: 'agent.followup', intent: '分析当前参数下的量化选择,区分理论权重体积与真实运行开销;缺少信息时说明不确定性。', stateKeys: ['parametersB', 'bits'] } }
38
+ };
39
+ export const comparisonExample = {
40
+ version: 1, title: '示例数据比较', fallback: '仅用于演示:A 为 12,B 为 18,C 为 15;不是实测性能。',
41
+ data: { results: [{ name: 'A', value: 12 }, { name: 'B', value: 18 }, { name: 'C', value: 15 }] },
42
+ nodes: [
43
+ { id: 'note', type: 'text', text: '以下为演示数据,不是实测性能。', tone: 'warning' },
44
+ { id: 'chart', type: 'chart', kind: 'bar', rows: { ref: 'data.results' }, labelKey: 'name', valueKey: 'value', label: '示例分数', unit: '分' },
45
+ { id: 'table', type: 'table', rows: { ref: 'data.results' }, columns: [{ key: 'name', label: '方案' }, { key: 'value', label: '示例分数', unit: '分' }] }
46
+ ]
47
+ };
48
+ export const budgetExample = {
49
+ ...calculatorExample, title: '理论权重预算比较',
50
+ fallback: '这是纯权重理论预算比较,不包含实际推理运行开销。默认 27B、4 bit、20 GiB 理论预算,权重约 12.57 GiB。',
51
+ state: { ...calculatorExample.state, budgetGiB: { type: 'number', default: 20, min: 1, max: 96, step: 1 } },
52
+ derived: { ...calculatorExample.derived,
53
+ verdict: { op: 'if', args: [{ op: 'gt', args: [{ ref: 'derived.weightGiB' }, { ref: 'state.budgetGiB' }] }, { const: '超过纯权重理论预算;请降低位宽或提高预算。' }, { const: '在纯权重理论预算内;仍需另计运行开销。' }] }
54
+ },
55
+ nodes: [
56
+ { id: 'assumptions', type: 'text', tone: 'warning', text: '以下均为理论估算,不能直接据此判断模型能否在显卡上运行。' },
57
+ { id: 'settings', type: 'card', title: '调整参数', icon: 'sliders', children: [calculatorExample.nodes[0], calculatorExample.nodes[1], { id: 'budget', type: 'slider', label: '纯权重理论预算 GiB', bind: 'state.budgetGiB' }] },
58
+ { id: 'results', type: 'row', children: [calculatorExample.nodes[2], { id: 'budgetMetric', type: 'metric', label: '理论预算', icon: 'target', tone: 'neutral', value: { ref: 'state.budgetGiB' }, unit: 'GiB', decimals: 0 }] },
59
+ { id: 'verdictText', type: 'text', icon: 'info', text: { ref: 'derived.verdict' } }
60
+ ], actions: {}
61
+ };
62
+ export const curveExample = {
63
+ version: 1, title: '上下文与理论内存曲线',
64
+ fallback: '理论估算,只计算纯权重与 KV cache,不包含运行缓冲、量化元数据或多卡分配。默认 27B、4.25 bpw、32K 上下文、48 KiB/token KV,权重约 13.36 GiB,权重加 KV 约 14.86 GiB。',
65
+ state: {
66
+ parametersB: { type: 'number', default: 27, min: 1, max: 200, step: 1 },
67
+ bits: { type: 'number', default: 4.25, min: 2, max: 8, step: 0.25 },
68
+ contextK: { type: 'number', default: 32, min: 8, max: 128, step: 8 },
69
+ kvKib: { type: 'number', default: 48, min: 8, max: 192, step: 8 }
70
+ },
71
+ data: { samples: [{ fraction: 0.25 }, { fraction: 0.5 }, { fraction: 0.75 }, { fraction: 1 }] },
72
+ derived: {
73
+ weightGiB: calculatorExample.derived.weightGiB,
74
+ totalGiB: { op: 'add', args: [{ ref: 'derived.weightGiB' }, { op: 'div', args: [{ op: 'mul', args: [{ ref: 'state.contextK' }, { ref: 'state.kvKib' }] }, { const: 1024 }] }] },
75
+ curve: { map: { source: 'data.samples', fields: {
76
+ contextK: { op: 'mul', args: [{ ref: 'state.contextK' }, { ref: 'item.fraction' }] },
77
+ totalGiB: { op: 'add', args: [{ ref: 'derived.weightGiB' }, { op: 'div', args: [{ op: 'mul', args: [{ ref: 'state.contextK' }, { ref: 'item.fraction' }, { ref: 'state.kvKib' }] }, { const: 1024 }] }] }
78
+ } } }
79
+ },
80
+ nodes: [
81
+ { id: 'note', type: 'text', tone: 'warning', text: '理论值,不是实测推理显存。仅计纯权重 + KV cache,不含运行缓冲、量化元数据或多卡分配。' },
82
+ { id: 'controls', type: 'card', title: '参数调整', icon: 'sliders', children: [
83
+ calculatorExample.nodes[0], calculatorExample.nodes[1],
84
+ { id: 'context', type: 'slider', label: '最大上下文 K tokens', bind: 'state.contextK' },
85
+ { id: 'kv', type: 'slider', label: '每 token KV 开销 KiB', bind: 'state.kvKib' }
86
+ ] },
87
+ { id: 'metric', type: 'metric', label: '最大上下文下的理论权重 + KV', icon: 'database', value: { ref: 'derived.totalGiB' }, unit: 'GiB', decimals: 2 },
88
+ { id: 'curveChart', type: 'chart', kind: 'line', xScale: 'linear', label: '上下文 K tokens → 权重 + KV', unit: 'GiB', rows: { ref: 'derived.curve' }, labelKey: 'contextK', valueKey: 'totalGiB' },
89
+ { id: 'curveTable', type: 'table', rows: { ref: 'derived.curve' }, columns: [{ key: 'contextK', label: '上下文', unit: 'K tokens' }, { key: 'totalGiB', label: '理论内存', unit: 'GiB' }] },
90
+ { id: 'followup', type: 'button', text: '按当前曲线继续分析', action: 'analyze' }
91
+ ],
92
+ actions: { analyze: { type: 'agent.followup', intent: '依据当前参数与 derived.curve 理论曲线分析上下文和内存取舍,说明尚未计入的实际运行开销。', stateKeys: ['parametersB', 'bits', 'contextK', 'kvKib'] } }
93
+ };
94
+
93
95
 
package/lib/components.js CHANGED
@@ -1,20 +1,25 @@
1
1
  // Shared attribute vocabulary for validation and model-facing capability discovery.
2
2
  const define = (fields, purpose, presentation = '') => Object.freeze({ fields: Object.freeze(fields), purpose, presentation });
3
3
  export const COMPONENTS = Object.freeze({
4
- heading: define(['text'], 'Section heading', 'tone'), text: define(['text'], 'Readable explanation or bound text', 'tone'),
5
- row: define(['children'], 'Wrapping parallel content'), column: define(['children'], 'Ordered vertical content'),
4
+ visual: define(['spec','stateKey','warnings'], 'Extended native charts, tables, plots, quiz, forms, diagrams, media and 3D'),
5
+ heading: define(['text','spans','size','weight','color','align','lineHeight'], 'Section heading', 'tone'), text: define(['text','spans','size','weight','color','align','lineHeight','copyable','title'], 'Readable explanation; copyable:true adds a reusable text panel', 'tone'),
6
+ code: define(['code','lang','title','wrap'], 'Literal copyable code; displayed only, never executed'),
7
+ callout: define(['text','spans','size','weight','color','align','lineHeight'], 'A short emphasized paragraph', 'tone'),
8
+ row: define(['children','columns','gap'], 'Wrapping parallel content'), column: define(['children','gap'], 'Ordered vertical content'),
6
9
  card: define(['title', 'children', 'surface'], 'Meaningful grouped section', 'tone'),
7
10
  details: define(['title', 'children', 'open'], 'Collapsible supporting content', 'tone'),
8
11
  badge: define(['text'], 'Short explicit status label', 'tone'),
9
12
  progress: define(['label', 'value', 'max', 'unit'], 'Declared numeric progress snapshot', 'tone'),
10
- steps: define(['items'], 'Ordered declared steps'),
13
+ steps: define(['title','items'], 'Ordered declared steps with an optional heading'),
11
14
  metric: define(['label', 'value', 'unit', 'decimals'], 'Number with units, or a literal/bound status string', 'tone'),
12
15
  input: define(['label', 'bind'], 'String or numeric state input'), slider: define(['label', 'bind'], 'Numeric state control'),
13
- select: define(['label', 'bind'], 'Selection from declared state options'), toggle: define(['label', 'bind'], 'Boolean state control'),
14
- button: define(['text', 'action'], 'Declared Agent follow-up', 'icon'),
16
+ select: define(['label', 'bind','variant'], 'Selection from declared state options; variant:segmented for 2..8 short options, dropdown otherwise'), toggle: define(['label', 'bind'], 'Boolean state control'),
17
+ button: define(['text', 'action','href','path','variant'], 'Agent follow-up, web link or host file opener', 'icon'),
18
+ link: define(['text','href','path','variant'], 'User-clicked webpage or local file button', 'icon'),
19
+ divider: define(['label'], 'Quiet section divider'),
15
20
  table: define(['label', 'rows', 'columns'], 'Supplied or derived values; inline rows or data/derived reference'),
16
21
  chart: define(['kind', 'rows', 'labelKey', 'valueKey', 'label', 'unit', 'xScale'], 'Bar/line comparison with accessible data'),
17
- sandbox: define(['title', 'html', 'css', 'script', 'fallback', 'height', 'stateKeys', 'dataKeys', 'derivedKeys'], 'Freeform isolated HTML/SVG/CSS/JavaScript visualization')
22
+ sandbox: define(['title', 'html', 'css', 'script', 'fallback', 'height', 'libraries', 'stateKeys', 'dataKeys', 'derivedKeys'], 'Freeform isolated HTML/SVG/CSS/JavaScript visualization')
18
23
  });
19
24
  export function componentFields(type) {
20
25
  const c = Object.hasOwn(COMPONENTS, type) ? COMPONENTS[type] : undefined;
@@ -0,0 +1,18 @@
1
+ import {validateDocument} from './protocol.js';
2
+ import {repairFenceJson} from './visual-guard.js';
3
+ export function deliveryDiagnostics(document){
4
+ const diagnostics=[];
5
+ const walk=nodes=>{for(const node of nodes){if(node.type==='visual')for(const message of node.warnings??[])diagnostics.push({severity:'warning',path:node.id,message});if(node.children)walk(node.children)}};walk(document.nodes);
6
+ return diagnostics;
7
+ }
8
+ export function diagnoseDelivery(input){
9
+ try{const document=validateDocument(input),diagnostics=deliveryDiagnostics(document);return {valid:true,complete:diagnostics.length===0,document,diagnostics}}catch(e){return {valid:false,complete:false,diagnostics:[{severity:'error',path:'document',message:e.message}]}}
10
+ }
11
+ export function resolveSourceFence(raw,{svg=false}={}){
12
+ if(typeof raw!=='string'||raw.length>131072)return {valid:false,complete:false,diagnostics:[{severity:'error',path:'source',message:'Source exceeds 128 KiB'}]};
13
+ if(/^[a-f\d]{8}-[a-f\d-]{27,}$/i.test(raw.trim()))return {marker:true};
14
+ if(svg)return diagnoseDelivery({items:[{type:'svg',title:'SVG',code:raw}]});
15
+ let value,repair;
16
+ try{value=JSON.parse(raw)}catch(e){repair=repairFenceJson(raw);if(!repair)return {valid:false,complete:false,diagnostics:[{severity:'error',path:'source',message:'Invalid JSON: '+e.message.slice(0,240)}]};value=JSON.parse(repair.text)}
17
+ const result=diagnoseDelivery(value);if(repair&&result.valid)result.repairs=repair.repairs;return result;
18
+ }
@@ -0,0 +1,6 @@
1
+ import {stat} from 'node:fs/promises';
2
+ import {BUILD_INFO} from './build-info.js';
3
+ export function diagnosticsRoute(feedback){return async()=>{
4
+ const assets=await Promise.all(['visual.js','visual.css','mermaid.js','echarts-core.js','echarts-full.js','three.js','standalone-runtime.js','katex.css'].map(async name=>{try{const s=await stat(new URL('./assets/'+name,import.meta.url));return {name,available:s.isFile()&&s.size>0,bytes:s.size}}catch{return {name,available:false}}}));
5
+ return Response.json({build:BUILD_INFO,hostActive:true,feedbackHookRegistered:feedback.available,assets,assetRoute:'/api/plugins/dsh-generative-ui/assets/',permissions:'unchanged',checkedAt:new Date().toISOString()},{headers:{'Cache-Control':'no-store'}});
6
+ }}
@@ -0,0 +1,27 @@
1
+ import {validateDocument} from './protocol.js';
2
+ const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
3
+ function merge(a={},b={},label){const result={...a};for(const[k,v]of Object.entries(b)){if(Object.hasOwn(result,k)&&!same(result[k],v))throw Error('DSH UI: update cannot redefine '+label+'.'+k+'; preserve existing input/calculation definitions');result[k]=v}return result}
4
+ export function updateDocument(current,{mode,targetId,document}){
5
+ if(!['append','replace'].includes(mode))throw Error('DSH UI: mode must be append or replace');
6
+ const incoming=validateDocument(document),next=structuredClone(current);
7
+ const find=(list,id)=>{for(const[n,node]of list.entries()){if(node.id===id)return {list,index:n,node};const child=node.children&&find(node.children,id);if(child)return child}};
8
+ const singleVisualAppend=!targetId&&mode==='append'&&next.nodes.length===1&&next.nodes[0].type==='visual'&&incoming.nodes.length===1&&incoming.nodes[0].type==='visual';
9
+ const target=targetId?find(next.nodes,targetId):singleVisualAppend?{list:next.nodes,index:0,node:next.nodes[0]}:null;if(targetId&&!target)throw Error('DSH UI: targetId must be an existing node ID');
10
+ // An extended fragment gets a generated state slot. A targeted update keeps
11
+ // the existing visual's slot rather than introducing an unrelated default.
12
+ if(target?.node.type==='visual'&&incoming.nodes.length===1&&incoming.nodes[0].type==='visual')delete incoming.state[incoming.nodes[0].stateKey];
13
+ next.state=merge(next.state,incoming.state,'state');next.derived=merge(next.derived,incoming.derived,'derived');next.data=merge(next.data,incoming.data,'data');next.actions=merge(next.actions,incoming.actions,'actions');
14
+ if(target?.node.type==='visual'){
15
+ if(incoming.nodes.length!==1||incoming.nodes[0].type!=='visual')throw Error('DSH UI: visual target requires one extended title/items document');
16
+ const input=incoming.nodes[0],spec=target.node.spec;
17
+ target.node.spec=mode==='append'?{...spec,items:[...spec.items,...input.spec.items]}:input.spec;
18
+ target.node.warnings=input.warnings??[];
19
+ }else if(target){
20
+ if(mode==='append'){if(!Array.isArray(target.node.children))throw Error('DSH UI: append target needs children or a visual spec');target.node.children.push(...incoming.nodes)}
21
+ else {if(incoming.nodes.length!==1)throw Error('DSH UI: targeted replace needs exactly one node');target.list[target.index]={...incoming.nodes[0],id:target.node.id}}
22
+ }else if(mode==='append')next.nodes.push(...incoming.nodes);
23
+ else {next.nodes=incoming.nodes;next.title=incoming.title;next.fallback=incoming.fallback;if(incoming.presentation!==undefined)next.presentation=incoming.presentation;else delete next.presentation}
24
+ // Existing state keys remain available even after replacing display nodes.
25
+ // New controls receive defaults; stored values are migrated by the store.
26
+ return validateDocument(next);
27
+ }