first
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
# OMP 风格动态工具状态设计
|
||||
|
||||
日期:2026-06-15
|
||||
|
||||
## 背景
|
||||
|
||||
当前工作台已经能接收 agent 的工具开始和工具结束事件,但前端会把每次工具调用都追加到当前回复里。复杂任务一多,聊天区会变成工具流水账,用户很难看清“agent 此刻到底在做什么”。
|
||||
|
||||
目标不是做一个简陋的工具提示,而是对齐 `omp` 的体验:当前动作始终以彩色动态状态呈现,历史动作沉淀为可折叠摘要。
|
||||
|
||||
## 已确认方向
|
||||
|
||||
- 动态工具条放在当前 assistant 回复内部,跟着当前回复走。
|
||||
- 工具条是彩色、动态、持续刷新的“跑马灯”状态,而不是普通灰色文本。
|
||||
- 工具结束后不再把所有动作摊开显示,改为丰富但折叠的历史摘要。
|
||||
- 不走 MVP 思维,优先做完整体验。
|
||||
- 采用“升级项目内 pi-agent 依赖 + 对齐新版 `omp` 事件模型”的路线。
|
||||
- 升级对象是本项目的依赖,不是用户本机全局安装的 `omp` 或 `pi-agent`。
|
||||
|
||||
## 目标体验
|
||||
|
||||
当 agent 开始调用工具时,当前 assistant 回复内部、正文之前出现一条动态工具条。它显示当前正在执行的动作,例如:
|
||||
|
||||
- Reading `workbench/PRODUCT.md`
|
||||
- Writing `wiki/topics/agent-tools.md`
|
||||
- Running `npm run typecheck`
|
||||
- Searching current knowledge base
|
||||
|
||||
同一轮回复里,如果工具连续切换,工具条只显示当前正在进行的一项,之前完成的动作进入折叠摘要。
|
||||
|
||||
当 agent 开始输出正文时,工具条仍留在同一条回复内,固定在正文上方,不抢正文阅读。当前工具全部结束后,工具条消失或变为短暂完成态,历史摘要保留。
|
||||
|
||||
## 历史摘要
|
||||
|
||||
历史摘要默认折叠,但不是只显示一个数字。它要像 `omp` 一样让用户快速判断 agent 做过什么:
|
||||
|
||||
- 按工具类型分组:Read、Write、Bash、Search、Skill 等。
|
||||
- 每组显示数量。
|
||||
- 每组显示少量关键目标,例如文件名、页面名、命令摘要。
|
||||
- 超过展示上限时显示“还有 N 项”。
|
||||
- 用户展开后可以看到完整工具清单。
|
||||
|
||||
摘要示例:
|
||||
|
||||
```text
|
||||
工具摘要
|
||||
Read (6)
|
||||
✓ SKILL.md:1-80
|
||||
✓ README.md:1-80
|
||||
✓ workbench/PRODUCT.md
|
||||
Write (4)
|
||||
✓ wiki/sources/...
|
||||
Bash (2)
|
||||
✓ npm run typecheck
|
||||
```
|
||||
|
||||
## 底层升级策略
|
||||
|
||||
先开分支验证项目内 `pi-agent` 依赖升级,而不是直接改功能。
|
||||
|
||||
验证目标:
|
||||
|
||||
1. 项目能安装成功。
|
||||
2. 后端能启动。
|
||||
3. 前端能连接后端。
|
||||
4. 基础对话能跑通。
|
||||
5. 当前知识库上下文仍能注入。
|
||||
6. Skill 调用仍能工作。
|
||||
7. 会话历史仍能读取。
|
||||
8. 新版事件里能拿到工具名、工具参数、工具状态、必要时的运行中更新。
|
||||
|
||||
如果新版依赖改了包名、导出路径或事件格式,后端要增加一个适配层,把新版事件统一整理成前端使用的稳定事件。
|
||||
|
||||
如果升级验证失败,不应退回到简陋前端方案;应记录失败点,并决定是补适配层、锁定可用版本,还是分阶段迁移。
|
||||
|
||||
## 事件设计
|
||||
|
||||
前端不直接依赖底层 `pi-agent` 原始事件。后端提供工作台自己的稳定事件:
|
||||
|
||||
- `assistant_text_delta`:assistant 正文增量。
|
||||
- `tool_status_start`:一个工具开始或当前工具切换。
|
||||
- `tool_status_update`:工具参数、目标、运行中描述更新。
|
||||
- `tool_status_end`:工具完成、失败或取消。
|
||||
- `tool_status_summary`:一轮回复的工具摘要更新。
|
||||
- `assistant_done`:本轮回复完成。
|
||||
- `assistant_error`:本轮回复异常。
|
||||
|
||||
每个工具事件至少包含:
|
||||
|
||||
- 工具调用 ID。
|
||||
- 工具名。
|
||||
- 面向用户的动作标签。
|
||||
- 目标对象,例如文件名、页面名、命令摘要。
|
||||
- 状态:运行中、完成、失败、取消。
|
||||
- 分组类型。
|
||||
- 是否可放入折叠摘要。
|
||||
|
||||
面向用户的动作标签优先来自新版事件里的自然语言意图;如果没有,就由后端根据工具名和参数生成。
|
||||
|
||||
## 前端结构
|
||||
|
||||
当前 `ChatPanel` 已经同时承担消息流、工具流、输入框、命令菜单、引用菜单等职责。实现时应把工具状态拆成独立小模块,而不是继续把逻辑塞进消息数组。
|
||||
|
||||
建议拆分:
|
||||
|
||||
- `ToolStatusRunway`:当前动态工具条。
|
||||
- `ToolHistorySummary`:折叠工具摘要。
|
||||
- `tool-status-model`:把 start/update/end 事件归并成当前状态和历史摘要。
|
||||
- `formatToolStatus`:把工具名和参数转成人能看懂的短句。
|
||||
|
||||
消息对象里不再存一长串实时工具标记。工具状态应作为当前 assistant 回复的附属状态存在;历史消息只保留完成后的摘要数据。
|
||||
|
||||
## 视觉设计
|
||||
|
||||
动态工具条应满足:
|
||||
|
||||
- 彩色流动背景或流动边框。
|
||||
- 有明确的当前动作动词。
|
||||
- 目标文字可截断,不撑破布局。
|
||||
- 工具切换时有轻微过渡。
|
||||
- 深色和浅色主题都可读。
|
||||
- 在手机宽度下不遮挡正文,不横向溢出。
|
||||
|
||||
折叠摘要应满足:
|
||||
|
||||
- 默认占用空间小。
|
||||
- 能一眼看出做过哪些类型的事。
|
||||
- 展开后清单结构清楚。
|
||||
- 不和正文 Markdown 表格、列表样式混在一起。
|
||||
|
||||
## 错误和边界状态
|
||||
|
||||
- 工具失败:当前工具条变为失败态,摘要中该项标红或标记失败。
|
||||
- 工具取消:当前工具条显示取消态,摘要保留已完成部分。
|
||||
- 工具事件缺参数:显示工具名和通用动作,不显示空目标。
|
||||
- 多个工具并行:工具条显示最近活跃工具,摘要中保留所有工具;如新版事件能提供并行状态,可显示“另有 N 项运行中”。
|
||||
- 长文件路径:中间截断,保留文件名或末尾路径。
|
||||
- 长命令:保留主命令和关键参数,展开后看完整命令。
|
||||
- 会话恢复:历史消息显示折叠摘要,不恢复动态跑马灯。
|
||||
|
||||
## 验证方式
|
||||
|
||||
升级依赖后先做底层验证:
|
||||
|
||||
- 安装依赖。
|
||||
- 类型检查。
|
||||
- 启动后端和前端。
|
||||
- 发送普通对话。
|
||||
- 发送会触发知识库检索的问题。
|
||||
- 发送会触发 read/write/bash 的任务。
|
||||
- 检查 Skill 调用是否仍可用。
|
||||
|
||||
界面实现后做体验验证:
|
||||
|
||||
- 打开本地页面实际跑一次长任务。
|
||||
- 确认屏幕上始终只有一个当前工具条,而不是一长串工具。
|
||||
- 确认工具结束后出现折叠摘要。
|
||||
- 展开摘要,确认分组和明细正确。
|
||||
- 在深色和浅色主题下检查可读性。
|
||||
- 在窄屏尺寸下检查不溢出、不遮挡输入框。
|
||||
|
||||
## 不做的事
|
||||
|
||||
- 不改用户本机全局 `omp`。
|
||||
- 不直接修改 `node_modules`。
|
||||
- 不为了工具条重做整个聊天系统。
|
||||
- 不把完整工具输出全部展示在主聊天区。
|
||||
- 不把这次升级扩展成桌面打包或新知识库能力。
|
||||
|
||||
## 成功标准
|
||||
|
||||
用户给 agent 一个复杂任务时,聊天区不再被工具流水账占满。用户始终能看到 agent 当前正在做什么;任务结束后,也能通过折叠摘要快速确认 agent 读了什么、写了什么、跑了什么。
|
||||
|
||||
最终观感应接近 `omp`:活、清楚、有节奏,而不是普通 Web 应用里的静态日志。
|
||||
@@ -0,0 +1,832 @@
|
||||
# Graph Interaction Architecture Design
|
||||
|
||||
Date: 2026-06-16
|
||||
|
||||
## Summary
|
||||
|
||||
The graph interaction bugs are not isolated pointer-handling mistakes. They come from an architectural gap: node placement, viewport transforms, pointer gestures, hover previews, community washes, minimap state, and render state currently compute or own positions in different places with different assumptions.
|
||||
|
||||
External graph architecture research confirms that the current `@llm-wiki/graph-engine` foundation is directionally strong: it already has a viewport system, live simulation, pin persistence, community tracking, density modes, and diff handling. The issue is not that the renderer technology is wrong. The issue is that the graph front-end lacks clear boundaries between state, layout, camera/geometry, gestures, overlays, and rendering.
|
||||
|
||||
This design upgrades the earlier "geometry layer" plan into a graph interaction architecture, but it should land as a vertical slice first. The first implementation must fix the visible regressions while putting the right boundaries in place; broader renderer splitting, spatial indexing, and zoom-level policy cleanup are follow-up gates, not prerequisites.
|
||||
|
||||
Every position-sensitive behavior should use one shared model:
|
||||
|
||||
- World position: where nodes, edges, and communities live in the graph model.
|
||||
- Viewport/camera: how the user is currently looking at that world through pan and zoom.
|
||||
- Screen position: where the user sees things and where the pointer is.
|
||||
- Projection: the only allowed way to convert between world, layer, minimap, and screen spaces.
|
||||
- Interaction rules: drag, wheel, pan, hover, and selection all use the same projection.
|
||||
- Graph runtime state: one explicit owner for viewport, hover, active gesture, selected/focused graph item, and pin snapshots inside the engine.
|
||||
- Renderer boundary: drawing code paints a state, but does not interpret user gestures.
|
||||
|
||||
The implementation should not be an MVP patch and should not be a full renderer rewrite. It should keep DOM+SVG for now, preserve the public engine API, and carve the graph front-end into explicit, testable modules only where doing so directly removes a duplicated rule or fixes a named regression.
|
||||
|
||||
## First Slice Status
|
||||
|
||||
As of 2026-06-16, the first delivery slice has landed as shared graph-engine work rather than a renderer migration. It keeps the existing public engine API, DOM+SVG renderer, workbench drawer ownership, offline built-in reader path, and persisted pin format.
|
||||
|
||||
Delivered boundaries:
|
||||
|
||||
- Geometry and camera: one tested route for world, screen, layer, SVG, and minimap projection; projection helpers do not silently clamp drag targets.
|
||||
- Gestures: wheel, pointer, click, drag, community click, blank pan, UI blocker, and minimap blocker classification now share one tested path.
|
||||
- Runtime state: viewport, hover, selected/focused graph item, pins, active gesture, and simulation proposals have one graph-local owner for the interaction slice.
|
||||
- Overlays: node and edge hover previews are anchored from projected positions and are repositioned after viewport, drawer, and drag changes.
|
||||
- Simulation bridge: node dragging preserves the grab offset, stays attached to the pointer under pan/zoom, and persists pins without firing a node click.
|
||||
- Community wash: washes are visual membership regions, not drag fences; they allow wheel zoom, allow nodes to leave, and respond to outliers only within tested caps.
|
||||
- Dual-host verification: both the workbench graph and generated offline HTML use the same engine behavior.
|
||||
|
||||
Deferred gates:
|
||||
|
||||
- Spatial index: add only after profiling shows current centralized classification or O(n) hit testing is the bottleneck at real user graph sizes.
|
||||
- Canvas/WebGL: migrate only after profiling proves DOM+SVG rendering is the bottleneck at real user graph sizes.
|
||||
- Density policy cleanup: revisit only if readability or scale problems remain after the interaction foundation is stable.
|
||||
- Minimap drag navigation: add only after a product decision makes the minimap an active navigation control; the first slice treats it as a status/control blocker.
|
||||
|
||||
## Context
|
||||
|
||||
Product decisions already point in this direction:
|
||||
|
||||
- ADR-21 defines the graph as a living map with simulation and pinning.
|
||||
- ADR-21 separates position from structure: dragging changes layout, not wikilink/community truth.
|
||||
- ADR-22 defines canvas navigation as a foundation capability.
|
||||
- Stage 4.6 makes local community focus the daily-use graph mode.
|
||||
|
||||
Recent fixes exposed the missing boundary:
|
||||
|
||||
- Wheel zoom over community washes needed special gesture target handling.
|
||||
- Wheel zoom over nodes needed another target exception.
|
||||
- Node dragging was fixed by adding viewport-aware pointer mapping, but this exposed two further problems:
|
||||
- Dragging can appear trapped inside the community wash because conversion/clamping is mixed into the wrong layer.
|
||||
- Hover previews drift because they still position themselves from pre-viewport node coordinates.
|
||||
|
||||
The common cause is not the community wash itself. The common cause is that interaction geometry is not centralized.
|
||||
|
||||
External project research adds a second conclusion: geometry centralization is necessary, but not sufficient. Mature graph products and libraries repeatedly separate these concerns:
|
||||
|
||||
- Layout computes graph positions and indexes.
|
||||
- Camera/geometry converts those positions to what the user sees.
|
||||
- Gestures translate DOM events into graph intentions.
|
||||
- Renderer draws the current graph state.
|
||||
- A facade coordinates state changes and host callbacks.
|
||||
|
||||
The closest reference is Logseq's graph architecture: data, layout logic, rendering, interaction, and orchestration are separated enough that layout logic can be tested apart from the browser. Its Pixi.js/WebGL renderer is not the right fit for llm-wiki's current scale, but its layering and single gesture pipeline are the right ideas to borrow. Athens contributes the lesson that a clean event model is useful, while full event sourcing, Datascript, CRDT, or graph database infrastructure would be too heavy here. Graphify contributes the community-detection and pipeline lessons, but its static HTML and delegated vis.js interaction model are not enough for llm-wiki's live graph.
|
||||
|
||||
## Goals
|
||||
|
||||
1. All graph interactions use one coordinate model.
|
||||
2. Node dragging follows the pointer under pan, zoom, drawer resize, and community focus.
|
||||
3. Nodes can be dragged outside their community wash.
|
||||
4. Community wash shapes can respond to dragged nodes, but cannot grow without bounds.
|
||||
5. Hover previews follow the node's actual rendered position.
|
||||
6. Wheel zoom behavior is consistent over blank canvas, nodes, and community washes.
|
||||
7. Pointer panning, node dragging, node click, community click, and minimap interaction do not conflict.
|
||||
8. Gesture interpretation is centralized instead of spread across renderer callbacks.
|
||||
9. Graph runtime state is centralized for the interaction-critical slice instead of hidden across renderer closures and DOM datasets.
|
||||
10. The renderer becomes a drawing layer instead of the owner of coordinate math or interaction rules.
|
||||
11. Hit testing has one explicit compatibility path now and can later be optimized with a spatial index if profiling requires it.
|
||||
12. Zoom level and density decisions are not made worse by this refactor; full policy cleanup is a later gate.
|
||||
13. The same engine behavior remains available to both the workbench graph and Skill/offline graph.
|
||||
14. The first shipped slice produces user-visible proof: drag does not jump, hover does not drift, wheel zoom is consistent, and community washes never act as fences.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not rewrite graph data generation.
|
||||
- Do not change community membership semantics.
|
||||
- Do not change relation edge color/typing rules.
|
||||
- Do not redesign the drawer content model.
|
||||
- Do not introduce a new rendering technology such as Canvas or WebGL.
|
||||
- Do not add a new npm dependency.
|
||||
- Do not implement free-form lasso selection.
|
||||
- Do not change the knowledge-base pin storage format unless a compatibility shim proves necessary.
|
||||
- Do not introduce Neo4j, Datascript, CRDT, or full event sourcing for this graph UI refactor.
|
||||
- Do not migrate DOM+SVG to Canvas 2D until profiling proves the current renderer is the bottleneck.
|
||||
- Do not add a graph database or LLM-driven graph rebuild step as part of this interaction refactor.
|
||||
- Do not make graph-engine own the workbench drawer state; the host owns hosted reading UI.
|
||||
- Do not require spatial indexing, minimap dragging, or density-policy redesign in the first slice unless profiling or implementation proves they are necessary.
|
||||
- Do not require `jsdom` or a DOM test dependency unless the project explicitly accepts that new dependency.
|
||||
|
||||
## Design Principle
|
||||
|
||||
The graph is a map with a camera.
|
||||
|
||||
Every position-aware feature must answer these questions explicitly:
|
||||
|
||||
1. Is this value in world space, viewport/layer space, screen space, or minimap space?
|
||||
2. Which function converts it?
|
||||
3. Which layer is allowed to clamp or constrain it?
|
||||
4. Is this a structural fact, a layout choice, or temporary UI state?
|
||||
|
||||
If a function cannot answer those questions, it should not own coordinate math.
|
||||
|
||||
## Coordinate Glossary
|
||||
|
||||
- World space: the graph model coordinate system. Node positions, pins, edges, and community regions live here.
|
||||
- Screen space: CSS pixels inside the graph root. Pointer events, hover card placement, and viewport anchors live here.
|
||||
- Layer space: DOM/SVG layer pixels after world-to-layer scaling but before viewport translation/zoom is applied.
|
||||
- Minimap space: the minimap's own local coordinate system.
|
||||
- Host space: UI outside the graph engine, such as the workbench drawer and page content.
|
||||
|
||||
Every coordinate helper must name its input and output spaces. Projection helpers convert values; they do not enforce layout rules.
|
||||
|
||||
## Ownership Rules
|
||||
|
||||
The first refactor needs fewer owners, not a larger app-shaped store inside graph-engine.
|
||||
|
||||
Source of truth:
|
||||
|
||||
- Graph data and community membership come from the wiki graph model.
|
||||
- Current node world positions and pin snapshots are committed in graph runtime state.
|
||||
- Layout and simulation compute proposed world-position updates.
|
||||
- The engine coordinator commits proposed layout/simulation updates into graph runtime state and calls the host persistence callback for pins.
|
||||
- The host owns workbench drawer content and reading UI. Offline HTML keeps the existing built-in reader when no host callback exists.
|
||||
- Geometry owns projection only. It never decides where a node is allowed to move.
|
||||
- Renderer owns drawing only. It never decides whether a pointer sequence is a click, drag, pan, or zoom.
|
||||
- Gestures own event interpretation only. They emit graph intents and do not mutate DOM.
|
||||
|
||||
Pin compatibility:
|
||||
|
||||
- Persisted pins remain in the existing world coordinate format.
|
||||
- Existing pins are read as-is and are not rewritten by migration code.
|
||||
- Drag targets may move outside the current community wash and outside the visible viewport.
|
||||
- The first slice keeps persisted pins within the current world extent unless a separate compatibility decision expands world bounds.
|
||||
- Any clamping for persisted layout bounds belongs in the engine coordinator or layout constraint layer and must be named; projection functions must not silently clamp.
|
||||
|
||||
State classes:
|
||||
|
||||
- Saved layout state: pins and committed node positions.
|
||||
- Local browsing state: viewport, focused community, selected graph item, open hover/preview target.
|
||||
- Temporary gesture state: active drag, grab offset, pointer origin, gesture lock.
|
||||
- Host-only state: workbench drawer contents, conversation state, knowledge-base path, page loading/error state.
|
||||
|
||||
## Proposed Architecture
|
||||
|
||||
Use the standard graph interaction route from the research, adapted to llm-wiki's current strengths:
|
||||
|
||||
```
|
||||
GraphData
|
||||
-> GraphLayout
|
||||
-> GraphViewport / Geometry
|
||||
-> GraphRenderer / Overlays
|
||||
|
||||
DOM events
|
||||
-> GraphGestures
|
||||
-> GraphIntent
|
||||
-> GraphEngine coordinator
|
||||
-> GraphRuntimeState update
|
||||
-> Layout / Viewport / Renderer updates
|
||||
```
|
||||
|
||||
The public API should continue to look like one graph engine. Internally, each module has a narrow job and can be tested without loading the whole renderer.
|
||||
|
||||
### 0. Graph Runtime State Module
|
||||
|
||||
Owns the interaction-critical state that is currently spread across closures, DOM datasets, and renderer-local variables.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Store committed node world positions and pin snapshots used by the renderer.
|
||||
- Store current viewport/camera state.
|
||||
- Store hover target, selected graph item, focused community, and preview state.
|
||||
- Store gesture state such as active drag session and gesture lock.
|
||||
- Expose a small subscription mechanism for renderer updates.
|
||||
|
||||
Rules:
|
||||
|
||||
- State is not business data. The host still owns the current knowledge base, current conversation, and page content.
|
||||
- State is not a new global store library. Use a small typed object and explicit update functions.
|
||||
- Renderer code reads state snapshots; it does not create hidden state islands.
|
||||
- Gestures produce intent; the engine coordinator decides how intent mutates state.
|
||||
- Workbench drawer state stays host-owned through `onOpenPage`; offline mode keeps the built-in reader path.
|
||||
- Search, toolbar, diff animation, and unrelated visual state stay where they are unless moving them removes a duplicated rule.
|
||||
|
||||
Expected file direction:
|
||||
|
||||
- New file: `packages/graph-engine/src/render/state.ts` or `packages/graph-engine/src/state/index.ts`.
|
||||
- Keep it local to graph-engine. Do not introduce Zustand, Redux, or another dependency.
|
||||
|
||||
### 1. Viewport / Camera Module
|
||||
|
||||
Owns the camera state.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Normalize viewport state.
|
||||
- Pan viewport.
|
||||
- Zoom viewport around a pointer.
|
||||
- Fit world points into the current viewport.
|
||||
- Center on a world point.
|
||||
- Recompute viewport after host resize or drawer width change.
|
||||
- Convert viewport state to DOM transform.
|
||||
- Report the visible world rectangle for minimap and tests.
|
||||
|
||||
Rules:
|
||||
|
||||
- Viewport transforms move the camera, not the graph data.
|
||||
- Opening the drawer can change the available viewport size, but cannot change node world positions.
|
||||
- Viewport clamping is only about how far the user can pan the camera, not where nodes are allowed to move.
|
||||
|
||||
Expected file direction:
|
||||
|
||||
- Keep current viewport helpers in `packages/graph-engine/src/render/viewport.ts`.
|
||||
- Expand it only for camera semantics.
|
||||
- Move generic projection helpers to `geometry.ts` so `viewport.ts` does not become the new catch-all.
|
||||
|
||||
### 2. Geometry Module
|
||||
|
||||
Owns projection between spaces.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- World point to layer pixel.
|
||||
- Layer pixel to world point.
|
||||
- World point to screen point.
|
||||
- Screen point to world point.
|
||||
- World delta to layer delta.
|
||||
- World bounds to screen/layer bounds.
|
||||
- Minimap point to world point.
|
||||
- World/viewport rectangle to minimap rectangle.
|
||||
- DOM rect to local graph-screen point.
|
||||
|
||||
Rules:
|
||||
|
||||
- Geometry conversion functions must not silently clamp drag targets.
|
||||
- If a conversion needs optional clamping for a UI case, the caller must opt in and the function name must make that obvious.
|
||||
- Every consumer must pass the current viewport and viewport size explicitly.
|
||||
- Hover previews, node drag, edge previews, minimap, and community washes must use this module.
|
||||
|
||||
Current bug prevention:
|
||||
|
||||
- A dragged pointer outside the current world rectangle should still produce a meaningful target for drag handling.
|
||||
- Hover preview position should come from the node's actual projected position, not from `node.x` / `node.y` percentages alone.
|
||||
|
||||
Expected file direction:
|
||||
|
||||
- New file: `packages/graph-engine/src/render/geometry.ts`.
|
||||
- Export through `packages/graph-engine/src/render/index.ts` only for tested public helpers.
|
||||
|
||||
### 3. Layout / Hit Testing Module
|
||||
|
||||
Owns layout outputs that are not directly about drawing.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Keep the current static and live simulation layout behavior.
|
||||
- Produce or update node world positions.
|
||||
- Provide one explicit hit-test classification path for nodes, edges, community washes, minimap, and UI blockers.
|
||||
- Provide hit-test candidates for nodes, edges, and community washes.
|
||||
- Compute drag influence weights if connected nodes should follow the dragged node.
|
||||
- Build a spatial index later only if profiling shows DOM/classification or O(n) hit testing is a real bottleneck.
|
||||
|
||||
Rules:
|
||||
|
||||
- Layout does not know DOM elements.
|
||||
- Layout does not know hover cards, drawers, or toolbar controls.
|
||||
- The first slice may keep DOM `closest(...)` target classification as a compatibility layer, but it must be centralized and tested.
|
||||
- If graph-owned hit testing is added, it stores graph-world positions and sizes, not screen guesses.
|
||||
- Hit testing must have one owned path so the renderer does not keep inventing event target rules.
|
||||
|
||||
Research references:
|
||||
|
||||
- Logseq uses a simple grid spatial index for fast hit testing.
|
||||
- Graphify's community pipeline shows that deterministic community IDs and remapping matter, but llm-wiki's current Jaccard tracking is already strong and should be preserved.
|
||||
|
||||
Expected file direction:
|
||||
|
||||
- Keep `packages/graph-engine/src/sim/index.ts` for force simulation for now.
|
||||
- Add `packages/graph-engine/src/render/hit-testing.ts` or `packages/graph-engine/src/render/gestures.ts` for the centralized compatibility classifier.
|
||||
- Add `packages/graph-engine/src/layout/spatial-index.ts` only after measured need.
|
||||
- Do not migrate the whole layout package until the interaction state boundary is stable.
|
||||
|
||||
### 4. Gestures Module
|
||||
|
||||
Owns interpretation of pointer and wheel events.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Decide whether a wheel event should zoom the graph or be ignored for a UI control.
|
||||
- Decide whether pointerdown starts blank-canvas pan, node drag, minimap interaction, or no graph gesture.
|
||||
- Track drag threshold so click and drag do not conflict.
|
||||
- Track node grab offset so the node does not snap to its center.
|
||||
- Route node drag targets through Geometry and Simulation Bridge.
|
||||
- Route blank pan deltas through Viewport.
|
||||
- Handle double-click reset.
|
||||
|
||||
Rules:
|
||||
|
||||
- Nodes and community washes allow wheel zoom to pass through to the viewport.
|
||||
- Nodes block blank-canvas panning because pointer drag means node drag.
|
||||
- Community washes block blank-canvas panning/click only when the interaction is selecting a community; they must not block wheel zoom.
|
||||
- Search, toolbar, drawer, legend, minimap, and text editing controls block graph gestures unless that control explicitly owns a graph gesture.
|
||||
|
||||
Expected file direction:
|
||||
|
||||
- New file: `packages/graph-engine/src/render/gestures.ts`.
|
||||
- `static-renderer.ts` wires DOM elements to gesture handlers but does not implement gesture math.
|
||||
|
||||
### 5. Renderer Module
|
||||
|
||||
Owns DOM/SVG painting.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Render nodes, edges, community washes, minimap, labels, and density states from graph state.
|
||||
- Apply viewport transforms from the camera module.
|
||||
- Update DOM incrementally where needed.
|
||||
- Delegate hover preview and floating UI placement to the overlays module.
|
||||
|
||||
Rules:
|
||||
|
||||
- Renderer does not decide whether a pointer event is a drag, click, or pan.
|
||||
- Renderer does not convert screen coordinates except through Geometry.
|
||||
- Renderer does not own hidden selection, hover, drag, or focus state.
|
||||
- Renderer remains DOM+SVG for this refactor.
|
||||
|
||||
Expected file direction:
|
||||
|
||||
- Keep `packages/graph-engine/src/render/static-renderer.ts` as the shell during migration.
|
||||
- Extract focused helpers only after state, geometry, and gestures have tests.
|
||||
- Do not switch to Canvas or WebGL in this phase.
|
||||
|
||||
### 6. Overlays Module
|
||||
|
||||
Owns floating UI placement and community visual shapes.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Position hover preview near the node's actual screen position.
|
||||
- Position edge hover preview near the projected edge midpoint.
|
||||
- Keep hover previews inside the available graph viewport.
|
||||
- Reposition previews when viewport changes, graph positions change, drawer width changes, or density mode changes.
|
||||
- Compute community wash shapes from current world positions.
|
||||
- Apply bounded community wash deformation.
|
||||
|
||||
Rules:
|
||||
|
||||
- Hover preview is UI overlay, not graph content. It should not scale with the graph content layer.
|
||||
- Hover preview must not change world position or pin state.
|
||||
- Community wash is a visual region for community membership, not a hard drag boundary.
|
||||
- Community wash can stretch, but one far dragged node cannot make it consume the whole canvas.
|
||||
|
||||
Expected file direction:
|
||||
|
||||
- New file: `packages/graph-engine/src/render/overlays.ts`.
|
||||
- Move `positionHoverPreview`, `positionEdgeHoverPreview`, and community wash geometry helpers into focused helpers.
|
||||
- Keep visual styles in the renderer CSS for now.
|
||||
|
||||
### 7. Simulation Bridge
|
||||
|
||||
Owns the boundary between UI drag and force simulation.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Start drag with current node world position.
|
||||
- Apply drag target world position.
|
||||
- Preserve pointer grab offset.
|
||||
- Let nearby nodes respond according to the existing low-heat simulation.
|
||||
- Freeze far nodes while dragging, as today.
|
||||
- End drag and persist pin.
|
||||
- Unpin on double-click.
|
||||
|
||||
Rules:
|
||||
|
||||
- Simulation should receive world-space target positions.
|
||||
- Simulation may constrain graph layout only where layout rules require it.
|
||||
- Pointer conversion must not be the place where layout constraints are applied.
|
||||
- If node world bounds are needed, they belong to layout constraints, not projection conversion.
|
||||
|
||||
Expected file direction:
|
||||
|
||||
- Keep core force simulation in `packages/graph-engine/src/sim/index.ts`.
|
||||
- Add a render-side bridge near the renderer, likely `packages/graph-engine/src/render/simulation-bridge.ts`, only if extracting from `static-renderer.ts` materially improves clarity.
|
||||
|
||||
### 8. Engine Coordinator
|
||||
|
||||
Owns orchestration between host API, graph runtime state, layout, viewport, gestures, renderer, and persistence. This may start as a cleaned-up part of the existing renderer entrypoint; it does not need to become a large new facade class on day one.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Preserve the current public `createGraphEngine` style API.
|
||||
- Register gesture intent handlers.
|
||||
- Decide how graph intents mutate state.
|
||||
- Call simulation, viewport, renderer, overlay, and host callbacks in the correct order.
|
||||
- Keep workbench and offline Skill output on the same engine behavior.
|
||||
|
||||
Rules:
|
||||
|
||||
- The coordinator is allowed to coordinate modules.
|
||||
- Other modules should not import across layers just to "reach" behavior.
|
||||
- Host callbacks receive semantic events such as open page, selection changed, pin persisted, and focus changed.
|
||||
- Do not move workbench drawer ownership into graph-engine.
|
||||
|
||||
## Community Wash Behavior
|
||||
|
||||
Community wash represents the visible region of a community. It is not a fence and it must not imply that dragging changed community membership.
|
||||
|
||||
Desired behavior:
|
||||
|
||||
1. A node can be dragged outside the current wash.
|
||||
2. The wash does not block drag, wheel, or pan behavior.
|
||||
3. The wash remains visually stable enough that users do not read layout changes as semantic membership changes.
|
||||
4. If the wash responds to dragged or pinned outliers, that response is capped and testable.
|
||||
5. Community membership remains data-driven; dragging does not move a page to another community.
|
||||
|
||||
First-slice rule:
|
||||
|
||||
- Keep community wash as a soft background region around the community's core cluster.
|
||||
- Let dragged nodes leave the wash.
|
||||
- Do not require the wash to fully include every outlier.
|
||||
- Prefer a stable wash plus selected/dragged node affordance over aggressive stretching.
|
||||
- Wheel always passes through community washes.
|
||||
- Pointerdown on a wash prepares a community click. Pointerup below the drag threshold enters community focus/selection. Movement over the threshold cancels the community click and may start canvas pan.
|
||||
|
||||
Optional deformation rule, only after fixtures prove it helps:
|
||||
|
||||
- Compute a core hull/ellipse from the densest majority of the community's nodes.
|
||||
- Include pinned or dragged outlier nodes as limited external influence points.
|
||||
- Apply a capped expansion factor to the core region.
|
||||
- If an outlier exceeds the cap, represent it through a selected-node affordance or limited directional hint rather than full bounding-box expansion.
|
||||
- Keep opacity stable so a stretched wash does not overpower the map.
|
||||
|
||||
Practical constraints:
|
||||
|
||||
- Minimum wash size remains similar to today so small communities remain visible.
|
||||
- The first slice must define numeric caps against the current world size before implementing deformation.
|
||||
- A candidate default cap is: wash width <= 38% of world width and wash height <= 42% of world height, unless a fixture proves this too tight.
|
||||
- Maximum outlier influence should be capped per axis and must not cause a single dragged node to consume the canvas.
|
||||
- The cap must be testable with deterministic fixtures.
|
||||
|
||||
Fixture requirements:
|
||||
|
||||
- Small community with two nodes.
|
||||
- Dense community with one dragged outlier.
|
||||
- Dense community with one pinned outlier.
|
||||
- Multiple outliers in different directions.
|
||||
- Community focus view with a dragged node outside the core wash.
|
||||
|
||||
This keeps the map truthful first. Visual deformation is allowed only when it improves clarity without implying semantic membership changed.
|
||||
|
||||
## Interaction Contracts
|
||||
|
||||
### Wheel Zoom
|
||||
|
||||
- Works over blank graph.
|
||||
- Works over nodes.
|
||||
- Works over community washes.
|
||||
- Does not trigger over search, toolbar, drawer, legend, minimap controls, or text editing controls.
|
||||
- Zoom anchor is the pointer location in screen space.
|
||||
|
||||
### Blank Canvas Pan
|
||||
|
||||
- Pointerdown on blank graph prepares a pan.
|
||||
- Movement over threshold becomes pan.
|
||||
- Pointerup without movement is a blank click.
|
||||
- Blank click closes transient UI first.
|
||||
- In community focus, blank click can retreat from the focused view after transient UI is handled.
|
||||
|
||||
### Node Drag
|
||||
|
||||
- Pointerdown on node prepares node drag.
|
||||
- Movement over threshold becomes drag.
|
||||
- Node stays under the grabbed pointer point.
|
||||
- Drag target is computed by screen-to-world projection using the current viewport.
|
||||
- Drag can move outside the current community wash.
|
||||
- Drag does not accidentally open the drawer.
|
||||
- Pointerup ends drag and persists a pin.
|
||||
|
||||
### Node Click
|
||||
|
||||
- Click opens the reading drawer.
|
||||
- Shift-click toggles manual selection.
|
||||
- Click and drag are disambiguated by pointer movement threshold.
|
||||
|
||||
### Hover Preview
|
||||
|
||||
- Opens after the existing hover delay.
|
||||
- Uses the current projected node/edge screen position.
|
||||
- Repositions on viewport commit and motion frame while open.
|
||||
- Stays inside the graph viewport and avoids drawer overlap through available bounds.
|
||||
- Does not block node dragging or wheel zoom.
|
||||
- Closes on pointer leave, drag start, node click, community focus change, graph data refresh, node removal, and Escape.
|
||||
- Wheel or pan while hovering should either keep the preview attached through projection or close it; it must not drift.
|
||||
- Touch devices must not hide unique information behind hover-only behavior.
|
||||
|
||||
### Community Click
|
||||
|
||||
- Clicking community wash enters community focus and opens the community selection state.
|
||||
- It does not prevent wheel zoom.
|
||||
- It does not create a drag boundary.
|
||||
|
||||
### Minimap
|
||||
|
||||
- First slice: minimap is a status/control blocker, not a full drag surface.
|
||||
- Main graph gestures do not leak through minimap.
|
||||
- Minimap viewport rectangle is derived from the same viewport state as the main graph.
|
||||
- Wheel over minimap is blocked unless a later design explicitly makes minimap wheel zoom a feature.
|
||||
- Click/drag minimap navigation is deferred unless required by the existing workbench behavior.
|
||||
|
||||
### Edge Interaction
|
||||
|
||||
- Edge hover may show relation preview at the projected edge midpoint.
|
||||
- Edge click is a no-op unless the existing host behavior already selects relation details.
|
||||
- Node hit targets have priority over nearby edge hit targets.
|
||||
- Wheel over edges follows the main graph wheel rule.
|
||||
- Edge preview content must handle missing relation details without opening an empty floating card.
|
||||
|
||||
### Keyboard and Accessibility
|
||||
|
||||
- Tab reaches graph controls, search, minimap if interactive, and visible node controls in a predictable order.
|
||||
- Enter on a focused node opens the drawer or built-in offline reader.
|
||||
- Space toggles manual selection where selection is available.
|
||||
- Escape closes hover preview first, then drawer/reader, then exits community focus.
|
||||
- `+` and `-` zoom around the graph center; `0` resets view.
|
||||
- Arrow keys pan the viewport when the graph canvas has focus.
|
||||
- Focus rings remain visible on nodes and controls.
|
||||
- Nodes and community controls expose readable labels; status changes such as focus change, drawer open, and pin saved should be announced where the host supports it.
|
||||
|
||||
### Touch
|
||||
|
||||
- Tap node opens the drawer or built-in offline reader.
|
||||
- Tap community wash enters community focus.
|
||||
- One-finger drag on blank graph pans.
|
||||
- One-finger drag on node drags the node after the same threshold rule.
|
||||
- Pinch zoom is optional; visible zoom controls must remain usable.
|
||||
- Long-press may open preview, but no core information may be hover-only.
|
||||
- Touch targets should remain at least 44 CSS pixels where practical.
|
||||
- Pointer cancel ends active drag/pan cleanly without committing a false click.
|
||||
|
||||
### Empty, Loading, and Error States
|
||||
|
||||
- Loading graph: show a graph loading state and do not attach graph gestures until data is ready.
|
||||
- Empty graph: show an empty state with no minimap interaction.
|
||||
- No edges: render nodes and make relation hover/click unavailable.
|
||||
- No community members: community focus should fall back to global view or show a clear empty community state.
|
||||
- Failed graph build or layout: show recovery copy and keep the rest of the workbench usable.
|
||||
- Drawer content loading/error remains host-owned in workbench and reader-owned in offline mode.
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. Graph data enters `buildRenderableGraph`.
|
||||
2. GraphRuntimeState stores committed positions, viewport, graph selection/focus, hover, pins, and active gesture state.
|
||||
3. Layout and simulation expose world positions for nodes, edges, and communities.
|
||||
4. Geometry projects world positions to layer/screen/minimap positions.
|
||||
5. Renderer applies DOM/SVG styles from state and projected positions.
|
||||
6. User input enters Gestures.
|
||||
7. Gestures classify the event target and emit an intent such as zoom, pan, drag node, hover node, click node, or click community.
|
||||
8. The engine coordinator handles the intent and mutates GraphRuntimeState.
|
||||
9. Node drag goes through Simulation Bridge using world-space targets.
|
||||
10. Motion frames update world positions for visible nodes and rebuild hit-test data as needed.
|
||||
11. Overlays recompute hover and wash positions from current world positions and viewport.
|
||||
12. Drag end persists pins by wiki-relative path.
|
||||
|
||||
No component should infer a coordinate space from a raw number. The caller must know whether it is using world, layer, screen, or minimap coordinates.
|
||||
|
||||
Renderer code should not directly decide graph intent. Gesture code should not directly mutate DOM. Layout code should not know UI controls. These boundaries are the core regression guard.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Graph Runtime State:
|
||||
|
||||
- State updates are explicit and observable through one subscription path.
|
||||
- Hover, selection, focus, drag, viewport, and pins do not live in separate hidden stores.
|
||||
- Renderer receives a state snapshot and does not mutate graph state directly.
|
||||
|
||||
Viewport:
|
||||
|
||||
- Zoom around pointer preserves the pointer anchor.
|
||||
- Pan changes viewport translation without changing scale.
|
||||
- Fit/center functions preserve intended scale limits.
|
||||
- Resize keeps selected/focused anchor visually comfortable.
|
||||
- Zoom level transitions are deterministic for the same scale values.
|
||||
|
||||
Geometry:
|
||||
|
||||
- World to screen and screen to world are inverse operations for common viewport states.
|
||||
- Projection works under pan and zoom.
|
||||
- Projection works when viewport host size changes.
|
||||
- Drag projection can represent pointer positions outside the currently visible world bounds without silent clamping.
|
||||
- World deltas convert to layer pixels correctly for non-1000px-wide viewports.
|
||||
|
||||
Gestures:
|
||||
|
||||
- Wheel is allowed over nodes and community washes.
|
||||
- Wheel is blocked over controls.
|
||||
- Pointer target classification separates node drag, blank pan, community click, minimap control, and UI controls.
|
||||
- Click vs drag threshold prevents accidental drawer opens after drag.
|
||||
- Gesture lock prevents new drag/pan/zoom starts during transitions.
|
||||
- Gesture handlers emit intents and do not mutate DOM directly.
|
||||
- First-slice gesture tests should use pure classifiers and fake event targets, avoiding new DOM test dependencies.
|
||||
|
||||
Hit Testing:
|
||||
|
||||
- Hit testing returns the expected node near a point.
|
||||
- Hit testing respects current world positions after drag.
|
||||
- Compatibility DOM-target classification is centralized and covered.
|
||||
- Spatial-index performance tests are deferred until spatial index work is explicitly triggered.
|
||||
|
||||
Overlays:
|
||||
|
||||
- Node hover preview uses projected node screen position.
|
||||
- Edge hover preview uses projected edge midpoint.
|
||||
- Hover preview remains inside graph bounds.
|
||||
- Hover preview repositions after viewport changes.
|
||||
|
||||
Community Wash:
|
||||
|
||||
- Wash does not block node drag or wheel zoom.
|
||||
- Wash has a maximum expansion cap for outliers if deformation is enabled.
|
||||
- Dragged outlier does not change community membership.
|
||||
- Wash remains deterministic for stable fixtures.
|
||||
|
||||
Simulation Bridge:
|
||||
|
||||
- Drag target is world-space.
|
||||
- Grab offset is preserved.
|
||||
- End drag persists the final position.
|
||||
- Double-click unpins without changing community membership.
|
||||
|
||||
Coordinator / Integration:
|
||||
|
||||
- A wheel DOM event becomes a zoom intent, then a viewport state change, then a renderer update.
|
||||
- A node pointer sequence below the drag threshold becomes a node click and opens the drawer.
|
||||
- A node pointer sequence above the drag threshold becomes node drag and does not open the drawer.
|
||||
- A community click enters community focus without blocking wheel zoom.
|
||||
- Keyboard zoom/reset and Escape behavior follow the accessibility contract.
|
||||
- Touch pointer cancellation does not commit a false click.
|
||||
|
||||
### Browser Verification
|
||||
|
||||
Run against the workbench at `localhost:5180`:
|
||||
|
||||
1. Open the graph view.
|
||||
2. Enter a community focus view.
|
||||
3. Wheel over blank graph, node, and community wash.
|
||||
4. Drag a node within the wash.
|
||||
5. Drag a node outside the wash.
|
||||
6. Confirm the node tracks the pointer without jumping.
|
||||
7. Confirm the wash does not block drag and does not grow without bound if deformation is enabled.
|
||||
8. Hover the dragged node before and after zoom.
|
||||
9. Open the right drawer and repeat hover and drag.
|
||||
10. Click node and confirm drawer opens.
|
||||
11. Click community wash and confirm community selection/focus still works.
|
||||
12. Pan the blank canvas and confirm minimap updates.
|
||||
13. Reset view and confirm graph returns to a stable full-graph view.
|
||||
|
||||
### Offline HTML Verification
|
||||
|
||||
Run against generated Skill/offline HTML after engine changes:
|
||||
|
||||
1. Build `@llm-wiki/graph-engine`.
|
||||
2. Run `scripts/build-graph-html.sh` on a fixture wiki.
|
||||
3. Open the generated HTML.
|
||||
4. Verify wheel zoom over blank graph, node, and community wash.
|
||||
5. Verify node drag, hover preview, localStorage pin persistence, theme toggle, and built-in reader behavior.
|
||||
6. Confirm offline output uses the built IIFE and does not rely on workbench-only React state.
|
||||
|
||||
### Regression Coverage
|
||||
|
||||
The final implementation should include tests for the three recent regressions:
|
||||
|
||||
- Wheel over community wash.
|
||||
- Wheel over node.
|
||||
- Dragged node under zoom follows the pointer.
|
||||
|
||||
It should also add tests for the two newly reported regressions:
|
||||
|
||||
- Node can be dragged outside the community wash.
|
||||
- Hover preview follows projected node position under pan/zoom/focus/drawer states.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The implementation should be phased to reduce risk while still being architectural. The first phase is a vertical slice: it fixes the current user-visible failures through the new coordinate and gesture boundaries instead of patching isolated event handlers.
|
||||
|
||||
0. Establish the regression and profiling baseline.
|
||||
- Add tests or repeatable browser probes for wheel over wash, wheel over node, drag under zoom, drag outside wash, hover after zoom, and hover after drawer resize.
|
||||
- Profile representative small, medium, and large graphs under pan, zoom, drag, hover, and live simulation before committing to any renderer-technology decision.
|
||||
- Record current workbench behavior and offline HTML behavior.
|
||||
|
||||
1. Establish geometry and coordinate compatibility.
|
||||
- Move projection helpers into Geometry.
|
||||
- Remove silent coordinate clamping from projection paths.
|
||||
- Define persisted pin range and layout clamping explicitly.
|
||||
- Keep old pin format compatible.
|
||||
- Add tests before behavior changes where practical.
|
||||
|
||||
2. Centralize gesture and hit-test classification for the first slice.
|
||||
- Extract wheel, pointer, drag, click, community click, hover, and minimap target classification from `static-renderer.ts`.
|
||||
- Keep DOM `closest(...)` target classification as a centralized compatibility layer unless graph-owned hit testing is needed immediately.
|
||||
- Make gesture handlers emit simple intents.
|
||||
- Use pure classifier tests with fake event targets; do not require jsdom unless approved.
|
||||
|
||||
3. Add the minimal graph runtime state needed by the slice.
|
||||
- Track viewport, hover target, selected/focused graph item, pins, active drag, grab offset, and gesture lock.
|
||||
- Do not move host drawer state, search state, toolbar state, or diff animation state unless required by the slice.
|
||||
- Preserve node click, drawer open, selection, focus, pin persistence, and reset behavior.
|
||||
|
||||
4. Fix overlays and drag through the shared path.
|
||||
- Hover and edge previews use projected screen positions.
|
||||
- Node drag uses unclamped screen-to-world projection plus explicit layout constraints.
|
||||
- Grab offset is preserved.
|
||||
- Dragging does not open the drawer.
|
||||
- Nodes can leave the current community wash.
|
||||
|
||||
5. Make community wash non-blocking and bounded.
|
||||
- Washes do not block dragging or wheel zoom.
|
||||
- Wash click has explicit threshold behavior.
|
||||
- First slice may keep the core wash stable instead of requiring full deformation.
|
||||
- Any deformation must use fixture-tested numeric caps.
|
||||
|
||||
6. Verify workbench and offline output.
|
||||
- Run engine unit tests.
|
||||
- Run workbench browser verification.
|
||||
- Build and inspect generated offline HTML.
|
||||
|
||||
7. Reassess follow-up architecture gates.
|
||||
- Add spatial index only if profiling proves hit testing is slow.
|
||||
- Add zoom-level/density-policy cleanup only if the first slice exposes drift.
|
||||
- Split renderer helpers after the interaction slice is stable.
|
||||
- Consider product-value graph work before continuing deeper plumbing.
|
||||
|
||||
Each phase should have tests before behavior changes where practical. Avoid mixing visual restyling with this refactor.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### First Slice Blocking Criteria
|
||||
|
||||
The first refactor slice is complete when:
|
||||
|
||||
- There is one documented path for world/screen/layer/minimap coordinate conversion.
|
||||
- Projection functions do not silently clamp drag targets.
|
||||
- Persisted pin compatibility is documented and preserved.
|
||||
- Wheel zoom works over blank graph, nodes, and community washes.
|
||||
- A dragged node stays visually attached to the pointer under pan, zoom, drawer resize, and community focus.
|
||||
- Nodes can be dragged outside community washes.
|
||||
- Dragging a node does not open the drawer.
|
||||
- Hover previews stay visually attached to nodes after pan, zoom, community focus, drag, and drawer open.
|
||||
- Community washes do not become hard drag fences.
|
||||
- Community wash click behavior is threshold-based and does not block wheel zoom.
|
||||
- Existing node click, community click, Shift selection, blank pan, double-click reset, and unpin behaviors still work.
|
||||
- Runtime state for viewport, hover, focus/selection, pins, and active gesture has one owner.
|
||||
- Gesture classification is centralized, even if the first implementation uses DOM target compatibility.
|
||||
- Unit tests cover projection, gesture classification, hover positioning, drag bounds, and pin compatibility.
|
||||
- Browser verification passes on the workbench.
|
||||
- Offline HTML verification passes against the built IIFE.
|
||||
|
||||
Measurable user-facing checks:
|
||||
|
||||
- Zoom changes when the wheel is used over blank graph, a node, and a community wash.
|
||||
- Drag start has no visible snap to another location.
|
||||
- The dragged node center remains within a small fixed pixel tolerance of the grabbed pointer offset.
|
||||
- Hover card anchor remains near the projected node/edge position after zoom and drawer resize.
|
||||
- Community wash width/height stays under the documented cap if deformation is enabled.
|
||||
- A pointer sequence that exceeds the drag threshold never fires node click.
|
||||
|
||||
### Follow-Up Criteria
|
||||
|
||||
The broader architecture is complete when:
|
||||
|
||||
- `static-renderer.ts` no longer owns ad hoc coordinate formulas for drag, hover, wheel, minimap, and community wash behavior.
|
||||
- Renderer code no longer directly interprets graph gestures.
|
||||
- Gesture code emits graph intents and does not directly mutate DOM.
|
||||
- Renderer helper extraction has reduced the renderer's responsibility without changing public API behavior.
|
||||
- Hit testing has one graph-owned path if DOM-target compatibility becomes insufficient.
|
||||
- Spatial index exists only if profiling justified it.
|
||||
- Zoom level / density behavior has one policy source if this becomes necessary for user-facing quality.
|
||||
- Keyboard and touch contracts are implemented or explicitly deferred with product approval.
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
Risk: Moving coordinate logic can regress current graph navigation.
|
||||
Mitigation: Keep viewport unit tests broad and run browser verification before commit.
|
||||
|
||||
Risk: Community wash deformation could make the visual design noisy.
|
||||
Mitigation: First make washes non-blocking and stable. Add deformation only behind numeric caps and outlier fixtures.
|
||||
|
||||
Risk: `static-renderer.ts` extraction may become a broad refactor.
|
||||
Mitigation: Establish Geometry, gesture classification, and minimal runtime state first. Split renderer helpers only after behavior is under tests.
|
||||
|
||||
Risk: Workbench and offline HTML diverge.
|
||||
Mitigation: Keep the behavior in `@llm-wiki/graph-engine`, verify workbench browser behavior, and verify generated offline HTML from the built IIFE.
|
||||
|
||||
Risk: The refactor becomes a renderer rewrite.
|
||||
Mitigation: Profile first. Keep DOM+SVG unless profiling proves it is the actual bottleneck. Do not adopt Canvas, WebGL, Pixi.js, or a new rendering dependency in the first slice.
|
||||
|
||||
Risk: The state layer turns into a general app store.
|
||||
Mitigation: Keep runtime state graph-local and limited to the interaction slice. Host business and drawer state stay outside graph-engine.
|
||||
|
||||
Risk: The architecture work ships without user-visible improvement.
|
||||
Mitigation: First-slice acceptance is based on visible graph behavior: no drag jump, no hover drift, no accidental drawer open, consistent wheel zoom, and non-blocking community washes.
|
||||
|
||||
## Decision
|
||||
|
||||
Use a gated Graph Interaction Architecture approach:
|
||||
|
||||
- Keep the current graph-engine strengths: DOM+SVG rendering, live d3 simulation, pin persistence, density modes, minimap, Jaccard community tracking, and shared workbench/offline behavior.
|
||||
- First land the vertical slice: Geometry, centralized gesture classification, minimal graph runtime state, shared overlay placement, explicit pin compatibility, and workbench/offline verification.
|
||||
- Add broader architecture only when it removes duplicated behavior or passes a measured trigger: renderer helper splitting, graph-owned hit testing, spatial index, density policy cleanup, and deeper coordinator extraction.
|
||||
- Do not choose the conservative patch route; it cannot prevent the next interaction regression.
|
||||
- Do not choose the aggressive renderer rewrite route; WebGL/Canvas would add risk before the current scale demands it.
|
||||
|
||||
This is intentionally more than a bug patch and less than a rendering-technology migration. It fixes the architectural source of the current regression class by forcing the first visible interaction path through shared coordinate, gesture, and runtime-state rules before expanding the architecture.
|
||||
@@ -0,0 +1,577 @@
|
||||
# Graph Six-Layer Architecture Design
|
||||
|
||||
Date: 2026-06-16
|
||||
Status: Ready for user review
|
||||
Branch context: continue from `codex/graph-interaction-architecture`; do not merge the current branch into `main` before the known interaction gaps are resolved.
|
||||
|
||||
## Purpose
|
||||
|
||||
This design defines the next graph architecture step for llm-wiki. The goal is not to patch three visible bugs one by one. The goal is to make the graph a self-owned, testable interaction module whose input, camera, state, rendering, layout, and host integration boundaries are explicit.
|
||||
|
||||
The current graph architecture already improved coordinate, runtime state, hover, community wash, and browser regression coverage. However, recent real use exposed that the graph still does not fully own its interaction surface:
|
||||
|
||||
- Trackpad zoom inside the graph can become browser page zoom.
|
||||
- A fast node drag release can fail to persist the released position.
|
||||
- Blank-canvas pan can select toolbar text through native browser selection.
|
||||
|
||||
These are symptoms of the same class of problem: browser default behavior, renderer event handling, DOM hit order, graph state, and graph camera rules are not yet fully owned by a single graph interaction architecture.
|
||||
|
||||
## Core Decision
|
||||
|
||||
Use a complete six-layer graph architecture, implemented through continuous takeover rather than a one-shot rewrite.
|
||||
|
||||
The chosen route is:
|
||||
|
||||
1. Keep DOM + SVG rendering for now.
|
||||
2. Do not switch to WebGL/Pixi.js/Three.js in this project phase.
|
||||
3. Build the six-layer architecture as the long-term foundation.
|
||||
4. Make each phase truly take over an old responsibility.
|
||||
5. Do not allow new architecture interfaces and old renderer-owned behavior to coexist indefinitely.
|
||||
|
||||
WebGL is not rejected forever. It is deferred until graph scale or profiling proves that DOM + SVG cannot meet performance needs. The current issues are interaction ownership problems, not rendering throughput problems.
|
||||
|
||||
## Goals
|
||||
|
||||
- The graph owns pointer, wheel, trackpad, touch, and keyboard interaction inside its own graph surface.
|
||||
- Browser default behavior cannot interfere with graph gestures except in explicitly allowed text/input/reader areas.
|
||||
- Workbench graph and offline HTML graph pass the same core interaction contracts.
|
||||
- GraphViewport is the only coordinate conversion authority.
|
||||
- GraphGestures is the only raw input and intent classification authority.
|
||||
- GraphState is the only graph-local runtime state authority.
|
||||
- GraphRenderer draws graph UI but does not decide gesture meaning.
|
||||
- SpatialIndex becomes the final hit-testing authority for nodes, edges, communities, and blank graph space.
|
||||
- `static-renderer.ts` is reduced to a compatibility shell or replaced by focused modules.
|
||||
- Known bugs and adjacent interaction risks are covered by tests or repeatable browser verification before the work is considered complete.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not migrate to WebGL, Pixi.js, Three.js, or Canvas in this phase.
|
||||
- Do not do a one-shot renderer rewrite that deletes all existing behavior at once.
|
||||
- Do not introduce free-form lasso selection, right-click menus, graph editing, or complex multi-touch editing as new product features.
|
||||
- Do not make community membership editable through drag. Drag changes layout position only.
|
||||
- Do not accept "new interfaces exist but old interaction logic still owns behavior" as complete.
|
||||
- Do not treat workbench verification as enough if generated offline HTML regresses.
|
||||
|
||||
## Hard Problems To Resolve First
|
||||
|
||||
These are not optional follow-ups. They are the first acceptance group:
|
||||
|
||||
1. Trackpad zoom inside graph-owned space zooms the graph, not the browser page.
|
||||
2. Wheel or trackpad zoom over nodes, edges, community washes, and blank graph space uses the same graph zoom path.
|
||||
3. Fast node drag release persists the final released position.
|
||||
4. Blank-canvas pan never creates native text selection in toolbar, title, node labels, or surrounding page chrome.
|
||||
5. Nodes can be dragged outside community washes.
|
||||
6. Community washes remain soft visual regions, not drag fences.
|
||||
7. Workbench and offline HTML both pass the same behavior.
|
||||
|
||||
## Interaction Risk Audit
|
||||
|
||||
Phase 0 must create and maintain an interaction risk audit. The audit is a hard gate before large module migration. It must include, at minimum:
|
||||
|
||||
- Trackpad pinch and trackpad wheel behavior.
|
||||
- Browser page zoom prevention inside graph-owned space.
|
||||
- Native text selection prevention during graph gestures.
|
||||
- Native page scroll prevention or reset behavior inside graph-owned space.
|
||||
- Fast drag release, pointer cancel, lost pointer capture, and Escape cancellation.
|
||||
- Click versus drag threshold behavior for node, community wash, edge, and blank graph.
|
||||
- Toolbar, search, legend, minimap, drawer, and reader boundaries.
|
||||
- Hover preview anchoring after zoom, pan, drag, resize, drawer open, and community focus.
|
||||
- Data refresh while dragging.
|
||||
- Keyboard behavior for Escape, Tab, Enter, Space, arrow keys, plus, minus, and zero.
|
||||
- Touch behavior for one-finger pan, node drag, pointer cancellation, and visible fallback controls.
|
||||
- Workbench and offline HTML parity.
|
||||
|
||||
The audit must name the expected behavior, the owner layer, and the verification method for each item.
|
||||
|
||||
## Six Layers
|
||||
|
||||
### 1. GraphData
|
||||
|
||||
GraphData describes graph truth:
|
||||
|
||||
- Nodes.
|
||||
- Edges.
|
||||
- Communities.
|
||||
- Source metadata.
|
||||
- Node and edge types.
|
||||
- Weights and confidence.
|
||||
|
||||
GraphData does not know:
|
||||
|
||||
- Screen position.
|
||||
- Current viewport.
|
||||
- Hover state.
|
||||
- Pointer state.
|
||||
- Drawer state.
|
||||
- DOM elements.
|
||||
|
||||
### 2. GraphLayout
|
||||
|
||||
GraphLayout converts graph truth into graph structure that can be drawn and interacted with:
|
||||
|
||||
- Node world positions.
|
||||
- Edge routes.
|
||||
- Community wash geometry.
|
||||
- Pinned positions.
|
||||
- Simulation state.
|
||||
- Drag influence behavior.
|
||||
- SpatialIndex output.
|
||||
|
||||
GraphLayout owns SpatialIndex because hit testing depends on layout world positions, not the current DOM stacking order.
|
||||
|
||||
GraphLayout does not know:
|
||||
|
||||
- Browser events.
|
||||
- React/workbench drawer behavior.
|
||||
- CSS DOM details.
|
||||
- Screen pixels, except through explicit viewport inputs where needed for culling.
|
||||
|
||||
### 3. GraphViewport
|
||||
|
||||
GraphViewport is the graph camera:
|
||||
|
||||
- World-to-screen conversion.
|
||||
- Screen-to-world conversion.
|
||||
- Zoom around pointer.
|
||||
- Pan.
|
||||
- Fit graph.
|
||||
- Center on node/community.
|
||||
- Resize anchoring.
|
||||
- Minimap viewport projection.
|
||||
- Zoom levels and density thresholds.
|
||||
|
||||
Rules:
|
||||
|
||||
- No other layer hand-rolls coordinate conversion.
|
||||
- Drag target projection must go through GraphViewport.
|
||||
- Hover preview anchors must go through GraphViewport.
|
||||
- Minimap projection must go through GraphViewport.
|
||||
- GraphViewport may constrain camera movement, but must not hide drag target constraints inside coordinate conversion.
|
||||
|
||||
### 4. GraphRenderer
|
||||
|
||||
GraphRenderer draws the graph:
|
||||
|
||||
- Node DOM.
|
||||
- Edge SVG.
|
||||
- Community washes.
|
||||
- Minimap.
|
||||
- Hover and edge preview cards.
|
||||
- Offline reader panel.
|
||||
- Workbench-compatible graph surface.
|
||||
- Toolbar visual state.
|
||||
- Density display variants.
|
||||
|
||||
GraphRenderer is not allowed to decide:
|
||||
|
||||
- Whether a pointer sequence is a click, drag, pan, or zoom.
|
||||
- Whether browser default behavior should run.
|
||||
- Which graph object a pointer intends to hit once SpatialIndex takes over.
|
||||
- What state is selected, hovered, focused, dragged, or pinned.
|
||||
|
||||
GraphRenderer can expose DOM refs or measured sizes to GraphFacade, but it cannot become the interaction owner.
|
||||
|
||||
### 5. GraphGestures
|
||||
|
||||
GraphGestures is the raw input and intent layer. It attaches to the graph root and translates browser events into graph intents.
|
||||
|
||||
Inputs:
|
||||
|
||||
- `pointerdown`, `pointermove`, `pointerup`.
|
||||
- `pointercancel`, `lostpointercapture`.
|
||||
- `wheel`, including trackpad wheel and browser pinch-like wheel events.
|
||||
- Keyboard events while graph focus is active.
|
||||
- Touch/pointer events where available.
|
||||
|
||||
Outputs:
|
||||
|
||||
- `zoomGraph`.
|
||||
- `panCanvasStart`, `panCanvasMove`, `panCanvasEnd`.
|
||||
- `dragNodeStart`, `dragNodeMove`, `dragNodeEnd`, `dragNodeCancel`.
|
||||
- `clickNode`.
|
||||
- `clickCommunity`.
|
||||
- `clickBlank`.
|
||||
- `hoverNode`, `hoverEdge`, `hoverLeave`.
|
||||
- `keyboardCommand`.
|
||||
- `blockNativeDefault`.
|
||||
|
||||
Rules:
|
||||
|
||||
- GraphGestures is the only owner of click/drag threshold policy.
|
||||
- GraphGestures owns browser default prevention for graph gestures.
|
||||
- GraphGestures classifies graph controls as blockers or explicit graph controls.
|
||||
- GraphGestures must not directly mutate DOM or persist pins.
|
||||
- GraphGestures must include final pointer coordinates on drag end so fast release can commit the right position.
|
||||
|
||||
### 6. GraphFacade
|
||||
|
||||
GraphFacade coordinates the system and preserves public API compatibility.
|
||||
|
||||
It owns:
|
||||
|
||||
- `createGraphEngine` and public graph-engine methods.
|
||||
- Workbench and offline HTML compatibility.
|
||||
- GraphData ingestion into GraphLayout.
|
||||
- GraphGestures intent handling.
|
||||
- GraphViewport state updates.
|
||||
- GraphRenderer update calls.
|
||||
- Host callbacks such as open page, selection change, pin persistence, ask action, and drag state change.
|
||||
|
||||
GraphFacade is the only layer that knows both internal graph modules and host callbacks.
|
||||
|
||||
## Shared GraphState
|
||||
|
||||
GraphState is the single graph-local runtime state owner.
|
||||
|
||||
It stores:
|
||||
|
||||
- Current viewport.
|
||||
- Committed node positions.
|
||||
- Pending simulation proposal positions.
|
||||
- Pin snapshot.
|
||||
- Hover target.
|
||||
- Selected graph item.
|
||||
- Focused community or graph item.
|
||||
- Active gesture.
|
||||
- Grab offset.
|
||||
- Gesture lock.
|
||||
- Toolbar/search state only if moving it removes duplicate rules.
|
||||
|
||||
GraphState does not store:
|
||||
|
||||
- Workbench drawer state.
|
||||
- Host page content.
|
||||
- External chat state.
|
||||
- Knowledge base selection outside the graph engine.
|
||||
|
||||
No layer may create a hidden parallel state island for hover, drag, selection, focus, pins, or viewport.
|
||||
|
||||
## Browser Default Behavior Policy
|
||||
|
||||
Graph-owned space must be explicit.
|
||||
|
||||
Graph-owned surface:
|
||||
|
||||
- Blank graph background.
|
||||
- Nodes.
|
||||
- Edges.
|
||||
- Community washes.
|
||||
- Minimap, unless explicitly treated as a blocker.
|
||||
- Graph toolbar buttons, as graph controls.
|
||||
|
||||
Graph-owned surface must prevent browser defaults that conflict with graph interaction:
|
||||
|
||||
- Native text selection during pan and drag.
|
||||
- Browser page zoom for graph wheel/trackpad zoom.
|
||||
- Native drag selection.
|
||||
- Native page scroll inside graph root unless explicitly permitted.
|
||||
|
||||
Allowed browser-default areas:
|
||||
|
||||
- Search text input.
|
||||
- Reader/article body where text selection is useful.
|
||||
- Editable controls.
|
||||
- Scrollable drawer/reader content where the user clearly interacts with content rather than graph canvas.
|
||||
|
||||
Controls such as toolbar buttons should not be text-selectable during graph panning. They can block graph gestures, but they must not let browser selection leak into graph gestures.
|
||||
|
||||
## SpatialIndex Scope
|
||||
|
||||
SpatialIndex is part of this architecture, not an optional enhancement.
|
||||
|
||||
Implementation policy:
|
||||
|
||||
1. Define SpatialIndex shape in the GraphLayout layer.
|
||||
2. Test SpatialIndex independently with node, edge, community, and blank-space cases.
|
||||
3. Use SpatialIndex first as an audit/check path alongside DOM hit testing.
|
||||
4. In Phase 4, SpatialIndex must become the real hit-testing source for:
|
||||
- Node hits.
|
||||
- Edge hits.
|
||||
- Community wash hits.
|
||||
- Blank canvas hits.
|
||||
5. After takeover, old DOM hit-order classification must be deleted or marked as a compatibility fallback with a removal gate.
|
||||
|
||||
Completion rule:
|
||||
|
||||
- Interface-only SpatialIndex is not final completion.
|
||||
- SpatialIndex is final only after it owns hit testing and old DOM hit classification no longer drives graph behavior.
|
||||
|
||||
## Community Wash Policy
|
||||
|
||||
Community washes are soft visual regions, not semantic boundaries.
|
||||
|
||||
Rules:
|
||||
|
||||
- A node can be dragged outside its current community wash.
|
||||
- Dragging does not change the node's community membership.
|
||||
- A wash may respond to dragged or pinned outliers with bounded deformation.
|
||||
- One far-away node cannot make a wash consume the canvas.
|
||||
- Wash response must be capped per axis and covered by fixtures.
|
||||
- Dense community plus one dragged outlier must remain readable.
|
||||
- Convex hull style washes are a possible later upgrade, not part of this first implementation target.
|
||||
|
||||
## Continuous Takeover Phases
|
||||
|
||||
### Phase 0: Interaction Risk Audit
|
||||
|
||||
Create the audit before large migration. Add tests or repeatable browser probes for each known risk.
|
||||
|
||||
Required output:
|
||||
|
||||
- Risk list.
|
||||
- Owner layer per risk.
|
||||
- Workbench verification path.
|
||||
- Offline HTML verification path.
|
||||
- Existing test coverage status.
|
||||
- Missing coverage list.
|
||||
|
||||
### Phase 1: GraphGestures Takes Over Input
|
||||
|
||||
GraphGestures becomes the only raw event classification path.
|
||||
|
||||
Must resolve:
|
||||
|
||||
- Trackpad zoom as graph zoom inside graph space.
|
||||
- Prevent browser page zoom inside graph space.
|
||||
- Prevent native text selection during canvas pan and node drag.
|
||||
- Fast drag release persists final position.
|
||||
- Pointer cancel and lost pointer capture cleanly end active gestures.
|
||||
- Toolbar/search/drawer/minimap/reader boundaries are explicit.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Workbench and offline HTML pass the hard problem group.
|
||||
- No renderer-local raw input classification remains active outside compatibility wrappers.
|
||||
|
||||
### Phase 2: GraphViewport And GraphState Close The Loops
|
||||
|
||||
Move all camera and runtime state ownership into GraphViewport and GraphState.
|
||||
|
||||
Must resolve:
|
||||
|
||||
- Drag target uses one screen-to-world path.
|
||||
- Hover anchors use one world-to-screen path.
|
||||
- Minimap uses the same viewport state.
|
||||
- Resize keeps selected/focused anchor stable.
|
||||
- Selection, focus, hover, active drag, and pins have one graph-local owner.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Searching, focusing, resetting, panning, dragging, and hover positioning remain consistent after zoom and drawer resize.
|
||||
- No hidden renderer-only state island drives behavior.
|
||||
|
||||
### Phase 3: GraphRenderer Split And Slimming
|
||||
|
||||
Split rendering responsibilities into focused modules.
|
||||
|
||||
Suggested modules:
|
||||
|
||||
- Node renderer.
|
||||
- Edge renderer.
|
||||
- Community wash renderer.
|
||||
- Minimap renderer.
|
||||
- Overlay renderer.
|
||||
- Toolbar renderer.
|
||||
- Offline reader renderer.
|
||||
- Density display renderer.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Renderer modules receive state and draw.
|
||||
- Renderer modules do not classify raw gestures.
|
||||
- `static-renderer.ts` is reduced to orchestration/compatibility only, or replaced by module composition.
|
||||
|
||||
### Phase 4: SpatialIndex Takes Over Hit Testing
|
||||
|
||||
SpatialIndex becomes the hit-test source for graph objects.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Node, edge, community, and blank hits are driven by SpatialIndex.
|
||||
- DOM stacking order is no longer the source of graph intent.
|
||||
- Workbench and offline HTML produce matching hit behavior.
|
||||
- Old DOM hit classification is removed or isolated behind an explicitly temporary fallback with a removal gate.
|
||||
|
||||
### Phase 5: Community Wash Soft Boundary Completion
|
||||
|
||||
Finish bounded wash response and membership semantics.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Dragged outliers can influence wash shape within caps.
|
||||
- Wash never becomes a drag fence.
|
||||
- Wash never implies community membership has changed.
|
||||
- Dense and focused community fixtures prove readability.
|
||||
|
||||
### Phase 6: Old Logic Removal And Dual-Target Verification
|
||||
|
||||
Clean up old paths and verify both graph targets.
|
||||
|
||||
Required cleanup:
|
||||
|
||||
- Old renderer-owned pointer/wheel/keyboard/dblclick classification.
|
||||
- Old hand-rolled coordinate math.
|
||||
- Old hover position calculations that bypass GraphViewport.
|
||||
- Old DOM-order hit classification after SpatialIndex takeover.
|
||||
- Old hidden drag/hover/selection/focus state.
|
||||
- Old browser-default leaks.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Workbench browser verification passes.
|
||||
- Offline HTML browser verification passes.
|
||||
- Unit and integration tests pass.
|
||||
- `static-renderer.ts` no longer owns primary interaction or coordinate logic.
|
||||
- Any remaining compatibility shell has a documented reason and narrow responsibility.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Layer 1: Pure Logic Tests
|
||||
|
||||
Cover:
|
||||
|
||||
- GraphViewport world/screen round trips.
|
||||
- Zoom around pointer.
|
||||
- Resize anchoring.
|
||||
- GraphState snapshot/update behavior.
|
||||
- GraphGestures click/drag threshold.
|
||||
- Trackpad-like wheel intent.
|
||||
- Fast release drag end intent.
|
||||
- Pointer cancel and lost pointer capture.
|
||||
- SpatialIndex node, edge, community, blank hits.
|
||||
- Community wash cap logic.
|
||||
|
||||
### Layer 2: DOM Integration Tests
|
||||
|
||||
Cover:
|
||||
|
||||
- Graph root disables native selection during pan/drag.
|
||||
- Toolbar text is not selected during blank canvas pan.
|
||||
- Controls block graph gestures where appropriate.
|
||||
- Search and reader content still allow intended text/input behavior.
|
||||
- Renderer does not decide gesture meaning.
|
||||
- Hover and preview cards follow projected anchors.
|
||||
- DOM structure works for both light and dark themes.
|
||||
|
||||
### Layer 3: Real Browser Verification
|
||||
|
||||
Run against both:
|
||||
|
||||
- Workbench at local dev URL.
|
||||
- Generated offline HTML file.
|
||||
|
||||
Required paths:
|
||||
|
||||
- Trackpad-like zoom does not change browser page zoom.
|
||||
- Wheel over blank, node, edge, and community wash changes graph zoom.
|
||||
- Wheel over controls does not zoom graph.
|
||||
- Fast node drag and immediate release persists final position.
|
||||
- Blank pan does not create native text selection.
|
||||
- Node drag does not open drawer.
|
||||
- Node click opens drawer or offline reader.
|
||||
- Hover preview stays attached after zoom/pan/drag/drawer resize.
|
||||
- Community click enters focus.
|
||||
- Node can leave wash.
|
||||
- Reset returns to stable full graph.
|
||||
- Root native scroll remains controlled.
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
This architecture is complete only when:
|
||||
|
||||
1. Known hard problems are fixed in both workbench and offline HTML.
|
||||
2. Interaction risk audit has tests or browser verification evidence.
|
||||
3. GraphGestures owns raw input classification.
|
||||
4. GraphViewport owns coordinate conversion.
|
||||
5. GraphState owns graph-local runtime state.
|
||||
6. GraphRenderer only renders.
|
||||
7. SpatialIndex owns graph hit testing.
|
||||
8. Old renderer-owned interaction and coordinate logic is removed or reduced to a documented compatibility shell.
|
||||
9. No phase leaves an unowned cleanup item without an assigned next phase and acceptance gate.
|
||||
|
||||
## Follow-Up Landing Index
|
||||
|
||||
This section exists so the required follow-ups are easy to find later.
|
||||
|
||||
### SpatialIndex Takeover
|
||||
|
||||
When:
|
||||
|
||||
- Phase 4.
|
||||
|
||||
Must take over:
|
||||
|
||||
- Node hit testing.
|
||||
- Edge hit testing.
|
||||
- Community wash hit testing.
|
||||
- Blank-canvas hit testing.
|
||||
|
||||
Must remove:
|
||||
|
||||
- Graph intent rules that depend on DOM stacking order as the primary source.
|
||||
|
||||
Not complete if:
|
||||
|
||||
- SpatialIndex exists but GraphGestures still relies on DOM target classification for graph object hits.
|
||||
|
||||
### Static Renderer Cleanup
|
||||
|
||||
When:
|
||||
|
||||
- Phase 3 begins slimming.
|
||||
- Phase 6 completes removal.
|
||||
|
||||
Must remove or migrate:
|
||||
|
||||
- Raw event listeners that own gesture meaning.
|
||||
- Coordinate formulas that bypass GraphViewport.
|
||||
- Hidden hover/drag/selection/focus state.
|
||||
- Browser default behavior policy scattered across renderer code.
|
||||
|
||||
Allowed final role:
|
||||
|
||||
- Thin compatibility shell that composes modules and preserves public API.
|
||||
|
||||
Not complete if:
|
||||
|
||||
- New graph interactions still need to be added inside `static-renderer.ts`.
|
||||
|
||||
### Browser Default Behavior
|
||||
|
||||
When:
|
||||
|
||||
- Phase 1.
|
||||
|
||||
Must prove:
|
||||
|
||||
- No native text selection during graph pan/drag.
|
||||
- No browser page zoom from graph-owned zoom.
|
||||
- Reader/search text behavior still works where intentionally allowed.
|
||||
|
||||
Not complete if:
|
||||
|
||||
- Only ordinary mouse wheel is tested and trackpad-like paths are untested.
|
||||
|
||||
### Workbench And Offline Parity
|
||||
|
||||
When:
|
||||
|
||||
- Every phase that changes graph behavior.
|
||||
|
||||
Must prove:
|
||||
|
||||
- Workbench graph and generated offline HTML both pass the affected interaction paths.
|
||||
|
||||
Not complete if:
|
||||
|
||||
- A behavior is verified in workbench only.
|
||||
|
||||
## Open Decisions For Implementation Planning
|
||||
|
||||
These do not block this design, but must be decided before implementation:
|
||||
|
||||
1. Whether Phase 1 starts from the current branch directly or from a new child branch such as `codex/graph-six-layer-architecture`.
|
||||
2. Whether to add a DOM test dependency or continue using existing Node tests plus browser scripts.
|
||||
3. The exact SpatialIndex grid size and edge hit tolerance.
|
||||
4. The exact community wash deformation cap values after fixture review.
|
||||
5. Whether `static-renderer.ts` remains as a named compatibility shell or is fully replaced by a new facade module.
|
||||
@@ -0,0 +1,172 @@
|
||||
# static-renderer 协调逻辑拆分设计
|
||||
|
||||
日期:2026-06-17
|
||||
状态:待用户复核
|
||||
分支:`spark/static-renderer-coordination-split`(基于 main `f8a834d`)
|
||||
路线:B —— 抽出 Controller,并把渲染侧按职责拆到位
|
||||
|
||||
## 目的
|
||||
|
||||
上一轮"图谱六层架构"重构(已合并进 main)把图谱的**决策权**收敛到了正确的模块:手势归 `gestures.ts`、坐标归 `viewport.ts`/`geometry.ts`、命中归 `spatial-index.ts`/`hit-testing.ts`、状态归 `state.ts`,画图零件也拆成了 `nodes.ts`/`edges.ts`/`community-washes.ts`/`minimap.ts` 等独立模块。
|
||||
|
||||
但有一块没收尾:`packages/graph-engine/src/render/static-renderer.ts` 仍有 **2481 行**,同时承担了**四种不同职责**——组装、调度指挥、画图编排、对外接口。它名叫"renderer"(渲染器),实际却是事实上的"总指挥"。这是六层设计原本想达到、但没做到的一块(设计原文要求"GraphRenderer 只负责画图")。
|
||||
|
||||
本设计的目标:把这块"既指挥又画图"的混合体,按职责拆成边界清楚的模块,让"做决定的"和"画图的"彻底分开,且**以后新增交互时不会再把逻辑塞回画图层**。
|
||||
|
||||
这不是性能优化。当前图谱性能良好(见下方性能基线)。这是纯粹的代码结构整理,**不改变任何用户可见行为**。
|
||||
|
||||
## 现状(事实,带证据)
|
||||
|
||||
`static-renderer.ts`(2481 行)内部混着四类东西:
|
||||
|
||||
1. **组装**:`createStaticGraphRenderer()` 创建 root 元素、gestures 控制器、runtime state、hit resolver、pin state、simulation、resize observer,并把它们接线。
|
||||
2. **调度指挥**:`applyGestureIntents()` 把手势意图派发给 `handleNodeClick` / `handleNodeDragStart` / `handleNodeDragMove` / `handleNodeDragEnd` / `handleNodeDragCancel` / `handleBlankClick`;以及语义命令 `selectCommunity` / `focusCommunity` / `resetViewState` / `retreatFocusedView` / `openSearch` / `applySearchQuery` / `closeSearch` / `clearInteractionState`;还有键盘意图路由 `handleDocumentKeydown`。
|
||||
3. **画图编排**:`render()`(重建渲染模型→重绘→挂控件→提交相机→画 overlay→重启模拟)、`paint()`(把画图零件拼成 DOM 树)、`mountSearchControl` / `mountGraphToolbar` / `mountCommunityLegend`、`commitViewport` / `updateMinimapViewport` / `updateEffectiveDensity`、diff 动画 `markDiffElements` / `settleDiffElements` / `animateDiff`。
|
||||
4. **hover / 阅读器 / 选择面板**:`scheduleHoverPreview` / `showEdgeHoverPreview` / `clearHoverPreview` / `renderHoverPreview` / `positionHoverPreview` / `positionEdgeHoverPreview` / `renderReader` / `renderSelectionPanel`。
|
||||
5. **对外接口**:返回的对象(`render` / `applyDiff` / `setData` / `setTheme` / `setPins` / `focusNode` / `focusCommunity` / `resetView` / `select` / `clearSelection` / `clearInteraction` / `resetLayout` / `destroy`),由 `facade.ts` 包装成公开 API。
|
||||
|
||||
三个必须正视的结构现实(自审时对回代码确认,决定了拆分怎么做才不返工):
|
||||
|
||||
- **调度函数会直接操作 DOM 元素**:如 `dom.nodeElements.get(id)?.focus()`、`.classList.add("is-dragging")`、社区 hover 高亮遍历 `dom.nodeElements`。这些是"交互附着物",不是画图——所以铁律必须区分"画图"与"附着",而不是简单说"调度不碰 DOM"。
|
||||
- **`render()` 是三合一**:它同时"把传入的改动写进 state(`setPins`/`setFocus`/`setSelection`/`setPositions`)+ 重建渲染模型(`buildRenderableGraph`/`new PinState`/`hitTargetResolver.refresh`)+ 绘制(`paint`/`mount*`)"。拆分时要把"写状态"和"重建并画"分开,不能整块塞给 render-pipeline 还说它"只画图"。
|
||||
- **存在共享可变状态**:`graph`、`pinState`、`dom`、`simulation`(以及 `data`/`theme`/`typeFilters`)现在是闭包变量,被调度和画图两边读写、还会被 `render()` 重新赋值。拆成多文件后,这些必须变成一个显式的"共享渲染上下文"对象(见下节),否则模块间无法干净协作。
|
||||
|
||||
基线(实测):单元测试 265 个全部通过;密集图(200 节点 / 231 边)连续缩放稳定约 50.5 fps。
|
||||
|
||||
## 核心原则(这是"不会再返工"的关键)
|
||||
|
||||
1. **铁律(精确版,已对回真实代码)**:
|
||||
- Controller **不构建、不绘制图**:不调用 `paint`/`mount*`、不创建节点/边/色块 DOM、不计算渲染模型、不做布局。
|
||||
- Controller **允许**在**已经画好的元素**上切换"交互附着物":给节点加/去 `is-dragging`、设置 `focus`、hover 高亮。这类是"对交互的附着",不是"画图"。(真实代码里这些操作本就存在,硬禁会一上手就违规——见"现状"第 2 点。)
|
||||
- Render-pipeline / Overlays-presenter **只重建模型并绘制**:不判断手势意义、不决定 selection/focus/pin 等语义。
|
||||
2. **停手线**:模块就定下面这几个,每个对应一个明确职责。拆到这个粒度就**停手**,不再为了凑文件数继续切碎。这是成熟标准——按职责拆到位,既不留 god 文件,也不拆成 confetti 碎片。
|
||||
3. **纯搬家**:本次只移动和重组代码,不改逻辑、不改行为。任何"顺手优化"都不在范围内。
|
||||
4. **共享状态集中**:`graph`/`pinState`/`dom`/`simulation` 等"连接组织"集中放进一个"共享渲染上下文",由组装根持有,传给各模块;不让任意模块各自藏一份平行状态(详见下节)。
|
||||
|
||||
## 目标模块结构
|
||||
|
||||
`gestures.ts` / `viewport.ts` / `state.ts` / `spatial-index.ts` / `hit-testing.ts` 以及所有画图零件模块(`nodes.ts` / `edges.ts` / `community-washes.ts` / `minimap.ts` / `controls.ts` / `offline-reader.ts` / `hover-card.ts` / `toolbar.ts` / `legend.ts` / `search.ts` / `preview.ts`)**保持不动**。本次只新增/重组下面的协调与编排层:
|
||||
|
||||
| 模块(文件) | 职责(一句话) | 拥有的代表函数 | 允许依赖 | 禁止 |
|
||||
|---|---|---|---|---|
|
||||
| `controller.ts`(新) | 总指挥:把手势意图和 API 命令翻译成"该改什么状态/模拟/相机,然后请求重画" | `applyGestureIntents`、`handleNode*`、`handleBlankClick`、语义命令(select/focus/reset/search/clear)、键盘意图路由、node-drag 计算 helper | `state`、`viewport`、`gestures`、`simulation-bridge`、`node-drag-lifecycle`、`hit-testing`、`keyboard` | 创建/绘制图 DOM、调用 `paint`/`mount*`、计算渲染模型(**允许**在已画元素上切换 `is-dragging`/`focus` 等交互附着物) |
|
||||
| `render-pipeline.ts`(新) | 画图编排:照当前状态把图重绘出来 | `render`、`paint`、`mount*`、`commitViewport`、`update*`、diff 动画、`restartSimulation`/`applyMotionFrame` 等绘制编排 | 所有画图零件模块、`state`(只读)、`viewport`(投影) | 判断手势意义;写入交互决策 |
|
||||
| `overlays-presenter.ts`(新) | hover 卡片 / 边预览 / 阅读器 / 选择面板的"贴位置 + 显示" | `scheduleHoverPreview`、`showEdgeHoverPreview`、`clearHoverPreview`、`renderHoverPreview`、`position*Preview`、`renderReader`、`renderSelectionPanel` | `overlays.ts`(锚点)、`preview.ts`、`hover-card.ts`、`viewport`、`state`(只读) | 判断手势意义;决定状态 |
|
||||
| `graph-renderer-root.ts`(由 `static-renderer.ts` 改名瘦身) | 组装根:创建上述模块、接线,返回供 facade 包装的引擎对象 | `createGraphRenderer()`(原 `createStaticGraphRenderer`)、`destroy`、对外方法的委派 | 上述所有协调/编排模块 | 自己实现调度或画图细节(只做组装与委派) |
|
||||
|
||||
说明:`controller.ts` 同时拥有"手势触发的处理"和"API 命令(如 `focusCommunity`/`resetView`)",因为它们是同一职责——"对一个动作做出决定"。把它们合在一个模块,正是为了守住"停手线",不再过度拆分。
|
||||
|
||||
## 共享渲染上下文(本设计最关键、最容易做错的一块)
|
||||
|
||||
调度和画图不是"井水不犯河水",它们之间有一坨**连接组织**:`graph`(当前渲染模型)、`pinState`(固定状态)、`dom`(已画出的元素引用)、`simulation`(力导模拟)、以及 `data`/`theme`/`typeFilters` 等当前输入。现在它们是 `static-renderer.ts` 里的闭包变量,谁都能直接读写。
|
||||
|
||||
拆成多文件后,必须把它们收进**一个显式的共享上下文对象**(暂名 `GraphRenderContext`),规则:
|
||||
|
||||
- **由组装根(`graph-renderer-root.ts`)创建并持有**这个上下文,传给 controller、render-pipeline、overlays-presenter。
|
||||
- 上下文里"会被重建/重新赋值"的字段(`graph`、`pinState`、`dom`)通过上下文对象的方法或属性更新,**只有 render-pipeline 在重建时写它们**;controller 和 presenter 只读。
|
||||
- `runtimeState`(已有的 state 模块)仍是 selection/focus/hover/pins/viewport 的唯一权威;上下文不复制这些,只持有"渲染产物"(graph/dom)和"运行期协作对象"(pinState/simulation)。
|
||||
- `render()` 拆成两步:**(a) 应用传入改动到 state**(归 controller / 根的命令侧)+ **(b) 重建模型并绘制**(归 render-pipeline,写回上下文的 graph/dom/pinState)。
|
||||
|
||||
这一节是整份设计的重点。若不先定清楚共享上下文,"按职责拆"会在落地时卡住或被迫返工——这正是要避免的。具体字段和方法签名在实现计划阶段定稿。
|
||||
|
||||
## 数据流
|
||||
|
||||
拖动节点:
|
||||
|
||||
```
|
||||
你拖节点
|
||||
-> gestures 翻译成"拖拽意图"
|
||||
-> controller 决定:更新该节点位置、固定它、改 state、通知 simulation
|
||||
-> controller 请求重画
|
||||
-> render-pipeline 照新状态把节点画到新位置
|
||||
-> overlays-presenter 把 hover 卡片贴回正确锚点
|
||||
```
|
||||
|
||||
API 命令(如宿主调用 `focusCommunity`):
|
||||
|
||||
```
|
||||
facade.focusCommunity(id)
|
||||
-> graph-renderer-root 委派给 controller
|
||||
-> controller 改 focus state、请求重画
|
||||
-> render-pipeline 重绘聚焦视图
|
||||
```
|
||||
|
||||
每一层只干自己那段;出问题能立刻定位是"指挥错了"还是"画错了"。
|
||||
|
||||
## 命名决策
|
||||
|
||||
- 文件 `render/static-renderer.ts` → `render/graph-renderer-root.ts`(它以后只管组装,名字应说明这一点)。
|
||||
- 对外函数 `createStaticGraphRenderer` → `createGraphRenderer`("static" 是早期遗留词,已无意义)。
|
||||
- 影响面(已精确核对):`facade.ts`(import + 调用)、`render/index.ts`(re-export `createStaticGraphRenderer` 与类型 `StaticGraphRenderer`,39/40 行)、`architecture.ts`(entrypoints 写死路径,43 行)、`renderer-boundary.test.ts`(按路径字符串读文件,117/124 行),外加文件自身。(注:`sim/index.ts`/`sim/pins.ts` 并不引用它,之前的宽松 grep 误命中,已剔除。)
|
||||
- 注意:其中两处**不是"测试自动接住",而是必须手动改**——`renderer-boundary.test.ts` 按路径字符串读 `render/static-renderer.ts`,`architecture.ts` 的 `entrypoints` 也写死了该路径。改名必须同步更新这两处,否则测试会因找不到文件而失败。
|
||||
- `architecture.ts` 中 `renderer` 层的 `entrypoints` 需相应更新;新增 `controller` 层条目(见下)。
|
||||
|
||||
`architecture.ts` 调整:当前六层 owner map 把 `static-renderer.ts` 归在 `renderer` 层。重组后:
|
||||
- `renderer` 层 entrypoints 指向 `render-pipeline.ts`、`overlays-presenter.ts` 及画图零件;不再把组装/调度算作 renderer。
|
||||
- 视实现情况,将"调度"明确为 `gestures` 层的下游协调,或在 owner map 中体现 `controller.ts` 的归属(实现时确定:要么并入 `gestures` 层描述,要么新增一层条目)。原则是 owner map 必须如实反映"谁做决定、谁画图"。
|
||||
|
||||
## 迁移计划(一次搬一块,每块跑全套测试才提交)
|
||||
|
||||
沿用上一条分支的纪律:每个工作单元完成并验证通过才提交,红了不提交,不自动 push/merge/amend。
|
||||
|
||||
- **Phase 0 基线**:跑全套单元测试 + 4 套浏览器回归 + 帧率测试,记录"改之前是绿的、约 50fps"。不写功能代码。
|
||||
- **Phase 1 建立共享渲染上下文 + 抽 Controller**:先把 `graph`/`pinState`/`dom`/`simulation` 等闭包变量收进显式的 `GraphRenderContext`,由根持有;再把调度/命令/键盘路由搬进 `controller.ts`(通过上下文只读这些、用 state 模块改语义状态、用现有 `dom` 引用切换交互附着物)。跑测试。
|
||||
- **Phase 2 抽 Render-pipeline**:把 `render()` 拆成"应用改动到 state"与"重建模型并绘制"两步,后者连同 `paint()`/挂控件/相机提交/diff 动画搬进 `render-pipeline.ts`,并由它写回上下文的 `graph`/`dom`/`pinState`。跑测试。
|
||||
- **Phase 3 抽 Overlays-presenter**:把 hover/边预览/阅读器/选择面板搬进 `overlays-presenter.ts`。跑测试。
|
||||
- **Phase 4 收尾与改名**:`static-renderer.ts` 缩成薄组装根并改名为 `graph-renderer-root.ts`;函数改名 `createGraphRenderer`;更新引用与 `architecture.ts`;补充清晰注释。跑测试。
|
||||
- **Phase 5 边界测试与总验收**:新增/扩展边界测试锁死铁律;跑完整验收命令集 + 帧率对比基线。
|
||||
|
||||
## 测试与安全策略
|
||||
|
||||
- **安全网**:现有 265 个单元测试 + 4 套浏览器回归(workbench / offline / community-wash / stage-4.5 perf)每个 phase 都必须全绿。因为是纯搬家,"行为不变"由这些既有测试保证。
|
||||
- **性能门槛**:收尾重跑 stage-4.5 密集图帧率测试,fps 必须 ≥ 基线(约 50fps,允许 10% 波动);低于则不通过。
|
||||
- **新增边界测试**(锁死铁律,防止以后回潮):
|
||||
- `controller.ts` 不引用画图零件模块(`nodes`/`edges`/`community-washes`/`minimap` 等的 `create*`)、不调用 `paint`/`mount*`、不计算渲染模型;但允许通过上下文的 `dom` 切换 `is-dragging`/`focus` 等交互附着物(这是被明确允许的,不算违规)。
|
||||
- `render-pipeline.ts` / `overlays-presenter.ts` 不调用 gesture 分类、不写 selection/focus/pin 等语义决策。
|
||||
- 尽量包含一个运行时探针(仿照现有 `renderer-boundary.test.ts`),不只做源码正则扫描。
|
||||
- **进度记录从简**:上一轮"每个代码提交都配一个进度记账提交"过重。本次按 phase 记一次即可,不要求逐提交记账。
|
||||
|
||||
## 性能基线(实测)
|
||||
|
||||
| 场景 | 数据 | 结果 |
|
||||
|---|---|---|
|
||||
| 密集图连续缩放 | 200 节点 / 231 边,桌面 1440×960,采样 3 秒 | 50.5 fps(空闲基线 12.6 仅因无动画时不刷帧,属正常)|
|
||||
|
||||
来源:`tests/graph-browser-stage-4-5.regression-1.sh --target offline`。
|
||||
|
||||
## 不在范围内
|
||||
|
||||
- 不改任何用户可见行为。
|
||||
- 不做性能优化(当前不卡)。
|
||||
- 不动画图零件模块、`gestures.ts`、`viewport.ts`、`state.ts`、`spatial-index.ts`。
|
||||
- 不动 `facade.ts` 的公开 API 形状(只更新它对内部函数的引用名)。
|
||||
- 不引入新依赖、新测试框架。
|
||||
- 不顺手重构无关代码。
|
||||
- 不切 WebGL / 不改图谱数据结构 / 不改知识库 markdown。
|
||||
- 不为了凑文件数把模块继续拆碎(守"停手线")。
|
||||
|
||||
## 风险与缓解
|
||||
|
||||
| 风险 | 缓解 |
|
||||
|---|---|
|
||||
| 搬家时漏接一根线导致行为变化 | 一次只搬一块,每块跑全套测试;红了不提交 |
|
||||
| 画图搬错导致视觉/帧率退化 | stage-4.5 帧率门槛 + 浏览器回归把关 |
|
||||
| 改名牵动多处引用 | 影响面已知(约 6 处);其中 2 处需手动改(boundary test 的路径字符串 + `architecture.ts` 的 entrypoints),其余由编译/测试兜底;改名集中在 Phase 4 一次做完 |
|
||||
| 以后又把调度塞回画图层 | 新增边界测试 + owner map 如实更新 |
|
||||
| 模块边界划得不够干净(仍互相伸手) | 铁律 + 边界测试强制"只读状态 / 请求重画"的交互方式 |
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. `static-renderer.ts` 已改名为 `graph-renderer-root.ts`,且只剩组装与委派,不含调度决策或画图编排细节。
|
||||
2. `controller.ts` / `render-pipeline.ts` / `overlays-presenter.ts` 三个模块各自职责单一、边界清楚。
|
||||
3. 265 个单元测试 + 4 套浏览器回归全绿。
|
||||
4. 密集图帧率 ≥ 基线(约 50fps)。
|
||||
5. 新边界测试通过,证明 Controller 不画图、Render-pipeline/Overlays 不做决定。
|
||||
6. `architecture.ts` owner map 如实反映新结构(含路径/entrypoints 更新)。
|
||||
7. 共享渲染上下文 `GraphRenderContext` 已建立:`graph`/`pinState`/`dom` 只由 render-pipeline 写,controller/presenter 只读;`render()` 已拆成"应用改动到 state"与"重建并绘制"两步。
|
||||
8. 全程行为零变化(由既有测试保证)。
|
||||
|
||||
## 开放问题(实现时确定,不阻塞本设计)
|
||||
|
||||
1. `controller.ts` 在 `architecture.ts` 里是并入 `gestures` 层描述,还是新增独立层条目。
|
||||
2. 语义命令(select/focus/reset/search)全部归 `controller.ts`,还是其中纯 UI 面板开关(如 toolbar 折叠)留在 `render-pipeline.ts`——以"是否构成交互决策"为判据,实现时按铁律归位。
|
||||
@@ -0,0 +1,909 @@
|
||||
# Large Graph Global/Community Interaction Design
|
||||
|
||||
日期:2026-06-18
|
||||
状态:待用户复核
|
||||
当前分支:`codex/graph-performance-optimization`
|
||||
视觉稿:[`docs/spark/mockups/2026-06-18-large-graph-global-community-flow.html`](mockups/2026-06-18-large-graph-global-community-flow.html)
|
||||
|
||||
## 目的
|
||||
|
||||
这份设计确定 llm-wiki 图谱在 5000+ / 10000+ 节点规模下的产品交互方向。
|
||||
|
||||
目标不是把当前图谱做成一个更复杂的自动模式系统,也不是照搬 `graphify` 或 `nashsu/llm_wiki`。目标是明确一条长期路线:
|
||||
|
||||
**全局图谱负责看结构,社区聚焦负责读内容。**
|
||||
|
||||
当前图谱已经有社区色块、社区图例、筛选、搜索、右侧抽屉、Pin、社区聚焦和密度模式。后续性能优化应围绕这些既有用户动作收敛,而不是增加一套让用户难以理解的自动模式切换。
|
||||
|
||||
## 设计结论
|
||||
|
||||
采用“全局降级,社区聚焦升级”的双层体验。
|
||||
|
||||
1. 全局视角的正式目标是支持 5000+ / 10000+ 节点,但只显示地图级信息;当前 DOM/SVG 全局图不再被要求硬撑万级。
|
||||
2. 普通节点点击只触发轻量查看,不改变全局视角。
|
||||
3. 社区色块或社区图例点击先选中社区并显示社区摘要,用户再通过按钮进入社区聚焦。
|
||||
4. 全局节点的“打开详情 / 阅读”进入所属社区,并自动选中该节点。
|
||||
5. 社区聚焦后完整社区必须在场:全部节点轻量可见,核心/选中/搜索命中/Pin 节点升级为卡片。
|
||||
6. 搜索和筛选不创建新模式,只改变当前全局或社区视角里的可见范围。
|
||||
7. 缩放只调整细节密度,不自动切换全局/社区主模式。
|
||||
8. 返回全图以可见按钮为主,双击空白只作为辅助快捷方式。
|
||||
9. 社区质量不好时不打断用户,但要降低社区边界的视觉确定感,并提供轻量的核心结构辅助查看。
|
||||
|
||||
一句话规则:
|
||||
|
||||
**单击决定当前检查什么;按钮决定是否深入;缩放只决定当前画多少细节。**
|
||||
|
||||
## 审查后修订决定
|
||||
|
||||
文档审查后,本设计补充以下硬规则。它们优先于早期草案里的含糊表述。
|
||||
|
||||
1. 全局不承担完整阅读。用户在全局节点摘要里点“打开详情 / 阅读”后,进入所属社区,并自动选中该节点。
|
||||
2. 社区摘要保留“查看核心节点列表”,它是进入社区前的预览:在抽屉内展开列表,悬停列表项时图上轻微高亮,点击列表项切到节点轻量摘要,但不自动进入社区。
|
||||
3. 搜索或筛选把当前选中对象排除时,不自动清空选择;抽屉保留对象并提示它不在当前结果中,同时提供“清除选择”和“显示该对象”。
|
||||
4. 核心节点、重要边和强关系由系统自动排序,规则必须可解释;Pin 是强信号,但不是唯一标准。
|
||||
5. 社区聚焦不是截断社区。完整社区都在场,表达层按点、标签、卡片和子主题逐级增强。
|
||||
6. 超大社区才启用子主题 / 子社区;普通社区不额外增加层级。
|
||||
7. 社区质量不好时,保留社区主视图和轻提示;第一阶段只提供“核心结构 / 连接度”这一个轻量辅助查看,不把类型、来源、时间做成完整备用模式。
|
||||
8. 大图交互降级必须保持视觉连续性:弱信息淡出淡入,核心锚点、选中对象、搜索命中和 Pin 不消失。
|
||||
9. 设计验收使用产品可感知标准:拖动不跟手、缩放卡住、抽屉慢开、搜索高亮迟迟不出现、画面大跳变,都算失败。
|
||||
10. 性能测试分三档:真实图谱、压力图谱、极限图谱;每档必须记录节点数、边数、社区数、最大社区规模、搜索命中数和 Pin 数。
|
||||
11. 社区聚合节点必须强区分,要看起来像“装着很多节点的容器”,不能像普通内容节点;聚合后也必须提示内部的选中、搜索命中和 Pin。
|
||||
12. 第一阶段只保证核心键盘路径可用,不做完整键盘漫游。
|
||||
13. 本次主打图谱体验。Agent 提问、边单击关系检查、移动端专项、完整键盘漫游和桌面版专项方案记录为后续探索,不进入第一阶段。
|
||||
14. 旧交互直接切到新规则,不做旧主路径兼容。
|
||||
15. 抽屉缺少信息时直接显示空状态和下一步动作,不隐藏,也不用推荐内容伪装成真实关系。
|
||||
16. 搜索、筛选、抽屉变化默认只更新高亮、淡化、列表和摘要,不重排整张图;重新组织布局必须来自用户明确动作。
|
||||
17. 重要性排序拆成稳定结构评分和临时上下文加权,避免核心节点和关键标签频繁跳变。
|
||||
18. 第一阶段按桌面宽屏体验设计,移动端和窄屏只保证基础查看,不作为大图主验收目标。
|
||||
|
||||
## 第一阶段范围
|
||||
|
||||
第一阶段的目标是把图谱体验和性能规则定硬,并完成可测量的优化路径,不是一次性完成所有未来能力。
|
||||
|
||||
第一阶段包含:
|
||||
|
||||
- 固定全局地图和社区阅读的分层规则。
|
||||
- 把全局节点、社区、搜索、筛选、Pin、右侧抽屉和返回全图的交互规则写清楚。
|
||||
- 补齐大图降级规则:节点、边、标签、卡片、社区聚合、搜索命中和 Pin 的显示预算。
|
||||
- 补齐社区聚焦的规模规则:完整社区在场,但点、标签、卡片和子主题按规模分层。
|
||||
- 补齐性能测量项和测试图谱数据要求。
|
||||
- 明确当前 DOM/SVG 继续服务小图、社区阅读和离线细节;全局万级体验必须经过新的大图渲染或聚合方案验证。
|
||||
|
||||
第一阶段不包含:
|
||||
|
||||
- Agent 提问入口。
|
||||
- 边单击关系检查。
|
||||
- 类型、来源、时间等多套备用组织方式。
|
||||
- 移动端或窄屏专项体验。
|
||||
- 桌面版专项技术方案。
|
||||
- 完整键盘漫游整张图谱。
|
||||
- 基于长期阅读历史的智能排序系统。
|
||||
|
||||
## 为什么不做自动切换
|
||||
|
||||
不建议根据缩放自动在“全局大图”和“局部阅读”之间切换。
|
||||
|
||||
原因:
|
||||
|
||||
- 当前已有社区入口和筛选入口,用户意图已经足够明确。
|
||||
- 自动切换容易让用户迷失:只是缩放一下,界面突然换成另一套阅读状态。
|
||||
- 大图性能优化需要稳定的规则,不能让细节组件在缩放边界反复出现和消失。
|
||||
- 现有社区色块点击已经是自然的社区检查入口,应继续强化它;真正深入社区由明确按钮承接。
|
||||
|
||||
允许的自动行为只有信息密度变化:
|
||||
|
||||
- 缩远时隐藏普通标签、弱边和复杂效果。
|
||||
- 放近时显示更多关键标签。
|
||||
- 手势进行中进一步降级,手势停止后补回允许显示的细节。
|
||||
- 降级必须渐进,弱信息淡出淡入,不能突然切成另一张图。
|
||||
- 核心锚点、选中对象、搜索命中和 Pin 节点在交互中不能消失。
|
||||
|
||||
## 交互规则
|
||||
|
||||
### 1. 全局视角
|
||||
|
||||
全局视角是地图,不是阅读界面。
|
||||
|
||||
显示:
|
||||
|
||||
- 节点点位。
|
||||
- 社区颜色。
|
||||
- 少量关键节点标签,不显示普通节点文字。
|
||||
- 重要边或社区间骨架关系。
|
||||
- 搜索命中和选中节点高亮。
|
||||
- 社区图例和筛选入口。
|
||||
|
||||
隐藏或降级:
|
||||
|
||||
- 普通节点卡片。
|
||||
- 大段文本摘要。
|
||||
- 全量节点标签。
|
||||
- 全量边。
|
||||
- 复杂阴影、动画和长时间运行的动态布局。
|
||||
|
||||
全局视角点击普通节点:
|
||||
|
||||
- 保持图谱视角不变。
|
||||
- 高亮该节点和少量直接邻居。
|
||||
- 右侧打开轻量查看抽屉。
|
||||
- 不自动进入社区聚焦。
|
||||
- 不自动打开完整阅读内容。
|
||||
- 用户在轻量抽屉里点“打开详情 / 阅读”后,进入所属社区,并自动选中该节点。
|
||||
|
||||
全局选中和抽屉优先级:
|
||||
|
||||
- 节点命中优先于社区色块。用户点到具体节点时,抽屉显示节点摘要。
|
||||
- 点社区色块、社区图例或社区背景,但没有点中具体节点时,抽屉显示社区摘要。
|
||||
- 悬停只做临时提示,不接管右侧抽屉。
|
||||
- 搜索和筛选可以改变高亮范围,但不抢占已选中的节点或社区。
|
||||
- Pin 是持久标记,不等于当前选中对象,也不自动切换抽屉。
|
||||
- 单击全局空白处只清空当前选中对象,抽屉回到全局概览或搜索结果列表;不改变缩放,也不返回全图。
|
||||
|
||||
### 2. 全局轻量查看抽屉
|
||||
|
||||
当前代码里点击节点会打开右侧抽屉。这个方向保留,但抽屉需要重新定义为“轻量查看”,不要变成图上大卡片的替代品。
|
||||
|
||||
这里不是两个抽屉,而是一个抽屉、两种内容层级:
|
||||
|
||||
- 点社区色块或社区图例时,抽屉显示社区摘要。
|
||||
- 点社区里的具体节点时,抽屉切到节点摘要。
|
||||
- 如果社区和节点同时存在,节点是主对象,社区只做上下文。
|
||||
|
||||
轻量抽屉显示:
|
||||
|
||||
- 节点名称。
|
||||
- 类型。
|
||||
- 所属社区。
|
||||
- 连接数。
|
||||
- 3-5 个最重要相邻节点。
|
||||
- 1-2 条最强关系。
|
||||
- “进入所属社区”动作。
|
||||
- “打开详情 / 阅读”动作,进入所属社区并选中该节点;全局图谱不承载完整阅读。
|
||||
|
||||
节点动作落点:
|
||||
|
||||
- “打开详情 / 阅读”是主动作:进入所属社区,自动选中该节点,并打开社区阅读抽屉里的节点内容。
|
||||
- “进入所属社区”是次动作:进入所属社区的概览状态,不自动打开节点阅读;该节点可作为上下文轻高亮。
|
||||
- 如果节点没有可读详情,只显示“进入所属社区”。
|
||||
|
||||
社区摘要时,这些字段会换成:
|
||||
|
||||
- 社区名称。
|
||||
- 节点总量。
|
||||
- 核心节点概览。
|
||||
- 关键桥接关系。
|
||||
- “进入社区”动作。
|
||||
- “查看核心节点列表”动作。
|
||||
|
||||
“查看核心节点列表”的行为:
|
||||
|
||||
- 在抽屉内展开核心节点列表。
|
||||
- 悬停列表项时,图上对应节点轻微高亮。
|
||||
- 点击列表项时,抽屉切到该节点轻量摘要。
|
||||
- 点击列表项不自动进入社区。
|
||||
|
||||
轻量抽屉不显示:
|
||||
|
||||
- 长正文。
|
||||
- 大量相邻节点列表。
|
||||
- 复杂 Markdown 阅读内容。
|
||||
- 会迫使全局图谱重排的大组件。
|
||||
|
||||
轻量抽屉的定位:
|
||||
|
||||
- 它是全局地图的检查面板。
|
||||
- 它帮助用户决定是否深入。
|
||||
- 它不承担完整阅读。
|
||||
- 它的文案和按钮要随“社区”或“节点”这两个对象切换,而不是固定不变。
|
||||
- 没有选中对象时,它可以显示全局概览、搜索结果或空状态,但不混进节点/社区摘要。
|
||||
|
||||
### 3. 社区入口
|
||||
|
||||
社区色块和社区图例点击继续作为“选择社区”的主入口。
|
||||
|
||||
这个设计是合理的,因为用户点击社区本身就是明确动作:我要先看这个社区是什么,再决定是否深入。
|
||||
|
||||
点击社区色块或社区图例后:
|
||||
|
||||
- 全局视角保持不变。
|
||||
- 高亮该社区和它的少量跨社区关系。
|
||||
- 右侧轻量抽屉显示社区摘要。
|
||||
- 不立刻进入社区聚焦。
|
||||
- 抽屉里的“进入社区”按钮才触发社区聚焦。
|
||||
- “查看核心节点列表”只在抽屉内展开列表;列表悬停可以轻微高亮图上节点,但不改变主图视角。
|
||||
|
||||
进入社区聚焦后:
|
||||
|
||||
- 图谱视角 fit 到该社区。
|
||||
- 该社区内全部节点都在场,默认以点显示。
|
||||
- 核心、选中、搜索命中、Pin 节点升级为卡片。
|
||||
- 其他社区退出主要视野或降为背景。
|
||||
- 显示卡片、更多标签、阅读抽屉和社区摘要。
|
||||
|
||||
普通节点也可以进入社区,但必须通过明确动作:
|
||||
|
||||
- 在轻量抽屉里点“进入所属社区”。
|
||||
- 不建议单击节点直接进入社区。
|
||||
- 不把双击作为社区入口。
|
||||
|
||||
### 4. 社区聚焦视角
|
||||
|
||||
社区聚焦是阅读和理解模式。
|
||||
|
||||
显示:
|
||||
|
||||
- 社区标题和返回全局入口。
|
||||
- 社区完整节点分布。
|
||||
- 核心节点卡片。
|
||||
- 核心关系。
|
||||
- 社区摘要。
|
||||
- 关键节点列表。
|
||||
- 与其他社区的桥接关系。
|
||||
- 阅读抽屉或详情抽屉。
|
||||
|
||||
规模控制:
|
||||
|
||||
- 小社区可以全部卡片化。
|
||||
- 中大型社区全部节点以点显示,核心、选中、搜索命中、Pin 节点卡片化。
|
||||
- 超大社区才启用子主题 / 子社区;普通社区不额外加层级。
|
||||
- 其余节点通过缩放、筛选、搜索、一跳邻居或“展开更多”逐步升级显示。
|
||||
- 不在社区聚焦里一次性把所有节点恢复成卡片。
|
||||
|
||||
### 5. 搜索和筛选
|
||||
|
||||
搜索和筛选是对当前视角的收窄,不是模式切换。
|
||||
|
||||
全局视角中:
|
||||
|
||||
- 搜索命中节点强高亮。
|
||||
- 命中节点的少量相邻节点轻高亮。
|
||||
- 非命中节点降淡。
|
||||
- 如果没有选中节点或社区,右侧轻量抽屉可以显示搜索命中列表。
|
||||
- 如果用户点中某个搜索结果,抽屉切到该节点摘要。
|
||||
- 清空搜索后,保留当前选中对象;如果没有选中对象,则回到全局概览。
|
||||
- 如果当前选中对象被搜索或筛选排除,不自动清空选择。
|
||||
- 抽屉继续显示该对象,并提示“当前对象不在筛选结果中”。
|
||||
- 抽屉提供“清除选择”和“显示该对象”动作。
|
||||
- 图上可以用很淡的位置提示保留该对象的上下文。
|
||||
- “显示该对象”不清空搜索或筛选,而是临时把该对象和必要的一跳上下文作为例外显示出来。
|
||||
- 临时显示时,搜索和筛选条件仍保留,并提示“正在临时显示筛选外对象”。
|
||||
- 用户可以清除临时显示,回到纯搜索或筛选结果。
|
||||
- 如果对象已被数据刷新移除,只显示“当前对象已不可用”,不再提供临时显示。
|
||||
|
||||
社区视角中:
|
||||
|
||||
- 搜索只在当前社区内优先匹配。
|
||||
- 可提供“扩展到全局搜索”的动作。
|
||||
|
||||
筛选规则:
|
||||
|
||||
- 第一阶段筛选保持轻量,只使用已有且稳定的字段,例如社区、节点类型、Pin 状态、是否有连接。
|
||||
- 多个筛选条件默认同时满足。
|
||||
- 搜索和筛选都显示结果数量。
|
||||
- 用户可以清除单个条件,也可以一键清空。
|
||||
- 社区聚焦中的筛选默认只作用于当前社区;扩展到全局必须是明确动作。
|
||||
|
||||
筛选后如果剩余节点少,系统可以逐步恢复更多标签和边,但仍遵守当前视角的主规则。
|
||||
|
||||
搜索、筛选和抽屉变化不得重排整张图:
|
||||
|
||||
- 搜索默认只改变高亮、淡化和结果列表,不重新计算全局布局。
|
||||
- 筛选默认隐藏或淡化不符合条件的内容,不自动重新组织全图。
|
||||
- 抽屉打开、关闭或切换内容,不得触发图谱重排。
|
||||
- 只有用户明确点击“重置布局”或“核心结构辅助查看”这类动作时,才允许重新组织图。
|
||||
- 搜索索引、社区摘要和核心节点列表应预先准备或缓存,避免每次交互从头计算。
|
||||
|
||||
### 6. 返回全图与双击
|
||||
|
||||
返回全图是导航动作,必须有可见入口,不依赖用户记住手势。
|
||||
|
||||
主规则:
|
||||
|
||||
- “回全图”按钮是主入口。
|
||||
- 双击真正的图谱空白背景可以作为快捷回全图。
|
||||
- 单击空白只取消当前选中,不改变当前视角。
|
||||
- 在社区聚焦里,单击空白只清空当前节点或社区选择,仍留在当前社区;关系选择留到边单击关系检查阶段再定义。
|
||||
- 从社区聚焦回全局,需要点“回全图”按钮,或双击真正的空白背景。
|
||||
|
||||
回全图后的状态:
|
||||
|
||||
- 视角回到全局并 fit 全图。
|
||||
- 搜索、筛选和 Pin 保留。
|
||||
- 如果当前选中节点仍存在,回全图后继续选中该节点,右侧抽屉显示全局轻量节点摘要。
|
||||
- 如果当前只选中社区,回全图后继续选中该社区,右侧抽屉显示社区摘要。
|
||||
- 如果选中对象已不存在,抽屉显示对象不可用状态,并提供清除选择。
|
||||
|
||||
双击限制:
|
||||
|
||||
- 双击节点不返回全图,不进入社区。
|
||||
- 双击社区色块不返回全图,不进入社区。
|
||||
- 双击社区图例、搜索框、工具栏、右侧抽屉不触发图谱导航。
|
||||
- 双击边不应作为回全图入口。
|
||||
- 现有“节点双击取消固定位置”的旧能力不作为新规则保留。固定/取消固定应改为节点轻量抽屉或节点工具里的明确动作。
|
||||
|
||||
关系边第一阶段规则:
|
||||
|
||||
- 第一阶段图上的边不作为可点击对象。
|
||||
- 边只承担视觉结构:连接、强弱、跨社区关系。
|
||||
- 节点和社区抽屉中保留强关系、桥接关系等摘要,因为这是对象摘要的一部分。
|
||||
- 边单击后的关系详情和证据检查进入后续探索。
|
||||
|
||||
键盘规则:
|
||||
|
||||
- Escape 先关闭搜索、工具栏或临时提示。
|
||||
- 如果当前有选中节点/社区,Escape 清空选中对象。
|
||||
- 如果已经在社区聚焦且没有选中对象,Escape 可以返回全图。
|
||||
- Escape 不清除 Pin,不重置布局。
|
||||
|
||||
第一阶段不做完整键盘漫游,但核心路径必须可用:
|
||||
|
||||
- 搜索结果可以用上下键选择。
|
||||
- Enter 打开当前结果的轻量摘要。
|
||||
- Tab 可以进入抽屉按钮、搜索结果、“核心结构辅助查看”和“回全图”按钮。
|
||||
- 抽屉里的“进入社区”“打开详情 / 阅读”“清除选择”“显示该对象”都可以聚焦和触发。
|
||||
- 选中对象、进入社区、返回全图等状态变化应有可感知反馈。
|
||||
- 第一阶段不要求用户用键盘逐个遍历几千个图谱节点。
|
||||
|
||||
## 大规模降级规则
|
||||
|
||||
全局图谱的目标规模按 5000+ / 10000+ 设计。
|
||||
|
||||
这不意味着万级图谱要完整显示全部细节。参考 `graphify` 的实际策略,大规模流畅来自降级和聚合,而不是硬画所有内容。
|
||||
|
||||
第一阶段已经补入预算口径:任何视角都有标签预算、边预算和卡片预算。当前 DOM/SVG 路线用于小图、社区阅读和离线细节;万级全局图的正式流畅目标进入后续 Sigma/Graphology 集成计划验收。
|
||||
|
||||
当前预算口径:
|
||||
|
||||
| 场景 | 节点 | 边 | 标签 | 卡片 | 交互中可更新对象 |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| 全局地图 | 10000 | 1000 | 40 | 0 | 1200 |
|
||||
| 小社区阅读 | 2500 | 1500 | 160 | 80 | 1800 |
|
||||
| 中社区阅读 | 2500 | 1500 | 60 | 40 | 1800 |
|
||||
| 大社区阅读 | 2500 | 1200 | 80 | 20 | 1500 |
|
||||
| 超大社区内部地图 | 2500 | 800 | 40 | 0 | 1200 |
|
||||
|
||||
这些数字是第一阶段的安全预算,不是长期上限。后续全局大图换到 Sigma/Graphology 后,可以继续用真实测量调高或调低,但不能取消预算机制。
|
||||
|
||||
### 节点
|
||||
|
||||
- 5000+:全局默认点位为主,只显示核心标签。
|
||||
- 10000+:进一步减少标签,只保留搜索命中、选中、核心节点和社区代表节点。
|
||||
- 节点大小按重要性变化,但普通节点不变成卡片。
|
||||
- 普通节点默认不显示文字;只有搜索命中、选中、Pin、社区代表节点和少数高连接节点显示标签。
|
||||
- 全局视角不显示节点卡片。
|
||||
- 搜索命中、Pin 和选中节点可以提升显示优先级,但不能突破当前视角的预算上限。
|
||||
- 超出预算的节点进入右侧抽屉列表或搜索结果列表,不继续堆在画布上。
|
||||
|
||||
### 边
|
||||
|
||||
- 默认只显示重要边、强关系边、社区间骨架边。
|
||||
- 弱边默认隐藏或极淡。
|
||||
- 拖动、缩放、平移过程中隐藏更多边。
|
||||
- 停止交互后再补回允许显示的边。
|
||||
- 边数量必须受当前视角预算控制。
|
||||
- 搜索、筛选和选中对象可以提高相关边的优先级,但不能恢复全量边。
|
||||
|
||||
### 标签
|
||||
|
||||
- 标签预算按当前可见节点数控制。
|
||||
- 标签优先级:搜索命中 > 选中节点 > Pin 节点 > 社区代表节点 > 高连接节点。
|
||||
- 普通节点标签只能在悬停、轻量抽屉或社区聚焦中出现。
|
||||
- 搜索命中可以全部高亮,但不能全部显示标签。
|
||||
- Pin 节点可以提高标签优先级,但 Pin 数量超过预算时,超出的 Pin 进入抽屉里的固定列表。
|
||||
|
||||
### 卡片
|
||||
|
||||
- 全局视角不显示节点卡片。
|
||||
- 社区聚焦允许卡片,但必须有卡片预算。
|
||||
- 卡片优先级:当前选中节点 > 打开的阅读节点 > 搜索命中 > Pin > 核心节点。
|
||||
- 超出预算的卡片降级为点或标签,并进入抽屉列表。
|
||||
- 卡片数量不能因为搜索命中或 Pin 数量增加而无限增长。
|
||||
|
||||
### 重要性排序
|
||||
|
||||
核心节点、重要边、强关系由系统自动排序,排序规则必须可解释。排序分为稳定结构评分和临时上下文加权。
|
||||
|
||||
稳定结构评分决定长期核心节点、社区代表节点和骨架边。默认信号包括:
|
||||
|
||||
- 连接数和局部中心性。
|
||||
- 跨社区桥接能力。
|
||||
- 社区代表性。
|
||||
|
||||
临时上下文加权只影响当前交互下的短期高亮和显示优先级。默认信号包括:
|
||||
|
||||
- 与当前搜索或筛选条件的相关度。
|
||||
- Pin。
|
||||
- 最近访问、最近选中或当前阅读路径。
|
||||
- 与当前选中对象的一跳关系强度。
|
||||
|
||||
Pin 是强信号,但不是唯一标准。没有足够信号时,系统优先选择结构上能解释社区的节点和边,而不是随机展示。核心节点和社区代表节点不应因为一次搜索、筛选或阅读路径变化而频繁改写。
|
||||
|
||||
### 社区
|
||||
|
||||
- 社区是全局大图的主要组织单位。
|
||||
- 当总节点超过性能阈值时,全局优先展示社区结构。
|
||||
- 如果完整节点图会影响流畅度,必须进入社区聚合或强降级显示,不能继续硬画全量细节。
|
||||
- 社区聚合视图中,社区本身是节点,社区之间的连接代表跨社区关系强度。
|
||||
- 社区聚合是全局视角的一种渲染层级,不是新的产品模式;点击后仍然先进入社区摘要,再由按钮进入社区聚焦。
|
||||
- 社区聚合节点必须强区分,呈现为装着很多节点的容器,而不是普通内容节点。
|
||||
- 聚合节点应显示社区名、节点数量、边界或内部点阵提示。
|
||||
- 聚合容器必须承载内部选中、搜索命中和 Pin 的可见提示。
|
||||
- 当前选中节点在聚合容器内时,抽屉继续显示该节点,容器显示选中标记。
|
||||
- 搜索命中在聚合容器内时,容器显示命中数量。
|
||||
- Pin 节点在聚合容器内时,容器显示 Pin 标记或数量。
|
||||
- 用户点击聚合容器后,先看社区摘要;如果容器内部有搜索命中或 Pin,抽屉直接列出这些内部对象。
|
||||
|
||||
### 社区聚焦规模
|
||||
|
||||
社区聚焦必须保留完整社区的空间轮廓和节点存在感,但完整在场不等于所有节点都升级成卡片、标签或阅读内容。
|
||||
|
||||
- 小社区:可以显示更多卡片和标签,进入后接近阅读状态。
|
||||
- 中社区:全部节点在场,大部分以点显示,核心、选中、搜索命中和 Pin 节点升级为卡片。
|
||||
- 大社区:保留完整轮廓和节点点位,严格限制卡片和标签数量,更多内容进入右侧抽屉列表。
|
||||
- 超大社区:先显示社区内部地图,包括子主题、核心节点和主要关系;用户再进入子主题继续阅读。
|
||||
- 超大社区不是回到全局,而是社区内部的第二层组织。
|
||||
- 当前分界值:40 个节点以内按小社区处理,250 个以内按中社区处理,1000 个以内按大社区处理,超过 1000 个进入超大社区内部地图。
|
||||
- 后续可按真实图谱继续校准,但调整时必须同时参考节点数、边数、最大连通密度和卡片预算。
|
||||
|
||||
### 社区质量兜底
|
||||
|
||||
社区仍是默认组织方式,但系统需要识别社区结构不清晰的情况。
|
||||
|
||||
触发信号包括:
|
||||
|
||||
- 单个社区过大。
|
||||
- 很多社区过小。
|
||||
- 社区之间关系混杂。
|
||||
- 社区命名或主题弱。
|
||||
- 社区数量异常。
|
||||
|
||||
出现这些情况时:
|
||||
|
||||
- 不自动切换视图。
|
||||
- 不弹窗打断用户。
|
||||
- 在组织区域给轻提示:当前社区结构不够清晰,可用核心结构辅助查看。
|
||||
- 第一阶段只提供“核心结构 / 连接度”这一种轻量辅助查看,不新增完整备用状态。
|
||||
- 类型、来源、时间等组织方式依赖数据质量,进入后续探索,不进第一阶段。
|
||||
- 社区质量一般时,只给轻提示。
|
||||
- 社区质量很差时,弱化社区色块、边界和命名的视觉确定感,突出核心结构和连接关系。
|
||||
- 抽屉中说明社区划分可信度较低,避免把不可靠社区包装成确定结论。
|
||||
|
||||
### 动画与物理布局
|
||||
|
||||
- 全局图谱不应持续运行高消耗动态布局。
|
||||
- 初始布局稳定后应停下来。
|
||||
- 拖动时只更新必要对象。
|
||||
- 社区聚焦可以允许更丰富的局部动画,但仍要受节点数量限制。
|
||||
- 拖动、缩放、平移时可以临时弱化普通标签、弱边和复杂效果。
|
||||
- 降级必须保持视觉连续性,使用淡出淡入,不突然跳变。
|
||||
- 交互中核心锚点、选中对象、搜索命中和 Pin 不消失。
|
||||
- 选中对象、搜索命中、Pin 和核心节点是视觉锚点;切换、聚合、展开和降级时必须尽量保持可追踪。
|
||||
- 聚合时,普通节点应表现为收进社区容器,而不是瞬间消失。
|
||||
- 从聚合进入社区时,社区容器应展开为内部地图。
|
||||
- 如果必须换布局,右侧抽屉仍保留当前对象和上下文,避免用户丢失正在看的内容。
|
||||
- 动画追求不迷路,不追求炫技。
|
||||
|
||||
## 技术方向
|
||||
|
||||
本设计不在这一阶段锁死具体库,但锁死技术主线:
|
||||
|
||||
**一个图谱数据底座,一个全局大图渲染器,一套社区/筛选/搜索规则,细节阅读由抽屉和社区聚焦承接。**
|
||||
|
||||
当前图谱仍是 DOM/SVG 渲染:节点是按钮,边是 SVG path。这个方向不适合继续承担万级全局大图,但仍适合承接社区阅读、小图、离线细节和界面化内容。
|
||||
|
||||
技术边界:
|
||||
|
||||
- 全局大图:后续通过 Canvas / WebGL / 聚合渲染方案验证,目标是承接 5000+ / 10000+ 地图级浏览。
|
||||
- 社区阅读:继续保留现有 DOM/SVG 和界面组件路线,重点是卡片、标签、抽屉和阅读体验。
|
||||
- 产品语义:图谱数据、社区、搜索、筛选、Pin、抽屉状态和阅读路径保持同一套,不因为全局渲染方案变化而重做。
|
||||
- 坐标和选择:全局渲染器和社区阅读层必须共享同一套对象 id、社区 id、选中状态和搜索结果。
|
||||
|
||||
可评估的全局大图渲染方向:
|
||||
|
||||
- `Sigma + Graphology`:已通过本轮试验,选为后续唯一全局大图主线。
|
||||
- `vis-network`:已完成对照试验,作为备选记录,但不进入下一步主线。
|
||||
- 聚合降级:保留为全局大图的结构总览和降级策略,不作为第二套长期产品。
|
||||
- 当前 DOM/SVG:继续用于小图、社区阅读或离线细节模式,不再要求硬撑万级全局图。
|
||||
|
||||
明确不做:
|
||||
|
||||
- 不同时引入 `Sigma` 和 `vis-network` 两套全局渲染器。
|
||||
- 不做三套坐标、三套点击、三套筛选。
|
||||
- 不把卡片和长文本塞进万级全局画布。
|
||||
- 不因为引入大图渲染器而丢掉现有阅读抽屉和社区体验。
|
||||
|
||||
后续探索记录:
|
||||
|
||||
- Agent 提问入口不进入第一阶段,后续单独讨论。
|
||||
- 边单击关系检查不进入第一阶段,后续作为关系理解能力讨论;节点和社区抽屉中的强关系、桥接关系摘要保留。
|
||||
- 桌面版专项方案不进入第一阶段,后续单独讨论。
|
||||
- 桌面版方向应保持同一套图谱语义:图谱数据、社区检测、搜索和阅读抽屉不另起一套产品逻辑。
|
||||
- 当前设计必须避免绑定只能在浏览器网页里成立的交互和数据结构;未来桌面版复用同一套图谱语义,只替换外壳、存储和性能执行层。
|
||||
|
||||
## 与参考项目的关系
|
||||
|
||||
### graphify
|
||||
|
||||
借鉴:
|
||||
|
||||
- 大图全局降级。
|
||||
- 超大图社区聚合。
|
||||
- 只给关键节点显示标签。
|
||||
- 交互时隐藏部分边。
|
||||
- 稳定后停止高消耗布局。
|
||||
|
||||
不照搬:
|
||||
|
||||
- 不把 llm-wiki 做成静态 HTML 图谱工具。
|
||||
- 不把完整阅读体验交给图谱画布。
|
||||
- 不让社区聚合替代社区阅读。
|
||||
|
||||
### nashsu/llm_wiki
|
||||
|
||||
借鉴:
|
||||
|
||||
- 桌面应用方向。
|
||||
- 成熟图谱库承接大图。
|
||||
- 布局计算后台化。
|
||||
- 节点数越大,标签和边越克制。
|
||||
- 图谱洞察面板的产品思路。
|
||||
|
||||
不照搬:
|
||||
|
||||
- 不放弃 llm-wiki 现有的社区聚焦、Pin、离线图谱和阅读抽屉。
|
||||
- 不把图谱只做成结构浏览器。
|
||||
|
||||
### 当前 llm-wiki graph-engine
|
||||
|
||||
保留:
|
||||
|
||||
- 社区色块和社区图例。
|
||||
- 社区聚焦。
|
||||
- 筛选和搜索。
|
||||
- Pin。
|
||||
- 右侧抽屉。
|
||||
- 离线 HTML 图谱能力。
|
||||
- 现有图谱数据语义。
|
||||
|
||||
调整:
|
||||
|
||||
- 全局视角减少卡片化表达。
|
||||
- 点击节点的右抽屉改成轻量查看。
|
||||
- 社区聚焦作为细节阅读主入口。
|
||||
- 大规模场景引入更强的降级和聚合规则。
|
||||
|
||||
## 产品状态机
|
||||
|
||||
核心状态只有四类:
|
||||
|
||||
1. `global`
|
||||
- 全局地图。
|
||||
- 支持筛选、搜索、全局概览、节点轻量查看、社区摘要。
|
||||
|
||||
2. `global + selected community`
|
||||
- 全局地图保持不变。
|
||||
- 右侧轻量抽屉显示社区摘要。
|
||||
- 可以查看核心节点列表或进入社区聚焦。
|
||||
|
||||
3. `global + selected node`
|
||||
- 全局地图保持不变。
|
||||
- 右侧轻量抽屉显示节点信息。
|
||||
- 可以进入所属社区。
|
||||
- “打开详情 / 阅读”进入所属社区,并自动选中该节点。
|
||||
|
||||
4. `community focus`
|
||||
- 聚焦社区。
|
||||
- 完整社区节点都在场。
|
||||
- 核心、选中、搜索命中、Pin 节点升级为卡片。
|
||||
- 承接社区阅读抽屉。
|
||||
- 支持返回全局。
|
||||
|
||||
不新增“自动阅读模式”或“缩放触发模式”。
|
||||
|
||||
辅助查看不是核心状态:
|
||||
|
||||
- “核心结构 / 连接度”只改变当前图的视觉强调和排序,不创建新的产品模式。
|
||||
- 辅助查看不拥有独立抽屉逻辑,不替代社区摘要或节点摘要。
|
||||
- 类型、来源、时间等备用组织如果后续进入设计,需要另补状态和抽屉规则。
|
||||
|
||||
高级选区不是核心状态:
|
||||
|
||||
- Shift+点击、多选、邻居扩展可以继续存在,但它是高级分析选区。
|
||||
- 高级选区也使用右侧抽屉显示摘要和动作,不改变全局或社区主模式。
|
||||
- 如果它影响大图流畅度,优先降级它的视觉表达,而不是让普通单击变复杂。
|
||||
|
||||
## 当前实现对齐结论
|
||||
|
||||
当前图谱已有一些旧交互,其中一部分可以保留,一部分需要在后续实现时改语义。
|
||||
|
||||
保留:
|
||||
|
||||
- 滚轮/触控板在图谱区域内缩放图谱。
|
||||
- 拖动空白处平移图谱。
|
||||
- 工具栏里的“回全图”作为返回全图主入口。
|
||||
- 双击空白回全图,作为辅助快捷方式。
|
||||
- 节点优先于社区命中。
|
||||
|
||||
需要调整:
|
||||
|
||||
- 社区色块和社区图例的单击不应直接进入社区聚焦,应先显示社区摘要。
|
||||
- 单击空白不应退出社区聚焦,只应取消当前选中。
|
||||
- 节点单击在全局视角不应直接打开完整阅读,应先打开轻量节点摘要。
|
||||
- 节点双击取消固定位置不应继续作为主设计,应改成明确按钮或菜单动作。
|
||||
- 边的双击不应承担回全图,因为边未来可能需要轻量关系查看。
|
||||
- 旧交互直接切到新规则,不做旧主路径兼容。
|
||||
|
||||
需要区分命名:
|
||||
|
||||
- “回全图”是视角导航:回到全局地图并 fit 全图。
|
||||
- “重置布局”是布局操作:清除用户拖动形成的固定位置。
|
||||
- 这两个动作不应共用同一文案或图标暗示。
|
||||
|
||||
## 旧交互调整方式
|
||||
|
||||
这一节用于把当前已有交互和新设计对齐。后续实现时,应按用户心智调整,不为了迁就旧行为保留隐蔽或多义的手势。
|
||||
|
||||
### 从“单击直接进入”改成“先检查,再深入”
|
||||
|
||||
社区色块和社区图例:
|
||||
|
||||
- 旧行为:单击后直接进入社区聚焦。
|
||||
- 新行为:单击后只选中社区,右侧抽屉显示社区摘要,全局图谱保持原视角。
|
||||
- 深入动作:用户在抽屉里点“进入社区”后,才进入社区聚焦。
|
||||
- 辅助动作:“查看核心节点列表”只在抽屉内展开列表;悬停列表项时轻微高亮图上节点,不改变主图视角。
|
||||
|
||||
普通节点:
|
||||
|
||||
- 旧行为:单击后偏完整阅读。
|
||||
- 新行为:单击后只显示节点轻量摘要,全局图谱保持原视角。
|
||||
- 深入动作:用户在抽屉里点“进入所属社区”或“打开详情 / 阅读”,都进入所属社区;“打开详情 / 阅读”会自动选中该节点。
|
||||
- 不建议把节点双击作为进入社区或打开详情的主入口。
|
||||
|
||||
关系边:
|
||||
|
||||
- 旧行为:边双击可能承担回全图。
|
||||
- 新行为:边不承担导航动作。
|
||||
- 后续方向:边单击可以显示关系检查,例如关系强度、两端节点、所属社区和相关证据;这个方向不进入第一阶段。
|
||||
|
||||
### 从“空白处改变模式”改成“空白处只清选择”
|
||||
|
||||
空白单击:
|
||||
|
||||
- 在全局视角,只清空当前选中的节点或社区。
|
||||
- 在社区聚焦里,只清空当前选中的节点或社区,仍留在当前社区;关系选择留到后续边单击关系检查再定义。
|
||||
- 不退出社区聚焦,不返回全图,不改变缩放。
|
||||
|
||||
空白双击:
|
||||
|
||||
- 只在真正点到图谱空白背景时,作为回全图快捷方式。
|
||||
- 不在节点、社区色块、边、图例、搜索框、工具栏或右侧抽屉上触发。
|
||||
- 它是快捷方式,不是主路径;主路径仍然是可见的“回全图”按钮。
|
||||
|
||||
### 从“隐藏手势”改成“明确按钮或菜单”
|
||||
|
||||
固定 / 取消固定:
|
||||
|
||||
- 旧行为:节点双击取消固定位置。
|
||||
- 新行为:在节点轻量抽屉或节点工具菜单里提供“固定位置 / 取消固定位置”。
|
||||
- 旧的双击取消固定不作为第一阶段主路径保留。
|
||||
|
||||
回全图:
|
||||
|
||||
- 使用明确的“回全图”按钮。
|
||||
- 行为是回到全局地图并重新 fit 全图。
|
||||
- 不清除 Pin,不清除用户固定的位置。
|
||||
|
||||
重置布局:
|
||||
|
||||
- 使用明确的“重置布局”按钮。
|
||||
- 行为是清除用户拖动形成的固定位置,让布局重新整理。
|
||||
- 不承担返回全图或退出社区聚焦。
|
||||
|
||||
### 抽屉内容随选中对象变化
|
||||
|
||||
全局视角共用一个右侧轻量抽屉,但内容和动作必须跟随选中对象变化:
|
||||
|
||||
- 选中社区:显示社区摘要、核心节点、桥接关系、“进入社区”、“查看核心节点列表”。
|
||||
- 选中节点:显示节点摘要、所属社区、重要邻居、强关系、“进入所属社区”、“打开详情 / 阅读”。
|
||||
- 没有选中对象:显示全局概览、搜索结果或空状态。
|
||||
- 选中对象被筛选排除:显示该对象摘要、提示不在当前结果中,并提供“清除选择”“显示该对象”。
|
||||
|
||||
这能避免用户误以为“点社区”和“点节点”进入的是同一个详情逻辑。抽屉可以是同一个位置、同一个组件,但用户看到的内容层级和动作必须不同。
|
||||
|
||||
## 抽屉设计
|
||||
|
||||
右侧抽屉分两个使用场景,不必一定是两套组件,但用户感知要清楚:
|
||||
|
||||
- 全局轻量抽屉:在全局地图里检查当前选中的节点、社区、搜索结果或全局概览。
|
||||
- 社区阅读抽屉:进入社区聚焦后承接更完整的阅读和理解。
|
||||
|
||||
### 全局轻量抽屉
|
||||
|
||||
目的:检查当前选中的对象,不打断地图。
|
||||
|
||||
选中节点时的推荐结构:
|
||||
|
||||
1. 标题区:节点名、类型、所属社区、连接数。
|
||||
2. 关系区:最强关系 1-2 条。
|
||||
3. 邻居区:最重要相邻节点 3-5 个。
|
||||
4. 动作区:进入所属社区、打开详情。
|
||||
|
||||
选中社区时的推荐结构:
|
||||
|
||||
1. 标题区:社区名、节点数量、核心节点数量。
|
||||
2. 摘要区:社区主题和主要内容范围。
|
||||
3. 结构区:核心节点、桥接到其他社区的关系。
|
||||
4. 动作区:进入社区、查看核心节点列表。
|
||||
|
||||
视觉要求:
|
||||
|
||||
- 窄、轻、信息克制。
|
||||
- 不使用大段正文。
|
||||
- 不撑大图谱布局。
|
||||
- 同一个抽屉根据选中对象换内容,不额外弹第二层面板。
|
||||
|
||||
空状态要求:
|
||||
|
||||
- 没有强关系时,直接显示“暂无强关系”,并给“进入所属社区”或“查看邻居”动作。
|
||||
- 没有邻居时,直接显示“暂无相邻节点”,不使用推荐内容伪装。
|
||||
- 搜索无结果时,显示“没有搜索结果”,并给“清空搜索”动作。
|
||||
- 社区摘要缺失时,显示“暂无社区摘要”,但仍显示节点数量、核心节点和进入社区动作。
|
||||
- 数据刷新后选中对象不存在时,显示“当前对象已不可用”,并给“清除选择”动作。
|
||||
|
||||
慢操作和失败状态:
|
||||
|
||||
| 状态 | 用户看到什么 | 仍可做什么 | 恢复动作 |
|
||||
|---|---|---|---|
|
||||
| 大图初次加载 | 先显示轻量骨架或加载中的地图区域,不显示空白页 | 可以看到加载进度 | 加载完成后进入全局地图 |
|
||||
| 布局整理中 | 显示正在整理的轻提示,结构逐步稳定 | 可以缩放、查看已出现对象 | 整理完成后停止高消耗布局 |
|
||||
| 搜索进行中 | 搜索框和图谱给轻反馈,旧结果先保留 | 可以继续输入或清空搜索 | 新结果完成后替换高亮和列表 |
|
||||
| 抽屉内容加载中 | 抽屉先打开,再显示加载态 | 可以关闭抽屉或切换对象 | 内容完成后填入摘要 |
|
||||
| 进入社区中 | 当前对象和社区容器保持可见,显示过渡提示 | 可以取消或等待 | 完成后进入社区聚焦 |
|
||||
| 性能降级中 | 提示已切到轻量显示,不作为错误处理 | 可以继续浏览、搜索、点选 | 停止交互后恢复预算内细节 |
|
||||
| 数据缺失 | 直接显示缺失原因和下一步动作 | 可以清除选择或返回全局 | 数据恢复后重新选择 |
|
||||
| 硬错误 | 显示错误状态,不伪装成空结果 | 可以重试或返回全局 | 重试成功后恢复当前视角 |
|
||||
|
||||
### 社区阅读抽屉
|
||||
|
||||
目的:帮助理解一个社区。
|
||||
|
||||
推荐结构:
|
||||
|
||||
1. 社区标题和摘要。
|
||||
2. 核心节点列表。
|
||||
3. 内部强关系。
|
||||
4. 桥接到其他社区的关系。
|
||||
5. 可进一步展开一跳邻居或打开节点详情。
|
||||
|
||||
视觉要求:
|
||||
|
||||
- 信息可以比全局轻量抽屉更重。
|
||||
- 允许阅读型内容。
|
||||
- 仍避免一次塞满所有节点。
|
||||
|
||||
## 验收标准
|
||||
|
||||
设计层验收:
|
||||
|
||||
1. 用户能清楚理解:全局是地图,社区是阅读。
|
||||
2. 普通节点点击不会让用户突然丢失全局上下文。
|
||||
3. 社区色块/图例点击作为社区检查入口合理保留,进入社区聚焦需要再点明确按钮。
|
||||
4. 右侧抽屉不再被定义为完整阅读入口,而是根据状态承担轻量查看或社区阅读。
|
||||
5. 5000+ / 10000+ 大图有明确降级规则。
|
||||
6. 方案没有同时引入多套全局图谱渲染主线。
|
||||
7. 选中优先级清楚:节点 > 社区 > 搜索列表 > 全局概览。
|
||||
8. 社区聚合被定义为全局渲染层级,而不是额外产品模式,但聚合节点在视觉上必须强区分。
|
||||
9. 返回全图有可见按钮,双击空白只是辅助快捷方式。
|
||||
10. 单击空白不承担返回全图或退出社区聚焦。
|
||||
11. 全局“打开详情 / 阅读”会进入所属社区并选中节点,不在全局承载完整阅读。
|
||||
12. 社区聚焦里完整社区都在场,只有重点节点升级为卡片。
|
||||
13. 社区质量不好时不打断用户,但要降低不可靠社区边界的视觉确定感。
|
||||
14. 核心键盘路径可用。
|
||||
15. 第一阶段范围和暂缓范围清楚,不把长期探索混进立即实现。
|
||||
|
||||
分阶段实现验收:
|
||||
|
||||
### 阶段一:测量和现有体验降级
|
||||
|
||||
1. 1000 节点只作为回归基线,不作为目标上限。
|
||||
2. 固化性能测量,覆盖真实图谱、压力图谱、极限图谱。
|
||||
3. 每档测试图谱记录节点数、边数、社区数、最大社区规模、搜索命中数、Pin 数、是否存在超大社区。
|
||||
4. 1000 节点全局缩放、平移、悬停、节点点击保持流畅。
|
||||
5. 当前 DOM/SVG 全局图只要求完成可测量降级,不承诺直接达到 5000+ / 10000+ 正式目标。
|
||||
6. 全局节点点击只打开轻量抽屉,不自动进入社区。
|
||||
7. 全局节点的“打开详情 / 阅读”进入所属社区并选中该节点。
|
||||
8. 社区色块/图例点击先显示社区摘要,点击“进入社区”后进入社区聚焦。
|
||||
9. 社区摘要里的“查看核心节点列表”只展开抽屉列表,点击列表项不自动进入社区。
|
||||
10. 社区聚焦后完整社区节点都在场,但按点、标签、卡片和子主题分层表达。
|
||||
11. 交互过程中弱边、普通标签和复杂效果可以临时淡出,停止后淡入预算内细节。
|
||||
12. 交互中核心锚点、选中对象、搜索命中和 Pin 不消失。
|
||||
13. 全局图谱不能因为抽屉内容变化而重排大图。
|
||||
14. 全局中点击空白处能清空当前选中对象,抽屉回到全局概览或搜索结果列表。
|
||||
15. 搜索、筛选、Pin 不应自动覆盖用户当前选中的节点或社区。
|
||||
16. 已选对象被筛选排除时,抽屉保留对象并提供“清除选择”“显示该对象”。
|
||||
17. “显示该对象”采用临时例外显示,不清空搜索或筛选。
|
||||
18. 社区聚焦中单击空白不返回全图;点“回全图”或双击空白才返回全图。
|
||||
19. 回全图后视角 fit 全图,搜索、筛选、Pin 和当前选择保留。
|
||||
20. 节点双击不进入社区,不返回全图;固定/取消固定应有明确按钮或菜单入口。
|
||||
21. 边第一阶段不可点,但抽屉保留强关系和桥接关系摘要。
|
||||
22. “回全图”和“重置布局”在文案和行为上保持区分。
|
||||
23. 抽屉缺少信息时显示明确空状态和下一步动作。
|
||||
24. 搜索结果、抽屉按钮、“核心结构辅助查看”和“回全图”支持基本键盘操作。
|
||||
|
||||
### 阶段二:全局大图方案验证
|
||||
|
||||
1. 用同一份数据验证候选全局大图方案。
|
||||
2. 验证对象包括全局打开、拖动、缩放、搜索高亮、节点点选、社区摘要、聚合容器提示。
|
||||
3. 只保留一个全局大图渲染主线。
|
||||
4. 全局大图方案必须能和现有社区阅读、抽屉、搜索、Pin 共享同一套对象语义。
|
||||
5. 通过验证后,才把 5000+ / 10000+ 作为正式实现验收目标。
|
||||
|
||||
### 阶段三:5000+ / 10000+ 正式验收
|
||||
|
||||
1. 5000+ 节点全局视角能稳定浏览。
|
||||
2. 10000+ 节点至少能通过社区聚合或强降级视图流畅浏览。
|
||||
3. 真实图谱、压力图谱、极限图谱都必须有性能记录。
|
||||
4. 必测动作:打开图谱、拖动、平移、缩放、节点点击、抽屉打开、搜索高亮、进入社区、返回全图。
|
||||
5. 必测风险:内存是否持续上涨、画面是否大跳变、当前对象是否丢失、搜索结果是否迟迟不出现。
|
||||
6. 产品可感知失败包括:拖动不跟手、缩放卡住、抽屉慢开、搜索高亮迟迟不出现、画面大跳变。
|
||||
7. 当前 DOM/SVG 已测出 5000 节点存在平移或抽屉超时风险,10000 节点和超大社区缩放帧率明显不足;这些结果不作为失败遮掩,而是作为后续 Sigma/Graphology 集成的依据。
|
||||
8. 后续 Sigma/Graphology 集成计划必须继续记录打开、拖动、缩放、搜索、点选、抽屉、进入社区、返回全图等动作的时间和流畅度。
|
||||
|
||||
## 后续实现建议
|
||||
|
||||
下一步不应让当前 DOM/SVG 全局图直接硬撑万级,也不应再同时推进多套全局渲染方案。本轮测量和选型已经完成,后续应围绕 Sigma/Graphology 写单一路线集成计划。
|
||||
|
||||
已完成的前置工作:
|
||||
|
||||
- 固化性能测量:覆盖真实代理图、1000、5000、10000、超大社区、很多小社区、很多搜索命中和很多 Pin 节点。
|
||||
- 定义降级策略:全局卡片为零、标签和边受预算约束,搜索命中、选中和 Pin 通过优先级保留。
|
||||
- 定义重要性排序:稳定结构评分和临时上下文加权分开。
|
||||
- 选型试验:Sigma/Graphology、vis-network 和聚合降级均已测量;下一步集成 Sigma/Graphology,聚合只作为降级策略。
|
||||
|
||||
下一步实现计划应只集成 Sigma/Graphology 这一条全局大图路线,并继续保留 DOM/SVG 作为小图、离线细节和社区阅读路线。
|
||||
|
||||
## 后续探索停车场
|
||||
|
||||
这些议题保留,但不进入第一阶段。
|
||||
|
||||
### Agent 提问入口
|
||||
|
||||
- 暂缓原因:需要先稳定图谱对象、社区摘要和关系摘要,否则 Agent 无法可靠理解当前上下文。
|
||||
- 后续讨论:Agent 如何基于当前选中节点、社区、搜索结果提出问题或生成探索建议。
|
||||
- 触发条件:全局轻量抽屉、社区阅读抽屉和对象选中语义稳定后再讨论。
|
||||
|
||||
### 边单击关系检查
|
||||
|
||||
- 暂缓原因:第一阶段边只承担视觉结构,避免把关系详情、证据和选择优先级一次性做复杂。
|
||||
- 后续讨论:点击一条边后如何展示关系强度、两端节点、所属社区和相关证据。
|
||||
- 触发条件:节点/社区抽屉中的强关系和桥接关系摘要稳定后再讨论。
|
||||
|
||||
### 多套备用组织方式
|
||||
|
||||
- 暂缓原因:类型、来源、时间依赖数据质量,过早加入会变成第二套图谱系统。
|
||||
- 后续讨论:类型、来源、时间等组织方式在什么数据覆盖率下可用,以及是否拥有独立摘要。
|
||||
- 触发条件:核心结构辅助查看验证有效,并且图谱数据字段覆盖率稳定后再讨论。
|
||||
|
||||
### 移动端和窄屏专项
|
||||
|
||||
- 暂缓原因:第一阶段主体验是桌面宽屏大图探索,移动端交互、抽屉和手势需要单独设计。
|
||||
- 后续讨论:窄屏下抽屉位置、手势冲突、搜索和社区阅读如何重排。
|
||||
- 触发条件:桌面大图主路径稳定后再讨论。
|
||||
|
||||
### 桌面版专项方案
|
||||
|
||||
- 暂缓原因:桌面版需要单独讨论外壳、存储、本地缓存、后台布局和导入导出。
|
||||
- 后续讨论:如何复用同一套图谱语义,同时替换性能执行层和本地能力。
|
||||
- 触发条件:全局大图渲染路线确定后再讨论。
|
||||
|
||||
### 完整键盘漫游
|
||||
|
||||
- 暂缓原因:第一阶段只保证核心键盘路径,不让键盘承担遍历万级节点的主任务。
|
||||
- 后续讨论:如何在不低效遍历大量节点的前提下提供可访问的图谱探索方式。
|
||||
- 触发条件:核心路径稳定,并且需要面向更完整的无障碍验收时再讨论。
|
||||
|
||||
### 智能阅读路径排序
|
||||
|
||||
- 暂缓原因:长期阅读历史、最近访问和 Agent 推荐容易让核心节点排序变得不稳定。
|
||||
- 后续讨论:如何把长期行为信号作为辅助,而不破坏稳定结构评分。
|
||||
- 触发条件:稳定结构评分和临时上下文加权验证清楚后再讨论。
|
||||
@@ -0,0 +1,360 @@
|
||||
# Sigma/Graphology Global Graph Renderer Design
|
||||
|
||||
日期:2026-06-19
|
||||
状态:待用户复核
|
||||
当前分支:`codex/pr52-sigma-global-renderer-design`
|
||||
|
||||
## 目的
|
||||
|
||||
这份设计确定 Sigma/Graphology 如何正式接入 llm-wiki 的全局图谱视角。
|
||||
|
||||
前一轮性能计划已经选择 Sigma/Graphology 作为未来全局大图渲染路线,但它只完成了路线选择和试验验证,没有切换生产图谱。当前任务不是继续做隐藏试验,也不是重新评估 Sigma、vis-network、聚合方案,而是把 Sigma/Graphology 作为首选正式全局路线接入的产品和工程边界定清楚,方便后续直接写实施计划。
|
||||
|
||||
这里的 Phase 0 是“正式接入前的最后开工门槛”,不是重新开启选型。如果 Phase 0 发现 Sigma 存在硬阻塞,本实施方向应停止并记录 blocker,再另开技术路线决策;不要在同一份落地计划里临时转向 vis-network。
|
||||
|
||||
本设计延续 2026-06-18 的大图体验结论:
|
||||
|
||||
**全局图谱负责看结构,社区聚焦负责读内容。**
|
||||
|
||||
## 设计结论
|
||||
|
||||
采用“全局统一 Sigma,社区继续阅读”的首选正式路线。
|
||||
|
||||
1. 所有全局视角统一使用 Sigma/Graphology,不按节点数量分成大小图两套全局渲染主路径。
|
||||
2. 全局视角只承担地图级结构浏览:社区分布、节点点位、关键标签、搜索命中、Pin、选中和高亮。
|
||||
3. 社区阅读、离线详情、内容阅读和富信息卡片继续使用现有 DOM/SVG 阅读路径,不在本次强行改成 Sigma。
|
||||
4. Sigma/Graphology 是首选正式全局路线,不是隐藏开关或实验入口;Phase 0 只决定是否允许进入正式接入,不在本计划内改选其它渲染路线。
|
||||
5. 旧 DOM/SVG 全局图不再作为用户可选的第二套主路径,只保留为小图异常兜底和社区/详情阅读能力。
|
||||
6. 如果 WebGL 不可用、Sigma 初始化失败或画布异常,系统按规模进入明确兜底:小图退回现有 DOM/SVG,全局大图进入最低可用的聚合安全视图,并记录可排查原因。
|
||||
|
||||
一句话:
|
||||
|
||||
**全局统一成一张高性能地图;社区继续承担阅读和理解。**
|
||||
|
||||
## 为什么不选“大图才用 Sigma”
|
||||
|
||||
只让大图走 Sigma、小图继续走当前全局 DOM/SVG,短期改动较小,但长期会留下两套全局图。
|
||||
|
||||
同样是全局视角,搜索、高亮、Pin、选中、筛选、社区摘要、返回全局等行为就必须在两条渲染路径里持续对齐。后续任何体验调整都容易变成双份维护,也容易让小图和大图出现细节不一致。
|
||||
|
||||
本次不以 MVP 思维处理,因此选择长期边界更清楚的方案:只要是全局视角,就统一走 Sigma/Graphology。
|
||||
|
||||
## 为什么不把社区也改成 Sigma
|
||||
|
||||
社区阅读不是单纯画更多点。社区内需要完整节点在场、核心节点卡片、摘要、阅读抽屉、节点内容、上下文动作和更高信息密度。
|
||||
|
||||
Sigma/Graphology 适合承担全局地图的高性能绘制和视口交互;社区阅读更适合继续使用现有富交互 DOM/SVG 路径。把全局和社区一次性全改成 Sigma 会扩大风险,也会偏离已经确定的“全局看结构,社区读内容”。
|
||||
|
||||
## 用户体验边界
|
||||
|
||||
### 全局视角
|
||||
|
||||
全局视角是地图,不是阅读界面。
|
||||
|
||||
显示:
|
||||
|
||||
- 节点点位。
|
||||
- 社区颜色和社区边界。
|
||||
- 少量关键节点标签。
|
||||
- 重要边或社区间骨架关系。
|
||||
- 搜索命中。
|
||||
- 当前选中对象。
|
||||
- Pin 提示。
|
||||
- 社区图例和筛选结果。
|
||||
|
||||
降级或不显示:
|
||||
|
||||
- 普通节点卡片。
|
||||
- 大段正文。
|
||||
- 全量标签。
|
||||
- 全量弱边。
|
||||
- 会导致大图卡顿的复杂阴影、动画和富组件。
|
||||
|
||||
### 社区阅读和详情
|
||||
|
||||
社区阅读继续承担内容理解。
|
||||
|
||||
进入社区后:
|
||||
|
||||
- 社区内节点完整在场。
|
||||
- 核心、选中、搜索命中和 Pin 节点可以升级为更高信息密度。
|
||||
- 继续使用现有阅读抽屉、节点详情和社区摘要能力。
|
||||
- 社区阅读路径不被 Sigma 的内部状态接管。
|
||||
|
||||
## 责任边界
|
||||
|
||||
### 图谱引擎负责什么
|
||||
|
||||
`packages/graph-engine/` 继续作为图谱语义和产品规则的唯一来源。
|
||||
|
||||
它负责:
|
||||
|
||||
- 节点、边和社区身份。
|
||||
- 搜索结果。
|
||||
- 筛选状态。
|
||||
- Pin 状态。
|
||||
- 选择状态。
|
||||
- 社区聚焦。
|
||||
- 聚合标记。
|
||||
- 全局摘要、节点摘要、社区摘要和不可见对象提示。
|
||||
- 抽屉命令,例如进入社区、打开详情、显示对象、清除选择。
|
||||
- 大图预算,例如标签、边、卡片和聚合容器显示规则。
|
||||
|
||||
### Sigma/Graphology 负责什么
|
||||
|
||||
Sigma/Graphology 只负责全局地图绘制和视口层交互。
|
||||
|
||||
它负责:
|
||||
|
||||
- 全局节点点位绘制。
|
||||
- 全局边绘制。
|
||||
- 社区颜色和必要的视觉分组表达。
|
||||
- 搜索命中、选中、Pin 等状态的视觉表达。
|
||||
- 拖动、缩放和画布命中。
|
||||
- 把用户点到的渲染对象投射回图谱引擎能识别的对象。
|
||||
|
||||
它不负责:
|
||||
|
||||
- 决定搜索命中是谁。
|
||||
- 决定抽屉显示什么。
|
||||
- 决定进入哪个社区。
|
||||
- 保存真实图谱状态。
|
||||
- 成为新的业务数据源。
|
||||
|
||||
### 适配层
|
||||
|
||||
正式接入需要清晰的 Sigma 全局适配层,但这一层不从零搭建。`packages/graph-engine/src/render/adapter.ts` 已经有渲染器无关契约(`GraphRendererAdapterData` / `GraphRendererBehaviorContract`),Sigma 试验也已经消费过其中的语义状态,例如选择、搜索命中、Pin、社区和聚合行为。
|
||||
|
||||
不过,试验代码仍直接遍历原始 `data.nodes` / `data.edges` 生成 Sigma 点线,因此不能把“数据出向”视为生产就绪。生产 Sigma 必须画适配层输出的受控渲染数据,尤其要使用图谱引擎已经算好的预算、过滤、聚合、标签可见性和语义状态;不能在 Sigma 路径里重新决定画哪些节点、边和标签。
|
||||
|
||||
本次主要补两块:第一,把“数据出向”从试验可用推进到生产可用;第二,把 Sigma 的点击、悬停和视口事件转回图谱引擎命令(“命令入向”),以及渲染器生命周期和失败兜底。
|
||||
|
||||
适配层必须遵守:
|
||||
|
||||
- `GraphData` 不为了 Sigma 改形状。
|
||||
- Graphology 只保存渲染用图结构,不保存另一份真实业务状态。
|
||||
- 抽屉永远通过图谱引擎摘要结果渲染,不读取 Sigma 内部状态。
|
||||
- 如果未来更换渲染库,搜索、Pin、社区、抽屉和选择规则不需要重写。
|
||||
- 社区区域命中由图谱引擎或适配层根据节点位置、社区 hull / 聚合区域和空间索引计算;Sigma 只上报画布坐标或渲染对象 id,不自己决定“点到了哪个社区”。命中优先级保持为节点、聚合/社区区域、空白。
|
||||
|
||||
## 交互规则
|
||||
|
||||
### 点击节点
|
||||
|
||||
点击全局节点时:
|
||||
|
||||
- 全局视角不自动跳转。
|
||||
- 选中该节点。
|
||||
- 高亮该节点和少量直接相关节点。
|
||||
- 右侧打开节点轻量摘要。
|
||||
- 不自动进入社区。
|
||||
- 不自动打开完整阅读内容。
|
||||
|
||||
用户在轻量摘要里选择“打开详情 / 阅读”后,进入所属社区并选中该节点。
|
||||
|
||||
### 点击社区
|
||||
|
||||
点击社区区域或社区图例时:
|
||||
|
||||
- 全局视角不自动跳转。
|
||||
- 选中该社区。
|
||||
- 高亮该社区和少量跨社区关系。
|
||||
- 右侧打开社区摘要。
|
||||
- 不自动进入社区。
|
||||
|
||||
用户点击“进入社区”后,才进入社区阅读路径。
|
||||
|
||||
### 搜索
|
||||
|
||||
搜索只改变当前视图的高亮、淡化、列表和摘要,不重排整张图。
|
||||
|
||||
搜索命中很多时:
|
||||
|
||||
- 命中节点保持可见提示。
|
||||
- Pin 节点保持可见提示。
|
||||
- 已选对象保持上下文。
|
||||
- 普通弱边和普通标签可以降级。
|
||||
|
||||
### 筛选
|
||||
|
||||
筛选不自动清空当前选择。
|
||||
|
||||
如果当前选中对象被筛选排除:
|
||||
|
||||
- 右侧抽屉保留该对象。
|
||||
- 明确提示它不在当前结果中。
|
||||
- 提供清除选择或显示该对象的动作。
|
||||
|
||||
### Pin
|
||||
|
||||
Pin 是持久标记,不等于当前选中。
|
||||
|
||||
大图降级时,Pin 节点仍要尽量保留可见提示。Pin 可以影响重要性排序,但不能成为唯一排序规则。
|
||||
|
||||
### 缩放和拖动
|
||||
|
||||
缩放只改变信息密度,不自动切换到社区阅读。
|
||||
|
||||
拖动和缩放过程中可以临时降级:
|
||||
|
||||
- 隐藏普通标签。
|
||||
- 隐藏弱边。
|
||||
- 减少复杂视觉效果。
|
||||
|
||||
交互停止后补回当前预算允许显示的信息。核心锚点、搜索命中、Pin 和选中对象不能在交互中突然消失。
|
||||
|
||||
### 返回全局
|
||||
|
||||
从社区返回全局后:
|
||||
|
||||
- 正常情况下回到 Sigma 全局图。
|
||||
- 如果当前环境已判定 Sigma 不可用,则回到对应规模的兜底全局视图,不能再次尝试进入一个已知失败的 Sigma 实例。
|
||||
- 保留合理上下文,例如刚刚所在社区轻微高亮。
|
||||
- 不把用户带到用户可选的另一套全局主路径;兜底只在异常状态下自动发生。
|
||||
|
||||
### 失败兜底
|
||||
|
||||
如果 Sigma 无法启动(WebGL 不可用、初始化失败、画布异常):
|
||||
|
||||
- 按规模分层兜底,而不是一律退回 DOM/SVG:小图退回现有 DOM/SVG 全局图;大图退回最低可用的聚合安全视图。因为 DOM/SVG 在 10000 节点本就约 8836ms、36.8 FPS、近乎拖不动,把大图退给它等于把用户丢进一张已知卡死的图。
|
||||
- “小图 / 大图”的具体阈值由实施计划用历史基线和真实机器验证确定,初始建议用节点数、边数和社区规模共同判断,而不是只看节点数。
|
||||
- 大图聚合安全视图只保证不空白、不卡死、能说明当前图谱结构和下一步动作;它不是完整替代图谱。它只显示社区容器、少量骨架边、选中 / 搜索 / Pin 标记,以及右侧抽屉中的溢出列表入口。
|
||||
- 聚合安全视图下的可用动作限定为:查看社区摘要、查看搜索 / Pin / 选中对象列表、进入社区阅读、清除选择或重试 Sigma。节点级自由点选、完整边浏览、完整标签显示不在兜底目标内。
|
||||
- 用户要看到轻提示,说明当前处于“图谱安全显示”而不是正常高性能全局图;提示不能打断使用。
|
||||
- 用户既不看到空白死图,也不被丢进卡死的大图。
|
||||
- 系统记录失败原因,供后续排查。
|
||||
- 兜底不是用户主路径,不提供“新旧图谱切换”作为常规入口。这里的聚合降级只是兜底形态,不是第二套全局主产品。
|
||||
|
||||
## 性能和验收标准
|
||||
|
||||
正式接入不是“Sigma 能画出来”就算完成,而是全局图从小到大都统一、顺滑、可恢复、行为不变。
|
||||
|
||||
收益锚点要先说清楚:Sigma 的主要价值不是伺候极端的 10000 节点,而是让数百到数千节点的常见知识库从明显卡顿变丝滑。当前 DOM/SVG 在 1000 节点已要约 900ms、5000 节点约 4615ms 首屏,Sigma 试验把它们压到约 146ms / 175ms。因此验收中心是“真实规模显著变快 + 大图不崩溃”;10000 节点是压力上界,只用来保证不崩,不是产品叙事中心,也不值得为它过度设计(例如为 90000 条弱边堆复杂边预算,而真实库根本到不了那个量级)。
|
||||
|
||||
最低验收:
|
||||
|
||||
1. 所有全局视角统一走 Sigma/Graphology。
|
||||
2. 社区阅读和详情继续使用现有阅读体验。
|
||||
3. 点击节点、点击社区、搜索、筛选、Pin、进入社区、返回全局、不可见对象提示语义保持一致。
|
||||
4. 1000 节点全局图接近无感。
|
||||
5. 5000 节点全局图拖动、缩放、搜索和抽屉打开明显流畅。
|
||||
6. 10000 节点全局图不能出现拖不动、缩放卡住、搜索迟迟不亮、抽屉慢开或画面大跳变。
|
||||
7. 大图拖动和缩放目标稳定在 45 FPS 以上。
|
||||
8. 10000 节点首次进入全局图时必须有明确加载状态,不能长时间空白。
|
||||
9. Sigma 初始化失败、WebGL 不可用或画布异常时能自动兜底。
|
||||
10. 现有图谱引擎测试、前端图谱行为测试和大图性能测试都必须通过。
|
||||
|
||||
## 必测图谱形状
|
||||
|
||||
后续实施计划必须跑完整 11 类图谱形状,而不是只复用早期 5 类试验结果。
|
||||
|
||||
必须覆盖:
|
||||
|
||||
1. 真实图谱快照代理。
|
||||
2. 1000 节点稀疏图。
|
||||
3. 1000 节点密集图。
|
||||
4. 5000 节点稀疏图。
|
||||
5. 5000 节点密集图。
|
||||
6. 10000 节点聚合图。
|
||||
7. 10000 节点高边图。
|
||||
8. 超大社区。
|
||||
9. 很多小社区。
|
||||
10. 很多搜索命中。
|
||||
11. 很多 Pin 节点。
|
||||
|
||||
每类至少验证:
|
||||
|
||||
- 首次渲染。
|
||||
- 拖动。
|
||||
- 缩放。
|
||||
- 搜索高亮。
|
||||
- 点选节点。
|
||||
- 点选社区或聚合容器。
|
||||
- 打开抽屉。
|
||||
- 进入社区。
|
||||
- 返回全局。
|
||||
- 重复交互后的内存和稳定性。
|
||||
|
||||
## 实施前置门禁(Phase 0)
|
||||
|
||||
写任何生产渲染代码之前,必须先过一道“允许开工”的性能门禁。当前选择 Sigma 的方向成立,但接入前的证据还不够硬:
|
||||
|
||||
- 现有 Sigma 试验数据(10000 节点约 289ms、60.9 FPS)是“历史隔离基线”,由第一版未硬化的试验脚本产出。决策文档自己也要求“锁定 Sigma 前必须重跑硬化试验”。
|
||||
- 关键缺口:5000 / 10000 节点的重复交互内存测试此前是 not run,只有 1000 节点测过内存增长——大图反复交互是否泄漏,目前没有任何数据。
|
||||
- 试验用的是极简 drawer 和简单视觉更新模型,不是 workbench 真实的抽屉 / 阅读器 / overlay 负载。
|
||||
|
||||
因此 Phase 0 分两层,不把所有发布回归都压到开工前。
|
||||
|
||||
开工门禁必须完成:
|
||||
|
||||
1. 先升级 harness 判定标准,确保它真的能按本文档验收,而不是继续用过低阈值或极简 drawer 得出虚假通过。
|
||||
2. 重跑完整 11 类形状的核心动作:首次渲染、拖动、缩放、搜索高亮、点选节点、点选社区/聚合、打开抽屉。
|
||||
3. 补齐大图(5000 / 10000)重复交互内存周期。
|
||||
4. 补一个接近真实 drawer / overlay 负载的延迟测试。这里不要求先完成生产集成;可以用代表性负载模拟,但必须比早期极简 drawer 更接近 workbench。
|
||||
|
||||
发布回归必须完成:
|
||||
|
||||
1. 进入社区。
|
||||
2. 返回全局。
|
||||
3. 失败兜底。
|
||||
4. 小图行为回归。
|
||||
5. 完整 11 类形状的端到端循环。
|
||||
|
||||
开工门禁任一项不达标即停下记录 blocker。若 blocker 指向 Sigma 方案本身不可行,停止本实施方向,另开技术路线决策;不要在同一份落地计划里临时改接 vis-network。
|
||||
|
||||
开工门禁不通过,下面的生产接入动作不启动;发布回归不通过,不能宣布正式接入完成。
|
||||
|
||||
## 工程落点
|
||||
|
||||
后续实施计划应围绕以下落点展开(按依赖排序,第 1 条是真正的硬骨头):
|
||||
|
||||
1. 先解耦协调层对 DOM/SVG 的隐性依赖。此前的渲染器协调层拆分(已合入 main,对应 `graph-renderer-coordination-split` 计划)已把“决策”(`controller.ts`)和“画图”(`render-pipeline.ts`)分到不同文件,但 controller 仍有多处直接操作具体 DOM(如 `context.dom.nodeElements.get(id)?.focus()`、`.classList.add("is-dragging")`;见 `render-context.ts` 里 `nodeElements: Map<…, HTMLButtonElement>`、`edgeElements: Map<…, SVGPathElement>`)。这些假设“每节点是可单独 focus / 加 class 的 DOM、每边是 SVG path”,对 Sigma 的 WebGL 画布全不成立。必须先把已知 DOM 附着点限定在当前渲染层命令里(如 `setNodeFocused` / `setNodeDragging` / `setSearchState`),不要把它扩成泛化渲染框架。
|
||||
2. 拆清 renderer root、pipeline、controls 的边界。当前生产入口会固定创建 DOM/SVG 根、节点按钮、SVG 边、搜索框、图例和小地图;只抽象 controller 还不够。实施计划需要明确哪些 UI 控件仍由 DOM 外壳承载,哪些图形内容交给 Sigma canvas,哪些能力保留在社区/详情 DOM/SVG 路径。
|
||||
3. 复用已有的 `render/adapter.ts` 契约,但要把“数据出向”补成生产可用:Sigma 画适配层输出的受控渲染数据,不直接遍历原始全量 `GraphData` 自己决定预算。
|
||||
4. 补“命令入向”:把 Sigma 的点击、悬停、视口事件转回图谱引擎命令,不在 Sigma 回调里重算语义。
|
||||
5. 把现有试验用 Sigma 代码收敛成生产路径,而不是直接复制测试 harness。
|
||||
6. 明确全局 Sigma 与社区 DOM/SVG 的路由点。当前 `createGraphFacade` 只创建一个 renderer,`focusCommunity` 只是让同一个 renderer 重新聚焦;正式接入必须决定是双 renderer 切换、单 facade 内部切换,还是 renderer host 管理两种 view,并写清共享状态、销毁、回调、返回全局和兜底如何走。
|
||||
7. 保持现有 workbench 的 `GraphPanel` 对外能力不大改:数据加载、Pin 持久化、选择回调、打开详情、可见状态通知继续通过当前边界通信。复杂度定位要摆正——真正动手术的是 `graph-engine/render` 和 facade 内部,不是 workbench/GraphPanel;后者只调 facade 高层 API,本就不碰渲染细节。
|
||||
8. 把 Graphology 和 Sigma 从试验依赖提升为正式运行依赖时,要明确包边界和构建产物影响。
|
||||
9. 给 WebGL/Sigma 初始化失败增加可测试兜底路径(分层方案见“失败兜底”)。
|
||||
10. 加一个开发期内部开关(env / config 级,不暴露为用户功能)用于新旧渲染对比与快速回退。它默认不进入正式用户路径;如果未来要作为线上应急开关,必须另行写明触发条件、谁能开启、用户会看到什么、何时关闭。它和“不给用户新旧切换按钮”不矛盾,一个是开发/运维工具,一个是产品形态。
|
||||
11. 把性能 harness 从“试验记录”升级为正式验收门禁(即 Phase 0 门禁)。
|
||||
|
||||
## 不在本次范围
|
||||
|
||||
本次不包含:
|
||||
|
||||
- 在本实施计划内重新评估或改接 vis-network。若 Phase 0 发现 Sigma 硬阻塞,应停止并另开技术路线决策。
|
||||
- 做聚合方案作为第二套全局主产品(聚合仅作为大图失败兜底的降级形态,见“失败兜底”,不作为可选主产品)。
|
||||
- 把社区阅读也改成 Sigma。
|
||||
- 新增旧图/新图用户切换按钮(开发期内部对比开关不属此列,见“工程落点”第 10 条)。
|
||||
- 新增 Agent 提问入口。
|
||||
- 新增边点击关系详情。
|
||||
- 移动端专项体验。
|
||||
- 桌面壳专项实现。
|
||||
- 完整键盘漫游。
|
||||
- 类型、来源、时间等多套备用组织视角。
|
||||
|
||||
## 残余风险
|
||||
|
||||
1. Sigma 在生产环境和试验 harness 中的表现可能不同,因此实施前必须通过开工门禁,发布前必须完成完整 11 类形状回归。
|
||||
2. WebGL 在不同电脑和未来桌面壳里的可用性不同,因此兜底路径必须是真实可测的。
|
||||
3. 全局统一 Sigma 会触碰小图全局体验,因此小图行为回归也必须测,不能只测大图。
|
||||
4. 如果适配层边界不够硬,后续容易把搜索、选择、抽屉逻辑写进 Sigma 回调,导致返工。
|
||||
5. controller、renderer root、pipeline 和 controls 现仍有 DOM/SVG 假设,若不先收敛成清晰渲染边界就接 Sigma,会在回调里重造一套交互附着,造成双份维护——这是本次最大的隐性工作量。
|
||||
6. 大图聚合安全视图必须严格保持兜底定位;一旦把它扩成完整可选图谱产品,就会重新引入第二套全局路线。
|
||||
|
||||
## 后续动作
|
||||
|
||||
用户复核通过后,再把这份设计转换成实施计划。
|
||||
|
||||
实施计划应按阶段推进:
|
||||
|
||||
1. 先过 Phase 0 开工门禁(升级 harness 判定标准、重跑核心动作、补大图内存周期、补代表性 drawer / overlay 负载);不达标即停,不进入生产接入。
|
||||
2. 解耦 controller、renderer root、pipeline 和 controls 对 DOM/SVG 的隐性依赖,把已知交互附着收敛成渲染器边界命令。
|
||||
3. 复用 adapter.ts,把 Sigma 数据出向补到生产可用,并补齐命令入向。
|
||||
4. 明确全局 Sigma 与社区 DOM/SVG 的路由点、共享状态、销毁、回调、返回全局和兜底。
|
||||
5. 落地生产级 Sigma 全局路径和分层兜底边界。
|
||||
6. 补齐交互语义和抽屉联动。
|
||||
7. 全程用 11 类形状持续回归,发布前完成完整端到端循环。
|
||||
8. 最后把旧 DOM/SVG 从全局主路径移除;它仍可作为小图异常兜底、社区阅读和详情能力保留并测试。
|
||||
@@ -0,0 +1,162 @@
|
||||
# Paper 视觉 + 布局 移植设计(图谱后置)
|
||||
|
||||
日期:2026-06-20
|
||||
状态:已转执行计划(被 plan 的评审修订更新)
|
||||
当前分支:`feat/paper-ui`
|
||||
|
||||
> **执行以 plan 为准**:本 spec 是设计意图;几处实现细节已被 `docs/plans/2026-06-20-paper-ui-port-phased-plan.md` 的 plan-eng-review + Codex 修订**取代**——①不引入 `.pw-*` 并行类,就地演进现有 `.msg-*/.chat-*` 等类;②强调色用 `data-accent` 预设而非行内变量;③`ModelSelector` 组件不存在,需新建(复用 SettingsPanel 的 config 路径);④TopBar 不显示「篇数」(`KnowledgeBaseInfo` 无此字段);⑤搜索 ⌘K 做客户端真搜索;⑥PRODUCT.md 对齐提前到实现第一步。以 plan 为实现真相。
|
||||
|
||||
## 目的
|
||||
|
||||
把已定稿的 Paper 设计原型(`/Users/kangjiaqi/designs/llm-wiki-skill/bright/paper-final-v2.html`,独立 design 仓库)的**视觉语言 + 布局**移植并复刻进 `workbench/web`,作为产品的新默认外观。
|
||||
|
||||
这份文档只确定移植的产品与工程边界,方便后续直接写实施计划。它**不**重新评估视觉方向(方向已通过 baoyu-design 多轮迭代锁定),**不**包含图谱画布内部视觉(明确后置),**不**做 MVP 取舍(除图谱外尽量一步到位)。
|
||||
|
||||
设计原型与真实 app **同组件架构、同「CSS 变量 + 语义类名」范式**——原型本就是照 workbench 真实结构画的(同名组件 ChatPanel / Sidebar / Composer / ToolStatus / RightDrawer;真实 app 没有 ModelSelector,需要新建 TopBar 模型选择控件并复用 SettingsPanel 的配置路径)。因此本次是「换皮 + 按 v2 重排布局」,不是重写业务逻辑。
|
||||
|
||||
## 设计结论
|
||||
|
||||
1. **以 v2 的视觉 + 布局为准做像素级复刻**,而不是「v2 视觉套真实结构」。引入 v2 的统一顶栏、药丸 Tab、气泡对话、单卡 Composer、右抽屉阅读视觉、外观调参面板。
|
||||
2. **默认浅色暖纸主题**(真实 app 现在默认 dark,本次改默认 light);夜灯(暖深色)一键切换保留。
|
||||
3. **Tweaks 外观调参做成真实用户偏好**:纸张质感 / 强调色 / 用户气泡 / 手写体点缀 / 密度,随时切换、localStorage 持久化,零后端改动。
|
||||
4. **真实 app 已有但 v2 没有的功能全部保留并 Paper 化**:导出、批量消化、产物 Artifacts、素材消化 input-chip、拖拽消化、抽屉 resize/tab/全屏、批量消化面板等。
|
||||
5. **图谱后置**:本次只把图谱 Tab 入口 / 工具条 / 图例 Paper 化到与整体不突兀;**图谱画布内部(Sigma 渲染配色)不动**。图谱活地图 Paper 化作为独立后续任务(见末节)。
|
||||
6. **未实现的功能留 UI + 接口,后期接真功能**,不在本次强行做后端。
|
||||
|
||||
一句话:**v2 的样子,真实的功能,图谱留到下一程。**
|
||||
|
||||
## 为什么引入统一 TopBar 是最优解,而不是沿用 statusbar
|
||||
|
||||
v2 的布局里,模型选择器、新对话、主题切换、外观调参、搜索这些控件是**横跨「对话」和「图谱」两个视图共享的一条顶栏**,位于 Tab 之上。
|
||||
|
||||
真实 app 现在没有顶栏:知识库信息分散在 Sidebar(库列表)和每个视图各自的 statusbar(`ChatPanel` 顶部有一条 `.statusbar` 显示库名 + 模型 pill;`GraphPanel` 另有自己的头部)。其后果是**主题切换按钮被复制传进 ChatPanel 和 GraphPanel 两处**(`App.tsx` 里 `onToggleTheme` 分别传给两个面板)。
|
||||
|
||||
引入一条**共享 TopBar** 不只是为了像 v2,它本身是更干净的架构:
|
||||
|
||||
- 跨视图共享的控件(模型、新对话、主题、外观、搜索)只需要在一处声明,消除当前的双份传递与潜在不一致。
|
||||
- 视图切换(对话/图谱)只切 Tab 下面的主区,顶栏稳定不动,符合「顶栏是全局控件、Tab 是视图、主区是内容」的清晰分层。
|
||||
- `ChatPanel` 的 `.statusbar` 与 `GraphPanel` 的头部职责被上提,两个面板回归「只渲染各自内容」,边界更单一。
|
||||
|
||||
因此 TopBar 是「顺手把结构理顺」,不是为了好看的妥协。**采用。**
|
||||
|
||||
## 用户拍板的具体决策(硬约束)
|
||||
|
||||
- **去掉**顶栏左侧知识库头里的「模型名 + 下拉」。左侧只保留:书本图标 + 库名 + origin/valid 标记,纯展示,不可点、无 chevron;不显示篇数(`KnowledgeBaseInfo` 无此字段)。模型展示与切换只在右侧 TopBar 模型控件,控件需新建并复用 SettingsPanel 的 `config.modelRoles.main` 读写路径。
|
||||
- **去掉**侧栏左下角的「夜灯模式」项(与右上角主题切换功能重合)。主题切换只保留在右上角顶栏。
|
||||
- **默认主题 = 浅色暖纸**。
|
||||
- **主页凸显三个现有功能**:导出(HTML/PDF/PPTX…)、搜索 ⌘K、批量消化。
|
||||
- **搜索 ⌘K**:本次做当前库页面引用 metadata 的本地真搜索,数据来自 App/TopBar 级 refs cache;真实跨库 / 全文搜索后端拆为子任务,不在本次扩 scope。
|
||||
- 其余 v2 元素**全部保留复刻**。
|
||||
|
||||
## 范围
|
||||
|
||||
### 本次做
|
||||
|
||||
- 全局 Paper token 层(浅纸 + 夜灯)映射到现有 `--app-*` 与 shadcn 变量。
|
||||
- 新 TopBar 组件 + 移除/上提原 statusbar 控件。
|
||||
- Sidebar Paper 化(去夜灯项)。
|
||||
- 对话区:扁平消息 → 气泡;工具状态暖化;Composer 单卡化;概念链接 / 荧光笔样式。
|
||||
- RightDrawer 阅读视觉 Paper 化(保留 resize/tab/全屏)。
|
||||
- 外观偏好系统(`lib/appearance.ts` + `AppearancePanel`)+ localStorage 持久化。
|
||||
- 导出 / 批量消化入口凸显并 Paper 化;搜索 ⌘K 做当前库 refs 本地过滤。
|
||||
- 图谱 Tab 外壳 / 工具条 / 图例 Paper 化到不突兀。
|
||||
|
||||
### 本次不做(后续任务)
|
||||
|
||||
- 图谱**画布内部**视觉(Sigma 节点 / 社区配色,在 `packages/graph-engine` 的 `render/render-styles.ts`)。
|
||||
- 真实跨库 / 全文搜索后端。
|
||||
- 任何与外观无关的后端改动。
|
||||
|
||||
## 布局骨架(以 v2 为准)
|
||||
|
||||
```
|
||||
app-shell(竖向 flex)
|
||||
├─ TopBar(新增)
|
||||
│ ├─ 左:知识库头 = 书本图标 + 库名 + origin/valid(纯展示,不显示模型名 / 不下拉 / 不显示篇数)
|
||||
│ └─ 右:搜索⌘K · 模型选择器(新建) · 新对话 · 主题切换 · 外观齿轮(Tweaks)
|
||||
├─ body(横向 flex)
|
||||
│ ├─ Sidebar:笔记本列表 + 会话列表 + footer(图谱活地图 · 设置)【去「夜灯模式」】
|
||||
│ ├─ Main:药丸 Tab(对话 / 图谱) + 对话视图 | 图谱视图
|
||||
│ └─ RightDrawer:阅读抽屉(节点 / wiki / 产物,保留 resize / tab / 全屏)
|
||||
├─ AppearancePanel(右上齿轮触发的 Tweaks 浮层,右上角展开)
|
||||
├─ SettingsPanel(侧栏「设置」→ 现有配置模态,沿用)
|
||||
└─ BatchDigestPanel(沿用,Paper 化)
|
||||
```
|
||||
|
||||
落地分工(避免功能重合):
|
||||
|
||||
- **右上角齿轮 = 外观调参(AppearancePanel)**:纸张 / 配色 / 气泡 / 手写 / 密度 / 主题。
|
||||
- **侧栏「设置」= 现有 SettingsPanel 配置模态**(模型凭证、库登记等),不变。
|
||||
|
||||
## 样式与 token 策略
|
||||
|
||||
1. **Paper token 层**:把 v2 的 `:root` / `[data-theme="light"]` / `[data-theme="dark"]` 暖纸 / 夜灯变量,映射到 `workbench/web/src/index.css` 现有的 `--app-*` 与 shadcn 变量(沿用 design 仓库 `bright/paper-theme.css` 的映射思路,但**补全 v2 实际用到、`paper-theme.css` 漏掉的**:社区色 `--comm-*`、纸张层 `--paper-glow / --paper-vignette / --paper-mottle / --paper-grain`、`--dot`、暖阴影 `--shadow / --shadow-lg`)。
|
||||
2. **组件样式层**:不引入 `.pw-*` 并行层;把现有 `.msg-*` / `.chat-*` / `.tool-*` / `.drawer-*` 等组件类就地演进为 Paper 外观。真实独有组件(导出栏、批量面板、抽屉 resize 把手等)保留既有类并 Paper 化对齐。
|
||||
3. **字体**:`index.html` 引入 Plus Jakarta Sans(正文)+ Caveat(手写点缀)+ JetBrains Mono(路径 / 代码)。注意 CJK:Latin 在前、CJK 系统字体兜底,CJK 正文 line-height 放大。
|
||||
4. **data 属性**:`documentElement` 上 `data-theme`(已有)+ 新增 `data-paper` / `data-userbubble` / `data-hand` / `data-density` / `data-accent`;强调色由 CSS 里的 `data-accent` 预设统一驱动(`--accent` / `--accent-deep` / `--accent-soft`),不用行内 JS 变量。
|
||||
|
||||
token 替换只换「颜色 / 字体」,达不到复刻;真正复刻需要组件级结构改动(气泡、单卡 Composer、药丸 Tab、摘要卡去左竖条等),本设计的组件清单即覆盖这些。
|
||||
|
||||
## 外观偏好(Tweaks = 真实功能)
|
||||
|
||||
- 新 `workbench/web/src/lib/appearance.ts`:
|
||||
- `AppearancePrefs` 类型:`{ theme, paper, accent, userbubble, hand, density }`。
|
||||
- 取值:`theme: light|dark`;`paper: clean|grid|laid`;`accent: terracotta|clay|amber|rose`;`userbubble: soft|solid`;`hand: on|off`;`density: cozy|compact`。
|
||||
- 读写 localStorage(键前缀 `llm-wiki-agent-appearance-*`,SSR-safe,`typeof window` 守卫),并提供 `applyAppearance(prefs)`:把上述写成 `documentElement` 的 data 属性;强调色通过 `data-accent` 预设生效。
|
||||
- 复用现有 `theme` 机制:`theme` 仍走现有 `THEME_STORAGE_KEY` 与 `dataset.theme` + `.dark` class,但**默认值改为 `light`**;其余外观项新增。
|
||||
- 新 `AppearancePanel` 组件:复刻 v2 `TweaksPanel`(分段控件 + 配色色板),右上齿轮 popover,右上角展开,带显隐。
|
||||
- 状态归属:偏好状态由 `App` 持有(与现有 `theme` 并列),通过一个 effect 应用到 `documentElement`(仿现有 theme effect),`AppearancePanel` 受控。
|
||||
- **默认值**:`theme=light` · `paper=clean` · `accent=terracotta` · `userbubble=soft` · `hand=on` · `density=cozy`。
|
||||
|
||||
## 组件改造清单
|
||||
|
||||
| 组件 / 文件 | 改造 | 备注 |
|
||||
|---|---|---|
|
||||
| `App.tsx` | 新增 TopBar 挂载、外观偏好状态 + effect、移除分散的 `onToggleTheme` 双传 | 顶栏控件回流到此处统一编排 |
|
||||
| **TopBar(新)** | 左 kb 头(静态,不显示模型名 / 不下拉 / 不显示篇数)+ 右控件组(搜索⌘K 当前库 refs 本地搜索、模型选择器[新建]、新对话、主题、外观齿轮) | 共享于对话 / 图谱两视图 |
|
||||
| `ChatPanel.tsx` | 移除 `.statusbar`(控件上提 TopBar);扁平消息 → 气泡(就地演进 `.msg-row/.msg-avatar/.msg-body/.msg-content` 等类,用户 soft/solid 两态、头像区分);导出栏 Paper 化并凸显;保留 input-chip / 拖拽消化 | 最大块 |
|
||||
| `MarkdownView.tsx` | 概念链接补 `.at` Paper 下划线样式(点击开抽屉逻辑已存在);荧光笔 `.hl` | 功能已在,补样式 |
|
||||
| `ToolStatusRunway.tsx` / `ToolHistorySummary.tsx` | 暖化对齐 v2(脉冲竖条 + 微光轨道 → 折叠 chips),保留真实信息量 | runway 比 v2 更全,套皮不减信息 |
|
||||
| Composer(在 `ChatPanel` 内) | 分离式 → v2 单卡(内嵌发送 + focus 暖光环 + 占位符随 `data-hand` 切手写 / 正文);`RefMenu` / `CommandMenu` Paper 化 | |
|
||||
| `Sidebar.tsx` | 笔记本 / 会话 / footer(图谱 · 设置) Paper 化;**移除「夜灯模式」项**;保留折叠 | |
|
||||
| `RightDrawer.tsx` + 摘要 / 节点视图 | v2 阅读视觉:摘要改「带标签柔卡」(**去掉左竖条 AI slop**)、meta chip 带社区圆点、关联列表、操作按钮;保留 resize / tab / 全屏 | |
|
||||
| `TopBar` 模型选择器 | 新建控件,Paper 化(分组、打勾、pop 动画),复用 SettingsPanel 的 config 读写路径 | 真实 app 无现成 ModelSelector |
|
||||
| `BatchDigestPanel.tsx` | Paper 化;入口上浮凸显 | |
|
||||
| `GraphPanel.tsx` | 仅 Tab 入口 / 工具条 / 图例 Paper 化;画布内部不动 | 后置任务的边界 |
|
||||
| **`lib/appearance.ts`(新)** / **`AppearancePanel`(新)** | 见上节 | |
|
||||
|
||||
## 持久化
|
||||
|
||||
- 外观偏好与现有 UI 偏好一致走 **localStorage**(现有:`llm-wiki-agent-theme` / sidebar-collapsed / drawer-width / main-view)。
|
||||
- 不写后端、不进 `~/.llm-wiki-agent/`。
|
||||
|
||||
## 测试 / 验收
|
||||
|
||||
- `npm run build`(含 `tsc -b`)、`npm run typecheck`、现有 `npm test`(node:test / vitest)全绿。
|
||||
- **真实功能不回归**:消息流式(SSE)、`@` 引用 / `/` 命令、导出、批量消化、产物抽屉、抽屉 resize/tab/全屏、切库 / 切对话、概念链接开抽屉。
|
||||
- **视觉回归**:2 主题 × 3 纸张 × 4 配色 × 2 气泡 × 2 密度 × 手写开关,逐项截图核对;浅 / 夜灯首屏。
|
||||
- 过一遍主流程(手动或 Playwright)。
|
||||
|
||||
## 落地节奏
|
||||
|
||||
一步到位,单分支 `feat/paper-ui`,内部按逻辑单元分 commit:
|
||||
|
||||
1. Paper token 层(`index.css` 变量 + 字体)
|
||||
2. 外观偏好系统(`lib/appearance.ts` + `AppearancePanel` + App 接线)
|
||||
3. TopBar 新增 + statusbar 控件上提
|
||||
4. Sidebar Paper 化(去夜灯项)
|
||||
5. 对话区气泡化 + 工具状态暖化
|
||||
6. Composer 单卡化 + 菜单 / 导出 Paper 化
|
||||
7. RightDrawer 阅读视觉
|
||||
8. 图谱 Tab 外壳 Paper 化 + 收尾回归
|
||||
|
||||
## 后续任务(不在本次,记录在案)
|
||||
|
||||
1. **图谱活地图 Paper 化**:把社区 / 节点配色对齐暖纸调,配色在 `packages/graph-engine` 的 `render/render-styles.ts`(Sigma 渲染),与图谱体验一起规划,不在 CSS 层。
|
||||
2. **真实跨库 / 全文搜索后端**:本次只做当前库 refs metadata 的本地搜索;跨库、全文内容索引、后端搜索 API 作为独立子任务。
|
||||
|
||||
## 风险 / 边界
|
||||
|
||||
- 引入 TopBar 涉及 `App.tsx` 与 `ChatPanel` / `GraphPanel` 的控件回流,是真实重构;已确认接受(理由见上)。属可控范围,因「不以 MVP 处理」而值得一次做对。
|
||||
- v2 原型用极简 `tinyMd` 渲染抽屉正文,`## 标题` / `- 列表` 当纯文本;真实 app 用 `MarkdownView`(正常 markdown),移植后此问题不存在。
|
||||
- 强调色 `data-accent` 预设需要同时覆盖浅 / 夜灯;用 `color-mix(... var(--card))` 推导 `--accent-soft` 保证两主题都成立。
|
||||
@@ -0,0 +1,384 @@
|
||||
# Paper UI V2 外壳对齐补完设计
|
||||
|
||||
日期:2026-06-20
|
||||
状态:待作者审阅
|
||||
分支:`codex/paper-ui-v2-layout`
|
||||
|
||||
## 背景
|
||||
|
||||
Paper UI 基础迁移已经完成,旧的 6 phase goal-plan 也已经收口。但当前实现更像是“旧工作台套上暖纸皮肤”,距离 `paper-final-v2.html` 的布局和交互仍有明显差距。
|
||||
|
||||
这份设计不是重开 Paper UI,也不是修改旧 goal-plan ledger,而是在当前 Paper 基础上做一个新的 **V2 外壳对齐补完包**。目标是把侧栏、主区、Composer、工具调用、右抽屉、图谱页外壳这些用户一眼能感知到的结构补齐。
|
||||
|
||||
V2 原型参考:
|
||||
|
||||
- `/Users/kangjiaqi/designs/llm-wiki-skill/bright/paper-final-v2.html`
|
||||
|
||||
相关旧文档:
|
||||
|
||||
- `docs/plans/2026-06-20-paper-ui-port-phased-plan.md`
|
||||
- `docs/plans/2026-06-20-paper-ui-port-progress.json`
|
||||
- `docs/spark/2026-06-20-paper-ui-port-design.md`
|
||||
|
||||
旧文档仍保留为基础迁移记录;本文件只描述下一轮深度还原。
|
||||
|
||||
## 目标
|
||||
|
||||
把当前 Paper UI 从“约 80% 像 V2”推进到可以作为 V2 还原版验收的状态。
|
||||
|
||||
具体目标:
|
||||
|
||||
- 侧边栏结构按 V2 重排,笔记本和会话分开显示。
|
||||
- 侧栏顶部只保留折叠入口,不再常驻刷新和设置。
|
||||
- 侧栏左下角按 V2 放置底部入口:图谱活地图、设置、新建知识库。
|
||||
- Composer 变成更轻的单卡输入体验,发送按钮内嵌,不再单独占一整行。
|
||||
- 工具调用展示只突出当前动作,历史默认轻量收起。
|
||||
- 右抽屉打开时桌面端主区让位,输入框不被覆盖;小屏端覆盖显示。
|
||||
- 图谱页外壳修正工具条遮挡和布局拥挤问题。
|
||||
- 保留真实产品已有功能和状态逻辑。
|
||||
|
||||
## 不做什么
|
||||
|
||||
本次不做以下内容:
|
||||
|
||||
- 不改图谱画布内部的 Sigma 节点、边、社区配色。
|
||||
- 不做跨库搜索或全文搜索后端。
|
||||
- 不重写后端。
|
||||
- 不搬 V2 原型里的 mock 数据。
|
||||
- 不把旧 goal-plan 的 progress ledger 继续翻状态。
|
||||
- 不为了像 V2 而删掉真实产品已有能力。
|
||||
|
||||
## 已确认的产品决定
|
||||
|
||||
1. 这次范围定义为 **V2 还原补完包**。
|
||||
2. 侧边栏改为 V2 结构:笔记本列表和会话列表独立显示。
|
||||
3. 会话不再嵌在当前知识库树下面。
|
||||
4. 侧栏底部保留图谱活地图、设置、新建知识库。
|
||||
5. 侧栏底部的“新建知识库”实际复用现在“添加现有库”的功能。
|
||||
6. 原来的空库“新建知识库”功能先不显示。
|
||||
7. 侧栏顶部只保留折叠按钮,不保留刷新和设置。
|
||||
8. 夜灯模式不在侧栏底部出现,主题切换继续放在 TopBar。
|
||||
9. 右抽屉桌面端打开时主区让位,小屏端覆盖。
|
||||
10. 工具调用只突出当前动作,历史轻量收起。
|
||||
11. 图谱画布内部视觉继续后置。
|
||||
|
||||
## 总体布局
|
||||
|
||||
页面继续保持四层结构:
|
||||
|
||||
```text
|
||||
App
|
||||
├─ TopBar:全局操作
|
||||
├─ AppBody
|
||||
│ ├─ Sidebar:笔记本 / 会话 / 底部入口
|
||||
│ ├─ Main:对话或图谱
|
||||
│ └─ RightDrawer:阅读 / 图谱摘要 / 产物
|
||||
├─ AppearancePanel
|
||||
└─ SettingsPanel
|
||||
```
|
||||
|
||||
### TopBar
|
||||
|
||||
TopBar 继续承载全局操作:
|
||||
|
||||
- 当前知识库信息
|
||||
- 对话 / 图谱状态
|
||||
- 搜索
|
||||
- 模型选择
|
||||
- 新对话
|
||||
- 主题切换
|
||||
- 外观偏好
|
||||
|
||||
TopBar 不承载图谱专属操作,也不放侧栏折叠按钮。图谱重置、重构等操作仍留在图谱页内部。
|
||||
|
||||
### Sidebar
|
||||
|
||||
侧边栏改为三段:
|
||||
|
||||
1. 顶部:品牌名 + 折叠按钮。
|
||||
2. 中间:笔记本列表、会话列表。
|
||||
3. 底部:图谱活地图、设置、新建知识库。
|
||||
|
||||
侧栏顶部只保留折叠功能。刷新和设置不再作为顶部常驻按钮。
|
||||
|
||||
笔记本列表只负责选库。会话列表只展示当前库会话。这样更接近 V2,也减少现在“知识库树里塞会话”的复杂感。
|
||||
|
||||
底部入口规则:
|
||||
|
||||
- “图谱活地图”:进入图谱视图。
|
||||
- “设置”:打开现有 SettingsPanel。
|
||||
- “新建知识库”:调用现有 AddExternalDialog,也就是现在“添加现有库”的功能,只改入口命名和视觉位置。
|
||||
|
||||
折叠状态下,侧栏变成窄 rail。至少保留:
|
||||
|
||||
- 展开侧栏
|
||||
- 当前库 / 笔记本
|
||||
- 对话
|
||||
- 图谱
|
||||
- 设置
|
||||
- 新建知识库
|
||||
|
||||
刷新不作为折叠 rail 的核心常驻入口。
|
||||
|
||||
### Main
|
||||
|
||||
主区继续在对话和图谱之间切换。
|
||||
|
||||
对话页的第一优先级是阅读消息和继续输入。输入区不能过高,不能把页面底部变成操作面板。
|
||||
|
||||
图谱页的第一优先级是画布可读。工具条、搜索、控制区、图例要围绕画布,不遮盖画布,也不压缩主要内容。
|
||||
|
||||
### RightDrawer
|
||||
|
||||
桌面和宽屏下,右抽屉作为布局中的一列。打开时主区宽度减少,Composer 跟着收窄,不能被抽屉覆盖。
|
||||
|
||||
小屏下,抽屉作为覆盖层显示,避免主区被压到不可用。
|
||||
|
||||
抽屉现有能力继续保留:
|
||||
|
||||
- resize
|
||||
- 全屏
|
||||
- 关闭
|
||||
- 产物 tab
|
||||
- wiki / graph reader / graph summary 内容
|
||||
|
||||
## 组件设计
|
||||
|
||||
### 侧边栏
|
||||
|
||||
当前侧边栏的问题:
|
||||
|
||||
- 顶部有折叠、刷新、设置三个按钮,视觉噪声高。
|
||||
- 底部是“新建知识库 / 添加现有库”,和 V2 不一致。
|
||||
- 会话嵌在知识库下面,结构偏工程化。
|
||||
|
||||
目标形态:
|
||||
|
||||
- 顶部只保留折叠。
|
||||
- 中部用“笔记本”和“会话”两个明确分区。
|
||||
- 底部形成固定操作区,接近 V2。
|
||||
- 当前选中库和当前会话都要有清晰高亮。
|
||||
- 外部库、默认库等 badge 继续保留,但视觉弱化,不抢主标题。
|
||||
|
||||
### Composer
|
||||
|
||||
当前 Composer 的问题:
|
||||
|
||||
- 输入区整体高度过大。
|
||||
- “就绪”和“发送”单独占一行。
|
||||
- 导出按钮占据输入卡底部一整条,显得比输入本身更重。
|
||||
|
||||
目标形态:
|
||||
|
||||
- 输入框和发送按钮合成一个卡片。
|
||||
- 发送 / 停止按钮内嵌到输入卡右下角。
|
||||
- “就绪”状态不再常驻占位,只在生成中、错误、消化素材等状态需要时显示。
|
||||
- `@`、`/`、`⌘↵` 提示写入 placeholder;只有菜单、错误、消化素材等临时状态需要额外轻提示。
|
||||
- 导出入口变轻,放在输入卡下方的轻量工具区,不能压低输入框主体。
|
||||
- 拖拽消化和素材 chip 继续保留。
|
||||
|
||||
### 工具调用展示
|
||||
|
||||
当前工具调用的问题:
|
||||
|
||||
- 历史工具摘要容易在对话里占据过多视觉空间。
|
||||
- 用户关注的是“现在正在做什么”,不是每次历史工具都扑脸展示。
|
||||
|
||||
目标形态:
|
||||
|
||||
- 运行中:显示一条清晰的当前动作,例如“正在读取 mamba.md”。
|
||||
- 完成后:默认收成轻量摘要,例如“已读取 2 个页面”。
|
||||
- 详情可展开,但不默认展开。
|
||||
- 错误和取消状态仍要明显可见。
|
||||
- 原有工具状态事件语义不删,只调整呈现层级。
|
||||
|
||||
### 右抽屉
|
||||
|
||||
当前右抽屉的问题:
|
||||
|
||||
- 已经是布局列,但体验还没有达到 V2 的丝滑让位。
|
||||
- Composer 和主区在抽屉打开时需要更稳定地响应宽度变化。
|
||||
|
||||
目标形态:
|
||||
|
||||
- 桌面端打开抽屉时,主区自然变窄。
|
||||
- Composer 仍完整可输入、可发送。
|
||||
- 抽屉宽度调整时,主区跟随变化,不出现遮挡。
|
||||
- 小屏端使用覆盖式抽屉。
|
||||
- resize、全屏、关闭不回归。
|
||||
|
||||
### 图谱页外壳
|
||||
|
||||
当前图谱页的问题:
|
||||
|
||||
- 工具条和控制区有遮挡、拥挤风险。
|
||||
- 图谱页外壳还没有完全变成 V2 的“活地图”感觉。
|
||||
|
||||
目标形态:
|
||||
|
||||
- 图谱工具条固定在画布上方的外壳区域,不覆盖画布。
|
||||
- 搜索框、重置布局、重构图谱等控件有稳定位置。
|
||||
- 图例和统计信息轻量显示,不抢画布。
|
||||
- 图谱页从侧栏底部“图谱活地图”进入时,命名和视觉一致。
|
||||
- Sigma 画布内部不改。
|
||||
|
||||
## 阶段拆分
|
||||
|
||||
### Phase A:侧栏 V2 重排
|
||||
|
||||
目标:先把导航骨架对齐 V2。
|
||||
|
||||
包含:
|
||||
|
||||
- 顶部只保留折叠按钮。
|
||||
- 移除顶部刷新和设置常驻按钮。
|
||||
- 笔记本列表和会话列表分区显示。
|
||||
- 会话不再嵌套在知识库下面。
|
||||
- 底部入口改为图谱活地图、设置、新建知识库。
|
||||
- “新建知识库”调用现有添加现有库流程。
|
||||
- 折叠 rail 对齐新信息架构。
|
||||
|
||||
验收:
|
||||
|
||||
- 页面侧栏顶部只看到折叠入口。
|
||||
- 侧栏中部能清楚区分笔记本和会话。
|
||||
- 侧栏底部不出现“添加现有库”文案,只出现“新建知识库”。
|
||||
- 点击底部“新建知识库”打开现有添加现有库流程。
|
||||
- 点击底部“设置”打开设置。
|
||||
- 点击底部“图谱活地图”进入图谱视图。
|
||||
- 折叠和展开都可用。
|
||||
|
||||
### Phase B:对话页 V2 补完
|
||||
|
||||
目标:让对话页接近 V2 的阅读和输入体验。
|
||||
|
||||
包含:
|
||||
|
||||
- Composer 单卡内嵌发送按钮。
|
||||
- 弱化或移除常驻“就绪”行。
|
||||
- 导出入口轻量化,不再撑高输入区。
|
||||
- 工具调用改为当前动作优先,历史轻量收起。
|
||||
- 消息区节奏和空态跟 V2 对齐。
|
||||
|
||||
验收:
|
||||
|
||||
- 发送按钮在输入框内,不单独占一整行。
|
||||
- 输入区高度明显低于当前实现。
|
||||
- 空对话和已有对话都不出现底部操作过重的问题。
|
||||
- 运行工具时能看清当前动作。
|
||||
- 工具完成后历史默认不扑脸。
|
||||
- `@`、`/`、发送、停止、导出、拖拽消化不回归。
|
||||
|
||||
### Phase C:右抽屉让位体验
|
||||
|
||||
目标:把 V2 的抽屉丝滑感补齐。
|
||||
|
||||
包含:
|
||||
|
||||
- 桌面端抽屉打开时主区让位。
|
||||
- Composer 跟随主区宽度变化。
|
||||
- 抽屉 resize 时不遮挡输入区。
|
||||
- 小屏端抽屉覆盖显示。
|
||||
|
||||
验收:
|
||||
|
||||
- 1440 宽度打开抽屉,主区和输入框完整可见。
|
||||
- 拖动抽屉宽度时,主区没有被遮挡。
|
||||
- 1024 宽度仍可使用。
|
||||
- 768 宽度使用覆盖式抽屉,不把主区压坏。
|
||||
- 全屏、关闭、resize 不回归。
|
||||
|
||||
### Phase D:图谱页外壳修正
|
||||
|
||||
目标:修掉图谱工具条遮挡和外壳割裂。
|
||||
|
||||
包含:
|
||||
|
||||
- 图谱工具条重新排布,固定在画布上方的外壳区域。
|
||||
- 搜索、控制、统计、图例重新整理层级。
|
||||
- “图谱活地图”命名与入口一致。
|
||||
- 不改 Sigma 内部渲染。
|
||||
|
||||
验收:
|
||||
|
||||
- 图谱工具条不遮挡画布。
|
||||
- 图谱搜索和控制按钮位置稳定。
|
||||
- 图例不抢主要视觉。
|
||||
- 1440、1024、768 三个宽度无明显重叠。
|
||||
- 图谱重置布局、重构图谱、搜索等真实功能不回归。
|
||||
|
||||
## 测试与验证
|
||||
|
||||
每个阶段完成后至少验证:
|
||||
|
||||
- `cd workbench/web && npm run typecheck`
|
||||
- `cd workbench/web && npm run build`
|
||||
- `cd workbench/web && npm test`
|
||||
- `cd workbench/web && npm run lint`
|
||||
|
||||
视觉和交互要实际打开页面检查:
|
||||
|
||||
- 1440 桌面宽度
|
||||
- 1024 中等宽度
|
||||
- 768 窄屏宽度
|
||||
|
||||
关键手动流程:
|
||||
|
||||
- 切库
|
||||
- 切会话
|
||||
- 折叠 / 展开侧栏
|
||||
- 底部图谱活地图入口
|
||||
- 底部设置入口
|
||||
- 底部新建知识库入口
|
||||
- 发送消息
|
||||
- `@` 引用
|
||||
- `/` 命令
|
||||
- 工具调用运行和完成
|
||||
- 打开 wiki 抽屉
|
||||
- 抽屉 resize / 全屏 / 关闭
|
||||
- 切图谱页
|
||||
- 图谱重置布局 / 重构图谱
|
||||
|
||||
建议补充自动化覆盖:
|
||||
|
||||
- Sidebar DOM 测试:底部按钮文案和行为、顶部按钮移除、折叠 rail。
|
||||
- Composer DOM 测试:发送按钮在输入卡内、状态行弱化、导出入口存在。
|
||||
- ToolStatus DOM 测试:运行态当前动作、完成态轻量摘要、展开详情。
|
||||
- Drawer layout 测试:桌面让位、小屏覆盖、resize 不遮挡。
|
||||
- Graph shell 测试:工具条不使用覆盖画布的定位,图例和画布区域独立。
|
||||
- Visual paper 脚本补一组 V2 shell 截图:sidebar、chat with drawer、graph 三个状态。
|
||||
|
||||
## 风险与处理
|
||||
|
||||
### 风险 1:侧栏重排影响切库 / 切会话
|
||||
|
||||
处理:只改信息架构和呈现,不改后端会话模型。切库、切会话仍复用现有回调和状态。
|
||||
|
||||
### 风险 2:Composer 轻量化时丢掉导出或批量消化入口
|
||||
|
||||
处理:导出和批量消化必须保留,只调整视觉重量和位置。
|
||||
|
||||
### 风险 3:工具调用轻量化导致信息不足
|
||||
|
||||
处理:默认轻量,但保留展开详情。错误和取消状态不能隐藏。
|
||||
|
||||
### 风险 4:抽屉让位在中等宽度下挤坏主区
|
||||
|
||||
处理:设置桌面让位和小屏覆盖的明确断点;1024 作为重点验收宽度。
|
||||
|
||||
### 风险 5:图谱页修外壳时误碰 Sigma 内部
|
||||
|
||||
处理:实现计划里明确限制,不改 `packages/graph-engine` 的渲染样式。
|
||||
|
||||
## 完成定义
|
||||
|
||||
这次补完包完成时,用户应能直观看到:
|
||||
|
||||
- 侧栏已经是 V2 的“笔记本 / 会话 / 底部入口”结构。
|
||||
- Composer 不再笨重。
|
||||
- 工具调用不再扑脸。
|
||||
- 抽屉打开不再遮挡或压坏输入区。
|
||||
- 图谱页工具条不再遮盖画布。
|
||||
- 整体从“换皮”变成“V2 外壳真正落地”。
|
||||
|
||||
完成后再视情况把这份设计转成新的执行计划;不要继续往旧 goal-plan ledger 里追加任务。
|
||||
@@ -0,0 +1,534 @@
|
||||
# 图谱体验完整收口设计
|
||||
|
||||
日期:2026-06-21
|
||||
状态:已收敛范围(≤2000 节点)并确认根因,待实现
|
||||
当前分支:`codex/graph-experience-completion-spec`
|
||||
|
||||
## 目的
|
||||
|
||||
这份设计用于收口 llm-wiki 图谱当前出现的“两套全图体验”问题。
|
||||
|
||||
现在的产品方向已经明确:全局图谱使用 Sigma/Graphology 承担高性能地图视角,社区阅读继续使用 DOM/SVG 承担富信息阅读视角。但当前体验里还残留旧 DOM 全图路径:刚进入图谱是新版 Sigma 全局,进入社区后点击工具栏“回全图”却可能回到旧 DOM 全图,导致样式、点击、社区选择、拖拽、Pin、搜索和抽屉行为割裂。
|
||||
|
||||
本设计的目标不是重新做一套 Sigma 全图,也不是把旧 DOM 全图整体搬进 Sigma,而是把已经成熟的全局交互规则统一下来,让 Sigma 全局和 DOM 小图兜底都遵守同一套产品规则。
|
||||
|
||||
一句话:
|
||||
|
||||
**全局永远是一张地图;正常全局永远回 Sigma;DOM 全图只做小图异常兜底,但规则必须一致。**
|
||||
|
||||
## 范围与规模假设
|
||||
|
||||
本设计按个人知识库的真实规模收敛,不为超大图过度设计:
|
||||
|
||||
- 目标规模:常态 1000 个节点以内,硬上限 2000 个节点。
|
||||
- 2000 节点以内,Sigma(WebGL)可直接流畅渲染点状全局,无需把节点折叠成聚合块。
|
||||
- 因此本设计**不包含**节点聚合折叠(“+N 省略号”)、聚合平滑,以及面向超大图的“聚合安全视图”。
|
||||
- 渲染承载从四种收敛为三种:Sigma 全局、DOM 社区、DOM 小图兜底。
|
||||
- 仅保留一条极简的超限保护:万一节点数超过上限,给出“图太大,请用筛选”的提示并降级,不卡死、不回旧 DOM 全图,也不为此构建聚合系统。
|
||||
|
||||
## 背景依据
|
||||
|
||||
本设计延续以下已确认结论:
|
||||
|
||||
- `2026-06-18-large-graph-global-community-design.md`:全局图谱负责看结构,社区聚焦负责读内容。
|
||||
- `2026-06-19-sigma-global-graph-renderer-design.md`:所有正常全局视角统一使用 Sigma/Graphology,DOM/SVG 不再作为用户可选的第二套全局主路径。
|
||||
- `2026-06-19-sigma-global-production-integration-result.md`:生产路由边界已定义为 `sigma-global`、`dom-svg-community`、`dom-svg-small-fallback` 和 `aggregation-safety-fallback`。
|
||||
- `2026-06-20-paper-ui-port-design.md`:当前阶段不做图谱画布内部的山水纸面化;图谱画布先跟随项目现有主题。
|
||||
|
||||
因此,这次收口不是选型问题,而是把体验、状态和路由规则对齐到已经批准的路线。
|
||||
|
||||
## 现状诊断(已复现)
|
||||
|
||||
收口前先确认问题真实存在并定位根因,避免在已经正确的代码上返工。
|
||||
|
||||
已确认的复现路径:从社区点工具栏“回全图”,会回到旧 DOM/SVG 全图(白色节点卡片、圆形社区计数标记),而不是 Sigma 全局点状地图。
|
||||
|
||||
根因(代码级):
|
||||
|
||||
- “进入社区”路由(`dom-svg-community`)默认接到旧 DOM/SVG 渲染管线。
|
||||
- 这套 DOM 管线自带一套工具条,其“回全图”按钮调用的是管线内部的 `resetViewState()`:它只是把 DOM 视图重新铺满全部节点并在 DOM 里重画,**不会通知 facade 切回 Sigma 全局**。
|
||||
- 对照之下,Sigma 自身工具条的“回全图”是正确的(留在 Sigma),但那不是用户在社区里点到的那个按钮。
|
||||
|
||||
因此根因与“架构边界 / 风险 2”一致:**社区的“回全图”必须上交全局路由切回 Sigma,而不是在 DOM 管线内部自我重置。** 这是本次收口的最小必改项。
|
||||
|
||||
## 设计结论
|
||||
|
||||
采用“同一套全局规则,三种渲染承载”的方案。
|
||||
|
||||
1. 正常全局视角只走 Sigma 全局。
|
||||
2. 社区视角继续走 DOM/SVG 社区阅读。
|
||||
3. DOM 全图不再作为正常产品路径,只允许在 Sigma 不可用且图在上限内时作为异常兜底。
|
||||
4. 节点规模控制在 2000 以内,Sigma 不需要节点聚合;超出上限只做极简降级提示,不回 DOM 全图。
|
||||
5. 全局交互规则必须从旧 DOM 全图成熟经验里抽出来,成为共享规则;Sigma 全局与 DOM 小图兜底共用同一套规则,不各写一套心智。
|
||||
6. 社区里所有“回全图”入口都必须回到 Sigma 全局;只有 Sigma 已判定不可用时,才按兜底策略进入 DOM 小图兜底。
|
||||
7. 全局节点可拖拽保留,并沿用既有“松手即固定 / Pin / 固定位置恢复”能力。
|
||||
|
||||
## 第一眼体验
|
||||
|
||||
图谱打开的第一页要有“地图感”,不是阅读页,也不是卡片墙。
|
||||
|
||||
默认全局视角应呈现:
|
||||
|
||||
- 点状节点结构。
|
||||
- 社区的空间分布和颜色区分。
|
||||
- 少量关键标签。
|
||||
- 关系骨架。
|
||||
- 搜索命中、选中、Pin 和固定位置提示。
|
||||
- 顶栏、图例、工具栏保持当前项目主题,不做额外山水纸面化。
|
||||
|
||||
默认全局视角不出现:
|
||||
|
||||
- 节点卡片。
|
||||
- 大段正文。
|
||||
- 成片摘要卡。
|
||||
- 自动展开的社区内容。
|
||||
- 可见的圆形社区选择按钮。
|
||||
- 用户可见的“新版 / 旧版全图”切换。
|
||||
|
||||
全局图的核心观感是“我在看一张知识地图”,而不是“我在读一组内容卡片”。
|
||||
|
||||
## 用户体验规则
|
||||
|
||||
### 1. 打开图谱
|
||||
|
||||
用户点击“图谱”Tab 后,默认进入 Sigma 全局点状地图。
|
||||
|
||||
如果上一次有搜索、选中、Pin、固定节点或所在社区上下文,应尽量恢复这些上下文,但不能因此进入旧 DOM 全图。
|
||||
|
||||
### 2. 点击节点
|
||||
|
||||
在全局视角点击节点时:
|
||||
|
||||
- 保持在全局地图。
|
||||
- 选中节点。
|
||||
- 高亮节点和少量直接相关节点。
|
||||
- 打开轻量节点摘要。
|
||||
- 不自动进入社区。
|
||||
- 不自动打开完整阅读内容。
|
||||
|
||||
轻量节点摘要里可以提供“进入所属社区”或“打开详情 / 阅读”。只有用户点这些明确动作,才进入 DOM 社区阅读。
|
||||
|
||||
### 3. 点击社区
|
||||
|
||||
在全局视角点击社区区域、社区标记或社区图例时:
|
||||
|
||||
- 保持在全局地图。
|
||||
- 选中社区。
|
||||
- 高亮社区和关键跨社区关系。
|
||||
- 打开轻量社区摘要。
|
||||
- 不自动进入社区。
|
||||
|
||||
轻量社区摘要里的“进入社区”是进入 DOM 社区阅读的明确动作。
|
||||
|
||||
社区选择入口只保留一套:
|
||||
|
||||
- 背景社区色块是选中社区的主入口。
|
||||
- 社区图例可以作为列表式辅助入口。
|
||||
- 不再额外显示圆形社区选择按钮。
|
||||
- 正常全局视角里,圆形社区按钮和背景色块不能同时存在。
|
||||
|
||||
这样用户只需要理解一件事:点地图上的社区区域,就是选中这个社区。
|
||||
|
||||
### 4. 进入社区
|
||||
|
||||
用户点击“进入社区”后,进入 DOM/SVG 社区阅读视角。
|
||||
|
||||
社区视角承担阅读任务:
|
||||
|
||||
- 社区内节点完整在场。
|
||||
- 核心、选中、搜索命中、Pin 和固定节点可以升级为更高信息密度。
|
||||
- 抽屉可以显示更完整的内容和关系。
|
||||
- 社区视角可以继续使用 DOM/SVG 的成熟富交互。
|
||||
|
||||
这条路径不改成 Sigma。
|
||||
|
||||
### 5. 回全图
|
||||
|
||||
社区里的所有“回全图”动作必须回到 Sigma 全局。
|
||||
|
||||
这包括:
|
||||
|
||||
- 社区工具栏里的“回全图”。
|
||||
- 抽屉里的返回全局动作。
|
||||
- 任何快捷返回全局的入口。
|
||||
|
||||
回全图后:
|
||||
|
||||
- 仍是点状 Sigma 全局地图。
|
||||
- 保留刚才社区的上下文高亮。
|
||||
- 保留搜索、选中、Pin、固定节点和视口上下文。
|
||||
- 不跳到旧 DOM 全图。
|
||||
|
||||
如果 Sigma 已经失败且不可用,才进入对应兜底路径。
|
||||
|
||||
### 6. 搜索和筛选
|
||||
|
||||
搜索和筛选只改变高亮、淡化、列表和摘要,不自动重排整张图。
|
||||
|
||||
如果当前选中的节点或社区被筛选排除:
|
||||
|
||||
- 抽屉保留该对象。
|
||||
- 提示它当前不在筛选结果中。
|
||||
- 提供“清除选择”或“显示该对象”的动作。
|
||||
|
||||
搜索、筛选、Pin 和固定位置不能互相抢状态。
|
||||
|
||||
### 7. 全局拖拽和固定位置
|
||||
|
||||
Sigma 全局必须支持节点拖拽。
|
||||
|
||||
采用已有成熟规则:
|
||||
|
||||
- 用户拖动全局节点时,节点跟随指针移动。
|
||||
- 松手后该节点自动固定。
|
||||
- 固定状态等同于用户明确调整过位置,应持久保存。
|
||||
- 固定节点在搜索、筛选、进入社区、回全局、刷新页面和重启应用后都要尽量保持位置。
|
||||
- 取消固定必须是明确动作,例如按钮或菜单项,不使用容易误触的双击。
|
||||
|
||||
固定位置是一套共享状态,不属于某一个渲染器。
|
||||
|
||||
因此:
|
||||
|
||||
- Sigma 全局读写同一份固定位置。
|
||||
- DOM 社区读取同一份固定位置,用于保持用户熟悉的锚点。
|
||||
- DOM 小图兜底读取同一份固定位置,避免兜底时图突然变成另一张。
|
||||
|
||||
### 8. 悬停、缩放和拖动画布
|
||||
|
||||
全局视角需要达到旧 DOM 全图的日常可用手感:
|
||||
|
||||
- 悬停能提示节点或社区。
|
||||
- 缩放只调整信息密度,不自动进入社区。
|
||||
- 拖动画布保持地图连续。
|
||||
- 交互过程中可以临时隐藏普通标签、弱边和复杂效果。
|
||||
- 核心锚点、搜索命中、Pin、固定节点和选中对象不能突然消失。
|
||||
|
||||
### 9. 社区名字与地图标注
|
||||
|
||||
社区名字按“地图地名”处理,不按按钮处理。
|
||||
|
||||
默认全局视角:
|
||||
|
||||
- 只显示少量重要社区名。
|
||||
- 社区名贴近对应社区色块的视觉中心或稳定空白位置。
|
||||
- 社区名是一行轻量标注,过长省略。
|
||||
- 社区名不放进圆形容器。
|
||||
- 拥挤时优先隐藏普通社区名,只保留当前选中、悬停、搜索相关或重要社区名。
|
||||
|
||||
悬停或选中社区时:
|
||||
|
||||
- 对应社区名变清晰。
|
||||
- 可以显示完整社区名和节点数。
|
||||
- 背景色块轻微高亮。
|
||||
- 抽屉显示社区摘要。
|
||||
|
||||
社区名的职责是帮助读地图,不是成为第二个社区入口。
|
||||
|
||||
### 10. 拥挤降噪
|
||||
|
||||
全局拥挤时,靠“缩小点和线、淡化弱边、减少标签”保持点状地图的可读性。2000 节点以内不需要把节点折叠成聚合块。
|
||||
|
||||
降噪分三层:
|
||||
|
||||
1. 正常密度:显示点、关键线、社区色块和少量社区名。
|
||||
2. 开始拥挤:缩小普通节点,降低弱边透明度,隐藏普通标签。
|
||||
3. 明显拥挤:继续缩小普通点,只保留关系骨架;社区名减少到重要、悬停、选中或搜索相关社区。
|
||||
|
||||
以下对象不被降噪隐藏,哪怕周围拥挤也要优先露出:
|
||||
|
||||
- 当前选中节点。
|
||||
- 搜索命中节点。
|
||||
- Pin 节点。
|
||||
- 用户拖过并固定位置的节点。
|
||||
- 社区核心节点。
|
||||
|
||||
这些对象是用户正在关心的锚点。
|
||||
|
||||
### 11. 规模上限与超限保护
|
||||
|
||||
正常情况下节点规模在 2000 以内,全局始终是可直接渲染的点状地图。
|
||||
|
||||
万一某个知识库节点数超过上限:
|
||||
|
||||
- 不卡死,不回旧 DOM 全图。
|
||||
- 给出明确提示,例如“图较大,请用筛选缩小范围”。
|
||||
- 引导用户用筛选 / 进入社区 / 搜索来缩小当前可见范围。
|
||||
|
||||
不为超限场景构建节点聚合或第二套全图产品。
|
||||
|
||||
### 12. 空白点击
|
||||
|
||||
点击全局空白区域只清理当前临时选择或关闭浮层。
|
||||
|
||||
空白点击不负责:
|
||||
|
||||
- 切换新旧全图。
|
||||
- 进入社区。
|
||||
- 回全图。
|
||||
- 重排布局。
|
||||
|
||||
## 视图切换的视觉连续性
|
||||
|
||||
全局是 Sigma(WebGL 点状),社区是 DOM/SVG(富阅读卡片),两种渲染天生不同。切换时要让用户觉得“我放大 / 缩小了同一张地图”,而不是“跳到了另一个页面”。
|
||||
|
||||
心法:全局与社区不是两个视图,而是同一张地图的两个缩放层级。
|
||||
|
||||
按重要性分三层处理。
|
||||
|
||||
### 1. 节点表现统一
|
||||
|
||||
同一个节点在全局和社区必须是“同一个对象的不同详细程度”,不是两种外观:
|
||||
|
||||
- 颜色按类型 / 社区决定,全局与社区完全一致。
|
||||
- 节点的基本形态是“带颜色的点 + 标签”;放大时同一个点展开为卡片,卡片是“点被打开”,不是另一种物件。
|
||||
- Sigma 高缩放下的卡片与 DOM 社区的卡片共用同一套卡片样式(圆角、阴影、社区色描边一致)。配色与字体已通过共享样式来源统一,重点是统一“点↔卡片”这层表现。
|
||||
|
||||
### 2. 位置连续,不瞬移
|
||||
|
||||
- 进入社区时,社区视图的初始节点位置以这些节点在全局中的相对位置为起点,再缓动到阅读布局;不丢进全新或随机布局。
|
||||
- 这一条同时解决“固定位置在社区如何使用”:固定 / 全局坐标作为进场起点,再过渡到阅读布局,二者不冲突。
|
||||
- 进入社区表现为“镜头推进”:Sigma 先朝目标社区色块聚拢、其余淡出,再把社区视图淡入到该色块刚才的位置与大小;回全图反向。
|
||||
- 用户刚点击 / 选中的节点,在切换前后保持位置不跳变、外观一致。
|
||||
|
||||
### 3. 切换是渐变,不是硬切
|
||||
|
||||
- 两套渲染器交替时使用约 200–300ms 的交叉淡入淡出,并对齐缩放与中心,让它像“一张图变清晰”,而不是“两张图对调”。
|
||||
- 外壳(工具条、右抽屉、背景)在切换前后保持一致,不在两条路由间换皮。
|
||||
|
||||
分阶段:先做便宜且收益最大的部分——共享卡片样式、社区色、统一外壳、进场用全局位置、交叉淡入淡出、选中节点不跳变。逐节点跨渲染器的位移动画(每个节点从全局坐标滑到社区坐标)工作量大、收益递减,先不做,必要时再加。
|
||||
|
||||
不为消除接缝而把社区改成 Sigma:社区是精读场景,DOM 的富文本卡片与抽屉天然优于 WebGL。两套渲染器是合理分工,接缝靠上面三层弥合。
|
||||
|
||||
## 架构边界
|
||||
|
||||
### 不直接“搬 DOM 到 Sigma”
|
||||
|
||||
把旧 DOM 全图的代码直接迁移到 Sigma 不是最优解。
|
||||
|
||||
原因是旧 DOM 全图里混合了两类东西:
|
||||
|
||||
- 成熟的产品规则,例如点击节点、点击社区、Pin、固定位置、抽屉、搜索高亮。
|
||||
- DOM/SVG 特有的绘制方式,例如元素结构、样式、事件绑定和布局细节。
|
||||
|
||||
真正应该保留的是第一类产品规则,而不是第二类绘制实现。
|
||||
|
||||
因此,正确做法是:
|
||||
|
||||
**抽出旧 DOM 全图已经验证过的交互语义,让 Sigma 全局实现这些语义。**
|
||||
|
||||
这样后续如果渲染器变化,用户心智不变。
|
||||
|
||||
### 规则层
|
||||
|
||||
全局规则层负责回答:
|
||||
|
||||
- 点到节点应该发生什么。
|
||||
- 点到社区应该发生什么。
|
||||
- 搜索命中如何影响图面和抽屉。
|
||||
- Pin 与固定位置如何表现。
|
||||
- 进入社区和回全图如何切换。
|
||||
- 当前对象被筛掉时如何提示。
|
||||
- 兜底时哪些能力必须保留。
|
||||
|
||||
规则层不负责画图。
|
||||
|
||||
### Sigma 全局
|
||||
|
||||
Sigma 全局负责正常全局地图:
|
||||
|
||||
- 画点、边、社区和关键标签。
|
||||
- 承接缩放、平移、悬停、节点点击、社区命中和节点拖拽。
|
||||
- 把用户点到或拖到的对象交给全局规则层处理。
|
||||
- 展示规则层给出的选中、高亮、Pin、固定和摘要状态。
|
||||
|
||||
Sigma 不保存另一份业务状态,也不自行决定抽屉内容。
|
||||
|
||||
### DOM 社区
|
||||
|
||||
DOM 社区负责社区阅读:
|
||||
|
||||
- 完整展示社区节点。
|
||||
- 承担内容阅读、节点详情和更高密度交互。
|
||||
- 使用同一份选中、搜索、Pin 和固定位置状态。
|
||||
- “回全图”动作交还给全局路由,而不是在 DOM 内部自己回旧全图。
|
||||
|
||||
### DOM 小图兜底
|
||||
|
||||
DOM 小图兜底只在 Sigma 不可用且图规模较小时出现。
|
||||
|
||||
它必须遵守与 Sigma 全局相同的规则:
|
||||
|
||||
- 点击节点打开轻量节点摘要。
|
||||
- 点击社区打开轻量社区摘要。
|
||||
- 进入社区走 DOM 社区。
|
||||
- 回全图仍由全局路由判断。
|
||||
- 搜索、筛选、Pin、固定位置和抽屉规则一致。
|
||||
|
||||
用户不应该感觉兜底后换了一套产品。
|
||||
|
||||
### 超限降级提示
|
||||
|
||||
不再设“聚合安全视图”。节点规模控制在 2000 以内,正常不会触发超大图。
|
||||
|
||||
唯一的超限保护是一条静态降级提示:
|
||||
|
||||
- 不卡死,不回旧 DOM 全图。
|
||||
- 告诉用户图较大,引导用筛选 / 搜索 / 进入社区缩小范围。
|
||||
- 提供清除选择、重试等基本动作。
|
||||
|
||||
它不是第二套全图产品,也不构建节点聚合。
|
||||
|
||||
### Sigma 可用性判定
|
||||
|
||||
“Sigma 是否可用”是一次性的全局判定结果,由全局统一判断并广播,各视图共享同一结论:
|
||||
|
||||
- 判定为不可用的条件:Sigma 初始化失败,或运行期发生不可恢复错误(例如 WebGL 上下文缺失 / 丢失)。
|
||||
- 判定为不可用后,图在上限内走 DOM 小图兜底。
|
||||
- “回全图”只有在已判定 Sigma 不可用时才进入兜底,否则一律回 Sigma 全局。
|
||||
- 超限降级是独立的另一条保护:只要节点数超过上限就触发,与 Sigma 是否可用无关。
|
||||
|
||||
## 状态同步规则
|
||||
|
||||
先区分两个“统一”:**交互规则**由全局承载(Sigma 全局、DOM 小图兜底)共用;**以下状态**由全部视图(含 DOM 社区)共用。
|
||||
|
||||
以下状态必须在 Sigma 全局、DOM 社区、DOM 小图兜底之间共享:
|
||||
|
||||
- 当前知识库。
|
||||
- 当前视图路由。
|
||||
- 当前选中节点或社区。
|
||||
- 当前搜索词和搜索命中。
|
||||
- 当前筛选条件。
|
||||
- Pin 节点。
|
||||
- 固定节点位置。
|
||||
- 当前社区上下文。
|
||||
- 右侧抽屉对象。
|
||||
- Sigma 是否可用的判定结果。
|
||||
|
||||
尤其要注意固定位置:
|
||||
|
||||
- 固定位置不是 Sigma 私有状态。
|
||||
- 固定位置不是 DOM 私有状态。
|
||||
- 固定位置是用户对知识地图的编辑结果。
|
||||
|
||||
只要用户拖过节点,后续各视图都应该尊重这次调整。
|
||||
|
||||
## 兜底规则
|
||||
|
||||
兜底不再按“旧版全图”理解,而按“同规则的异常承载”理解。
|
||||
|
||||
正常情况:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["打开图谱"] --> B["Sigma 全局地图"]
|
||||
B --> C["轻量节点/社区摘要"]
|
||||
C --> D["进入 DOM 社区阅读"]
|
||||
D --> E["回全图"]
|
||||
E --> B
|
||||
```
|
||||
|
||||
Sigma 不可用:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["打开图谱"] --> B["Sigma 不可用"]
|
||||
B --> C["DOM 小图兜底"]
|
||||
C --> D["同一套全局规则"]
|
||||
```
|
||||
|
||||
节点超过上限(极少发生):
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["打开图谱"] --> B["节点数超过上限"]
|
||||
B --> C["超限降级提示"]
|
||||
C --> D["引导筛选/搜索/进社区"]
|
||||
```
|
||||
|
||||
兜底状态下可以降低视觉和性能要求,但不能换掉交互心智。
|
||||
|
||||
## 明确不做
|
||||
|
||||
本次收口不包含:
|
||||
|
||||
- 把社区阅读改成 Sigma。
|
||||
- 把全局改成卡片化视图。
|
||||
- 保留正常全局里的圆形社区选择按钮。
|
||||
- 新增山水纸面感图谱视觉。
|
||||
- 增加用户可见的新旧全图切换。
|
||||
- 重做关系数据生产。
|
||||
- 新增路径解释、导出图片、图谱 lint 等额外能力。
|
||||
- 把大图异常回退到旧 DOM 全图硬撑。
|
||||
- 用双击空白这类隐式动作承担取消固定或切换视图。
|
||||
- 节点聚合折叠(“+N 省略号”)与聚合平滑。
|
||||
- 面向超大图的聚合安全视图(改为极简超限提示)。
|
||||
- 逐节点跨渲染器的位移动画(先做淡入淡出即可)。
|
||||
|
||||
## 验收标准
|
||||
|
||||
实现完成后,至少需要满足以下可观察结果:
|
||||
|
||||
1. 打开“图谱”Tab,第一眼是 Sigma 点状全局地图,不是 DOM 全图,不是卡片墙。
|
||||
2. 全局点击节点,停留在全局,打开轻量节点摘要。
|
||||
3. 全局点击社区,停留在全局,打开轻量社区摘要。
|
||||
4. 点击“进入社区”,进入 DOM 社区阅读。
|
||||
5. 在社区点击工具栏“回全图”,回到 Sigma 全局,不回旧 DOM 全图。
|
||||
6. 从抽屉或其他入口回全图,也回到 Sigma 全局。
|
||||
7. Sigma 全局可以拖动节点,松手后节点固定。
|
||||
8. 刷新页面后,固定节点位置仍恢复。
|
||||
9. 进入社区再回全局,固定节点位置仍恢复。
|
||||
10. 搜索、筛选、选中、Pin 和固定位置在全局与社区之间不丢失。
|
||||
11. Sigma 不可用时,DOM 小图兜底仍遵守同一套全局规则,不进入旧 DOM 全图。
|
||||
12. 节点超过上限时只显示超限降级提示,不卡死、不进入旧 DOM 全图、不出现节点聚合。
|
||||
13. 全局视角不出现节点卡片。
|
||||
14. 正常全局视角不出现圆形社区选择按钮;背景社区色块是地图上的社区选择入口。
|
||||
15. 社区名以地图地名方式显示,不塞进圆形按钮。
|
||||
16. 拥挤时缩小普通节点、淡化弱边、减少标签;不出现节点聚合折叠。
|
||||
17. 社区里的“回全图”上交全局路由切回 Sigma;代码层不存在第二条用户正常可达的 DOM 全图入口。
|
||||
18. 切换全局↔社区时,被选中节点颜色 / 标签一致、位置不跳变;切换为淡入淡出而非硬切;社区进场以全局相对位置为起点。
|
||||
19. 用户看不到“旧版 / 新版全图”这样的模式分叉。
|
||||
|
||||
## 风险与处理原则
|
||||
|
||||
### 风险 1:拖拽固定位置牵涉多视图状态
|
||||
|
||||
处理原则:固定位置是用户成果,必须做成共享状态。不要让 Sigma、DOM 社区和 DOM 兜底各自保存位置。
|
||||
|
||||
### 风险 2:DOM 社区内部仍有自己的回全图逻辑
|
||||
|
||||
已确认为本次根因(见“现状诊断”):社区工具条的“回全图”调用 DOM 管线内部的 `resetViewState()`,没有切回 Sigma。
|
||||
|
||||
处理原则:社区里的回全图动作必须上交全局路由,不允许 DOM 社区自行切回旧 DOM 全图。
|
||||
|
||||
### 风险 3:兜底体验再次分叉
|
||||
|
||||
处理原则:DOM 小图兜底不是旧产品复活。它必须调用同一套全局规则,只换渲染承载。
|
||||
|
||||
### 风险 4:全局地图被做成阅读界面
|
||||
|
||||
处理原则:全局只看结构。阅读、详情和高密度内容留给社区视角和右侧抽屉。
|
||||
|
||||
### 风险 5:两套渲染器在切换处视觉割裂
|
||||
|
||||
处理原则:把全局与社区当作同一张地图的两个缩放层级——统一节点表现(点↔卡片同源)、保持位置连续、用淡入淡出替代硬切。详见“视图切换的视觉连续性”。
|
||||
|
||||
## 后续交接边界
|
||||
|
||||
后续如果进入实现计划,应只围绕以下交付边界展开,不扩大到图谱新功能:
|
||||
|
||||
- 统一全局返回路径(社区“回全图”上交全局路由切回 Sigma,替换 DOM 管线内部的 `resetViewState` 自我重置)。
|
||||
- 统一全局交互规则。
|
||||
- 补齐 Sigma 全局日常交互。
|
||||
- 沿用全局节点拖拽和固定位置能力。
|
||||
- 移除正常全局里的圆形社区按钮,改为社区色块 + 地图地名。
|
||||
- 接入拥挤降噪规则:缩点、淡边、减标签,不做节点聚合。
|
||||
- 实现全局↔社区切换的视觉连续性(节点表现统一、位置连续、淡入淡出)。
|
||||
- 同步 DOM 小图兜底规则。
|
||||
- 节点超过上限时显示超限降级提示。
|
||||
- 按本文验收标准逐项验证。
|
||||
|
||||
这份文档只定义设计和验收,不直接进入实现。
|
||||
|
||||
## 修订记录
|
||||
|
||||
- 2026-06-21(二版):按 ≤2000 节点收敛——删除节点聚合、聚合平滑与聚合安全视图,渲染承载收敛为三种;补“现状诊断”确认根因(社区回全图调用 DOM 管线 `resetViewState`,未切回 Sigma);新增“视图切换的视觉连续性”;补 Sigma 可用性判定与相关验收项。
|
||||
@@ -0,0 +1,185 @@
|
||||
# 对话自动跟随设计
|
||||
|
||||
日期:2026-06-22
|
||||
状态:设计已确认,待实现
|
||||
当前分支:`codex/fix-chat-auto-scroll`
|
||||
|
||||
## 目的
|
||||
|
||||
这份设计用于解决 llm-wiki-agent 对话区在发送消息后不会自动向下推进的问题。
|
||||
|
||||
当前现象是:用户发送消息后,AI 回复已经开始生成,但对话内容不会自动滚到最新位置。用户需要手动用滚轮向下滚,才能看到新回复或回复后续内容。这会打断对话连续性,尤其在长回复和工具调用之后更明显。
|
||||
|
||||
目标体验是一句话:
|
||||
|
||||
**用户在看最新回复时,对话自动跟随;用户主动上翻看历史时,界面尊重用户,不强行拉回底部。**
|
||||
|
||||
## 背景依据
|
||||
|
||||
llm-wiki-agent 的产品定位是“以对话为中心的本地知识库工作台”。对话主区是核心入口,自动跟随最新回复属于基础对话体验。
|
||||
|
||||
本设计不是新增复杂功能,也不是改变对话模型、消息协议或会话存储。它只收口一件事:对话区应该在正确的时候展示最新内容。
|
||||
|
||||
已确认的现状:
|
||||
|
||||
- 对话消息区本身是可滚动区域。
|
||||
- 发送消息会立即新增用户消息和空的 AI 回复气泡。
|
||||
- AI 回复是流式生成的,内容会持续变长。
|
||||
- 当前没有专门的“是否跟随底部”的状态管理。
|
||||
|
||||
## 设计结论
|
||||
|
||||
采用“智能跟随 + 向下箭头按钮”的方案。
|
||||
|
||||
1. 用户发送消息后,对话区立即滚到最新消息位置。
|
||||
2. AI 回复生成过程中,如果用户仍在底部附近,对话区持续跟随最新内容。
|
||||
3. 如果用户主动上翻历史,对话区暂停自动跟随。
|
||||
4. 暂停跟随后,显示向下箭头按钮,提醒用户有最新内容可回到。
|
||||
5. 用户点击向下箭头按钮,或自己手动滚回底部附近后,恢复自动跟随。
|
||||
|
||||
这里的“底部附近”指用户距离消息区底部很近,已经是在看最新内容。实现时可使用一个固定容差,例如 80-120 px;具体数值以不误判用户上翻为优先。
|
||||
|
||||
不采用以下方案:
|
||||
|
||||
- 不做“永远强制滚到底部”,因为会打断用户阅读历史消息。
|
||||
- 不做“只在发送时滚一次”,因为 AI 回复流式变长时仍可能看不到后续内容。
|
||||
|
||||
## 用户体验规则
|
||||
|
||||
### 1. 发送消息
|
||||
|
||||
用户点击发送或使用快捷键发送后:
|
||||
|
||||
- 输入框清空。
|
||||
- 新的用户消息和 AI 回复占位出现在对话尾部。
|
||||
- 对话区自动滚到尾部。
|
||||
- 自动跟随状态开启。
|
||||
|
||||
这样用户能立即看到“消息已发出,AI 正在回复”。
|
||||
|
||||
### 2. 回复生成中
|
||||
|
||||
AI 回复逐步生成时:
|
||||
|
||||
- 如果用户没有上翻,最新回复始终保持在可见范围内。
|
||||
- 回复内容变长、工具状态出现、工具状态完成、Markdown 表格展开等情况,都应继续跟随。
|
||||
- 跟随不需要动画夸张,重点是稳定地看到最新内容。
|
||||
|
||||
### 3. 用户主动上翻
|
||||
|
||||
如果用户把对话区往上滚,离开底部一段距离:
|
||||
|
||||
- 自动跟随暂停。
|
||||
- 后续 AI 回复继续生成,但不强行滚动页面。
|
||||
- 显示向下箭头按钮。
|
||||
|
||||
这里的产品原则是:一旦用户主动查看历史,系统不能替用户夺回滚动位置。
|
||||
|
||||
### 4. 恢复跟随
|
||||
|
||||
有两种方式恢复:
|
||||
|
||||
- 用户点击向下箭头按钮。
|
||||
- 用户自己滚回底部附近。
|
||||
|
||||
恢复后:
|
||||
|
||||
- 对话区滚到最新内容。
|
||||
- 向下箭头按钮消失。
|
||||
- 如果 AI 仍在生成,继续自动跟随。
|
||||
|
||||
### 5. 切换会话或知识库
|
||||
|
||||
切换会话或知识库后,新的对话面板应按当前会话历史展示:
|
||||
|
||||
- 如果进入的是已有长对话,默认展示底部最新消息。
|
||||
- 如果是空会话,展示空状态。
|
||||
- 切换动作不继承上一个会话的“暂停跟随”状态。
|
||||
|
||||
## 界面边界
|
||||
|
||||
回到底部入口采用类似 Codex 的向下箭头图标按钮,不使用文字按钮。
|
||||
|
||||
按钮规则:
|
||||
|
||||
- 使用单个向下箭头表达“回到底部”。
|
||||
- 按钮为轻量圆形或近圆形,视觉上像悬浮的小控制点。
|
||||
- 放在对话区底部中间附近,悬浮在输入区上方,不遮挡消息正文。
|
||||
- 样式跟随当前主题,使用柔和背景、细边框和轻微阴影;暗色和浅色模式都要自然。
|
||||
- 鼠标悬停时只做轻微强调,不做夸张动画。
|
||||
- 只在用户离开底部且有必要回到底部时出现。
|
||||
- 按钮本身不显示文字,但可访问名称应是“回到底部”。
|
||||
- 本次不增加未读数量;如果以后需要未读计数,另开设计,不混入本次实现。
|
||||
|
||||
本次不做:
|
||||
|
||||
- 未读消息数量。
|
||||
- 跳转到某一条消息。
|
||||
- 对话搜索定位。
|
||||
- 历史消息虚拟列表。
|
||||
- 全局页面滚动重构。
|
||||
|
||||
## 组件与数据流
|
||||
|
||||
实现应集中在对话面板内部完成,不改变后端接口。
|
||||
|
||||
需要维护的前端状态:
|
||||
|
||||
- 当前是否处于自动跟随状态。
|
||||
- 当前是否离底部足够近。
|
||||
- 是否需要显示向下箭头按钮。
|
||||
|
||||
主要数据流:
|
||||
|
||||
1. 用户发送消息,消息列表新增内容,开启自动跟随。
|
||||
2. AI 流式回复更新消息内容。
|
||||
3. 消息内容变化后,如果自动跟随开启,则滚到尾部。
|
||||
4. 用户滚动消息区时,判断是否离开底部。
|
||||
5. 离开底部则暂停自动跟随并显示向下箭头按钮。
|
||||
6. 用户点击向下箭头按钮或滚回底部,则恢复自动跟随。
|
||||
|
||||
## 异常与边界
|
||||
|
||||
需要覆盖以下情况:
|
||||
|
||||
- 对话为空时不显示向下箭头按钮。
|
||||
- 只有图标按钮时,键盘和读屏仍能识别它是“回到底部”。
|
||||
- 回复出错时,如果用户在底部附近,错误提示也应可见。
|
||||
- 用户停止生成时,滚动状态不应乱跳。
|
||||
- 工具调用状态展开或收起时,如果用户在底部附近,应继续跟随。
|
||||
- 大段 Markdown、表格、代码块渲染后高度变化时,仍能保持跟随。
|
||||
- 用户正在上翻时,即使回复继续增长,也不能强行滚回底部。
|
||||
|
||||
## 验收标准
|
||||
|
||||
实现完成后至少验证以下场景:
|
||||
|
||||
1. 发出一条普通消息后,不需要滚轮就能看到 AI 回复开始。
|
||||
2. 长回复生成过程中,页面持续停在最新内容附近。
|
||||
3. 回复生成时用户上翻历史,页面不再被自动拉到底部。
|
||||
4. 用户上翻后看到符合当前主题的向下箭头按钮,而不是文字按钮。
|
||||
5. 用户点击向下箭头按钮后,立即回到最新回复,并恢复自动跟随。
|
||||
6. 用户自己滚回底部后,向下箭头按钮消失,后续回复继续自动跟随。
|
||||
7. 切换到已有长会话时,默认看到最新消息。
|
||||
8. 回复出错或停止生成时,界面没有异常跳动。
|
||||
|
||||
## 验证方式
|
||||
|
||||
推荐验证分三层:
|
||||
|
||||
1. 增加前端组件测试,覆盖发送、流式更新、用户上翻、向下箭头按钮显示与恢复。
|
||||
2. 运行现有前端测试和类型检查,确认没有破坏输入框、导出按钮、消息气泡和工具状态。
|
||||
3. 启动本地应用,在浏览器里真实发送一条长回复请求,手动验证自动跟随和上翻暂停。
|
||||
|
||||
## 后续实现范围
|
||||
|
||||
后续实现应只修改对话面板和必要样式。
|
||||
|
||||
不改:
|
||||
|
||||
- 后端流式协议。
|
||||
- 会话存储格式。
|
||||
- 知识库选择逻辑。
|
||||
- 图谱、右抽屉、搜索、导出等无关模块。
|
||||
|
||||
如果实现过程中发现对话区滚动容器和页面布局有冲突,应先优先修正对话区内部滚动边界,不扩大为整体布局重构。
|
||||
@@ -0,0 +1,266 @@
|
||||
# 社区节点轻量关系地图设计
|
||||
|
||||
日期:2026-06-22
|
||||
状态:待用户复核
|
||||
当前分支:`codex/graph-community-node-visual-design`
|
||||
视觉稿:`designs/graph-community-node-map/Community Node Map Prototype.html`
|
||||
|
||||
## 目的
|
||||
|
||||
这份设计用于优化 llm-wiki 图谱里“进入社区后”的节点呈现。
|
||||
|
||||
当前社区内节点更像一组大卡片,卡片体积偏大、节点挨得近,社区背景色块又强化了被框住的感觉。用户进入社区后,本来想看清“这个社区内部有哪些知识点、谁和谁有关”,却容易先看到一片拥挤的卡片。
|
||||
|
||||
本次目标不是重做整个图谱,也不是重新定义社区、边或右侧抽屉,而是把社区内部的视觉层级改成一张轻量关系地图:
|
||||
|
||||
**社区里默认看地图,悬停临时看关系,点击固定看内容。**
|
||||
|
||||
## 强范围边界
|
||||
|
||||
本次只聚焦于图谱里的社区节点设计,具体是:用户已经进入某个社区后,社区内部节点、线、标签、社区底色,以及节点悬停/点击后的关系聚焦表现。
|
||||
|
||||
本次不改:
|
||||
|
||||
- 全局图谱的默认呈现、路由、性能策略和 Sigma 全局规则。
|
||||
- 对话、会话列表、知识库创建、消化、检索、导入导出等非图谱功能。
|
||||
- 社区入口规则,例如从全局如何选择社区、如何进入社区。
|
||||
- 右侧面板的信息结构和内容层级。
|
||||
- 现有关系类型、置信度、权重这些图例概念的含义。
|
||||
- 节点真实数据、知识库文件结构和图谱构建流程。
|
||||
|
||||
允许的联动只有两类:
|
||||
|
||||
- 用户点击社区内节点时,沿用现有机制更新右侧面板里的节点内容。
|
||||
- 为了让社区内线条表现正确,可以读取并复用现有边字段:关系类型、置信度、权重、节点类型和节点连接关系。
|
||||
|
||||
任何超出以上范围的体验调整,都应另开设计,不混进本次实现。
|
||||
|
||||
## 已确认现状
|
||||
|
||||
代码和图例里已经存在三套边信息表达,本次必须继续沿用:
|
||||
|
||||
- 关系类型:实现 / 依赖 / 衍生 / 对比 / 矛盾,用线的颜色表达。
|
||||
- 置信度:原文 / 推断 / 待确认,用线的虚实表达。
|
||||
- 联系强弱:用线的粗细和透明度表达。
|
||||
|
||||
当前实现已有“选中节点后相关边优先”的基础,但还没有把社区内节点明确分成一度、二度和无关三层,也没有把悬停和点击统一成同一套聚焦逻辑。
|
||||
|
||||
因此本次设计不推翻原来的边图例,而是在社区视图里补上更清楚的关系距离规则。
|
||||
|
||||
## 方案选择
|
||||
|
||||
对比过三种方向:
|
||||
|
||||
1. 轻量关系地图:节点像地图坐标点,默认留白多、线很细;聚焦时一度关系清晰,二度关系很淡。
|
||||
2. 聚焦星图:点中节点后把它变成中心,周围关系强烈围绕它呈现。
|
||||
3. 社区知识岛:强化社区边界感,让节点像散落在一个岛上的知识点。
|
||||
|
||||
采用方案 1:轻量关系地图。
|
||||
|
||||
原因:
|
||||
|
||||
- 它最能解决“卡片大、挤、不美观”的问题。
|
||||
- 它保留当前视觉稿里已经被认可的空间感。
|
||||
- 它不会把社区背景变成新的视觉负担。
|
||||
- 它能自然承接现有边图例,而不是发明一套新说明。
|
||||
|
||||
## 默认视觉状态
|
||||
|
||||
进入社区后,画面不再铺满节点卡片,而是一张轻量关系地图。
|
||||
|
||||
节点像散开的坐标点,有明显留白和空间感。普通节点保持小点形态,重点节点才默认显示名字。重点节点包括社区核心节点、结构上更重要的节点、搜索命中、已固定节点,以及当前选中节点。
|
||||
|
||||
社区底色保留,但只作为很淡的氛围提示。它说明“你正在这个社区里”,不作为硬边界,也不限制节点位置。节点可以在视觉上更自由地分布,避免被一整块背景色压住。
|
||||
|
||||
线在默认状态下很细、很安静,只提示社区内部存在关系,不抢节点层级。默认状态不强调所有边的完整解读,只有用户悬停或点击节点时,关系才进入可读状态。
|
||||
|
||||
节点颜色继续区分节点性质,例如主题、实体、来源、对比或综合类节点。颜色必须克制,不和边的颜色抢语义。
|
||||
|
||||
## 标签策略
|
||||
|
||||
默认只显示重点节点标签。
|
||||
|
||||
普通节点默认不显示名字,只保留可见点位。鼠标悬停或节点被聚焦时,才显示对应标签。这样社区第一眼是地图,而不是文字墙。
|
||||
|
||||
标签显示必须稳定:
|
||||
|
||||
- 悬停时标签出现,但节点本身不能因为标签出现而移位。
|
||||
- 标签不应撑大节点的命中区域。
|
||||
- 标签应优先避开核心节点和高亮线,不让关键关系被遮住。
|
||||
- 标签过长时截断,不把画面横向撑开。
|
||||
|
||||
## 悬停与点击
|
||||
|
||||
悬停和点击使用同一套关系聚焦逻辑。
|
||||
|
||||
区别只在于:
|
||||
|
||||
- 悬停是临时聚焦,鼠标移走后恢复。
|
||||
- 点击是固定聚焦,会保持住,并同步更新右侧面板。
|
||||
|
||||
当没有固定选中节点时,鼠标放到某个节点上,地图立即进入临时聚焦:
|
||||
|
||||
- 当前节点最清楚。
|
||||
- 一度相关节点清楚显示。
|
||||
- 当前节点到一度节点之间的线清楚显示。
|
||||
- 二度相关节点保留很淡的背景感。
|
||||
- 无关节点和无关线退到几乎不可见。
|
||||
- 鼠标移走后恢复默认地图。
|
||||
|
||||
点击某个节点后:
|
||||
|
||||
- 使用同样的聚焦视觉。
|
||||
- 该聚焦状态固定。
|
||||
- 右侧面板显示该节点内容。
|
||||
- 点击另一个节点时切换固定聚焦。
|
||||
- 点击社区画布空白处时,取消固定聚焦,回到默认地图。
|
||||
|
||||
当已经有固定选中节点时,再悬停其他节点:
|
||||
|
||||
- 临时展示悬停节点的关系。
|
||||
- 鼠标移走后回到已固定节点的聚焦状态。
|
||||
- 不因为悬停改写右侧面板内容。
|
||||
|
||||
## 关系距离规则
|
||||
|
||||
以当前被悬停或点击的节点作为焦点节点。
|
||||
|
||||
一度相关节点:
|
||||
|
||||
- 与焦点节点直接相连。
|
||||
- 直接连线清晰可见。
|
||||
- 节点标签优先显示。
|
||||
- 节点透明度和点尺寸略高于默认。
|
||||
|
||||
二度相关节点:
|
||||
|
||||
- 与一度节点相连,但不直接连接焦点节点。
|
||||
- 只保留很淡的背景存在感。
|
||||
- 默认不显示标签。
|
||||
- 二度连线通常不完整呈现,只作为极淡的延伸提示。
|
||||
|
||||
无关节点:
|
||||
|
||||
- 与焦点节点没有一度或二度关系。
|
||||
- 退到几乎不可见,但不制造画面跳变。
|
||||
- 默认不显示标签。
|
||||
- 无关节点之间的线隐藏或接近不可见。
|
||||
|
||||
这里的“一度 / 二度”按现有图谱边关系计算,不引入新的语义数据。
|
||||
|
||||
## 线条表达
|
||||
|
||||
线条继续遵守现有图例:
|
||||
|
||||
- 颜色表示关系类型。
|
||||
- 虚实表示置信度。
|
||||
- 粗细表示联系强弱。
|
||||
|
||||
聚焦状态下,一度直接连线应最清楚。它既要保留颜色和虚实语义,也要通过粗细让强关系更容易被看见。
|
||||
|
||||
二度关系可以保留轻微线索,但不能抢一度关系。无关线默认隐藏或极淡。
|
||||
|
||||
默认状态下,社区内部所有线都应更克制,避免整张社区图变成线团。只有悬停或点击后,用户才需要读具体关系。
|
||||
|
||||
## 右侧面板
|
||||
|
||||
地图只负责表达关系,右侧面板负责承接内容。
|
||||
|
||||
社区地图里的节点不再展开大卡片,也不显示正文摘要。节点旁边最多显示名字。正文、摘要、路径、相关页面、节点详情继续放在右侧面板里。
|
||||
|
||||
点击节点会更新右侧面板,这是允许的现有联动。悬停节点只做地图内临时关系预览,不更新右侧面板。
|
||||
|
||||
## 社区底色
|
||||
|
||||
社区底色应变得更轻。
|
||||
|
||||
它的作用是提示当前社区,而不是圈住节点。视觉上更像一层淡淡的纸面水洗背景,而不是一块实心区域。
|
||||
|
||||
当节点进入聚焦状态时,社区底色应进一步退后,让当前节点和一度关系成为主视觉。
|
||||
|
||||
## 状态优先级
|
||||
|
||||
同一时间最多有一个固定焦点节点。
|
||||
|
||||
视觉优先级从高到低:
|
||||
|
||||
1. 当前悬停节点。
|
||||
2. 当前固定选中节点。
|
||||
3. 一度相关节点和直接连线。
|
||||
4. 二度相关节点。
|
||||
5. 默认重点节点。
|
||||
6. 普通节点。
|
||||
7. 无关节点和无关线。
|
||||
|
||||
如果存在固定选中节点,悬停其他节点时可以临时盖过固定焦点;悬停结束后恢复固定焦点。
|
||||
|
||||
## 空状态和边界情况
|
||||
|
||||
社区没有边:
|
||||
|
||||
- 节点仍按轻量地图显示。
|
||||
- 默认只显示重点标签。
|
||||
- 悬停节点时只强调该节点本身,并提示暂无直接关系。
|
||||
|
||||
节点只有一度、没有二度:
|
||||
|
||||
- 一度关系正常高亮。
|
||||
- 不强行制造二度背景。
|
||||
|
||||
节点连接过多:
|
||||
|
||||
- 一度节点可以全部参与高亮,但标签仍按重要性控制。
|
||||
- 线条可按权重区分强弱,避免全部同等抢眼。
|
||||
|
||||
关系类型或置信度缺失:
|
||||
|
||||
- 关系类型缺失时走默认关系色。
|
||||
- 置信度缺失时走默认实线。
|
||||
- 不因为缺字段破坏社区地图整体可读性。
|
||||
|
||||
## 体验验收
|
||||
|
||||
实现完成后,进入社区的第一眼必须更像关系地图,不像卡片墙。
|
||||
|
||||
必须满足:
|
||||
|
||||
- 默认状态有留白,有空间感,节点不挤。
|
||||
- 普通节点是轻量点位,不是大卡片。
|
||||
- 只有重点节点默认显示标签。
|
||||
- 社区底色很淡,不压住节点。
|
||||
- 默认线条细、轻,不形成线团。
|
||||
- 悬停节点时,关系立刻变清楚。
|
||||
- 点击节点时,关系固定,并更新右侧面板。
|
||||
- 鼠标移走后,悬停关系自动收回。
|
||||
- 点击空白处后,固定关系取消。
|
||||
- 一度关系清楚,二度关系很淡,无关内容退后。
|
||||
- 线的颜色、虚实、粗细与图例一致。
|
||||
- 标签出现和消失时,节点不移位。
|
||||
- 不改变全局图谱、对话、知识库流程或其他非社区图谱功能。
|
||||
|
||||
## 验证建议
|
||||
|
||||
实现时至少验证以下场景:
|
||||
|
||||
1. 进入一个有 10 个以上节点、关系较密的社区,确认默认状态不是卡片墙。
|
||||
2. 悬停一个核心节点,确认一度关系清晰、二度很淡、无关线隐藏或极淡。
|
||||
3. 鼠标移走,确认画面恢复默认状态。
|
||||
4. 点击节点,确认聚焦固定并更新右侧面板。
|
||||
5. 固定节点后悬停另一个节点,确认只是临时预览,移走后回到固定节点。
|
||||
6. 点击空白处,确认取消固定聚焦。
|
||||
7. 检查图例里的关系类型和置信度,与画布线条颜色和虚实一致。
|
||||
8. 检查长标签、密集节点、无边节点,确认节点不会因为标签出现而移位。
|
||||
9. 回到全局图谱,确认本次改动没有改变全局图谱行为。
|
||||
|
||||
## 后续不在本次做
|
||||
|
||||
以下内容不进入本次社区节点视觉优化:
|
||||
|
||||
- 全局图谱重新设计。
|
||||
- 移动端专项布局。
|
||||
- 新增关系类型或置信度体系。
|
||||
- 新的右侧面板结构。
|
||||
- 自动社区总结。
|
||||
- 图谱数据构建流程调整。
|
||||
- 新增完整键盘漫游。
|
||||
- 新增 Agent 提问入口。
|
||||
@@ -0,0 +1,170 @@
|
||||
# Sigma 覆盖层重构 + 性能改造 设计文档
|
||||
|
||||
- 日期:2026-06-25
|
||||
- 分支:`feat/sigma-overlay-refactor-perf`
|
||||
- 关联 issue:#64 拆分 `sigma-global-renderer.ts`、#63 第 2 条(DOM 重绘重建节点)
|
||||
- 范围:本 spec 覆盖 Phase 1(#64)+ Phase 2(#63.2)两阶段。#63 第 1 条(云层签名全量遍历)与 #65(无障碍)**不在本 spec 范围内**,后续单独处理。
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与动机
|
||||
|
||||
全局视角的知识图谱用 Sigma 在 canvas 上画点和边,再在上面盖一层 DOM/SVG 覆盖物(社区云团、节点命中框、社区标签)。这层覆盖物的全部逻辑集中在
|
||||
`packages/graph-engine/src/render/sigma-global-renderer.ts`,当前 **1386 行**,存在两个已确认的问题:
|
||||
|
||||
1. **单文件过大(#64)**:云层几何、SVG 渲染、凸包计算、坐标变换都堆在一个文件里。仓库已把 wash 逻辑拆到
|
||||
`community-wash.ts` / `community-washes.ts`,本文件应顺势拆分。
|
||||
2. **每帧重建 DOM(#63 第 2 条)**:`renderSigmaOverlays()` 绑在 Sigma 的 `afterRender` 事件上
|
||||
(`sigma-global-renderer.ts:426`),**平移/缩放的每一帧**都先 `overlayRoot.replaceChildren()` 清空
|
||||
(`:623`),再全量重建所有节点命中框、云层 SVG,并重新绑定 click / pointerdown / mousedown / dragstart
|
||||
监听(`:621-715`)。但相机移动时节点集合并未改变,只有屏幕坐标变了——这是典型的 DOM 抖动热路径。
|
||||
|
||||
> 注:PR #60 的 1000 节点回归已通过,当前规模不卡。本改造目标是降低维护成本、消除架构坏味道,为更大规模做准备。
|
||||
|
||||
### 设计原则
|
||||
|
||||
- 两个 Phase 相互独立、可分别合并。**Phase 1 是纯搬迁(零行为变化),Phase 2 才动行为。**
|
||||
- 先拆后优化:在 1000+ 行单文件里直接改性能热路径风险叠加;拆完后 Phase 2 改动面更小、更好测。
|
||||
- 依赖单向:`sigma-coordinates` ← `community-cloud-geometry` ← `sigma-overlay-svg` ← 主文件(编排)。
|
||||
- 不改对外接口(`SigmaGlobalRenderer`),workbench 侧零感知。
|
||||
- 遵循 YAGNI:不做通用 diff 框架、不顺手重构无关代码。
|
||||
|
||||
---
|
||||
|
||||
## 2. Phase 1(#64):按职责拆 3 个 helper
|
||||
|
||||
从 `sigma-global-renderer.ts` 抽出三组纯函数 / 无状态工厂。主文件只保留编排核心:
|
||||
`createSigmaGlobalRenderer`、生命周期、拖拽会话、事件接线、Graphology 属性映射、hit projector、edge style。
|
||||
|
||||
| 新文件 | 搬入内容 | 性质 |
|
||||
|---|---|---|
|
||||
| `render/sigma-coordinates.ts` | `sigmaWorldPointToScreenPoint`、`sigmaScreenPointToWorldPoint`、`overlayPointerScreenPoint` | 坐标变换 |
|
||||
| `render/community-cloud-geometry.ts` | `SigmaCommunityCloud` / `SigmaCommunityCloudBasis` 接口、`sigmaCommunityCloudBasisById` + `...WithReuse` + `...WithNodePoint`、`sigmaCommunityCloudSignature`、`sigmaProjectedCloudHullPoints`、`clampPointToWorldEllipse`、`clampPointToScreenEllipse`、`sigmaCommunityCloud`、`convexHull2d` | 几何 / 凸包,多为纯函数 |
|
||||
| `render/sigma-overlay-svg.ts` | `SIGMA_OVERLAY_SVG_NS`、`nextSigmaCloudFilterSequence`、`sigmaSharedCloudFilterDef`、`sigmaCloudSvg`,及 overlay 元素工厂 `sigmaOverlayButton` / `sigmaOverlayPassiveElement` / `applyOverlayBox` / `createSigmaOverlayRoot` | DOM / SVG 构造 |
|
||||
|
||||
依赖说明:`community-cloud-geometry` 中的投影函数(`sigmaProjectedCloudHullPoints`、`sigmaCommunityCloud`)
|
||||
依赖坐标变换,故 import `sigma-coordinates`,单向依赖。
|
||||
|
||||
预计主文件从 1386 行降到约 1000 行。
|
||||
|
||||
### Phase 1 验收
|
||||
|
||||
- 行为零变化。
|
||||
- 现有 8 个 graph-engine 测试全绿(`npm test`),`npm run typecheck` 通过。
|
||||
- 几何纯函数补独立单测(凸包、签名、clamp、坐标往返)——这些此前埋在主文件内未单独测。
|
||||
|
||||
> 这些函数当前均为**模块私有、未对外导出**,搬到新 helper 后由新 helper 各自 `export`(供单测直接
|
||||
> import),主文件再 import 回来使用。**公开 API(`SigmaGlobalRenderer` 等)与 `src/index.ts`
|
||||
> barrel 不变**,workbench 侧零感知。模块级计数器 `sigmaCloudFilterSequence` 随 SVG 函数整组搬走,
|
||||
> 仍是同一个计数器,行为不变。
|
||||
|
||||
---
|
||||
|
||||
## 3. Phase 2(#63.2):拆"结构更新"与"位置更新"
|
||||
|
||||
把唯一的 `renderSigmaOverlays()`(每帧 `replaceChildren` 全量重建)拆成两条路径。
|
||||
|
||||
### `rebuildSigmaOverlays()` — 仅数据 / 选中变化时调用
|
||||
|
||||
- 按 id 做 diff:复用已存在元素,新增缺失的、移除多余的;维护三张
|
||||
`Map<id, element>`(节点命中框、社区云层 region、社区标签各一张)。
|
||||
- 监听器(click / pointerdown / mousedown / dragstart)**只在元素创建时绑一次**。
|
||||
- 处理由数据决定的状态:selected / dim / searchHit / pinned、label 文本等。
|
||||
|
||||
> 覆盖层只渲染**这 3 类** DOM 元素。aggregation 容器画在 Sigma canvas 上(走 Graphology
|
||||
> 属性),不属于本覆盖层,本改造不涉及。
|
||||
|
||||
### `repositionSigmaOverlays()` — 绑 `afterRender` / resize / 拖拽移动,每帧调用
|
||||
|
||||
- 遍历已存在元素,只更新屏幕坐标(`applyOverlayBox`)与云层 SVG 的 `points` 属性。
|
||||
- **绝不** `replaceChildren` / `createElement` / `addEventListener`。
|
||||
|
||||
### 调用点改造
|
||||
|
||||
| 位置 | 现状 | 改为 |
|
||||
|---|---|---|
|
||||
| 初始渲染(`:336`) | `renderSigmaOverlays()` | `rebuildSigmaOverlays()` |
|
||||
| 数据更新(`:382`) | `renderSigmaOverlays()` | `rebuildSigmaOverlays()` |
|
||||
| `afterRender` 相机移动 | `renderSigmaOverlays()` | `repositionSigmaOverlays()` |
|
||||
| resize raf | `renderSigmaOverlays()` | `repositionSigmaOverlays()` |
|
||||
| 拖拽提交/取消(`applyNodeDragPoint` 终态) | `renderSigmaOverlays()` | `rebuildSigmaOverlays()` |
|
||||
|
||||
选中变化走数据更新路径(触发 rebuild),因此 dim / selected 不进每帧热路径。
|
||||
|
||||
> 实现修正(TDD 发现):原以为"拖拽 → reposition"。实际上拖拽**过程中**的每帧位置更新由
|
||||
> `sigma.refresh()` → `afterRender` → reposition 负责;而 `applyNodeDragPoint` 里那处直接调用是
|
||||
> 拖拽**提交/取消的终态**,此时 pin 状态与永久坐标已变(数据变化),必须 rebuild 才能刷新
|
||||
> `dataset.pinned` 等属性。
|
||||
|
||||
### 状态归属与不变量(防止双路径打架)
|
||||
|
||||
覆盖层完全自封闭:生产代码里只有本文件操作 `overlayRoot` 与几何/SVG 函数,无第二处
|
||||
监听 `afterRender`。因此冲突风险只在文件内部的 rebuild / reposition 之间,必须守住以下不变量:
|
||||
|
||||
- **rebuild 是元素生命周期的唯一权威**:只有它能增删元素、维护三张 `Map<id, element>`、绑监听。
|
||||
- **reposition 对结构只读、且幂等**:只改已存在元素的位置/box 与云 hull 投影;遇到表里有、
|
||||
但当前 `adapterData` 已无的 id,安全跳过(不报错、不创建)。
|
||||
- **reposition 每帧从 `cloudBasisByCommunityId` 重新投影云 hull**:拖拽中 basis 冻结、松手才重算,
|
||||
reposition 必须每次重投影才能让云层跟手(与现有行为一致)。
|
||||
- **`update()` 的执行顺序安全前提**:现状 `sigma.refresh()`(`:381`)→ 真实 Sigma 触发 `afterRender`
|
||||
→ reposition,**早于** 紧随其后的 rebuild(`:382`)。因 reposition 满足"只读+容忍不同步+幂等",
|
||||
这次提前的 reposition 至多对旧元素做一次无害定位,rebuild 随后纠正为准。
|
||||
- **`afterRender` 与 `cameraUpdated` 都绑同一处理器**(`:425-426`),reposition 一帧可能跑 2 次;
|
||||
因幂等无害,暂不做 rAF 去重(YAGNI,后续按需)。
|
||||
|
||||
### 关键风险点
|
||||
|
||||
云层每帧的 hull 投影点会变,所以 reposition 必须更新 polygon 的 `points` 属性和容器 box,
|
||||
同时保留同一个 shape 元素及其 click 监听。这是整个改造里最需要小心的一处,测试需专门覆盖。
|
||||
|
||||
---
|
||||
|
||||
## 4. 错误处理与边界
|
||||
|
||||
- 两条路径都保留 `destroyed` 守卫。
|
||||
- 销毁时清空三张 `Map<id, element>`,避免悬挂引用。
|
||||
- diff 按 id 增删;id 不变但内容变(如 label 文本)在 rebuild 内更新属性。
|
||||
- 不改对外接口 `SigmaGlobalRenderer`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 测试方案
|
||||
|
||||
### Phase 1
|
||||
|
||||
- 现有回归全绿 + 几何纯函数新单测(凸包、签名、clamp、坐标往返)。
|
||||
|
||||
### Phase 2(验收标准:可量化性能基线)
|
||||
|
||||
在 `test/sigma-global-renderer.test.ts` 复用现有 `fakeContainer()` / `fakeRuntime()` / `sigma.emit()`
|
||||
基础设施(fake 元素内部已用 Map 记录子节点与监听,无需引入 jsdom):
|
||||
|
||||
1. **量化基线断言**(核心):渲染后记录元素引用与 createElement / addEventListener 调用计数 →
|
||||
多次 `sigma.emit("afterRender")` → 断言**元素实例不变、createElement 计数不增、监听计数不增、
|
||||
`replaceChildren` 在 reposition 路径未被调用**。
|
||||
2. **行为保持**:点击云层 → 社区选中、点击节点 → 节点选中、拖拽仍生效(沿用现有 emit 用例)。
|
||||
3. **结构 diff**:数据更新增 / 删节点后,元素表正确增删。
|
||||
4. **需更新的现有断言**:现有 resize 测试(`test/sigma-global-renderer.test.ts:574`)当前断言
|
||||
resize 后覆盖层元素被**新建**(`assert.notEqual`)。Phase 2 后元素改为**复用**,这条须改成
|
||||
`assert.equal`。这是**预期内的行为变更,不是回归**——它恰好验证了"reposition 不再重建元素"。
|
||||
5. **第三层手动回归**:在 codex 跑 1000 节点平移 / 缩放,主观确认不卡 + 基线测试通过
|
||||
(按 CLAUDE.md 推送前测试规则)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 分 Phase 推进计划
|
||||
|
||||
- **Phase 0(本次 spark)**:写本 spec,分支 `feat/sigma-overlay-refactor-perf`。
|
||||
- **Phase 1**:拆 3 个 helper → 跑回归 → 分步 commit → 可单独开 PR 合入(纯重构、低风险)。
|
||||
- **Phase 2**:基于 Phase 1 改双路径 + 量化基线测试 → codex 回归 → commit → PR。
|
||||
- **文档**:每个含功能改动的 Phase,按 CLAUDE.md 更新 CHANGELOG / README / 版本号。
|
||||
|
||||
---
|
||||
|
||||
## 7. 验收清单(汇总)
|
||||
|
||||
- [ ] Phase 1:主文件显著瘦身、行为零变化、现有测试与 typecheck 全绿、几何函数有独立单测。
|
||||
- [ ] Phase 2:`afterRender` 路径不再销毁 / 重建 DOM、不重绑监听(量化测试断言通过)。
|
||||
- [ ] Phase 2:点选社区 / 点选节点 / 拖拽行为与改造前一致。
|
||||
- [ ] Phase 2:1000 节点 codex 手动回归平移缩放不卡。
|
||||
- [ ] 对外接口 `SigmaGlobalRenderer` 无变化,workbench 无需改动。
|
||||
@@ -0,0 +1,393 @@
|
||||
# 全局 Sigma 社区高亮交互设计
|
||||
|
||||
日期:2026-06-26
|
||||
状态:已确认方向,待用户复核
|
||||
当前分支:`codex/global-sigma-community-spotlight-design`
|
||||
|
||||
> **实现勘误(plan-eng-review 2026-06-26)**:核实代码后两点收口,实现以 `workbench/docs/global-sigma-community-spotlight-plan.md` 为准。
|
||||
> 1. 本文“点社区后地图几乎没有反馈”部分不成立——现状点社区已驱动云团/标签/边 dim(`selection`→`community.selected`,sigma-global-renderer.ts:251/679/716/1034)。真正缺的只有:其他社区**节点** dim + 相机轻量动画。
|
||||
> 2. 下文“新增 `spotlightCommunityId` 独立状态”按 plan 收口为**复用现有 `selection`**(高亮社区与选中社区始终同步,无需平行字段);正文 `spotlightCommunityId` 字样一律读作“selection 的社区视觉态”。`focusCommunity()` 仍绝不复用。
|
||||
|
||||
## 目的
|
||||
|
||||
这份设计用于补齐 llm-wiki 全局图谱里“点击社区之后,图本身几乎没有反馈”的体验缺口。
|
||||
|
||||
当前全局图已经承担“知识库地图”的职责:用户可以看到社区分布、节点点位、少量地标标签和跨社区关系。点击社区后,右侧抽屉会显示社区简介;只有点击抽屉里的“进入社区”,才会进入 DOM/SVG 社区阅读视图。这个两步边界是正确的,应该保留。
|
||||
|
||||
但现在点击社区后,地图本身没有形成足够强的“我正在看这个社区”的状态:用户主要靠右侧抽屉理解选择结果,图谱区域更像静态背景。本设计要补的是全局图内部的临时高亮态。
|
||||
|
||||
一句话:
|
||||
|
||||
**点击社区后仍停留在全局地图,但地图要轻量高亮当前社区;真正进入社区仍由抽屉里的“进入社区”按钮负责。**
|
||||
|
||||
## 术语说明
|
||||
|
||||
本设计避免继续使用“社区聚焦”命名这个新状态。
|
||||
|
||||
在现有 PRODUCT / ADR 语境里,“社区聚焦视图”已经指向进入社区后的 DOM/SVG 局部阅读视图,也就是 `focusCommunity()` 会切过去的那条路径。本文要设计的是“仍停留在全局 Sigma 地图里的临时强调状态”,两者不是一回事。
|
||||
|
||||
因此,本文统一使用:
|
||||
|
||||
- **全局社区高亮态**:留在 Sigma 全局图里,当前社区被强调,其他社区弱化。
|
||||
- **社区阅读视图 / 社区聚焦视图**:进入 DOM/SVG 社区视图,主要用于阅读和深入探索。
|
||||
- **`spotlightCommunityId`**:后续实现中建议使用的临时状态名,避免继续复用 `focus` / `focusCommunity`。
|
||||
|
||||
## 设计结论
|
||||
|
||||
采用“全局内高亮”方案。
|
||||
|
||||
1. 点击社区后,不切到社区视图。
|
||||
2. 右侧抽屉继续显示社区简介。
|
||||
3. 全局 Sigma 图进入临时社区高亮态。
|
||||
4. 当前社区变清晰、变突出。
|
||||
5. 其他社区明显弱化但仍可见。
|
||||
6. 视角只做轻量构图调整,不做强镜头推进。
|
||||
7. 左上“回全图”按钮复用为退出高亮入口。
|
||||
8. 点空白处也可以退出高亮并清空当前选择。
|
||||
9. 抽屉里的“进入社区”按钮继续负责进入 DOM/SVG 社区阅读。
|
||||
|
||||
这个方案保留了“全局图是地图”的心智:用户是在地图里选中一片区域,而不是已经进入那片区域阅读。
|
||||
|
||||
## 为什么不直接进入社区
|
||||
|
||||
点击社区直接进入社区视图会让操作更快,但会牺牲全局地图的浏览能力。
|
||||
|
||||
全局图里的社区点击有两个不同目的:
|
||||
|
||||
- 看这个社区在全局里的位置。
|
||||
- 进入这个社区阅读细节。
|
||||
|
||||
这两个目的不应该合并成一个动作。当前产品已经用“点击社区打开简介抽屉,再点进入社区”的方式把它们分开,这是合理的。
|
||||
|
||||
因此,本设计不改变已有两步逻辑,只增强第一步的地图反馈。
|
||||
|
||||
## 为什么不强拉近镜头
|
||||
|
||||
最初可以想到一种动画:点击社区后,镜头明显拉近到该社区,其他社区隐藏,像进入一个局部世界。
|
||||
|
||||
这个方案不推荐,原因是它会和“尚未进入社区视图”的心智冲突。用户看到画面只剩一个社区时,很容易以为自己已经进入社区;这会削弱抽屉里“进入社区”按钮的意义。
|
||||
|
||||
更好的体验是“高亮构图”,不是“深入切换”:
|
||||
|
||||
- 视角可以轻微平移,让当前社区处在更舒服的位置。
|
||||
- 视角可以轻微放大,但不能放大到只剩当前社区。
|
||||
- 主要靠高亮、清晰度和弱化规则表达当前对象。
|
||||
- 其他社区仍然在场,维持全局空间感。
|
||||
|
||||
最终感觉应该是:
|
||||
|
||||
**我仍在看整张知识地图,只是当前社区被重点标出来了。**
|
||||
|
||||
## 现有代码边界
|
||||
|
||||
这次设计基于当前代码事实,不把全局和社区两条路线混在一起。
|
||||
|
||||
当前已有边界:
|
||||
|
||||
- `focusCommunity(id)` 会把路由切到 `dom-svg-community`,它代表“进入社区阅读”。
|
||||
- Sigma 全局路线里的社区点击现在走 `selectOnSigma({ kind: "community", id })`,代表“选中社区并打开摘要”。
|
||||
- Sigma 全局路线的适配数据目前显式传入 `focus: null`,所以全局路线不会消费 DOM 社区视图的 focus 状态。
|
||||
- Sigma 全局已经有社区云团覆盖层、社区标签覆盖层、节点命中热区、空白点击、相机状态读取和恢复。
|
||||
- Sigma 全局的“回全图”按钮现在只在本路线内清空 focus/selection;社区视图里的“回全图”通过全局路由回调切回 Sigma。
|
||||
|
||||
因此,后续实现不应复用 `focusCommunity(id)` 来做全局高亮。需要新增一个只属于全局 Sigma 的临时状态,例如 `spotlightCommunityId`。它和“进入社区阅读”的 focus 必须分开。
|
||||
|
||||
## 与既有 ADR 和阶段关系
|
||||
|
||||
本设计不推翻 ADR-21 / ADR-22 的社区点击修订,而是增强它们的第一步。
|
||||
|
||||
ADR-21 和 ADR-22 已经明确:全局图里点击社区不再直接进入社区聚焦视图,而是先显示社区摘要,再由明确按钮进入社区。本设计继续遵守这条边界,只是在“先显示社区摘要”的同时,让全局地图也给出足够清楚的视觉反馈。
|
||||
|
||||
阶段归属上,本设计应作为阶段 4.7 之后的独立图谱交互任务处理,建议归入后续阶段 4.8 或独立 issue。它依赖阶段 4.7 已完成的“地图心智”和 Sigma 覆盖层性能地基,但不属于 #70 标签兜底,也不应混入社区阅读视图改造。
|
||||
|
||||
进入实现前,需要同步 PRODUCT.md 的对应阶段记录和 ADR-21 / ADR-22 关系说明,避免“社区聚焦视图”和“全局社区高亮态”在产品文档里继续混用。
|
||||
|
||||
## 用户交互规则
|
||||
|
||||
### 1. 普通全局状态
|
||||
|
||||
用户打开图谱时,默认进入普通全局状态。
|
||||
|
||||
普通全局状态显示:
|
||||
|
||||
- 社区分布。
|
||||
- 节点点位。
|
||||
- 关系骨架。
|
||||
- 少量地标标签。
|
||||
- 搜索、筛选、Pin、选中等已有状态。
|
||||
|
||||
普通全局状态不显示:
|
||||
|
||||
- 全量标签。
|
||||
- 节点卡片。
|
||||
- 大段正文。
|
||||
- 社区内部阅读布局。
|
||||
|
||||
### 2. 点击社区
|
||||
|
||||
用户点击社区云团、社区区域或社区图例时:
|
||||
|
||||
- 保持在 Sigma 全局路线。
|
||||
- 选中该社区。
|
||||
- 右侧抽屉显示社区简介。
|
||||
- 图谱进入全局社区高亮态。
|
||||
- 当前社区视觉增强。
|
||||
- 其他社区明显弱化但仍可见。
|
||||
- 不自动进入社区视图。
|
||||
|
||||
如果用户再次点击另一个社区:
|
||||
|
||||
- 高亮态切换到新社区。
|
||||
- 抽屉切换到新社区简介。
|
||||
- 视角做一次轻量构图调整。
|
||||
|
||||
### 3. 进入社区
|
||||
|
||||
用户只有点击抽屉里的“进入社区”按钮,才进入 DOM/SVG 社区阅读视图。
|
||||
|
||||
进入社区视图后:
|
||||
|
||||
- 图谱切到社区阅读路线。
|
||||
- 社区内节点可以使用更高信息密度。
|
||||
- 社区视图继续承担阅读和探索任务。
|
||||
- 这不属于全局社区高亮态。
|
||||
|
||||
### 4. 回全图按钮复用
|
||||
|
||||
左上“回全图”按钮保留同一文案和同一入口,但按当前状态分层处理。
|
||||
|
||||
在社区视图中点击:
|
||||
|
||||
- 切回 Sigma 全局路线。
|
||||
|
||||
在 Sigma 全局高亮态中点击:
|
||||
|
||||
- 不切路线。
|
||||
- 清除全局社区高亮态。
|
||||
- 清空当前社区选择并关闭抽屉。
|
||||
- 视角回到完整全局构图。
|
||||
|
||||
在普通 Sigma 全局状态中点击:
|
||||
|
||||
- 重置全局视角。
|
||||
- 清空临时选择。
|
||||
- 保持筛选、Pin、搜索等明确用户状态。
|
||||
|
||||
这样用户只需要理解一个含义:“回到全局地图”。系统内部根据当前层级选择正确动作。
|
||||
|
||||
### 5. 点空白处
|
||||
|
||||
点空白处是“退出当前临时高亮”的快捷动作。
|
||||
|
||||
如果当前在全局社区高亮态:
|
||||
|
||||
- 退出高亮态。
|
||||
- 清空当前社区选择。
|
||||
- 抽屉同步关闭。
|
||||
- 视角回到普通全局构图。
|
||||
|
||||
如果当前只是普通全局选中节点或社区:
|
||||
|
||||
- 清空选择。
|
||||
- 关闭抽屉。
|
||||
|
||||
如果工具栏面板打开:
|
||||
|
||||
- 先关闭工具栏面板,再处理选择或高亮,保持现有空白点击优先级。
|
||||
|
||||
“回全图”和“点空白处”在全局社区高亮态下做相同清理动作,这是有意冗余:前者给用户一个可发现按钮,后者给熟练用户一个快速退出手势。两者都代表“离开当前社区摘要,回到普通全局地图”,所以都清空选择并关闭抽屉。
|
||||
|
||||
## 高亮态视觉规则
|
||||
|
||||
全局社区高亮态的核心不是“把当前社区单独放大”,而是“在全局地图上强调当前社区”。
|
||||
|
||||
当前社区:
|
||||
|
||||
- 社区云团不弱化。
|
||||
- 社区标签优先显示。
|
||||
- 社区内重要节点标签优先显示。
|
||||
- 社区内节点和内部关系保持正常或略增强。
|
||||
- 与当前社区相关的跨社区关系可以保留一部分。
|
||||
|
||||
其他社区:
|
||||
|
||||
- 社区云团降低不透明度。
|
||||
- 节点降低不透明度,但仍保留可辨认的点位。
|
||||
- 普通节点标签隐藏或明显弱化。
|
||||
- 普通关系线弱化。
|
||||
- 与当前社区存在强关系的邻近社区可以弱化得更轻。
|
||||
|
||||
这种“弱化但仍可见”的规则优于完全隐藏。完全隐藏会让用户误以为已经进入社区视图;弱化保留全局定位。
|
||||
|
||||
### 叠加优先级
|
||||
|
||||
全局社区高亮态必须和搜索、筛选、Pin、选中状态同时工作,不能互相覆盖。
|
||||
|
||||
优先级建议如下:
|
||||
|
||||
1. **筛选**决定对象是否属于当前可见集合。被筛掉的对象不因为高亮态强行出现;如果当前抽屉对象被筛掉,沿用既有“对象不在当前结果中”的提示和“显示该对象 / 清除选择”动作。
|
||||
2. **搜索命中**优先于社区弱化。即使搜索结果在其他社区,也不能被当前社区高亮态压到看不见;命中节点、命中社区标签和必要路径应保持可见。
|
||||
3. **当前选中对象**优先于普通弱化。当前抽屉对象必须始终可辨认。
|
||||
4. **Pin / 固定节点**优先于普通节点。被用户固定过的节点不能因为高亮态突然消失。
|
||||
5. **社区高亮态**只处理剩余普通对象的强弱关系。
|
||||
|
||||
典型例子:用户正在高亮社区 A,同时搜索命中社区 B 的节点。此时社区 A 仍是当前上下文,但社区 B 的搜索命中节点必须清晰可见,不能被当作普通非当前社区节点压暗。
|
||||
|
||||
## 动画规则
|
||||
|
||||
动画采用“轻量构图调整”,不采用强镜头推进。
|
||||
|
||||
点击社区后:
|
||||
|
||||
1. 抽屉立即打开,不等待动画结束。
|
||||
2. 当前社区马上进入高亮状态。
|
||||
3. 视角在短时间内轻微平移,让该社区落到更舒服的位置。
|
||||
4. 如果社区已经处于良好位置,不必移动视角。
|
||||
5. 如果社区在画面边缘,可以适度平移。
|
||||
6. 可以轻微放大,但放大比例必须受上限约束。
|
||||
7. 右侧抽屉打开时,构图中心应略偏左,避免当前社区被抽屉遮挡。
|
||||
|
||||
连续点击不同社区时:
|
||||
|
||||
- 新点击立即接管当前高亮状态。
|
||||
- 新构图动画立即打断旧动画。
|
||||
- 动画不排队、不叠加。
|
||||
- 如果旧动画已经接近终点,可以从当前相机状态平滑过渡到新目标。
|
||||
- 抽屉内容以最后一次点击为准。
|
||||
|
||||
动画时长建议:
|
||||
|
||||
- 普通情况:约 300-500ms。
|
||||
- 用户开启系统减少动态效果时:直接跳到终态或使用极短过渡。
|
||||
|
||||
动画目标:
|
||||
|
||||
- 让用户感觉“视线被带到当前社区”。
|
||||
- 不制造“已经进入社区”的错觉。
|
||||
- 不让全图位置关系发生重排。
|
||||
|
||||
明确不做:
|
||||
|
||||
- 不移动节点真实坐标。
|
||||
- 不重新布局整张图。
|
||||
- 不把当前社区节点物理拖到中心。
|
||||
- 不隐藏所有其他社区。
|
||||
- 不用强 zoom 取代高亮表达。
|
||||
|
||||
## 状态模型
|
||||
|
||||
后续实现应区分三类状态。
|
||||
|
||||
### 路由状态
|
||||
|
||||
决定用户在哪种视图:
|
||||
|
||||
- Sigma 全局。
|
||||
- DOM/SVG 社区阅读。
|
||||
- DOM 小图兜底。
|
||||
- 超限或不可用提示。
|
||||
|
||||
“进入社区”改变路由状态。
|
||||
|
||||
### 全局社区高亮状态
|
||||
|
||||
只在 Sigma 全局路线内存在。
|
||||
|
||||
它记录当前被全局高亮的社区 id,建议命名为 `spotlightCommunityId`,用来驱动:
|
||||
|
||||
- 当前社区高亮。
|
||||
- 其他社区弱化。
|
||||
- 社区标签优先级。
|
||||
- 轻量构图动画。
|
||||
- “回全图”在全局路线内的行为。
|
||||
|
||||
它不等于进入社区。
|
||||
|
||||
状态落点建议:
|
||||
|
||||
- 放在 Sigma 全局 route renderer / facade renderer 的局部状态里。
|
||||
- 不进入顶层 `GraphFacadeState.focus`。
|
||||
- 不复用 `focusCommunity()`。
|
||||
- 不写入知识库文件。
|
||||
- 路由离开 Sigma 全局时可以丢弃。
|
||||
- 底层 Sigma renderer 只消费这个视觉状态,不把它当成业务数据源。
|
||||
|
||||
### 选择状态
|
||||
|
||||
继续记录当前抽屉对象,例如选中某个社区或节点。
|
||||
|
||||
点击社区时,选择状态和全局社区高亮状态通常同步:
|
||||
|
||||
- 选择状态决定抽屉显示什么。
|
||||
- 全局社区高亮状态决定地图如何表达。
|
||||
|
||||
退出高亮时,同时清空选择并关闭抽屉。这样“回全图”和“点空白处”的结果一致,用户不会看到地图已经回全局、抽屉还停在旧社区的割裂状态。
|
||||
|
||||
## 性能边界
|
||||
|
||||
这次交互不能把每次点击变成一次昂贵的全图重建。
|
||||
|
||||
优先策略:
|
||||
|
||||
- 节点真实坐标不变。
|
||||
- 布局不重算。
|
||||
- 相机只做平移和轻微缩放。
|
||||
- 视觉弱化通过渲染状态或属性控制。
|
||||
- 社区云团和标签覆盖层继续复用现有结构。
|
||||
- 相机移动过程中只更新覆盖层位置,不重建覆盖层 DOM。
|
||||
|
||||
现有 Sigma 全局已经做过覆盖层拆分:结构更新和相机重定位分开。这个方向应继续保持。
|
||||
|
||||
需要避免:
|
||||
|
||||
- 点击社区就重新计算全图布局。
|
||||
- 每一帧重建社区云团 DOM。
|
||||
- 大量新增 hover DOM 元素。
|
||||
- 用全量标签显示解决高亮反馈问题。
|
||||
- 在全局社区高亮态中引入社区阅读级别的富内容。
|
||||
|
||||
## 后续实现边界
|
||||
|
||||
建议后续实现拆成一项独立任务,不和 #70 标签兜底混在一起。
|
||||
|
||||
#70 适合继续保持小修复:只给全局已显示标签加长度兜底,防止长标签破图。
|
||||
|
||||
全局社区高亮是新交互能力,应另开 issue 或设计任务实现。它至少会涉及:
|
||||
|
||||
- Sigma 全局路线的临时高亮状态。
|
||||
- 社区点击后的 `spotlightCommunityId` 写入。
|
||||
- 回全图按钮的分层行为。
|
||||
- 空白点击退出高亮。
|
||||
- 相机轻量构图动画。
|
||||
- 高亮态视觉弱化规则。
|
||||
- 搜索、筛选、Pin、抽屉选择的状态协调。
|
||||
- 浏览器交互验证。
|
||||
|
||||
## 验收标准
|
||||
|
||||
实现完成后,应至少验证以下路径。
|
||||
|
||||
1. 普通全局图点击社区:右侧抽屉打开,当前社区高亮,其他社区弱化但仍可见,不进入社区视图。
|
||||
2. 点击抽屉“进入社区”:进入 DOM/SVG 社区阅读视图。
|
||||
3. 社区视图点“回全图”:回到 Sigma 全局路线。
|
||||
4. 全局高亮态点“回全图”:停留在 Sigma,全图恢复普通构图。
|
||||
5. 全局高亮态点空白处:退出高亮,抽屉关闭,选择清空。
|
||||
6. 连续点击不同社区:抽屉和图谱高亮同步切换,动画不抖动。
|
||||
7. 高亮状态下搜索和筛选:不丢失明确用户状态,不出现空白图。
|
||||
8. 节点 Pin 和拖拽后的固定位置:不因高亮和回全图丢失。
|
||||
9. 右侧抽屉打开时:高亮社区不被抽屉遮挡。
|
||||
10. 减少动态效果设置开启时:动画降级为无动画或极短过渡。
|
||||
|
||||
## 不在本设计内
|
||||
|
||||
本设计不包含:
|
||||
|
||||
- 改写社区阅读视图。
|
||||
- 把社区视图迁移到 Sigma。
|
||||
- 全局 hover 完整方案。
|
||||
- 全局节点详情卡片重做。
|
||||
- 社区内部布局重排。
|
||||
- 大图聚合系统。
|
||||
- #70 标签长度兜底实现。
|
||||
|
||||
全局 hover 可以在本设计之后另开一份设计:它应基于这里定义的全局社区高亮状态继续补,而不是重新定义点击社区的意义。
|
||||
@@ -0,0 +1,219 @@
|
||||
# 全局 Sigma 图谱平滑缩放与左下缩放按钮设计
|
||||
|
||||
日期:2026-06-26
|
||||
分支:`codex/fix-global-graph-zoom-controls`
|
||||
关联:GitHub issue #73
|
||||
|
||||
## 背景
|
||||
|
||||
当前工作台图谱有两套视图:
|
||||
|
||||
- 全局视图使用 Sigma/Graphology。
|
||||
- 社区视图使用现有 DOM 图谱相机。
|
||||
|
||||
用户反馈集中在全局视图:滚轮和触控板缩放太快,轻轻一滑就突然变大或变小;继续缩小还可能让节点小到几乎消失。社区视图的缩放手感相对舒服,原因是它按真实滚轮距离连续计算缩放,并且有缩放边界。
|
||||
|
||||
这次 issue #73 提供的是重要线索,不作为唯一方案来源。结合当前代码后,推荐方案是:工作台全局 Sigma 图谱继续支持滚轮/触控板缩放,但不再使用 Sigma 默认的“按事件方向跳档”的滚轮手感;改为项目自己的连续缩放逻辑,并新增左下独立的放大/缩小按钮。
|
||||
|
||||
## 目标
|
||||
|
||||
1. 全局图谱滚轮/触控板缩放变得平顺、可控,接近社区视图现有手感。
|
||||
2. 全局图谱不再因为轻微滑动突然变很大或很小。
|
||||
3. 全局图谱缩放有合理上下限,避免无限放大或无限缩小。
|
||||
4. 全局图谱左下增加独立的 `+` / `-` 缩放按钮。
|
||||
5. 滚轮、触控板、按钮使用同一套缩放规则,不出现三种不同手感。
|
||||
|
||||
## 不做
|
||||
|
||||
1. 不改离线 HTML 的完整体验;本次只保证共享代码改动不主动破坏离线入口。
|
||||
2. 不改社区视图的缩放体验。
|
||||
3. 不改图谱数据、选择、搜索、筛选、Pin、社区高亮、右抽屉等功能语义。
|
||||
4. 不引入新的图谱库或新的 npm 依赖。
|
||||
5. 不修改 `node_modules/`。
|
||||
|
||||
## 用户体验定义
|
||||
|
||||
### 1. 滚轮和触控板仍然可以缩放
|
||||
|
||||
全局图谱保留滚轮/触控板缩放。改变的是缩放计算方式,不是取消滚轮缩放。
|
||||
|
||||
### 2. 缩放必须连续
|
||||
|
||||
触控板轻滑一点,只缩放一点;滑动距离更大,缩放幅度才更大。
|
||||
|
||||
当前 Sigma 默认滚轮逻辑主要看滚轮方向,一次事件就是固定倍率跳档。这个模型会让高频小 delta 的触控板变成连续猛跳。新设计要改为按真实 `deltaY` 计算缩放倍率。
|
||||
|
||||
参考社区视图现有逻辑:
|
||||
|
||||
- 先把 wheel delta 标准化为像素距离。
|
||||
- 再用指数曲线把 delta 映射成缩放倍率。
|
||||
- 单次缩放倍率做防甩动限制。
|
||||
|
||||
全局 Sigma 视图采用同一类规则:
|
||||
|
||||
```text
|
||||
nextRatio = currentRatio * exp(normalizedDeltaY * WHEEL_ZOOM_SPEED)
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 社区视图的 `scale` 越大表示越放大。
|
||||
- Sigma 的 `ratio` 越小表示越放大。
|
||||
- 所以全局 Sigma 用 `exp(+deltaY * speed)`,与社区视图的 `scale * exp(-deltaY * speed)` 在手感方向上对应。
|
||||
- `WHEEL_ZOOM_SPEED` 使用社区视图当前舒服的值:`0.0016`。只有浏览器实测证明全局 Sigma 与社区视图仍有明显偏差时,才允许带测试记录调整这个常量;不能回到按事件跳档。
|
||||
|
||||
### 3. 缩放必须稳定
|
||||
|
||||
滚轮/触控板缩放时,以鼠标所在点为缩放锚点。用户指着哪里缩放,哪里就尽量留在原地。
|
||||
|
||||
按钮缩放时,以当前画面中心为缩放锚点。点击 `+` 或 `-` 不应该让画面突然飞到别处。
|
||||
|
||||
### 4. 缩放必须有边界
|
||||
|
||||
全局 Sigma 相机要设置 `minCameraRatio` 和 `maxCameraRatio`,并且项目自己的缩放逻辑也要遵守同一边界。
|
||||
|
||||
边界目标:
|
||||
|
||||
- 最大放大:能看清局部节点,但不把节点放到失控巨大。
|
||||
- 最大缩小:能退回全局关系,但不让节点小到消失。
|
||||
|
||||
实现初始值:
|
||||
|
||||
- 放大下限:`minCameraRatio = 0.3`。
|
||||
- 缩小上限:`maxCameraRatio = 3`。
|
||||
|
||||
如果视觉验证发现这两个值仍然让节点过大或过小,必须用新的浏览器验证结果说明为什么调整,并同步更新测试预期。无验证记录时,不继续调参。
|
||||
|
||||
### 5. 滚轮缩放不能积压动画
|
||||
|
||||
滚轮/触控板缩放应该即时响应,不排队播放动画。用户停止滚动后,画面也应很快停下,不能继续追一串旧动画。
|
||||
|
||||
因此:
|
||||
|
||||
- 滚轮/触控板缩放直接更新相机状态。
|
||||
- 按钮缩放可以有短动画,但下一次点击必须接管上一次动画目标。
|
||||
|
||||
### 6. 按钮步长要明确但不猛
|
||||
|
||||
左下缩放按钮是地图导航控件,不是精细触控板替代品。
|
||||
|
||||
按钮点击一次使用中等固定步长:
|
||||
|
||||
- `+`:放大一档,`ratio` 乘以 `1 / 1.18`。
|
||||
- `-`:缩小一档,`ratio` 乘以 `1.18`。
|
||||
|
||||
这个步长比触控板轻滑更明确,但不能像当前全局滚轮那样猛跳。
|
||||
|
||||
## 交互布局
|
||||
|
||||
新增一个独立的左下缩放控件:
|
||||
|
||||
- 位置:全局图谱画布左下角。
|
||||
- 内容:竖向排列 `+` 和 `-`。
|
||||
- 样式:沿用现有图谱工具条的半透明纸面风格,但作为单独控件存在。
|
||||
- 与现有工具条关系:不放进顶部“筛选 / 图例 / 回全图”工具条。
|
||||
- 与“回全图”关系:`+` / `-` 只缩放;“回全图”继续负责回到全局构图。
|
||||
|
||||
控件区域本身要阻止画布滚轮缩放和点击穿透,避免用户在按钮附近滚动时误操作图谱。
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 1. 缩放控制归属
|
||||
|
||||
缩放控制放在共享图谱引擎的 Sigma 全局渲染路径中,而不是放在工作台 React 外壳里。
|
||||
|
||||
理由:
|
||||
|
||||
- 当前全局图谱相机由 `packages/graph-engine/src/render/sigma-global-renderer.ts` 管理。
|
||||
- Sigma 的鼠标事件、相机状态、锚点缩放都在这一层更容易正确处理。
|
||||
- 工作台外壳不应该理解 Sigma 相机细节。
|
||||
|
||||
### 2. Sigma 默认滚轮处理
|
||||
|
||||
Sigma 的 mouse captor 会先发出 `wheel` 事件,再执行默认缩放。项目应监听这个 `wheel` 事件并调用 `preventSigmaDefault()`,阻止 Sigma 默认跳档缩放,然后执行项目自己的连续缩放。
|
||||
|
||||
需要使用的 Sigma 能力:
|
||||
|
||||
- `getMouseCaptor().on("wheel", handler)`:接管滚轮事件。
|
||||
- wheel payload 的 `original.deltaY` / `original.deltaMode`:读取真实滚轮输入。
|
||||
- `getCamera().getState()`:读取当前相机。
|
||||
- `getViewportZoomedState(pointer, nextRatio)`:按鼠标位置计算新相机状态。
|
||||
- `getCamera().setState(nextState)`:即时更新滚轮缩放。
|
||||
|
||||
如果某个运行环境缺少上述能力,必须降级为保守的 Sigma 参数方案,而不是让全局缩放不可用。
|
||||
|
||||
### 3. 缩放按钮
|
||||
|
||||
`createGraphToolbar` 不承载缩放按钮。新增一个独立的 Sigma 缩放控件创建函数,挂载到 Sigma 全局 route shell 内。
|
||||
|
||||
控件通过 renderer 暴露的缩放方法驱动相机,例如:
|
||||
|
||||
- `zoomIn()`
|
||||
- `zoomOut()`
|
||||
|
||||
按钮缩放使用当前 Sigma 容器中心点作为锚点,计算方式与滚轮一致,只是输入为固定倍率。
|
||||
|
||||
### 4. 与现有高亮相机动画的关系
|
||||
|
||||
当前 `main` 已经有全局社区高亮与轻微相机构图动画。缩放改动必须与它共存:
|
||||
|
||||
- 选中社区后仍允许现有轻微构图。
|
||||
- 用户滚轮/按钮缩放后,以用户缩放后的相机为准。
|
||||
- 滚轮缩放不触发社区选择或清空选择。
|
||||
- “回全图”仍然可以回到全局构图。
|
||||
|
||||
## 错误与降级
|
||||
|
||||
1. 如果无法读取真实 wheel delta,使用 Sigma wheel payload 的 delta 做保守计算,但仍要避免默认 `1.7` 跳档。
|
||||
2. 如果无法使用 `getViewportZoomedState`,按钮和滚轮都不应让画面飞走;可以退化为以当前相机中心缩放。
|
||||
3. 如果 Sigma runtime 不支持 mouse captor,则至少设置较温和的 `zoomingRatio` 与相机上下限,保证不出现无限缩放。
|
||||
4. 所有异常通过现有 `onFatalError` 路径上报,不让整张图谱黑屏。
|
||||
|
||||
## 测试计划
|
||||
|
||||
### 单元测试
|
||||
|
||||
1. `sigma-global-renderer.test.ts`
|
||||
- 创建 Sigma 时包含合理的相机边界。
|
||||
- wheel handler 会阻止 Sigma 默认缩放。
|
||||
- 小 `deltaY` 只产生小幅缩放,大 `deltaY` 产生更明显缩放。
|
||||
- 连续 wheel 不排队动画,使用即时相机更新。
|
||||
- wheel 缩放使用鼠标位置作为锚点。
|
||||
- `zoomIn` / `zoomOut` 使用同一套缩放计算与相机边界。
|
||||
|
||||
2. `renderer-boundary.test.ts`
|
||||
- 左下缩放控件存在。
|
||||
- `+` / `-` 点击分别触发放大/缩小回调。
|
||||
- 控件点击不穿透到图谱选择。
|
||||
|
||||
3. `gestures.test.ts` 或等价覆盖
|
||||
- 滚轮经过缩放控件时不触发图谱缩放。
|
||||
|
||||
### 浏览器验证
|
||||
|
||||
1. 启动工作台,进入全局图谱。
|
||||
2. 使用触控板轻滑:图谱只轻微缩放,不猛跳。
|
||||
3. 连续滚动后松手:画面快速停止,不继续追动画。
|
||||
4. 鼠标停在节点附近滚轮缩放:该区域保持稳定,不飞跑。
|
||||
5. 多次缩小:节点仍可见,不能缩到消失。
|
||||
6. 多次放大:节点不会失控巨大。
|
||||
7. 点击左下 `+` / `-`:画面按中等步长缩放,且以画面中心为基准。
|
||||
8. 选中社区后再缩放:高亮仍在,缩放正常。
|
||||
9. 回全图仍按原语义工作。
|
||||
|
||||
### 回归检查
|
||||
|
||||
- `npm run test -w @llm-wiki/graph-engine`
|
||||
- 受影响情况下运行 Sigma 全局浏览器验证脚本。
|
||||
- 若改动 CSS 或控件布局,打开工作台实际查看浅色/深色下左下按钮是否清晰、不遮挡主要图谱内容。
|
||||
|
||||
## 完成标准
|
||||
|
||||
这个任务实现完成时,必须同时满足:
|
||||
|
||||
1. 工作台全局图谱滚轮/触控板缩放手感接近社区视图,不再轻滑猛跳。
|
||||
2. 工作台全局图谱缩放有上下限,不会无限缩小或无限放大。
|
||||
3. 工作台全局图谱左下有独立 `+` / `-` 缩放按钮。
|
||||
4. 按钮和滚轮使用同一套缩放规则。
|
||||
5. 现有全局社区高亮、搜索、筛选、Pin、回全图没有被破坏。
|
||||
6. 单元测试和浏览器验证通过。
|
||||
@@ -0,0 +1,746 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>大图谱交互方案:全局降级,社区聚焦升级</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 32px;
|
||||
background: #eef2f7;
|
||||
color: #0f172a;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 24px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.subtitle {
|
||||
margin: 0 0 22px;
|
||||
color: #475569;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.flow-board {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
.flow-note {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(148, 163, 184, .24);
|
||||
border-radius: 10px;
|
||||
background: rgba(15, 23, 42, .04);
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
}
|
||||
.state-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.05fr .95fr;
|
||||
gap: 16px;
|
||||
}
|
||||
.screen {
|
||||
border: 1px solid rgba(15, 23, 42, .14);
|
||||
border-radius: 14px;
|
||||
background: #f8fafc;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 18px 40px rgba(15, 23, 42, .08);
|
||||
}
|
||||
.screen-bar {
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid rgba(15, 23, 42, .1);
|
||||
background: #ffffff;
|
||||
font-size: 12px;
|
||||
color: #334155;
|
||||
font-weight: 650;
|
||||
}
|
||||
.mini-controls {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
.app-frame {
|
||||
display: grid;
|
||||
grid-template-columns: 44px 142px minmax(320px, 1fr) 228px;
|
||||
min-height: 360px;
|
||||
}
|
||||
.rail {
|
||||
background: #fff;
|
||||
border-right: 1px solid rgba(15, 23, 42, .08);
|
||||
padding: 12px 8px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
align-content: start;
|
||||
}
|
||||
.rail-dot {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 6px;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
.rail-dot.active {
|
||||
background: #111827;
|
||||
}
|
||||
.library {
|
||||
background: #fff;
|
||||
border-right: 1px solid rgba(15, 23, 42, .08);
|
||||
padding: 12px 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.lib-title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.lib-row {
|
||||
height: 18px;
|
||||
margin: 8px 0;
|
||||
border-radius: 4px;
|
||||
background: #e5e7eb;
|
||||
}
|
||||
.lib-row.short { width: 68%; }
|
||||
.lib-row.mid { width: 84%; }
|
||||
.graph-stage {
|
||||
position: relative;
|
||||
background: #f8fafc;
|
||||
overflow: hidden;
|
||||
}
|
||||
.graph-toolbar {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 10px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
.chip {
|
||||
padding: 5px 8px;
|
||||
border: 1px solid rgba(15, 23, 42, .1);
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,.84);
|
||||
font-size: 10px;
|
||||
color: #334155;
|
||||
font-weight: 650;
|
||||
}
|
||||
.chip.active {
|
||||
background: #111827;
|
||||
color: white;
|
||||
}
|
||||
.legend {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
bottom: 12px;
|
||||
z-index: 2;
|
||||
width: 150px;
|
||||
padding: 8px;
|
||||
border: 1px solid rgba(15, 23, 42, .12);
|
||||
border-radius: 10px;
|
||||
background: rgba(255,255,255,.9);
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, .08);
|
||||
}
|
||||
.legend-title {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
margin-bottom: 5px;
|
||||
color: #334155;
|
||||
}
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 10px;
|
||||
color: #475569;
|
||||
padding: 3px 0;
|
||||
}
|
||||
.swatch {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 999px;
|
||||
flex: none;
|
||||
}
|
||||
.zoom-stack {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 12px;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
.zoom-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 1px solid rgba(15, 23, 42, .12);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
.field {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
.cluster-halo {
|
||||
position: absolute;
|
||||
border-radius: 999px;
|
||||
opacity: .18;
|
||||
filter: blur(.2px);
|
||||
}
|
||||
.node {
|
||||
position: absolute;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 999px;
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 0 0 1px rgba(255,255,255,.72);
|
||||
}
|
||||
.node.key {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
box-shadow: 0 0 0 3px rgba(255,255,255,.82), 0 0 22px rgba(37,99,235,.18);
|
||||
}
|
||||
.node.selected {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
box-shadow: 0 0 0 4px rgba(14,165,233,.18), 0 0 0 1px #0369a1;
|
||||
}
|
||||
.edge {
|
||||
position: absolute;
|
||||
height: 1px;
|
||||
background: rgba(100,116,139,.22);
|
||||
transform-origin: left center;
|
||||
}
|
||||
.edge.dim {
|
||||
opacity: .28;
|
||||
}
|
||||
.node-label {
|
||||
position: absolute;
|
||||
transform: translate(8px, -9px);
|
||||
font-size: 9px;
|
||||
font-weight: 750;
|
||||
color: #1e293b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.drawer {
|
||||
background: #fff;
|
||||
border-left: 1px solid rgba(15, 23, 42, .08);
|
||||
padding: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.drawer-kicker {
|
||||
font-size: 9px;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
color: #64748b;
|
||||
font-weight: 800;
|
||||
}
|
||||
.drawer-title {
|
||||
margin-top: 6px;
|
||||
font-size: 15px;
|
||||
line-height: 1.25;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
}
|
||||
.drawer-meta {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
.pill {
|
||||
font-size: 9px;
|
||||
padding: 4px 6px;
|
||||
border-radius: 999px;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
font-weight: 650;
|
||||
}
|
||||
.drawer-section {
|
||||
margin-top: 14px;
|
||||
}
|
||||
.drawer-section h4 {
|
||||
margin: 0 0 7px;
|
||||
font-size: 10px;
|
||||
color: #334155;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .06em;
|
||||
}
|
||||
.relation-list {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
.relation {
|
||||
border: 1px solid rgba(15, 23, 42, .08);
|
||||
border-radius: 9px;
|
||||
padding: 7px;
|
||||
background: #f8fafc;
|
||||
font-size: 10px;
|
||||
color: #475569;
|
||||
}
|
||||
.relation strong {
|
||||
display: block;
|
||||
color: #0f172a;
|
||||
font-size: 11px;
|
||||
}
|
||||
.drawer-actions {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.primary-action,
|
||||
.secondary-action {
|
||||
border-radius: 9px;
|
||||
padding: 8px 9px;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
font-weight: 800;
|
||||
}
|
||||
.primary-action {
|
||||
background: #111827;
|
||||
color: #fff;
|
||||
}
|
||||
.secondary-action {
|
||||
background: #f1f5f9;
|
||||
color: #334155;
|
||||
}
|
||||
.focus-frame {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(360px, 1fr) 260px;
|
||||
min-height: 360px;
|
||||
}
|
||||
.focus-stage {
|
||||
position: relative;
|
||||
background: #fbfdff;
|
||||
overflow: hidden;
|
||||
}
|
||||
.focus-header {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
top: 12px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.back-chip {
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(15,23,42,.1);
|
||||
font-size: 10px;
|
||||
color: #334155;
|
||||
font-weight: 750;
|
||||
}
|
||||
.community-title {
|
||||
padding: 6px 9px;
|
||||
border-radius: 999px;
|
||||
background: rgba(59,130,246,.1);
|
||||
color: #1d4ed8;
|
||||
font-size: 10px;
|
||||
font-weight: 850;
|
||||
}
|
||||
.card-node {
|
||||
position: absolute;
|
||||
width: 118px;
|
||||
min-height: 48px;
|
||||
border: 1px solid rgba(15, 23, 42, .1);
|
||||
border-radius: 10px;
|
||||
background: rgba(255,255,255,.94);
|
||||
box-shadow: 0 10px 24px rgba(15,23,42,.08);
|
||||
padding: 8px;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.card-node h5 {
|
||||
margin: 0 0 5px;
|
||||
font-size: 10px;
|
||||
color: #0f172a;
|
||||
}
|
||||
.card-node p {
|
||||
margin: 0;
|
||||
font-size: 8.5px;
|
||||
line-height: 1.35;
|
||||
color: #64748b;
|
||||
}
|
||||
.small-node {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 999px;
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 0 0 3px #fff;
|
||||
}
|
||||
.read-drawer {
|
||||
border-left: 1px solid rgba(15,23,42,.08);
|
||||
background: #fff;
|
||||
padding: 14px;
|
||||
}
|
||||
.read-block {
|
||||
border-bottom: 1px solid rgba(15,23,42,.08);
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.read-block h4 {
|
||||
margin: 0 0 7px;
|
||||
font-size: 13px;
|
||||
color: #0f172a;
|
||||
}
|
||||
.read-line {
|
||||
height: 8px;
|
||||
border-radius: 99px;
|
||||
background: #e2e8f0;
|
||||
margin: 7px 0;
|
||||
}
|
||||
.read-line.w70 { width: 70%; }
|
||||
.read-line.w85 { width: 85%; }
|
||||
.read-line.w55 { width: 55%; }
|
||||
.rule-row {
|
||||
display: grid;
|
||||
grid-template-columns: 110px 1fr;
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
padding: 9px 0;
|
||||
border-top: 1px solid rgba(15,23,42,.08);
|
||||
font-size: 12px;
|
||||
}
|
||||
.rule-row strong {
|
||||
color: #0f172a;
|
||||
}
|
||||
.rule-row span {
|
||||
color: #475569;
|
||||
}
|
||||
.path-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.path-pill {
|
||||
border-radius: 999px;
|
||||
background: #f1f5f9;
|
||||
padding: 5px 8px;
|
||||
font-weight: 750;
|
||||
}
|
||||
.arrow {
|
||||
color: #94a3b8;
|
||||
font-weight: 900;
|
||||
}
|
||||
@media (max-width: 980px) {
|
||||
body { padding: 18px; }
|
||||
.state-grid { grid-template-columns: 1fr; }
|
||||
.app-frame { grid-template-columns: 38px 118px minmax(260px, 1fr); }
|
||||
.drawer { grid-column: 1 / -1; border-left: 0; border-top: 1px solid rgba(15,23,42,.08); }
|
||||
.focus-frame { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>大图谱交互方案:全局降级,社区聚焦升级</h2>
|
||||
<p class="subtitle">这不是最终视觉皮肤,是用来讨论交互边界的画板:全局只看地图,点节点看节点摘要,点社区看社区摘要,再通过按钮进入阅读。</p>
|
||||
|
||||
<div class="flow-board">
|
||||
<div class="flow-note">
|
||||
<strong>主线:</strong>
|
||||
<span>普通节点点击不跳视角;社区色块/图例点击只切换抽屉内容。这样万级全局图谱不被卡片和长文本拖慢。</span>
|
||||
</div>
|
||||
|
||||
<div class="screen">
|
||||
<div class="screen-bar">
|
||||
<span>状态 A · 全局大图:点、颜色、少量关键标签</span>
|
||||
<div class="mini-controls"><span>筛选</span><span>社区</span><span>搜索</span></div>
|
||||
</div>
|
||||
<div class="app-frame">
|
||||
<div class="rail">
|
||||
<div class="rail-dot active"></div>
|
||||
<div class="rail-dot"></div>
|
||||
<div class="rail-dot"></div>
|
||||
<div class="rail-dot"></div>
|
||||
</div>
|
||||
<div class="library">
|
||||
<div class="lib-title">Knowledge</div>
|
||||
<div class="lib-row mid"></div>
|
||||
<div class="lib-row short"></div>
|
||||
<div class="lib-row"></div>
|
||||
<div class="lib-row mid"></div>
|
||||
<div class="lib-row short"></div>
|
||||
<div class="lib-row"></div>
|
||||
<div class="lib-row mid"></div>
|
||||
<div class="lib-row short"></div>
|
||||
</div>
|
||||
<div class="graph-stage">
|
||||
<div class="graph-toolbar">
|
||||
<div class="chip active">全局</div>
|
||||
<div class="chip">类型筛选</div>
|
||||
<div class="chip">社区 18</div>
|
||||
<div class="chip">显示关键标签</div>
|
||||
</div>
|
||||
<div class="zoom-stack">
|
||||
<div class="zoom-btn">+</div>
|
||||
<div class="zoom-btn">-</div>
|
||||
<div class="zoom-btn">⌂</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="cluster-halo" style="left:11%;top:18%;width:190px;height:130px;background:#60a5fa;"></div>
|
||||
<div class="cluster-halo" style="left:46%;top:13%;width:180px;height:150px;background:#34d399;"></div>
|
||||
<div class="cluster-halo" style="left:29%;top:54%;width:210px;height:140px;background:#f59e0b;"></div>
|
||||
<div class="cluster-halo" style="left:63%;top:56%;width:170px;height:126px;background:#a78bfa;"></div>
|
||||
|
||||
<div class="edge" style="left:26%;top:35%;width:150px;transform:rotate(13deg);"></div>
|
||||
<div class="edge" style="left:52%;top:39%;width:132px;transform:rotate(28deg);"></div>
|
||||
<div class="edge dim" style="left:42%;top:63%;width:170px;transform:rotate(-20deg);"></div>
|
||||
<div class="edge dim" style="left:19%;top:60%;width:120px;transform:rotate(-46deg);"></div>
|
||||
|
||||
<div class="node key" style="left:24%;top:34%;background:#3b82f6;"></div>
|
||||
<div class="node-label" style="left:24%;top:34%;">项目总览</div>
|
||||
<div class="node key selected" style="left:49%;top:42%;background:#10b981;"></div>
|
||||
<div class="node-label" style="left:49%;top:42%;">数据管线</div>
|
||||
<div class="node key" style="left:66%;top:65%;background:#8b5cf6;"></div>
|
||||
<div class="node-label" style="left:66%;top:65%;">评估系统</div>
|
||||
<div class="node key" style="left:35%;top:69%;background:#f59e0b;"></div>
|
||||
<div class="node-label" style="left:35%;top:69%;">素材入口</div>
|
||||
|
||||
<div class="node" style="left:16%;top:25%;background:#60a5fa;"></div>
|
||||
<div class="node" style="left:19%;top:39%;background:#60a5fa;"></div>
|
||||
<div class="node" style="left:29%;top:22%;background:#60a5fa;"></div>
|
||||
<div class="node" style="left:34%;top:31%;background:#60a5fa;"></div>
|
||||
<div class="node" style="left:45%;top:24%;background:#34d399;"></div>
|
||||
<div class="node" style="left:56%;top:27%;background:#34d399;"></div>
|
||||
<div class="node" style="left:61%;top:36%;background:#34d399;"></div>
|
||||
<div class="node" style="left:48%;top:54%;background:#34d399;"></div>
|
||||
<div class="node" style="left:24%;top:62%;background:#f59e0b;"></div>
|
||||
<div class="node" style="left:29%;top:75%;background:#f59e0b;"></div>
|
||||
<div class="node" style="left:42%;top:74%;background:#f59e0b;"></div>
|
||||
<div class="node" style="left:54%;top:70%;background:#f59e0b;"></div>
|
||||
<div class="node" style="left:72%;top:57%;background:#8b5cf6;"></div>
|
||||
<div class="node" style="left:76%;top:70%;background:#8b5cf6;"></div>
|
||||
<div class="node" style="left:61%;top:78%;background:#8b5cf6;"></div>
|
||||
<div class="node" style="left:70%;top:31%;background:#ef4444;"></div>
|
||||
<div class="node" style="left:79%;top:42%;background:#ef4444;"></div>
|
||||
<div class="node" style="left:82%;top:25%;background:#ef4444;"></div>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<div class="legend-title">社区入口</div>
|
||||
<div class="legend-item"><span class="swatch" style="background:#3b82f6"></span><span>基础知识 820</span></div>
|
||||
<div class="legend-item"><span class="swatch" style="background:#10b981"></span><span>数据管线 1260</span></div>
|
||||
<div class="legend-item"><span class="swatch" style="background:#f59e0b"></span><span>素材入口 940</span></div>
|
||||
<div class="legend-item"><span class="swatch" style="background:#8b5cf6"></span><span>评估系统 610</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drawer">
|
||||
<div class="drawer-kicker">轻量查看 · 不改变全局视角</div>
|
||||
<div class="drawer-title">数据管线</div>
|
||||
<div class="drawer-meta">
|
||||
<span class="pill">概念</span>
|
||||
<span class="pill">社区:数据管线</span>
|
||||
<span class="pill">连接 48</span>
|
||||
</div>
|
||||
<div class="drawer-section">
|
||||
<h4>最强关系</h4>
|
||||
<div class="relation-list">
|
||||
<div class="relation"><strong>素材入口</strong>输入清洗、来源追踪</div>
|
||||
<div class="relation"><strong>评估系统</strong>质量检查、结果反馈</div>
|
||||
<div class="relation"><strong>项目总览</strong>主流程依赖</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drawer-section">
|
||||
<h4>相邻节点</h4>
|
||||
<div class="drawer-meta">
|
||||
<span class="pill">chunking</span>
|
||||
<span class="pill">metadata</span>
|
||||
<span class="pill">dedupe</span>
|
||||
<span class="pill">cache</span>
|
||||
<span class="pill">source map</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drawer-actions">
|
||||
<div class="primary-action">进入所属社区</div>
|
||||
<div class="secondary-action">打开详情 / 阅读</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="path-line">
|
||||
<span class="path-pill">点节点</span><span class="arrow">→</span><span>节点摘要</span>
|
||||
<span class="arrow">/</span>
|
||||
<span class="path-pill">点社区色块</span><span class="arrow">→</span><span>社区摘要</span>
|
||||
<span class="arrow">/</span>
|
||||
<span class="path-pill">点进入社区</span><span class="arrow">→</span><span>社区聚焦</span>
|
||||
<span class="arrow">/</span>
|
||||
<span class="path-pill">点回全图</span><span class="arrow">→</span><span>返回全局</span>
|
||||
</div>
|
||||
|
||||
<div class="flow-note">
|
||||
<strong>返回规则:</strong>
|
||||
<span>单击空白只取消选中;回全图用可见按钮,双击真正空白背景只是快捷方式。节点、社区、边、图例、搜索和抽屉都不触发双击回全图。</span>
|
||||
</div>
|
||||
|
||||
<div class="state-grid">
|
||||
<div class="screen">
|
||||
<div class="screen-bar">
|
||||
<span>抽屉状态 A1 · 选中社区</span>
|
||||
<div class="mini-controls"><span>全局不跳转</span></div>
|
||||
</div>
|
||||
<div style="padding:14px 16px;background:#fff;min-height:210px;">
|
||||
<div class="drawer-kicker">社区摘要 · 仍在全局视角</div>
|
||||
<div class="drawer-title">数据管线社区</div>
|
||||
<div class="drawer-meta">
|
||||
<span class="pill">1260 节点</span>
|
||||
<span class="pill">核心 80</span>
|
||||
<span class="pill">桥接 12</span>
|
||||
</div>
|
||||
<div class="drawer-section">
|
||||
<h4>核心节点</h4>
|
||||
<div class="relation-list">
|
||||
<div class="relation"><strong>数据管线</strong>社区主轴</div>
|
||||
<div class="relation"><strong>清洗规则</strong>来源、重复、格式</div>
|
||||
<div class="relation"><strong>缓存策略</strong>复用与追踪</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drawer-actions">
|
||||
<div class="primary-action">进入社区</div>
|
||||
<div class="secondary-action">查看核心节点列表</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="screen">
|
||||
<div class="screen-bar">
|
||||
<span>抽屉状态 A2 · 选中节点</span>
|
||||
<div class="mini-controls"><span>节点优先</span></div>
|
||||
</div>
|
||||
<div style="padding:14px 16px;background:#fff;min-height:210px;">
|
||||
<div class="drawer-kicker">节点摘要 · 社区作为上下文</div>
|
||||
<div class="drawer-title">数据管线</div>
|
||||
<div class="drawer-meta">
|
||||
<span class="pill">概念</span>
|
||||
<span class="pill">社区:数据管线</span>
|
||||
<span class="pill">连接 48</span>
|
||||
</div>
|
||||
<div class="drawer-section">
|
||||
<h4>最强关系</h4>
|
||||
<div class="relation-list">
|
||||
<div class="relation"><strong>素材入口</strong>输入清洗、来源追踪</div>
|
||||
<div class="relation"><strong>评估系统</strong>质量检查、结果反馈</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drawer-actions">
|
||||
<div class="primary-action">进入所属社区</div>
|
||||
<div class="secondary-action">打开详情 / 阅读</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="state-grid">
|
||||
<div class="screen">
|
||||
<div class="screen-bar">
|
||||
<span>状态 B · 社区聚焦:恢复卡片和可读关系</span>
|
||||
<div class="mini-controls"><span>返回全局</span><span>筛选</span></div>
|
||||
</div>
|
||||
<div class="focus-frame">
|
||||
<div class="focus-stage">
|
||||
<div class="focus-header">
|
||||
<div class="back-chip">← 全局</div>
|
||||
<div class="community-title">数据管线社区 · 1260 节点中的核心 80</div>
|
||||
</div>
|
||||
<div class="edge" style="left:26%;top:44%;width:190px;transform:rotate(-4deg);background:rgba(59,130,246,.32);"></div>
|
||||
<div class="edge" style="left:36%;top:56%;width:168px;transform:rotate(22deg);background:rgba(59,130,246,.28);"></div>
|
||||
<div class="edge" style="left:49%;top:39%;width:130px;transform:rotate(48deg);background:rgba(59,130,246,.22);"></div>
|
||||
<div class="card-node" style="left:31%;top:42%;">
|
||||
<h5>数据管线</h5>
|
||||
<p>社区核心节点,连接素材、缓存、评估。</p>
|
||||
</div>
|
||||
<div class="card-node" style="left:62%;top:40%;">
|
||||
<h5>清洗规则</h5>
|
||||
<p>处理标题、来源、重复内容。</p>
|
||||
</div>
|
||||
<div class="card-node" style="left:44%;top:68%;">
|
||||
<h5>缓存策略</h5>
|
||||
<p>减少重复分析,保留可追踪结果。</p>
|
||||
</div>
|
||||
<div class="small-node" style="left:22%;top:66%;background:#10b981;"></div>
|
||||
<div class="small-node" style="left:76%;top:61%;background:#10b981;"></div>
|
||||
<div class="small-node" style="left:54%;top:23%;background:#10b981;"></div>
|
||||
<div class="small-node" style="left:68%;top:73%;background:#10b981;"></div>
|
||||
</div>
|
||||
<div class="read-drawer">
|
||||
<div class="drawer-kicker">社区阅读抽屉</div>
|
||||
<div class="drawer-title">数据管线</div>
|
||||
<div class="drawer-section">
|
||||
<h4>社区摘要</h4>
|
||||
<div class="read-line w85"></div>
|
||||
<div class="read-line w70"></div>
|
||||
<div class="read-line w55"></div>
|
||||
</div>
|
||||
<div class="read-block">
|
||||
<h4>核心节点</h4>
|
||||
<div class="relation-list">
|
||||
<div class="relation"><strong>数据管线</strong>48 条连接</div>
|
||||
<div class="relation"><strong>清洗规则</strong>31 条连接</div>
|
||||
<div class="relation"><strong>缓存策略</strong>26 条连接</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drawer-actions">
|
||||
<div class="primary-action">查看核心关系</div>
|
||||
<div class="secondary-action">只看一跳邻居</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="screen">
|
||||
<div class="screen-bar">
|
||||
<span>大规模降级规则</span>
|
||||
<div class="mini-controls"><span>5000+</span><span>10000+</span></div>
|
||||
</div>
|
||||
<div style="padding:16px 18px;background:#fff;min-height:360px;">
|
||||
<div class="rule-row">
|
||||
<strong>全局节点</strong>
|
||||
<span>只画点;核心节点变大;普通节点不显示卡片。</span>
|
||||
</div>
|
||||
<div class="rule-row">
|
||||
<strong>标签</strong>
|
||||
<span>只显示高连接、搜索命中、选中节点;其余悬停或抽屉里看。</span>
|
||||
</div>
|
||||
<div class="rule-row">
|
||||
<strong>边</strong>
|
||||
<span>按权重裁剪;移动和缩放时隐藏弱边,停下再补回来。</span>
|
||||
</div>
|
||||
<div class="rule-row">
|
||||
<strong>社区</strong>
|
||||
<span>超过阈值时先看社区聚合;点击社区后再展开局部。</span>
|
||||
</div>
|
||||
<div class="rule-row">
|
||||
<strong>抽屉</strong>
|
||||
<span>全局抽屉只轻量查看;社区抽屉承接阅读和深挖,提问能力后续接入。</span>
|
||||
</div>
|
||||
<div class="rule-row">
|
||||
<strong>返回</strong>
|
||||
<span>回全图是按钮主入口;双击空白只是快捷。重置布局只处理钉位,不等于回全图。</span>
|
||||
</div>
|
||||
<div class="rule-row">
|
||||
<strong>底线</strong>
|
||||
<span>拖动、缩放、筛选时优先保证流畅,视觉细节可以延后出现。</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user