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,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 自定义 agent preset 的**纯数据与纯函数**(0029)。
|
|
3
|
+
*
|
|
4
|
+
* 与 `problem-domains.ts` 同一个理由:把「不依赖 React、可以在 node:test 里直接跑」
|
|
5
|
+
* 的那部分从 `.tsx` 里分出来。这里的两样东西都值得被测:preset 的 id 是一个会被
|
|
6
|
+
* 组合文件、安装脚本与界面同时引用的**冻结契约**;`readPersonaPrompt` 是一个真的
|
|
7
|
+
* 会读错的解析小函数。
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** 我们这个 preset 的 id。与 `preset/<id>/`、安装脚本、组合文件里的引用一致。 */
|
|
13
|
+
export const TITA_PRESET_ID = 'tita-data-agent'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 承载「默认 preset」的那条 DSH **entry**(0.1.5 时叫设置命名空间)曾经在这里:
|
|
17
|
+
* `agent-preset-registry`。0.1.7 起控制台**不再读写它**。
|
|
18
|
+
*
|
|
19
|
+
* 原因是它是重复的:DSH 设置里本来就有同一项,写的是同一个字段。控制台再摆一个下拉,
|
|
20
|
+
* 用户就要回答「这两处哪个算数」—— 答案是都算数,但光看界面看不出来。所以这一项归
|
|
21
|
+
* DSH 设置,`AgentPresetCard` 只在我们的 preset 不是默认时把这件事说出来。
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** 三个只在自定义 preset 里挂载的意图工具。子代理的 `toolFilter.deny` 也引用它们。 */
|
|
25
|
+
export const INTENT_TOOL_NAMES = ['intent_record', 'intent_store_view', 'intent_registry_list'] as const
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `agentPresets/list` 的一行。结构来自 `dsh-agent-preset-registry` 的 client-safe 类型。
|
|
29
|
+
*
|
|
30
|
+
* 没有 `trust`:0.1.5 的 roster 会区分随包 / 本地,0.1.7 的 `remoteExportList` 只回
|
|
31
|
+
* `id` / `name` / `description` / `order` / `broken` 与 `isDefault`。声明一个宿主不回的
|
|
32
|
+
* 字段,只会让界面拿一个永远 undefined 的值去分支 —— 那种分支看起来像功能,实际是死代码。
|
|
33
|
+
*/
|
|
34
|
+
export interface PresetRow {
|
|
35
|
+
id: string
|
|
36
|
+
isDefault: boolean
|
|
37
|
+
name?: string
|
|
38
|
+
description?: string
|
|
39
|
+
broken?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Remote 调用的返回。失败那一支带一句给用户看的话,不带就退回默认文案。 */
|
|
43
|
+
export type RemoteResult<T> = { ok: true; value: T } | { ok: false; message?: string }
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 客户端 Remote 命名空间的结构化视图。
|
|
47
|
+
*
|
|
48
|
+
* 与目录选择同理:`@deepseek-ai/dsh-agent-preset-registry` 是 DSH 安装里的包,不是可依赖的
|
|
49
|
+
* npm 版本,所以这里只声明真正用到的成员。取不到就是没有 —— 退化成「宿主没提供
|
|
50
|
+
* preset 名单」这一条如实回答,而不是崩掉。
|
|
51
|
+
*
|
|
52
|
+
* 0.1.7 起**没有** `settings.openAgentPresetDirectory`:DSH 那边的 preset 不再是一个
|
|
53
|
+
* 目录,而是组合里的一条声明行(见 docs/0036)。所以「打开 preset 目录」不走 settings
|
|
54
|
+
* 命名空间,而是走 `session.openWorkspacePath` —— 那是 DSH 里唯一一个「把宿主上的一个
|
|
55
|
+
* 路径交给系统文件管理器」的入口(文件预览的「在访达中显示」用的也是它)。它打开的是
|
|
56
|
+
* **本插件随包分发的 preset 真源目录**(`preset/tita-data-agent/`),因为提示词的唯一
|
|
57
|
+
* 真源是那里面的 `agent.cordis.yml`。
|
|
58
|
+
*/
|
|
59
|
+
export interface AgentPresetRemote {
|
|
60
|
+
agentPresets?: {
|
|
61
|
+
list(): Promise<RemoteResult<{ presets: PresetRow[] }>>
|
|
62
|
+
read(id: string): Promise<RemoteResult<{ content: string; name?: string; description?: string }>>
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* `dsh-api-session-controller` 的 `session` 命名空间,这里只声明路径打开那一个方法。
|
|
66
|
+
*
|
|
67
|
+
* 宿主会先验证这个路径确实映射到它自己的文件系统上(`verifyDesktopPath`),验证不过
|
|
68
|
+
* 就拒 —— 所以调用方必须把失败当成一种正常结果(如实说「打不开」并给出路径),
|
|
69
|
+
* 而不是当成崩了。
|
|
70
|
+
*/
|
|
71
|
+
session?: {
|
|
72
|
+
openWorkspacePath(
|
|
73
|
+
request: { path: string; action?: 'reveal' | 'open' },
|
|
74
|
+
signal?: AbortSignal,
|
|
75
|
+
): Promise<RemoteResult<{ opened: boolean }>>
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 从组合正文里抽出 `persona` 行的 `prefix`。
|
|
81
|
+
*
|
|
82
|
+
* **这是预览用的读取,不是一个 YAML 解析器。** 它只认 `persona` 行里那一个
|
|
83
|
+
* `prefix:` 标量,三种写法都支持:
|
|
84
|
+
*
|
|
85
|
+
* - `prefix: 一行文字`
|
|
86
|
+
* - `prefix: |` 块标量(保留换行 —— 提示词正文用的就是这种)
|
|
87
|
+
* - `prefix: >-` 折叠标量(换行折成空格)
|
|
88
|
+
*
|
|
89
|
+
* 只读这一段是因为界面上要显示的就是它;真正的解析与校验由 DSH 挂载 preset 时做,
|
|
90
|
+
* 而如果组合坏了,`agentPresets/list` 会把 `broken` 一起报回来,界面照实显示。
|
|
91
|
+
*
|
|
92
|
+
* 返回 `null` 表示「这个 preset 里没有 persona 提示词」—— 那本身是一个有意义的
|
|
93
|
+
* 结论(它用的是部署级默认人格),不是失败。
|
|
94
|
+
*/
|
|
95
|
+
export function readPersonaPrompt(content: string): string | null {
|
|
96
|
+
const lines = content.split('\n')
|
|
97
|
+
for (let i = 0; i < lines.length; i++) {
|
|
98
|
+
if (!/^\s*-\s+id:\s*persona\s*$/.test(lines[i]!)) continue
|
|
99
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
100
|
+
const line = lines[j]!
|
|
101
|
+
// 下一个顶层行 = persona 这一行结束了。
|
|
102
|
+
if (/^\s*-\s+id:/.test(line)) break
|
|
103
|
+
const match = /^(\s*)prefix:\s*((?:\||>)[-+]?)?\s*(.*)$/.exec(line)
|
|
104
|
+
if (!match) continue
|
|
105
|
+
const indent = match[1]!.length
|
|
106
|
+
// 单行标量:值就在这一行上。
|
|
107
|
+
if (!match[2]) return match[3]!.trim() || null
|
|
108
|
+
// 块标量:内容在下面,缩进比 `prefix:` 深,遇到不更深的行就结束。
|
|
109
|
+
const body: string[] = []
|
|
110
|
+
for (let k = j + 1; k < lines.length; k++) {
|
|
111
|
+
const inner = lines[k]!
|
|
112
|
+
// 空行属于块内(它就是要保留的段落间隔),不能当成结束。
|
|
113
|
+
if (inner.trim() === '') { body.push(''); continue }
|
|
114
|
+
if (inner.search(/\S/) <= indent) break
|
|
115
|
+
body.push(inner.slice(Math.min(inner.length, indent + 2)))
|
|
116
|
+
}
|
|
117
|
+
return body.join('\n').trim() || null
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent 架构图 —— 一张 AntV G6 画布画出整个方案。
|
|
3
|
+
*
|
|
4
|
+
* 六条纪律:
|
|
5
|
+
* 1. **画布只画,不算。** 坐标、配色、卡片 DOM 全在 `./model.ts` 里算好,这个组件
|
|
6
|
+
* 负责挂载、交互、全屏。所以「重排在 Runtime 下方」「跨链路只有四条」这些结论
|
|
7
|
+
* 可以在 node:test 里断言,不需要启浏览器。
|
|
8
|
+
* 2. **点击只改选中态,不重建画布。** 缩放和平移是用户的空间记忆,点一下节点就把视图
|
|
9
|
+
* 重置回 fitView 会把它清掉。选中反馈走 `updateNodeData`(只换那一张卡的 DOM)。
|
|
10
|
+
* 3. **窄栏默认「适应宽度」,1:1 是一个可选档。**
|
|
11
|
+
* 这张画布设计宽度 1172px,宿主右侧栏只有 380–840px。只给一个 1:1 档的话,
|
|
12
|
+
* 用户第一眼只能看到 5 列里的 2 列半 —— 「整个链路在一张图里」就只是一句话。
|
|
13
|
+
* 所以默认整图缩到栏宽(看得全),需要读字时切到 1:1(看得清)。
|
|
14
|
+
* 缩放**不用 G6 的 `fitView` / `zoomTo`**:这版 G6 对 HTML 节点算不出内容包围盒,
|
|
15
|
+
* 两个 API 都实测无效(画布按 0.45 定了尺寸,内容仍按 1:1 画出去被裁掉)。
|
|
16
|
+
* 改成画布永远按设计尺寸渲染、再缩整块板:比例是我们算的,不依赖 G6 的判断。
|
|
17
|
+
* 4. **画布不接管视口。** 不注册 `drag-canvas` / `zoom-canvas`:滚轮该滚容器,
|
|
18
|
+
* 而不是缩画布;平移交给滚动条。G6 在这一页只负责画和点选。
|
|
19
|
+
* 5. **操作是浮层,画布上没有图例。**
|
|
20
|
+
* 0026 把图例、提示、状态统计和全屏按钮放在画布上方的一条 flex 行里,栏宽一窄
|
|
21
|
+
* `flex-wrap` 就把全屏按钮甩到第二行最左边 —— 用户看到的「全屏不在右上角」就是这么来的。
|
|
22
|
+
* 0029 又把左上角那两行图例(边语义 + 全图状态分布)也撤了:读法已经写在带名、
|
|
23
|
+
* 卡片状态字和浮层正文里,再在画布上压一块半透明说明板,只是把图挡掉一角。
|
|
24
|
+
* 现在浮层只剩右上角的操作(缩放档 + 全屏)。
|
|
25
|
+
* 6. **细节也在画布内。** 节点详情是画布自己的浮层(`inspector` 插槽),不是旁边另一列:
|
|
26
|
+
* 0027 之前它是一块 320px 的侧栏,图因此被挤窄(实测缩放只有 0.42,字缩到读不出),
|
|
27
|
+
* 而且「图」与「这张图上的东西」分居两块表面。浮层不参与缩放,所以它始终是
|
|
28
|
+
* 可读的正文尺寸;关掉它,画布就恢复成完整的一张图。
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { Graph } from '@antv/g6'
|
|
32
|
+
import { useCallback, useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react'
|
|
33
|
+
import { Maximize2, Minimize2 } from '@tita-data-agent/ui/icons'
|
|
34
|
+
import { Button, cn } from '@tita-data-agent/ui/components'
|
|
35
|
+
import { NODE_META, NODE_STATUS_LABEL, nodeMetrics, type KnowledgeStats, type NodeId } from '../architecture.tsx'
|
|
36
|
+
import {
|
|
37
|
+
CANVAS_HEIGHT, CANVAS_WIDTH, cardHtml, isDecoration, isProblemPlate, problemIdOf, problemPlateHtml,
|
|
38
|
+
toGraphData,
|
|
39
|
+
} from './model.ts'
|
|
40
|
+
import { PROBLEM_DOMAINS } from '../problem-domains.ts'
|
|
41
|
+
import { useFitScale, type ZoomMode } from '../useFitScale.ts'
|
|
42
|
+
import { styles } from '../styles.ts'
|
|
43
|
+
// 架构视图自己的版式(图 + 浮层)跟着这个组件走,不并进插件通用 styles.css。
|
|
44
|
+
import '../architecture.css'
|
|
45
|
+
|
|
46
|
+
export interface AgentArchitectureGraphProps {
|
|
47
|
+
stats: KnowledgeStats
|
|
48
|
+
version: number
|
|
49
|
+
selected: NodeId | null
|
|
50
|
+
onSelect: (id: NodeId | null) => void
|
|
51
|
+
/** 选中的问题域(画布最下面那条带里的牌子)。与节点选中互斥,由调用方保证。 */
|
|
52
|
+
selectedDomain: string | null
|
|
53
|
+
onSelectDomain: (id: string | null) => void
|
|
54
|
+
/** 面板不可见时不建图:隐藏容器尺寸为 0,G6 拿到 0 会画出一块空白。 */
|
|
55
|
+
visible: boolean
|
|
56
|
+
/**
|
|
57
|
+
* 画布内的详情浮层。**它在画布自己的框里**,不是旁边另一列:选中节点或问题域时
|
|
58
|
+
* 出现,关掉就消失。它是画布内唯一不随缩放变小的东西,所以长文本放它这里。
|
|
59
|
+
*/
|
|
60
|
+
inspector?: ReactNode
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function AgentArchitectureGraph({ stats, version, selected, onSelect, selectedDomain, onSelectDomain, visible, inspector }: AgentArchitectureGraphProps): ReactNode {
|
|
64
|
+
const host = useRef<HTMLDivElement>(null)
|
|
65
|
+
const scroller = useRef<HTMLDivElement>(null)
|
|
66
|
+
const stage = useRef<HTMLElement>(null)
|
|
67
|
+
const graphRef = useRef<Graph | null>(null)
|
|
68
|
+
const selectRef = useRef(onSelect)
|
|
69
|
+
selectRef.current = onSelect
|
|
70
|
+
const selectDomainRef = useRef(onSelectDomain)
|
|
71
|
+
selectDomainRef.current = onSelectDomain
|
|
72
|
+
const [fullscreen, setFullscreen] = useState(false)
|
|
73
|
+
const [mode, setMode] = useState<ZoomMode>('fit')
|
|
74
|
+
const { scale, boardH } = useFitScale(scroller, CANVAS_WIDTH, CANVAS_HEIGHT, visible, fullscreen, mode)
|
|
75
|
+
/** 选中态要在「建图 / 换数据 / 换选中」三条路径上都对得上,所以集中存一份。 */
|
|
76
|
+
const stateRef = useRef({ selected, selectedDomain, stats, version })
|
|
77
|
+
stateRef.current = { selected, selectedDomain, stats, version }
|
|
78
|
+
/** 上一次选中的对象:取消高亮要精确到「哪一个」,不能整块重画。 */
|
|
79
|
+
const lastSelected = useRef<NodeId | null>(null)
|
|
80
|
+
const lastDomain = useRef<string | null>(null)
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------- 建图
|
|
83
|
+
// 只依赖 `visible`:画布尺寸永远是设计尺寸(CANVAS_WIDTH × CANVAS_HEIGHT),
|
|
84
|
+
// 缩放由外层那块板的 transform 承担,所以换档、换全屏都不必重建图。
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const el = host.current
|
|
87
|
+
if (!el || !visible) return undefined
|
|
88
|
+
const graph = new Graph({
|
|
89
|
+
container: el,
|
|
90
|
+
width: el.clientWidth || CANVAS_WIDTH,
|
|
91
|
+
height: el.clientHeight || CANVAS_HEIGHT,
|
|
92
|
+
// 画布底色交给容器(`.tita-archScroll` 的 `--tita-surface`):canvas 不认 CSS 变量,
|
|
93
|
+
// 它会先 clearRect(清成全透明)再把 background 当 fillStyle 填 —— 传令牌字符串等于
|
|
94
|
+
// 交给浏览器一个无效值,填色被忽略,默认的黑色 fillStyle 会直接盖上来。透明才是
|
|
95
|
+
//「跟着主题走」在画布上的唯一正确写法,浅色下与原来的纯白一致。
|
|
96
|
+
background: 'transparent',
|
|
97
|
+
// 画布不承担缩放平移:视口交给外层滚动条与缩放档,G6 只管画。
|
|
98
|
+
// 图级 `node.type` / `edge.type` **不要设**:G6 v5 用它覆盖每个元素自己声明的
|
|
99
|
+
// `type`,于是装饰节点会被当成 HTML 节点画成空盒子,整张图只剩悬空的卡片
|
|
100
|
+
// (0025 起就是这样,实测确认)。类型一律由元素自己声明。
|
|
101
|
+
})
|
|
102
|
+
graphRef.current = graph
|
|
103
|
+
graph.on('node:click', event => {
|
|
104
|
+
const id = (event as { target?: { id?: string } }).target?.id
|
|
105
|
+
// 带名条是装饰节点:点它等于点空白。
|
|
106
|
+
if (!id || isDecoration(id)) return
|
|
107
|
+
// 问题域牌子不是架构节点,它点开的是这个域的完整论述。
|
|
108
|
+
if (isProblemPlate(id)) selectDomainRef.current(problemIdOf(id))
|
|
109
|
+
else selectRef.current(id as NodeId)
|
|
110
|
+
})
|
|
111
|
+
graph.on('canvas:click', () => { selectRef.current(null); selectDomainRef.current(null) })
|
|
112
|
+
return () => {
|
|
113
|
+
graph.destroy()
|
|
114
|
+
graphRef.current = null
|
|
115
|
+
}
|
|
116
|
+
}, [visible])
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------- 喂数据
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
const graph = graphRef.current
|
|
121
|
+
if (!graph || !visible) return
|
|
122
|
+
const { selected: node, selectedDomain: domain } = stateRef.current
|
|
123
|
+
graph.setData(toGraphData(stats, version, node, domain))
|
|
124
|
+
void graph.render()
|
|
125
|
+
}, [visible, stats, version])
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------- 选中态
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const graph = graphRef.current
|
|
130
|
+
if (!graph || !visible) return
|
|
131
|
+
const { stats: currentStats, version: currentVersion } = stateRef.current
|
|
132
|
+
const previousNode = lastSelected.current
|
|
133
|
+
const previousDomain = lastDomain.current
|
|
134
|
+
lastSelected.current = selected
|
|
135
|
+
lastDomain.current = selectedDomain
|
|
136
|
+
const patch: Array<{ id: string; style: Record<string, unknown> }> = []
|
|
137
|
+
if (previousNode && previousNode !== selected) {
|
|
138
|
+
patch.push({ id: previousNode, style: { innerHTML: cardHtml(previousNode, nodeMetrics(previousNode, currentStats, currentVersion), false) } })
|
|
139
|
+
}
|
|
140
|
+
if (selected) {
|
|
141
|
+
patch.push({ id: selected, style: { innerHTML: cardHtml(selected, nodeMetrics(selected, currentStats, currentVersion), true) } })
|
|
142
|
+
}
|
|
143
|
+
// 问题域牌子与节点卡片是两种 DOM,但选中反馈是同一条规则:换 innerHTML。
|
|
144
|
+
const plate = (domainId: string, on: boolean): { id: string; style: Record<string, unknown> } | null => {
|
|
145
|
+
const domain = PROBLEM_DOMAINS.find(item => item.id === domainId)
|
|
146
|
+
if (!domain) return null
|
|
147
|
+
return { id: `domain:${domain.id}`, style: { innerHTML: problemPlateHtml(domain, currentStats, currentVersion, on) } }
|
|
148
|
+
}
|
|
149
|
+
if (previousDomain && previousDomain !== selectedDomain) {
|
|
150
|
+
const off = plate(previousDomain, false)
|
|
151
|
+
if (off) patch.push(off)
|
|
152
|
+
}
|
|
153
|
+
if (selectedDomain) {
|
|
154
|
+
const on = plate(selectedDomain, true)
|
|
155
|
+
if (on) patch.push(on)
|
|
156
|
+
}
|
|
157
|
+
if (patch.length === 0) return
|
|
158
|
+
graph.updateNodeData(patch)
|
|
159
|
+
void graph.draw()
|
|
160
|
+
}, [selected, selectedDomain, visible])
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------- 全屏
|
|
163
|
+
const toggleFullscreen = useCallback((): void => {
|
|
164
|
+
const el = stage.current
|
|
165
|
+
if (!el) return
|
|
166
|
+
if (document.fullscreenElement === el) void document.exitFullscreen()
|
|
167
|
+
else void el.requestFullscreen()
|
|
168
|
+
}, [])
|
|
169
|
+
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
const onChange = (): void => setFullscreen(document.fullscreenElement === stage.current)
|
|
172
|
+
document.addEventListener('fullscreenchange', onChange)
|
|
173
|
+
return () => document.removeEventListener('fullscreenchange', onChange)
|
|
174
|
+
}, [])
|
|
175
|
+
|
|
176
|
+
// 画布永远是设计尺寸,缩放由这块板承担。`fit` 档下板高就是缩完的高度,
|
|
177
|
+
// 容器因此没有可滚的余量;`1:1` 档下板高是设计高度,横向(窄栏)或双向(全屏)滚动。
|
|
178
|
+
const scrollStyle = fullscreen ? undefined : { height: boardH }
|
|
179
|
+
const boardStyle = {
|
|
180
|
+
width: CANVAS_WIDTH,
|
|
181
|
+
height: CANVAS_HEIGHT,
|
|
182
|
+
transform: `scale(${scale})`,
|
|
183
|
+
transformOrigin: 'top left',
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return <section className={styles.archStage} ref={stage} aria-label="Tita Data Agent 整体架构">
|
|
187
|
+
<div className={styles.archPane}>
|
|
188
|
+
{/* 浮层只剩操作,固定在右上角。画布上不再压图例板:读法在带名、卡片状态字与
|
|
189
|
+
浮层正文里,左上角那块说明板只是把图挡掉一角。 */}
|
|
190
|
+
<div className={styles.archOverlay}>
|
|
191
|
+
<div className={styles.archOverlayActions}>
|
|
192
|
+
{/* 缩放档:两段的分段控件。两段都在,是因为一个来回翻转的按钮说不清「现在是哪一档」。 */}
|
|
193
|
+
<div className={styles.archZoom} role="group" aria-label="缩放">
|
|
194
|
+
<button type="button" className={cn(styles.archZoomOption, mode === 'fit' && styles.archZoomOn)}
|
|
195
|
+
aria-pressed={mode === 'fit'} onClick={() => setMode('fit')} title="整图缩到栏宽,一眼看全三条链路">适应</button>
|
|
196
|
+
<button type="button" className={cn(styles.archZoomOption, mode === 'actual' && styles.archZoomOn)}
|
|
197
|
+
aria-pressed={mode === 'actual'} onClick={() => setMode('actual')} title="按设计尺寸 1:1 显示,横向滚动查看">1:1</button>
|
|
198
|
+
</div>
|
|
199
|
+
<Button variant="outline" size="sm" onClick={toggleFullscreen} aria-pressed={fullscreen}>
|
|
200
|
+
{fullscreen ? <Minimize2 size={14} /> : <Maximize2 size={14} />}{fullscreen ? '退出全屏' : '全屏'}
|
|
201
|
+
</Button>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
<div className={styles.archScroll} style={scrollStyle} ref={scroller}>
|
|
205
|
+
<div className={styles.archBoard} style={boardStyle}>
|
|
206
|
+
<div className={styles.archCanvas} ref={host} />
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
{/* 详情浮层:画布内,不缩放,可关闭。关掉之后画布恢复成完整的一张图。 */}
|
|
210
|
+
{inspector && <div className={styles.archInspector} style={{ '--arch-board-h': `${boardH}px` } as CSSProperties}>{inspector}</div>}
|
|
211
|
+
</div>
|
|
212
|
+
{/* 图上每个节点的状态说明:卡片只有 10px 的状态文字,完整依据挂在这里,视觉与无障碍同源。 */}
|
|
213
|
+
<ul className={styles.srOnly} aria-label="架构节点状态">
|
|
214
|
+
{(Object.keys(NODE_META) as NodeId[]).map(id => <li key={id}>
|
|
215
|
+
{NODE_META[id].title}:{NODE_STATUS_LABEL[NODE_META[id].status]} —— {NODE_META[id].statusNote}
|
|
216
|
+
</li>)}
|
|
217
|
+
</ul>
|
|
218
|
+
</section>
|
|
219
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 架构图内的节点面板:**核心逻辑 + 数据统计 + 状态记录 + 跳转**。
|
|
3
|
+
*
|
|
4
|
+
* 0027 之前它住在画布右边的一个侧栏里(`.tita-archRail`),而侧栏是画布**外面**的
|
|
5
|
+
* 一列:图被挤窄、面板和图各占一块表面,读起来是两件事。现在它是画布内的浮层,
|
|
6
|
+
* 由 `AgentArchitectureGraph` 的 `inspector` 插槽承载,壳(边框、圆角、投影)由浮层给。
|
|
7
|
+
*
|
|
8
|
+
* 这里**没有设置项**,一个都没有。设置已经回到它们真正生效的地方(意图 Tab、
|
|
9
|
+
* 知识库的各个分栏),面板只回答三个问题:这个节点是干什么的、它现在的数字是多少、
|
|
10
|
+
* 它实现到哪一步,以及「要改它就去哪」。
|
|
11
|
+
*
|
|
12
|
+
* 空态已经撤掉:画布上不再有「未选中」这一态 —— 没选中时浮层根本不出现,
|
|
13
|
+
* 而原来空态里那份「整体是什么」的内容(全图状态分布、逐带明细)已经搬到画布上的
|
|
14
|
+
* 问题域带名条里,那里本来就是「这一页整体是什么」的落点。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ReactNode } from 'react'
|
|
18
|
+
import {
|
|
19
|
+
ArrowRight, Check, ChevronRight, CircleDot, CircleHelp, X,
|
|
20
|
+
} from '@tita-data-agent/ui/icons'
|
|
21
|
+
import { Badge, Button, cn } from '@tita-data-agent/ui/components'
|
|
22
|
+
import {
|
|
23
|
+
NODE_META, NODE_STATUS_LABEL, jumpTargetOf, laneOf, nodeMetrics,
|
|
24
|
+
type KnowledgeStats, type KnowledgeTab, type NodeId, type NodeStatus, type PanelTab,
|
|
25
|
+
} from '../architecture.tsx'
|
|
26
|
+
import { LANE_PALETTE, recordRows } from './model.ts'
|
|
27
|
+
import { styles } from '../styles.ts'
|
|
28
|
+
|
|
29
|
+
const STATUS_BADGE: Record<NodeStatus, 'success' | 'warning' | 'neutral' | 'blue'> = {
|
|
30
|
+
live: 'success', partial: 'warning', planned: 'neutral', host: 'blue',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const STEP_ICON = { done: Check, partial: CircleDot, todo: CircleHelp } as const
|
|
34
|
+
|
|
35
|
+
export interface NodeLogicPanelProps {
|
|
36
|
+
selected: NodeId
|
|
37
|
+
stats: KnowledgeStats
|
|
38
|
+
version: number
|
|
39
|
+
/** 跳到节点所属的功能 tab;`knowledgeTab` 存在时还要落到知识库里那一栏。 */
|
|
40
|
+
onJump: (tab: PanelTab, knowledgeTab?: KnowledgeTab) => void
|
|
41
|
+
onOpenTab?: (kind: 'data-sheet' | 'data-notebook') => void
|
|
42
|
+
onClose: () => void
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 浮层里的一段。节点面板与问题域面板共用它 —— 两种内容、一种骨架,
|
|
47
|
+
* 换选中对象时版面不会跳。
|
|
48
|
+
*/
|
|
49
|
+
export function PanelSection({ title, extra, children }: { title: string; extra?: ReactNode; children: ReactNode }): ReactNode {
|
|
50
|
+
return <section className={styles.archPanelSection}>
|
|
51
|
+
<header className={styles.archPanelSectionHead}>
|
|
52
|
+
<h4>{title}</h4>
|
|
53
|
+
{extra}
|
|
54
|
+
</header>
|
|
55
|
+
{children}
|
|
56
|
+
</section>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function NodeLogicPanel({ selected, stats, version, onJump, onOpenTab, onClose }: NodeLogicPanelProps): ReactNode {
|
|
60
|
+
const meta = NODE_META[selected]
|
|
61
|
+
const Icon = meta.icon
|
|
62
|
+
const lane = laneOf(selected)
|
|
63
|
+
const laneColor = LANE_PALETTE[meta.lane].accent
|
|
64
|
+
const metrics = nodeMetrics(selected, stats, version)
|
|
65
|
+
const records = recordRows(selected, stats, version)
|
|
66
|
+
const jump = jumpTargetOf(selected)
|
|
67
|
+
|
|
68
|
+
return <aside className={styles.archPanel} aria-label={`${meta.title} 节点详情`}>
|
|
69
|
+
<header className={styles.archPanelHead}>
|
|
70
|
+
<span className={styles.archPanelIcon}><Icon size={16} strokeWidth={1.85} /></span>
|
|
71
|
+
<div className={styles.archPanelHeadCopy}>
|
|
72
|
+
<div className={styles.archPanelTitleRow}>
|
|
73
|
+
<h3>{meta.title}</h3>
|
|
74
|
+
<Badge tone={STATUS_BADGE[meta.status]}>{NODE_STATUS_LABEL[meta.status]}</Badge>
|
|
75
|
+
</div>
|
|
76
|
+
<p>{meta.subtitle}</p>
|
|
77
|
+
{lane && <span className={styles.archPanelLane}>
|
|
78
|
+
<i style={{ background: laneColor }} aria-hidden="true" />{lane.index} · {lane.title}
|
|
79
|
+
</span>}
|
|
80
|
+
</div>
|
|
81
|
+
<Button variant="ghost" size="icon" aria-label="关闭节点详情" title="关闭" onClick={onClose}><X size={14} /></Button>
|
|
82
|
+
</header>
|
|
83
|
+
|
|
84
|
+
<div className={styles.archPanelBody}>
|
|
85
|
+
<PanelSection title="核心逻辑">
|
|
86
|
+
<p className={styles.archPanelText}>{meta.detail}</p>
|
|
87
|
+
<div className={styles.archPanelFlow}>
|
|
88
|
+
<span><small>输入</small>{meta.input}</span>
|
|
89
|
+
<ArrowRight size={13} aria-hidden="true" />
|
|
90
|
+
<span><small>输出</small>{meta.output}</span>
|
|
91
|
+
</div>
|
|
92
|
+
<ol className={styles.archPanelSteps}>
|
|
93
|
+
{meta.steps.map(step => {
|
|
94
|
+
const StepIcon = STEP_ICON[step.state ?? 'done']
|
|
95
|
+
return <li key={step.label} className={cn(styles.archPanelStep, step.state === 'todo' && styles.archPanelStepTodo)}>
|
|
96
|
+
<StepIcon size={13} strokeWidth={2} aria-hidden="true" />
|
|
97
|
+
<span><b>{step.label}</b><small>{step.detail}</small></span>
|
|
98
|
+
</li>
|
|
99
|
+
})}
|
|
100
|
+
</ol>
|
|
101
|
+
</PanelSection>
|
|
102
|
+
|
|
103
|
+
<PanelSection title="数据统计" extra={<span className={styles.archPanelEyebrow}>{metrics.length} 项</span>}>
|
|
104
|
+
{metrics.length === 0
|
|
105
|
+
? <p className={styles.archPanelHint}><CircleHelp size={13} />这个节点没有本地计数:它不是本插件的产物,或者还没有可统计的落盘数据。</p>
|
|
106
|
+
: <div className={styles.archPanelMetrics}>
|
|
107
|
+
{metrics.map(metric => <span key={metric.label} className={styles.archPanelMetric}>
|
|
108
|
+
<small>{metric.label}</small><b>{metric.value}</b>
|
|
109
|
+
</span>)}
|
|
110
|
+
</div>}
|
|
111
|
+
</PanelSection>
|
|
112
|
+
|
|
113
|
+
<PanelSection title="状态记录">
|
|
114
|
+
<p className={styles.archPanelNote}>{meta.statusNote}</p>
|
|
115
|
+
<dl className={styles.archPanelRecords}>
|
|
116
|
+
{records.map(([label, value]) => <div key={label}><dt>{label}</dt><dd>{value}</dd></div>)}
|
|
117
|
+
</dl>
|
|
118
|
+
</PanelSection>
|
|
119
|
+
|
|
120
|
+
<PanelSection title="跳转" extra={<span className={styles.archPanelEyebrow}>{jump ? '可跳转' : '无独立页面'}</span>}>
|
|
121
|
+
{jump
|
|
122
|
+
? <Button variant="default" size="sm" onClick={() => onJump(jump.tab, jump.knowledgeTab)}>
|
|
123
|
+
去配置<ArrowRight size={14} />
|
|
124
|
+
</Button>
|
|
125
|
+
: <p className={styles.archPanelHint}><CircleHelp size={13} />这个节点没有可配置页:{meta.status === 'host' ? '它由宿主提供' : meta.status === 'planned' ? '它还没实现,先在这里说明设计意图' : '它的行为由代码决定,没有可调参数'}。</p>}
|
|
126
|
+
{(selected === 'answer' || selected === 'data-sheet' || selected === 'data-notebook') && onOpenTab && <div className={styles.archPanelActions}>
|
|
127
|
+
<Button variant="outline" size="sm" onClick={() => onOpenTab('data-sheet')}><ChevronRight size={13} />打开 Table</Button>
|
|
128
|
+
<Button variant="outline" size="sm" onClick={() => onOpenTab('data-notebook')}><ChevronRight size={13} />打开 Notebook</Button>
|
|
129
|
+
</div>}
|
|
130
|
+
</PanelSection>
|
|
131
|
+
</div>
|
|
132
|
+
</aside>
|
|
133
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 画布内浮层里的**问题域详情**。
|
|
3
|
+
*
|
|
4
|
+
* 问题域的牌子画在画布最下面一条带里,只放得下一眼能读完的东西(这一带是什么、
|
|
5
|
+
* 走到哪一步、衡量口径)。业界对照那一段是整段引述,在「适应」档缩到 0.7 之后
|
|
6
|
+
* 根本读不了,所以它落在浮层里 —— 那里是画布内唯一不随缩放变小的 HTML。
|
|
7
|
+
*
|
|
8
|
+
* 骨架与节点面板共用 `PanelSection`:两种内容、一种形状,换选中对象时版面不跳。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ReactNode } from 'react'
|
|
12
|
+
import { ChevronRight, Compass, X } from '@tita-data-agent/ui/icons'
|
|
13
|
+
import { Badge, Button } from '@tita-data-agent/ui/components'
|
|
14
|
+
import { PanelSection } from './NodeLogicPanel.tsx'
|
|
15
|
+
import { PROBLEM_BASE_NOTE, problemDomainOf } from '../problem-domains.ts'
|
|
16
|
+
import type { KnowledgeStats, NodeId } from '../architecture.tsx'
|
|
17
|
+
import { styles } from '../styles.ts'
|
|
18
|
+
|
|
19
|
+
export interface ProblemDetailPanelProps {
|
|
20
|
+
domainId: string
|
|
21
|
+
stats: KnowledgeStats
|
|
22
|
+
version: number
|
|
23
|
+
onSelect: (id: NodeId) => void
|
|
24
|
+
onClose: () => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function ProblemDetailPanel({ domainId, stats, version, onSelect, onClose }: ProblemDetailPanelProps): ReactNode {
|
|
28
|
+
const domain = problemDomainOf(domainId)
|
|
29
|
+
// 找不到就不画:宁可浮层不出现,也不要显示一个 id 对不上的空壳。
|
|
30
|
+
if (!domain) return null
|
|
31
|
+
const Icon = domain.icon
|
|
32
|
+
const progress = domain.progress?.(stats, version) ?? []
|
|
33
|
+
|
|
34
|
+
return <aside className={styles.archPanel} aria-label={`${domain.title} 问题域详情`}>
|
|
35
|
+
<header className={styles.archPanelHead}>
|
|
36
|
+
<span className={styles.archPanelIcon}><Icon size={16} strokeWidth={1.85} /></span>
|
|
37
|
+
<div className={styles.archPanelHeadCopy}>
|
|
38
|
+
<div className={styles.archPanelTitleRow}>
|
|
39
|
+
<h3>{domain.title}</h3>
|
|
40
|
+
<Badge tone={domain.status.tone}>{domain.status.label}</Badge>
|
|
41
|
+
</div>
|
|
42
|
+
<p>{domain.question}</p>
|
|
43
|
+
<span className={styles.archPanelLane}>
|
|
44
|
+
<Compass size={11} aria-hidden="true" />问题域 {domain.index} · 画布最下面一条带
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
<Button variant="ghost" size="icon" aria-label="关闭问题域详情" title="关闭" onClick={onClose}><X size={14} /></Button>
|
|
48
|
+
</header>
|
|
49
|
+
|
|
50
|
+
<div className={styles.archPanelBody}>
|
|
51
|
+
<PanelSection title="业界怎么做">
|
|
52
|
+
<p className={styles.archPanelText}>{domain.industry}</p>
|
|
53
|
+
</PanelSection>
|
|
54
|
+
|
|
55
|
+
<PanelSection title="落在哪些节点上" extra={<span className={styles.archPanelEyebrow}>{domain.actions.filter(action => action.node).length} 个可跳转</span>}>
|
|
56
|
+
<div className={styles.archPanelChips}>
|
|
57
|
+
{domain.actions.map(action => action.node
|
|
58
|
+
? <button
|
|
59
|
+
key={action.label}
|
|
60
|
+
type="button"
|
|
61
|
+
className={styles.archPanelChip}
|
|
62
|
+
onClick={() => onSelect(action.node as NodeId)}
|
|
63
|
+
title={`打开「${action.label}」节点`}
|
|
64
|
+
>{action.label}<ChevronRight size={12} aria-hidden="true" /></button>
|
|
65
|
+
: <span key={action.label} className={styles.archPanelChipOff}>{action.label}<Badge tone="warning">未落地</Badge></span>)}
|
|
66
|
+
</div>
|
|
67
|
+
</PanelSection>
|
|
68
|
+
|
|
69
|
+
<PanelSection title="现在走到哪一步" extra={<span className={styles.archPanelEyebrow}>{progress.length > 0 ? `${progress.length} 项真实计数` : '没有可统计的落盘数据'}</span>}>
|
|
70
|
+
{progress.length === 0
|
|
71
|
+
? <p className={styles.archPanelHint}><Compass size={13} />这个域还没有可统计的落盘产物,所以这里不给数字 —— 一个编出来的进度比没有进度更糟。</p>
|
|
72
|
+
: <div className={styles.archPanelMetrics}>
|
|
73
|
+
{progress.map(item => <span key={item.label} className={styles.archPanelMetric}>
|
|
74
|
+
<small>{item.label}</small><b>{item.value}</b>
|
|
75
|
+
</span>)}
|
|
76
|
+
</div>}
|
|
77
|
+
</PanelSection>
|
|
78
|
+
|
|
79
|
+
<PanelSection title="衡量方式">
|
|
80
|
+
<p className={styles.archPanelNote}>{domain.measure}</p>
|
|
81
|
+
</PanelSection>
|
|
82
|
+
|
|
83
|
+
<PanelSection title="底座">
|
|
84
|
+
<p className={styles.archPanelText}>{PROBLEM_BASE_NOTE}</p>
|
|
85
|
+
</PanelSection>
|
|
86
|
+
</div>
|
|
87
|
+
</aside>
|
|
88
|
+
}
|