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,19 +1,32 @@
1
1
  /**
2
- * 预览抽屉的三个公共分块(0035):**元信息 → 内容 → 版本**。
2
+ * 预览抽屉的公共分块(0035 起):**元信息 → 内容 → 版本**。
3
3
  *
4
4
  * SourceRaw 的原文预览与 wiki 卡片的预览必须是**同一种形态**。两处各写一遍结构,
5
5
  * 就会出现「一个把元信息放最上面、一个放最下面」「一个能给文本预览、另一个不能」
6
6
  * 这类漂移 —— 而它们回答的是同一个问题:这份东西是什么、写了什么、改过几次。
7
7
  *
8
- * 这一层只定义**顺序与分块**(小节标题 + 计数 + 内容),字段仍由调用方给:原文有
8
+ * 这一层只定义**顺序与分块**(小节头 + 计数 + 内容),字段仍由调用方给:原文有
9
9
  * `content_hash`,卡片有 `entity_id`,本来就不一样,硬凑成同一张表只会两处都别扭。
10
10
  * 时间也由调用方格式化(见 `format.ts`)—— 元信息用标准格式,列表行用紧凑格式。
11
+ *
12
+ * 0042 重排了三件事,理由都是「三种层级要一眼分得开」:
13
+ *
14
+ * 1. **小节头换了字号**(`drawerSubHead` 13px/700,上加分隔线)。此前小节头是
15
+ * 12px/700、正文 12px/400 —— 同字号只差一个字重,于是「内容」两个字和它下面的
16
+ * 正文在视觉上是同一段文字的第 0 行与第 1 行。
17
+ * 2. **视图切换离开小节头那一行。** 它被压到 20px 高站在 12px 的小节标题旁边,与
18
+ * 小节名、字符数、(曾经的)出口按钮四种东西共享一行。现在它有自己的「读法工具栏」。
19
+ * 3. **正文块锁高度、自己滚。** 此前整条抽屉体是一根长滚动条:正文有多长,「版本」
20
+ * 就被推到多远,而版本是要动手的地方(回滚)。现在正文块有上限、块内滚动,
21
+ * 下面的分块位置不再随正文长度漂移。
11
22
  */
12
23
 
13
- import { useState, type ReactNode } from 'react'
14
- import { FileCode2, FileText, History, Image, ListTree } from '@tita-data-agent/ui/icons'
15
- import { Tabs, TabsContent, TabsList, TabsTrigger } from '@tita-data-agent/ui/components'
24
+ import { useMemo, useState, type ReactNode } from 'react'
25
+ import { ChevronDown, ChevronUp, FileCode2, FileText, GitBranch, History, ListTree } from '@tita-data-agent/ui/icons'
26
+ import { Button, Tabs, TabsContent, TabsList, TabsTrigger, cn } from '@tita-data-agent/ui/components'
16
27
  import { VersionManager, type VersionManagerProps } from '@tita-data-agent/ui/versions'
28
+ import { DrawerSectionHead } from './DrawerSection.tsx'
29
+ import { pathImagesOf, referencedRelsOf } from './asset-images.ts'
17
30
  // 平台提供的 Markdown 原子(DSH 的模块表里有它,本插件的 client bundle 也已把它列为
18
31
  // external)。原文与卡片正文本身就是 Markdown,用 `<pre>` 显示等于把标题、表格、链接
19
32
  // 全都拍平成源码 —— 所以默认阅读态走它,源码视图是**另一个**选项,不是替代品。
@@ -35,27 +48,35 @@ const MARKDOWN_LABELS: MarkdownLabels = {
35
48
  footnotes: '脚注',
36
49
  }
37
50
 
