@changfenhuang/dsh-genui 0.9.3 → 0.9.7

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 (50) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +7 -18
  3. package/README.zh-CN.md +13 -18
  4. package/SKILL.md +4 -2
  5. package/lib/assets/three.js +1 -1
  6. package/lib/client.js +3 -3
  7. package/lib/index.js +273 -61
  8. package/lib/types/client/GenuiBlock.d.ts +5 -2
  9. package/lib/types/client/TemplateDrawer.d.ts +7 -0
  10. package/lib/types/client/achievement-store.d.ts +35 -0
  11. package/lib/types/client/achievement-toast.d.ts +2 -0
  12. package/lib/types/client/achievements.d.ts +48 -0
  13. package/lib/types/client/blocks/advanced.d.ts +1 -1
  14. package/lib/types/client/blocks/image.d.ts +6 -0
  15. package/lib/types/client/blocks/state.d.ts +2 -0
  16. package/lib/types/client/fence-render.d.ts +3 -0
  17. package/lib/types/client/gallery.d.ts +3 -5
  18. package/lib/types/client/guard.d.ts +10 -5
  19. package/lib/types/client/panel.d.ts +4 -1
  20. package/lib/types/client/spec.d.ts +8 -1
  21. package/lib/types/client/templates.d.ts +22 -0
  22. package/lib/types/plugin/chart-contract.d.ts +7 -0
  23. package/lib/types/plugin/index.d.ts +1 -1
  24. package/package.json +9 -8
  25. package/src/client/GenuiBlock.module.css +141 -10
  26. package/src/client/GenuiBlock.tsx +53 -25
  27. package/src/client/PlotBlock.tsx +25 -10
  28. package/src/client/TemplateDrawer.module.css +152 -0
  29. package/src/client/TemplateDrawer.tsx +137 -0
  30. package/src/client/achievement-store.ts +151 -0
  31. package/src/client/achievement-toast.tsx +68 -0
  32. package/src/client/achievements.ts +111 -0
  33. package/src/client/blocks/advanced.tsx +34 -25
  34. package/src/client/blocks/charts.tsx +4 -4
  35. package/src/client/blocks/image.tsx +25 -0
  36. package/src/client/blocks/render-node.tsx +3 -0
  37. package/src/client/blocks/state.ts +2 -0
  38. package/src/client/dom-fence.tsx +12 -5
  39. package/src/client/fence-render.tsx +35 -11
  40. package/src/client/gallery.ts +4 -5
  41. package/src/client/guard.ts +125 -29
  42. package/src/client/index.tsx +13 -0
  43. package/src/client/panel.tsx +109 -10
  44. package/src/client/safe-math.ts +29 -1
  45. package/src/client/scene3d-core.ts +5 -2
  46. package/src/client/spec.ts +9 -1
  47. package/src/client/templates.ts +280 -0
  48. package/src/plugin/chart-contract.ts +103 -0
  49. package/src/plugin/index.ts +48 -2
  50. package/src/plugin/tool.ts +25 -4
@@ -32,6 +32,7 @@ import { renderGenuiFence, type GenuiFenceContext } from './fence-render.tsx'
32
32
  import { createPanelSlashSource } from './panel-command.ts'
33
33
  import { GenuiPanel, type GenuiPanelInjected } from './panel.tsx'
34
34
  import { GenuiToolView } from './toolview.tsx'
35
+ import { mountAchievementToasts } from './achievement-toast.tsx'
35
36
  import type { InputTriggerServiceContract } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
36
37
  import { assetUrl } from './asset-loader.ts'
37
38
 
@@ -77,6 +78,16 @@ function panelActionSend(ctx: Context, sessionId: SessionId): GenuiPanelInjected
77
78
  console.warn(`[genui] 面板动作 "${action}" 发送失败(session ${sessionId}):`, err instanceof Error ? err.message : String(err))
78
79
  })
79
80
  },
81
+ insertTemplate: (text) => {
82
+ // Same insert channel as dsh-memory's organizeMemory: the standard
83
+ // conversation.input.for(scope).setDraft — templates land in the
84
+ // composer draft, not a queued message, so the user can edit before
85
+ // sending.
86
+ const input = (conversation as unknown as {
87
+ input?: { for?(actx: unknown): { setDraft(text: string): void } | undefined }
88
+ } | undefined)?.input?.for?.(scoped)
89
+ input?.setDraft(text)
90
+ },
80
91
  }
