@@ -5,22 +5,26 @@ import {
55 DeleteOutlined ,
66 EditOutlined ,
77 ReloadOutlined ,
8+ CloseOutlined ,
89} from "@ant-design/icons" ;
910import { useNavigate , useParams } from "react-router" ;
1011import DetailHeader from "@/components/DetailHeader" ;
1112import { SearchControls } from "@/components/SearchControls" ;
12- import { KBFile , KnowledgeBaseItem } from "../knowledge-base.model" ;
13+ import { KBFile , KnowledgeBaseItem , KnowledgeGraphNode , KnowledgeGraphEdge , KBType } from "../knowledge-base.model" ;
1314import { mapFileData , mapKnowledgeBase } from "../knowledge-base.const" ;
1415import {
1516 deleteKnowledgeBaseByIdUsingDelete ,
1617 deleteKnowledgeBaseFileByIdUsingDelete ,
1718 queryKnowledgeBaseByIdUsingGet ,
1819 queryKnowledgeBaseFilesUsingGet ,
1920 retrieveKnowledgeBaseContent ,
21+ fetchKnowledgeGraph ,
2022} from "../knowledge-base.api" ;
2123import useFetchData from "@/hooks/useFetchData" ;
2224import AddDataDialog from "../components/AddDataDialog" ;
2325import CreateKnowledgeBase from "../components/CreateKnowledgeBase" ;
26+ import KnowledgeGraphView , { GraphEntitySelection } from "../components/KnowledgeGraphView" ;
27+ import { Network } from "lucide-react" ;
2428
2529interface 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