dsh-data-domain-knowledge 0.1.0-alpha.2 → 0.1.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +147 -19
  2. package/cordis.patch.yml +25 -10
  3. package/lib/client.js +6400 -3326
  4. package/lib/doc-page.js +7761 -0
  5. package/lib/types/convert/host.d.ts.map +1 -1
  6. package/lib/types/convert/host.js +63 -11
  7. package/lib/types/convert/host.js.map +1 -1
  8. package/lib/types/convert/ingest.d.ts +7 -4
  9. package/lib/types/convert/ingest.d.ts.map +1 -1
  10. package/lib/types/convert/ingest.js +52 -14
  11. package/lib/types/convert/ingest.js.map +1 -1
  12. package/lib/types/convert/types.d.ts +2 -2
  13. package/lib/types/convert/types.d.ts.map +1 -1
  14. package/lib/types/convert/types.js +2 -2
  15. package/lib/types/convert/types.js.map +1 -1
  16. package/lib/types/diagnosis/cards.d.ts +8 -7
  17. package/lib/types/diagnosis/cards.d.ts.map +1 -1
  18. package/lib/types/diagnosis/cards.js +119 -49
  19. package/lib/types/diagnosis/cards.js.map +1 -1
  20. package/lib/types/diagnosis/run.d.ts +57 -0
  21. package/lib/types/diagnosis/run.d.ts.map +1 -0
  22. package/lib/types/diagnosis/run.js +245 -0
  23. package/lib/types/diagnosis/run.js.map +1 -0
  24. package/lib/types/diagnosis/schema.d.ts +25 -2
  25. package/lib/types/diagnosis/schema.d.ts.map +1 -1
  26. package/lib/types/diagnosis/schema.js +64 -25
  27. package/lib/types/diagnosis/schema.js.map +1 -1
  28. package/lib/types/diagnosis/seed-source.d.ts +17 -2
  29. package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
  30. package/lib/types/diagnosis/seed-source.js +19 -2
  31. package/lib/types/diagnosis/seed-source.js.map +1 -1
  32. package/lib/types/diagnosis/seed.d.ts +32 -42
  33. package/lib/types/diagnosis/seed.d.ts.map +1 -1
  34. package/lib/types/diagnosis/seed.js +187 -51
  35. package/lib/types/diagnosis/seed.js.map +1 -1
  36. package/lib/types/diagnosis/store.d.ts +95 -25
  37. package/lib/types/diagnosis/store.d.ts.map +1 -1
  38. package/lib/types/diagnosis/store.js +297 -166
  39. package/lib/types/diagnosis/store.js.map +1 -1
  40. package/lib/types/diagnosis/types.d.ts +168 -16
  41. package/lib/types/diagnosis/types.d.ts.map +1 -1
  42. package/lib/types/diagnosis/types.js +1 -1
  43. package/lib/types/diagnosis/types.js.map +1 -1
  44. package/lib/types/document-page.d.ts +39 -0
  45. package/lib/types/document-page.d.ts.map +1 -0
  46. package/lib/types/document-page.js +43 -0
  47. package/lib/types/document-page.js.map +1 -0
  48. package/lib/types/extract-model.d.ts +67 -27
  49. package/lib/types/extract-model.d.ts.map +1 -1
  50. package/lib/types/extract-model.js +225 -97
  51. package/lib/types/extract-model.js.map +1 -1
  52. package/lib/types/extract.d.ts +16 -0
  53. package/lib/types/extract.d.ts.map +1 -1
  54. package/lib/types/extract.js +19 -2
  55. package/lib/types/extract.js.map +1 -1
  56. package/lib/types/fs-atomic.d.ts +11 -0
  57. package/lib/types/fs-atomic.d.ts.map +1 -0
  58. package/lib/types/fs-atomic.js +41 -0
  59. package/lib/types/fs-atomic.js.map +1 -0
  60. package/lib/types/index.d.ts.map +1 -1
  61. package/lib/types/index.js +466 -63
  62. package/lib/types/index.js.map +1 -1
  63. package/lib/types/intent/types.js +1 -1
  64. package/lib/types/intent/types.js.map +1 -1
  65. package/lib/types/knowledge-bases.d.ts +23 -12
  66. package/lib/types/knowledge-bases.d.ts.map +1 -1
  67. package/lib/types/knowledge-bases.js +37 -44
  68. package/lib/types/knowledge-bases.js.map +1 -1
  69. package/lib/types/layout.d.ts +3 -3
  70. package/lib/types/layout.d.ts.map +1 -1
  71. package/lib/types/layout.js +4 -4
  72. package/lib/types/layout.js.map +1 -1
  73. package/lib/types/migrate.d.ts +1 -1
  74. package/lib/types/migrate.js +2 -2
  75. package/lib/types/migrate.js.map +1 -1
  76. package/lib/types/model-text.d.ts +99 -0
  77. package/lib/types/model-text.d.ts.map +1 -0
  78. package/lib/types/model-text.js +103 -0
  79. package/lib/types/model-text.js.map +1 -0
  80. package/lib/types/present.d.ts +239 -0
  81. package/lib/types/present.d.ts.map +1 -0
  82. package/lib/types/present.js +255 -0
  83. package/lib/types/present.js.map +1 -0
  84. package/lib/types/recall-rank.d.ts +37 -0
  85. package/lib/types/recall-rank.d.ts.map +1 -1
  86. package/lib/types/recall-rank.js +41 -1
  87. package/lib/types/recall-rank.js.map +1 -1
  88. package/lib/types/runtime.d.ts +564 -31
  89. package/lib/types/runtime.d.ts.map +1 -1
  90. package/lib/types/runtime.js +2645 -292
  91. package/lib/types/runtime.js.map +1 -1
  92. package/lib/types/settings-schema.d.ts +4 -2
  93. package/lib/types/settings-schema.d.ts.map +1 -1
  94. package/lib/types/settings-schema.js +2 -0
  95. package/lib/types/settings-schema.js.map +1 -1
  96. package/lib/types/source-images.d.ts +118 -0
  97. package/lib/types/source-images.d.ts.map +1 -0
  98. package/lib/types/source-images.js +288 -0
  99. package/lib/types/source-images.js.map +1 -0
  100. package/lib/types/types.d.ts +356 -28
  101. package/lib/types/types.d.ts.map +1 -1
  102. package/lib/types/types.js +112 -18
  103. package/lib/types/types.js.map +1 -1
  104. package/lib/types/wiki-finalize.d.ts +140 -0
  105. package/lib/types/wiki-finalize.d.ts.map +1 -0
  106. package/lib/types/wiki-finalize.js +269 -0
  107. package/lib/types/wiki-finalize.js.map +1 -0
  108. package/lib/types/wiki-types.d.ts +10 -2
  109. package/lib/types/wiki-types.d.ts.map +1 -1
  110. package/lib/types/wiki-types.js +27 -30
  111. package/lib/types/wiki-types.js.map +1 -1
  112. package/package.json +4 -3
  113. package/preset/tita-data-agent/agent.cordis.yml +25 -10
  114. package/src/client/DirectoryPicker.tsx +12 -12
  115. package/src/client/DrawerSection.tsx +18 -0
  116. package/src/client/InfoHint.tsx +21 -8
  117. package/src/client/IntentTab.tsx +20 -26
  118. package/src/client/KnowledgeBaseTab.tsx +458 -158
  119. package/src/client/KnowledgePanel.tsx +44 -10
  120. package/src/client/KnowledgeStepRail.tsx +3 -0
  121. package/src/client/KnowledgeToolRow.tsx +331 -0
  122. package/src/client/NodeSettings.tsx +20 -22
  123. package/src/client/PreviewSections.tsx +178 -43
  124. package/src/client/RecallPanel.tsx +418 -127
  125. package/src/client/TypeDefinitionDrawer.tsx +154 -54
  126. package/src/client/WikiCardReview.tsx +266 -0
  127. package/src/client/WikiFlowCard.tsx +57 -95
  128. package/src/client/WikiTypeEditor.tsx +197 -95
  129. package/src/client/arch/AgentArchitectureGraph.tsx +30 -14
  130. package/src/client/arch/NodeLogicPanel.tsx +6 -3
  131. package/src/client/arch/model.ts +164 -26
  132. package/src/client/architecture.css +25 -10
  133. package/src/client/architecture.tsx +66 -41
  134. package/src/client/asset-images.ts +125 -0
  135. package/src/client/attribution/AttributionGraph.tsx +481 -34
  136. package/src/client/attribution/AttributionTab.tsx +501 -106
  137. package/src/client/attribution/CaseView.tsx +9 -14
  138. package/src/client/attribution/GraphStage.tsx +25 -18
  139. package/src/client/attribution/RunView.tsx +27 -9
  140. package/src/client/attribution/StageChrome.tsx +280 -140
  141. package/src/client/attribution/graph-options.ts +611 -47
  142. package/src/client/attribution/model.ts +559 -30
  143. package/src/client/convert/ConvertPanel.tsx +36 -4
  144. package/src/client/doc-page.css +134 -0
  145. package/src/client/doc-page.tsx +242 -0
  146. package/src/client/index.ts +18 -0
  147. package/src/client/panel-state.ts +105 -0
  148. package/src/client/problem-domains.ts +1 -1
  149. package/src/client/request.ts +36 -0
  150. package/src/client/styles.css +397 -54
  151. package/src/client/useFitScale.ts +15 -2
  152. package/src/client/wiki-type-groups.ts +86 -0
  153. package/src/convert/host.ts +68 -9
  154. package/src/convert/ingest.ts +61 -18
  155. package/src/convert/types.ts +2 -2
  156. package/src/diagnosis/cards.ts +120 -50
  157. package/src/diagnosis/run.ts +251 -0
  158. package/src/diagnosis/schema.ts +67 -26
  159. package/src/diagnosis/seed-source.ts +29 -4
  160. package/src/diagnosis/seed.ts +187 -85
  161. package/src/diagnosis/store.ts +310 -173
  162. package/src/diagnosis/types.ts +181 -16
  163. package/src/document-page.ts +51 -0
  164. package/src/extract-model.ts +254 -99
  165. package/src/extract.ts +35 -3
  166. package/src/fs-atomic.ts +41 -0
  167. package/src/index.ts +499 -62
  168. package/src/intent/types.ts +1 -1
  169. package/src/knowledge-bases.ts +50 -49
  170. package/src/layout.ts +4 -4
  171. package/src/migrate.ts +2 -2
  172. package/src/model-text.ts +126 -0
  173. package/src/present.ts +367 -0
  174. package/src/recall-rank.ts +57 -2
  175. package/src/runtime.ts +2802 -293
  176. package/src/settings-schema.ts +2 -0
  177. package/src/source-images.ts +326 -0
  178. package/src/types.ts +446 -45
  179. package/src/wiki-finalize.ts +310 -0
  180. package/src/wiki-types.ts +28 -32