81
92
  }
82
93
 
@@ -130,6 +141,8 @@ export function apply(ctx: Context): () => void {
130
141
  // a session usually hits a warm cache instead of paying the fetch on first
131
142
  // use. Never blocks first paint; the loader still handles a miss.
132
143
  prefetchGenuiAssets()
144
+ // Achievement toasts (0.9.5): a body-level stack independent of the panel.
145
+ disposers.push(mountAchievementToasts())
133
146
  // Keyed toolview: the harness dispatches 'tool.call.toolview' by wire tool
134
147
  // name; registering under 'render_ui' gives the tool's result card the
135
148
  // GenUI renderer (reading the repaired spec from result meta). The toolview
@@ -25,11 +25,13 @@
25
25
  */
26
26
  import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'
27
27
  import type {} from '@deepseek-ai/dsh-client-runtime/client'
28
+ import { IconChevronDownOutline14, IconChevronUpOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
28
29
  import { GenuiActionContext, type GenuiActionHandler } from './action-context.ts'
29
30
  import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
30
31
  import { GenuiBlock } from './GenuiBlock.tsx'
31
32
  import { ErrorBoundary } from './ErrorBoundary.tsx'
32
33
  import { panelStateKey } from './interaction-store.ts'
34
+ import { TemplateDrawer } from './TemplateDrawer.tsx'
33
35
  import { clearSessionPanel, getPanelExpandToken, getPanelSpec, setLocalPanel, subscribePanel, subscribePanelExpand } from './panel-store.ts'
34
36
  import css from './GenuiBlock.module.css'
35
37
 
@@ -37,10 +39,20 @@ import css from './GenuiBlock.module.css'
37
39
  const PANEL_HEIGHT_MIN = 120
38
40
  const PANEL_HEIGHT_MAX = 600
39
41
 
42
+ /** First-panel hint dismissal flag (module-level localStorage key):
43
+ * shown once beside the first rendered spec, never again. */
44
+ const ONBOARD_KEY = 'dsh.genui.panel-hint'
45
+
46
+ // Achievement telemetry re-exports (already injected at apply).
47
+ import { recordPanel, recordTemplateUse } from './achievement-store.ts'
48
+
40
49
  /** Injection face built per session in apply (scoped conversation send). */
41
50
  export interface GenuiPanelInjected {
42
51
  sessionId: string
43
52
  sendGenuiAction: GenuiActionHandler
53
+ /** Template center "try it": insert the template instruction into the
54
+ * current composer draft (standard conversation.input.for channel). */
55
+ insertTemplate: (text: string) => void
44
56
  }
45
57
 
46
58
  export type GenuiPanelProps = PropsRuntime<'conversation.input.dock'> & GenuiPanelInjected
@@ -51,7 +63,7 @@ export type GenuiPanelProps = PropsRuntime<'conversation.input.dock'> & GenuiPan
51
63
  * default so the dock never steals the message flow's scroll room; the
52
64
  * header always shows the current panel title.
53
65
  */
54
- export function GenuiPanel({ sessionId, sendGenuiAction }: GenuiPanelProps) {
66
+ export function GenuiPanel({ sessionId, sendGenuiAction, insertTemplate }: GenuiPanelProps) {
55
67
  const spec = useSyncExternalStore(subscribePanel, () => getPanelSpec(sessionId))
56
68
  const expandToken = useSyncExternalStore(subscribePanelExpand, () => getPanelExpandToken(sessionId))
57
69
  const [collapsed, setCollapsed] = useState(true)
@@ -60,6 +72,39 @@ export function GenuiPanel({ sessionId, sendGenuiAction }: GenuiPanelProps) {
60
72
  const [resizing, setResizing] = useState(false)
61
73
  const dragStart = useRef<{ y: number; height: number } | null>(null)
62
74
  const bodyRef = useRef<HTMLDivElement | null>(null)
75
+ // Template center (0.9.4) + achievements (0.9.5): the drawer host —
76
+ // null = closed, 'templates' | 'achievements' = open on that tab.
77
+ const [drawer, setDrawer] = useState<null | 'templates' | 'achievements'>(null)
78
+ const [flash, setFlash] = useState<string | null>(null)
79
+ const flashTimer = useRef(0)
80
+ const hasSpec = spec !== null && spec.items.length > 0
81
+ // First-panel hint: shown once beside the first rendered spec (never on the
82
+ // hero — the host composer hero flex-compresses dock items with no content,
83
+ // and the template button is a permanent header entry anyway).
84
+ const [hint, setHint] = useState<boolean>(() => {
85
+ try {
86
+ return hasSpec && localStorage.getItem(ONBOARD_KEY) !== '1'
87
+ } catch {
88
+ return hasSpec
89
+ }
90
+ })
91
+ useEffect(() => {
92
+ if (!hint) return
93
+ const t = window.setTimeout(() => {
94
+ try {
95
+ localStorage.setItem(ONBOARD_KEY, '1')
96
+ } catch {
97
+ // Private-mode storage failures are non-fatal: the hint may reappear.
98
+ }
99
+ setHint(false)
100
+ }, 6000)
101
+ return () => window.clearTimeout(t)
102
+ }, [hint])
103
+ // Achievement telemetry (0.9.5): one panel appearance per dock mount.
104
+ useEffect(() => {
105
+ if (!hasSpec) return
106
+ recordPanel()
107
+ }, [hasSpec])
63
108
 
64
109
  // Explicit expand requests (the /panel command) open the dock even when the
65
110
  // user collapsed it manually: the monotone token guarantees a fresh request
@@ -89,6 +134,9 @@ export function GenuiPanel({ sessionId, sendGenuiAction }: GenuiPanelProps) {
89
134
  }
90
135
  }, [resizing])
91
136
 
137
+ // Template-center flash timer must not outlive the dock.
138
+ useEffect(() => () => window.clearTimeout(flashTimer.current), [])
139
+
92
140
  /** Start a vertical resize drag on the panel's top edge. Dragging UP grows
93
141
  * the panel (the edge moves toward the message flow); dragging down shrinks
94
142
  * it. Pointer capture binds move/up/cancel to the HANDLE, so dragging past
@@ -126,7 +174,14 @@ export function GenuiPanel({ sessionId, sendGenuiAction }: GenuiPanelProps) {
126
174
  handle.addEventListener('pointercancel', onUp)
127
175
  }, [bodyHeight])
128
176
 
129
- if (spec === null || spec.items.length === 0) return null
177
+ if (!hasSpec) return null
178
+
179
+ const showFlash = (text: string): void => {
180
+ setFlash(text)
181
+ window.clearTimeout(flashTimer.current)
182
+ flashTimer.current = window.setTimeout(() => setFlash(null), 2500)
183
+ }
184
+
130
185
  return (
131
186
  <div className={css.panel} data-genui-panel>
132
187
  {/* Resize grip on the panel's TOP EDGE (above the header): the drag
@@ -149,8 +204,33 @@ export function GenuiPanel({ sessionId, sendGenuiAction }: GenuiPanelProps) {
149
204
  onClick={() => setCollapsed(c => !c)}
150
205
  >
151
206
  <span className={css.panelBadge}>面板</span>
152
- <span className={css.panelTitle}>{spec.title ?? 'GenUI 面板'}</span>
153
- <span className={css.panelChevron} aria-hidden>{collapsed ? '▸' : '▾'}</span>
207
+ <span className={css.panelTitle}>{spec?.title ?? (drawer !== null ? 'GenUI 探索' : 'GenUI 面板')}</span>
208
+ <span className={css.panelChevron} aria-hidden>
209
+ {/* Host-style glyphs (same icon set as the TodoDock header) instead of typed arrows. */}
210
+ {collapsed ? <IconChevronUpOutline14 /> : <IconChevronDownOutline14 />}
211
+ </span>
212
+ </button>
213
+ {/* Template center (0.9.4): one button — the discovery surface for
214
+ new users and a quick-start entry for everyone. */}
215
+ <button
216
+ type="button"
217
+ className={`${css.panelTpl}${drawer === 'templates' ? ` ${css.panelTplActive}` : ''}`}
218
+ aria-label="模板中心"
219
+ title="模板中心:预览并试用 GenUI 示例"
220
+ onClick={() => { setCollapsed(false); setDrawer(d => (d === 'templates' ? null : 'templates')) }}
221
+ >
222
+ 模板
223
+ </button>
224
+ {/* Achievements (0.9.5): the exploration trophies, rendered by GenUI
225
+ itself (dogfooding). */}
226
+ <button
227
+ type="button"
228
+ className={`${css.panelTpl}${drawer === 'achievements' ? ` ${css.panelTplActive}` : ''}`}
229
+ aria-label="探索成就"
230
+ title="探索成就:解锁徽章与进度"
231
+ onClick={() => { setCollapsed(false); setDrawer(d => (d === 'achievements' ? null : 'achievements')) }}
232
+ >
233
+ 成就
154
234
  </button>
155
235
  {/* In-place dismiss (issue #23): the same local override `/panel
156
236
  clear` applies — persists to localStorage, notifies subscribers,
@@ -165,6 +245,11 @@ export function GenuiPanel({ sessionId, sendGenuiAction }: GenuiPanelProps) {
165
245
  <span aria-hidden>✕</span>
166
246
  </button>
167
247
  </div>
248
+ {hint && drawer === null && (
249
+ <div className={css.panelHint} role="status">
250
+ 💡 这是 GenUI 会话面板:点右上角「模板」可浏览并试用示例
251
+ </div>
252
+ )}
168
253
  {!collapsed && (
169
254
  <div
170
255
  ref={bodyRef}
@@ -172,14 +257,28 @@ export function GenuiPanel({ sessionId, sendGenuiAction }: GenuiPanelProps) {
172
257
  data-genui-panel-body
173
258
  style={bodyHeight === null ? undefined : { height: bodyHeight }}
174
259
  >
175
- <GenuiActionContext.Provider value={sendGenuiAction}>
176
- <ErrorBoundary label="面板">
177
- {/* content-fingerprinted: same panel spec re-published restores its state */}
178
- <GenuiBlock spec={spec} stateKey={panelStateKey(sessionId, JSON.stringify(spec))} />
179
- </ErrorBoundary>
180
- </GenuiActionContext.Provider>
260
+ {drawer !== null ? (
261
+ <TemplateDrawer
262
+ tab={drawer}
263
+ onUse={(text) => {
264
+ recordTemplateUse()
265
+ insertTemplate(text)
266
+ showFlash('指令已插入输入框,发送即可让模型生成')
267
+ }}
268
+ />
269
+ ) : spec !== null ? (
270
+ <GenuiActionContext.Provider value={sendGenuiAction}>
271
+ <ErrorBoundary label="面板">
272
+ {/* content-fingerprinted: same panel spec re-published restores its state */}
273
+ <GenuiBlock spec={spec} stateKey={panelStateKey(sessionId, JSON.stringify(spec))} />
274
+ </ErrorBoundary>
275
+ </GenuiActionContext.Provider>
276
+ ) : null}
181
277
  </div>
182
278
  )}
