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,332 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GitBranch, History, Stethoscope, X,
|
|
3
|
+
} from '@tita-data-agent/ui/icons'
|
|
4
|
+
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
|
5
|
+
import { createPortal } from 'react-dom'
|
|
6
|
+
import { Button } from '@tita-data-agent/ui/components'
|
|
7
|
+
import { AttributionGraph } from './AttributionGraph.tsx'
|
|
8
|
+
import { CaseView } from './CaseView.tsx'
|
|
9
|
+
import { GraphStage } from './GraphStage.tsx'
|
|
10
|
+
import { RunView } from './RunView.tsx'
|
|
11
|
+
import { ViewSwitch, type ViewSwitchItem } from './ViewSwitch.tsx'
|
|
12
|
+
import {
|
|
13
|
+
FilterBar, LegendBody, LegendToggle, NodeDetailCard, StatsStrip, ToolPill, ToolRail,
|
|
14
|
+
} from './StageChrome.tsx'
|
|
15
|
+
import type { GraphLayout } from './graph-options.ts'
|
|
16
|
+
import {
|
|
17
|
+
candidatesOf, defaultFocusId, EMPTY_FILTER, filterGraph, gapHypothesisIds,
|
|
18
|
+
neighborhoodOf, summarize, symptomCountByProblemClass,
|
|
19
|
+
type AttributionGraphData, type AttributionNodeKind, type FailureMode, type ProblemClassId,
|
|
20
|
+
} from './model.ts'
|
|
21
|
+
import { SEED_CASES, SEED_GRAPH } from '../../diagnosis/seed.js'
|
|
22
|
+
|
|
23
|
+
type SubView = 'graph' | 'case' | 'run'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 智能归因面板。
|
|
27
|
+
*
|
|
28
|
+
* **整块是一个画框,不是一个表单。** 承载面板的右侧边栏实测只有 380px(宿主
|
|
29
|
+
* `dsh-client-ui-sidebar-right` 的 `width:380px`),所以改版把筛选、图例、统计
|
|
30
|
+
* 全部从「图谱的兄弟节点」改成「浮在画布上的四条边」:画布吃掉全部高度,控件
|
|
31
|
+
* 叠在它上面。视觉结果就是「打开之后就是一张大图」。
|
|
32
|
+
*
|
|
33
|
+
* 三条读法各自回答一个问题(图谱=结构、案例=路径、排查=剪枝),它们是同一画框里
|
|
34
|
+
* 的三层内容,由顶部的分段切换换页 —— 切换器本身也浮在同一个框里,所以**全屏时
|
|
35
|
+
* 它天然跟着一起上去**,不需要为全屏再抄一份控件。
|
|
36
|
+
*
|
|
37
|
+
* 另外两个开关各自回答一个不同的问题:
|
|
38
|
+
* - **布局**:`力导向` 回答「这张图长什么样」(浮动、可拖、靠聚簇表达关系);
|
|
39
|
+
* `分层` 回答「谁是上游」(现象→假设→检查 的方向被显式画出来)。
|
|
40
|
+
* - **筛选**:按类型 / 失败模式 / 文本收窄。它是**切面**而不是树的一层 ——
|
|
41
|
+
* 可以多选、可以为空、随时能改回看全图,不会像层级那样「选错就得往回退」。
|
|
42
|
+
*/
|
|
43
|
+
export function AttributionTab({ visible }: { visible: boolean }): ReactNode {
|
|
44
|
+
const [subView, setSubView] = useState<SubView>('graph')
|
|
45
|
+
const [layout, setLayout] = useState<GraphLayout>('force')
|
|
46
|
+
const [overview, setOverview] = useState(true)
|
|
47
|
+
const [focusId, setFocusId] = useState<string | undefined>(() => defaultFocusId(SEED_GRAPH))
|
|
48
|
+
const [activeCase, setActiveCase] = useState('CASE-001')
|
|
49
|
+
const [full, setFull] = useState(false)
|
|
50
|
+
const [railOpen, setRailOpen] = useState(true)
|
|
51
|
+
const [legendOpen, setLegendOpen] = useState(false)
|
|
52
|
+
const [data, setData] = useState<AttributionGraphData>(SEED_GRAPH)
|
|
53
|
+
/**
|
|
54
|
+
* 宿主返回的**来源状态**。
|
|
55
|
+
*
|
|
56
|
+
* 三种形态的可信度不同,必须分开显示:卡片是当前真相;`05_diagnosis` 旧格式意味着迁移
|
|
57
|
+
* 没能无损生效(有人手工加的边或孤立的资产节点会让它放弃迁移);`seed 数据` 是宿主没就绪
|
|
58
|
+
* 时的兜底示例。`problems` 一并显示 —— 写卡片失败、迁移分歧都属于「不能只进日志」的信息。
|
|
59
|
+
*/
|
|
60
|
+
const [remote, setRemote] = useState<{ source: string; problems: string[] } | undefined>(undefined)
|
|
61
|
+
const [resetToken, setResetToken] = useState(0)
|
|
62
|
+
const [query, setQuery] = useState('')
|
|
63
|
+
const [kinds, setKinds] = useState<ReadonlySet<AttributionNodeKind>>(new Set())
|
|
64
|
+
const [modes, setModes] = useState<ReadonlySet<FailureMode>>(new Set())
|
|
65
|
+
const [problems, setProblems] = useState<ReadonlySet<ProblemClassId>>(new Set())
|
|
66
|
+
const [includeNeighbors, setIncludeNeighbors] = useState(true)
|
|
67
|
+
const [symptomId, setSymptomId] = useState('S1')
|
|
68
|
+
|
|
69
|
+
// `/diagnosis/graph`:宿主已实现这条路由(05_diagnosis)。拿不到就静默回落到 seed,
|
|
70
|
+
// 并在界面上写明来源 ——「有没有真实数据」必须可见,不能靠猜。
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!visible) return
|
|
73
|
+
let cancelled = false
|
|
74
|
+
void (async () => {
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch('/diagnosis/graph')
|
|
77
|
+
if (!response.ok) return
|
|
78
|
+
const payload = await response.json() as {
|
|
79
|
+
data?: { graph?: AttributionGraphData; source?: string; migrated?: boolean; problems?: string[] }
|
|
80
|
+
} & AttributionGraphData
|
|
81
|
+
const graph = payload.data?.graph ?? payload
|
|
82
|
+
if (!cancelled && Array.isArray(graph.nodes) && graph.nodes.length > 0) {
|
|
83
|
+
setData(graph)
|
|
84
|
+
const source = payload.data?.source
|
|
85
|
+
const problems = payload.data?.problems ?? []
|
|
86
|
+
setRemote({
|
|
87
|
+
source: source === 'cards' ? '02_wiki/diagnosis'
|
|
88
|
+
: source === 'jsonl' ? '05_diagnosis(旧格式)'
|
|
89
|
+
: '05_diagnosis',
|
|
90
|
+
// 迁移成功也是信息:它说明这个知识根刚从旧格式换过来。
|
|
91
|
+
problems: payload.data?.migrated ? [...problems, '本次已把 05_diagnosis 的知识迁移为 wiki 卡片'] : problems,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
} catch { /* 宿主未就绪:保留 seed。 */ }
|
|
95
|
+
})()
|
|
96
|
+
return () => { cancelled = true }
|
|
97
|
+
}, [visible])
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 全屏期间锁住宿主页面的滚动。
|
|
101
|
+
*
|
|
102
|
+
* 只把浮层铺满视口是不够的:滚轮落在浮层的非画布区域上时,事件会冒泡给宿主,
|
|
103
|
+
* 背后的页面照样滚。锁 `body` 是这里唯一简单可靠的做法(宿主用的是文档级滚动),
|
|
104
|
+
* 退出时按原值还原而不是写死 `''`。
|
|
105
|
+
*/
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!full) return
|
|
108
|
+
const previous = document.body.style.overflow
|
|
109
|
+
document.body.style.overflow = 'hidden'
|
|
110
|
+
const onKeyDown = (event: KeyboardEvent): void => { if (event.key === 'Escape') setFull(false) }
|
|
111
|
+
window.addEventListener('keydown', onKeyDown)
|
|
112
|
+
return () => {
|
|
113
|
+
document.body.style.overflow = previous
|
|
114
|
+
window.removeEventListener('keydown', onKeyDown)
|
|
115
|
+
}
|
|
116
|
+
}, [full])
|
|
117
|
+
|
|
118
|
+
const filter = useMemo(
|
|
119
|
+
() => ({ ...EMPTY_FILTER, query, kinds, failureModes: modes, problemClasses: problems, includeNeighbors }),
|
|
120
|
+
[query, kinds, modes, problems, includeNeighbors],
|
|
121
|
+
)
|
|
122
|
+
const filtered = useMemo(() => filterGraph(data, filter), [data, filter])
|
|
123
|
+
const gapIds = useMemo(() => new Set(gapHypothesisIds(filtered)), [filtered])
|
|
124
|
+
const stats = useMemo(() => summarize(filtered), [filtered])
|
|
125
|
+
const candidateCount = useMemo(() => candidatesOf(data, symptomId).length, [data, symptomId])
|
|
126
|
+
const symptomCounts = useMemo(() => symptomCountByProblemClass(data.nodes), [data])
|
|
127
|
+
const symptoms = useMemo(() => data.nodes.filter(node => node.kind === 'symptom'), [data])
|
|
128
|
+
const graphData = useMemo(
|
|
129
|
+
() => (overview ? filtered : (focusId ? neighborhoodOf(filtered, focusId, 1) : filtered)),
|
|
130
|
+
[filtered, overview, focusId],
|
|
131
|
+
)
|
|
132
|
+
const focused = data.nodes.find(node => node.id === focusId)
|
|
133
|
+
const filtering = query.trim() !== '' || kinds.size > 0 || modes.size > 0 || problems.size > 0
|
|
134
|
+
|
|
135
|
+
const toggleKind = (kind: AttributionNodeKind): void => {
|
|
136
|
+
const next = new Set(kinds)
|
|
137
|
+
if (next.has(kind)) next.delete(kind); else next.add(kind)
|
|
138
|
+
setKinds(next)
|
|
139
|
+
}
|
|
140
|
+
const toggleMode = (mode: FailureMode): void => {
|
|
141
|
+
const next = new Set(modes)
|
|
142
|
+
if (next.has(mode)) next.delete(mode); else next.add(mode)
|
|
143
|
+
setModes(next)
|
|
144
|
+
}
|
|
145
|
+
const toggleProblem = (id: ProblemClassId): void => {
|
|
146
|
+
const next = new Set(problems)
|
|
147
|
+
if (next.has(id)) next.delete(id); else next.add(id)
|
|
148
|
+
setProblems(next)
|
|
149
|
+
}
|
|
150
|
+
const clearFilter = (): void => { setQuery(''); setKinds(new Set()); setModes(new Set()); setProblems(new Set()) }
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 进出全屏。
|
|
154
|
+
*
|
|
155
|
+
* 进全屏时顺手把图例打开:那个档位本来就该「相关选项都展示上去」,而且全屏里
|
|
156
|
+
* 右侧副栏宽一倍,放得下。退出时不动用户的选择 —— 收起图例是用户的决定,
|
|
157
|
+
* 不该被一次全屏往返改回去。
|
|
158
|
+
*/
|
|
159
|
+
const toggleFull = (): void => {
|
|
160
|
+
if (!full) setLegendOpen(true)
|
|
161
|
+
setFull(!full)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 页签上的计数是「这一页有多少东西可看」,而不是装饰 —— 空页和满页要能一眼分开。 */
|
|
165
|
+
const viewItems: ReadonlyArray<ViewSwitchItem<SubView>> = [
|
|
166
|
+
{
|
|
167
|
+
id: 'graph', label: '图谱', icon: GitBranch,
|
|
168
|
+
hint: '结构:谁可能是谁的原因。空区和僵尸边只在这张图上可见。',
|
|
169
|
+
meta: String(stats.typeNodes + stats.referenceNodes),
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: 'case', label: '案例', icon: History,
|
|
173
|
+
hint: '路径:一次真实排查怎么走的,包括走错的那一段。',
|
|
174
|
+
meta: String(SEED_CASES.length),
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: 'run', label: '排查', icon: Stethoscope,
|
|
178
|
+
hint: '剪枝:按优先分排出下一步该查什么,每个检查都有主操作。',
|
|
179
|
+
meta: String(candidateCount),
|
|
180
|
+
},
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
const layoutHint = layout === 'force'
|
|
184
|
+
? '力导向:节点可拖动,松手后图会继续摊开 —— 关系靠聚簇和距离表达,方向不保证。'
|
|
185
|
+
: '分层:现象 → 假设 → 检查 的方向被显式画出来,但看起来更像流程图。'
|
|
186
|
+
|
|
187
|
+
const stage = <GraphStage
|
|
188
|
+
mode={full ? 'fullscreen' : 'panel'}
|
|
189
|
+
// 画布不随子视图卸载:切走再切回来时,拖过的位置要还在。
|
|
190
|
+
canvas={<AttributionGraph
|
|
191
|
+
key={`${resetToken}-${layout}`}
|
|
192
|
+
data={graphData}
|
|
193
|
+
gapIds={gapIds}
|
|
194
|
+
rankdir="LR"
|
|
195
|
+
layout={layout}
|
|
196
|
+
mode={full ? 'fullscreen' : 'panel'}
|
|
197
|
+
selectedId={focusId}
|
|
198
|
+
onSelect={setFocusId}
|
|
199
|
+
/>}
|
|
200
|
+
topLeft={
|
|
201
|
+
// 画布左上角只剩「这张图怎么画」:布局 + 类型切面。页面切换已经搬到画框之外。
|
|
202
|
+
// 案例 / 排查页没有图可读,这两个开关也就没有对象 —— 不传,不留空壳。
|
|
203
|
+
subView !== 'graph' ? undefined : railOpen
|
|
204
|
+
? <ToolRail
|
|
205
|
+
layout={layout}
|
|
206
|
+
onLayout={setLayout}
|
|
207
|
+
onReset={() => setResetToken(token => token + 1)}
|
|
208
|
+
kinds={kinds}
|
|
209
|
+
onToggleKind={toggleKind}
|
|
210
|
+
problems={problems}
|
|
211
|
+
onToggleProblem={toggleProblem}
|
|
212
|
+
symptomCounts={symptomCounts}
|
|
213
|
+
hint={layoutHint + (!overview && focused ? ` 聚焦 ${focused.id} 的 1 跳邻域,点节点切换。` : '')}
|
|
214
|
+
onCollapse={() => setRailOpen(false)}
|
|
215
|
+
/>
|
|
216
|
+
: <ToolPill layout={layout} kinds={kinds} problems={problems} onClick={() => setRailOpen(true)} />
|
|
217
|
+
}
|
|
218
|
+
topRight={<LegendToggle
|
|
219
|
+
open={legendOpen}
|
|
220
|
+
onToggle={() => setLegendOpen(!legendOpen)}
|
|
221
|
+
full={full}
|
|
222
|
+
onToggleFull={toggleFull}
|
|
223
|
+
showLegend={subView === 'graph'}
|
|
224
|
+
/>}
|
|
225
|
+
{...(subView === 'case' ? { layer: <CaseView activeId={activeCase} onActiveChange={setActiveCase} /> } : {})}
|
|
226
|
+
{...(subView === 'run' ? {
|
|
227
|
+
layer: <RunView
|
|
228
|
+
data={data}
|
|
229
|
+
symptoms={symptoms}
|
|
230
|
+
symptomId={symptomId}
|
|
231
|
+
onSymptomChange={setSymptomId}
|
|
232
|
+
onSelectNode={id => { setFocusId(id); setSubView('graph') }}
|
|
233
|
+
/>,
|
|
234
|
+
} : {})}
|
|
235
|
+
{...(subView === 'graph' ? {
|
|
236
|
+
bottomCenter: <FilterBar
|
|
237
|
+
query={query}
|
|
238
|
+
onQuery={setQuery}
|
|
239
|
+
modes={modes}
|
|
240
|
+
onToggleMode={toggleMode}
|
|
241
|
+
includeNeighbors={includeNeighbors}
|
|
242
|
+
onIncludeNeighbors={setIncludeNeighbors}
|
|
243
|
+
filtering={filtering}
|
|
244
|
+
onClear={clearFilter}
|
|
245
|
+
hit={filtered.nodes.length}
|
|
246
|
+
total={data.nodes.length}
|
|
247
|
+
/>,
|
|
248
|
+
bottomRight: <StatsStrip
|
|
249
|
+
stats={stats}
|
|
250
|
+
source={remote?.source ?? 'seed 数据'}
|
|
251
|
+
problems={remote?.problems ?? []}
|
|
252
|
+
/>,
|
|
253
|
+
side: <>
|
|
254
|
+
{/* 「全景 / 聚焦邻域」是画布读法,不是筛选,所以它贴在图例下面而不是筛选条里。 */}
|
|
255
|
+
<div className="mb-1.5 flex justify-end">
|
|
256
|
+
<Button
|
|
257
|
+
size="sm"
|
|
258
|
+
className="h-[26px] min-h-[26px] px-2 text-[11px]"
|
|
259
|
+
variant={overview ? 'default' : 'outline'}
|
|
260
|
+
onClick={() => setOverview(!overview)}
|
|
261
|
+
>{overview ? '全景' : '聚焦 1 跳'}</Button>
|
|
262
|
+
</div>
|
|
263
|
+
<div className="flex flex-col gap-1.5">
|
|
264
|
+
{legendOpen && <LegendBody stats={stats} />}
|
|
265
|
+
{focused && <NodeDetailCard node={focused} />}
|
|
266
|
+
</div>
|
|
267
|
+
</>,
|
|
268
|
+
} : {})}
|
|
269
|
+
/>
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 页面切换器:**画框之外,页面左上角**。
|
|
273
|
+
*
|
|
274
|
+
* 它曾经浮在画布的左上角。搬到外面来的理由不是审美:**页面切换是页面级控件,
|
|
275
|
+
* 画布是它切换的内容之一**。放在画布里,等于把「换一页」和「改这张图怎么画」
|
|
276
|
+
* 混成同一层的浮层 —— 而 §7.1 的分区原则正好相反(改「怎么画」的贴画布,
|
|
277
|
+
* 换「看哪一页」的贴页面)。搬出来之后画布左上角只剩布局与类型两个开关。
|
|
278
|
+
*
|
|
279
|
+
* 说明文案不在切换器内部,而是同一行的右侧:一整行读下来是
|
|
280
|
+
* 「你在哪一页 → 这一页回答什么问题」,比把它折到切换器下面更省一行高度。
|
|
281
|
+
*/
|
|
282
|
+
const switcher = <ViewSwitch
|
|
283
|
+
items={viewItems}
|
|
284
|
+
value={subView}
|
|
285
|
+
onChange={setSubView}
|
|
286
|
+
ariaLabel="智能归因视图"
|
|
287
|
+
showHint={false}
|
|
288
|
+
className="shrink-0"
|
|
289
|
+
/>
|
|
290
|
+
const viewHint = viewItems.find(item => item.id === subView)?.hint ?? ''
|
|
291
|
+
|
|
292
|
+
if (full) {
|
|
293
|
+
return createPortal(
|
|
294
|
+
<div
|
|
295
|
+
role="dialog"
|
|
296
|
+
aria-modal="true"
|
|
297
|
+
aria-label="智能归因全屏视图"
|
|
298
|
+
className="fixed inset-0 z-[900] flex flex-col gap-2 overscroll-contain bg-tita-surface-muted p-3"
|
|
299
|
+
>
|
|
300
|
+
{/* 全屏只有一行页头:标题 + 页面切换 + 当前页说明 + 出口。全屏是「同一块内容
|
|
301
|
+
换成整个视口」,所以这一行就是面板里那两行(页头 + 切换行)压成一行。 */}
|
|
302
|
+
<header className="flex items-center gap-2">
|
|
303
|
+
<span
|
|
304
|
+
className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-lg border"
|
|
305
|
+
style={{ borderColor: 'var(--tita-tone-green-line)', background: 'var(--tita-tone-green-tint)', color: 'var(--tita-tone-green)' }}
|
|
306
|
+
>
|
|
307
|
+
<Stethoscope size={14} />
|
|
308
|
+
</span>
|
|
309
|
+
<h2 className="shrink-0 text-[14px] font-[750] tracking-[-0.02em]">智能归因</h2>
|
|
310
|
+
{switcher}
|
|
311
|
+
<p className="min-w-0 flex-1 truncate text-[11px] text-tita-ink-muted">{viewHint}</p>
|
|
312
|
+
<Button
|
|
313
|
+
size="sm"
|
|
314
|
+
variant="outline"
|
|
315
|
+
className="h-[26px] min-h-[26px] shrink-0 px-2 text-[11px]"
|
|
316
|
+
onClick={() => setFull(false)}
|
|
317
|
+
><X size={12} />退出全屏(Esc)</Button>
|
|
318
|
+
</header>
|
|
319
|
+
{stage}
|
|
320
|
+
</div>,
|
|
321
|
+
document.body,
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return <div className="flex min-h-0 flex-1 flex-col gap-1.5">
|
|
326
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
327
|
+
{switcher}
|
|
328
|
+
<p className="min-w-0 flex-1 truncate text-[10px] text-tita-ink-muted">{viewHint}</p>
|
|
329
|
+
</div>
|
|
330
|
+
{stage}
|
|
331
|
+
</div>
|
|
332
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { ChevronDown, CircleDot, FileText, TriangleAlert } from '@tita-data-agent/ui/icons'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { Badge, Button, Card, cn } from '@tita-data-agent/ui/components'
|
|
4
|
+
import { SEED_CASES } from '../../diagnosis/seed.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 案例视图:**wiki 定义 + 排查节点**。
|
|
8
|
+
*
|
|
9
|
+
* 这两段是案例的两个面,不是一个东西的两种画法:
|
|
10
|
+
* - wiki 段回答「当时到底发生了什么」(散文、可评审、可被向量召回);
|
|
11
|
+
* - 节点段回答「知识怎么走的」(有序、可判真假、含被否定的分支)。
|
|
12
|
+
*
|
|
13
|
+
* 所以这里不做「文档里嵌一张图」那种合成视图,而是把两段并排放在同一张卡里 ——
|
|
14
|
+
* 读者能同时看到叙述和结构,而两者的数据源仍然各自独立。
|
|
15
|
+
*
|
|
16
|
+
* 红色底边与绿色底没有共享的 Tailwind 工具类(工具类只在 packages/ui 构建时按用到的类
|
|
17
|
+
* 生成,而插件不能改 packages/ui),所以这两处走行内 `style`,取值仍是共享令牌。
|
|
18
|
+
*/
|
|
19
|
+
/** 被否定的一步 / 误导段共用的红色底与边(见文件头)。 */
|
|
20
|
+
const REFUTED_WASH = { borderColor: 'var(--ddk-danger-line)', background: 'var(--ddk-danger-tint)' }
|
|
21
|
+
|
|
22
|
+
export function CaseView({ activeId, onActiveChange }: {
|
|
23
|
+
activeId: string
|
|
24
|
+
onActiveChange: (id: string) => void
|
|
25
|
+
}): ReactNode {
|
|
26
|
+
const active = SEED_CASES.find(item => item.id === activeId) ?? SEED_CASES[0]!
|
|
27
|
+
|
|
28
|
+
return <div className="flex flex-col gap-3">
|
|
29
|
+
<div className="flex flex-wrap gap-1.5">
|
|
30
|
+
{SEED_CASES.map(item => <Button
|
|
31
|
+
key={item.id}
|
|
32
|
+
variant={item.id === active.id ? 'default' : 'outline'}
|
|
33
|
+
size="sm"
|
|
34
|
+
onClick={() => onActiveChange(item.id)}
|
|
35
|
+
>{item.id.replace('CASE-', '#')} {item.title}</Button>)}
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<Card className="gap-3 p-3">
|
|
39
|
+
<header className="flex items-start justify-between gap-2">
|
|
40
|
+
<div className="min-w-0">
|
|
41
|
+
<h3 className="text-[13px] font-semibold leading-snug">{active.title}</h3>
|
|
42
|
+
<p className="mt-0.5 text-[10.5px] text-tita-ink-muted">{active.id} · 耗时 {active.durationMinutes} min</p>
|
|
43
|
+
</div>
|
|
44
|
+
<Badge tone="success">已确认根因</Badge>
|
|
45
|
+
</header>
|
|
46
|
+
|
|
47
|
+
{/* ── wiki 段 ── */}
|
|
48
|
+
<section aria-label="wiki 定义">
|
|
49
|
+
<div className="mb-1.5 flex items-center gap-1.5 text-[10px] tracking-wide text-tita-ink-muted">
|
|
50
|
+
<FileText size={12} />WIKI 定义
|
|
51
|
+
</div>
|
|
52
|
+
<div className="flex flex-col gap-2">
|
|
53
|
+
{active.sections.map(section => <div key={section.heading}>
|
|
54
|
+
<div className="text-[11px] font-semibold">{section.heading}</div>
|
|
55
|
+
<p className="mt-0.5 text-[11px] leading-relaxed text-tita-ink-muted">{section.body}</p>
|
|
56
|
+
</div>)}
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
{/* ── 节点段 ── */}
|
|
61
|
+
<section aria-label="排查节点">
|
|
62
|
+
<div className="mb-1.5 flex items-center gap-1.5 text-[10px] tracking-wide text-tita-ink-muted">
|
|
63
|
+
<CircleDot size={12} />排查节点
|
|
64
|
+
</div>
|
|
65
|
+
<ol className="flex flex-col gap-1">
|
|
66
|
+
{active.path.map((step, index) => <li key={`${step.nodeId}-${index}`} className="flex items-start gap-2">
|
|
67
|
+
<span className={cn(
|
|
68
|
+
'mt-0.5 inline-flex h-[18px] min-w-[42px] shrink-0 items-center justify-center rounded-md border px-1 font-mono text-[10px]',
|
|
69
|
+
step.refuted ? 'text-tita-danger' : 'border-tita-line bg-tita-surface-muted text-tita-ink-muted',
|
|
70
|
+
)} style={step.refuted ? REFUTED_WASH : undefined}>{step.label}</span>
|
|
71
|
+
<span className="pt-0.5 text-[11px] leading-snug text-tita-ink-muted">{step.note}</span>
|
|
72
|
+
</li>)}
|
|
73
|
+
</ol>
|
|
74
|
+
</section>
|
|
75
|
+
|
|
76
|
+
{active.misleading && <div className="rounded-lg border p-2" style={REFUTED_WASH}>
|
|
77
|
+
<div className="flex items-center gap-1.5 text-[11px] font-semibold text-tita-danger">
|
|
78
|
+
<TriangleAlert size={12} />误导段 · {active.misleading.label} · 浪费 {active.misleading.wastedMinutes} min
|
|
79
|
+
</div>
|
|
80
|
+
<p className="mt-1 text-[11px] leading-relaxed text-tita-danger">{active.misleading.why}</p>
|
|
81
|
+
</div>}
|
|
82
|
+
|
|
83
|
+
<footer
|
|
84
|
+
className="rounded-lg p-2 text-[11px] leading-relaxed"
|
|
85
|
+
style={{ background: 'var(--tita-tone-green-tint)', color: 'var(--tita-tone-green)' }}
|
|
86
|
+
>
|
|
87
|
+
<b>根因</b>{active.rootCause}
|
|
88
|
+
</footer>
|
|
89
|
+
</Card>
|
|
90
|
+
|
|
91
|
+
<details className="group rounded-lg border border-tita-line bg-tita-surface p-2.5">
|
|
92
|
+
<summary className="flex cursor-pointer list-none items-center gap-1.5 text-[11.5px] font-medium">
|
|
93
|
+
<ChevronDown size={13} className="transition-transform group-open:rotate-180" />
|
|
94
|
+
为什么案例要分「wiki 段」和「节点段」两段
|
|
95
|
+
</summary>
|
|
96
|
+
<p className="mt-1.5 text-[10.5px] leading-relaxed text-tita-ink-muted">
|
|
97
|
+
wiki 段是给人读、给向量召回的散文;节点段是给归并与剪枝读的结构。把「序」和「枚举」写进散文,
|
|
98
|
+
归并算法就再也读不到它们了 —— 这也是案例不能只存成一篇文档的原因。
|
|
99
|
+
</p>
|
|
100
|
+
</details>
|
|
101
|
+
</div>
|
|
102
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { cn } from '@tita-data-agent/ui/components'
|
|
3
|
+
import type { StageMode } from './graph-options.ts'
|
|
4
|
+
|
|
5
|
+
export interface GraphStageProps {
|
|
6
|
+
/** 视口档位。只影响浮层的宽度预算,不影响画布(画布由父级尺寸决定)。 */
|
|
7
|
+
mode: StageMode
|
|
8
|
+
/** 画布层。它在两种排布下都铺满整个框。 */
|
|
9
|
+
canvas: ReactNode
|
|
10
|
+
/**
|
|
11
|
+
* 覆盖层(案例 / 排查视图)。
|
|
12
|
+
*
|
|
13
|
+
* 给了它,画布**不卸载**,只是被盖住。理由是位置记忆:力导向图被拖动过之后,
|
|
14
|
+
* 切到案例再切回来应该还是原来的形状;卸载重建会让每次切页都把图重新吹散。
|
|
15
|
+
*/
|
|
16
|
+
layer?: ReactNode
|
|
17
|
+
/** 左上:布局切换 + 类型切面。 */
|
|
18
|
+
topLeft?: ReactNode
|
|
19
|
+
/** 右上:图例 + 全屏。 */
|
|
20
|
+
topRight?: ReactNode
|
|
21
|
+
/** 底部中间:其余筛选。 */
|
|
22
|
+
bottomCenter?: ReactNode
|
|
23
|
+
/** 右下:统计。 */
|
|
24
|
+
bottomRight?: ReactNode
|
|
25
|
+
/** 右侧副栏,贴在顶部那一行之下(图例正文、选中节点详情)。 */
|
|
26
|
+
side?: ReactNode
|
|
27
|
+
className?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 图谱画框。
|
|
32
|
+
*
|
|
33
|
+
* **它存在的唯一理由是「图谱占满内容区」。** 改版前,筛选、统计、图例都是图谱的
|
|
34
|
+
* 兄弟节点,从上到下堆成一列 —— 在 380px 宽的右栏里,图谱被自己的控件挤到只剩
|
|
35
|
+
* 一半高度。改版后所有这些控件变成**浮在画布上的四条边**(左上 / 右上 / 底中 /
|
|
36
|
+
* 右下),画布本身吃掉整块面积。这是「打开就是一张大图」在结构上的实现方式:
|
|
37
|
+
* 不是把图放大,而是把其它东西从它的高度里拿走。
|
|
38
|
+
*
|
|
39
|
+
* 两套排布分开写,而不是用一套 `absolute` 加条件:
|
|
40
|
+
* - **画布模式**(无明显层):四条边浮在画布上,画布铺满,节点在浮层下面继续延展。
|
|
41
|
+
* - **内容模式**(案例 / 排查):内容要滚动,浮层浮在滚动内容上会遮住正文,
|
|
42
|
+
* 所以顶部控件改为正常流里的一行,内容占满其余高度。
|
|
43
|
+
*
|
|
44
|
+
* 浮层容器整体 `pointer-events-none`、只给面板本身打开命中 —— 否则四条边的
|
|
45
|
+
* 空档会把画布的拖拽和滚轮缩放吞掉。
|
|
46
|
+
*/
|
|
47
|
+
export function GraphStage({ mode, canvas, layer, topLeft, topRight, bottomCenter, bottomRight, side, className }: GraphStageProps): ReactNode {
|
|
48
|
+
const frame = cn(
|
|
49
|
+
'relative min-h-0 w-full flex-1 overflow-hidden rounded-xl border border-tita-line bg-tita-surface',
|
|
50
|
+
className,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if (layer) {
|
|
54
|
+
return <div className={frame}>
|
|
55
|
+
<div className="absolute inset-0">{canvas}</div>
|
|
56
|
+
<div className="relative flex h-full flex-col gap-1.5 bg-tita-surface">
|
|
57
|
+
{(topLeft || topRight) && <div className="flex items-start gap-1.5 px-2 pt-2">
|
|
58
|
+
{topLeft && <div className="min-w-0 max-w-full">{topLeft}</div>}
|
|
59
|
+
{topRight && <div className="ml-auto shrink-0">{topRight}</div>}
|
|
60
|
+
</div>}
|
|
61
|
+
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-2.5 pb-2.5">{layer}</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return <div className={frame}>
|
|
67
|
+
<div className="absolute inset-0">{canvas}</div>
|
|
68
|
+
<div className="pointer-events-none relative flex h-full flex-col gap-1.5 p-2">
|
|
69
|
+
<div className="flex items-start gap-1.5">
|
|
70
|
+
{topLeft && <div className="pointer-events-auto min-w-0 max-w-full">{topLeft}</div>}
|
|
71
|
+
{topRight && <div className="pointer-events-auto ml-auto shrink-0">{topRight}</div>}
|
|
72
|
+
</div>
|
|
73
|
+
{side && <div className={cn(
|
|
74
|
+
'pointer-events-auto ml-auto overflow-y-auto overscroll-contain',
|
|
75
|
+
// 副栏宽度按**档位**给:面板里 380px 的侧栏减去两条浮层只剩约 240px,
|
|
76
|
+
// 全屏才有余量把图例铺成两列。
|
|
77
|
+
mode === 'fullscreen' ? 'max-h-[62%] w-[min(360px,62%)]' : 'max-h-[52%] w-[min(238px,76%)]',
|
|
78
|
+
)}>
|
|
79
|
+
{side}
|
|
80
|
+
</div>}
|
|
81
|
+
<div className="mt-auto flex flex-col gap-1.5">
|
|
82
|
+
{bottomCenter && <div className="pointer-events-auto mx-auto w-full">{bottomCenter}</div>}
|
|
83
|
+
{bottomRight && <div className="pointer-events-auto ml-auto">{bottomRight}</div>}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
}
|