Files
TERES_web_frontend/src/pages/knowledge/detail.tsx

323 lines
9.6 KiB
TypeScript
Raw Normal View History

import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Box,
Typography,
LinearProgress,
Alert,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
Breadcrumbs,
Link,
Stack,
} 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 FileUploadDialog from '@/components/FileUploadDialog';
import KnowledgeInfoCard from './components/KnowledgeInfoCard';
import FileListComponent from './components/FileListComponent';
import FloatingActionButtons from './components/FloatingActionButtons';
import { useDocumentList, useDocumentOperations } from '@/hooks/document-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>({
type: 'include',
ids: new Set()
});
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [testingDialogOpen, setTestingDialogOpen] = useState(false);
const [configDialogOpen, setConfigDialogOpen] = useState(false);
// 使用新的document hooks
const {
documents: files,
loading: filesLoading,
error: filesError,
refresh: refreshFiles,
setKeywords,
} = useDocumentList(id || '');
const {
uploadDocuments,
deleteDocuments,
runDocuments,
loading: operationLoading,
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 handleDeleteFiles = async () => {
try {
await deleteDocuments(Array.from(rowSelectionModel.ids) as string[]);
setDeleteDialogOpen(false);
setRowSelectionModel({
type: 'include',
ids: new Set()
});
refreshFiles();
} catch (err: any) {
setError(err.response?.data?.message || err.message || '删除文件失败');
}
};
// 上传文件处理
const handleUploadFiles = async (uploadFiles: File[]) => {
console.log('上传文件:', uploadFiles);
const kb_id = knowledgeBase?.id || '';
try {
await uploadDocuments(kb_id, uploadFiles);
refreshFiles();
} catch (err: any) {
throw new Error(err.response?.data?.message || err.message || '上传文件失败');
}
};
// 重新解析文件
const handleReparse = async (docIds: string[]) => {
try {
await runDocuments(docIds);
refreshFiles(); // 刷新列表
} catch (err: any) {
setError(err.response?.data?.message || err.message || '重新解析失败');
}
};
// 初始化数据
useEffect(() => {
fetchKnowledgeDetail();
}, [id]);
// 搜索文件
useEffect(() => {
const timer = setTimeout(() => {
setKeywords(searchKeyword);
}, 500);
return () => clearTimeout(timer);
}, [searchKeyword, setKeywords]);
// 合并错误状态
const combinedError = error || filesError || operationError;
if (loading) {
return (
<Box sx={{ p: 3 }}>
<LinearProgress />
<Typography sx={{ mt: 2 }}>...</Typography>
</Box>
);
}
if (combinedError) {
return (
<Box sx={{ p: 3 }}>
<Alert severity="error">{combinedError}</Alert>
</Box>
);
}
if (!knowledgeBase) {
return (
<Box sx={{ p: 3 }}>
<Alert severity="warning"></Alert>
</Box>
);
}
return (
<Box sx={{ p: 3 }}>
{/* 面包屑导航 */}
<Breadcrumbs sx={{ mb: 2 }}>
<Link
component="button"
variant="body1"
onClick={() => navigate('/knowledge')}
sx={{ textDecoration: 'none' }}
>
</Link>
<Typography color="text.primary">{knowledgeBase.name}</Typography>
</Breadcrumbs>
{/* 知识库信息卡片 */}
<KnowledgeInfoCard knowledgeBase={knowledgeBase} />
{/* 文件列表组件 */}
<FileListComponent
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);
}}
/>
{/* 浮动操作按钮 */}
<FloatingActionButtons
onTestClick={() => setTestingDialogOpen(true)}
onConfigClick={() => setConfigDialogOpen(true)}
/>
{/* 上传文件对话框 */}
<FileUploadDialog
open={uploadDialogOpen}
onClose={() => setUploadDialogOpen(false)}
onUpload={handleUploadFiles}
title="上传文件到知识库"
acceptedFileTypes={['.pdf', '.docx', '.txt', '.md', '.png', '.jpg', '.jpeg', '.mp4', '.wav']}
maxFileSize={100}
maxFiles={10}
/>
{/* 删除确认对话框 */}
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
<DialogTitle></DialogTitle>
<DialogContent>
<Typography>
{rowSelectionModel.ids.size}
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteDialogOpen(false)}></Button>
<Button color="error" onClick={handleDeleteFiles}></Button>
</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>
</Box>
);
}
export default KnowledgeBaseDetail;