@@ -1,5 +1,6 @@
1
1
  import {
2
- ChevronDown, CircleHelp, Maximize2, Minimize2, RotateCcw, Search, SlidersHorizontal, X,
2
+ CircleHelp, ChevronDown, ChevronRight, Maximize2, Minimize2, RotateCcw, Search,
3
+ SlidersHorizontal, X,
3
4
  } from '@tita-data-agent/ui/icons'
4
5
  import type { ReactNode } from 'react'
5
6
  import { Badge, Button, Input, cn } from '@tita-data-agent/ui/components'
@@ -7,9 +8,10 @@ import {
7
8
  EDGE_LEGEND, type GraphLayout,
8
9
  } from './graph-options.ts'
9
10
  import {
10
- FAILURE_MODE_STYLE, FILTERABLE_KINDS, NODE_KIND_LABEL, PROBLEM_CLASSES, PROBLEM_CLASS_LABEL,
11
- symptomCountByProblemClass,
12
- type AttributionNode, type AttributionNodeKind, type AttributionStats, type FailureMode, type ProblemClassId,
11
+ CHECK_COST_BANDS, FAILURE_REASON_STYLE, FILTERABLE_KINDS, LANES, NODE_KIND_LABEL,
12
+ PROBLEM_CLASSES, PROBLEM_CLASS_LABEL,
13
+ type AttributionNode, type AttributionNodeKind, type AttributionStats, type FailureReason,
14
+ type LaneId, type ProblemClassId,
13
15
  } from './model.ts'
14
16
 
