Skip to content

Commit 132a288

Browse files
committed
feat: implement KnowledgeGraphView component and integrate knowledge graph fetching in detail view
1 parent 45b634e commit 132a288

4 files changed

Lines changed: 412 additions & 2 deletions

File tree

frontend/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.tsx

Lines changed: 156 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,26 @@ import {
55
DeleteOutlined,
66
EditOutlined,
77
ReloadOutlined,
8+
CloseOutlined,
89
} from "@ant-design/icons";
910
import { useNavigate, useParams } from "react-router";
1011
import DetailHeader from "@/components/DetailHeader";
1112
import { SearchControls } from "@/components/SearchControls";
12-
import { KBFile, KnowledgeBaseItem } from "../knowledge-base.model";
13+
import { KBFile, KnowledgeBaseItem, KnowledgeGraphNode, KnowledgeGraphEdge, KBType } from "../knowledge-base.model";
1314
import { mapFileData, mapKnowledgeBase } from "../knowledge-base.const";
1415
import {
1516
deleteKnowledgeBaseByIdUsingDelete,
1617
deleteKnowledgeBaseFileByIdUsingDelete,
1718
queryKnowledgeBaseByIdUsingGet,
1819
queryKnowledgeBaseFilesUsingGet,
1920
retrieveKnowledgeBaseContent,
21+
fetchKnowledgeGraph,
2022
} from "../knowledge-base.api";
2123
import useFetchData from "@/hooks/useFetchData";
2224
import AddDataDialog from "../components/AddDataDialog";
2325
import CreateKnowledgeBase from "../components/CreateKnowledgeBase";
26+
import KnowledgeGraphView, { GraphEntitySelection } from "../components/KnowledgeGraphView";
27+
import { Network } from "lucide-react";
2428

