dsh-data-domain-knowledge 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +360 -0
- package/cordis.patch.yml +410 -0
- package/eval/README.md +76 -0
- package/eval/evaluate-retrieval.mjs +121 -0
- package/eval/queries.example.jsonl +2 -0
- package/lib/client.js +111415 -0
- package/lib/types/config-schema.d.ts +35 -0
- package/lib/types/config-schema.d.ts.map +1 -0
- package/lib/types/config-schema.js +34 -0
- package/lib/types/config-schema.js.map +1 -0
- package/lib/types/convert/core-loader.d.ts +32 -0
- package/lib/types/convert/core-loader.d.ts.map +1 -0
- package/lib/types/convert/core-loader.js +60 -0
- package/lib/types/convert/core-loader.js.map +1 -0
- package/lib/types/convert/host.d.ts +88 -0
- package/lib/types/convert/host.d.ts.map +1 -0
- package/lib/types/convert/host.js +479 -0
- package/lib/types/convert/host.js.map +1 -0
- package/lib/types/convert/ingest.d.ts +125 -0
- package/lib/types/convert/ingest.d.ts.map +1 -0
- package/lib/types/convert/ingest.js +228 -0
- package/lib/types/convert/ingest.js.map +1 -0
- package/lib/types/convert/settings-schema.d.ts +18 -0
- package/lib/types/convert/settings-schema.d.ts.map +1 -0
- package/lib/types/convert/settings-schema.js +26 -0
- package/lib/types/convert/settings-schema.js.map +1 -0
- package/lib/types/convert/types.d.ts +46 -0
- package/lib/types/convert/types.d.ts.map +1 -0
- package/lib/types/convert/types.js +41 -0
- package/lib/types/convert/types.js.map +1 -0
- package/lib/types/diagnosis/cards.d.ts +71 -0
- package/lib/types/diagnosis/cards.d.ts.map +1 -0
- package/lib/types/diagnosis/cards.js +482 -0
- package/lib/types/diagnosis/cards.js.map +1 -0
- package/lib/types/diagnosis/problem-classes.d.ts +51 -0
- package/lib/types/diagnosis/problem-classes.d.ts.map +1 -0
- package/lib/types/diagnosis/problem-classes.js +66 -0
- package/lib/types/diagnosis/problem-classes.js.map +1 -0
- package/lib/types/diagnosis/schema.d.ts +65 -0
- package/lib/types/diagnosis/schema.d.ts.map +1 -0
- package/lib/types/diagnosis/schema.js +208 -0
- package/lib/types/diagnosis/schema.js.map +1 -0
- package/lib/types/diagnosis/seed-source.d.ts +20 -0
- package/lib/types/diagnosis/seed-source.d.ts.map +1 -0
- package/lib/types/diagnosis/seed-source.js +96 -0
- package/lib/types/diagnosis/seed-source.js.map +1 -0
- package/lib/types/diagnosis/seed.d.ts +50 -0
- package/lib/types/diagnosis/seed.d.ts.map +1 -0
- package/lib/types/diagnosis/seed.js +242 -0
- package/lib/types/diagnosis/seed.js.map +1 -0
- package/lib/types/diagnosis/store.d.ts +81 -0
- package/lib/types/diagnosis/store.d.ts.map +1 -0
- package/lib/types/diagnosis/store.js +409 -0
- package/lib/types/diagnosis/store.js.map +1 -0
- package/lib/types/diagnosis/types.d.ts +124 -0
- package/lib/types/diagnosis/types.d.ts.map +1 -0
- package/lib/types/diagnosis/types.js +13 -0
- package/lib/types/diagnosis/types.js.map +1 -0
- package/lib/types/directory-picker.d.ts +94 -0
- package/lib/types/directory-picker.d.ts.map +1 -0
- package/lib/types/directory-picker.js +110 -0
- package/lib/types/directory-picker.js.map +1 -0
- package/lib/types/extract-model.d.ts +76 -0
- package/lib/types/extract-model.d.ts.map +1 -0
- package/lib/types/extract-model.js +193 -0
- package/lib/types/extract-model.js.map +1 -0
- package/lib/types/extract.d.ts +121 -0
- package/lib/types/extract.d.ts.map +1 -0
- package/lib/types/extract.js +549 -0
- package/lib/types/extract.js.map +1 -0
- package/lib/types/fts.d.ts +96 -0
- package/lib/types/fts.d.ts.map +1 -0
- package/lib/types/fts.js +184 -0
- package/lib/types/fts.js.map +1 -0
- package/lib/types/index.d.ts +39 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +1112 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/intent/engine.d.ts +108 -0
- package/lib/types/intent/engine.d.ts.map +1 -0
- package/lib/types/intent/engine.js +501 -0
- package/lib/types/intent/engine.js.map +1 -0
- package/lib/types/intent/host.d.ts +88 -0
- package/lib/types/intent/host.d.ts.map +1 -0
- package/lib/types/intent/host.js +389 -0
- package/lib/types/intent/host.js.map +1 -0
- package/lib/types/intent/preset-tools.d.ts +37 -0
- package/lib/types/intent/preset-tools.d.ts.map +1 -0
- package/lib/types/intent/preset-tools.js +45 -0
- package/lib/types/intent/preset-tools.js.map +1 -0
- package/lib/types/intent/settings-schema.d.ts +59 -0
- package/lib/types/intent/settings-schema.d.ts.map +1 -0
- package/lib/types/intent/settings-schema.js +57 -0
- package/lib/types/intent/settings-schema.js.map +1 -0
- package/lib/types/intent/store.d.ts +75 -0
- package/lib/types/intent/store.d.ts.map +1 -0
- package/lib/types/intent/store.js +139 -0
- package/lib/types/intent/store.js.map +1 -0
- package/lib/types/intent/tools.d.ts +63 -0
- package/lib/types/intent/tools.d.ts.map +1 -0
- package/lib/types/intent/tools.js +108 -0
- package/lib/types/intent/tools.js.map +1 -0
- package/lib/types/intent/types.d.ts +189 -0
- package/lib/types/intent/types.d.ts.map +1 -0
- package/lib/types/intent/types.js +62 -0
- package/lib/types/intent/types.js.map +1 -0
- package/lib/types/knowledge-bases.d.ts +266 -0
- package/lib/types/knowledge-bases.d.ts.map +1 -0
- package/lib/types/knowledge-bases.js +1107 -0
- package/lib/types/knowledge-bases.js.map +1 -0
- package/lib/types/layout.d.ts +262 -0
- package/lib/types/layout.d.ts.map +1 -0
- package/lib/types/layout.js +312 -0
- package/lib/types/layout.js.map +1 -0
- package/lib/types/migrate.d.ts +110 -0
- package/lib/types/migrate.d.ts.map +1 -0
- package/lib/types/migrate.js +280 -0
- package/lib/types/migrate.js.map +1 -0
- package/lib/types/recall-rank.d.ts +158 -0
- package/lib/types/recall-rank.d.ts.map +1 -0
- package/lib/types/recall-rank.js +261 -0
- package/lib/types/recall-rank.js.map +1 -0
- package/lib/types/runtime.d.ts +568 -0
- package/lib/types/runtime.d.ts.map +1 -0
- package/lib/types/runtime.js +3045 -0
- package/lib/types/runtime.js.map +1 -0
- package/lib/types/settings-schema.d.ts +117 -0
- package/lib/types/settings-schema.d.ts.map +1 -0
- package/lib/types/settings-schema.js +92 -0
- package/lib/types/settings-schema.js.map +1 -0
- package/lib/types/settings-volatile.d.ts +102 -0
- package/lib/types/settings-volatile.d.ts.map +1 -0
- package/lib/types/settings-volatile.js +114 -0
- package/lib/types/settings-volatile.js.map +1 -0
- package/lib/types/types.d.ts +799 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/types.js +387 -0
- package/lib/types/types.js.map +1 -0
- package/lib/types/wiki-types.d.ts +68 -0
- package/lib/types/wiki-types.d.ts.map +1 -0
- package/lib/types/wiki-types.js +322 -0
- package/lib/types/wiki-types.js.map +1 -0
- package/package.json +101 -0
- package/preset/tita-data-agent/agent.cordis.yml +376 -0
- package/preset/tita-data-agent/preset.yml +3 -0
- package/python/requirements.txt +8 -0
- package/scripts/doctor.mjs +70 -0
- package/scripts/e2e-pipeline.mjs +355 -0
- package/scripts/install-agent-preset.mjs +92 -0
- package/scripts/setup-embedding.sh +34 -0
- package/scripts/sync-agent-preset.mjs +188 -0
- package/src/client/AgentPresetCard.tsx +247 -0
- package/src/client/DirectoryPicker.tsx +357 -0
- package/src/client/InfoHint.tsx +65 -0
- package/src/client/IntentLogicTab.tsx +185 -0
- package/src/client/IntentTab.tsx +715 -0
- package/src/client/KnowledgeBaseTab.tsx +1454 -0
- package/src/client/KnowledgeGraphView.tsx +306 -0
- package/src/client/KnowledgePanel.tsx +381 -0
- package/src/client/KnowledgeStepRail.tsx +60 -0
- package/src/client/NodeSettings.tsx +392 -0
- package/src/client/PreviewSections.tsx +116 -0
- package/src/client/RecallPanel.tsx +399 -0
- package/src/client/TypeDefinitionDrawer.tsx +215 -0
- package/src/client/WikiFlowCard.tsx +226 -0
- package/src/client/WikiTypeEditor.tsx +332 -0
- package/src/client/agent-preset.ts +121 -0
- package/src/client/arch/AgentArchitectureGraph.tsx +219 -0
- package/src/client/arch/NodeLogicPanel.tsx +133 -0
- package/src/client/arch/ProblemDetailPanel.tsx +88 -0
- package/src/client/arch/model.ts +691 -0
- package/src/client/architecture.css +191 -0
- package/src/client/architecture.tsx +530 -0
- package/src/client/attribution/AttributionGraph.tsx +104 -0
- package/src/client/attribution/AttributionTab.tsx +332 -0
- package/src/client/attribution/CaseView.tsx +102 -0
- package/src/client/attribution/GraphStage.tsx +87 -0
- package/src/client/attribution/RunView.tsx +184 -0
- package/src/client/attribution/StageChrome.tsx +403 -0
- package/src/client/attribution/ViewSwitch.tsx +107 -0
- package/src/client/attribution/graph-options.ts +381 -0
- package/src/client/attribution/model.ts +462 -0
- package/src/client/config-scope.ts +119 -0
- package/src/client/convert/ConvertPanel.tsx +594 -0
- package/src/client/convert/styles.css +138 -0
- package/src/client/convert/styles.ts +49 -0
- package/src/client/format.ts +39 -0
- package/src/client/graph/layout.ts +181 -0
- package/src/client/graph.css +113 -0
- package/src/client/index.ts +98 -0
- package/src/client/problem-domains.ts +114 -0
- package/src/client/styles.css +940 -0
- package/src/client/styles.ts +11 -0
- package/src/client/useFitScale.ts +68 -0
- package/src/config-schema.ts +45 -0
- package/src/convert/core-loader.ts +68 -0
- package/src/convert/host.ts +579 -0
- package/src/convert/ingest.ts +367 -0
- package/src/convert/settings-schema.ts +27 -0
- package/src/convert/types.ts +65 -0
- package/src/diagnosis/cards.ts +498 -0
- package/src/diagnosis/problem-classes.ts +117 -0
- package/src/diagnosis/schema.ts +260 -0
- package/src/diagnosis/seed-source.ts +107 -0
- package/src/diagnosis/seed.ts +291 -0
- package/src/diagnosis/store.ts +432 -0
- package/src/diagnosis/types.ts +152 -0
- package/src/directory-picker.ts +164 -0
- package/src/extract-model.ts +227 -0
- package/src/extract.ts +632 -0
- package/src/fts.ts +203 -0
- package/src/index.ts +1091 -0
- package/src/intent/README.md +63 -0
- package/src/intent/engine.ts +527 -0
- package/src/intent/host.ts +462 -0
- package/src/intent/preset-tools.ts +48 -0
- package/src/intent/settings-schema.ts +63 -0
- package/src/intent/store.ts +143 -0
- package/src/intent/tools.ts +139 -0
- package/src/intent/types.ts +245 -0
- package/src/knowledge-bases.ts +1160 -0
- package/src/layout.ts +350 -0
- package/src/migrate.ts +262 -0
- package/src/python/embedding_worker.py +471 -0
- package/src/recall-rank.ts +299 -0
- package/src/runtime.ts +3069 -0
- package/src/settings-schema.ts +94 -0
- package/src/settings-volatile.ts +122 -0
- package/src/types.ts +1140 -0
- package/src/wiki-types.ts +357 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 知识图谱 —— 一张 AntV G6 画布画出当前知识库编译出来的图。
|
|
3
|
+
*
|
|
4
|
+
* 与「Agent 概览」那张架构图的分工:那张图回答**方案怎么搭**(三条链路、谁注入谁),
|
|
5
|
+
* 节点是固定的十七个,所以坐标是设计结论;这张图回答**这个库里到底连出了什么**
|
|
6
|
+
* (哪张卡片由哪几份原始文档支撑),节点随知识库增长,所以坐标交给 dagre。
|
|
7
|
+
*
|
|
8
|
+
* 三条纪律与架构图一致:
|
|
9
|
+
* 1. **画布只画,不算。** 筛选与坐标在 `./graph/layout.ts` 里,纯数据进出,可以断言。
|
|
10
|
+
* 2. **点击只改高亮,不重建画布。** 重建会把滚动位置和缩放档一起清掉。
|
|
11
|
+
* 3. **缩放不用 G6 的 `fitView`**:这版 G6 对 HTML 节点算不出内容包围盒(实测无效),
|
|
12
|
+
* 所以画布按设计尺寸渲染、再缩整块板 —— 见 `useFitScale`。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Graph } from '@antv/g6'
|
|
16
|
+
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
|
17
|
+
import { Maximize2, Minimize2, Search } from '@tita-data-agent/ui/icons'
|
|
18
|
+
import { Button, Input, cn } from '@tita-data-agent/ui/components'
|
|
19
|
+
import {
|
|
20
|
+
GRAPH_NODE_H, GRAPH_NODE_W, edgeTypeCounts, filterGraph, keyOf, layoutGraph, neighborsOf, nodeTypeCounts,
|
|
21
|
+
type GraphEdgeView, type GraphNodeView, type PlacedGraph, type PlacedNode,
|
|
22
|
+
} from './graph/layout.ts'
|
|
23
|
+
import { useFitScale, type ZoomMode } from './useFitScale.ts'
|
|
24
|
+
import { styles } from './styles.ts'
|
|
25
|
+
// 图谱视图自己的版式(筛选栏 + 画布 + 详情条)跟着这个组件走。
|
|
26
|
+
import './graph.css'
|
|
27
|
+
|
|
28
|
+
const FONT = 'var(--font-tita)'
|
|
29
|
+
/* 卡片是 G6 的 HTML 节点(innerHTML 进真实 DOM),所以这些值走共享令牌;画布**画**出来
|
|
30
|
+
* 的东西(边、画布底色)不行 —— canvas 渲染器把 `var()` 判为无效色,见下面的注释。 */
|
|
31
|
+
const INK = 'var(--tita-ink)'
|
|
32
|
+
const INK_MUTED = 'var(--tita-ink-muted)'
|
|
33
|
+
const RULE = 'var(--tita-line)'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 节点类型色。**这里是颜色承担信息的地方**,所以它必须与筛选条上的色点一一对应 ——
|
|
37
|
+
* 筛「metric」时用户找的就是那几个同色的小方块。
|
|
38
|
+
*
|
|
39
|
+
* 六档换到共享词表时收窄了:词表只有四个分类色(blue / violet / green / amber)加主色与
|
|
40
|
+
* danger,没有青色也没有品红,所以原来的青绿 `#0e7490` 取最近的绿、品红 `#be185d` 取
|
|
41
|
+
* danger —— 第 2 与第 5 档(tone-green / success)现在都是绿,靠标签文字区分。
|
|
42
|
+
* `source` 不参与轮转:它是溯源层,不是一类知识,所以它拿中性灰。
|
|
43
|
+
*/
|
|
44
|
+
const TYPE_COLORS = [
|
|
45
|
+
'var(--tita-primary)', 'var(--tita-tone-green)', 'var(--tita-tone-violet)',
|
|
46
|
+
'var(--tita-tone-amber)', 'var(--tita-success)', 'var(--tita-danger)',
|
|
47
|
+
]
|
|
48
|
+
export function typeColor(type: string, types: string[]): string {
|
|
49
|
+
if (type === 'source') return INK_MUTED
|
|
50
|
+
const index = types.filter(item => item !== 'source').indexOf(type)
|
|
51
|
+
return TYPE_COLORS[(index < 0 ? 0 : index) % TYPE_COLORS.length] as string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function escapeHtml(value: string): string {
|
|
55
|
+
return value.replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char] as string)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 一张节点卡片。宽度固定 176px,所以标题只能一行,靠 `title` 属性补全。
|
|
60
|
+
*
|
|
61
|
+
* `source` 节点的标题在接口里就是它的 docId(`question:e062c1bf…`),读不出是哪份文档 ——
|
|
62
|
+
* 所以调用方把「docId → 文档标题」的映射传进来,能对上就显示标题,对不上才退回 id。
|
|
63
|
+
*/
|
|
64
|
+
function nodeHtml(node: PlacedNode, color: string, selected: boolean, dim: boolean, label: string): string {
|
|
65
|
+
// 来源节点不挂「来源」两个字:那两个字要从标题里借 28px,而来源的文档标题本来就长
|
|
66
|
+
// (「CASE-001 日报 GMV 为 0」),借完就只剩省略号。它靠三件事认出来就够了 ——
|
|
67
|
+
// 中性灰的圆点、右边那一列的位置、以及箭头都指向它。
|
|
68
|
+
const source = node.type === 'source'
|
|
69
|
+
return `<div class="tita-kgNode${selected ? ' tita-kgNodeOn' : ''}" title="${escapeHtml(`${node.type} · ${label}`)}" style="box-sizing:border-box;width:100%;height:100%;display:flex;align-items:center;gap:6px;padding:0 9px;border-radius:6px;background:${source ? 'var(--tita-surface-muted)' : 'var(--tita-surface)'};border:1px solid ${selected ? color : RULE};box-shadow:${selected ? `0 0 0 3px color-mix(in srgb, ${color} 13%, transparent)` : '0 1px 2px rgba(16,24,40,.04)'};font-family:${FONT};overflow:hidden;opacity:${dim ? .3 : 1}">
|
|
70
|
+
<span style="width:6px;height:6px;border-radius:999px;background:${color};flex:0 0 auto"></span>
|
|
71
|
+
<strong style="min-width:0;flex:1 1 auto;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:11.5px;font-weight:${source ? 500 : 600};color:${source ? INK_MUTED : INK}">${escapeHtml(label)}</strong>
|
|
72
|
+
</div>`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* 画布侧的两个字面量颜色:canvas 不能读 CSS 变量(见建图那一处的注释),而图例色点
|
|
76
|
+
* 必须与画布上的边同色,所以它们共用这两个常量而不是各写一份。 */
|
|
77
|
+
const EDGE_GREY = '#7d8b9e'
|
|
78
|
+
const EDGE_SELECTED = '#175cd3'
|
|
79
|
+
/* 选中光晕是「类型色 13%」。原写法 `${color}22` 依赖 color 是十六进制串;换成令牌之后
|
|
80
|
+
* 那个后缀会拼出非法颜色(整条 box-shadow 会被丢掉),所以用 color-mix 表达同一个 13%。 */
|
|
81
|
+
|
|
82
|
+
/** 画布可视高度上限:知识图谱会长到几百上千像素,整块撑开会把页面拉得很长。 */
|
|
83
|
+
const GRAPH_VIEWPORT_H = 520
|
|
84
|
+
|
|
85
|
+
export interface KnowledgeGraphViewProps {
|
|
86
|
+
nodes: GraphNodeView[]
|
|
87
|
+
edges: GraphEdgeView[]
|
|
88
|
+
visible: boolean
|
|
89
|
+
loading?: boolean
|
|
90
|
+
/** SourceRaw 的 docId → 文档标题。图谱里的来源节点只有 id,得靠它才读得出来。 */
|
|
91
|
+
sourceTitles?: Map<string, string>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function KnowledgeGraphView({ nodes, edges, visible, loading = false, sourceTitles }: KnowledgeGraphViewProps): ReactNode {
|
|
95
|
+
const host = useRef<HTMLDivElement>(null)
|
|
96
|
+
const scroller = useRef<HTMLDivElement>(null)
|
|
97
|
+
const stage = useRef<HTMLElement>(null)
|
|
98
|
+
const graphRef = useRef<Graph | null>(null)
|
|
99
|
+
const [fullscreen, setFullscreen] = useState(false)
|
|
100
|
+
const [mode, setMode] = useState<ZoomMode>('fit')
|
|
101
|
+
const [nodeTypes, setNodeTypes] = useState<string[]>([])
|
|
102
|
+
const [edgeTypes, setEdgeTypes] = useState<string[]>([])
|
|
103
|
+
const [query, setQuery] = useState('')
|
|
104
|
+
const [selected, setSelected] = useState<string | null>(null)
|
|
105
|
+
const [placed, setPlaced] = useState<PlacedGraph | null>(null)
|
|
106
|
+
|
|
107
|
+
const allTypes = useMemo(() => nodeTypeCounts(nodes).map(item => item.type), [nodes])
|
|
108
|
+
const filtered = useMemo(
|
|
109
|
+
() => filterGraph(nodes, edges, { nodeTypes, edgeTypes, query }),
|
|
110
|
+
[nodes, edges, nodeTypes, edgeTypes, query],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
// 排一次版。筛选变了就重排 —— dagre 对同一份输入给出同一个形状,所以这是幂等的。
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (!visible) return undefined
|
|
116
|
+
let cancelled = false
|
|
117
|
+
void layoutGraph(filtered.nodes, filtered.edges).then(result => { if (!cancelled) setPlaced(result) })
|
|
118
|
+
return () => { cancelled = true }
|
|
119
|
+
}, [visible, filtered])
|
|
120
|
+
|
|
121
|
+
// 筛掉的节点如果正被选中,选中态要跟着失效,否则面板会显示一个图上已经没有的节点。
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (selected && !filtered.nodes.some(node => keyOf(node) === selected)) setSelected(null)
|
|
124
|
+
}, [filtered, selected])
|
|
125
|
+
|
|
126
|
+
const contentW = placed?.width ?? GRAPH_NODE_W * 2
|
|
127
|
+
const contentH = placed?.height ?? GRAPH_NODE_H * 2
|
|
128
|
+
const { scale, boardH } = useFitScale(scroller, contentW, contentH, visible && placed !== null, fullscreen, mode)
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------- 建图
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
const el = host.current
|
|
133
|
+
if (!el || !visible || !placed) return undefined
|
|
134
|
+
const graph = new Graph({
|
|
135
|
+
container: el,
|
|
136
|
+
width: el.clientWidth || contentW,
|
|
137
|
+
height: el.clientHeight || contentH,
|
|
138
|
+
// 画布底色交给容器(`.tita-kgScroll` 的 `--tita-surface`):G6 的 canvas 渲染器
|
|
139
|
+
// 不认 CSS 变量,写 `var(--tita-surface)` 会被解析成全透明黑(`parseColor` 的兜底),
|
|
140
|
+
// 反而画出黑底。透明则透出容器已主题化的底,浅色下与原来的纯白一致。
|
|
141
|
+
background: 'transparent',
|
|
142
|
+
// 图级 node/edge type 不要设:G6 v5 用它覆盖元素自己声明的 type。
|
|
143
|
+
})
|
|
144
|
+
graphRef.current = graph
|
|
145
|
+
graph.on('node:click', event => {
|
|
146
|
+
const id = (event as { target?: { id?: string } }).target?.id
|
|
147
|
+
if (id) setSelected(current => current === id ? null : id)
|
|
148
|
+
})
|
|
149
|
+
graph.on('canvas:click', () => setSelected(null))
|
|
150
|
+
return () => { graph.destroy(); graphRef.current = null }
|
|
151
|
+
}, [visible, placed, contentW, contentH])
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------- 喂数据
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
const graph = graphRef.current
|
|
156
|
+
if (!graph || !visible || !placed) return
|
|
157
|
+
const colors = new Map(placed.nodes.map(node => [node.key, typeColor(node.type, allTypes)]))
|
|
158
|
+
graph.setData({
|
|
159
|
+
nodes: placed.nodes.map(node => ({
|
|
160
|
+
id: node.key,
|
|
161
|
+
type: 'html',
|
|
162
|
+
style: {
|
|
163
|
+
x: node.x, y: node.y, size: [GRAPH_NODE_W, GRAPH_NODE_H],
|
|
164
|
+
dx: -GRAPH_NODE_W / 2, dy: -GRAPH_NODE_H / 2,
|
|
165
|
+
innerHTML: nodeHtml(node, colors.get(node.key) as string, false, false, labelOf(node)),
|
|
166
|
+
pointerEvents: 'auto',
|
|
167
|
+
},
|
|
168
|
+
})),
|
|
169
|
+
edges: placed.edges.map((edge, index) => ({
|
|
170
|
+
id: `e${index}`,
|
|
171
|
+
source: edge.from,
|
|
172
|
+
target: edge.to,
|
|
173
|
+
// 分层图里用横向 S 曲线:直线在多层之间会互相穿插,曲线能看出哪条从哪来。
|
|
174
|
+
type: 'cubic-horizontal',
|
|
175
|
+
data: { kind: edge.type },
|
|
176
|
+
// 画布侧字面量:canvas 不认 `var()`(见上面的 background 注释)。
|
|
177
|
+
style: { stroke: EDGE_GREY, lineWidth: 1.2, endArrow: true, endArrowType: 'triangle', endArrowSize: 7 },
|
|
178
|
+
})),
|
|
179
|
+
})
|
|
180
|
+
void graph.render()
|
|
181
|
+
}, [visible, placed, allTypes, sourceTitles])
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------- 高亮
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
const graph = graphRef.current
|
|
186
|
+
if (!graph || !visible || !placed) return
|
|
187
|
+
const colors = new Map(placed.nodes.map(node => [node.key, typeColor(node.type, allTypes)]))
|
|
188
|
+
// 选中一个节点时,它的邻居仍然全亮,其余降到 0.3 —— 「这张卡片接着谁」一眼可见。
|
|
189
|
+
const keep = new Set<string>()
|
|
190
|
+
if (selected) {
|
|
191
|
+
keep.add(selected)
|
|
192
|
+
for (const edge of placed.edges) {
|
|
193
|
+
if (edge.from === selected) keep.add(edge.to)
|
|
194
|
+
if (edge.to === selected) keep.add(edge.from)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
graph.updateNodeData(placed.nodes.map(node => ({
|
|
198
|
+
id: node.key,
|
|
199
|
+
style: { innerHTML: nodeHtml(node, colors.get(node.key) as string, node.key === selected, Boolean(selected) && !keep.has(node.key), labelOf(node)) },
|
|
200
|
+
})))
|
|
201
|
+
graph.updateEdgeData(placed.edges.map((edge, index) => ({
|
|
202
|
+
id: `e${index}`,
|
|
203
|
+
style: selected && (edge.from === selected || edge.to === selected)
|
|
204
|
+
? { stroke: EDGE_SELECTED, lineWidth: 1.8 }
|
|
205
|
+
: { stroke: EDGE_GREY, lineWidth: 1.2 },
|
|
206
|
+
})))
|
|
207
|
+
void graph.draw()
|
|
208
|
+
}, [selected, visible, placed, allTypes, sourceTitles])
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------- 全屏
|
|
211
|
+
const toggleFullscreen = (): void => {
|
|
212
|
+
const el = stage.current
|
|
213
|
+
if (!el) return
|
|
214
|
+
if (document.fullscreenElement === el) void document.exitFullscreen()
|
|
215
|
+
else void el.requestFullscreen()
|
|
216
|
+
}
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
const onChange = (): void => setFullscreen(document.fullscreenElement === stage.current)
|
|
219
|
+
document.addEventListener('fullscreenchange', onChange)
|
|
220
|
+
return () => document.removeEventListener('fullscreenchange', onChange)
|
|
221
|
+
}, [])
|
|
222
|
+
|
|
223
|
+
const typeCounts = useMemo(() => nodeTypeCounts(nodes), [nodes])
|
|
224
|
+
const edgeCounts = useMemo(() => edgeTypeCounts(edges), [edges])
|
|
225
|
+
const selectedNode = selected ? placed?.nodes.find(node => node.key === selected) ?? null : null
|
|
226
|
+
const neighbors = selected && placed ? neighborsOf(selected, placed.nodes, placed.edges) : []
|
|
227
|
+
/** 卡片上显示什么:来源节点优先用真实文档标题,其余用卡片标题。 */
|
|
228
|
+
const labelOf = (node: PlacedNode): string =>
|
|
229
|
+
node.type === 'source' ? sourceTitles?.get(node.id) ?? node.title : node.title
|
|
230
|
+
|
|
231
|
+
const toggle = (list: string[], value: string, set: (next: string[]) => void): void => {
|
|
232
|
+
set(list.includes(value) ? list.filter(item => item !== value) : [...list, value])
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return <section className={styles.kgStage} ref={stage} aria-label="知识图谱">
|
|
236
|
+
<header className={styles.kgBar}>
|
|
237
|
+
<div className={styles.kgFilters}>
|
|
238
|
+
<label className={styles.kgSearch}>
|
|
239
|
+
<Search size={13} aria-hidden="true" />
|
|
240
|
+
<Input value={query} placeholder="按标题或 id 搜索节点" onChange={event => setQuery(event.target.value)} />
|
|
241
|
+
</label>
|
|
242
|
+
<div className={styles.kgChips} role="group" aria-label="按节点类型筛选">
|
|
243
|
+
{typeCounts.map(item => <button
|
|
244
|
+
key={item.type} type="button"
|
|
245
|
+
className={cn(styles.kgChip, nodeTypes.includes(item.type) && styles.kgChipOn)}
|
|
246
|
+
aria-pressed={nodeTypes.includes(item.type)}
|
|
247
|
+
onClick={() => toggle(nodeTypes, item.type, setNodeTypes)}
|
|
248
|
+
>
|
|
249
|
+
<i style={{ background: typeColor(item.type, allTypes) }} aria-hidden="true" />
|
|
250
|
+
<code>{item.type}</code><b>{item.count}</b>
|
|
251
|
+
</button>)}
|
|
252
|
+
</div>
|
|
253
|
+
{edgeCounts.length > 1 && <div className={styles.kgChips} role="group" aria-label="按边类型筛选">
|
|
254
|
+
{edgeCounts.map(item => <button
|
|
255
|
+
key={item.type} type="button"
|
|
256
|
+
className={cn(styles.kgChip, edgeTypes.includes(item.type) && styles.kgChipOn)}
|
|
257
|
+
aria-pressed={edgeTypes.includes(item.type)}
|
|
258
|
+
onClick={() => toggle(edgeTypes, item.type, setEdgeTypes)}
|
|
259
|
+
>
|
|
260
|
+
{/* 色点与画布上的边同色,所以这里也用那个字面量(EDGE_GREY)。 */}
|
|
261
|
+
<i style={{ background: EDGE_GREY }} aria-hidden="true" />
|
|
262
|
+
<code>{item.type}</code><b>{item.count}</b>
|
|
263
|
+
</button>)}
|
|
264
|
+
</div>}
|
|
265
|
+
</div>
|
|
266
|
+
<div className={styles.kgActions}>
|
|
267
|
+
<span className={styles.kgCounts}>
|
|
268
|
+
<b>{filtered.nodes.length}</b>/{nodes.length} 节点
|
|
269
|
+
{/* 带进来的邻居要说出来,否则「筛了 8 个类型却显示 12 个节点」像是算错了。 */}
|
|
270
|
+
{filtered.nodes.length > filtered.matched && <em>含 {filtered.nodes.length - filtered.matched} 个邻居</em>}
|
|
271
|
+
<b>{filtered.edges.length}</b>/{edges.length} 边
|
|
272
|
+
</span>
|
|
273
|
+
<div className={styles.kgZoom} role="group" aria-label="缩放">
|
|
274
|
+
<button type="button" className={cn(styles.kgZoomOption, mode === 'fit' && styles.kgZoomOn)} aria-pressed={mode === 'fit'} onClick={() => setMode('fit')}>适应</button>
|
|
275
|
+
<button type="button" className={cn(styles.kgZoomOption, mode === 'actual' && styles.kgZoomOn)} aria-pressed={mode === 'actual'} onClick={() => setMode('actual')}>1:1</button>
|
|
276
|
+
</div>
|
|
277
|
+
<Button variant="outline" size="sm" onClick={toggleFullscreen} aria-pressed={fullscreen}>
|
|
278
|
+
{fullscreen ? <Minimize2 size={14} /> : <Maximize2 size={14} />}{fullscreen ? '退出全屏' : '全屏'}
|
|
279
|
+
</Button>
|
|
280
|
+
</div>
|
|
281
|
+
</header>
|
|
282
|
+
|
|
283
|
+
<div className={styles.kgScroll} style={fullscreen ? undefined : { height: Math.min(boardH, GRAPH_VIEWPORT_H) }} ref={scroller}>
|
|
284
|
+
<div className={styles.kgBoard} style={{ width: contentW, height: contentH, transform: `scale(${scale})`, transformOrigin: 'top left' }}>
|
|
285
|
+
<div className={styles.kgCanvas} ref={host} />
|
|
286
|
+
</div>
|
|
287
|
+
{placed && placed.nodes.length === 0 && <p className={styles.kgEmpty}>
|
|
288
|
+
{loading ? '正在读取图谱…' : nodes.length === 0 ? '这个知识库还没有图谱节点。先在上面的工具栏编译一次。' : '当前筛选下没有节点。清掉筛选条件看看。'}
|
|
289
|
+
</p>}
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
{/* 选中一个节点:它的类型、度数,以及直接相连的邻居(可点着走过去)。 */}
|
|
293
|
+
{selectedNode && <div className={styles.kgDetail}>
|
|
294
|
+
<span className={styles.kgDetailHead}>
|
|
295
|
+
<i style={{ background: typeColor(selectedNode.type, allTypes) }} aria-hidden="true" />
|
|
296
|
+
<code>{selectedNode.type}</code>
|
|
297
|
+
<strong>{labelOf(selectedNode)}</strong>
|
|
298
|
+
<em>{selectedNode.degree} 条边</em>
|
|
299
|
+
</span>
|
|
300
|
+
{neighbors.length > 0 && <span className={styles.kgNeighbors}>
|
|
301
|
+
相连:
|
|
302
|
+
{neighbors.map(node => <button key={keyOf(node)} type="button" onClick={() => setSelected(keyOf(node))}>{node.title}</button>)}
|
|
303
|
+
</span>}
|
|
304
|
+
</div>}
|
|
305
|
+
</section>
|
|
306
|
+
}
|