38
- /** 正文只渲染前 2 万字符:更长的原文在抽屉里没有阅读意义,也会拖慢 Markdown 渲染。 */
39
- const PREVIEW_CHARS = 20000
51
+ /**
52
+ * 正文在抽屉里最多渲染这么多字符。**与服务端 `SOURCE_PREVIEW_CHARS` 同值。**
53
+ *
54
+ * 服务端在传输层就把原文封顶了,前端再取同一个数,是为了让「界面上写出来的那个数」
55
+ * 与「服务端封顶的那个数」不可能是两个:0042 之前客户端另有一个 20000,
56
+ * 于是同一份材料有两个上限,而界面上一个都没写。
57
+ *
58
+ * wiki 卡片没有独立整页出口,所以卡片正文超过它时给**本地展开**(见 `PreviewContent`
59
+ * 的「展开全部」),不是只截断不给出路。
60
+ */
61
+ const PREVIEW_CHARS = 4000
40
62
 
41
63
  export interface PreviewRow {
42
64
  label: string
43
65
  value: ReactNode
44
66
  /** 等宽显示:hash / id / 路径这类要逐字比对的值。 */
45
67
  code?: boolean
68
+ /** 悬停说明。**必须渲染出来** —— 否则调用方写了它、界面上什么也没有。 */
69
+ title?: string
46
70
  }
47
71
 
48
72
  /** 元信息:一张两列的事实表。标签列宽固定,所以标签要短且彼此对齐。 */