15
17
  /**
@@ -19,6 +21,12 @@ import {
19
21
  * 不读数据、不算分数、不改状态。这样「浮层放在哪个角」这种纯排版决定可以随时改,
20
22
  * 而不会牵动任何业务逻辑 —— 而这恰恰是这一轮改动的主要内容。
21
23
  *
24
+ * **四角分区(0021 §7.1)**,一条视线:
25
+ * - 画布左上「改这张图」:四组按动线顺序 —— ① 布局 → ② 问题定义 → ③ 问题假设 → ④ 检查点;
26
+ * - 画布右上「怎么看这张图」:图例 · 全景 / 聚焦 · 保留 1 跳邻居 · 全屏,**四个同排**;
27
+ * - 画布左下「这张图现在多大」:统计 + 数据来源;
28
+ * - 画布底中「在图里找东西」:只剩文本搜索与命中数。
29
+ *
22
30
  * 每条浮层都走同一个面板原语 `ChromePanel`:半透明白底 + 细描边 + 轻阴影。
23
31
  * 之所以不各写一套边框,是因为它们都盖在画布上,**任何一条比别的更重都会把注意力
24
32
  * 从图上偷走**。统一是这里唯一正确的风格决定。
@@ -40,7 +48,7 @@ export function ChromePanel({ className, children }: { className?: string; child
40
48
  /** 浮层里的小按钮:比标准 `sm` 再矮 4px,否则一条 380px 宽的浮层会被按钮撑到两行。 */
41
49
  const MINI = 'h-[26px] min-h-[26px] px-2 text-[11px] font-[600]'
42
50
 