2529
interface StatisticItem {
2630
icon?: React.ReactNode;
@@ -49,6 +53,10 @@ const KnowledgeBaseDetailPage: React.FC = () => {
4953
const [recallLoading, setRecallLoading] = useState(false);
5054
const [recallResults, setRecallResults] = useState<RecallResult[]>([]);
5155
const [recallQuery, setRecallQuery] = useState("");
56+
const [graphVisible, setGraphVisible] = useState(false);
57+
const [graphLoading, setGraphLoading] = useState(false);
58+
const [graphData, setGraphData] = useState<{ nodes: KnowledgeGraphNode[]; edges: KnowledgeGraphEdge[] }>({ nodes: [], edges: [] });
59+
const [graphSelection, setGraphSelection] = useState<GraphEntitySelection | null>(null);
5260

5361
const fetchKnowledgeBaseDetails = async (id: string) => {
5462
const { data } = await queryKnowledgeBaseByIdUsingGet(id);
@@ -61,6 +69,17 @@ const KnowledgeBaseDetailPage: React.FC = () => {
6169
}
6270
}, [id]);
6371

72+
useEffect(() => {
73+
if (!graphVisible) {
74+
return;
75+
}
76+
const previousOverflow = document.body.style.overflow;
77+
document.body.style.overflow = "hidden";
78+
return () => {
79+
document.body.style.overflow = previousOverflow;
80+
};
81+
}, [graphVisible]);
82+
6483
const {
6584
loading,
6685
tableData: files,
@@ -119,7 +138,47 @@ const KnowledgeBaseDetailPage: React.FC = () => {
119138
setRecallLoading(false);
120139
};
121140

122-
const operations = [
141+
const handleGraphFetch = async () => {
142+
if (!knowledgeBase?.id) return;
143+
setGraphLoading(true);
144+
setGraphSelection(null);
145+
try {
146+
const { data } = await fetchKnowledgeGraph({ knowledge_base_id: knowledgeBase.id, query: "*" });
147+
setGraphData({ nodes: data?.nodes ?? [], edges: data?.edges ?? [] });
148+
} catch {
149+
setGraphData({ nodes: [], edges: [] });
150+
}
151+
setGraphLoading(false);
152+
};
153+
154+
const handleOpenGraph = () => {
155+
setGraphSelection(null);
156+
setGraphVisible(true);
157+
if (!graphData.nodes.length) {
158+
handleGraphFetch();
159+
}
160+
};
161+
162+
const handleCloseGraph = () => {
163+
setGraphVisible(false);
164+
setGraphSelection(null);
165+
};
166+
167+
const handleGraphRefresh = () => {
168+
handleGraphFetch();
169+
};
170+
171+
type DetailOperation = NonNullable<React.ComponentProps<typeof DetailHeader>["operations"][number]>;
172+
const graphOperation: DetailOperation | null = knowledgeBase?.type === KBType.GRAPH
173+
? {
174+
key: "graph",
175+
label: "知识图谱",
176+
icon: <Network />,
177+
onClick: handleOpenGraph,
178+
}
179+
: null;
180+
181+
const baseOperations: DetailOperation[] = [
123182
{
124183
key: "edit",
125184
label: "编辑知识库",
@@ -152,6 +211,8 @@ const KnowledgeBaseDetailPage: React.FC = () => {
152211
},
153212
];
154213

214+
const operations: DetailOperation[] = [graphOperation, ...baseOperations].filter(Boolean) as DetailOperation[];
215+
155216
const fileOps = [
156217
{
157218
key: "delete",
@@ -256,6 +317,99 @@ const KnowledgeBaseDetailPage: React.FC = () => {
256317
onUpdate={handleRefreshPage}
257318
onClose={() => setShowEdit(false)}
258319
/>
320+
{graphVisible && (
321+
<div className="fixed inset-0 z-[2000] cosmic-modal-bg">
322+
<div className="absolute inset-0 flex flex-col cosmic-modal-panel">
323+
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10 backdrop-blur">
324+
<div>
325+
<div className="text-lg font-semibold text-white/90">知识图谱</div>
326+
<div className="text-xs text-white/50">{knowledgeBase?.name}</div>
327+
</div>
328+
<div className="flex gap-2">
329+
<Button
330+
ghost
331+
type="primary"
332+
icon={<ReloadOutlined />}
333+
onClick={handleGraphRefresh}
334+
loading={graphLoading}
335+
className="cosmic-btn"
336+
>
337+
刷新
338+
</Button>
339+
<Button
340+
type="primary"
341+
icon={<CloseOutlined />}
342+
onClick={handleCloseGraph}
343+
className="cosmic-btn danger"
344+
>
345+
关闭
346+
</Button>
347+
</div>
348+
</div>
349+
<div className="flex-1 relative">
350+
{graphLoading ? (
351+
<div className="absolute inset-0 flex items-center justify-center">
352+
<Spin size="large" />
353+
</div>
354+
) : (
355+
<KnowledgeGraphView
356+
nodes={graphData.nodes}
357+
edges={graphData.edges}
358+
height="100%"
359+
onSelectEntity={setGraphSelection}
360+
/>
361+
)}
362+
{graphSelection && (
363+
<div className="absolute bottom-4 right-4 w-80 max-h-[65vh] overflow-auto rounded-lg bg-slate-900/85 text-slate-100 shadow-[0_10px_50px_rgba(15,23,42,0.7)] border border-white/10 p-4">
364+
<div className="text-sm font-semibold mb-1 text-white/85">
365+
{graphSelection.type === "node" ? "节点详情" : "边详情"}
366+
</div>
367+
<div className="text-xs text-white/50 mb-3">ID: {graphSelection.data.id}</div>
368+
{graphSelection.type === "edge" && (
369+
<div className="space-y-1 text-xs mb-3">
370+
<div className="flex justify-between gap-2">
371+
<span className="text-gray-500">类型</span>
372+
<span className="text-right break-all">{graphSelection.data.type}</span>
373+
</div>
374+
<div className="flex justify-between gap-2">
375+
<span className="text-gray-500">源节点</span>
376+
<span className="text-right break-all">{graphSelection.data.source}</span>
377+
</div>
378+
<div className="flex justify-between gap-2">
379+
<span className="text-gray-500">目标节点</span>
380+
<span className="text-right break-all">{graphSelection.data.target}</span>
381+
</div>
382+
<div className="border-t border-gray-200 my-2" />
383+
</div>
384+
)}
385+
{graphSelection.type === "node" && (
386+
<div className="space-y-1 text-xs mb-3">
387+
<div className="flex justify-between gap-2">
388+
<span className="text-gray-500">标签</span>
389+
<span className="text-right break-all">{graphSelection.data.labels?.join(", ") || "-"}</span>
390+
</div>
391+
<div className="border-t border-gray-200 my-2" />
392+
</div>
393+
)}
394+
<div className="space-y-1 text-xs">
395+
{Object.entries(graphSelection.data.properties ?? {}).map(([key, value]) => (
396+
<div key={key} className="flex justify-between gap-2">
397+
<span className="text-gray-500">{key}</span>
398+
<span className="text-right break-all text-gray-900">
399+
{typeof value === "object" ? JSON.stringify(value) : String(value ?? "-")}
400+
</span>
401+
</div>
402+
))}
403+
{!Object.keys(graphSelection.data.properties ?? {}).length && (
404+
<div className="text-gray-500">暂无属性</div>
405+
)}
406+
</div>
407+
</div>
408+
)}
409+
</div>
410+
</div>
411+
</div>
412+
)}
259413
<div className="flex-1 border-card p-6 mt-4">
260414
<div className="flex items-center justify-between mb-4 gap-3">
261415
<div className="flex items-center gap-2">

0 commit comments

Comments
 (0)