279
+ {flash !== null && (
280
+ <div className={css.panelFlash} role="status">{flash}</div>
281
+ )}
183
282
  </div>
184
283
  )
185
284
  }
@@ -139,7 +139,35 @@ class SafeMathParser {
139
139
 
140
140
  private parseNumber(): number {
141
141
  const start = this.i
142
- while (this.i < this.src.length && /[0-9.eE+\-]/.test(this.src[this.i]!)) this.i++
142
+ let sawDigit = false
143
+
144
+ while (this.i < this.src.length && /[0-9]/.test(this.src[this.i]!)) {
145
+ sawDigit = true
146
+ this.i++
147
+ }
148
+
149
+ if (this.peek() === '.') {
150
+ this.i++
151
+ while (this.i < this.src.length && /[0-9]/.test(this.src[this.i]!)) {
152
+ sawDigit = true
153
+ this.i++
154
+ }
155
+ }
156
+
157
+ if (!sawDigit) {
158
+ throw new ParseError(`invalid number '${this.src.slice(start, this.i)}'`, start)
159
+ }
160
+
161
+ if (this.peek() === 'e' || this.peek() === 'E') {
162
+ this.i++
163
+ if (this.peek() === '+' || this.peek() === '-') this.i++
164
+ const exponentStart = this.i
165
+ while (this.i < this.src.length && /[0-9]/.test(this.src[this.i]!)) this.i++
166
+ if (this.i === exponentStart) {
167
+ throw new ParseError(`invalid number '${this.src.slice(start, this.i)}'`, start)
168
+ }
169
+ }
170
+
143
171
  const text = this.src.slice(start, this.i)
144
172
  const value = Number(text)
145
173
  if (Number.isNaN(value)) throw new ParseError(`invalid number '${text}'`, start)
@@ -135,8 +135,11 @@ export async function mountScene(container: HTMLElement, scene: GenuiScene3D): P
135
135
  }
136
136
  const onPointerMove = (e: PointerEvent): void => {
137
137
  if (!isDragging) return
138
- theta -= (e.clientX - prevX) * 0.01
139
- phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi + (e.clientY - prevY) * 0.01))
138
+ // Drag follows the cursor (standard orbit feel): dragging right/left
139
+ // sweeps the azimuth with the pointer, dragging up/down tilts the polar
140
+ // angle with the pointer (phi grows from +Y down toward the equator).
141
+ theta += (e.clientX - prevX) * 0.01
142
+ phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi - (e.clientY - prevY) * 0.01))
140
143
  prevX = e.clientX