43
- /** 可多选的切面 chip。选中态用描边而不是填色:填色会把失败模式的语义色盖掉。 */
51
+ /** 可多选的切面 chip。选中态用描边而不是填色:填色会把失败原因的语义色盖掉。 */
44
52
  export function Chip({ active, color, onClick, children, title }: {
45
53
  active: boolean
46
54
  color?: string
@@ -65,68 +73,172 @@ export function Chip({ active, color, onClick, children, title }: {
65
73
  >{children}</button>
66
74
  }
67
75
 
68
- /* ───────────────────────────────────────────────── 左上:分层 + 类型 ── */
76
+ /** 组标题。四组各有一行 —— 顺序本身就是读法,所以标题不能被省掉。 */
77
+ function GroupLabel({ children, index }: { children: ReactNode; index: string }): ReactNode {
78
+ return <span className="flex shrink-0 items-center gap-1 pt-0.5 text-[9.5px] font-[650] text-tita-ink-faint">
79
+ <span className="font-mono">{index}</span>{children}
80
+ </span>
81
+ }
82
+
83
+ function Group({ index, label, children }: { index: string; label: string; children: ReactNode }): ReactNode {
84
+ return <div className="flex items-start gap-1.5 text-[10px] text-tita-ink-muted">
85
+ <GroupLabel index={index}>{label}</GroupLabel>
86
+ <div className="flex min-w-0 flex-wrap gap-1">{children}</div>
87
+ </div>
88
+ }
89
+
90
+ /* ─────────────────────────────────────────────── 画布左上:操作(四组切面) ── */
91
+
92
+ /** 布局的两种读法。**只有两个** —— 「分层」已撤销(§7.2)。 */
93
+ const LAYOUT_OPTIONS: Array<[GraphLayout, string, string]> = [
94
+ ['line', '动线', '动线:五段泳道 + 点亮一条链,回答「下一步查什么、这条链断在哪」'],
95
+ ['force', '关系', '关系:浮动节点、可拖拽,关系靠聚簇与距离表达,回答「哪块密、哪里空」'],
96
+ ]
97
+
98
+ export const LAYOUT_LABEL: Record<GraphLayout, string> = { line: '动线', force: '关系' }
69
99
 
70
100
  /**
71
- * 左上角工具条:**布局**(这张图怎么摆)与**类型切面**(看哪些节点)。
101
+ * 画布左上角工具条:**四组按动线顺序的切面**。
102
+ *
103
+ * 顺序不是排版偏好,是动线本身:先决定这张图怎么画(① 布局),再按
104
+ * 「问题 → 假设 → 检查点」逐段收窄(② ③ ④)。§7.1 把这条顺序写进了规格,
105
+ * 所以组标题带序号,而且**不提供另一种排列**。
72
106
  *
73
- * 两件事放在一起是因为它们都是「改变我看到的这张图」,而失败模式/文本筛选是
74
- * 「在图里找东西」—— 后者留在底部,前者贴住左上,动线上就不互相打架。
107
+ * 「类型切面」被这三组吸收(§7.3):节点可见性由它所属的那一段承担,
108
+ * 关掉「检查点」= 第三段整段不画。同时存在两套说法只会让人犹豫点哪个。
75
109
  *
76
110
  * 宽度钉在 240px:它是**这一行左侧能拿到的全部预算**(380px 侧栏减内边距、
77
- * 再减右上角那对图标按钮)。不写成自适应是因为自适应在窄容器里会先压扁
78
- * 「力导向 / 分层」这两个按钮,正好是最不该被压的两个。
111
+ * 再减右上角那排读法开关)。不写成自适应是因为自适应在窄容器里会先压扁
112
+ * 最不该被压的那两个布局按钮。
79
113
  */
80
- export function ToolRail({ layout, onLayout, onReset, kinds, onToggleKind, problems, onToggleProblem, symptomCounts, hint, onCollapse }: {
114
+ export function OperationsRail({
115
+ layout, onLayout, onReset, kinds, onToggleKind, problems, onToggleProblem,
116
+ subclasses, onToggleSubclass, symptomCounts, failureReasons, onToggleFailureReason,
117
+ gapOnly, onGapOnly, costBands, onToggleCostBand, automations, onToggleAutomation,
118
+ laneCounts, lanesOff, onToggleLane, hint, onCollapse,
119
+ }: {
81
120
  layout: GraphLayout
82
121
  onLayout: (layout: GraphLayout) => void
83
122
  onReset: () => void
123
+ /** 生效的段(= 节点种类)切面。 */
84
124
  kinds: ReadonlySet<AttributionNodeKind>
85
125
  onToggleKind: (kind: AttributionNodeKind) => void
86
- /** 问题大类切面。它排在类型之前 —— 「哪类问题」比「哪些节点」更靠前。 */
87
126
  problems: ReadonlySet<ProblemClassId>
88
127
  onToggleProblem: (id: ProblemClassId) => void
128
+ subclasses: ReadonlySet<string>
129
+ onToggleSubclass: (id: string) => void
89
130
  symptomCounts: Record<ProblemClassId, number>
131
+ failureReasons: ReadonlySet<FailureReason>
132
+ onToggleFailureReason: (id: FailureReason) => void
133
+ gapOnly: boolean
134
+ onGapOnly: () => void
135
+ costBands: ReadonlySet<string>
136
+ onToggleCostBand: (id: string) => void
137
+ automations: ReadonlySet<string>
138
+ onToggleAutomation: (id: string) => void
139
+ /** 每段当前画了几个 / 一共几个。段头读数。 */
140
+ laneCounts: Record<LaneId, { shown: number; total: number }>
141
+ /** 被关掉的段。它是 `kinds` 的投影,用来给段 chip 标选中态。 */
142
+ lanesOff: ReadonlySet<LaneId>
143
+ onToggleLane: (id: LaneId) => void
90
144
  hint: string
91
145
  onCollapse: () => void
92
146
  }): ReactNode {
147
+ const activeProblems = PROBLEM_CLASSES.filter(item => problems.has(item.id))
93
148
  return <ChromePanel className="flex w-[min(240px,100%)] flex-col gap-1.5 p-2">
94
149
  <div className="flex items-center gap-1">
95
- <span className="mr-0.5 text-[10px] font-[650] text-tita-ink-faint">布局</span>
96
- {([['force', '力导向'], ['layered', '分层']] as Array<[GraphLayout, string]>).map(([id, label]) =>
97
- <Button
98
- key={id}
99
- size="sm"
100
- variant={layout === id ? 'default' : 'outline'}
101
- className={MINI}
102
- onClick={() => onLayout(id)}
103
- >{label}</Button>)}
104
- <Button size="icon" variant="outline" className="ml-auto h-[26px] w-[26px] min-w-[26px]" title="重新布局" aria-label="重新布局" onClick={onReset}>
105
- <RotateCcw size={12} />
106
- </Button>
107
150
  <Button size="icon" variant="ghost" className="h-[26px] w-[26px] min-w-[26px]" title="收起工具条" aria-label="收起工具条" onClick={onCollapse}>
108
151
  <ChevronDown size={12} />
109
152
  </Button>
153
+ <span className="text-[9.5px] font-[650] text-tita-ink-faint">按动线顺序</span>
154
+ <Button size="icon" variant="outline" className="ml-auto h-[26px] w-[26px] min-w-[26px]" title="重新布局" aria-label="重新布局" onClick={onReset}>
155
+ <RotateCcw size={12} />
156
+ </Button>
110
157
  </div>
111
158
 
112
- <div className="flex items-start gap-1.5 text-[10px] text-tita-ink-muted">
113
- <span className="shrink-0 pt-0.5 font-[650] text-tita-ink-faint">问题</span>
114
- <div className="flex flex-wrap gap-1">
115
- {PROBLEM_CLASSES.map(item => <Chip
116
- key={item.id}
117
- active={problems.has(item.id)}
118
- title={`${item.cue} · ${symptomCounts[item.id]} 个现象`}
119
- onClick={() => onToggleProblem(item.id)}
120
- >{item.label}<span className="ml-1 font-mono text-[9.5px] opacity-70">{symptomCounts[item.id]}</span></Chip>)}
159
+ {/* ① 布局 */}
160
+ <Group index="①" label="布局">
161
+ {LAYOUT_OPTIONS.map(([id, label, title]) => <Button
162
+ key={id}
163
+ size="sm"
164
+ variant={layout === id ? 'default' : 'outline'}
165
+ className={MINI}
166
+ title={title}
167
+ onClick={() => onLayout(id)}
168
+ >{label}</Button>)}
169
+ </Group>
170
+
171
+ {/* ② 问题定义:大类(子图选择器)→ 子类(只在选中的大类内出现) */}
172
+ <Group index="②" label="问题定义">
173
+ {PROBLEM_CLASSES.map(item => <Chip
174
+ key={item.id}
175
+ active={problems.has(item.id)}
176
+ title={`${item.cue} · ${symptomCounts[item.id]} 个现象`}
177
+ onClick={() => onToggleProblem(item.id)}
178
+ >{item.label}<span className="ml-1 font-mono text-[9.5px] opacity-70">{symptomCounts[item.id]}</span></Chip>)}
179
+ </Group>
180
+ {/* 子类行只在选了大类之后出现:没选大类时摊开 15 个子类,是把「两级切面」
181
+ 又变回了一张目录。选了大类才收窄,这才是 §4.5 说的随大类收窄。 */}
182
+ {activeProblems.length > 0 && <div className="flex items-start gap-1.5 pl-[13px] text-[10px] text-tita-ink-muted">
183
+ <div className="flex min-w-0 flex-wrap gap-1">
184
+ {activeProblems.flatMap(item => item.subclasses).map(sub => <Chip
185
+ key={sub.id}
186
+ active={subclasses.has(sub.id)}
187
+ title={`${sub.label} · 形态 ${sub.shape}${sub.timePattern ? ` · ${sub.timePattern}` : ''}`}
188
+ onClick={() => onToggleSubclass(sub.id)}
189
+ >{sub.label}</Chip>)}
121
190
  </div>
122
- </div>
191
+ </div>}
123
192
 
124
- <div className="flex items-start gap-1.5 text-[10px] text-tita-ink-muted">
125
- <span className="shrink-0 pt-0.5 font-[650] text-tita-ink-faint">类型</span>
126
- <div className="flex flex-wrap gap-1">
127
- {FILTERABLE_KINDS.map(kind => <Chip key={kind} active={kinds.has(kind)} onClick={() => onToggleKind(kind)}>
128
- {NODE_KIND_LABEL[kind]}
129
- </Chip>)}
193
+ {/* ③ 问题假设:失败原因 + 空区 */}
194
+ <Group index="③" label="问题假设">
195
+ {Object.entries(FAILURE_REASON_STYLE).map(([key, tone]) => <Chip
196
+ key={key}
197
+ active={failureReasons.has(key as FailureReason)}
198
+ color={tone.stroke}
199
+ onClick={() => onToggleFailureReason(key as FailureReason)}
200
+ >{tone.label}</Chip>)}
201
+ <Chip
202
+ active={gapOnly}
203
+ color="var(--tita-danger)"
204
+ title="只留空区:有假设、一条检查点都没有 —— 它是「这张图哪里还没建完」的读数"
205
+ onClick={onGapOnly}
206
+ >空区</Chip>
207
+ </Group>
208
+
209
+ {/* ④ 检查点:成本 + 自动 / 人工 */}
210
+ <Group index="④" label="检查点">
211
+ {CHECK_COST_BANDS.map(band => <Chip
212
+ key={band.id}
213
+ active={costBands.has(band.id)}
214
+ onClick={() => onToggleCostBand(band.id)}
215
+ >{band.label}</Chip>)}
216
+ {([['auto', '自动'], ['manual', '人工']] as Array<[string, string]>).map(([id, label]) => <Chip
217
+ key={id}
218
+ active={automations.has(id)}
219
+ onClick={() => onToggleAutomation(id)}
220
+ >{label}</Chip>)}
221
+ </Group>
222
+
223
+ {/* 段开关:它就是「类型切面」的归位处(§7.3)。段名 + 实时计数,
224
+ 空的或不画的那一段要能一眼看出来,而不是点进去才发现是 0。 */}
225
+ <div className="flex items-start gap-1.5 border-t border-tita-line pt-1.5 text-[10px] text-tita-ink-muted">
226
+ <span className="shrink-0 pt-0.5 text-[9.5px] font-[650] text-tita-ink-faint">段</span>
227
+ <div className="flex min-w-0 flex-wrap gap-1">
228
+ {LANES.map(lane => {
229
+ const counts = laneCounts[lane.id]
230
+ const off = lanesOff.has(lane.id)
231
+ return <Chip
232
+ key={lane.id}
233
+ active={!off}
234
+ title={off ? `已关掉:这${lane.instance ? '一' : '一'}段整段不画` : `${lane.label} · ${lane.orderHint}`}
235
+ onClick={() => onToggleLane(lane.id)}
236
+ >
237
+ <span className={cn(off && 'line-through')}>{lane.ordinal} {lane.label}</span>
238
+ <span className="ml-1 font-mono text-[9.5px] opacity-70">{counts?.shown ?? 0}</span>
239
+ {counts && counts.total !== counts.shown && <span className="ml-0.5 font-mono text-[9.5px] opacity-60">/{counts.total}</span>}
240
+ </Chip>
241
+ })}
130
242
  </div>
131
243
  </div>
132
244
 
@@ -134,63 +246,99 @@ export function ToolRail({ layout, onLayout, onReset, kinds, onToggleKind, probl
134
246
  </ChromePanel>
135
247
  }
136
248
 
137
- /** 收起来之后留下的那颗药丸。收起不等于隐藏 —— 数字还在上面。 */
138
- export function ToolPill({ layout, kinds, problems, onClick }: {
249
+ /** 收起来之后留下的那颗药丸。收起不等于隐藏 —— 布局与生效筛选数还在上面。 */
250
+ export function ToolPill({ layout, filterCount, laneCount, onClick }: {
139
251
  layout: GraphLayout
140
- kinds: ReadonlySet<AttributionNodeKind>
141
- problems: ReadonlySet<ProblemClassId>
252
+ /** 生效的切面数(不含文本)。 */
253
+ filterCount: number
254
+ /** 会被画出来的节点数。 */
255
+ laneCount: number
142
256
  onClick: () => void
143
257
  }): ReactNode {
144
- const active = kinds.size + problems.size
258
+ /**
259
+ * 0044:原来这里挂着一枚「N 条待确认的卡片提议」角标,现在没有它了。
260
+ *
261
+ * 那枚角标的紧迫感来自**确认门**:未确认的提议不落 `02_wiki/`,没人点头,
262
+ * 一次排查就等于白跑。门撤掉之后卡片直接入库、已经能召回,review 只是给它抬分 ——
263
+ * 「有 N 张还没人看过」不再是需要从归因图谱上催的事,它属于「知识库 → Wiki」那一屏。
264
+ * 留一枚不再紧急的角标,只会教人忽略角标。
265
+ */
145
266
  return <ChromePanel className="p-[3px]">
146
- <Button size="sm" variant="ghost" className={MINI} onClick={onClick} title="展开问题大类、布局与类型筛选">
267
+ <span className="flex items-center gap-1">
268
+ <Button
269
+ size="sm"
270
+ variant="ghost"
271
+ className={MINI}
272
+ onClick={onClick}
273
+ title="展开四组切面:布局 → 问题定义 → 问题假设 → 检查点"
274
+ >
147
275
  <SlidersHorizontal size={12} />
148
- {layout === 'force' ? '力导向' : '分层'}
149
- {active > 0 && <span className="rounded-[4px] bg-tita-primary-soft px-1 font-mono text-[9.5px] text-tita-primary">{active}</span>}
276
+ {LAYOUT_LABEL[layout]}
277
+ {filterCount > 0 && <span className="rounded-[4px] bg-tita-primary-soft px-1 font-mono text-[9.5px] text-tita-primary">{filterCount} 项</span>}
278
+ <span className="font-mono text-[9.5px] text-tita-ink-faint">{laneCount}</span>
150
279
  </Button>
280
+ </span>
151
281
  </ChromePanel>
152
282
  }
153
283
 
154
- /* ───────────────────────────────────────────────── 右上:图例 + 全屏 ── */
284
+ /* ─────────────────────────────────────────────── 画布右上:读法(四个同排) ── */
155
285
 
156
286
  /**
157
- * 右上角的一对图标按钮:展开图例、进出全屏。
287
+ * 右上角:**四个读法开关同排**(§7.1 的 v1.4 调整)。
158
288
  *
159
- * 为什么图例不直接摊开在右上角:**380px 的一行放不下两块面板**(工具条内容
160
- * 约 240px,图例配色至少 200px)。硬挤的结果是两块都变形 —— 所以这里只留
161
- * 「图例」这一个入口,展开的正文落到下一行右侧(`GraphStage` 的 `side` 槽),
162
- * 那里是整行宽度,放得下 9 种边和 4 条说明。
289
+ * 图例 · 全景 / 聚焦 · 保留 1 跳邻居 · 全屏 —— 这四个都是「怎么看这张图」,
290
+ * 既不是筛选也不是内容,所以集中在一处。原来「全景 / 聚焦」在右侧中部、
291
+ * 「保留 1 跳邻居」在底中,四件事散在三个角上,扫一遍要来回跳。
163
292
  *
164
- * 这也是「图例在右上角」的正确读法:**入口在右上角,正文跟着入口展开**,
165
- * 而不是把正文钉死在角上、盖住半张图。
293
+ * 全屏按钮**任何子视图都留着**:换个视图就丢掉出口,是这一轮最不该犯的错。
166
294
  */
167
- export function LegendToggle({ open, onToggle, full, onToggleFull, showLegend = true }: {
168
- open: boolean
169
- onToggle: () => void
295
+ export function ReadBar({ legendOpen, onToggleLegend, showLegend, overview, onOverview,
296
+ includeNeighbors, onIncludeNeighbors, full, onToggleFull, showScope = true }: {
297
+ legendOpen: boolean
298
+ onToggleLegend: () => void
299
+ /** 案例 / 排查视图里没有图可读,图例、全景与邻居开关就没有对象。 */
300
+ showLegend: boolean
301
+ overview: boolean
302
+ onOverview: (value: boolean) => void
303
+ includeNeighbors: boolean
304
+ onIncludeNeighbors: (value: boolean) => void
170
305
  full: boolean
171
306
  onToggleFull: () => void
172
- /**
173
- * 案例 / 排查视图里没有图可读,图例就没有对象。
174
- *
175
- * 但**全屏按钮必须留下来**:全屏是「这一整块(含三个视图)占满视口」,
176
- * 换个视图就丢掉出口,是这一轮最不该犯的错。
177
- */
178
- showLegend?: boolean
307
+ showScope?: boolean
179
308
  }): ReactNode {
180
- return <ChromePanel className="flex items-center gap-1 p-[3px]">
309
+ return <ChromePanel className="flex items-center gap-[3px] p-[3px]">
181
310
  {showLegend && <Button
182
311
  size="sm"
183
- variant={open ? 'default' : 'ghost'}
184
- className="h-[22px] min-h-[22px] gap-1 px-1.5 text-[10.5px]"
185
- aria-label={open ? '隐藏图例' : '显示图例'}
186
- aria-expanded={open}
187
- title={open ? '隐藏图例' : '显示图例:配色、九种边、空区与僵尸边'}
188
- onClick={onToggle}
312
+ variant={legendOpen ? 'default' : 'ghost'}
313
+ className="h-[24px] min-h-[24px] gap-1 px-1.5 text-[10.5px]"
314
+ aria-label={legendOpen ? '隐藏图例' : '显示图例'}
315
+ aria-expanded={legendOpen}
316
+ title={legendOpen ? '隐藏图例' : '显示图例:配色、十种边、空区与僵尸边'}
317
+ onClick={onToggleLegend}
189
318
  ><CircleHelp size={12} />图例</Button>}
319
+
320
+ {showLegend && showScope && <Button
321
+ size="sm"
322
+ variant="ghost"
323
+ className="h-[24px] min-h-[24px] px-1.5 text-[10.5px]"
324
+ aria-pressed={!overview}
325
+ title={overview ? '聚焦:只看选中节点的 1 跳邻域' : '全景:看整张图'}
326
+ onClick={() => onOverview(!overview)}
327
+ >{overview ? '全景' : '聚焦'}</Button>}
328
+
329
+ {showLegend && showScope && <Button
330
+ size="sm"
331
+ variant="ghost"
332
+ className="h-[24px] min-h-[24px] px-1.5 text-[10.5px]"
333
+ aria-pressed={includeNeighbors}
334
+ title="保留 1 跳邻居:只留命中节点会让图散成孤点,边的语义一起丢"
335
+ onClick={() => onIncludeNeighbors(!includeNeighbors)}
336
+ >1 跳邻居</Button>}
337
+
190
338
  <Button
191
339
  size="icon"
192
- variant="outline"
193
- className="h-[22px] w-[22px] min-w-[22px]"
340
+ variant={full ? 'default' : 'outline'}
341
+ className="h-[24px] w-[24px] min-w-[24px]"
194
342
  aria-label={full ? '退出全屏' : '全屏查看'}
195
343
  title={full ? '退出全屏(Esc)' : '全屏查看:整个视口都用来放这块内容'}
196
344
  onClick={onToggleFull}
@@ -205,18 +353,18 @@ export function LegendToggle({ open, onToggle, full, onToggleFull, showLegend =
205
353
  * 用户会以为「图例已经开着」,而真正说明含义的那半仍然是藏着的。要么整块在,
206
354
  * 要么整块不在,状态才可读。
207
355
  *
208
- * **两个分组必须分开写。** 这里的五种颜色**全部是原因侧**(失败模式,挂在假设上),
356
+ * **两个分组必须分开写。** 这里的五种颜色**全部是原因侧**(失败原因,挂在假设上),
209
357
  * 一份只画颜色的图例会让人以为它在说「问题有哪几类」—— 这正是最常被看错的一处。
210
- * 所以原因侧明写「失败模式 · 原因侧」,问题侧(现象)单独一段、并且把
358
+ * 所以原因侧明写「失败原因 · 原因侧」,问题侧(现象)单独一段、并且把
211
359
  * 「现象节点有几个、有没有分类」如实报出来:**分类还没建的地方要看得见,而不是留白。**
212
360
  */
213
361
  export function LegendBody({ stats }: { stats: AttributionStats }): ReactNode {
214
362
  return <ChromePanel className="flex flex-col gap-1.5 p-2">
215
363
  <div className="text-[9.5px] leading-[14px] text-tita-ink-faint">
216
- <span className="font-[650] text-tita-ink-muted">失败模式</span> · 原因侧(挂在假设上)
364
+ <span className="font-[650] text-tita-ink-muted">失败原因</span> · 原因侧(挂在假设上)
217
365
  </div>
218
366
  <div className="flex flex-wrap gap-1">
219
- {Object.entries(FAILURE_MODE_STYLE).map(([key, tone]) => <span
367
+ {Object.entries(FAILURE_REASON_STYLE).map(([key, tone]) => <span
220
368
  key={key}
221
369
  className="flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-[10px] leading-[15px]"
222
370
  style={{ borderColor: tone.stroke, background: tone.fill, color: tone.text }}
@@ -237,8 +385,8 @@ export function LegendBody({ stats }: { stats: AttributionStats }): ReactNode {
237
385
  >{item.label}</span>)}
238
386
  </div>
239
387
  <p className="text-[9.5px] leading-relaxed text-tita-ink-muted">
240
- 问题大类刻意不给颜色:颜色通道已经被失败模式占用。问题侧走文字 —— 现象节点下方的
241
- 徽标写着它属于哪个大类,左上的「问题」切面按大类收窄,并把它的检查一起带出来。
388
+ 问题大类刻意不给颜色:颜色通道已经被失败原因占用。问题侧走文字 —— 现象节点下方的
389
+ 徽标写着它属于哪个大类,左上的「问题定义」切面按大类收窄,并把它的检查一起带出来。
242
390
  </p>
243
391
  </div>
244
392
 
@@ -253,78 +401,59 @@ export function LegendBody({ stats }: { stats: AttributionStats }): ReactNode {
253
401
  </span>)}
254
402
  </div>
255
403
  <ul className="flex flex-col gap-0.5 text-[9.5px] leading-relaxed text-tita-ink-muted">
256
- <li>圆的大小 = 复用次数;圆里是节点 id,标题挂在圆下方</li>
257
- <li><span className="text-tita-danger">红色虚环 + 空区徽标</span> = 有假设但一条检查都没有(当前 {stats.gaps} 个)</li>
404
+ <li>圆的大小 = 复用次数(关系视图);动线视图里每个节点都读得出名字</li>
405
+ <li><span className="text-tita-danger">红色虚环 + 空区徽标</span> = 有假设但一条检查点都没有(当前 {stats.gaps} 个)。动线视图里它就是「② 段右边没有线」</li>
258
406
  <li><span className="text-tita-ink-faint">灰色虚线</span> = 僵尸边,90 天未走(当前 {stats.zombies} 条)</li>
259
- <li>实例层默认折叠 —— 它随每次排查线性增长,展开会把类型层挤成小点</li>
407
+ <li>⑤ 段(实际查询与结果)默认折叠 —— 它随每次排查线性增长;被点亮的那条链例外</li>
260
408
  </ul>
261
409
  </div>
262
410
  </ChromePanel>
263
411
  }
264
412
 
265
- /* ───────────────────────────────────────────── 底部中间:其余筛选 ── */
413
+ /* ─────────────────────────────────────────────── 画布底中:只剩搜索 ── */
266
414
 
267
415
  /**
268
- * 底部中间的筛选条:文本、失败模式、邻居展开、清除。
416
+ * 底中:**只剩文本搜索 + 命中数**(§7.1 的 v1.4 调整)。
269
417
  *
270
- * 放在底部而不是顶部,是因为它是**读图之后的动作**:先看到形状,再收窄。
271
- * 而顶部的分层与类型是「决定这张图怎么画」,顺序相反。
418
+ * 失败原因切面上移到了左栏第 ③ 组,「保留 1 跳邻居」上移到了右上读法排。
419
+ * 搜索留在底中,因为它是**跨全图**的动作,不属于任何一段。
272
420
  */
273
- export function FilterBar({ query, onQuery, modes, onToggleMode, includeNeighbors, onIncludeNeighbors, filtering, onClear, hit, total }: {
421
+ export function FilterBar({ query, onQuery, filtering, onClear, hit, total }: {
274
422
  query: string
275
423
  onQuery: (value: string) => void
276
- modes: ReadonlySet<FailureMode>
277
- onToggleMode: (mode: FailureMode) => void
278
- includeNeighbors: boolean
279
- onIncludeNeighbors: (value: boolean) => void
280
424
  filtering: boolean
281
425
  onClear: () => void
282
426
  hit: number
283
427
  total: number
284
428
  }): ReactNode {
285
- return <ChromePanel className="flex w-[min(520px,100%)] flex-col gap-1.5 p-2">
286
- <div className="flex items-center gap-1.5">
287
- <div className="relative min-w-0 flex-1">
288
- <Search size={12} className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 text-tita-ink-faint" />
289
- <Input
290
- value={query}
291
- onChange={event => onQuery(event.target.value)}
292
- placeholder="筛选节点:id / 标题 / 判据…"
293
- className="h-[26px] min-h-[26px] pl-6 text-[11px]"
294
- />
295
- </div>
296
- <span className="shrink-0 text-[10px] tabular-nums text-tita-ink-faint">命中 {hit}/{total}</span>
297
- {filtering && <Button size="sm" variant="ghost" className={MINI} onClick={onClear}><X size={11} />清除</Button>}
298
- </div>
299
-
300
- <div className="flex items-start gap-1.5 text-[10px] text-tita-ink-muted">
301
- <span className="shrink-0 pt-0.5 font-[650] text-tita-ink-faint">切面</span>
302
- <div className="flex flex-wrap gap-1">
303
- {Object.entries(FAILURE_MODE_STYLE).map(([key, tone]) => <Chip
304
- key={key}
305
- active={modes.has(key as FailureMode)}
306
- color={tone.stroke}
307
- onClick={() => onToggleMode(key as FailureMode)}
308
- >{tone.label}</Chip>)}
309
- </div>
310
- <Chip active={includeNeighbors} onClick={() => onIncludeNeighbors(!includeNeighbors)} title="只留命中节点会让图散成孤点">
311
- 保留 1 跳邻居
312
- </Chip>
429
+ return <ChromePanel className="flex w-[min(520px,100%)] items-center gap-1.5 p-2">
430
+ <div className="relative min-w-0 flex-1">
431
+ <Search size={12} className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 text-tita-ink-faint" />
432
+ <Input
433
+ value={query}
434
+ onChange={event => onQuery(event.target.value)}
435
+ placeholder="筛选节点:id / 标题 / 判据…"
436
+ className="h-[26px] min-h-[26px] pl-6 text-[11px]"
437
+ />
313
438
  </div>
439
+ <span className="shrink-0 text-[10px] tabular-nums text-tita-ink-faint">命中 {hit}/{total}</span>
440
+ {filtering && <Button size="sm" variant="ghost" className={MINI} onClick={onClear}><X size={11} />清除</Button>}
314
441
  </ChromePanel>
315
442
  }
316
443
 
317
- /* ───────────────────────────────────────────────── 右下:统计 ── */
444
+ /* ─────────────────────────────────────────────── 画布左下:读数 ── */
318
445
 
319
446
  /**
320
- * 右下角统计。
447
+ * 左下角统计:**从右下搬来**(§7.1 的 v1.4 调整)。
448
+ *
449
+ * 搬家的理由是左列因此形成「上面改、下面读」的一列,而右下让给图例 / 详情的展开正文。
321
450
  *
322
451
  * 只放**随筛选变化**的数:它回答的是「我现在看的这张图有多大、缺多少」。
323
- * 图例里已经写了空区/僵尸的含义,这里只给数,不重复解释。
452
+ * 图例里已经写了空区 / 僵尸边的含义,这里只给数,不重复解释。
324
453
  *
325
- * 数据来源徽标(`05_diagnosis` / `seed 数据`)也挂在这里:它和这些数一样是
326
- * 「这份内容从哪来」的事实,而且它必须始终可见 —— 看到的是真数据还是兜底样例,
327
- * 不能靠猜。
454
+ * 数据来源徽标(`02_wiki/diagnosis` / `05_diagnosis(旧格式)`)也挂在这里:它和这些数
455
+ * 一样是「这份内容从哪来」的事实,而且它必须始终可见 —— 看到的是卡片还是旧格式的
456
+ * jsonl,不能靠猜。
328
457
  */
329
458
  export function StatsStrip({ stats, source, problems }: {
330
459
  stats: AttributionStats
@@ -332,7 +461,7 @@ export function StatsStrip({ stats, source, problems }: {
332
461
  /** 编译 / 迁移过程中发现的问题。空数组表示一切正常。 */
333
462
  problems: string[]
334
463
  }): ReactNode {
335
- return <ChromePanel className="flex flex-wrap items-center gap-x-2 gap-y-0.5 px-2 py-1">
464
+ return <ChromePanel className="flex w-[min(200px,100%)] flex-wrap items-center gap-x-2 gap-y-0.5 px-2 py-1">
336
465
  <Stat label="类型" value={stats.typeNodes} />
337
466
  <Divider />
338
467
  <Stat label="引用" value={stats.referenceNodes} />
@@ -348,24 +477,23 @@ export function StatsStrip({ stats, source, problems }: {
348
477
  title={problems.join('\n')}
349
478
  >{problems.length} 个问题</span>}
350
479
  <span
351
- className="ml-auto shrink-0 rounded-[4px] bg-tita-surface-muted px-1 font-mono text-[9.5px] text-tita-ink-faint"
352
- title={SOURCE_HINT[source] ?? SOURCE_HINT['seed 数据']}
353
- >{source}</span>
480
+ className="w-full shrink-0 truncate font-mono text-[9.5px] text-tita-ink-faint"
481
+ title={SOURCE_HINT[source] ?? source}
482
+ >来源 {source}</span>
354
483
  </ChromePanel>
355
484
  }
356
485
 
357
486
  /**
358
487
  * 数据来源的说明。
359
488
  *
360
- * 这里必须能分清三种形态,因为它们意味着不同的可信度:卡片是当前真相,
361
- * `05_diagnosis` 是**旧格式**(迁移没能无损生效),`seed 数据` 是宿主没就绪时的兜底示例。
362
- * 一句笼统的「05_diagnosis」会把它们混成一种,而这正是最不该让人猜的地方。
489
+ * 这里必须能分清两种形态,因为它们意味着不同的可信度:`02_wiki/diagnosis` 是当前真相
490
+ * (卡片),`05_diagnosis(旧格式)` 意味着迁移没能无损生效。一句笼统的「05_diagnosis」
491
+ * 会把它们混成一种,而这正是最不该让人猜的地方。
363
492
  */
364
493
  const SOURCE_HINT: Record<string, string> = {
365
494
  '02_wiki/diagnosis': '来自 wiki 卡片(诊断卡片是当前唯一的真相)',
366
495
  '05_diagnosis(旧格式)': '旧格式 jsonl:迁移会丢知识,因此仍按原样读',
367
- '05_diagnosis': '来自 05_diagnosis 目录',
368
- 'seed 数据': '宿主未就绪,当前展示的是内置示例',
496
+ '还没有排查知识': '这个知识库还没有排查知识',
369
497
  }
370
498
 
371
499
  function Divider(): ReactNode {
@@ -382,9 +510,9 @@ function Stat({ label, value, tone }: { label: string; value: number; tone?: 'wa
382
510
  </span>
383
511
  }
384
512
 
385
- /* ───────────────────────────────────────────── 选中节点详情卡 ── */
513
+ /* ─────────────────────────────────────────────── 画布右侧:选中节点详情 ── */
386
514
 
387
- export function NodeDetailCard({ node }: { node: AttributionNode }): ReactNode {
515
+ export function NodeDetailCard({ node, onClear }: { node: AttributionNode; onClear?: () => void }): ReactNode {
388
516
  return <ChromePanel className="flex flex-col gap-1.5 p-2">
389
517
  <div className="flex items-start justify-between gap-2">
390
518
  <span className="text-[11.5px] font-semibold leading-snug">{node.title}</span>
@@ -393,11 +521,23 @@ export function NodeDetailCard({ node }: { node: AttributionNode }): ReactNode {
393
521
  {node.summary && <p className="text-[10.5px] leading-relaxed text-tita-ink-muted">{node.summary}</p>}
394
522
  <div className="flex flex-wrap gap-1">
395
523
  <Badge tone="neutral">{NODE_KIND_LABEL[node.kind]}</Badge>
396
- {node.failureMode && <Badge tone="violet">{FAILURE_MODE_STYLE[node.failureMode].label}</Badge>}
524
+ {node.failureReason && <Badge tone="violet">{FAILURE_REASON_STYLE[node.failureReason].label}</Badge>}
525
+ {node.problemClass && <Badge tone="neutral">{PROBLEM_CLASS_LABEL[node.problemClass]}</Badge>}
397
526
  {node.reuseCount !== undefined && <Badge tone="neutral">复用 {node.reuseCount}</Badge>}
398
527
  {node.costMinutes !== undefined && <Badge tone={node.automation === 'auto' ? 'success' : 'warning'}>
399
528
  {node.costMinutes} min · {node.automation === 'auto' ? '自动' : '人工'}
400
529
  </Badge>}
401
530
  </div>
531
+ {/* 检查点的查询依据:它落在 `queries` 边上,从节点本身读不出来。 */}
532
+ {node.assets && node.assets.length > 0 && <p className="text-[10px] leading-snug text-tita-ink-faint">
533
+ 要去查:{node.assets.join(' · ')}
534
+ </p>}
535
+ {node.discriminator && <p className="text-[10px] leading-snug text-tita-ink-muted">判据:{node.discriminator}</p>}
536
+ {node.why && <p className="text-[10px] leading-snug text-tita-ink-muted">为什么算数:{node.why}</p>}
537
+ {onClear && <button
538
+ type="button"
539
+ onClick={onClear}
540
+ className="mt-0.5 flex items-center gap-1 self-start rounded-md px-1 py-0.5 text-[10px] text-tita-ink-faint hover:text-tita-ink focus-visible:outline-[3px] focus-visible:outline-offset-1 focus-visible:outline-tita-focus-ring"
541
+ ><ChevronRight size={10} />取消点亮,回到全亮</button>}
402
542
  </ChromePanel>
403
543
  }