49
73
  export function PreviewMeta({ rows }: { rows: PreviewRow[] }): ReactNode {
50
- // 每一块都包一层 `drawerSection`:标题与它自己的内容是一块,块与块之间的间距由
51
- // 外层 `drawerStack` 给。不包的话「元信息」标题和它下面那张表是两个同级节点,
52
- // 外层 gap 会把标题和自己的表也拉开。
53
74
  return <section className={styles.drawerSection}>
54
- <div className={styles.kbSubHead}><h4><ListTree size={13} />元信息</h4></div>
75
+ <DrawerSectionHead icon={<ListTree size={13} />} title="元信息" />
55
76
  <dl className={styles.kbKv}>
56
77
  {rows.map(row => <div key={row.label}>
57
78
  <dt>{row.label}</dt>
58
- <dd className={row.code ? styles.kbCode : undefined}>{row.value}</dd>
79
+ <dd className={row.code ? styles.kbCode : undefined} {...(row.title ? { title: row.title } : {})}>{row.value}</dd>
59
80
  </div>)}
60
81
  </dl>
61
82
  </section>
@@ -64,52 +85,166 @@ export function PreviewMeta({ rows }: { rows: PreviewRow[] }): ReactNode {
64
85
  export interface PreviewAsset { rel: string; url: string; bytes: number }
65
86
 
66
87
  /**
67
- * 内容:小节标题 + 视图切换 + 正文。
88
+ * 内容:小节头 + 读法工具栏 + 正文(锁高内滚)+ 正文没提到的图。
68
89
  *
69
90
  * 两个视图是**同一份内容**的两种读法:Markdown 是阅读态(默认),文本是源码态,
70
91
  * 用于逐字核对 —— 与版本 diff 是同一种读法。切换不重新取数,也不改内容。
71
- * 图片素材排在这一块的末尾:它也是内容,只是渲染器没能就地展开的那部分。
92
+ *
93
+ * **图片与正文是一份内容,不是一个附件区**(0049)。此前末尾另起一块「图片素材」
94
+ * 网格:正文里的图已经就地渲染过一遍,末尾再排一遍,同一张图一屏里出现两次。
95
+ * 现在只有一份读法 —— 正文里提到过的图就地渲染,**没提到的排在正文末尾**,
96
+ * 都在同一个滚动容器里。独立整页也是这么读的(它靠 `resolveImage` 就地渲染)。
97
+ *
98
+ * **正文里的图要就地显示**(0047):`MarkdownText` 的 `pathImages` 拿 `assets` 把
99
+ * 相对引用换成 `/knowledge/assets/...`。不接它的话,浏览器会按当前页面地址去解析
100
+ * `_assets/<bucket>/img-x.png`(结果是 `/_assets/...` 404),于是「预览里看不到图」。
101
+ *
102
+ * `totalChars` 说全文有多少字符(服务端按 `SOURCE_PREVIEW_CHARS` 封顶,
103
+ * `content.length` 已经不是全文长度了)。读全文的出口由调用方放在**小节头上**
104
+ * (`action`):它是「这份东西还能去哪看」,位置要固定,不该随正文是否被截断而出现或消失。
72
105
  */
73
- export function PreviewContent({ content, assets = [] }: { content: string; assets?: PreviewAsset[] }): ReactNode {
106
+ export function PreviewContent({ content, assets = [], totalChars, action }: {
107
+ content: string
108
+ assets?: PreviewAsset[]
109
+ /** 全文总字符数。省略时按 `content.length` 算(它本来就是全文)。 */
110
+ totalChars?: number
111
+ /** 小节头右侧的动作(例如「新页面打开」)。与「内容」同一行,不占正文宽度。 */
112
+ action?: ReactNode
113
+ }): ReactNode {
74
114
  const [view, setView] = useState<'md' | 'text'>('md')
75
- const shown = content.slice(0, PREVIEW_CHARS)
76
- const clipped = shown.length < content.length
77
- return <section className={styles.drawerSection}><Tabs value={view} onValueChange={next => setView(next as 'md' | 'text')}>
78
- <div className={styles.kbSubHead}>
79
- <h4><FileText size={13} />内容</h4>
80
- <TabsList className={`${styles.tabsList} ${styles.kbViewTabs}`}>
81
- <TabsTrigger value="md">Markdown</TabsTrigger>
82
- <TabsTrigger value="text"><FileCode2 size={11} />文本</TabsTrigger>
83
- </TabsList>
84
- <span>{content.length} 字符{clipped ? ` · 只渲染前 ${shown.length}` : ''}</span>
85
- </div>
86
- <TabsContent value="md">
87
- <div className={styles.kbMarkdown}><MarkdownText text={shown} labels={MARKDOWN_LABELS} /></div>
88
- </TabsContent>
89
- <TabsContent value="text">
90
- <pre className={styles.kbPre}>{shown}</pre>
91
- </TabsContent>
92
- {assets.length > 0 && <>
93
- <div className={styles.kbSubHead}><h4><Image size={13} />图片素材</h4><span>{assets.length} 张</span></div>
94
- <div className={styles.kbAssets}>
95
- {assets.map(asset => <figure key={asset.rel} className={styles.kbAsset}>
96
- <img src={asset.url} alt={asset.rel} loading="lazy" />
97
- <figcaption>{formatBytes(asset.bytes)}</figcaption>
98
- </figure>)}
115
+ /** 卡片正文超过上限时的本地展开:卡片没有整页出口,只能在这里给出路。 */
116
+ const [expanded, setExpanded] = useState(false)
117
+ const shown = expanded ? content : content.slice(0, PREVIEW_CHARS)
118
+ const total = totalChars ?? content.length
119
+ const clipped = shown.length < total
120
+ const canExpand = content.length > PREVIEW_CHARS
121
+ /**
122
+ * 解析器按 `assets` 的记忆化:`MarkdownText` 是 memo 组件,每次渲染换一个新对象
123
+ * 会让它的流式渲染缓存整体作废(平台对这个 prop 的身份有明确要求)。
124
+ */
125
+ const pathImages = useMemo(() => pathImagesOf(assets), [assets])
126
+ /** 正文没提到的图:排在正文末尾,与正文同一个滚动容器。 */
127
+ const extra = useMemo(() => {
128
+ const referenced = referencedRelsOf(content, assets)
129
+ return assets.filter(asset => !referenced.has(asset.rel))
130
+ }, [content, assets])
131
+ return <section className={styles.drawerSection}>
132
+ <DrawerSectionHead icon={<FileText size={13} />} title="内容">
133
+ {/* 事实与动作**成组**靠右:`drawerSubHead` 是 `space-between`,三个平级子节点会
134
+ 把「20 字符」摆到行的正中间,读起来像另一栏。 */}
135
+ <span className={styles.drawerHeadAside}>
136
+ <span className={styles.drawerHeadFact}>
137
+ {total} 字符{clipped ? ` · 这里显示前 ${shown.length}` : ''}{assets.length > 0 ? ` · ${assets.length} 张图` : ''}
138
+ </span>
139
+ {action}
140
+ </span>
141
+ </DrawerSectionHead>
142
+ {/* 读法工具栏:这个分块的**两种读法**是它自己的事,不再挤进小节标题那一行。 */}
143
+ <Tabs value={view} onValueChange={next => setView(next as 'md' | 'text')}>
144
+ <div className={styles.drawerToolbar}>
145
+ <TabsList className={cn(styles.tabsList, styles.kbViewTabs)}>
146
+ <TabsTrigger value="md">Markdown</TabsTrigger>
147
+ <TabsTrigger value="text"><FileCode2 size={11} />文本</TabsTrigger>
148
+ </TabsList>
149
+ {canExpand && <Button
150
+ variant="ghost" size="sm" className={styles.drawerMore}
151
+ onClick={() => setExpanded(value => !value)}
152
+ >{expanded ? <ChevronUp size={11} /> : <ChevronDown size={11} />}
153
+ {expanded ? '收起' : `展开全部(还有 ${content.length - PREVIEW_CHARS} 字符)`}</Button>}
99
154
  </div>
100
- </>}
101
- </Tabs></section>
155
+ <TabsContent value="md">
156
+ <div className={styles.drawerBody}>
157
+ <div className={styles.kbMarkdown}>
158
+ <MarkdownText text={shown} labels={MARKDOWN_LABELS} {...(pathImages === undefined ? {} : { pathImages })} />
159
+ </div>
160
+ {extra.length > 0 && <>
161
+ {/* 这块不是「另一些内容」,是**同一份内容的余下部分**:正文里没出现(或落在
162
+ 预览长度之外)的图。写清楚它是什么,人才不会以为图丢了。 */}
163
+ <p className={styles.kbAssetsNote}>
164
+ 正文里没出现的图 · {extra.length} 张{clipped ? `(正文只显示前 ${shown.length} 字符)` : ''}
165
+ </p>
166
+ <div className={styles.kbAssets}>
167
+ {extra.map(asset => <figure key={asset.rel} className={styles.kbAsset} title={asset.rel}>
168
+ {/* alt 用文件名而不是整条相对路径:图挂了的时候,`01_source_raw/_assets/…`
169
+ 会在 84px 的格子里折成三行,把这一块撑得比正文还高。 */}
170
+ <img src={asset.url} alt={asset.rel.split('/').pop() ?? asset.rel} loading="lazy" />
171
+ <figcaption>{formatBytes(asset.bytes)}</figcaption>
172
+ </figure>)}
173
+ </div>
174
+ </>}
175
+ </div>
176
+ </TabsContent>
177
+ <TabsContent value="text">
178
+ <div className={styles.drawerBody}><pre className={styles.kbPre}>{shown}</pre></div>
179
+ </TabsContent>
180
+ </Tabs>
181
+ </section>
182
+ }
183
+
184
+ /**
185
+ * 相关卡片(0049):出链与入链,两处都是**可点的入口**。
186
+ *
187
+ * 为什么正文里已经有 `## 相关` 一块、这里还要再列一遍:正文里那一块写的是
188
+ * `[[类型/标题]]`,是给 `grep` 和编辑器用的**文件内引用**(面板渲染不了它,卡片也没有
189
+ * 自己的页面地址);这里这几行是面板里真的能点开的东西 —— 点一下换到那张卡片。两处
190
+ * 的清单来自同一个计划(服务端的 `planRelated`),所以不会一处写着 A、一处列着 B。
191
+ *
192
+ * 「它引用 / 引用它」分开列,是因为方向不同:前者是这张卡片自己的判断,后者是别人
193
+ * 把它拉进来的。合成一列就分不清「我提到它」和「它提到我」。
194
+ */
195
+ export interface RelatedLink {
196
+ entityId: string
197
+ entityType: string
198
+ title: string
199
+ reason: 'relation' | 'source'
200
+ }
201
+
202
+ const RELATED_REASON: Record<RelatedLink['reason'], string> = {
203
+ relation: '来源里写明的关系',
204
+ source: '同一份来源',
205
+ }
206
+
207
+ export function PreviewRelated({ outgoing, incoming, onOpen }: {
208
+ outgoing: RelatedLink[]
209
+ incoming: RelatedLink[]
210
+ onOpen: (link: RelatedLink) => void
211
+ }): ReactNode {
212
+ if (outgoing.length === 0 && incoming.length === 0) return null
213
+ const group = (label: string, links: RelatedLink[], prefix: string): ReactNode => links.length === 0 ? null : <>
214
+ <p className={styles.kbRelatedGroup}>{label}</p>
215
+ <ul className={styles.kbRelatedList}>
216
+ {links.map(link => <li key={`${prefix}-${link.entityType}:${link.entityId}`}>
217
+ <button type="button" className={styles.kbRelatedRow} title={`${link.entityType}/${link.entityId}`} onClick={() => onOpen(link)}>
218
+ <span className={styles.kbRelatedTitle}>{link.title}</span>
219
+ <span className={styles.kbRelatedMeta}>{RELATED_REASON[link.reason]}</span>
220
+ </button>
221
+ </li>)}
222
+ </ul>
223
+ </>
224
+ return <section className={styles.drawerSection}>
225
+ <DrawerSectionHead icon={<GitBranch size={13} />} title="相关卡片">
226
+ <span className={styles.drawerHeadFact}>{outgoing.length + incoming.length} 条</span>
227
+ </DrawerSectionHead>
228
+ <div className={styles.kbRelated}>
229
+ {group('它引用', outgoing, 'out')}
230
+ {group('引用它', incoming, 'in')}
231
+ </div>
232
+ </section>
102
233
  }
103
234
 
104
235
  /**
105
- * 版本:小节标题 + 计数 + 一句「什么时候会留快照」+ 时间线。
236
+ * 版本:小节头 + 计数 + 一句「什么时候会留快照」+ 时间线。
106
237
  *
107
238
  * `hint` 不是可有可无的装饰:两种产物的快照触发条件不同(原文是内容变化,卡片是
108
239
  * 正文变化 —— 只改状态不造版本),不写清楚,用户会以为「我改过它,为什么没有版本」。
240
+ *
241
+ * 0042 没有把它降级成抽屉头的一枚按钮,理由有两条:它是当前**唯一**的回滚入口
242
+ * (`setRestoreTarget` 只在它这里被调用),而且正文块锁高之后它已经在一屏之内 ——
243
+ * 降级要新造一层状态,收益只是省一块高度。
109
244
  */
110
245
  export function PreviewVersions({ hint, ...manager }: { hint?: string } & VersionManagerProps): ReactNode {
111
246
  return <section className={styles.drawerSection}>
112
- <div className={styles.kbSubHead}><h4><History size={13} />版本</h4><span>{manager.entries?.length ?? 0} 个</span></div>
247
+ <DrawerSectionHead icon={<History size={13} />} title="版本"><span className={styles.drawerHeadFact}>{manager.entries?.length ?? 0} 个</span></DrawerSectionHead>
113
248
  {hint && <p className={styles.kbHint}>{hint}</p>}
114
249
  <VersionManager {...manager} />
115
250
  </section>