dsh-data-domain-knowledge 0.1.0-alpha.3 → 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.
- package/README.md +126 -22
- package/cordis.patch.yml +25 -16
- package/lib/client.js +2144 -1571
- package/lib/doc-page.js +40 -19
- package/lib/types/convert/host.d.ts.map +1 -1
- package/lib/types/convert/host.js +27 -2
- package/lib/types/convert/host.js.map +1 -1
- package/lib/types/convert/ingest.d.ts +7 -4
- package/lib/types/convert/ingest.d.ts.map +1 -1
- package/lib/types/convert/ingest.js +52 -14
- package/lib/types/convert/ingest.js.map +1 -1
- package/lib/types/convert/types.d.ts +2 -2
- package/lib/types/convert/types.d.ts.map +1 -1
- package/lib/types/convert/types.js +2 -2
- package/lib/types/convert/types.js.map +1 -1
- package/lib/types/diagnosis/cards.js +2 -2
- package/lib/types/diagnosis/run.d.ts +7 -1
- package/lib/types/diagnosis/run.d.ts.map +1 -1
- package/lib/types/diagnosis/run.js +8 -2
- package/lib/types/diagnosis/run.js.map +1 -1
- package/lib/types/diagnosis/seed-source.js +1 -1
- package/lib/types/diagnosis/seed.d.ts +1 -1
- package/lib/types/diagnosis/seed.js +1 -1
- package/lib/types/diagnosis/store.d.ts +13 -31
- package/lib/types/diagnosis/store.d.ts.map +1 -1
- package/lib/types/diagnosis/store.js +33 -71
- package/lib/types/diagnosis/store.js.map +1 -1
- package/lib/types/diagnosis/types.d.ts +1 -1
- package/lib/types/diagnosis/types.js +1 -1
- package/lib/types/extract-model.d.ts +67 -27
- package/lib/types/extract-model.d.ts.map +1 -1
- package/lib/types/extract-model.js +225 -97
- package/lib/types/extract-model.js.map +1 -1
- package/lib/types/extract.d.ts +16 -0
- package/lib/types/extract.d.ts.map +1 -1
- package/lib/types/extract.js +19 -2
- package/lib/types/extract.js.map +1 -1
- package/lib/types/fs-atomic.d.ts +3 -4
- package/lib/types/fs-atomic.d.ts.map +1 -1
- package/lib/types/fs-atomic.js +3 -4
- package/lib/types/fs-atomic.js.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +302 -89
- package/lib/types/index.js.map +1 -1
- package/lib/types/intent/types.js +1 -1
- package/lib/types/intent/types.js.map +1 -1
- package/lib/types/knowledge-bases.d.ts +13 -10
- package/lib/types/knowledge-bases.d.ts.map +1 -1
- package/lib/types/knowledge-bases.js +18 -27
- package/lib/types/knowledge-bases.js.map +1 -1
- package/lib/types/layout.d.ts +3 -3
- package/lib/types/layout.d.ts.map +1 -1
- package/lib/types/layout.js +4 -4
- package/lib/types/layout.js.map +1 -1
- package/lib/types/migrate.d.ts +1 -1
- package/lib/types/migrate.js +2 -2
- package/lib/types/migrate.js.map +1 -1
- package/lib/types/model-text.d.ts +99 -0
- package/lib/types/model-text.d.ts.map +1 -0
- package/lib/types/model-text.js +103 -0
- package/lib/types/model-text.js.map +1 -0
- package/lib/types/present.d.ts +239 -0
- package/lib/types/present.d.ts.map +1 -0
- package/lib/types/present.js +255 -0
- package/lib/types/present.js.map +1 -0
- package/lib/types/recall-rank.d.ts +37 -0
- package/lib/types/recall-rank.d.ts.map +1 -1
- package/lib/types/recall-rank.js +41 -1
- package/lib/types/recall-rank.js.map +1 -1
- package/lib/types/runtime.d.ts +419 -78
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/runtime.js +2104 -334
- package/lib/types/runtime.js.map +1 -1
- package/lib/types/settings-schema.d.ts +2 -0
- package/lib/types/settings-schema.d.ts.map +1 -1
- package/lib/types/settings-schema.js +2 -0
- package/lib/types/settings-schema.js.map +1 -1
- package/lib/types/source-images.d.ts +118 -0
- package/lib/types/source-images.d.ts.map +1 -0
- package/lib/types/source-images.js +288 -0
- package/lib/types/source-images.js.map +1 -0
- package/lib/types/types.d.ts +316 -39
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js +57 -11
- package/lib/types/types.js.map +1 -1
- package/lib/types/wiki-finalize.d.ts +140 -0
- package/lib/types/wiki-finalize.d.ts.map +1 -0
- package/lib/types/wiki-finalize.js +269 -0
- package/lib/types/wiki-finalize.js.map +1 -0
- package/lib/types/wiki-types.d.ts +2 -1
- package/lib/types/wiki-types.d.ts.map +1 -1
- package/lib/types/wiki-types.js +13 -7
- package/lib/types/wiki-types.js.map +1 -1
- package/package.json +3 -3
- package/preset/tita-data-agent/agent.cordis.yml +25 -16
- package/src/client/DrawerSection.tsx +18 -0
- package/src/client/KnowledgeBaseTab.tsx +294 -104
- package/src/client/KnowledgePanel.tsx +4 -3
- package/src/client/KnowledgeToolRow.tsx +331 -0
- package/src/client/PreviewSections.tsx +115 -34
- package/src/client/RecallPanel.tsx +418 -127
- package/src/client/TypeDefinitionDrawer.tsx +82 -16
- package/src/client/WikiCardReview.tsx +266 -0
- package/src/client/WikiFlowCard.tsx +57 -95
- package/src/client/WikiTypeEditor.tsx +6 -20
- package/src/client/arch/AgentArchitectureGraph.tsx +22 -135
- package/src/client/architecture.css +22 -27
- package/src/client/architecture.tsx +12 -12
- package/src/client/asset-images.ts +125 -0
- package/src/client/attribution/AttributionTab.tsx +4 -10
- package/src/client/attribution/StageChrome.tsx +8 -24
- package/src/client/convert/ConvertPanel.tsx +30 -3
- package/src/client/doc-page.tsx +6 -27
- package/src/client/index.ts +18 -0
- package/src/client/problem-domains.ts +1 -1
- package/src/client/styles.css +80 -11
- package/src/convert/host.ts +27 -2
- package/src/convert/ingest.ts +61 -18
- package/src/convert/types.ts +2 -2
- package/src/diagnosis/cards.ts +2 -2
- package/src/diagnosis/run.ts +8 -2
- package/src/diagnosis/seed-source.ts +1 -1
- package/src/diagnosis/seed.ts +1 -1
- package/src/diagnosis/store.ts +32 -79
- package/src/diagnosis/types.ts +2 -2
- package/src/extract-model.ts +254 -99
- package/src/extract.ts +35 -3
- package/src/fs-atomic.ts +3 -4
- package/src/index.ts +338 -88
- package/src/intent/types.ts +1 -1
- package/src/knowledge-bases.ts +31 -32
- package/src/layout.ts +4 -4
- package/src/migrate.ts +2 -2
- package/src/model-text.ts +126 -0
- package/src/present.ts +367 -0
- package/src/recall-rank.ts +57 -2
- package/src/runtime.ts +2282 -374
- package/src/settings-schema.ts +2 -0
- package/src/source-images.ts +326 -0
- package/src/types.ts +354 -47
- package/src/wiki-finalize.ts +310 -0
- package/src/wiki-types.ts +13 -7
- package/lib/types/proposals.d.ts +0 -166
- package/lib/types/proposals.d.ts.map +0 -1
- package/lib/types/proposals.js +0 -238
- package/lib/types/proposals.js.map +0 -1
- package/src/client/ProposalReview.tsx +0 -313
- package/src/proposals.ts +0 -337
|
@@ -156,7 +156,7 @@ function EvalTab({ visible, settings, onSelect }: { visible: boolean; settings?:
|
|
|
156
156
|
<p className={styles.cardDescription}>评测集接入前,这一栏只声明要评什么;左边的单问题检查已经是真实链路。</p>
|
|
157
157
|
<div className={styles.evalRows}>
|
|
158
158
|
<div><span>编译正确性</span><strong>待运行</strong></div>
|
|
159
|
-
<div><span
|
|
159
|
+
<div><span>Hybrid Recall@10(关键词 + 向量)</span><strong>待运行</strong></div>
|
|
160
160
|
<div><span>引用可追溯性</span><strong>待运行</strong></div>
|
|
161
161
|
<div><span>多模态 Markdown</span><strong>待运行</strong></div>
|
|
162
162
|
<div><span>增量编译复用率</span><strong>待运行</strong></div>
|
|
@@ -311,8 +311,9 @@ export function KnowledgePanel({ visible, onOpenTab, settings, remote }: Props):
|
|
|
311
311
|
折叠区里 —— 三块内容、两条边框,读起来是三个东西。现在画布是四条带
|
|
312
312
|
(A/B/C 三条链路 + 一条问题域带),点节点或问题域牌子都在**画布内**的
|
|
313
313
|
浮层里展开详情:不占侧栏、不缩字、可关闭。
|
|
314
|
-
|
|
315
|
-
|
|
314
|
+
0050:这一栏是**一块带发丝线的画布,整图居中、不可拖拽** ——
|
|
315
|
+
两个方向都适应容器,所以「图被拖到一边」这种状态不再存在;
|
|
316
|
+
1:1 档放不下时由容器自己出滚动条。
|
|
316
317
|
设置不在这里:面板上唯一的设置入口在顶部 tab 栏最右边的齿轮里。 */}
|
|
317
318
|
<AgentArchitectureGraph
|
|
318
319
|
stats={stats} version={version} selected={selected}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 知识工具在**对话流**里的卡片(0045)。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么要有它
|
|
5
|
+
*
|
|
6
|
+
* 0045 之前,`import_sources` / `recall_knowledge` 这类调用在对话里只有两样东西:工具名
|
|
7
|
+
* 和一坨 JSON。人看到的是「import_sources」加一屏花括号,于是「Agent 到底导了几份、
|
|
8
|
+
* 编出了几张卡、召回命中哪几条」只能自己去右边面板里找 —— 而右侧面板压根不知道
|
|
9
|
+
* 刚才那一次调用做了什么。
|
|
10
|
+
*
|
|
11
|
+
* 现在这一层注册的是平台的 `tool.call.toolview` 槽位,按**工具名**命中
|
|
12
|
+
* (`key: 'recall_knowledge'`),命中即**替换**默认的通用行。数据有两个来源:
|
|
13
|
+
*
|
|
14
|
+
* 1. **参数**(`block.argsRaw` / `phase === 'start'`):跑起来的那一刻就能显示
|
|
15
|
+
* 「导入 12 份来源并编译」,不必等结果;
|
|
16
|
+
* 2. **`block.meta`**:宿主侧 `presentationMeta` 产出的结构化账本(见 `src/present.ts`),
|
|
17
|
+
* 随会话日志落盘,所以**回放时与当时看到的一模一样**。
|
|
18
|
+
*
|
|
19
|
+
* ## 三条实现约束
|
|
20
|
+
*
|
|
21
|
+
* - 卡片长在**对话区**里,不是右侧面板里:排版密度、字号跟着对话走(13px / 24px 行高),
|
|
22
|
+
* 不套面板那套 11px 的小字。颜色仍然读同一份共享令牌(`--tita-*` 挂在 `:root, body`
|
|
23
|
+
* 上),所以深浅色主题自动一致。
|
|
24
|
+
* - **不猜**。meta 缺字段就少显示一行,不编造 0 或「未知」。宿主没给 meta(旧会话、或
|
|
25
|
+
* 调用在复合传输里跑的)时退回默认通用行 —— 由 `undefined` 返回触发。
|
|
26
|
+
* - 参数解析失败**不抛**:一条工具记录因为渲染报错整块消失,比显示得不好看严重得多。
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { ReactNode } from 'react'
|
|
30
|
+
import {
|
|
31
|
+
ChevronRight, Database, FileText, Layers3, RefreshCw, Search, TriangleAlert, X,
|
|
32
|
+
} from '@tita-data-agent/ui/icons'
|
|
33
|
+
import { Badge, cn } from '@tita-data-agent/ui/components'
|
|
34
|
+
|
|
35
|
+
/** 本插件注册了专用卡片的工具名 —— 与 `src/index.ts` 里的注册一一对应。 */
|
|
36
|
+
export const KNOWLEDGE_TOOL_KEYS = ['import_sources', 'recall_knowledge', 'rebuild_knowledge', 'add_source', 'propose_wiki_cards'] as const
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 槽位 props 的**结构性子集**。
|
|
40
|
+
*
|
|
41
|
+
* 平台把 `tool.call.toolview` 的完整类型声明在 `@deepseek-ai/dsh-client-ui-tool/client`,
|
|
42
|
+
* 而那个包不在本插件的依赖里(客户端只 inject 了 sidebar-right / slots / renderer /
|
|
43
|
+
* settings 四个)。为一个**纯类型**去加一条 peer dependency,代价是发布面与安装面都变大,
|
|
44
|
+
* 而这里真正用到的只有五个字段。
|
|
45
|
+
*
|
|
46
|
+
* 所以:自己声明这五个字段 + 把槽位登记进 `SlotMap`(下面那段 `declare module`),
|
|
47
|
+
* 形状以平台那份声明为准(`ToolCallCommonProps` + 三个阶段 props)。
|
|
48
|
+
* **平台那份一旦改名,这里会编译不过** —— 那正是我们想要的失败方式。
|
|
49
|
+
*/
|
|
50
|
+
export interface KnowledgeToolViewProps {
|
|
51
|
+
/** 三个阶段二选一:`preparing` 连参数都没有。 */
|
|
52
|
+
phase: 'preparing' | 'start' | 'result'
|
|
53
|
+
/** 平台派发用的工具名(本卡片的键)。 */
|
|
54
|
+
toolName: string
|
|
55
|
+
callId: string
|
|
56
|
+
block: {
|
|
57
|
+
/** 只有 `start` 阶段有完整参数。 */
|
|
58
|
+
argsRaw?: string
|
|
59
|
+
/** 结果阶段:模型可见的内容块。 */
|
|
60
|
+
content?: readonly unknown[]
|
|
61
|
+
isError?: boolean
|
|
62
|
+
/** 宿主 `presentationMeta` 落下来的账本。 */
|
|
63
|
+
meta?: unknown
|
|
64
|
+
}
|
|
65
|
+
/** 平台提供的展开状态钩子:**每个调用各自持有**,切轮次会被重置。 */
|
|
66
|
+
useDisclosure: () => { readonly expanded: boolean; readonly setExpanded: (open: boolean) => void; readonly toggle: () => void }
|
|
67
|
+
/** 在轨迹视图里定位这次调用(可能没有)。 */
|
|
68
|
+
inspect?: () => void
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 把槽位登记进平台的 `SlotMap`。
|
|
73
|
+
*
|
|
74
|
+
* 平台自己的声明在 `dsh-client-ui-tool/client` 里;这里补一份**同样的形状**,因为那个包
|
|
75
|
+
* 不在本插件的类型可见范围内。运行时不受影响 —— 派发按名字字符串走,这段只在编译期
|
|
76
|
+
* 让 `slots.register({ name: 'tool.call.toolview' })` 通过检查。
|
|
77
|
+
*/
|
|
78
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
79
|
+
interface SlotMap {
|
|
80
|
+
'tool.call.toolview': { kind: 'keyed'; scope: 'session'; owner: KnowledgeToolViewProps }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** meta 里那本账的最小形状。所有字段都可缺:旧会话、失败信封都可能是残的。 */
|
|
85
|
+
interface ToolMeta {
|
|
86
|
+
kind?: string
|
|
87
|
+
// 导入
|
|
88
|
+
attempted?: number
|
|
89
|
+
written?: Array<{ sourceId?: string; title?: string; bytes?: number }>
|
|
90
|
+
failed?: Array<{ index?: number; title?: string; code?: string; message?: string }>
|
|
91
|
+
compile?: { cardsCreated?: number; cardsUpdated?: number; cardsReused?: number; notesReused?: number; notesRecompiled?: number; sourcesExtracted?: number; sourcesSkipped?: number; embedding?: string; version?: number } | null
|
|
92
|
+
// 召回
|
|
93
|
+
query?: string
|
|
94
|
+
hits?: Array<{ entityId?: string; entityType?: string; title?: string; score?: number; rerankScore?: number; reviewScore?: number }>
|
|
95
|
+
contextTopN?: number
|
|
96
|
+
backend?: string
|
|
97
|
+
routes?: string[]
|
|
98
|
+
rerankStatus?: string
|
|
99
|
+
insufficientEvidence?: boolean
|
|
100
|
+
// 编译
|
|
101
|
+
only?: string
|
|
102
|
+
version?: number
|
|
103
|
+
cardsCreated?: number
|
|
104
|
+
cardsUpdated?: number
|
|
105
|
+
cardsReused?: number
|
|
106
|
+
notesReused?: number
|
|
107
|
+
notesRecompiled?: number
|
|
108
|
+
sourcesExtracted?: number
|
|
109
|
+
sourcesSkipped?: number
|
|
110
|
+
embedding?: string
|
|
111
|
+
skipped?: boolean
|
|
112
|
+
// 写卡
|
|
113
|
+
ids?: string[]
|
|
114
|
+
// 单份来源
|
|
115
|
+
sourceId?: string
|
|
116
|
+
path?: string
|
|
117
|
+
title?: string
|
|
118
|
+
sourceType?: string
|
|
119
|
+
bytes?: number
|
|
120
|
+
rejected?: Array<{ title?: string; reason?: string; code?: string }>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 参数:`phase === 'preparing'` 时还没有,别去猜。 */
|
|
124
|
+
function argsOf(block: unknown): Record<string, any> {
|
|
125
|
+
const raw = (block as { argsRaw?: unknown } | undefined)?.argsRaw
|
|
126
|
+
if (typeof raw !== 'string' || raw.trim() === '') return {}
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(raw)
|
|
129
|
+
return parsed && typeof parsed === 'object' ? parsed as Record<string, any> : {}
|
|
130
|
+
} catch {
|
|
131
|
+
return {}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 模型可见的文本结果 —— 结果阶段之外用它兜底(也用于错误态)。 */
|
|
136
|
+
function textOf(block: unknown): string {
|
|
137
|
+
const content = (block as { content?: unknown } | undefined)?.content
|
|
138
|
+
if (!Array.isArray(content)) return ''
|
|
139
|
+
return content
|
|
140
|
+
.map(part => (part && typeof part === 'object' && (part as { type?: string }).type === 'text' ? String((part as { text?: unknown }).text ?? '') : ''))
|
|
141
|
+
.filter(Boolean)
|
|
142
|
+
.join('\n')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function short(text: string, max = 60): string {
|
|
146
|
+
const flat = text.replace(/\s+/g, ' ').trim()
|
|
147
|
+
return flat.length > max ? `${flat.slice(0, max)}…` : flat
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 标题与摘要:**先写这一下要动什么**,结果回来之后再补上产物。 */
|
|
151
|
+
function headOf(toolName: string, args: Record<string, any>, meta: ToolMeta | null): { icon: ReactNode; title: string; summary: string } {
|
|
152
|
+
const icon = (size = 14): ReactNode => {
|
|
153
|
+
switch (toolName) {
|
|
154
|
+
case 'recall_knowledge': return <Search size={size} />
|
|
155
|
+
case 'import_sources': return <Database size={size} />
|
|
156
|
+
case 'rebuild_knowledge': return <RefreshCw size={size} />
|
|
157
|
+
case 'propose_wiki_cards': return <Layers3 size={size} />
|
|
158
|
+
default: return <FileText size={size} />
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (toolName === 'recall_knowledge') {
|
|
162
|
+
const query = short(String(meta?.query ?? args?.query ?? ''), 36)
|
|
163
|
+
const hits = meta?.hits?.length
|
|
164
|
+
const summary = meta?.insufficientEvidence
|
|
165
|
+
? '没有内容越过相关性阈值'
|
|
166
|
+
: meta
|
|
167
|
+
? [hits !== undefined ? `命中 ${hits} 条` : '', meta.backend ? `${meta.backend}` : '', meta.contextTopN !== undefined ? `进上下文 ${meta.contextTopN} 条` : ''].filter(Boolean).join(' · ')
|
|
168
|
+
: '正在检索…'
|
|
169
|
+
return { icon: icon(), title: `召回「${query}」`, summary }
|
|
170
|
+
}
|
|
171
|
+
if (toolName === 'import_sources') {
|
|
172
|
+
const count = Array.isArray(args?.sources) ? args.sources.length : 0
|
|
173
|
+
const summary = meta
|
|
174
|
+
? [
|
|
175
|
+
`写入 ${meta.written?.length ?? 0} 份`,
|
|
176
|
+
meta.failed && meta.failed.length > 0 ? `失败 ${meta.failed.length} 份` : '',
|
|
177
|
+
meta.compile ? `新建 ${meta.compile.cardsCreated ?? 0} 张卡 · 并入 ${meta.compile.cardsUpdated ?? 0} 张` : '未编译',
|
|
178
|
+
].filter(Boolean).join(' · ')
|
|
179
|
+
: '正在写入并编译…'
|
|
180
|
+
return { icon: icon(), title: `导入 ${count} 份来源${args?.compile === false ? '' : '并编译'}`, summary }
|
|
181
|
+
}
|
|
182
|
+
if (toolName === 'rebuild_knowledge') {
|
|
183
|
+
const only = String(meta?.only ?? args?.only ?? 'all')
|
|
184
|
+
const what = only === 'all' ? '全链路' : only === 'wiki' ? 'wiki 与 wikiNotes' : only === 'embeddings' ? '向量索引' : '注册表'
|
|
185
|
+
const summary = meta
|
|
186
|
+
? meta.skipped
|
|
187
|
+
? '没有变化,全部复用'
|
|
188
|
+
: `新建 ${meta.cardsCreated ?? 0} · 并入 ${meta.cardsUpdated ?? 0} · 复用 ${meta.cardsReused ?? 0}${meta.version !== undefined ? ` · v${meta.version}` : ''}`
|
|
189
|
+
: '正在编译…'
|
|
190
|
+
return { icon: icon(), title: `编译知识库 · ${what}`, summary }
|
|
191
|
+
}
|
|
192
|
+
if (toolName === 'propose_wiki_cards') {
|
|
193
|
+
const count = Array.isArray(args?.proposals) ? args.proposals.length : 0
|
|
194
|
+
const summary = meta
|
|
195
|
+
? `入库 ${meta.ids?.length ?? 0} 张${meta.rejected && meta.rejected.length > 0 ? ` · 被拒 ${meta.rejected.length} 张` : ''}`
|
|
196
|
+
: '正在写入…'
|
|
197
|
+
return { icon: icon(), title: `写入 ${count} 张 wiki 卡片`, summary }
|
|
198
|
+
}
|
|
199
|
+
const title = args?.title ? short(String(args.title), 30) : '(未命名)'
|
|
200
|
+
const summary = meta
|
|
201
|
+
? `${meta.sourceType ?? 'document'} · ${meta.bytes ?? 0} 字符`
|
|
202
|
+
: '正在写入…'
|
|
203
|
+
return { icon: icon(), title: `写入来源「${title}」`, summary }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** 展开之后的正文:**这一屏要回答「刚才到底发生了什么」**。 */
|
|
207
|
+
function DetailsOf({ toolName, meta, fallback }: { toolName: string; meta: ToolMeta | null; fallback: string }): ReactNode {
|
|
208
|
+
if (!meta) {
|
|
209
|
+
return fallback
|
|
210
|
+
? <pre className="max-h-[240px] overflow-auto whitespace-pre-wrap break-words rounded-lg border border-tita-line bg-tita-surface-muted p-2 font-mono text-[11px] leading-relaxed text-tita-ink-muted">{fallback}</pre>
|
|
211
|
+
: null
|
|
212
|
+
}
|
|
213
|
+
if (meta.kind === 'recall') {
|
|
214
|
+
if (meta.insufficientEvidence) {
|
|
215
|
+
return <p className="text-[12px] leading-relaxed text-tita-ink-muted">库里没有一条越过相关性阈值 —— 这是结论,不是错误。把来源导进来再问一次。</p>
|
|
216
|
+
}
|
|
217
|
+
return <div className="flex flex-col gap-1">
|
|
218
|
+
{(meta.hits ?? []).map((hit, index) => {
|
|
219
|
+
const score = typeof hit.rerankScore === 'number' ? hit.rerankScore : hit.score
|
|
220
|
+
return <div key={`${hit.entityType}:${hit.entityId}:${index}`} className="flex items-baseline gap-2 text-[12px]">
|
|
221
|
+
<span className="w-4 shrink-0 text-right tabular-nums text-tita-ink-faint">{index + 1}</span>
|
|
222
|
+
<span className="min-w-0 flex-1 truncate text-tita-ink" title={hit.title}>{hit.title || hit.entityId}</span>
|
|
223
|
+
<code className="shrink-0 font-mono text-[10.5px] text-tita-ink-faint">{hit.entityType}:{hit.entityId}</code>
|
|
224
|
+
<span className="w-12 shrink-0 text-right tabular-nums text-tita-ink-muted">{typeof score === 'number' ? score.toFixed(3) : '—'}</span>
|
|
225
|
+
{typeof hit.reviewScore === 'number' && <span className="shrink-0 text-[10.5px] text-tita-ink-faint">评分 {hit.reviewScore.toFixed(2)}</span>}
|
|
226
|
+
</div>
|
|
227
|
+
})}
|
|
228
|
+
{/* 路由与重排状态:它们解释「为什么是这个顺序」,所以和命中列在同一块里。 */}
|
|
229
|
+
<p className="text-[11px] text-tita-ink-faint">
|
|
230
|
+
{[meta.backend ? `检索 ${meta.backend}` : '', meta.routes && meta.routes.length > 0 ? `路由 ${meta.routes.join('+')}` : '', meta.rerankStatus ? `重排 ${meta.rerankStatus}` : ''].filter(Boolean).join(' · ')}
|
|
231
|
+
</p>
|
|
232
|
+
</div>
|
|
233
|
+
}
|
|
234
|
+
if (meta.kind === 'import') {
|
|
235
|
+
return <div className="flex flex-col gap-1.5">
|
|
236
|
+
<ul className="flex flex-col gap-0.5">
|
|
237
|
+
{(meta.written ?? []).map(item => <li key={item.sourceId} className="flex items-baseline gap-2 text-[12px]">
|
|
238
|
+
<FileText size={11} className="mt-1 shrink-0 text-tita-ink-faint" />
|
|
239
|
+
<span className="min-w-0 flex-1 truncate text-tita-ink" title={item.title}>{item.title || item.sourceId}</span>
|
|
240
|
+
<span className="shrink-0 tabular-nums text-tita-ink-faint">{item.bytes ?? 0} 字符</span>
|
|
241
|
+
</li>)}
|
|
242
|
+
</ul>
|
|
243
|
+
{(meta.failed ?? []).length > 0 && <ul className="flex flex-col gap-0.5">
|
|
244
|
+
{(meta.failed ?? []).map(item => <li key={`${item.index}-${item.code}`} className="flex items-baseline gap-2 text-[12px] text-tita-danger">
|
|
245
|
+
<X size={11} className="mt-1 shrink-0" />
|
|
246
|
+
<span className="min-w-0 flex-1">第 {(item.index ?? 0) + 1} 份{item.title ? `「${short(item.title, 24)}」` : ''}:{item.message}</span>
|
|
247
|
+
<code className="shrink-0 font-mono text-[10.5px]">{item.code}</code>
|
|
248
|
+
</li>)}
|
|
249
|
+
</ul>}
|
|
250
|
+
{meta.compile
|
|
251
|
+
? <p className="text-[11.5px] text-tita-ink-muted">
|
|
252
|
+
编译:新建 {meta.compile.cardsCreated ?? 0} 张 · 并入 {meta.compile.cardsUpdated ?? 0} 张 · 复用 {meta.compile.cardsReused ?? 0} 张 · wikiNotes 复用 {meta.compile.notesReused ?? 0} / 重编 {meta.compile.notesRecompiled ?? 0}
|
|
253
|
+
{meta.compile.version !== undefined ? ` · 索引 v${meta.compile.version}` : ''}
|
|
254
|
+
</p>
|
|
255
|
+
: <p className="text-[11.5px] text-tita-ink-muted">这次没有编译(<code className="font-mono">compile: false</code>)—— 卡片还没生成,需要时再编译一次。</p>}
|
|
256
|
+
</div>
|
|
257
|
+
}
|
|
258
|
+
if (meta.kind === 'rebuild') {
|
|
259
|
+
return <p className="text-[11.5px] leading-relaxed text-tita-ink-muted">
|
|
260
|
+
{meta.skipped ? '知识库没有变化:产物逐字复用,没有生成新的索引版本。' : <>新建 {meta.cardsCreated ?? 0} 张 · 并入 {meta.cardsUpdated ?? 0} 张 · 复用 {meta.cardsReused ?? 0} 张 · 重抽来源 {meta.sourcesExtracted ?? 0} 份 / 跳过 {meta.sourcesSkipped ?? 0} 份</>}
|
|
261
|
+
{meta.embedding ? ` · 向量 ${meta.embedding}` : ''}{meta.version !== undefined ? ` · 索引 v${meta.version}` : ''}
|
|
262
|
+
</p>
|
|
263
|
+
}
|
|
264
|
+
if (meta.kind === 'writeCards') {
|
|
265
|
+
return <div className="flex flex-col gap-1">
|
|
266
|
+
<ul className="flex flex-col gap-0.5">
|
|
267
|
+
{(meta.ids ?? []).map(id => <li key={id} className="text-[12px]"><code className="font-mono text-[11px] text-tita-ink-muted">{id}</code></li>)}
|
|
268
|
+
</ul>
|
|
269
|
+
{(meta.rejected ?? []).length > 0 && <ul className="flex flex-col gap-0.5">
|
|
270
|
+
{(meta.rejected ?? []).map((item, index) => <li key={index} className="text-[12px] text-tita-danger">{item.title ? `「${short(item.title, 24)}」` : '一条'}被拒:{item.reason ?? item.code ?? '原因未知'}</li>)}
|
|
271
|
+
</ul>}
|
|
272
|
+
</div>
|
|
273
|
+
}
|
|
274
|
+
if (meta.kind === 'addSource') {
|
|
275
|
+
return <p className="text-[11.5px] leading-relaxed text-tita-ink-muted">
|
|
276
|
+
已写入 <code className="font-mono text-[11px]">{meta.sourceId}</code>({meta.sourceType},{meta.bytes ?? 0} 字符)。下一步:编译它,才会生成 wiki 卡片。
|
|
277
|
+
</p>
|
|
278
|
+
}
|
|
279
|
+
return fallback
|
|
280
|
+
? <pre className="max-h-[240px] overflow-auto whitespace-pre-wrap break-words rounded-lg border border-tita-line bg-tita-surface-muted p-2 font-mono text-[11px] leading-relaxed text-tita-ink-muted">{fallback}</pre>
|
|
281
|
+
: null
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 一条知识工具调用的卡片。
|
|
286
|
+
*
|
|
287
|
+
* 返回 `null` 会让槽位退回默认通用行 —— 这是**刻意的出口**:拿到不认识的形状时,
|
|
288
|
+
* 宁可显示平台默认的样子,也不要显示一张自己编的卡。
|
|
289
|
+
*/
|
|
290
|
+
export function KnowledgeToolRow(props: KnowledgeToolViewProps): ReactNode {
|
|
291
|
+
const { phase, toolName, block, useDisclosure } = props
|
|
292
|
+
const { expanded, toggle } = useDisclosure()
|
|
293
|
+
|
|
294
|
+
const args = phase === 'preparing' ? {} : argsOf(block)
|
|
295
|
+
const meta = phase === 'result' ? (((block as { meta?: unknown }).meta ?? null) as ToolMeta | null) : null
|
|
296
|
+
const isError = phase === 'result' && (block as { isError?: boolean }).isError === true
|
|
297
|
+
const head = headOf(toolName, args, meta)
|
|
298
|
+
const fallback = phase === 'result' ? textOf(block) : ''
|
|
299
|
+
const details = phase === 'result' || Object.keys(args).length > 0
|
|
300
|
+
const summary = isError ? '失败' : head.summary
|
|
301
|
+
|
|
302
|
+
return <div className="flex flex-col" data-tool={toolName} data-phase={phase}>
|
|
303
|
+
<button
|
|
304
|
+
type="button"
|
|
305
|
+
className={cn(
|
|
306
|
+
'group flex w-full min-w-0 items-center gap-1.5 py-0.5 text-left',
|
|
307
|
+
details ? 'cursor-pointer' : 'cursor-default',
|
|
308
|
+
)}
|
|
309
|
+
aria-expanded={details ? expanded : undefined}
|
|
310
|
+
onClick={() => { if (details) toggle() }}
|
|
311
|
+
>
|
|
312
|
+
<span className={cn('flex size-4 shrink-0 items-center justify-center', isError ? 'text-tita-danger' : 'text-tita-ink-faint')}>
|
|
313
|
+
{isError ? <TriangleAlert size={14} /> : head.icon}
|
|
314
|
+
</span>
|
|
315
|
+
<span className="shrink-0 text-[13px] text-tita-ink" title={head.title}>{head.title}</span>
|
|
316
|
+
<span className="size-[2px] shrink-0 rounded-full bg-tita-ink-faint" aria-hidden />
|
|
317
|
+
<span className={cn('min-w-0 flex-1 truncate text-[13px]', isError ? 'text-tita-danger' : 'text-tita-ink-faint')}>{summary}</span>
|
|
318
|
+
{meta?.kind === 'import' && (meta.failed?.length ?? 0) > 0 && <Badge tone="warning" className="shrink-0">失败 {meta.failed?.length}</Badge>}
|
|
319
|
+
{meta?.insufficientEvidence && <Badge tone="warning" className="shrink-0">无命中</Badge>}
|
|
320
|
+
{details && <ChevronRight size={12} className={cn('shrink-0 text-tita-ink-faint transition-transform', expanded && 'rotate-90')} />}
|
|
321
|
+
</button>
|
|
322
|
+
{expanded && details && <div className="mt-1 mb-1 ml-[22px]">
|
|
323
|
+
{isError
|
|
324
|
+
? <p className="flex items-start gap-1.5 text-[12px] leading-relaxed text-tita-danger">
|
|
325
|
+
<TriangleAlert size={12} className="mt-0.5 shrink-0" />
|
|
326
|
+
<span className="min-w-0 flex-1">{short(fallback || '调用失败', 400)}</span>
|
|
327
|
+
</p>
|
|
328
|
+
: <DetailsOf toolName={toolName} meta={meta} fallback={fallback} />}
|
|
329
|
+
</div>}
|
|
330
|
+
</div>
|
|
331
|
+
}
|
|
@@ -21,10 +21,12 @@
|
|
|
21
21
|
* 下面的分块位置不再随正文长度漂移。
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { useState, type ReactNode } from 'react'
|
|
25
|
-
import { ChevronDown, ChevronUp, FileCode2, FileText,
|
|
24
|
+
import { useMemo, useState, type ReactNode } from 'react'
|
|
25
|
+
import { ChevronDown, ChevronUp, FileCode2, FileText, GitBranch, History, ListTree } from '@tita-data-agent/ui/icons'
|
|
26
26
|
import { Button, Tabs, TabsContent, TabsList, TabsTrigger, cn } from '@tita-data-agent/ui/components'
|
|
27
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'
|
|
28
30
|
// 平台提供的 Markdown 原子(DSH 的模块表里有它,本插件的 client bundle 也已把它列为
|
|
29
31
|
// external)。原文与卡片正文本身就是 Markdown,用 `<pre>` 显示等于把标题、表格、链接
|
|
30
32
|
// 全都拍平成源码 —— 所以默认阅读态走它,源码视图是**另一个**选项,不是替代品。
|
|
@@ -63,27 +65,18 @@ export interface PreviewRow {
|
|
|
63
65
|
value: ReactNode
|
|
64
66
|
/** 等宽显示:hash / id / 路径这类要逐字比对的值。 */
|
|
65
67
|
code?: boolean
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* 抽屉里的小节头 —— 三种层级里的中间那层(标题 15px / 小节头 13px / 正文 12px)。
|
|
70
|
-
* 分隔线在 `.tita-drawerSection` 上(小节与上一块之间),所以这里只管内容。
|
|
71
|
-
*/
|
|
72
|
-
function SectionHead({ icon, title, children }: { icon: ReactNode; title: string; children?: ReactNode }): ReactNode {
|
|
73
|
-
return <div className={styles.drawerSubHead}>
|
|
74
|
-
<h4>{icon}{title}</h4>
|
|
75
|
-
{children}
|
|
76
|
-
</div>
|
|
68
|
+
/** 悬停说明。**必须渲染出来** —— 否则调用方写了它、界面上什么也没有。 */
|
|
69
|
+
title?: string
|
|
77
70
|
}
|
|
78
71
|
|
|
79
72
|
/** 元信息:一张两列的事实表。标签列宽固定,所以标签要短且彼此对齐。 */
|
|
80
73
|
export function PreviewMeta({ rows }: { rows: PreviewRow[] }): ReactNode {
|
|
81
74
|
return <section className={styles.drawerSection}>
|
|
82
|
-
<
|
|
75
|
+
<DrawerSectionHead icon={<ListTree size={13} />} title="元信息" />
|
|
83
76
|
<dl className={styles.kbKv}>
|
|
84
77
|
{rows.map(row => <div key={row.label}>
|
|
85
78
|
<dt>{row.label}</dt>
|
|
86
|
-
<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>
|
|
87
80
|
</div>)}
|
|
88
81
|
</dl>
|
|
89
82
|
</section>
|
|
@@ -92,21 +85,31 @@ export function PreviewMeta({ rows }: { rows: PreviewRow[] }): ReactNode {
|
|
|
92
85
|
export interface PreviewAsset { rel: string; url: string; bytes: number }
|
|
93
86
|
|
|
94
87
|
/**
|
|
95
|
-
* 内容:小节头 + 读法工具栏 +
|
|
88
|
+
* 内容:小节头 + 读法工具栏 + 正文(锁高内滚)+ 正文没提到的图。
|
|
96
89
|
*
|
|
97
90
|
* 两个视图是**同一份内容**的两种读法:Markdown 是阅读态(默认),文本是源码态,
|
|
98
91
|
* 用于逐字核对 —— 与版本 diff 是同一种读法。切换不重新取数,也不改内容。
|
|
99
|
-
*
|
|
92
|
+
*
|
|
93
|
+
* **图片与正文是一份内容,不是一个附件区**(0049)。此前末尾另起一块「图片素材」
|
|
94
|
+
* 网格:正文里的图已经就地渲染过一遍,末尾再排一遍,同一张图一屏里出现两次。
|
|
95
|
+
* 现在只有一份读法 —— 正文里提到过的图就地渲染,**没提到的排在正文末尾**,
|
|
96
|
+
* 都在同一个滚动容器里。独立整页也是这么读的(它靠 `resolveImage` 就地渲染)。
|
|
97
|
+
*
|
|
98
|
+
* **正文里的图要就地显示**(0047):`MarkdownText` 的 `pathImages` 拿 `assets` 把
|
|
99
|
+
* 相对引用换成 `/knowledge/assets/...`。不接它的话,浏览器会按当前页面地址去解析
|
|
100
|
+
* `_assets/<bucket>/img-x.png`(结果是 `/_assets/...` 404),于是「预览里看不到图」。
|
|
100
101
|
*
|
|
101
102
|
* `totalChars` 说全文有多少字符(服务端按 `SOURCE_PREVIEW_CHARS` 封顶,
|
|
102
|
-
* `content.length`
|
|
103
|
-
*
|
|
103
|
+
* `content.length` 已经不是全文长度了)。读全文的出口由调用方放在**小节头上**
|
|
104
|
+
* (`action`):它是「这份东西还能去哪看」,位置要固定,不该随正文是否被截断而出现或消失。
|
|
104
105
|
*/
|
|
105
|
-
export function PreviewContent({ content, assets = [], totalChars }: {
|
|
106
|
+
export function PreviewContent({ content, assets = [], totalChars, action }: {
|
|
106
107
|
content: string
|
|
107
108
|
assets?: PreviewAsset[]
|
|
108
109
|
/** 全文总字符数。省略时按 `content.length` 算(它本来就是全文)。 */
|
|
109
110
|
totalChars?: number
|
|
111
|
+
/** 小节头右侧的动作(例如「新页面打开」)。与「内容」同一行,不占正文宽度。 */
|
|
112
|
+
action?: ReactNode
|
|
110
113
|
}): ReactNode {
|
|
111
114
|
const [view, setView] = useState<'md' | 'text'>('md')
|
|
112
115
|
/** 卡片正文超过上限时的本地展开:卡片没有整页出口,只能在这里给出路。 */
|
|
@@ -115,10 +118,27 @@ export function PreviewContent({ content, assets = [], totalChars }: {
|
|
|
115
118
|
const total = totalChars ?? content.length
|
|
116
119
|
const clipped = shown.length < total
|
|
117
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])
|
|
118
131
|
return <section className={styles.drawerSection}>
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
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>
|
|
122
142
|
{/* 读法工具栏:这个分块的**两种读法**是它自己的事,不再挤进小节标题那一行。 */}
|
|
123
143
|
<Tabs value={view} onValueChange={next => setView(next as 'md' | 'text')}>
|
|
124
144
|
<div className={styles.drawerToolbar}>
|
|
@@ -133,21 +153,82 @@ export function PreviewContent({ content, assets = [], totalChars }: {
|
|
|
133
153
|
{expanded ? '收起' : `展开全部(还有 ${content.length - PREVIEW_CHARS} 字符)`}</Button>}
|
|
134
154
|
</div>
|
|
135
155
|
<TabsContent value="md">
|
|
136
|
-
<div className={styles.drawerBody}
|
|
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>
|
|
137
176
|
</TabsContent>
|
|
138
177
|
<TabsContent value="text">
|
|
139
178
|
<div className={styles.drawerBody}><pre className={styles.kbPre}>{shown}</pre></div>
|
|
140
179
|
</TabsContent>
|
|
141
180
|
</Tabs>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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>
|
|
151
232
|
</section>
|
|
152
233
|
}
|
|
153
234
|
|
|
@@ -163,7 +244,7 @@ export function PreviewContent({ content, assets = [], totalChars }: {
|
|
|
163
244
|
*/
|
|
164
245
|
export function PreviewVersions({ hint, ...manager }: { hint?: string } & VersionManagerProps): ReactNode {
|
|
165
246
|
return <section className={styles.drawerSection}>
|
|
166
|
-
<
|
|
247
|
+
<DrawerSectionHead icon={<History size={13} />} title="版本"><span className={styles.drawerHeadFact}>{manager.entries?.length ?? 0} 个</span></DrawerSectionHead>
|
|
167
248
|
{hint && <p className={styles.kbHint}>{hint}</p>}
|
|
168
249
|
<VersionManager {...manager} />
|
|
169
250
|
</section>
|