feat: update knowledge and agent UI
This commit is contained in:
1 parent
bebe4feccf
commit
7efd811e75
27 files changed
+2949
-782
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { Loader2, Network, Tags } from "lucide-react";
|
||||
import { Loader2, Network, RotateCcw, Tags, Undo2 } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
@@ -13,6 +13,8 @@ import { GraphHeader } from "@/components/knowledge-graph/GraphHeader";
|
||||
import { GraphSidebar } from "@/components/knowledge-graph/GraphSidebar";
|
||||
import { GraphTooltip } from "@/components/knowledge-graph/GraphTooltip";
|
||||
import type {
|
||||
ExploreStep,
|
||||
GraphMode,
|
||||
LayoutType,
|
||||
NodeKind,
|
||||
TooltipState,
|
||||
@@ -20,6 +22,7 @@ import type {
|
||||
import {
|
||||
computeDegrees,
|
||||
computeGraphStats,
|
||||
getEdgeId,
|
||||
getEdgeLabel,
|
||||
getNodeKind,
|
||||
getNodeTitle,
|
||||
@@ -48,16 +51,23 @@ export default function KnowledgeGraphPage() {
|
||||
const [layoutType, setLayoutType] = useState<LayoutType>("force");
|
||||
const [showLabels, setShowLabels] = useState(true);
|
||||
const [showSidebar, setShowSidebar] = useState(true);
|
||||
const [showDetail, setShowDetail] = useState(true);
|
||||
const [showDetail, setShowDetail] = useState(false);
|
||||
const [zoomLevel, setZoomLevel] = useState(1);
|
||||
const [tooltip, setTooltip] = useState<TooltipState | null>(null);
|
||||
const [graphMode, setGraphMode] = useState<GraphMode>("explore");
|
||||
const [exploreRootId, setExploreRootId] = useState<string | null>(null);
|
||||
const [expandedNodeIds, setExpandedNodeIds] = useState<Set<string>>(new Set());
|
||||
const [visibleNodeIds, setVisibleNodeIds] = useState<Set<string>>(new Set());
|
||||
const [visibleEdgeIds, setVisibleEdgeIds] = useState<Set<string>>(new Set());
|
||||
const [nodeDepthMap, setNodeDepthMap] = useState<Map<string, number>>(new Map());
|
||||
const [exploreHistory, setExploreHistory] = useState<ExploreStep[]>([]);
|
||||
|
||||
// Auto-collapse the side panels on narrow viewports.
|
||||
useEffect(() => {
|
||||
const detailQuery = window.matchMedia("(max-width: 1280px)");
|
||||
const sidebarQuery = window.matchMedia("(max-width: 900px)");
|
||||
const apply = () => {
|
||||
setShowDetail(!detailQuery.matches);
|
||||
if (detailQuery.matches) setShowDetail(false);
|
||||
setShowSidebar(!sidebarQuery.matches);
|
||||
};
|
||||
apply();
|
||||
@@ -122,6 +132,133 @@ export default function KnowledgeGraphPage() {
|
||||
);
|
||||
}, [allEdges, filteredNodes, disabledEdgeLabels]);
|
||||
|
||||
const resetExplore = useCallback(() => {
|
||||
setExploreRootId(null);
|
||||
setExpandedNodeIds(new Set());
|
||||
setVisibleNodeIds(new Set());
|
||||
setVisibleEdgeIds(new Set());
|
||||
setNodeDepthMap(new Map());
|
||||
setExploreHistory([]);
|
||||
}, []);
|
||||
|
||||
const rebuildExploreState = useCallback((history: ExploreStep[]) => {
|
||||
const nextVisibleNodeIds = new Set<string>();
|
||||
const nextVisibleEdgeIds = new Set<string>();
|
||||
const nextNodeDepthMap = new Map<string, number>();
|
||||
const nextExpandedNodeIds = new Set<string>();
|
||||
|
||||
for (const step of history) {
|
||||
for (const nodeId of step.addedNodeIds) {
|
||||
nextVisibleNodeIds.add(nodeId);
|
||||
if (!nextNodeDepthMap.has(nodeId)) nextNodeDepthMap.set(nodeId, step.depth);
|
||||
}
|
||||
for (const edgeId of step.addedEdgeIds) nextVisibleEdgeIds.add(edgeId);
|
||||
if (step.depth > 0) nextExpandedNodeIds.add(step.nodeId);
|
||||
}
|
||||
|
||||
setVisibleNodeIds(nextVisibleNodeIds);
|
||||
setVisibleEdgeIds(nextVisibleEdgeIds);
|
||||
setNodeDepthMap(nextNodeDepthMap);
|
||||
setExpandedNodeIds(nextExpandedNodeIds);
|
||||
}, []);
|
||||
|
||||
const startExploreFromNode = useCallback((nodeId: string) => {
|
||||
setExploreRootId(nodeId);
|
||||
setVisibleNodeIds(new Set([nodeId]));
|
||||
setVisibleEdgeIds(new Set());
|
||||
setExpandedNodeIds(new Set());
|
||||
setNodeDepthMap(new Map([[nodeId, 0]]));
|
||||
setExploreHistory([
|
||||
{
|
||||
nodeId,
|
||||
depth: 0,
|
||||
addedNodeIds: [nodeId],
|
||||
addedEdgeIds: [],
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
]);
|
||||
window.setTimeout(() => canvasRef.current?.focusNode(nodeId), 0);
|
||||
}, []);
|
||||
|
||||
const expandOneHop = useCallback(
|
||||
(nodeId: string) => {
|
||||
const currentDepth = nodeDepthMap.get(nodeId) ?? 0;
|
||||
const nextDepth = currentDepth + 1;
|
||||
const addedNodeIds: string[] = [];
|
||||
const addedEdgeIds: string[] = [];
|
||||
const nextVisibleNodeIds = new Set(visibleNodeIds);
|
||||
const nextVisibleEdgeIds = new Set(visibleEdgeIds);
|
||||
const nextNodeDepthMap = new Map(nodeDepthMap);
|
||||
|
||||
for (const edge of filteredEdges) {
|
||||
if (edge.source !== nodeId && edge.target !== nodeId) continue;
|
||||
const edgeId = getEdgeId(edge);
|
||||
const neighborId = edge.source === nodeId ? edge.target : edge.source;
|
||||
if (!nodeById.has(neighborId)) continue;
|
||||
|
||||
if (!nextVisibleEdgeIds.has(edgeId)) {
|
||||
nextVisibleEdgeIds.add(edgeId);
|
||||
addedEdgeIds.push(edgeId);
|
||||
}
|
||||
if (!nextVisibleNodeIds.has(neighborId)) {
|
||||
nextVisibleNodeIds.add(neighborId);
|
||||
nextNodeDepthMap.set(neighborId, nextDepth);
|
||||
addedNodeIds.push(neighborId);
|
||||
}
|
||||
}
|
||||
|
||||
nextVisibleNodeIds.add(nodeId);
|
||||
nextNodeDepthMap.set(nodeId, currentDepth);
|
||||
setVisibleNodeIds(nextVisibleNodeIds);
|
||||
setVisibleEdgeIds(nextVisibleEdgeIds);
|
||||
setNodeDepthMap(nextNodeDepthMap);
|
||||
setExpandedNodeIds((prev) => new Set(prev).add(nodeId));
|
||||
setExploreHistory((prev) => [
|
||||
...prev,
|
||||
{ nodeId, depth: nextDepth, addedNodeIds, addedEdgeIds, timestamp: Date.now() },
|
||||
]);
|
||||
},
|
||||
[filteredEdges, nodeById, nodeDepthMap, visibleEdgeIds, visibleNodeIds],
|
||||
);
|
||||
|
||||
const undoExploreStep = useCallback(() => {
|
||||
if (exploreHistory.length <= 1) {
|
||||
resetExplore();
|
||||
return;
|
||||
}
|
||||
const nextHistory = exploreHistory.slice(0, -1);
|
||||
setExploreHistory(nextHistory);
|
||||
rebuildExploreState(nextHistory);
|
||||
}, [exploreHistory, rebuildExploreState, resetExplore]);
|
||||
|
||||
useEffect(() => {
|
||||
if (graphMode === "explore") resetExplore();
|
||||
}, [
|
||||
disabledNodeKinds,
|
||||
disabledEdgeLabels,
|
||||
hiddenNodeIds,
|
||||
searchKeyword,
|
||||
resetExplore,
|
||||
]);
|
||||
|
||||
const displayedGraph = useMemo(() => {
|
||||
if (graphMode === "full") return { nodes: filteredNodes, edges: filteredEdges };
|
||||
if (!exploreRootId) return { nodes: filteredNodes, edges: [] };
|
||||
return {
|
||||
nodes: allNodes.filter((node) => visibleNodeIds.has(node.id)),
|
||||
edges: allEdges.filter((edge) => visibleEdgeIds.has(getEdgeId(edge))),
|
||||
};
|
||||
}, [
|
||||
allEdges,
|
||||
allNodes,
|
||||
exploreRootId,
|
||||
filteredEdges,
|
||||
filteredNodes,
|
||||
graphMode,
|
||||
visibleEdgeIds,
|
||||
visibleNodeIds,
|
||||
]);
|
||||
|
||||
const searchMatchIds = useMemo(() => {
|
||||
const keyword = searchKeyword.trim().toLowerCase();
|
||||
if (!keyword) return null;
|
||||
@@ -146,7 +283,7 @@ export default function KnowledgeGraphPage() {
|
||||
if (!selectedNodeId) return [];
|
||||
const items: Array<{ node: (typeof allNodes)[number]; relation: string }> = [];
|
||||
const seen = new Set<string>();
|
||||
for (const e of filteredEdges) {
|
||||
for (const e of displayedGraph.edges) {
|
||||
const otherId =
|
||||
e.source === selectedNodeId
|
||||
? e.target
|
||||
@@ -162,7 +299,7 @@ export default function KnowledgeGraphPage() {
|
||||
return items.sort(
|
||||
(a, b) => (degrees.get(b.node.id) ?? 0) - (degrees.get(a.node.id) ?? 0),
|
||||
);
|
||||
}, [selectedNodeId, filteredEdges, nodeById, degrees, allNodes]);
|
||||
}, [selectedNodeId, displayedGraph.edges, nodeById, degrees, allNodes]);
|
||||
|
||||
// Note detail for note-type nodes (summary / tags / timestamps / status).
|
||||
const noteId =
|
||||
@@ -172,14 +309,44 @@ export default function KnowledgeGraphPage() {
|
||||
const { data: note, isLoading: noteLoading } = useKnowledgeNote(noteId);
|
||||
|
||||
// --------------------------------------------------------------- handlers
|
||||
const handleSelectNode = useCallback((id: string | null) => {
|
||||
setSelectedNodeId(id);
|
||||
}, []);
|
||||
const handleSelectNode = useCallback(
|
||||
(id: string | null) => {
|
||||
if (!id) {
|
||||
setSelectedNodeId(null);
|
||||
setShowDetail(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const handlePickNode = useCallback((id: string) => {
|
||||
setSelectedNodeId(id);
|
||||
canvasRef.current?.focusNode(id);
|
||||
}, []);
|
||||
setSelectedNodeId(id);
|
||||
setShowDetail(true);
|
||||
|
||||
if (graphMode !== "explore") return;
|
||||
if (!exploreRootId) {
|
||||
startExploreFromNode(id);
|
||||
return;
|
||||
}
|
||||
if (!visibleNodeIds.has(id)) return;
|
||||
if (!expandedNodeIds.has(id)) expandOneHop(id);
|
||||
},
|
||||
[
|
||||
expandedNodeIds,
|
||||
expandOneHop,
|
||||
exploreRootId,
|
||||
graphMode,
|
||||
startExploreFromNode,
|
||||
visibleNodeIds,
|
||||
],
|
||||
);
|
||||
|
||||
const handlePickNode = useCallback(
|
||||
(id: string) => {
|
||||
setSelectedNodeId(id);
|
||||
setShowDetail(true);
|
||||
if (graphMode === "explore" && !exploreRootId) startExploreFromNode(id);
|
||||
canvasRef.current?.focusNode(id);
|
||||
},
|
||||
[exploreRootId, graphMode, startExploreFromNode],
|
||||
);
|
||||
|
||||
const handleSearchEnter = useCallback(() => {
|
||||
if (!searchMatchIds || searchMatchIds.size === 0) return;
|
||||
@@ -205,10 +372,14 @@ export default function KnowledgeGraphPage() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleHideNode = useCallback((id: string) => {
|
||||
setHiddenNodeIds((prev) => new Set(prev).add(id));
|
||||
setSelectedNodeId((current) => (current === id ? null : current));
|
||||
}, []);
|
||||
const handleHideNode = useCallback(
|
||||
(id: string) => {
|
||||
setHiddenNodeIds((prev) => new Set(prev).add(id));
|
||||
setSelectedNodeId((current) => (current === id ? null : current));
|
||||
if (selectedNodeId === id) setShowDetail(false);
|
||||
},
|
||||
[selectedNodeId],
|
||||
);
|
||||
|
||||
const handleRestoreHidden = useCallback(() => {
|
||||
setHiddenNodeIds(new Set());
|
||||
@@ -238,6 +409,35 @@ export default function KnowledgeGraphPage() {
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const handleSetExploreRoot = useCallback(
|
||||
(id: string) => {
|
||||
setGraphMode("explore");
|
||||
startExploreFromNode(id);
|
||||
setSelectedNodeId(id);
|
||||
setShowDetail(true);
|
||||
},
|
||||
[startExploreFromNode],
|
||||
);
|
||||
|
||||
const exploreDepth = useMemo(() => {
|
||||
let max = 0;
|
||||
for (const depth of nodeDepthMap.values()) max = Math.max(max, depth);
|
||||
return max;
|
||||
}, [nodeDepthMap]);
|
||||
|
||||
const exploreRootTitle = exploreRootId
|
||||
? getNodeTitle(nodeById.get(exploreRootId) ?? { id: exploreRootId })
|
||||
: null;
|
||||
|
||||
const selectedExploreInfo =
|
||||
graphMode === "explore" && selectedNodeId
|
||||
? {
|
||||
depth: nodeDepthMap.get(selectedNodeId) ?? null,
|
||||
expanded: expandedNodeIds.has(selectedNodeId),
|
||||
isRoot: selectedNodeId === exploreRootId,
|
||||
}
|
||||
: null;
|
||||
|
||||
// ---------------------------------------------------------------- overlay
|
||||
let overlay: React.ReactNode = null;
|
||||
if (isLoading) {
|
||||
@@ -284,6 +484,50 @@ export default function KnowledgeGraphPage() {
|
||||
|
||||
const toolbar = (
|
||||
<>
|
||||
<div className="kg-floating-pill">
|
||||
<span className="kg-pill-label">模式</span>
|
||||
<button
|
||||
type="button"
|
||||
className={`kg-pill-button${graphMode === "explore" ? " kg-pill-button-active" : ""}`}
|
||||
onClick={() => setGraphMode("explore")}
|
||||
>
|
||||
探索模式
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`kg-pill-button${graphMode === "full" ? " kg-pill-button-active" : ""}`}
|
||||
onClick={() => setGraphMode("full")}
|
||||
>
|
||||
全量图谱
|
||||
</button>
|
||||
</div>
|
||||
{graphMode === "explore" ? (
|
||||
<>
|
||||
<div className="kg-floating-pill">
|
||||
起点:{exploreRootTitle ? exploreRootTitle : "未选择"}
|
||||
</div>
|
||||
<div className="kg-floating-pill">
|
||||
深度 {exploreDepth} · 已显示 {displayedGraph.nodes.length} 节点 /{" "}
|
||||
{displayedGraph.edges.length} 关系
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="kg-explore-button"
|
||||
disabled={exploreHistory.length === 0}
|
||||
onClick={undoExploreStep}
|
||||
>
|
||||
<Undo2 size={13} /> 返回上一层
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="kg-explore-button"
|
||||
disabled={!exploreRootId}
|
||||
onClick={resetExplore}
|
||||
>
|
||||
<RotateCcw size={13} /> 重置探索
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
<div className="kg-floating-pill">
|
||||
<span className="kg-pill-label">布局</span>
|
||||
<button
|
||||
@@ -313,16 +557,23 @@ export default function KnowledgeGraphPage() {
|
||||
</>
|
||||
);
|
||||
|
||||
const hint =
|
||||
graphMode === "explore" ? (
|
||||
<div className="kg-canvas-hint">
|
||||
{!exploreRootId
|
||||
? "点击任意节点作为起点,开始逐层探索知识关系。"
|
||||
: "点击已显示节点,可继续展开它的一跳邻居。已展开的节点和路径会保留。"}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="kg-page" ref={pageRef}>
|
||||
<GraphHeader
|
||||
nodeCount={filteredNodes.length}
|
||||
edgeCount={filteredEdges.length}
|
||||
nodeCount={displayedGraph.nodes.length}
|
||||
edgeCount={displayedGraph.edges.length}
|
||||
totalNodes={allNodes.length}
|
||||
totalEdges={allEdges.length}
|
||||
refreshing={isFetching}
|
||||
showSidebar={showSidebar}
|
||||
showDetail={showDetail}
|
||||
onBack={() => navigate("/page/workspace/knowledge")}
|
||||
onRelayout={() => canvasRef.current?.relayout()}
|
||||
onFitView={() => canvasRef.current?.fitView()}
|
||||
@@ -331,8 +582,6 @@ export default function KnowledgeGraphPage() {
|
||||
onExport={() => canvasRef.current?.exportImage()}
|
||||
onFullscreen={handleFullscreen}
|
||||
onRefresh={handleRefresh}
|
||||
onToggleSidebar={() => setShowSidebar((v) => !v)}
|
||||
onToggleDetail={() => setShowDetail((v) => !v)}
|
||||
/>
|
||||
|
||||
<div
|
||||
@@ -361,24 +610,56 @@ export default function KnowledgeGraphPage() {
|
||||
onToggleEdgeLabel={handleToggleEdgeLabel}
|
||||
onRestoreHidden={handleRestoreHidden}
|
||||
onPickNode={handlePickNode}
|
||||
onClose={() => setShowSidebar(false)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<GraphCanvas
|
||||
ref={canvasRef}
|
||||
nodes={filteredNodes}
|
||||
edges={filteredEdges}
|
||||
degrees={degrees}
|
||||
selectedNodeId={selectedNodeId}
|
||||
searchMatchIds={searchMatchIds}
|
||||
layoutType={layoutType}
|
||||
showLabels={showLabels}
|
||||
onSelectNode={handleSelectNode}
|
||||
onTooltip={setTooltip}
|
||||
onZoomChange={setZoomLevel}
|
||||
toolbar={toolbar}
|
||||
overlay={overlay}
|
||||
/>
|
||||
<div className="kg-canvas-shell">
|
||||
{!showSidebar ? (
|
||||
<button
|
||||
type="button"
|
||||
className="kg-side-rail-toggle kg-side-rail-toggle-left"
|
||||
onClick={() => setShowSidebar(true)}
|
||||
>
|
||||
筛选
|
||||
</button>
|
||||
) : null}
|
||||
<GraphCanvas
|
||||
ref={canvasRef}
|
||||
nodes={displayedGraph.nodes}
|
||||
edges={displayedGraph.edges}
|
||||
degrees={degrees}
|
||||
selectedNodeId={selectedNodeId}
|
||||
searchMatchIds={searchMatchIds}
|
||||
layoutType={layoutType}
|
||||
showLabels={showLabels}
|
||||
onSelectNode={handleSelectNode}
|
||||
onTooltip={setTooltip}
|
||||
onZoomChange={setZoomLevel}
|
||||
explore={
|
||||
graphMode === "explore"
|
||||
? {
|
||||
preview: !exploreRootId,
|
||||
rootId: exploreRootId,
|
||||
nodeDepthMap,
|
||||
expandedNodeIds,
|
||||
}
|
||||
: null
|
||||
}
|
||||
toolbar={toolbar}
|
||||
hint={hint}
|
||||
overlay={overlay}
|
||||
/>
|
||||
{!showDetail ? (
|
||||
<button
|
||||
type="button"
|
||||
className="kg-side-rail-toggle kg-side-rail-toggle-right"
|
||||
onClick={() => setShowDetail(true)}
|
||||
>
|
||||
详情
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{showDetail ? (
|
||||
<GraphDetailPanel
|
||||
@@ -387,10 +668,14 @@ export default function KnowledgeGraphPage() {
|
||||
neighbors={selectedNeighbors}
|
||||
note={noteId ? note : undefined}
|
||||
noteLoading={Boolean(noteId) && noteLoading}
|
||||
explore={selectedExploreInfo}
|
||||
onViewSource={handleViewSource}
|
||||
onLocate={(id) => canvasRef.current?.focusNode(id)}
|
||||
onHide={handleHideNode}
|
||||
onPickNeighbor={handlePickNode}
|
||||
onExpandNode={expandOneHop}
|
||||
onSetExploreRoot={handleSetExploreRoot}
|
||||
onClose={() => setShowDetail(false)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -400,8 +685,8 @@ export default function KnowledgeGraphPage() {
|
||||
edgeLabels={[...edgeLabelCounts.keys()]}
|
||||
zoomLevel={zoomLevel}
|
||||
selectedTitle={selectedNode ? getNodeTitle(selectedNode) : null}
|
||||
visibleNodes={filteredNodes.length}
|
||||
visibleEdges={filteredEdges.length}
|
||||
visibleNodes={displayedGraph.nodes.length}
|
||||
visibleEdges={displayedGraph.edges.length}
|
||||
/>
|
||||
|
||||
<GraphTooltip tooltip={tooltip} />
|
||||
|
||||
Reference in new issue
Block a user