feat(knowledge): add knowledge graph visualization component
- Add @xyflow/react dependency for graph visualization - Create KnowledgeGraphView component with custom nodes and edges - Extend knowledge detail hook to fetch and display graph data - Add tabs in knowledge detail page to switch between documents and graph views
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import knowledgeService from '@/services/knowledge_service';
|
||||
import type { IKnowledge, IKnowledgeResult, IParserConfig } from '@/interfaces/database/knowledge';
|
||||
import type { IKnowledge, IKnowledgeGraph, IKnowledgeResult } from '@/interfaces/database/knowledge';
|
||||
import type { IFetchKnowledgeListRequestParams } from '@/interfaces/request/knowledge';
|
||||
|
||||
/**
|
||||
@@ -200,6 +200,12 @@ export const useKnowledgeDetail = (kbId: string) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [knowledgeGraph, setKnowledgeGraph] = useState<IKnowledgeGraph | null>(null);
|
||||
|
||||
const showKnowledgeGraph = useMemo(() => {
|
||||
return knowledgeGraph !== null && Object.keys(knowledgeGraph?.graph || {}).length > 0;
|
||||
}, [knowledgeGraph]);
|
||||
|
||||
const fetchKnowledgeDetail = useCallback(async () => {
|
||||
if (!kbId) return;
|
||||
|
||||
@@ -223,15 +229,41 @@ export const useKnowledgeDetail = (kbId: string) => {
|
||||
}
|
||||
}, [kbId]);
|
||||
|
||||
const fetchKnowledgeGraph = useCallback(async () => {
|
||||
if (!kbId) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await knowledgeService.getKnowledgeGraph(kbId);
|
||||
|
||||
if (response.data.code === 0) {
|
||||
setKnowledgeGraph(response.data.data);
|
||||
} else {
|
||||
throw new Error(response.data.message || '获取知识库图失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.message || err.message || '获取知识库图失败';
|
||||
setError(errorMessage);
|
||||
console.error('Failed to fetch knowledge graph:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [kbId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchKnowledgeDetail();
|
||||
}, [fetchKnowledgeDetail]);
|
||||
fetchKnowledgeGraph();
|
||||
}, [fetchKnowledgeDetail, fetchKnowledgeGraph]);
|
||||
|
||||
return {
|
||||
knowledge,
|
||||
knowledgeGraph,
|
||||
loading,
|
||||
error,
|
||||
refresh: fetchKnowledgeDetail,
|
||||
showKnowledgeGraph,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
351
src/pages/knowledge/components/KnowledgeGraphView.tsx
Normal file
351
src/pages/knowledge/components/KnowledgeGraphView.tsx
Normal file
@@ -0,0 +1,351 @@
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
type Node,
|
||||
type Edge,
|
||||
Controls,
|
||||
Background,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
ConnectionMode,
|
||||
Panel,
|
||||
Handle,
|
||||
Position,
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { Box, Typography, Alert, Chip, Card, CardContent, Tooltip, Avatar } from '@mui/material';
|
||||
import type { IKnowledgeGraph } from '@/interfaces/database/knowledge';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
interface KnowledgeGraphProps {
|
||||
knowledgeGraph?: IKnowledgeGraph | null;
|
||||
}
|
||||
|
||||
// 根据实体类型获取节点颜色
|
||||
const getNodeColor = (entityType?: string): string => {
|
||||
const colorMap: Record<string, string> = {
|
||||
PERSON: '#FF6B6B', // 红色 - 人物
|
||||
ORGANIZATION: '#4ECDC4', // 青色 - 组织
|
||||
LOCATION: '#45B7D1', // 蓝色 - 地点
|
||||
EVENT: '#96CEB4', // 绿色 - 事件
|
||||
CONCEPT: '#FFEAA7', // 黄色 - 概念
|
||||
CATEGORY: '#DDA0DD', // 紫色 - 分类
|
||||
TECHNOLOGY: '#98D8C8', // 薄荷绿 - 技术
|
||||
PRODUCT: '#F7DC6F', // 金黄色 - 产品
|
||||
SERVICE: '#BB8FCE', // 淡紫色 - 服务
|
||||
};
|
||||
return colorMap[entityType || 'CONCEPT'] || '#74B9FF';
|
||||
};
|
||||
|
||||
// 自定义节点组件
|
||||
const CustomNode = ({ data }: { data: any }) => {
|
||||
const nodeColor = getNodeColor(data.entity_type);
|
||||
const nodeSize = Math.max(80, Math.min(140, (data.pagerank || 0.1) * 500));
|
||||
|
||||
// 获取节点首字母作为Avatar显示
|
||||
const getInitials = (name: string) => {
|
||||
if (!name) return '?';
|
||||
const words = name.trim().split(/\s+/);
|
||||
if (words.length === 1) {
|
||||
return words[0].charAt(0).toUpperCase();
|
||||
}
|
||||
return words.slice(0, 2).map(word => word.charAt(0).toUpperCase()).join('');
|
||||
};
|
||||
|
||||
const tooltipContent = (
|
||||
<Box sx={{ p: 1, maxWidth: 300 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 1 }}>
|
||||
{data.label || data.name || 'Unknown'}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mb: 0.5 }}>
|
||||
<strong>类型:</strong> {data.entity_type || 'Unknown'}
|
||||
</Typography>
|
||||
{data.description && (
|
||||
<Typography variant="body2">
|
||||
<strong>描述:</strong> {data.description}
|
||||
</Typography>
|
||||
)}
|
||||
{data.pagerank && (
|
||||
<Typography variant="caption" sx={{ display: 'block', mt: 1 }}>
|
||||
PageRank: {data.pagerank.toFixed(4)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={tooltipContent}
|
||||
placement="top"
|
||||
arrow
|
||||
enterDelay={500}
|
||||
leaveDelay={200}
|
||||
>
|
||||
<Card
|
||||
sx={{
|
||||
width: nodeSize,
|
||||
height: nodeSize,
|
||||
borderRadius: '50%',
|
||||
border: '3px solid #fff',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: nodeColor,
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.1)',
|
||||
boxShadow: '0 6px 20px rgba(0,0,0,0.25)',
|
||||
zIndex: 10,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* React Flow Handle 组件 */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
style={{ background: '#555', opacity: 0 }}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
style={{ background: '#555', opacity: 0 }}
|
||||
/>
|
||||
|
||||
<CardContent
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: 1,
|
||||
'&:last-child': { pb: 1 },
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
fontSize: Math.max(10, nodeSize * 0.08),
|
||||
textAlign: 'center',
|
||||
lineHeight: 1.1,
|
||||
wordBreak: 'break-word',
|
||||
textShadow: '1px 1px 2px rgba(0,0,0,0.7)',
|
||||
maxWidth: '90%',
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
>
|
||||
{data.label || data.name || 'Unknown'}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const nodeTypes = {
|
||||
custom: CustomNode,
|
||||
};
|
||||
|
||||
const KnowledgeGraphView: React.FC<KnowledgeGraphProps> = ({ knowledgeGraph }) => {
|
||||
// 转换数据格式为 React Flow 所需的格式
|
||||
const { nodes: initialNodes, edges: initialEdges } = useMemo(() => {
|
||||
const graphData = knowledgeGraph?.graph || {};
|
||||
if (!graphData?.nodes || !graphData?.edges) {
|
||||
return { nodes: [], edges: [] };
|
||||
}
|
||||
|
||||
// 转换节点数据
|
||||
// @ts-ignore
|
||||
const nodes: Node[] = graphData.nodes.map((node, index) => {
|
||||
console.log(`节点 ${index}:`, node);
|
||||
console.log(`节点ID: ${node.id}`);
|
||||
|
||||
const encodeId = encodeURIComponent(String(node.id));
|
||||
|
||||
const n: Node = {
|
||||
id: encodeId,
|
||||
type: 'custom',
|
||||
position: {
|
||||
x: Math.random() * 800,
|
||||
y: Math.random() * 600,
|
||||
},
|
||||
data: {
|
||||
label: node.entity_name || node.id,
|
||||
entity_type: node.entity_type,
|
||||
pagerank: node.pagerank,
|
||||
description: node.description,
|
||||
...node,
|
||||
},
|
||||
};
|
||||
return n
|
||||
});
|
||||
|
||||
// 转换边数据
|
||||
// @ts-ignore
|
||||
const edges: Edge[] = graphData.edges.map((edge, index) => {
|
||||
console.log(`边 ${index}:`, edge);
|
||||
console.log(`src_id: ${edge.src_id}, tgt_id: ${edge.tgt_id}`);
|
||||
// 检查source和target是否存在
|
||||
if (!edge.src_id || !edge.tgt_id) {
|
||||
console.warn(`边 ${index} 缺少src_id或tgt_id:`, edge);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查对应的节点是否存在
|
||||
const sourceExists = nodes.some(node => node.id === encodeURIComponent(edge.src_id));
|
||||
const targetExists = nodes.some(node => node.id === encodeURIComponent(edge.tgt_id));
|
||||
|
||||
if (!sourceExists || !targetExists) {
|
||||
console.warn(`边 ${index} 的节点不存在: source=${sourceExists}, target=${targetExists}`, edge);
|
||||
return null;
|
||||
}
|
||||
|
||||
const weight = Number(edge.weight) || 1;
|
||||
const strokeWidth = Math.max(1, Math.min(3, weight));
|
||||
|
||||
const sourceId = encodeURIComponent(String(edge.src_id));
|
||||
const targetId = encodeURIComponent(String(edge.tgt_id));
|
||||
|
||||
const edgeObj: Edge = {
|
||||
id: `${sourceId}-${targetId}`,
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
type: 'simplebezier',
|
||||
// animated: weight > 5,
|
||||
style: {
|
||||
strokeWidth,
|
||||
stroke: '#99ADD1',
|
||||
},
|
||||
// label: edge.description ? edge.description.substring(0, 50) + '...' : '',
|
||||
labelStyle: {
|
||||
fontSize: '10px',
|
||||
fill: '#666',
|
||||
},
|
||||
data: {
|
||||
weight: edge.weight,
|
||||
description: edge.description,
|
||||
keywords: edge.keywords,
|
||||
},
|
||||
};
|
||||
return edgeObj;
|
||||
})
|
||||
// @ts-ignore
|
||||
.filter(edge => edge != null); // 过滤掉null值
|
||||
return { nodes, edges };
|
||||
}, [knowledgeGraph]);
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
console.log('转换后的节点:', nodes);
|
||||
console.log('转换后的边:', edges);
|
||||
// 节点点击事件
|
||||
const onNodeClick = useCallback((event: React.MouseEvent, node: Node) => {
|
||||
console.log('节点点击:', node.data);
|
||||
}, []);
|
||||
|
||||
// 边点击事件
|
||||
const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => {
|
||||
console.log('边点击:', edge.data);
|
||||
}, []);
|
||||
|
||||
if (!knowledgeGraph || initialNodes.length === 0) {
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', alignItems: 'center', height: 400 }}>
|
||||
<Alert severity="info">暂无知识图谱数据</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%', height: '600px', border: '1px solid #e0e0e0', borderRadius: 1 }}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onEdgeClick={onEdgeClick}
|
||||
nodeTypes={nodeTypes}
|
||||
connectionMode={ConnectionMode.Loose}
|
||||
fitView
|
||||
fitViewOptions={{ padding: 0.2 }}
|
||||
minZoom={0.1}
|
||||
maxZoom={2}
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
|
||||
{/* 图例面板 */}
|
||||
<Panel position="top-right">
|
||||
<Box sx={{
|
||||
backgroundColor: 'rgba(255,255,255,0.9)',
|
||||
p: 2,
|
||||
borderRadius: 1,
|
||||
minWidth: 200,
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
|
||||
图例
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
{['PERSON', 'ORGANIZATION', 'CATEGORY', 'TECHNOLOGY'].map((type) => (
|
||||
<Box key={type} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: getNodeColor(type),
|
||||
border: '1px solid #fff',
|
||||
}}
|
||||
/>
|
||||
<Typography variant="caption">{type}</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Panel>
|
||||
|
||||
{/* 统计信息面板 */}
|
||||
<Panel position="top-left">
|
||||
<Box sx={{
|
||||
backgroundColor: 'rgba(255,255,255,0.9)',
|
||||
p: 2,
|
||||
borderRadius: 1,
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
|
||||
图谱统计
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Chip
|
||||
label={`节点: ${nodes.length}`}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
label={`边: ${edges.length}`}
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Panel>
|
||||
</ReactFlow>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeGraphView;
|
||||
@@ -18,9 +18,10 @@ import {
|
||||
CardContent,
|
||||
Divider,
|
||||
Chip,
|
||||
Tabs,
|
||||
Tab,
|
||||
} from '@mui/material';
|
||||
import { type GridRowSelectionModel } from '@mui/x-data-grid';
|
||||
import knowledgeService from '@/services/knowledge_service';
|
||||
import type { IKnowledge, IKnowledgeFile } from '@/interfaces/database/knowledge';
|
||||
import type { IDocumentInfoFilter } from '@/interfaces/database/document';
|
||||
import FileUploadDialog from '@/components/FileUploadDialog';
|
||||
@@ -28,16 +29,16 @@ import KnowledgeInfoCard from './components/KnowledgeInfoCard';
|
||||
import DocumentListComponent from './components/DocumentListComponent';
|
||||
import FloatingActionButtons from './components/FloatingActionButtons';
|
||||
import KnowledgeBreadcrumbs from './components/KnowledgeBreadcrumbs';
|
||||
import KnowledgeGraphView from './components/KnowledgeGraphView';
|
||||
import { useDocumentList, useDocumentOperations } from '@/hooks/document-hooks';
|
||||
import { RUNNING_STATUS_KEYS } from '@/constants/knowledge';
|
||||
import { useKnowledgeDetail } from '@/hooks/knowledge-hooks';
|
||||
|
||||
function KnowledgeBaseDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 状态管理
|
||||
const [knowledgeBase, setKnowledgeBase] = useState<IKnowledge | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [rowSelectionModel, setRowSelectionModel] = useState<GridRowSelectionModel>({
|
||||
@@ -46,11 +47,13 @@ function KnowledgeBaseDetail() {
|
||||
});
|
||||
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [testingDialogOpen, setTestingDialogOpen] = useState(false);
|
||||
const [configDialogOpen, setConfigDialogOpen] = useState(false);
|
||||
|
||||
const [processDetailsDialogOpen, setProcessDetailsDialogOpen] = useState(false);
|
||||
const [selectedFileDetails, setSelectedFileDetails] = useState<IKnowledgeFile | null>(null);
|
||||
|
||||
// 标签页状态
|
||||
const [currentTab, setCurrentTab] = useState(0);
|
||||
|
||||
// 轮询相关状态
|
||||
const pollingIntervalRef = useRef<any>(null);
|
||||
const [isPolling, setIsPolling] = useState(false);
|
||||
@@ -80,25 +83,9 @@ function KnowledgeBaseDetail() {
|
||||
error: operationError,
|
||||
} = useDocumentOperations();
|
||||
|
||||
// 获取知识库详情
|
||||
const fetchKnowledgeDetail = async () => {
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await knowledgeService.getKnowledgeDetail({ kb_id: id });
|
||||
|
||||
if (response.data.code === 0) {
|
||||
setKnowledgeBase(response.data.data);
|
||||
} else {
|
||||
setError(response.data.message || '获取知识库详情失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || err.message || '获取知识库详情失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const { knowledge: knowledgeBase, refresh: fetchKnowledgeDetail, loading, showKnowledgeGraph, knowledgeGraph } = useKnowledgeDetail(id || '');
|
||||
|
||||
console.log('showKnowledgeGraph:', showKnowledgeGraph, knowledgeGraph);
|
||||
|
||||
// 删除文件
|
||||
const handleDeleteFiles = async () => {
|
||||
@@ -291,42 +278,104 @@ function KnowledgeBaseDetail() {
|
||||
{/* 知识库信息卡片 */}
|
||||
<KnowledgeInfoCard knowledgeBase={knowledgeBase} />
|
||||
|
||||
{/* 文件列表组件 */}
|
||||
<DocumentListComponent
|
||||
files={files}
|
||||
loading={filesLoading}
|
||||
searchKeyword={searchKeyword}
|
||||
onSearchChange={setSearchKeyword}
|
||||
onReparse={(fileIds) => handleReparse(fileIds)}
|
||||
onDelete={(fileIds) => {
|
||||
console.log('删除文件:', fileIds);
|
||||
setRowSelectionModel({
|
||||
type: 'include',
|
||||
ids: new Set(fileIds)
|
||||
});
|
||||
setDeleteDialogOpen(true);
|
||||
}}
|
||||
onUpload={() => setUploadDialogOpen(true)}
|
||||
onRefresh={() => {
|
||||
refreshFiles();
|
||||
fetchKnowledgeDetail();
|
||||
}}
|
||||
rowSelectionModel={rowSelectionModel}
|
||||
onRowSelectionModelChange={(newModel) => {
|
||||
console.log('新的选择模型:', newModel);
|
||||
setRowSelectionModel(newModel);
|
||||
}}
|
||||
total={total}
|
||||
page={currentPage}
|
||||
pageSize={pageSize}
|
||||
onPageChange={setCurrentPage}
|
||||
onPageSizeChange={setPageSize}
|
||||
onRename={handleRename}
|
||||
onChangeStatus={handleChangeStatus}
|
||||
onCancelRun={handleCancelRun}
|
||||
onViewDetails={handleViewDetails}
|
||||
onViewProcessDetails={handleViewProcessDetails}
|
||||
/>
|
||||
{/* 标签页组件 - 仅在showKnowledgeGraph为true时显示 */}
|
||||
{showKnowledgeGraph ? (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Tabs
|
||||
value={currentTab}
|
||||
onChange={(event, newValue) => setCurrentTab(newValue)}
|
||||
sx={{ borderBottom: 1, borderColor: 'divider' }}
|
||||
>
|
||||
<Tab label="Documents" />
|
||||
<Tab label="Graph" />
|
||||
</Tabs>
|
||||
|
||||
{/* Document List 标签页内容 */}
|
||||
{currentTab === 0 && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<DocumentListComponent
|
||||
files={files}
|
||||
loading={filesLoading}
|
||||
searchKeyword={searchKeyword}
|
||||
onSearchChange={setSearchKeyword}
|
||||
onReparse={(fileIds) => handleReparse(fileIds)}
|
||||
onDelete={(fileIds) => {
|
||||
console.log('删除文件:', fileIds);
|
||||
setRowSelectionModel({
|
||||
type: 'include',
|
||||
ids: new Set(fileIds)
|
||||
});
|
||||
setDeleteDialogOpen(true);
|
||||
}}
|
||||
onUpload={() => setUploadDialogOpen(true)}
|
||||
onRefresh={() => {
|
||||
refreshFiles();
|
||||
fetchKnowledgeDetail();
|
||||
}}
|
||||
rowSelectionModel={rowSelectionModel}
|
||||
onRowSelectionModelChange={(newModel) => {
|
||||
console.log('新的选择模型:', newModel);
|
||||
setRowSelectionModel(newModel);
|
||||
}}
|
||||
total={total}
|
||||
page={currentPage}
|
||||
pageSize={pageSize}
|
||||
onPageChange={setCurrentPage}
|
||||
onPageSizeChange={setPageSize}
|
||||
onRename={handleRename}
|
||||
onChangeStatus={handleChangeStatus}
|
||||
onCancelRun={handleCancelRun}
|
||||
onViewDetails={handleViewDetails}
|
||||
onViewProcessDetails={handleViewProcessDetails}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Graph 标签页内容 */}
|
||||
{currentTab === 1 && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<KnowledgeGraphView knowledgeGraph={knowledgeGraph} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
/* 原有的文件列表组件 - 当showKnowledgeGraph为false时显示 */
|
||||
<DocumentListComponent
|
||||
files={files}
|
||||
loading={filesLoading}
|
||||
searchKeyword={searchKeyword}
|
||||
onSearchChange={setSearchKeyword}
|
||||
onReparse={(fileIds) => handleReparse(fileIds)}
|
||||
onDelete={(fileIds) => {
|
||||
console.log('删除文件:', fileIds);
|
||||
setRowSelectionModel({
|
||||
type: 'include',
|
||||
ids: new Set(fileIds)
|
||||
});
|
||||
setDeleteDialogOpen(true);
|
||||
}}
|
||||
onUpload={() => setUploadDialogOpen(true)}
|
||||
onRefresh={() => {
|
||||
refreshFiles();
|
||||
fetchKnowledgeDetail();
|
||||
}}
|
||||
rowSelectionModel={rowSelectionModel}
|
||||
onRowSelectionModelChange={(newModel) => {
|
||||
console.log('新的选择模型:', newModel);
|
||||
setRowSelectionModel(newModel);
|
||||
}}
|
||||
total={total}
|
||||
page={currentPage}
|
||||
pageSize={pageSize}
|
||||
onPageChange={setCurrentPage}
|
||||
onPageSizeChange={setPageSize}
|
||||
onRename={handleRename}
|
||||
onChangeStatus={handleChangeStatus}
|
||||
onCancelRun={handleCancelRun}
|
||||
onViewDetails={handleViewDetails}
|
||||
onViewProcessDetails={handleViewProcessDetails}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 浮动操作按钮 */}
|
||||
<FloatingActionButtons
|
||||
@@ -359,88 +408,6 @@ function KnowledgeBaseDetail() {
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* 检索测试对话框 */}
|
||||
<Dialog open={testingDialogOpen} onClose={() => setTestingDialogOpen(false)} maxWidth="md" fullWidth>
|
||||
<DialogTitle>检索测试</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack spacing={3} sx={{ mt: 1 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="测试查询"
|
||||
placeholder="输入要测试的查询内容..."
|
||||
multiline
|
||||
rows={3}
|
||||
/>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<TextField
|
||||
label="返回结果数量"
|
||||
type="number"
|
||||
defaultValue={5}
|
||||
sx={{ width: 150 }}
|
||||
/>
|
||||
<TextField
|
||||
label="相似度阈值"
|
||||
type="number"
|
||||
defaultValue={0.7}
|
||||
inputProps={{ min: 0, max: 1, step: 0.1 }}
|
||||
sx={{ width: 150 }}
|
||||
/>
|
||||
</Stack>
|
||||
<Box sx={{ minHeight: 200, border: '1px solid #e0e0e0', borderRadius: 1, p: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
测试结果将在这里显示...
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setTestingDialogOpen(false)}>关闭</Button>
|
||||
<Button variant="contained">开始测试</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* 配置设置对话框 */}
|
||||
<Dialog open={configDialogOpen} onClose={() => setConfigDialogOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>配置设置</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack spacing={3} sx={{ mt: 1 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="知识库名称"
|
||||
defaultValue={knowledgeBase?.name}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="描述"
|
||||
multiline
|
||||
rows={3}
|
||||
defaultValue={knowledgeBase?.description}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="语言"
|
||||
defaultValue={knowledgeBase?.language}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="嵌入模型"
|
||||
defaultValue={knowledgeBase?.embd_id}
|
||||
disabled
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="解析器"
|
||||
defaultValue={knowledgeBase?.parser_id}
|
||||
disabled
|
||||
/>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setConfigDialogOpen(false)}>取消</Button>
|
||||
<Button variant="contained">保存</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* 文档详情对话框 */}
|
||||
<Dialog
|
||||
open={processDetailsDialogOpen}
|
||||
|
||||
Reference in New Issue
Block a user