dsh-data-domain-knowledge 0.1.0-alpha.2 → 0.1.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -7
- package/cordis.patch.yml +11 -5
- package/lib/client.js +5557 -3056
- package/lib/doc-page.js +7740 -0
- package/lib/types/convert/host.d.ts.map +1 -1
- package/lib/types/convert/host.js +36 -9
- package/lib/types/convert/host.js.map +1 -1
- package/lib/types/diagnosis/cards.d.ts +8 -7
- package/lib/types/diagnosis/cards.d.ts.map +1 -1
- package/lib/types/diagnosis/cards.js +119 -49
- package/lib/types/diagnosis/cards.js.map +1 -1
- package/lib/types/diagnosis/run.d.ts +51 -0
- package/lib/types/diagnosis/run.d.ts.map +1 -0
- package/lib/types/diagnosis/run.js +239 -0
- package/lib/types/diagnosis/run.js.map +1 -0
- package/lib/types/diagnosis/schema.d.ts +25 -2
- package/lib/types/diagnosis/schema.d.ts.map +1 -1
- package/lib/types/diagnosis/schema.js +64 -25
- package/lib/types/diagnosis/schema.js.map +1 -1
- package/lib/types/diagnosis/seed-source.d.ts +17 -2
- package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
- package/lib/types/diagnosis/seed-source.js +18 -1
- package/lib/types/diagnosis/seed-source.js.map +1 -1
- package/lib/types/diagnosis/seed.d.ts +32 -42
- package/lib/types/diagnosis/seed.d.ts.map +1 -1
- package/lib/types/diagnosis/seed.js +187 -51
- package/lib/types/diagnosis/seed.js.map +1 -1
- package/lib/types/diagnosis/store.d.ts +114 -26
- package/lib/types/diagnosis/store.d.ts.map +1 -1
- package/lib/types/diagnosis/store.js +336 -167
- package/lib/types/diagnosis/store.js.map +1 -1
- package/lib/types/diagnosis/types.d.ts +168 -16
- package/lib/types/diagnosis/types.d.ts.map +1 -1
- package/lib/types/diagnosis/types.js +1 -1
- package/lib/types/diagnosis/types.js.map +1 -1
- package/lib/types/document-page.d.ts +39 -0
- package/lib/types/document-page.d.ts.map +1 -0
- package/lib/types/document-page.js +43 -0
- package/lib/types/document-page.js.map +1 -0
- package/lib/types/fs-atomic.d.ts +12 -0
- package/lib/types/fs-atomic.d.ts.map +1 -0
- package/lib/types/fs-atomic.js +42 -0
- package/lib/types/fs-atomic.js.map +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +214 -24
- package/lib/types/index.js.map +1 -1
- package/lib/types/knowledge-bases.d.ts +10 -2
- package/lib/types/knowledge-bases.d.ts.map +1 -1
- package/lib/types/knowledge-bases.js +20 -18
- package/lib/types/knowledge-bases.js.map +1 -1
- package/lib/types/proposals.d.ts +166 -0
- package/lib/types/proposals.d.ts.map +1 -0
- package/lib/types/proposals.js +238 -0
- package/lib/types/proposals.js.map +1 -0
- package/lib/types/runtime.d.ts +199 -7
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/runtime.js +728 -145
- package/lib/types/runtime.js.map +1 -1
- package/lib/types/settings-schema.d.ts +2 -2
- package/lib/types/types.d.ts +61 -10
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js +56 -8
- package/lib/types/types.js.map +1 -1
- package/lib/types/wiki-types.d.ts +8 -1
- package/lib/types/wiki-types.d.ts.map +1 -1
- package/lib/types/wiki-types.js +16 -25
- package/lib/types/wiki-types.js.map +1 -1
- package/package.json +4 -3
- package/preset/tita-data-agent/agent.cordis.yml +11 -5
- package/src/client/DirectoryPicker.tsx +12 -12
- package/src/client/InfoHint.tsx +21 -8
- package/src/client/IntentTab.tsx +20 -26
- package/src/client/KnowledgeBaseTab.tsx +190 -80
- package/src/client/KnowledgePanel.tsx +42 -9
- package/src/client/KnowledgeStepRail.tsx +3 -0
- package/src/client/NodeSettings.tsx +20 -22
- package/src/client/PreviewSections.tsx +87 -33
- package/src/client/ProposalReview.tsx +313 -0
- package/src/client/TypeDefinitionDrawer.tsx +72 -38
- package/src/client/WikiTypeEditor.tsx +193 -77
- package/src/client/arch/AgentArchitectureGraph.tsx +143 -14
- package/src/client/arch/NodeLogicPanel.tsx +6 -3
- package/src/client/arch/model.ts +164 -26
- package/src/client/architecture.css +29 -9
- package/src/client/architecture.tsx +54 -29
- package/src/client/attribution/AttributionGraph.tsx +481 -34
- package/src/client/attribution/AttributionTab.tsx +507 -106
- package/src/client/attribution/CaseView.tsx +9 -14
- package/src/client/attribution/GraphStage.tsx +25 -18
- package/src/client/attribution/RunView.tsx +27 -9
- package/src/client/attribution/StageChrome.tsx +296 -140
- package/src/client/attribution/graph-options.ts +611 -47
- package/src/client/attribution/model.ts +559 -30
- package/src/client/convert/ConvertPanel.tsx +6 -1
- package/src/client/doc-page.css +134 -0
- package/src/client/doc-page.tsx +263 -0
- package/src/client/panel-state.ts +105 -0
- package/src/client/request.ts +36 -0
- package/src/client/styles.css +317 -43
- package/src/client/useFitScale.ts +15 -2
- package/src/client/wiki-type-groups.ts +86 -0
- package/src/convert/host.ts +41 -7
- package/src/diagnosis/cards.ts +120 -50
- package/src/diagnosis/run.ts +245 -0
- package/src/diagnosis/schema.ts +67 -26
- package/src/diagnosis/seed-source.ts +28 -3
- package/src/diagnosis/seed.ts +187 -85
- package/src/diagnosis/store.ts +358 -174
- package/src/diagnosis/types.ts +181 -16
- package/src/document-page.ts +51 -0
- package/src/fs-atomic.ts +42 -0
- package/src/index.ts +209 -22
- package/src/knowledge-bases.ts +20 -18
- package/src/proposals.ts +337 -0
- package/src/runtime.ts +736 -135
- package/src/types.ts +113 -19
- package/src/wiki-types.ts +17 -27
|
@@ -14,8 +14,12 @@
|
|
|
14
14
|
* 缩放**不用 G6 的 `fitView` / `zoomTo`**:这版 G6 对 HTML 节点算不出内容包围盒,
|
|
15
15
|
* 两个 API 都实测无效(画布按 0.45 定了尺寸,内容仍按 1:1 画出去被裁掉)。
|
|
16
16
|
* 改成画布永远按设计尺寸渲染、再缩整块板:比例是我们算的,不依赖 G6 的判断。
|
|
17
|
-
* 4.
|
|
18
|
-
*
|
|
17
|
+
* 4. **画布自持视口,平移是空格 + 拖拽。** 不注册 G6 的 `drag-canvas` / `zoom-canvas`:
|
|
18
|
+
* 滚轮该滚容器而不是缩画布,缩放由两档按钮决定。0042 之前这里的平移交给滚动条,
|
|
19
|
+
* 而窄栏「适应」档下板高正好等于容器高、两个方向的可滚余量都是 0 —— 那个滚动条
|
|
20
|
+
* 从来没有滚动过。现在这一栏是一块**自持视口的画布**(`.tita-archScrollStage`:
|
|
21
|
+
* 占满面板高度、只按宽度适应、隐藏滚动条),平移由按住空格 + 鼠标拖拽承担,
|
|
22
|
+
* G6 在这一页只负责画和点选。
|
|
19
23
|
* 5. **操作是浮层,画布上没有图例。**
|
|
20
24
|
* 0026 把图例、提示、状态统计和全屏按钮放在画布上方的一条 flex 行里,栏宽一窄
|
|
21
25
|
* `flex-wrap` 就把全屏按钮甩到第二行最左边 —— 用户看到的「全屏不在右上角」就是这么来的。
|
|
@@ -29,7 +33,7 @@
|
|
|
29
33
|
*/
|
|
30
34
|
|
|
31
35
|
import { Graph } from '@antv/g6'
|
|
32
|
-
import { useCallback, useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react'
|
|
36
|
+
import { useCallback, useEffect, useRef, useState, type CSSProperties, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from 'react'
|
|
33
37
|
import { Maximize2, Minimize2 } from '@tita-data-agent/ui/icons'
|
|
34
38
|
import { Button, cn } from '@tita-data-agent/ui/components'
|
|
35
39
|
import { NODE_META, NODE_STATUS_LABEL, nodeMetrics, type KnowledgeStats, type NodeId } from '../architecture.tsx'
|
|
@@ -43,6 +47,16 @@ import { styles } from '../styles.ts'
|
|
|
43
47
|
// 架构视图自己的版式(图 + 浮层)跟着这个组件走,不并进插件通用 styles.css。
|
|
44
48
|
import '../architecture.css'
|
|
45
49
|
|
|
50
|
+
/**
|
|
51
|
+
* 空格是双用途键:输入框里它是空格,画布上是平移修饰键。
|
|
52
|
+
* 这个判据只管前者;「是不是在画布上按的」由 `pointerInside` 负责。
|
|
53
|
+
*/
|
|
54
|
+
function isTypingTarget(target: EventTarget | null): boolean {
|
|
55
|
+
if (!(target instanceof HTMLElement)) return false
|
|
56
|
+
const tag = target.tagName
|
|
57
|
+
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target.isContentEditable
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
export interface AgentArchitectureGraphProps {
|
|
47
61
|
stats: KnowledgeStats
|
|
48
62
|
version: number
|
|
@@ -71,7 +85,22 @@ export function AgentArchitectureGraph({ stats, version, selected, onSelect, sel
|
|
|
71
85
|
selectDomainRef.current = onSelectDomain
|
|
72
86
|
const [fullscreen, setFullscreen] = useState(false)
|
|
73
87
|
const [mode, setMode] = useState<ZoomMode>('fit')
|
|
74
|
-
|
|
88
|
+
// 侧栏里 `fit: 'width'` —— 这一栏是自持视口的画布,纵向溢出交给平移(见文件头第 4 条)。
|
|
89
|
+
// 全屏恢复 `both`:全屏的用途就是「一眼看全」,那里再让人平移是帮倒忙(而且全屏够宽,
|
|
90
|
+
// 比例通常已经是 1 或由宽度决定)。
|
|
91
|
+
const { scale, boardH } = useFitScale(
|
|
92
|
+
scroller, CANVAS_WIDTH, CANVAS_HEIGHT, visible, fullscreen, mode, fullscreen ? 'both' : 'width',
|
|
93
|
+
)
|
|
94
|
+
/**
|
|
95
|
+
* 空格 + 拖拽平移。三份状态分开存:
|
|
96
|
+
* - `panReady`:修饰键现在是否按下(决定光标、提示与是否拦截指针);
|
|
97
|
+
* - `panFrom`:一次拖拽的起点(鼠标位置 + 当时的滚动量);
|
|
98
|
+
* - `dragged`:刚发生过一次真的拖动 —— 用来吃掉紧随其后的 `click`。
|
|
99
|
+
*/
|
|
100
|
+
const [panReady, setPanReady] = useState(false)
|
|
101
|
+
const panFrom = useRef<{ x: number; y: number; left: number; top: number } | null>(null)
|
|
102
|
+
const dragged = useRef(false)
|
|
103
|
+
const pointerInside = useRef(false)
|
|
75
104
|
/** 选中态要在「建图 / 换数据 / 换选中」三条路径上都对得上,所以集中存一份。 */
|
|
76
105
|
const stateRef = useRef({ selected, selectedDomain, stats, version })
|
|
77
106
|
stateRef.current = { selected, selectedDomain, stats, version }
|
|
@@ -89,12 +118,14 @@ export function AgentArchitectureGraph({ stats, version, selected, onSelect, sel
|
|
|
89
118
|
container: el,
|
|
90
119
|
width: el.clientWidth || CANVAS_WIDTH,
|
|
91
120
|
height: el.clientHeight || CANVAS_HEIGHT,
|
|
92
|
-
// 画布底色交给容器(`.tita-
|
|
121
|
+
// 画布底色交给容器(`.tita-archScrollStage` 的 `--tita-canvas`):canvas 不认 CSS 变量,
|
|
93
122
|
// 它会先 clearRect(清成全透明)再把 background 当 fillStyle 填 —— 传令牌字符串等于
|
|
94
123
|
// 交给浏览器一个无效值,填色被忽略,默认的黑色 fillStyle 会直接盖上来。透明才是
|
|
95
124
|
//「跟着主题走」在画布上的唯一正确写法,浅色下与原来的纯白一致。
|
|
96
125
|
background: 'transparent',
|
|
97
|
-
//
|
|
126
|
+
// 画布不承担缩放平移:缩放由外层那块板上的 `transform` 决定,
|
|
127
|
+
// 平移由组件自己接管(空格 + 拖拽改滚动容器的 `scrollLeft/scrollTop`),
|
|
128
|
+
// G6 只管画与点选。
|
|
98
129
|
// 图级 `node.type` / `edge.type` **不要设**:G6 v5 用它覆盖每个元素自己声明的
|
|
99
130
|
// `type`,于是装饰节点会被当成 HTML 节点画成空盒子,整张图只剩悬空的卡片
|
|
100
131
|
// (0025 起就是这样,实测确认)。类型一律由元素自己声明。
|
|
@@ -159,6 +190,85 @@ export function AgentArchitectureGraph({ stats, version, selected, onSelect, sel
|
|
|
159
190
|
void graph.draw()
|
|
160
191
|
}, [selected, selectedDomain, visible])
|
|
161
192
|
|
|
193
|
+
// ---------------------------------------------------------------- 平移
|
|
194
|
+
/**
|
|
195
|
+
* 空格 = 平移修饰键,但**只在鼠标位于画布上时**才算。
|
|
196
|
+
*
|
|
197
|
+
* 两个判据缺一不可:
|
|
198
|
+
* - 输入框里按空格是打字,必须原样放行;
|
|
199
|
+
* - 鼠标不在画布上时不该拦(这个面板里到处是按钮,焦点落在哪一格都可能)——
|
|
200
|
+
* `preventDefault` 一次,那一处的空格就失效了。
|
|
201
|
+
*/
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (!visible) return undefined
|
|
204
|
+
const onKeyDown = (event: KeyboardEvent): void => {
|
|
205
|
+
if (event.code !== 'Space' || event.repeat) return
|
|
206
|
+
if (!pointerInside.current || isTypingTarget(event.target)) return
|
|
207
|
+
// 拦在最前面:不拦的话浏览器拿这一下去滚页面(宿主的可滚区)。
|
|
208
|
+
event.preventDefault()
|
|
209
|
+
setPanReady(true)
|
|
210
|
+
}
|
|
211
|
+
const onKeyUp = (event: KeyboardEvent): void => {
|
|
212
|
+
if (event.code !== 'Space') return
|
|
213
|
+
setPanReady(false)
|
|
214
|
+
panFrom.current = null
|
|
215
|
+
}
|
|
216
|
+
/** 切走窗口 / 切标签页时收不到 keyup —— 不兜底的话空格会「卡住」。 */
|
|
217
|
+
const onBlur = (): void => { setPanReady(false); panFrom.current = null }
|
|
218
|
+
window.addEventListener('keydown', onKeyDown)
|
|
219
|
+
window.addEventListener('keyup', onKeyUp)
|
|
220
|
+
window.addEventListener('blur', onBlur)
|
|
221
|
+
return () => {
|
|
222
|
+
window.removeEventListener('keydown', onKeyDown)
|
|
223
|
+
window.removeEventListener('keyup', onKeyUp)
|
|
224
|
+
window.removeEventListener('blur', onBlur)
|
|
225
|
+
}
|
|
226
|
+
}, [visible])
|
|
227
|
+
|
|
228
|
+
const onPointerDown = (event: ReactPointerEvent<HTMLDivElement>): void => {
|
|
229
|
+
const el = scroller.current
|
|
230
|
+
if (!el) return
|
|
231
|
+
// 每一次按下都先清掉「刚拖动过」这个标记。不清的话:一次拖动结束在画布外
|
|
232
|
+
// (浏览器没补 click)会把它留下,下一次无关的点击就被吃掉一次。
|
|
233
|
+
dragged.current = false
|
|
234
|
+
if (!panReady || event.button !== 0) return
|
|
235
|
+
// 拦下指针:这次拖动不该触发 G6 的命中判定,也不该选中卡片文字。
|
|
236
|
+
event.preventDefault()
|
|
237
|
+
event.stopPropagation()
|
|
238
|
+
panFrom.current = { x: event.clientX, y: event.clientY, left: el.scrollLeft, top: el.scrollTop }
|
|
239
|
+
try { el.setPointerCapture(event.pointerId) } catch { /* 指针已失效 */ }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const onPointerMove = (event: ReactPointerEvent<HTMLDivElement>): void => {
|
|
243
|
+
const el = scroller.current
|
|
244
|
+
const from = panFrom.current
|
|
245
|
+
if (!el || !from) return
|
|
246
|
+
const dx = event.clientX - from.x
|
|
247
|
+
const dy = event.clientY - from.y
|
|
248
|
+
// 4px 以内当手抖:没有这道闸,一次点选会被记成一次平移。
|
|
249
|
+
if (Math.abs(dx) > 4 || Math.abs(dy) > 4) dragged.current = true
|
|
250
|
+
el.scrollLeft = from.left - dx
|
|
251
|
+
el.scrollTop = from.top - dy
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const endPan = (event: ReactPointerEvent<HTMLDivElement>): void => {
|
|
255
|
+
const el = scroller.current
|
|
256
|
+
if (!el || !panFrom.current) return
|
|
257
|
+
panFrom.current = null
|
|
258
|
+
try { el.releasePointerCapture(event.pointerId) } catch { /* 指针已失效 */ }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 拖动之后浏览器仍会补一个 `click`(点在哪个节点上就是哪个节点的点击)。
|
|
263
|
+
* 在**捕获阶段**吃掉它:G6 的监听挂在画布元素上,冒泡阶段再拦就已经晚了。
|
|
264
|
+
*/
|
|
265
|
+
const onClickCapture = (event: ReactMouseEvent<HTMLDivElement>): void => {
|
|
266
|
+
if (!dragged.current) return
|
|
267
|
+
dragged.current = false
|
|
268
|
+
event.preventDefault()
|
|
269
|
+
event.stopPropagation()
|
|
270
|
+
}
|
|
271
|
+
|
|
162
272
|
// ---------------------------------------------------------------- 全屏
|
|
163
273
|
const toggleFullscreen = useCallback((): void => {
|
|
164
274
|
const el = stage.current
|
|
@@ -173,9 +283,11 @@ export function AgentArchitectureGraph({ stats, version, selected, onSelect, sel
|
|
|
173
283
|
return () => document.removeEventListener('fullscreenchange', onChange)
|
|
174
284
|
}, [])
|
|
175
285
|
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
|
|
286
|
+
// 画布永远是设计尺寸,缩放由板上的 transform 承担。**外面还要一层按比例定尺寸的框**:
|
|
287
|
+
// transform 不改变布局尺寸,只写 transform 的话滚动区永远是 1172×1132 ——
|
|
288
|
+
// `适应` 档下缩到 0.32 也照样能往虚空里拖出 770px。框的尺寸 = 缩完的尺寸,
|
|
289
|
+
// 于是可平移的范围**正好是图的边界**:适应档只有纵向可推,1:1 档两个方向都推得动。
|
|
290
|
+
const frameStyle = { width: CANVAS_WIDTH * scale, height: CANVAS_HEIGHT * scale }
|
|
179
291
|
const boardStyle = {
|
|
180
292
|
width: CANVAS_WIDTH,
|
|
181
293
|
height: CANVAS_HEIGHT,
|
|
@@ -189,21 +301,38 @@ export function AgentArchitectureGraph({ stats, version, selected, onSelect, sel
|
|
|
189
301
|
浮层正文里,左上角那块说明板只是把图挡掉一角。 */}
|
|
190
302
|
<div className={styles.archOverlay}>
|
|
191
303
|
<div className={styles.archOverlayActions}>
|
|
304
|
+
{/* 平移是这块画布唯一的移动方式(滚动条已隐藏),所以提示留在操作区里 ——
|
|
305
|
+
它回答的是「怎么看这张图」,与缩放档、全屏同一层。按住空格时它顺便当模式指示。 */}
|
|
306
|
+
<span className={cn(styles.archPanHint, panReady && styles.archPanHintOn)} aria-hidden="true">
|
|
307
|
+
{panReady ? '拖拽平移中' : '空格 + 拖拽平移'}
|
|
308
|
+
</span>
|
|
192
309
|
{/* 缩放档:两段的分段控件。两段都在,是因为一个来回翻转的按钮说不清「现在是哪一档」。 */}
|
|
193
310
|
<div className={styles.archZoom} role="group" aria-label="缩放">
|
|
194
311
|
<button type="button" className={cn(styles.archZoomOption, mode === 'fit' && styles.archZoomOn)}
|
|
195
|
-
aria-pressed={mode === 'fit'} onClick={() => setMode('fit')} title="
|
|
312
|
+
aria-pressed={mode === 'fit'} onClick={() => setMode('fit')} title="按栏宽适应:宽度铺满,纵向溢出靠空格拖拽平移">适应</button>
|
|
196
313
|
<button type="button" className={cn(styles.archZoomOption, mode === 'actual' && styles.archZoomOn)}
|
|
197
|
-
aria-pressed={mode === 'actual'} onClick={() => setMode('actual')} title="按设计尺寸 1:1
|
|
314
|
+
aria-pressed={mode === 'actual'} onClick={() => setMode('actual')} title="按设计尺寸 1:1 显示,两个方向都可以平移查看">1:1</button>
|
|
198
315
|
</div>
|
|
199
316
|
<Button variant="outline" size="sm" onClick={toggleFullscreen} aria-pressed={fullscreen}>
|
|
200
317
|
{fullscreen ? <Minimize2 size={14} /> : <Maximize2 size={14} />}{fullscreen ? '退出全屏' : '全屏'}
|
|
201
318
|
</Button>
|
|
202
319
|
</div>
|
|
203
320
|
</div>
|
|
204
|
-
<div
|
|
205
|
-
|
|
206
|
-
|
|
321
|
+
<div
|
|
322
|
+
className={cn(styles.archScroll, styles.archScrollStage, panReady && styles.archPanOn)}
|
|
323
|
+
ref={scroller}
|
|
324
|
+
onPointerEnter={() => { pointerInside.current = true }}
|
|
325
|
+
onPointerLeave={() => { pointerInside.current = false; setPanReady(false); panFrom.current = null }}
|
|
326
|
+
onPointerDown={onPointerDown}
|
|
327
|
+
onPointerMove={onPointerMove}
|
|
328
|
+
onPointerUp={endPan}
|
|
329
|
+
onPointerCancel={endPan}
|
|
330
|
+
onClickCapture={onClickCapture}
|
|
331
|
+
>
|
|
332
|
+
<div className={styles.archFrame} style={frameStyle}>
|
|
333
|
+
<div className={styles.archBoard} style={boardStyle}>
|
|
334
|
+
<div className={styles.archCanvas} ref={host} />
|
|
335
|
+
</div>
|
|
207
336
|
</div>
|
|
208
337
|
</div>
|
|
209
338
|
{/* 详情浮层:画布内,不缩放,可关闭。关掉之后画布恢复成完整的一张图。 */}
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
import { Badge, Button, cn } from '@tita-data-agent/ui/components'
|
|
22
22
|
import {
|
|
23
23
|
NODE_META, NODE_STATUS_LABEL, jumpTargetOf, laneOf, nodeMetrics,
|
|
24
|
-
type KnowledgeStats, type KnowledgeTab, type NodeId, type NodeStatus, type PanelTab,
|
|
24
|
+
type KnowledgeStats, type KnowledgeTab, type NodeId, type NodeStatus, type PanelTab, type WorkspaceTabKind,
|
|
25
25
|
} from '../architecture.tsx'
|
|
26
26
|
import { LANE_PALETTE, recordRows } from './model.ts'
|
|
27
27
|
import { styles } from '../styles.ts'
|
|
@@ -38,7 +38,8 @@ export interface NodeLogicPanelProps {
|
|
|
38
38
|
version: number
|
|
39
39
|
/** 跳到节点所属的功能 tab;`knowledgeTab` 存在时还要落到知识库里那一栏。 */
|
|
40
40
|
onJump: (tab: PanelTab, knowledgeTab?: KnowledgeTab) => void
|
|
41
|
-
|
|
41
|
+
/** 打开一个工作区页面(Table / Notebook / 画布)。 */
|
|
42
|
+
onOpenTab?: (kind: WorkspaceTabKind) => void
|
|
42
43
|
onClose: () => void
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -123,9 +124,11 @@ export function NodeLogicPanel({ selected, stats, version, onJump, onOpenTab, on
|
|
|
123
124
|
去配置<ArrowRight size={14} />
|
|
124
125
|
</Button>
|
|
125
126
|
: <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
|
+
{(selected === 'answer' || selected === 'data-sheet' || selected === 'data-notebook' || selected === 'data-canvas') && onOpenTab && <div className={styles.archPanelActions}>
|
|
127
128
|
<Button variant="outline" size="sm" onClick={() => onOpenTab('data-sheet')}><ChevronRight size={13} />打开 Table</Button>
|
|
128
129
|
<Button variant="outline" size="sm" onClick={() => onOpenTab('data-notebook')}><ChevronRight size={13} />打开 Notebook</Button>
|
|
130
|
+
{/* 画布与 Table / Notebook 并列:三者都是这一格的输出产物,之前少列了一个。 */}
|
|
131
|
+
<Button variant="outline" size="sm" onClick={() => onOpenTab('data-excalidraw')}><ChevronRight size={13} />打开画布</Button>
|
|
129
132
|
</div>}
|
|
130
133
|
</PanelSection>
|
|
131
134
|
</div>
|
package/src/client/arch/model.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import {
|
|
24
|
-
NODE_LANES, NODE_META, NODE_STATUS_LABEL, NODE_STATUS_SHORT,
|
|
24
|
+
NODE_LANES, NODE_META, NODE_STATUS_LABEL, NODE_STATUS_SHORT, memoryCountOf, nodeMetrics, sourceCountOf,
|
|
25
25
|
type KnowledgeStats, type LaneId, type LaneMeta, type NodeId, type NodeMetric, type NodeStatus,
|
|
26
26
|
} from '../architecture.tsx'
|
|
27
27
|
import { PROBLEM_BASE_NOTE, PROBLEM_DOMAINS, type ProblemDomain } from '../problem-domains.ts'
|
|
@@ -40,12 +40,54 @@ export const GUTTER = 116
|
|
|
40
40
|
export const NODE_W = 180
|
|
41
41
|
/** 卡片高度要装下四层:标题行、两行说明、统计行。 */
|
|
42
42
|
export const NODE_H = 96
|
|
43
|
-
/**
|
|
43
|
+
/** 竖排两到三张时用的规格。两张那一列装得下计数行,三张那一列装不下。 */
|
|
44
44
|
export const NODE_H_STACKED = 80
|
|
45
|
+
/**
|
|
46
|
+
* 一列竖排**三张**时的卡片高度(2026-09-26 加画布带来的第三张)。
|
|
47
|
+
*
|
|
48
|
+
* 它比两排矮一档,不是审美选择,是算术:三张要挤进同一条带,而带高是全图共享的。
|
|
49
|
+
* 内容也正好只差一行 —— 这一列的 Table / Notebook / 画布都没有计数行,两行文字
|
|
50
|
+
* (标题 + 说明)加 8px 上下内边距正好 52px;而情节 / 操作记忆两张带计数行,需要 73px,
|
|
51
|
+
* 所以它们仍用 80。写死一个高度给两种内容,不是裁掉计数就是白留一条空带。
|
|
52
|
+
*/
|
|
53
|
+
export const NODE_H_STACKED_TRIO = 52
|
|
45
54
|
export const COL_GAP = 32
|
|
46
|
-
/**
|
|
47
|
-
*
|
|
48
|
-
|
|
55
|
+
/**
|
|
56
|
+
* 竖排卡片之间的间隙。**它与卡片高度一起决定列间距**,所以只写一处:
|
|
57
|
+
* 两张那一列 = 2×80 + 14 = 174,三张那一列 = 3×52 + 2×14 = 184。
|
|
58
|
+
*/
|
|
59
|
+
export const STACK_GAP = 14
|
|
60
|
+
/**
|
|
61
|
+
* **先写「哪一列竖排几张」,再算带高。**
|
|
62
|
+
*
|
|
63
|
+
* 带高由最高的一列决定(`CONTENT_H`),而带高又决定画布高度 —— 都是模块初始化的常量,
|
|
64
|
+
* 所以这张表必须在它们之前出现。它与 `ARCH_SLOTS` 里的 `stack` 序号是同一件事的两面,
|
|
65
|
+
* 测试会断言两者一致(一处写三张、另一处只放两张是最容易犯的错,而且肉眼看不出来)。
|
|
66
|
+
*/
|
|
67
|
+
export const STACK_COLUMNS: ReadonlyArray<{ lane: number; col: number; count: number }> = [
|
|
68
|
+
{ lane: 0, col: 4, count: 3 },
|
|
69
|
+
{ lane: 2, col: 3, count: 2 },
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
/** 一列竖排 `count` 张时的卡片高度。 */
|
|
73
|
+
export function stackHeight(count: number): number {
|
|
74
|
+
return count >= 3 ? NODE_H_STACKED_TRIO : NODE_H_STACKED
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 一列竖排 `count` 张时需要的内容区高度 = `count × 卡高 + (count-1) × 间隙`。
|
|
79
|
+
*
|
|
80
|
+
* 卡片在列内**等距**排:第 i 张的中心偏移 = (i - (count-1)/2) × (卡高 + 间隙)。
|
|
81
|
+
* 所以最外两张的外沿正好落在 ±CONTENT_H/2 上,不多不少。
|
|
82
|
+
*/
|
|
83
|
+
export function columnHeight(count: number): number {
|
|
84
|
+
return count <= 0 ? 0 : count * stackHeight(count) + (count - 1) * STACK_GAP
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 一列竖排 `count` 张时,第 `index` 张相对带中心线的偏移。 */
|
|
88
|
+
export function stackOffset(count: number, index: number): number {
|
|
89
|
+
return (index - (count - 1) / 2) * (stackHeight(count) + STACK_GAP)
|
|
90
|
+
}
|
|
49
91
|
/** 带名条的高度:一枚字母徽标 + 链路名。 */
|
|
50
92
|
export const LANE_HEAD_H = 30
|
|
51
93
|
/**
|
|
@@ -62,8 +104,8 @@ export const LANE_HEAD_W = 224
|
|
|
62
104
|
*/
|
|
63
105
|
export const BAND_GAP = 24
|
|
64
106
|
export const LANE_PAD = 12
|
|
65
|
-
/**
|
|
66
|
-
export const CONTENT_H =
|
|
107
|
+
/** 带的内容区高度:**由最高的那一列决定**(三张 52 的那列 = 184),单节点在其中居中。 */
|
|
108
|
+
export const CONTENT_H = Math.max(...STACK_COLUMNS.map(column => columnHeight(column.count)))
|
|
67
109
|
/** 带高 = 带名条 + 上下内边距 + 内容区。三条带等高,网格才对得齐。 */
|
|
68
110
|
export const BAND_H = LANE_HEAD_H + LANE_PAD + CONTENT_H + LANE_PAD
|
|
69
111
|
/** 两条带的上沿间距 = 带高 + 通道。 */
|
|
@@ -140,15 +182,16 @@ export interface ArchBox { x: number; y: number; width: number; height: number }
|
|
|
140
182
|
* 出来,经过记忆与质量门,最后回到最左边的「资金化」—— 反向流不是排版花招,
|
|
141
183
|
* 它就是闭环的方向,也是 A→C 那条连线能画得短的原因。
|
|
142
184
|
*/
|
|
143
|
-
interface Slot { lane: number; col: number; stack?:
|
|
185
|
+
interface Slot { lane: number; col: number; /** 在竖排里的序号(0 起)。见 `STACK_COLUMNS`。 */ stack?: number }
|
|
144
186
|
|
|
145
187
|
export const ARCH_SLOTS: Record<NodeId, Slot> = {
|
|
146
188
|
conversation: { lane: 0, col: 0 },
|
|
147
189
|
intent: { lane: 0, col: 1 },
|
|
148
190
|
runtime: { lane: 0, col: 2 },
|
|
149
191
|
answer: { lane: 0, col: 3 },
|
|
150
|
-
'data-sheet': { lane: 0, col: 4, stack:
|
|
151
|
-
'data-notebook': { lane: 0, col: 4, stack:
|
|
192
|
+
'data-sheet': { lane: 0, col: 4, stack: 0 },
|
|
193
|
+
'data-notebook': { lane: 0, col: 4, stack: 1 },
|
|
194
|
+
'data-canvas': { lane: 0, col: 4, stack: 2 },
|
|
152
195
|
|
|
153
196
|
'source-raw': { lane: 1, col: 0 },
|
|
154
197
|
compile: { lane: 1, col: 1 },
|
|
@@ -159,8 +202,8 @@ export const ARCH_SLOTS: Record<NodeId, Slot> = {
|
|
|
159
202
|
'funded-value': { lane: 2, col: 0 },
|
|
160
203
|
'daily-update': { lane: 2, col: 1 },
|
|
161
204
|
'memory-quality': { lane: 2, col: 2 },
|
|
162
|
-
'episodic-memory': { lane: 2, col: 3, stack:
|
|
163
|
-
'procedural-memory': { lane: 2, col: 3, stack:
|
|
205
|
+
'episodic-memory': { lane: 2, col: 3, stack: 0 },
|
|
206
|
+
'procedural-memory': { lane: 2, col: 3, stack: 1 },
|
|
164
207
|
interaction: { lane: 2, col: 4 },
|
|
165
208
|
}
|
|
166
209
|
|
|
@@ -182,14 +225,14 @@ export function isDecoration(id: string): boolean {
|
|
|
182
225
|
|
|
183
226
|
export function boxOf(id: NodeId): ArchBox {
|
|
184
227
|
const slot = ARCH_SLOTS[id]
|
|
185
|
-
const
|
|
228
|
+
const column = STACK_COLUMNS.find(item => item.lane === slot.lane && item.col === slot.col)
|
|
229
|
+
const height = column ? stackHeight(column.count) : NODE_H
|
|
186
230
|
const center = laneCenter(slot.lane)
|
|
187
|
-
const y = slot.stack
|
|
188
|
-
? center + (slot.stack === 'upper' ? -STACK_OFFSET : STACK_OFFSET)
|
|
189
|
-
: center
|
|
231
|
+
const y = column ? center + stackOffset(column.count, slot.stack ?? 0) : center
|
|
190
232
|
return { x: columnX(slot.col), y, width: NODE_W, height }
|
|
191
233
|
}
|
|
192
234
|
|
|
235
|
+
|
|
193
236
|
// ----------------------------------------------------------------- 连边
|
|
194
237
|
|
|
195
238
|
/**
|
|
@@ -222,6 +265,8 @@ export const ARCH_EDGES: ArchEdge[] = [
|
|
|
222
265
|
{ id: 'a3', source: 'runtime', target: 'answer', kind: 'flow' },
|
|
223
266
|
{ id: 'a4', source: 'answer', target: 'data-sheet', kind: 'branch' },
|
|
224
267
|
{ id: 'a5', source: 'answer', target: 'data-notebook', kind: 'branch' },
|
|
268
|
+
// 画布与 Table / Notebook 是同一格的三种输出产物,所以也是同一种边。
|
|
269
|
+
{ id: 'a6', source: 'answer', target: 'data-canvas', kind: 'branch' },
|
|
225
270
|
|
|
226
271
|
// B · 知识编译与召回链路(写入侧 → 读取侧)
|
|
227
272
|
{ id: 'b1', source: 'source-raw', target: 'compile', kind: 'flow' },
|
|
@@ -246,6 +291,77 @@ export const ARCH_EDGES: ArchEdge[] = [
|
|
|
246
291
|
{ id: 'x4', source: 'context', target: 'runtime', kind: 'inject', label: 'Context Pack 注入' },
|
|
247
292
|
]
|
|
248
293
|
|
|
294
|
+
/**
|
|
295
|
+
* 「同一个发点出去的两条以上」或「汇进同一个落点的两条以上」的边 —— 它们**走一条总线**。
|
|
296
|
+
*
|
|
297
|
+
* ## 为什么不能各走各的(2026-09-26 GUI 反馈)
|
|
298
|
+
*
|
|
299
|
+
* 原来 `branch` 用 `router: { type: 'orth' }`。路由器是**每条边各算各的**:它把每条线
|
|
300
|
+
* 连到「离自己最近的那两个边界点」,于是回答那一格的右边被三条线在三个高度上分别咬开 ——
|
|
301
|
+
* 一张卡片伸出三根箭头,各自还带一个小台阶。用户的原话是「箭头改成从一个地方出,
|
|
302
|
+
* 这样样式不好看」。
|
|
303
|
+
*
|
|
304
|
+
* ## 判据是**边的邻居**,不是边的种类
|
|
305
|
+
*
|
|
306
|
+
* 一个节点只要有两条及以上的同带相邻列连线,那几条就共用一段总线。所以 2026-09-26 把
|
|
307
|
+
* 画布加成第三张输出产物时,这条规则**没有改一个字** —— 它自己就把新边算了进去。
|
|
308
|
+
* 反过来若按「`branch` 就走总线」写,多对一的那种(情节 / 操作记忆 → 记忆质量检查,
|
|
309
|
+
* 那是两条 `flow`)就漏了,而它们看着是同一毛病。
|
|
310
|
+
*
|
|
311
|
+
* 跨链路的四条长线不在这里:它们不同带,本来就有自己的正交路由与避障。
|
|
312
|
+
*/
|
|
313
|
+
const BUS_EDGE_IDS: ReadonlySet<string> = (() => {
|
|
314
|
+
const local = ARCH_EDGES.filter(edge => {
|
|
315
|
+
const from = ARCH_SLOTS[edge.source]
|
|
316
|
+
const to = ARCH_SLOTS[edge.target]
|
|
317
|
+
return from !== undefined && to !== undefined && from.lane === to.lane && Math.abs(from.col - to.col) === 1
|
|
318
|
+
})
|
|
319
|
+
const out = new Map<string, number>()
|
|
320
|
+
const into = new Map<string, number>()
|
|
321
|
+
for (const edge of local) {
|
|
322
|
+
out.set(edge.source, (out.get(edge.source) ?? 0) + 1)
|
|
323
|
+
into.set(edge.target, (into.get(edge.target) ?? 0) + 1)
|
|
324
|
+
}
|
|
325
|
+
return new Set(local
|
|
326
|
+
.filter(edge => (out.get(edge.source) ?? 0) > 1 || (into.get(edge.target) ?? 0) > 1)
|
|
327
|
+
.map(edge => edge.id))
|
|
328
|
+
})()
|
|
329
|
+
|
|
330
|
+
/** 这条边是否参与总线(测试与 `toGraphData` 读它,两处不可能各算一遍)。 */
|
|
331
|
+
export function isBusEdge(edgeId: string): boolean {
|
|
332
|
+
return BUS_EDGE_IDS.has(edgeId)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* 一条总线上的**拐点**。给 G6 的 `controlPoints`,它就按我们算的点画,不再自己找出口。
|
|
337
|
+
*
|
|
338
|
+
* 从发点出来先汇到一个 x(两条列之间的中线),再竖直走到目标那一行,最后横向进盒子:
|
|
339
|
+
*
|
|
340
|
+
* ```
|
|
341
|
+
* 回答 ──┐ 拐点 1 把三条边锁在同一个 y(发点的中心),
|
|
342
|
+
* ├──▶ Table 所以三根箭头从**同一个点**出来;
|
|
343
|
+
* ├──▶ Notebook 拐点 2 各自落在目标那一行。
|
|
344
|
+
* └──▶ 画布
|
|
345
|
+
* ```
|
|
346
|
+
*
|
|
347
|
+
* 一对多是「汇出去再分开」,多对一是「各自汇过来再进同一个点」—— 同一组拐点同时满足
|
|
348
|
+
* 两种读法:前者共用第一段,后者共用最后一段。
|
|
349
|
+
*
|
|
350
|
+
* 返回 `null` 表示不必给拐点:不在总线上,或者发点与落点本来就同高(那时一条直线就是
|
|
351
|
+
* 总线,它的出口与总线的出口是同一个点)。
|
|
352
|
+
*/
|
|
353
|
+
export function busControlPoints(edge: ArchEdge): Array<[number, number]> | null {
|
|
354
|
+
if (!BUS_EDGE_IDS.has(edge.id)) return null
|
|
355
|
+
const from = boxOf(edge.source)
|
|
356
|
+
const to = boxOf(edge.target)
|
|
357
|
+
if (Math.abs(from.y - to.y) < 0.5) return null
|
|
358
|
+
const goingRight = to.x > from.x
|
|
359
|
+
const startX = from.x + (goingRight ? NODE_W / 2 : -NODE_W / 2)
|
|
360
|
+
const endX = to.x + (goingRight ? -NODE_W / 2 : NODE_W / 2)
|
|
361
|
+
const trunkX = (startX + endX) / 2
|
|
362
|
+
return [[trunkX, from.y], [trunkX, to.y]]
|
|
363
|
+
}
|
|
364
|
+
|
|
249
365
|
/**
|
|
250
366
|
* 边的外观。灰 / 蓝两档,没有第三档色相。
|
|
251
367
|
*
|
|
@@ -316,21 +432,39 @@ function escapeHtml(value: string): string {
|
|
|
316
432
|
/**
|
|
317
433
|
* 一张节点卡片的 DOM 字符串。
|
|
318
434
|
*
|
|
319
|
-
* 用 G6 的 HTML 节点(而不是 canvas
|
|
320
|
-
*
|
|
321
|
-
*
|
|
435
|
+
* 用 G6 的 HTML 节点(而不是 canvas 文本)是因为这张卡片要写**三层信息**:
|
|
436
|
+
* 标题、一句话说明、数据统计。canvas 文本只有一种字号与字重,三层挤成一样大
|
|
437
|
+
* 就等于没有层次。
|
|
322
438
|
*
|
|
323
439
|
* 关键样式全部内联:卡片是画布里的 DOM,不保证和插件样式表同时到位,
|
|
324
440
|
* 靠类名会得到一张没有边框的白块。类名只用于选中态这类可选增强。
|
|
325
441
|
*
|
|
326
|
-
* 视觉上是「白卡 + 1px 发丝边 + 6px 圆角 +
|
|
327
|
-
*
|
|
442
|
+
* 视觉上是「白卡 + 1px 发丝边 + 6px 圆角 + 几乎看不见的投影」。
|
|
443
|
+
*
|
|
444
|
+
* ## 状态字(宿主 / 待实现 / 已实现)从卡片上撤掉了(2026-09-26)
|
|
445
|
+
*
|
|
446
|
+
* 它原来在每一张卡的右上角:一枚小圆点 + 一个词。撤掉的理由不是「不好看」,
|
|
447
|
+
* 是**它同时出现十六次,于是十几张卡片的标题全被它挤窄**(`shortTitle` 就是为它
|
|
448
|
+
* 让位而存在的),而它说的是同一件事的四个值。
|
|
449
|
+
*
|
|
450
|
+
* 实现状态没有丢,只是换了三个更合适的地方:
|
|
451
|
+
* - 每条链路的名条上写着**逐带计数**(`A 已实现 1 · 宿主 5 · …`)——「整条链路做完了没」
|
|
452
|
+
* 本来就比「这一格做完了没」更接近用户要问的。
|
|
453
|
+
* - 点开任意节点,浮层标题旁就是同一档状态(`Badge` + `statusNote` 依据)。
|
|
454
|
+
* - 卡片自身的 `title` 悬停仍然给出完整状态依据,无障碍列表也照旧逐节点列出。
|
|
328
455
|
*/
|
|
329
456
|
export function cardHtml(id: NodeId, metrics: NodeMetric[], selected = false): string {
|
|
330
457
|
const meta = NODE_META[id]
|
|
331
458
|
const lane = LANE_PALETTE[meta.lane]
|
|
332
|
-
const
|
|
333
|
-
const
|
|
459
|
+
const slot = ARCH_SLOTS[id]
|
|
460
|
+
const column = STACK_COLUMNS.find(item => item.lane === slot.lane && item.col === slot.col)
|
|
461
|
+
const compact = column !== undefined
|
|
462
|
+
/**
|
|
463
|
+
* 竖排卡片的内边距**收到 8px**:三张那一列的高度是 52(见 `NODE_H_STACKED_TRIO`),
|
|
464
|
+
* 而「标题 16 + 间隙 5 + 说明 15」已经占掉 36 —— 上下各 8 正好 52,各 10 就溢出 4px。
|
|
465
|
+
* 这一列没有计数行,所以省下的就是这一圈的空气。
|
|
466
|
+
*/
|
|
467
|
+
const padY = compact ? 8 : 10
|
|
334
468
|
/**
|
|
335
469
|
* 卡片上最多两个计数,而且是**一行纯文字**,不是会自动换行的 chip 组。
|
|
336
470
|
*
|
|
@@ -342,11 +476,10 @@ export function cardHtml(id: NodeId, metrics: NodeMetric[], selected = false): s
|
|
|
342
476
|
const chips = metrics.slice(0, 2).map(metric =>
|
|
343
477
|
`<span style="display:inline-flex;align-items:baseline;gap:3px;white-space:nowrap;font-size:10px;line-height:16px;color:${INK_FAINT}">${escapeHtml(metric.label)}<b style="color:${INK};font-weight:600;font-variant-numeric:tabular-nums">${escapeHtml(metric.value)}</b></span>`).join('')
|
|
344
478
|
// 悬停给出完整名字与状态依据:卡片上的名字可能被省略号截短,但信息不能丢。
|
|
345
|
-
return `<div class="tita-archCard${selected ? ' tita-archCardOn' : ''}" title="${escapeHtml(`${meta.title} —— ${meta.statusNote}`)}" style="box-sizing:border-box;width:100%;height:100%;display:flex;flex-direction:column;gap:5px;padding
|
|
479
|
+
return `<div class="tita-archCard${selected ? ' tita-archCardOn' : ''}" title="${escapeHtml(`${meta.title} —— ${meta.statusNote}`)}" style="box-sizing:border-box;width:100%;height:100%;display:flex;flex-direction:column;gap:5px;padding:${padY}px 11px;border-radius:6px;background:${SURFACE};border:1px solid ${selected ? lane.accent : RULE};box-shadow:${selected ? `0 0 0 3px ${lane.soft}` : '0 1px 2px rgba(16,24,40,.04)'};font-family:${FONT};overflow:hidden">
|
|
346
480
|
<div style="display:flex;align-items:center;gap:6px">
|
|
347
481
|
<span style="width:6px;height:6px;border-radius:999px;background:${lane.accent};flex:0 0 auto"></span>
|
|
348
482
|
<strong style="font-size:12.5px;font-weight:600;color:${INK};line-height:1.25;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escapeHtml(meta.shortTitle ?? meta.title)}</strong>
|
|
349
|
-
<span style="margin-left:auto;flex:0 0 auto;display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;color:${status.text}"><i style="width:5px;height:5px;border-radius:999px;background:${status.dot}"></i>${escapeHtml(NODE_STATUS_SHORT[meta.status])}</span>
|
|
350
483
|
</div>
|
|
351
484
|
<div style="font-size:11px;line-height:1.4;color:${INK_MUTED};display:-webkit-box;-webkit-line-clamp:${compact ? 1 : 2};-webkit-box-orient:vertical;overflow:hidden">${escapeHtml(meta.subtitle)}</div>
|
|
352
485
|
${chips ? `<div style="margin-top:auto;display:flex;align-items:baseline;gap:10px;overflow:hidden">${chips}</div>` : ''}
|
|
@@ -526,7 +659,7 @@ function decorations(): ArchNodeDatum[] {
|
|
|
526
659
|
|
|
527
660
|
/** 带上按阅读顺序排好的节点(自进化带是反向的,由 ARCH_SLOTS 的列号决定)。 */
|
|
528
661
|
export function nodesOfLane(lane: number): NodeId[] {
|
|
529
|
-
const rank = (slot: Slot): number => slot.stack
|
|
662
|
+
const rank = (slot: Slot): number => slot.stack ?? 0
|
|
530
663
|
return (Object.keys(ARCH_SLOTS) as NodeId[])
|
|
531
664
|
.filter(id => ARCH_SLOTS[id].lane === lane)
|
|
532
665
|
.sort((left, right) => ARCH_SLOTS[left].col - ARCH_SLOTS[right].col || rank(ARCH_SLOTS[left]) - rank(ARCH_SLOTS[right]))
|
|
@@ -582,6 +715,11 @@ export function toGraphData(stats: KnowledgeStats, version: number, selected: No
|
|
|
582
715
|
// 正交 + 自动避障:这几条是长线,斜着连会横穿中间的卡片。
|
|
583
716
|
style.radius = 10
|
|
584
717
|
style.router = { type: 'shortest-path', enableObstacleAvoidance: true, offset: 14, gridSize: 8 }
|
|
718
|
+
} else if (busControlPoints(edge) !== null) {
|
|
719
|
+
// 总线上的边:拐点是我们算的,**不要路由器** —— 路由器每条边各找一个出口,
|
|
720
|
+
// 那正是「三根箭头从三个高度出来、各自还带一个小台阶」的原因。
|
|
721
|
+
style.radius = 8
|
|
722
|
+
style.controlPoints = busControlPoints(edge)
|
|
585
723
|
} else if (edge.kind === 'branch') {
|
|
586
724
|
style.radius = 8
|
|
587
725
|
style.router = { type: 'orth', padding: 14 }
|
|
@@ -16,20 +16,23 @@
|
|
|
16
16
|
|
|
17
17
|
/* ---------------------------------------------------------------- 版式 */
|
|
18
18
|
|
|
19
|
-
/*
|
|
20
|
-
|
|
19
|
+
/* **一块画布,不是一个盒子。**
|
|
20
|
+
0042 之前这里有一圈边框 + 10px 圆角 + 自己的底色,读起来是「卡片里的一张图」;
|
|
21
|
+
而同一屏上它又不接管视口(平移交给滚动条)。两种语汇缝在一起,两头都不像。
|
|
22
|
+
现在这一栏是**舞台**:没有边框与圆角,底色与面板一致(边界连色差都不留),
|
|
23
|
+
高度由面板给(`flex: 1 1 auto`),画布自己当视口 —— 见 `.tita-archScrollStage`。
|
|
24
|
+
底色仍显式给一个:全屏时这一块要自己不透明,否则背后是浏览器的黑底。 */
|
|
21
25
|
.tita-archStage {
|
|
22
26
|
position: relative;
|
|
23
27
|
display: flex;
|
|
24
28
|
min-width: 0;
|
|
29
|
+
min-height: 0;
|
|
30
|
+
flex: 1 1 auto;
|
|
25
31
|
flex-direction: column;
|
|
26
|
-
|
|
27
|
-
border-radius: 10px;
|
|
28
|
-
background: var(--tita-surface);
|
|
32
|
+
background: var(--tita-canvas);
|
|
29
33
|
overflow: hidden;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
.tita-archStage:fullscreen { border: 0; border-radius: 0; }
|
|
33
36
|
.tita-archStage:fullscreen .tita-archPane { flex: 1 1 auto; min-height: 0; }
|
|
34
37
|
|
|
35
38
|
/* 画布那一列。**浮层的定位上下文是它**,不是整块表面 —— 否则右上角那排操作
|
|
@@ -65,10 +68,27 @@
|
|
|
65
68
|
|
|
66
69
|
/* 画布容器:**画布永远是设计尺寸,缩放由板上的 transform 承担**。
|
|
67
70
|
这张画布设计宽 1172px、高 1132px(三条链路 + 一条问题域带),宿主右侧栏只有
|
|
68
|
-
380–840px
|
|
69
|
-
正好等于栏宽(没有可滚的余量),`1:1` 档下板保持设计尺寸、由滚动条承载。
|
|
71
|
+
380–840px。所以外层不去「适配」画布,而是按缩放比把整块板缩下来。
|
|
70
72
|
缩放比由组件按设计常量算出(`CANVAS_WIDTH/HEIGHT`),不写进 CSS,避免两处各写一份。 */
|
|
71
|
-
.tita-archScroll { width: 100%; min-width: 0; overflow: auto; background: var(--tita-
|
|
73
|
+
.tita-archScroll { width: 100%; min-width: 0; overflow: auto; background: var(--tita-canvas); }
|
|
74
|
+
/* 舞台模式:画布自己当视口。高度交给面板,滚动条**隐藏** —— 平移由空格 + 拖拽承担,
|
|
75
|
+
留着一条滚动条等于承认这个手势不够用(0042)。`overflow: auto` 必须保留:
|
|
76
|
+
拖拽改的是 `scrollLeft / scrollTop`,这个元素得先是滚动容器。 */
|
|
77
|
+
.tita-archScrollStage { flex: 1 1 auto; min-height: 0; scrollbar-width: none; }
|
|
78
|
+
.tita-archScrollStage::-webkit-scrollbar { display: none; }
|
|
79
|
+
/* 空格按下:光标变抓手 + 禁止选中,否则一次平移会顺手把卡片上的字选起来。 */
|
|
80
|
+
.tita-archPanOn { cursor: grab; user-select: none; }
|
|
81
|
+
.tita-archPanOn:active { cursor: grabbing; }
|
|
82
|
+
/* **按比例定尺寸的框。** `transform` 不改布局尺寸,只写 transform 的话滚动区永远是
|
|
83
|
+
1172×1132 —— 「适应」档缩到 0.32 也照样能往虚空里拖出 770px。框的尺寸等于缩完的
|
|
84
|
+
尺寸,于是可平移的范围正好是图的边界:适应档只有纵向,1:1 档两个方向。 */
|
|
85
|
+
.tita-archFrame { position: relative; flex: 0 0 auto; overflow: hidden; }
|
|
86
|
+
/* 平移提示:与缩放档同层,次要文字;按住空格时换成模式指示。
|
|
87
|
+
用 `--tita-ink-muted` 而不是 faint:这是一句**操作说明**,得读得清
|
|
88
|
+
(PRODUCT.md 已记着 faint 一档的对比度不够)。
|
|
89
|
+
`pointer-events: none` —— 它是说明,不该在拖拽起点上抢走一次 pointerdown。 */
|
|
90
|
+
.tita-archPanHint { color: var(--tita-ink-muted); font-size: 10.5px; white-space: nowrap; pointer-events: none; }
|
|
91
|
+
.tita-archPanHintOn { color: var(--tita-primary); font-weight: 650; }
|
|
72
92
|
.tita-archStage:fullscreen .tita-archScroll {
|
|
73
93
|
flex: 1 1 auto;
|
|
74
94
|
min-height: 0;
|