feat(knowledge): add chunk management and document processing features

This commit is contained in:
2025-10-16 16:23:53 +08:00
parent 4f956e79ba
commit 5a0a9ef2a1
17 changed files with 1655 additions and 366 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Box,
@@ -14,16 +14,22 @@ import {
Breadcrumbs,
Link,
Stack,
Card,
CardContent,
Divider,
Chip,
} 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';
import KnowledgeInfoCard from './components/KnowledgeInfoCard';
import FileListComponent from './components/FileListComponent';
import DocumentListComponent from './components/DocumentListComponent';
import FloatingActionButtons from './components/FloatingActionButtons';
import KnowledgeBreadcrumbs from './components/KnowledgeBreadcrumbs';
import { useDocumentList, useDocumentOperations } from '@/hooks/document-hooks';
import { RUNNING_STATUS_KEYS } from '@/constants/knowledge';
function KnowledgeBaseDetail() {
const { id } = useParams<{ id: string }>();
@@ -42,6 +48,12 @@ function KnowledgeBaseDetail() {
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 pollingIntervalRef = useRef<any>(null);
const [isPolling, setIsPolling] = useState(false);
// 使用新的document hooks
const {
@@ -61,6 +73,9 @@ function KnowledgeBaseDetail() {
uploadDocuments,
deleteDocuments,
runDocuments,
renameDocument,
changeDocumentStatus,
cancelRunDocuments,
loading: operationLoading,
error: operationError,
} = useDocumentOperations();
@@ -119,11 +134,98 @@ function KnowledgeBaseDetail() {
try {
await runDocuments(docIds);
refreshFiles(); // 刷新列表
startPolling(); // 开始轮询
} catch (err: any) {
setError(err.response?.data?.message || err.message || '重新解析失败');
}
};
// 重命名文件
const handleRename = async (fileId: string, newName: string) => {
try {
await renameDocument(fileId, newName);
refreshFiles();
} catch (err: any) {
setError(err.response?.data?.message || err.message || '重命名失败');
}
};
// 更改文件状态
const handleChangeStatus = async (fileIds: string[], status: string) => {
try {
await changeDocumentStatus(fileIds, status);
refreshFiles();
} catch (err: any) {
setError(err.response?.data?.message || err.message || '更改状态失败');
}
};
// 取消运行
const handleCancelRun = async (fileIds: string[]) => {
try {
await cancelRunDocuments(fileIds);
refreshFiles();
} catch (err: any) {
setError(err.response?.data?.message || err.message || '取消运行失败');
}
};
// 查看详情
const handleViewDetails = (file: IKnowledgeFile) => {
console.log("查看详情:", file);
navigate(`/chunk/parsed-result?kb_id=${id}&doc_id=${file.id}`);
};
// 查看解析详情
const handleViewProcessDetails = (file: IKnowledgeFile) => {
console.log("查看解析详情:", file);
setSelectedFileDetails(file);
setProcessDetailsDialogOpen(true);
};
// 开始轮询
const startPolling = () => {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
}
setIsPolling(true);
pollingIntervalRef.current = setInterval(() => {
refreshFiles();
}, 3000); // 每3秒轮询一次
};
// 停止轮询
const stopPolling = () => {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
setIsPolling(false);
};
// 检查是否有运行中的文档
const hasRunningDocuments = files.some(file => file.run === RUNNING_STATUS_KEYS.RUNNING);
// 根据运行状态自动管理轮询
useEffect(() => {
if (hasRunningDocuments && !isPolling) {
startPolling();
} else if (!hasRunningDocuments && isPolling) {
stopPolling();
}
}, [hasRunningDocuments, isPolling]);
// 组件卸载时清理轮询
useEffect(() => {
return () => {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
}
};
}, []);
// 初始化数据
useEffect(() => {
fetchKnowledgeDetail();
@@ -166,6 +268,10 @@ function KnowledgeBaseDetail() {
);
}
function fetchDocumentsFilter(): Promise<IDocumentInfoFilter | undefined> {
throw new Error('Function not implemented.');
}
return (
<Box sx={{ p: 3 }}>
{/* 面包屑导航 */}
@@ -175,7 +281,7 @@ function KnowledgeBaseDetail() {
<KnowledgeInfoCard knowledgeBase={knowledgeBase} />
{/* 文件列表组件 */}
<FileListComponent
<DocumentListComponent
files={files}
loading={filesLoading}
searchKeyword={searchKeyword}
@@ -183,7 +289,6 @@ function KnowledgeBaseDetail() {
onReparse={(fileIds) => handleReparse(fileIds)}
onDelete={(fileIds) => {
console.log('删除文件:', fileIds);
setRowSelectionModel({
type: 'include',
ids: new Set(fileIds)
@@ -205,6 +310,11 @@ function KnowledgeBaseDetail() {
pageSize={pageSize}
onPageChange={setCurrentPage}
onPageSizeChange={setPageSize}
onRename={handleRename}
onChangeStatus={handleChangeStatus}
onCancelRun={handleCancelRun}
onViewDetails={handleViewDetails}
onViewProcessDetails={handleViewProcessDetails}
/>
{/* 浮动操作按钮 */}
@@ -319,6 +429,137 @@ function KnowledgeBaseDetail() {
<Button variant="contained"></Button>
</DialogActions>
</Dialog>
{/* 文档详情对话框 */}
<Dialog
open={processDetailsDialogOpen}
onClose={() => setProcessDetailsDialogOpen(false)}
maxWidth="md"
fullWidth
>
<DialogTitle></DialogTitle>
<DialogContent sx={{ p: 3 }}>
{selectedFileDetails && (
<Stack spacing={3}>
{/* 基本信息卡片 */}
<Card variant="outlined">
<CardContent>
<Typography variant="h6" gutterBottom color="primary">
</Typography>
<Stack spacing={2}>
<Box>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
</Typography>
<Typography variant="body1" sx={{ fontWeight: 500 }}>
{selectedFileDetails.name}
</Typography>
</Box>
<Divider />
<Box>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
ID
</Typography>
<Typography variant="body1">
{selectedFileDetails.parser_id || '未指定'}
</Typography>
</Box>
</Stack>
</CardContent>
</Card>
{/* 处理状态卡片 */}
<Card variant="outlined">
<CardContent>
<Typography variant="h6" gutterBottom color="primary">
</Typography>
<Stack spacing={2}>
<Box>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
</Typography>
<Typography variant="body1">
{selectedFileDetails.process_begin_at || '未开始'}
</Typography>
</Box>
<Divider />
<Box>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
</Typography>
<Typography variant="body1">
{selectedFileDetails.process_duration ? `${selectedFileDetails.process_duration}` : '未完成'}
</Typography>
</Box>
<Divider />
<Box>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Chip
label={`${100*(selectedFileDetails.progress || 0)}%`}
color="primary"
size="small"
/>
<LinearProgress
variant="determinate"
value={100*(selectedFileDetails.progress || 0)}
sx={{ flexGrow: 1, height: 8, borderRadius: 4 }}
/>
</Box>
</Box>
</Stack>
</CardContent>
</Card>
{/* 处理详情卡片 */}
{selectedFileDetails.progress_msg && (
<Card variant="outlined">
<CardContent>
<Typography variant="h6" gutterBottom color="primary">
</Typography>
<Box
sx={{
maxHeight: 300,
overflow: 'auto',
backgroundColor: 'grey.50',
borderRadius: 1,
p: 2,
border: '1px solid',
borderColor: 'grey.200',
}}
>
<Typography
variant="body2"
component="pre"
sx={{
fontFamily: 'monospace',
fontSize: '0.875rem',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
margin: 0,
color: 'text.primary',
}}
>
{selectedFileDetails.progress_msg}
</Typography>
</Box>
</CardContent>
</Card>
)}
</Stack>
)}
</DialogContent>
<DialogActions sx={{ p: 3, pt: 0 }}>
<Button onClick={() => setProcessDetailsDialogOpen(false)} variant="contained">
</Button>
</DialogActions>
</Dialog>
</Box>
);
}