141
144
  prevY = e.clientY
142
145
  orbit()
@@ -21,6 +21,7 @@ export type GenuiNode =
21
21
  | GenuiSelect
22
22
  | GenuiCheckbox
23
23
  | GenuiLink
24
+ | GenuiImage
24
25
  | GenuiAudio
25
26
  | GenuiVideo
26
27
  | GenuiBadge
@@ -151,6 +152,14 @@ export interface GenuiLink {
151
152
  href?: string
152
153
  }
153
154
 
155
+ /** User-controlled image from a browser-reachable URL. */
156
+ export interface GenuiImage {
157
+ type: 'image'
158
+ src: string
159
+ /** Visible caption and accessible image description. */
160
+ alt?: string
161
+ }
162
+
154
163
  /** User-controlled audio from a browser-reachable URL. */
155
164
  export interface GenuiAudio {
156
165
  type: 'audio'
@@ -517,7 +526,6 @@ export interface GenuiScene3D {
517
526
  }
518
527
 
519
528
  /* ---------------- v1.4: more content structure ---------------- */
520
-
521
529
  export interface GenuiTimelineItem {
522
530
  /** Title of the event. */
523
531
  title: string
@@ -0,0 +1,280 @@
1
+ /**
2
+ * templates.ts — GenUI 模板中心数据(0.9.4)。
3
+ *
4
+ * 每个模板 = 一段自然语言 `instruction`(试用时插入输入框,模型按 genui
5
+ * skill 生成对应界面)+ 一份**合法**的 `demo` spec(预览时由 GenuiBlock
6
+ * 直接渲染,用户先看到效果再决定试用)。字段对齐 spec.ts / guard.ts 的
7
+ * 实际 schema(tests/templates.spec.ts 用 validateGenuiSpec 逐条校验)。
8
+ *
9
+ * 模板即说明书:覆盖布局/数据/图表/交互/测验/高级各能力面。
10
+ */
11
+ import type { GenuiSpec } from './spec.ts'
12
+
13
+ export interface GenuiTemplate {
14
+ id: string
15
+ category: '仪表盘' | '数据' | '流程' | '图表' | '交互' | '测验' | '高级'
16
+ name: string
17
+ description: string
18
+ /** 试用时插入输入框的自然语言指令。 */
19
+ instruction: string
20
+ /** 预览用的合法 spec(≤200 节点、嵌套 ≤8 层)。 */
21
+ demo: GenuiSpec
22
+ }
23
+
24
+ export const GENUI_TEMPLATES: readonly GenuiTemplate[] = [
25
+ {
26
+ id: 'tpl-dashboard',
27
+ category: '仪表盘',
28
+ name: '项目仪表盘',
29
+ description: '一张卡片汇总核心指标:数字 + 环比 + 进度条,决策一眼可见。',
30
+ instruction: '请用 dsh-ui 给当前项目做一个仪表盘:4 个关键指标的 stat 卡片(带环比 delta)、整体进度 progress 条、今天的 3 件待办 list。标题用「项目仪表盘」。',
31
+ demo: {
32
+ title: '项目仪表盘(示例)',
33
+ gap: 12,
34
+ items: [
35
+ { type: 'card', title: '本周关键指标', items: [
36
+ { type: 'grid', cols: 2, items: [
37
+ { type: 'stat', label: '本周完成', value: '12 / 18', delta: '+4' },
38
+ { type: 'stat', label: '阻塞事项', value: '2', delta: '-1' },
39
+ { type: 'stat', label: '平均耗时', value: '3.2h', delta: '-8%' },
40
+ { type: 'stat', label: '覆盖率', value: '86%' },
41
+ ] },
42
+ ] },
43
+ { type: 'card', title: '整体进度', items: [
44
+ { type: 'progress', label: 'v1.0 里程碑', value: 68, valueLabel: '68 / 100' },
45
+ ] },
46
+ { type: 'card', title: '今日待办', items: [
47
+ { type: 'list', items: ['评审 M1 模板中心 PR', '同步 vendor 到 SSiD', '周三周会材料'] },
48
+ ] },
49
+ ],
50
+ },
51
+ },
52
+ {
53
+ id: 'tpl-compare',
54
+ category: '数据',
55
+ name: '方案对比表',
56
+ description: '多方案逐维度对照,表格直接可读,争议点一目了然。',
57
+ instruction: '请用 dsh-ui 把以下方案的对比做成一张 table:方案 A/B/C × 维度(成本/复杂度/风险/收益),列首高亮推荐项;顶部加一行 callout 说明结论。',
58
+ demo: {
59
+ title: '方案对比(示例)',
60
+ items: [
61
+ { type: 'callout', tone: 'info', title: '结论', content: '方案 B 综合最优:成本中、复杂度低、风险低。' },
62
+ { type: 'table', columns: ['维度', '方案 A', '方案 B ★', '方案 C'], rows: [
63
+ ['成本', '高', '中', '低'],
64
+ ['复杂度', '高', '中', '低'],
65
+ ['风险', '低', '低', '高'],
66
+ ['收益', '大', '中', '小'],
67
+ ] },
68
+ ],
69
+ },
70
+ },
71
+ {
72
+ id: 'tpl-steps',
73
+ category: '流程',
74
+ name: '五步上手流程',
75
+ description: '关键步骤排成时间线,每步一句要点,适合教程/交接/发布流程。',
76
+ instruction: '请用 dsh-ui 把操作步骤做成 steps 教程:5 步(按实际流程),每步标题 + 一两句要点,顶部用 badge 标出预计耗时。',
77
+ demo: {
78
+ title: '上手流程(示例)',
79
+ items: [
80
+ { type: 'badge', label: '约 5 分钟', tone: 'accent' },
81
+ { type: 'steps', current: 2, steps: [
82
+ { title: '安装插件', desc: '从插件中心安装,或 npm 安装到 profile' },
83
+ { title: '发送第一条指令', desc: '让模型用 dsh-ui 输出一个界面' },
84
+ { title: '体验交互', desc: '点按钮/判卷/切 tab,动作会回传模型' },
85
+ { title: '常驻面板', desc: '模型用 panel:true 把结果钉在会话面板' },
86
+ { title: '进阶组件', desc: '图表/流程/3D 按需懒加载' },
87
+ ] },
88
+ ],
89
+ },
90
+ },
91
+ {
92
+ id: 'tpl-quiz',
93
+ category: '测验',
94
+ name: '随堂测验',
95
+ description: '一题一答即时判卷:选中即知对错,附讲解,适合培训问答。',
96
+ instruction: '请用 dsh-ui 出 3 道随堂测验(quiz 组件:question + options,其中一项 correct,附 explanation),标题「随堂测验」,每题即选即评。',
97
+ demo: {
98
+ title: '随堂测验(示例)',
99
+ gap: 10,
100
+ items: [
101
+ { type: 'text', content: '选中即判卷并显示讲解。', size: 'muted' },
102
+ { type: 'quiz', id: 'q-dsh', question: 'DSH 插件的最小单位是?', options: [
103
+ { label: '服务' },
104
+ { label: '插件(cordis 插件)', correct: true, feedback: '✓ 一切皆插件:服务、工具、UI 都通过插件注册。' },
105
+ { label: '工具' },
106
+ ], explanation: 'DSH 基于 cordis,任何能力(含服务与工具)都以插件形式注册。' },
107
+ { type: 'quiz', id: 'q-fence', question: 'dsh-ui 围栏由谁输出?', options: [
108
+ { label: '用户' },
109
+ { label: '模型', correct: true, feedback: '✓ 模型在回复中输出 dsh-ui fence。' },
110
+ { label: '宿主' },
111
+ ], explanation: '模型输出 fence,渲染器将其画成真实组件。' },
112
+ { type: 'quiz', id: 'q-panel', question: 'panel:true 的围栏渲染到哪里?', options: [
113
+ { label: '消息流' },
114
+ { label: '会话面板 dock', correct: true, feedback: '✓ 面板更新不刷屏。' },
115
+ { label: '设置页' },
116
+ ] },
117
+ ],
118
+ },
119
+ },
120
+ {
121
+ id: 'tpl-stats',
122
+ category: '数据',
123
+ name: '关键值 + 进度',
124
+ description: '一次交付:数值、进度、里程碑三件套,周报/复盘通用。',
125
+ instruction: '请用 dsh-ui 总结本周关键数据:keyvalue 列 3 组关键值、两根 progress 进度(计划 vs 实际)、timeline 放 3 个里程碑,标题「周报速览」。',
126
+ demo: {
127
+ title: '周报速览(示例)',
128
+ items: [
129
+ { type: 'keyvalue', pairs: [
130
+ { key: '完成项', value: '12' },
131
+ { key: '延期', value: '1' },
132
+ { key: '风险', value: '2(均已有缓解)' },
133
+ ] },
134
+ { type: 'progress', label: '计划进度', value: 75, valueLabel: '75%' },
135
+ { type: 'progress', label: '实际进度', value: 82, valueLabel: '82%' },
136
+ { type: 'timeline', items: [
137
+ { title: '周一', desc: '完成 M1 开发', time: '08-25' },
138
+ { title: '周三', desc: '全链路验证通过', time: '08-27' },
139
+ { title: '周五', desc: '发布 0.9.4', time: '08-29' },
140
+ ] },
141
+ ],
142
+ },
143
+ },
144
+ {
145
+ id: 'tpl-chart',
146
+ category: '图表',
147
+ name: '趋势柱状图',
148
+ description: '数据直接画成柱状图,多序列对比,走势一眼看清。',
149
+ instruction: '请用 dsh-ui 把下列数据画成 chart(bars,多序列):近 6 周完成量 vs 计划量;图表上方给一句趋势结论。',
150
+ demo: {
151
+ title: '趋势对比(示例)',
152
+ items: [
153
+ { type: 'text', content: '结论:近 3 周完成量连续上升,本周首次反超计划。' },
154
+ { type: 'chart', kind: 'line', data: [
155
+ { label: 'W21', value: 8 },
156
+ { label: 'W22', value: 10 },
157
+ { label: 'W23', value: 9 },
158
+ { label: 'W24', value: 12 },
159
+ { label: 'W25', value: 15 },
160
+ { label: 'W26', value: 18 },
161
+ ] },
162
+ ],
163
+ },
164
+ },
165
+ {
166
+ id: 'tpl-tabs',
167
+ category: '交互',
168
+ name: '分标签页',
169
+ description: '一大块内容拆成标签页,锚点清晰;面板不挤、消息不刷屏。',
170
+ instruction: '请用 dsh-ui 把内容分成 tabs 三个标签:概览/明细/FAQ,每个标签 3 条以内要点;标题「功能速览」。',
171
+ demo: {
172
+ title: '功能速览(示例)',
173
+ items: [
174
+ { type: 'tabs', tabs: [
175
+ { label: '概览', items: [{ type: 'text', content: '三项能力:实时渲染、交互回传、常驻面板。' }] },
176
+ { label: '明细', items: [{ type: 'list', items: ['渲染:fence 增量解析,边流边出', '交互:动作回传模型增量更新', '面板:会话级 dock,折叠/调高'] }] },
177
+ { label: 'FAQ', items: [{ type: 'text', content: '问:不动模型会怎样?答:组件仍可本地判卷/展开。' }] },
178
+ ] },
179
+ ],
180
+ },
181
+ },
182
+ {
183
+ id: 'tpl-checklist',
184
+ category: '流程',
185
+ name: '交付检查清单',
186
+ description: '提交前逐项打勾:清单即流程,漏项看得见。',
187
+ instruction: '请用 dsh-ui 做一张交付检查清单:6 项(测试/文档/发布/回滚/监控/公告),checkbox 可勾选,顶部 badge 提示「全部勾选再合并」。',
188
+ demo: {
189
+ title: '交付清单(示例)',
190
+ items: [
191
+ { type: 'badge', label: '全部勾选再合并', tone: 'warn' },
192
+ { type: 'list', items: [
193
+ { type: 'checkbox', label: '单元测试通过' },
194
+ { type: 'checkbox', label: '文档同步' },
195
+ { type: 'checkbox', label: '发布套餐就绪' },
196
+ { type: 'checkbox', label: '回滚预案' },
197
+ ] },
198
+ ],
199
+ },
200
+ },
201
+ {
202
+ id: 'tpl-accordion',
203
+ category: '高级',
204
+ name: 'FAQ 手风琴',
205
+ description: '一问一答收在折叠面板里,长文档变短,回答不吓人。',
206
+ instruction: '请用 dsh-ui 做 FAQ 手风琴(accordion):5 个高频问题,标题栏就是问题、展开一条给答案;第一问默认展开。',
207
+ demo: {
208
+ title: 'FAQ(示例)',
209
+ items: [
210
+ { type: 'accordion', items: [
211
+ { title: '支持哪些图表?', items: [{ type: 'text', content: 'chart(bars/line/donut)、plot 函数图、echart 全功能图表。' }] },
212
+ { title: '面板能持久吗?', items: [{ type: 'text', content: '同一内容指纹刷新/重放恢复状态;新内容重置。' }] },
213
+ { title: '交互会打扰模型吗?', items: [{ type: 'text', content: '带 action 的组件才回传;纯展示组件零往返。' }] },
214
+ ] },
215
+ ],
216
+ },
217
+ },
218
+ {
219
+ id: 'tpl-diagram',
220
+ category: '高级',
221
+ name: '系统架构图',
222
+ description: '框架图画成 SVG:节点分层、连接线、分区标注,比截图清楚。',
223
+ instruction: '请用 dsh-ui 画一幅架构图(diagram):描述当前系统的 5 个节点与连接(kind: architecture 或 flowchart),包含一个安全区 zone。',
224
+ demo: {
225
+ title: '架构图(示例)',
226
+ items: [
227
+ { type: 'diagram', kind: 'flowchart', title: 'DSH 插件链路', nodes: [
228
+ { id: 'a', label: '模型', x: 20, y: 20, w: 120, h: 48 },
229
+ { id: 'b', label: '插件', x: 20, y: 120, w: 120, h: 48 },
230
+ { id: 'c', label: '宿主', x: 200, y: 70, w: 120, h: 48 },
231
+ ], edges: [
232
+ { from: 'a', to: 'b' },
233
+ { from: 'b', to: 'c' },
234
+ ] },
235
+ ],
236
+ },
237
+ },
238
+ {
239
+ id: 'tpl-3d',
240
+ category: '高级',
241
+ name: '3D 场景',
242
+ description: '一个可旋转的 3D 场景:产品演示/空间示意用得上。',
243
+ instruction: '请用 dsh-ui 渲染一个 3D 场景(scene3d):一个立方体 + 一个球体,标题「示例场景」,附一句使用场景说明。',
244
+ demo: {
245
+ title: '3D 场景(示例)',
246
+ items: [
247
+ { type: 'text', content: '首次加载需下载 three.js(约 700KB,懒加载),之后走缓存。', size: 'muted' },
248
+ { type: 'scene3d', title: '示例场景', meshes: [
249
+ { shape: 'box', size: 1, color: '#4FC3F7', position: [-1.2, 0, 0] },
250
+ { shape: 'sphere', size: 0.6, color: '#81C784', position: [1.2, 0, 0] },
251
+ ] },
252
+ ],
253
+ },
254
+ },
255
+ {
256
+ id: 'tpl-breadcrumb',
257
+ category: '数据',
258
+ name: '路径面包屑',
259
+ description: '长路径拆成面包屑:文件位置、组织层级一读就懂。',
260
+ instruction: '请用 dsh-ui 展示当前工作区结构:breadcrumb 显示文件路径(4 段)、file-tree 描述项目目录(3 个目录、每个 2 个子项)。',
261
+ demo: {
262
+ title: '目录结构(示例)',
263
+ items: [
264
+ { type: 'breadcrumb', items: ['工作区', 'dsh-genui', 'src', 'client'] },
265
+ { type: 'file-tree', items: [
266
+ { name: 'src', type: 'dir', children: [
267
+ { name: 'client', type: 'dir', children: [
268
+ { name: 'panel.tsx' },
269
+ { name: 'templates.ts' },
270
+ ] },
271
+ { name: 'index.tsx' },
272
+ ] },
273
+ { name: 'tests', type: 'dir', children: [
274
+ { name: 'templates.spec.ts' },
275
+ ] },
276
+ ] },
277
+ ],
278
+ },
279
+ },
280
+ ]