feat(knowledge): add knowledge base detail page components and hooks

refactor(knowledge): restructure knowledge detail page with new components
feat(components): add FileUploadDialog for file upload functionality
feat(hooks): implement document management hooks for CRUD operations
This commit is contained in:
2025-10-14 15:42:40 +08:00
parent 34181cf025
commit 7384ae36d0
12 changed files with 1456 additions and 356 deletions

View File

@@ -0,0 +1,295 @@
import React, { useState, useRef, useCallback } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Box,
Typography,
LinearProgress,
List,
ListItem,
ListItemIcon,
ListItemText,
ListItemSecondaryAction,
IconButton,
Alert,
} from '@mui/material';
import {
CloudUpload as CloudUploadIcon,
InsertDriveFile as FileIcon,
Delete as DeleteIcon,
} from '@mui/icons-material';
interface FileUploadDialogProps {
open: boolean;
onClose: () => void;
onUpload: (files: File[]) => Promise<void>;
acceptedFileTypes?: string[];
maxFileSize?: number; // in MB
maxFiles?: number;
title?: string;
}
interface UploadFile {
file: File;
id: string;
progress: number;
error?: string;
}
const FileUploadDialog: React.FC<FileUploadDialogProps> = ({
open,
onClose,
onUpload,
acceptedFileTypes = ['.pdf', '.docx', '.txt', '.md', '.png', '.jpg', '.jpeg', '.mp4', '.wav'],
maxFileSize = 100, // 100MB
maxFiles = 10,
title = '上传文件',
}) => {
const [files, setFiles] = useState<UploadFile[]>([]);
const [isDragOver, setIsDragOver] = useState(false);
const [uploading, setUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const validateFile = (file: File): string | null => {
// 检查文件大小
if (file.size > maxFileSize * 1024 * 1024) {
return `文件大小不能超过 ${maxFileSize}MB`;
}
// 检查文件类型
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
if (!acceptedFileTypes.includes(fileExtension)) {
return `不支持的文件类型: ${fileExtension}`;
}
return null;
};
const addFiles = useCallback((newFiles: FileList | File[]) => {
const fileArray = Array.from(newFiles);
const validFiles: UploadFile[] = [];
let hasError = false;
// 检查文件数量限制
if (files.length + fileArray.length > maxFiles) {
setError(`最多只能上传 ${maxFiles} 个文件`);
return;
}
fileArray.forEach((file) => {
const validationError = validateFile(file);
if (validationError) {
setError(validationError);
hasError = true;
return;
}
// 检查是否已存在同名文件
const isDuplicate = files.some(f => f.file.name === file.name);
if (isDuplicate) {
setError(`文件 "${file.name}" 已存在`);
hasError = true;
return;
}
validFiles.push({
file,
id: Math.random().toString(36).substr(2, 9),
progress: 0,
});
});
if (!hasError && validFiles.length > 0) {
setFiles(prev => [...prev, ...validFiles]);
setError(null);
}
}, [files, maxFiles, maxFileSize, acceptedFileTypes]);
const removeFile = (id: string) => {
setFiles(prev => prev.filter(f => f.id !== id));
setError(null);
};
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(true);
}, []);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
}, []);
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
const droppedFiles = e.dataTransfer.files;
if (droppedFiles.length > 0) {
addFiles(droppedFiles);
}
}, [addFiles]);
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFiles = e.target.files;
if (selectedFiles && selectedFiles.length > 0) {
addFiles(selectedFiles);
}
// 清空input值允许重复选择同一文件
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const handleUpload = async () => {
if (files.length === 0) return;
setUploading(true);
setError(null);
try {
const filesToUpload = files.map(f => f.file);
await onUpload(filesToUpload);
// 上传成功后清空文件列表
setFiles([]);
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : '上传失败');
} finally {
setUploading(false);
}
};
const handleClose = () => {
if (!uploading) {
setFiles([]);
setError(null);
onClose();
}
};
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
return (
<Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{/* 拖拽上传区域 */}
<Box
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
sx={{
border: `2px dashed ${isDragOver ? '#1976d2' : '#ccc'}`,
borderRadius: 2,
p: 4,
textAlign: 'center',
cursor: 'pointer',
backgroundColor: isDragOver ? 'action.hover' : 'transparent',
transition: 'all 0.2s ease-in-out',
mb: 2,
'&:hover': {
borderColor: 'primary.main',
backgroundColor: 'action.hover',
},
}}
>
<CloudUploadIcon sx={{ fontSize: 48, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" gutterBottom>
{isDragOver ? '释放文件到此处' : '拖拽文件到此处或点击上传'}
</Typography>
<Typography variant="body2" color="text.secondary">
: {acceptedFileTypes.join(', ')}
</Typography>
<Typography variant="body2" color="text.secondary">
: {maxFileSize}MB {maxFiles}
</Typography>
</Box>
<input
ref={fileInputRef}
type="file"
multiple
accept={acceptedFileTypes.join(',')}
onChange={handleFileSelect}
style={{ display: 'none' }}
/>
{/* 文件列表 */}
{files.length > 0 && (
<Box>
<Typography variant="subtitle2" gutterBottom>
({files.length}/{maxFiles})
</Typography>
<List dense>
{files.map((uploadFile) => (
<ListItem key={uploadFile.id} divider>
<ListItemIcon>
<FileIcon />
</ListItemIcon>
<ListItemText
primary={uploadFile.file.name}
secondary={formatFileSize(uploadFile.file.size)}
/>
<ListItemSecondaryAction>
<IconButton
edge="end"
onClick={() => removeFile(uploadFile.id)}
disabled={uploading}
>
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
</Box>
)}
{uploading && (
<Box sx={{ mt: 2 }}>
<Typography variant="body2" gutterBottom>
...
</Typography>
<LinearProgress />
</Box>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleClose} disabled={uploading}>
</Button>
<Button
variant="contained"
onClick={handleUpload}
disabled={files.length === 0 || uploading}
>
{uploading ? '上传中...' : `上传 (${files.length})`}
</Button>
</DialogActions>
</Dialog>
);
};
export default FileUploadDialog;

472
src/hooks/document-hooks.ts Normal file
View File

@@ -0,0 +1,472 @@
import { useState, useEffect, useCallback } from 'react';
import knowledgeService from '@/services/knowledge_service';
import type { IKnowledgeFile } from '@/interfaces/database/knowledge';
import type { IFetchKnowledgeListRequestParams, IFetchDocumentListRequestBody } from '@/interfaces/request/knowledge';
// 文档列表Hook状态接口
export interface UseDocumentListState {
documents: IKnowledgeFile[];
total: number;
loading: boolean;
error: string | null;
currentPage: number;
pageSize: number;
keywords: string;
}
// 文档列表Hook返回值接口
export interface UseDocumentListReturn extends UseDocumentListState {
fetchDocuments: (params?: IFetchKnowledgeListRequestParams) => Promise<void>;
setKeywords: (keywords: string) => void;
setCurrentPage: (page: number) => void;
setPageSize: (size: number) => void;
refresh: () => Promise<void>;
}
/**
* 文档列表数据管理Hook
* 支持关键词搜索、分页等功能
*/
export const useDocumentList = (
kbId: string,
initialParams?: IFetchKnowledgeListRequestParams
): UseDocumentListReturn => {
const [documents, setDocuments] = useState<IKnowledgeFile[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(initialParams?.page || 1);
const [pageSize, setPageSize] = useState(initialParams?.page_size || 10);
const [keywords, setKeywords] = useState(initialParams?.keywords || '');
/**
* 获取文档列表
*/
const fetchDocuments = useCallback(async (params?: IFetchKnowledgeListRequestParams) => {
if (!kbId) return;
try {
setLoading(true);
setError(null);
// 合并参数
const queryParams = {
keywords: params?.keywords ?? keywords,
page: params?.page ?? currentPage,
page_size: params?.page_size ?? pageSize,
};
// 构建请求体
const requestBody: IFetchDocumentListRequestBody = {};
// 构建查询参数
const requestParams: IFetchKnowledgeListRequestParams = {
kb_id: kbId,
};
if (queryParams.page) {
requestParams.page = queryParams.page;
}
if (queryParams.page_size) {
requestParams.page_size = queryParams.page_size;
}
if (queryParams.keywords && queryParams.keywords.trim()) {
requestParams.keywords = queryParams.keywords.trim();
}
const response = await knowledgeService.getDocumentList(
Object.keys(requestParams).length > 0 ? requestParams : undefined,
requestBody
);
// 检查响应状态
if (response.data.code === 0) {
const data = response.data.data;
setDocuments(data.docs || []);
setTotal(data.total || 0);
} 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 documents:', err);
} finally {
setLoading(false);
}
}, [kbId, keywords, currentPage, pageSize]);
/**
* 刷新当前页面数据
*/
const refresh = useCallback(() => {
return fetchDocuments();
}, [fetchDocuments]);
/**
* 设置关键词并重置到第一页
*/
const handleSetKeywords = useCallback((newKeywords: string) => {
setKeywords(newKeywords);
setCurrentPage(1);
}, []);
/**
* 设置当前页
*/
const handleSetCurrentPage = useCallback((page: number) => {
setCurrentPage(page);
}, []);
/**
* 设置页面大小并重置到第一页
*/
const handleSetPageSize = useCallback((size: number) => {
setPageSize(size);
setCurrentPage(1);
}, []);
// 当关键词、页码或页面大小变化时重新获取数据
useEffect(() => {
fetchDocuments();
}, [keywords, currentPage, pageSize]);
return {
documents,
total,
loading,
error,
currentPage,
pageSize,
keywords,
fetchDocuments,
setKeywords: handleSetKeywords,
setCurrentPage: handleSetCurrentPage,
setPageSize: handleSetPageSize,
refresh,
};
};
/**
* 文档操作Hook
* 提供上传、删除、重命名等功能
*/
export const useDocumentOperations = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
/**
* 上传文档
*/
const uploadDocuments = useCallback(async (kbId: string, files: File[]) => {
try {
setLoading(true);
setError(null);
const formData = new FormData();
formData.append('kb_id', kbId);
files.forEach((file) => {
formData.append('file', file);
});
const response = await knowledgeService.uploadDocument(formData);
if (response.data.code === 0) {
return 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 upload documents:', err);
throw err;
} finally {
setLoading(false);
}
}, []);
/**
* 上传并解析文档
*/
const uploadAndParseDocuments = useCallback(async (kbId: string, files: File[]) => {
try {
setLoading(true);
setError(null);
const formData = new FormData();
formData.append('kb_id', kbId);
files.forEach((file) => {
formData.append('file', file);
});
const response = await knowledgeService.uploadAndParseDocument(formData);
if (response.data.code === 0) {
return 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 upload and parse documents:', err);
throw err;
} finally {
setLoading(false);
}
}, []);
/**
* 删除文档
*/
const deleteDocuments = useCallback(async (docIds: string | string[]) => {
try {
setLoading(true);
setError(null);
const response = await knowledgeService.removeDocument({ doc_id: docIds });
if (response.data.code === 0) {
return 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 delete documents:', err);
throw err;
} finally {
setLoading(false);
}
}, []);
/**
* 重命名文档
*/
const renameDocument = useCallback(async (docId: string, name: string) => {
try {
setLoading(true);
setError(null);
const response = await knowledgeService.renameDocument({ doc_id: docId, name });
if (response.data.code === 0) {
return 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 rename document:', err);
throw err;
} finally {
setLoading(false);
}
}, []);
/**
* 创建文档
*/
const createDocument = useCallback(async (data: any) => {
try {
setLoading(true);
setError(null);
const response = await knowledgeService.createDocument(data);
if (response.data.code === 0) {
return 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 create document:', err);
throw err;
} finally {
setLoading(false);
}
}, []);
/**
* 更改文档状态
*/
const changeDocumentStatus = useCallback(async (docIds: string[], status: string) => {
try {
setLoading(true);
setError(null);
const response = await knowledgeService.changeDocumentStatus({ doc_id: docIds, status });
if (response.data.code === 0) {
return 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 change document status:', err);
throw err;
} finally {
setLoading(false);
}
}, []);
/**
* 运行文档处理
*/
const runDocuments = useCallback(async (docIds: string[]) => {
try {
setLoading(true);
setError(null);
const response = await knowledgeService.runDocument({ doc_id: docIds });
if (response.data.code === 0) {
return 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 run documents:', err);
throw err;
} finally {
setLoading(false);
}
}, []);
/**
* 清除错误状态
*/
const clearError = useCallback(() => {
setError(null);
}, []);
return {
loading,
error,
uploadDocuments,
uploadAndParseDocuments,
deleteDocuments,
renameDocument,
createDocument,
changeDocumentStatus,
runDocuments,
clearError,
};
};
/**
* 文档批量操作Hook
* 提供批量删除等功能
*/
export const useDocumentBatchOperations = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
/**
* 批量删除文档
*/
const batchDeleteDocuments = useCallback(async (docIds: string[]) => {
try {
setLoading(true);
setError(null);
const results = await Promise.allSettled(
docIds.map(docId => knowledgeService.removeDocument({ doc_id: docId }))
);
const failures = results
.map((result, index) => ({ result, index }))
.filter(({ result }) => result.status === 'rejected')
.map(({ index }) => docIds[index]);
if (failures.length > 0) {
throw new Error(`删除失败的文档: ${failures.join(', ')}`);
}
return results;
} catch (err: any) {
const errorMessage = err.response?.data?.message || err.message || '批量删除文档失败';
setError(errorMessage);
console.error('Failed to batch delete documents:', err);
throw err;
} finally {
setLoading(false);
}
}, []);
/**
* 批量更改文档状态
*/
const batchChangeDocumentStatus = useCallback(async (docIds: string[], status: string) => {
try {
setLoading(true);
setError(null);
const response = await knowledgeService.changeDocumentStatus({ doc_id: docIds, status });
if (response.data.code === 0) {
return 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 batch change document status:', err);
throw err;
} finally {
setLoading(false);
}
}, []);
/**
* 批量运行文档处理
*/
const batchRunDocuments = useCallback(async (docIds: string[]) => {
try {
setLoading(true);
setError(null);
const response = await knowledgeService.runDocument({ doc_id: docIds });
if (response.data.code === 0) {
return 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 batch run documents:', err);
throw err;
} finally {
setLoading(false);
}
}, []);
/**
* 清除错误状态
*/
const clearError = useCallback(() => {
setError(null);
}, []);
return {
loading,
error,
batchDeleteDocuments,
batchChangeDocumentStatus,
batchRunDocuments,
clearError,
};
};

View File

@@ -13,3 +13,9 @@ body {
a {
text-decoration: inherit;
}
.grid-center-cell {
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -172,7 +172,7 @@ export interface IKnowledgeFile {
size: number;
/** 文件来源类型 */
source_type: string;
/** 文件状态(启用/禁用) */
/** 文件状态(1 启用/ 0 禁用) */
status: string;
/** 文件缩略图base64编码可选 */
thumbnail?: any;

View File

@@ -0,0 +1,323 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Box,
Typography,
Paper,
Chip,
IconButton,
TextField,
InputAdornment,
Menu,
MenuItem,
Tooltip,
Button,
Stack,
} from '@mui/material';
import {
Search as SearchIcon,
MoreVert as MoreVertIcon,
InsertDriveFile as FileIcon,
PictureAsPdf as PdfIcon,
Description as DocIcon,
Image as ImageIcon,
VideoFile as VideoIcon,
AudioFile as AudioIcon,
Upload as UploadIcon,
Refresh as RefreshIcon,
Delete as DeleteIcon,
} from '@mui/icons-material';
import { DataGrid, type GridColDef, type GridRowSelectionModel } from '@mui/x-data-grid';
import { zhCN, enUS } from '@mui/x-data-grid/locales';
import type { IKnowledgeFile } from '@/interfaces/database/knowledge';
import { RUNNING_STATUS_KEYS, type RunningStatus } from '@/constants/knowledge';
import { LanguageAbbreviation } from '@/locales';
import dayjs from 'dayjs';
interface FileListComponentProps {
files: IKnowledgeFile[];
loading: boolean;
searchKeyword: string;
onSearchChange: (keyword: string) => void;
onReparse: (fileIds: string[]) => void;
onDelete: (fileIds: string[]) => void;
onUpload: () => void;
onRefresh: () => void;
rowSelectionModel: GridRowSelectionModel;
onRowSelectionModelChange: (model: GridRowSelectionModel) => void;
}
const getFileIcon = (type: string) => {
switch (type.toLowerCase()) {
case 'pdf': return <PdfIcon color="error" />;
case 'doc':
case 'docx': return <DocIcon color="primary" />;
case 'jpg':
case 'jpeg':
case 'png':
case 'gif': return <ImageIcon color="success" />;
case 'mp4':
case 'avi':
case 'mov': return <VideoIcon color="secondary" />;
case 'mp3':
case 'wav': return <AudioIcon color="warning" />;
default: return <FileIcon />;
}
};
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const getStatusChip = (status: string) => {
return <Chip label={status === '1' ? '启用' : '禁用'}
color={status === '1' ? 'success' : 'error'}
size="small" />;
};
const getRunStatusChip = (run: RunningStatus, progress: number) => {
const statusConfig = {
[RUNNING_STATUS_KEYS.UNSTART]: { label: '未开始', color: 'default' as const },
[RUNNING_STATUS_KEYS.RUNNING]: { label: `解析中 ${progress}%`, color: 'info' as const },
[RUNNING_STATUS_KEYS.CANCEL]: { label: '已取消', color: 'warning' as const },
[RUNNING_STATUS_KEYS.DONE]: { label: '完成', color: 'success' as const },
[RUNNING_STATUS_KEYS.FAIL]: { label: '失败', color: 'error' as const },
};
const config = statusConfig[run] || { label: '未知', color: 'default' as const };
return <Chip label={config.label} color={config.color} size="small" />;
};
const FileListComponent: React.FC<FileListComponentProps> = ({
files,
loading,
searchKeyword,
onSearchChange,
onReparse,
onDelete,
onUpload,
onRefresh,
rowSelectionModel,
onRowSelectionModelChange,
}) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedFileId, setSelectedFileId] = useState<string>('');
const { i18n } = useTranslation();
// 根据当前语言获取DataGrid的localeText
const getDataGridLocale = () => {
const currentLanguage = i18n.language;
return currentLanguage === LanguageAbbreviation.Zh ? zhCN : enUS;
};
const handleMenuClick = (event: React.MouseEvent<HTMLElement>, fileId: string) => {
event.stopPropagation();
setAnchorEl(event.currentTarget);
setSelectedFileId(fileId);
};
const handleMenuClose = () => {
setAnchorEl(null);
setSelectedFileId('');
};
const handleReparse = () => {
if (selectedFileId) {
onReparse([selectedFileId]);
}
handleMenuClose();
};
const handleDelete = () => {
if (selectedFileId) {
onDelete([selectedFileId]);
}
handleMenuClose();
};
// 过滤文件列表
const filteredFiles = files.filter(file =>
file.name.toLowerCase().includes(searchKeyword.toLowerCase())
);
// DataGrid 列定义
const columns: GridColDef[] = [
{
field: 'name',
headerName: '文件名',
flex: 1,
minWidth: 100,
maxWidth: 300,
cellClassName: 'grid-center-cell',
renderCell: (params) => (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getFileIcon(params.row.type)}
<Tooltip title={params.value}>
<Typography variant="body2">{params.value}</Typography>
</Tooltip>
</Box>
),
},
{
field: 'type',
headerName: '类型',
width: 100,
renderCell: (params) => (
<Chip label={params.value.toUpperCase()} size="small" variant="outlined" />
),
},
{
field: 'size',
headerName: '大小',
width: 120,
renderCell: (params) => formatFileSize(params.value),
},
{
field: 'chunk_num',
headerName: '分块数',
width: 100,
type: 'number',
},
{
field: 'status',
headerName: '状态',
width: 120,
renderCell: (params) => getStatusChip(params.value),
},
{
field: 'run',
headerName: '解析状态',
width: 120,
renderCell: (params) => getRunStatusChip(params.value, params.row.progress),
},
{
field: 'create_time',
headerName: '上传时间',
width: 160,
valueFormatter: (value) => dayjs(value).format('YYYY-MM-DD HH:mm:ss'),
},
{
field: 'actions',
headerName: '操作',
width: 80,
sortable: false,
renderCell: (params) => (
<IconButton
size="small"
onClick={(e) => handleMenuClick(e, params.row.id)}
>
<MoreVertIcon />
</IconButton>
),
},
];
return (
<Box>
{/* 文件操作栏 */}
<Paper sx={{ p: 2, mb: 2 }}>
<Stack direction="row" spacing={2} alignItems="center" justifyContent="space-between">
<TextField
placeholder="搜索文件..."
value={searchKeyword}
onChange={(e) => onSearchChange(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
sx={{ minWidth: 300 }}
size="small"
/>
<Stack direction="row" spacing={1}>
<Button
variant="contained"
startIcon={<UploadIcon />}
onClick={onUpload}
>
</Button>
{rowSelectionModel.ids.size > 0 && (
<>
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={() => onReparse(Array.from(rowSelectionModel.ids).map((id) => id.toString()))}
>
({rowSelectionModel.ids.size})
</Button>
<Button
variant="outlined"
color="error"
startIcon={<DeleteIcon />}
onClick={() => onDelete(Array.from(rowSelectionModel.ids).map((id) => id.toString()))}
>
({rowSelectionModel.ids.size})
</Button>
</>
)}
<IconButton onClick={onRefresh}>
<RefreshIcon />
</IconButton>
</Stack>
</Stack>
</Paper>
{/* 文件列表 */}
<Paper sx={{ height: 600, width: '100%' }}>
<DataGrid
rows={filteredFiles}
columns={columns}
loading={loading}
checkboxSelection
disableRowSelectionOnClick
rowSelectionModel={rowSelectionModel}
onRowSelectionModelChange={onRowSelectionModelChange}
pageSizeOptions={[10, 25, 50, 100]}
initialState={{
pagination: {
paginationModel: { page: 0, pageSize: 25 },
},
}}
localeText={getDataGridLocale().components.MuiDataGrid.defaultProps.localeText}
sx={{
'& .MuiDataGrid-cell:focus': {
outline: 'none',
},
'& .MuiDataGrid-row:hover': {
backgroundColor: 'action.hover',
},
}}
/>
</Paper>
{/* 右边菜单 */}
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
>
<MenuItem onClick={handleReparse}>
<Typography></Typography>
</MenuItem>
<MenuItem onClick={handleDelete}>
<Typography color="error"></Typography>
</MenuItem>
</Menu>
</Box>
);
};
export default FileListComponent;

View File

@@ -0,0 +1,70 @@
import React from 'react';
import { SpeedDial, SpeedDialAction } from '@mui/material';
import {
Settings as SettingsIcon,
Search as TestIcon,
Settings as ConfigIcon,
} from '@mui/icons-material';
interface FloatingActionButtonsProps {
onTestClick: () => void;
onConfigClick: () => void;
}
const FloatingActionButtons: React.FC<FloatingActionButtonsProps> = ({
onTestClick,
onConfigClick,
}) => {
const actions = [
{
icon: <TestIcon />,
name: '检索测试',
onClick: onTestClick,
},
{
icon: <ConfigIcon />,
name: '配置设置',
onClick: onConfigClick,
},
];
return (
<SpeedDial
ariaLabel="知识库操作"
sx={{
position: 'fixed',
bottom: 128,
right: 64,
'& .MuiSpeedDial-fab': {
bgcolor: 'primary.main',
'&:hover': {
bgcolor: 'primary.dark',
},
},
}}
icon={<SettingsIcon />}
>
{actions.map((action) => (
<SpeedDialAction
key={action.name}
icon={action.icon}
slotProps={{
tooltip: {
title: action.name,
sx: {
bgcolor: 'primary.main',
color: 'white',
":hover": {
bgcolor: 'primary.dark',
},
},
},
}}
onClick={action.onClick}
/>
))}
</SpeedDial>
);
};
export default FloatingActionButtons;

View File

@@ -0,0 +1,72 @@
import React from 'react';
import {
Card,
CardContent,
Grid,
Typography,
Chip,
Stack,
} from '@mui/material';
import dayjs from 'dayjs';
import type { IKnowledge } from '@/interfaces/database/knowledge';
interface KnowledgeInfoCardProps {
knowledgeBase: IKnowledge;
}
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const KnowledgeInfoCard: React.FC<KnowledgeInfoCardProps> = ({ knowledgeBase }) => {
return (
<Card sx={{ mb: 3 }}>
<CardContent>
<Grid container spacing={3}>
<Grid size={{ xs: 12, md: 8 }}>
<Typography variant="h4" gutterBottom>
{knowledgeBase.name}
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 2 }}>
{knowledgeBase.description || '暂无描述'}
</Typography>
<Stack direction="row" spacing={2} flexWrap="wrap">
<Chip label={`${knowledgeBase.doc_num || 0} 个文件`} variant="outlined" />
<Chip label={`${knowledgeBase.chunk_num || 0} 个分块`} variant="outlined" />
<Chip label={`${knowledgeBase.token_num || 0} 个令牌`} variant="outlined" />
<Chip label={`大小: ${formatFileSize(knowledgeBase.size || 0)}`} variant="outlined" />
</Stack>
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
<Stack spacing={1} alignItems="flex-end">
<Typography variant="body2" color="text.secondary">
: {dayjs(knowledgeBase.create_time).format('YYYY-MM-DD HH:mm:ss')}
</Typography>
<Typography variant="body2" color="text.secondary">
: {dayjs(knowledgeBase.update_time).format('YYYY-MM-DD HH:mm:ss')}
</Typography>
<Typography variant="body2" color="text.secondary">
: {knowledgeBase.language || 'English'}
</Typography>
<Typography variant="body2" color="text.secondary">
: {knowledgeBase.permission}
</Typography>
<Typography variant="body2" color="text.secondary">
: {knowledgeBase.embd_id}
</Typography>
<Typography variant="body2" color="text.secondary">
: {knowledgeBase.parser_id}
</Typography>
</Stack>
</Grid>
</Grid>
</CardContent>
</Card>
);
};
export default KnowledgeInfoCard;

View File

@@ -58,7 +58,7 @@ function KnowledgeBaseCreate() {
// 使用知识库操作 hooks
const {
loading: isSubmitting,
loading: isSubmitting,
error,
createKnowledge,
updateKnowledgeModelConfig,
@@ -82,9 +82,9 @@ function KnowledgeBaseCreate() {
embd_id: 'text-embedding-v3@Tongyi-Qianwen',
chunk_token_num: 512,
layout_recognize: 'DeepDOC',
delimiter: '\n!?。;!?',
auto_keywords: 5,
auto_questions: 3,
delimiter: '\n',
auto_keywords: 0,
auto_questions: 0,
html4excel: false,
topn_tags: 10,
use_raptor: false,

View File

@@ -3,110 +3,26 @@ import { useParams, useNavigate } from 'react-router-dom';
import {
Box,
Typography,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Chip,
IconButton,
Button,
TextField,
InputAdornment,
LinearProgress,
Alert,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Menu,
MenuItem,
Tooltip,
Stack,
Card,
CardContent,
Grid,
Button,
TextField,
Breadcrumbs,
Link,
Stack,
} from '@mui/material';
import {
Search as SearchIcon,
Upload as UploadIcon,
Delete as DeleteIcon,
Refresh as RefreshIcon,
MoreVert as MoreVertIcon,
InsertDriveFile as FileIcon,
PictureAsPdf as PdfIcon,
Description as DocIcon,
Image as ImageIcon,
VideoFile as VideoIcon,
AudioFile as AudioIcon,
CloudUpload as CloudUploadIcon,
Settings as SettingsIcon,
} from '@mui/icons-material';
import { type GridRowSelectionModel } from '@mui/x-data-grid';
import knowledgeService from '@/services/knowledge_service';
import type { IKnowledge, IKnowledgeFile } from '@/interfaces/database/knowledge';
import { RUNNING_STATUS_KEYS, type RunningStatus } from '@/constants/knowledge';
// 文件类型图标映射
const getFileIcon = (type: string) => {
const lowerType = type.toLowerCase();
if (lowerType.includes('pdf')) return <PdfIcon />;
if (lowerType.includes('doc') || lowerType.includes('txt') || lowerType.includes('md')) return <DocIcon />;
if (lowerType.includes('jpg') || lowerType.includes('png') || lowerType.includes('jpeg')) return <ImageIcon />;
if (lowerType.includes('mp4') || lowerType.includes('avi') || lowerType.includes('mov')) return <VideoIcon />;
if (lowerType.includes('mp3') || lowerType.includes('wav') || lowerType.includes('m4a')) return <AudioIcon />;
return <FileIcon />;
};
// 文件大小格式化
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// 解析状态映射
const getStatusChip = (status: string, progress: number) => {
switch (status) {
case '1':
return <Chip label="已启用" color="success" size="small" />;
case '0':
return <Chip label="已禁用" color="default" size="small" />;
default:
return <Chip label="未知" color="warning" size="small" />;
}
};
// 运行状态映射
const getRunStatusChip = (run: RunningStatus, progress: number) => {
switch (run) {
case RUNNING_STATUS_KEYS.UNSTART:
return <Chip label="未开始" color="default" size="small" />;
case RUNNING_STATUS_KEYS.RUNNING:
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Chip label="解析中" color="primary" size="small" />
<Box sx={{ width: 60 }}>
<LinearProgress variant="determinate" value={progress} />
</Box>
<Typography variant="caption">{progress}%</Typography>
</Box>
);
case RUNNING_STATUS_KEYS.CANCEL:
return <Chip label="已取消" color="warning" size="small" />;
case RUNNING_STATUS_KEYS.DONE:
return <Chip label="已完成" color="success" size="small" />;
case RUNNING_STATUS_KEYS.FAIL:
return <Chip label="失败" color="error" size="small" />;
default:
return <Chip label="未知" color="default" size="small" />;
}
};
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 }>();
@@ -114,15 +30,34 @@ function KnowledgeBaseDetail() {
// 状态管理
const [knowledgeBase, setKnowledgeBase] = useState<IKnowledge | null>(null);
const [files, setFiles] = useState<IKnowledgeFile[]>([]);
const [loading, setLoading] = useState(true);
const [filesLoading, setFilesLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [searchKeyword, setSearchKeyword] = useState('');
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
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 () => {
@@ -144,48 +79,38 @@ function KnowledgeBaseDetail() {
}
};
// 获取文件列表
const fetchFileList = async () => {
if (!id) return;
try {
setFilesLoading(true);
// const response = await knowledgeService.getDocumentList(
// { kb_id: id },
// { keywords: searchKeyword }
// );
// if (response.data.code === 0) {
// setFiles(response.data.data.docs || []);
// } else {
// setError(response.data.message || '获取文件列表失败');
// }
// 删除文件
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 || '获取文件列表失败');
} finally {
setFilesLoading(false);
setError(err.response?.data?.message || err.message || '删除文件失败');
}
};
// 删除文件
const handleDeleteFiles = async () => {
if (selectedFiles.length === 0) return;
// 上传文件处理
const handleUploadFiles = async (uploadFiles: File[]) => {
console.log('上传文件:', uploadFiles);
const kb_id = knowledgeBase?.id || '';
try {
await knowledgeService.removeDocument({ doc_ids: selectedFiles });
setSelectedFiles([]);
setDeleteDialogOpen(false);
fetchFileList(); // 刷新列表
await uploadDocuments(kb_id, uploadFiles);
refreshFiles();
} catch (err: any) {
setError(err.response?.data?.message || err.message || '删除文件失败');
throw new Error(err.response?.data?.message || err.message || '上传文件失败');
}
};
// 重新解析文件
const handleReparse = async (docIds: string[]) => {
try {
await knowledgeService.runDocument({ doc_ids: docIds });
fetchFileList(); // 刷新列表
await runDocuments(docIds);
refreshFiles(); // 刷新列表
} catch (err: any) {
setError(err.response?.data?.message || err.message || '重新解析失败');
}
@@ -194,22 +119,19 @@ function KnowledgeBaseDetail() {
// 初始化数据
useEffect(() => {
fetchKnowledgeDetail();
// fetchFileList();
}, [id]);
// 搜索文件
useEffect(() => {
const timer = setTimeout(() => {
fetchFileList();
setKeywords(searchKeyword);
}, 500);
return () => clearTimeout(timer);
}, [searchKeyword]);
// 过滤文件
const filteredFiles = files.filter(file =>
file.name.toLowerCase().includes(searchKeyword.toLowerCase())
);
return () => clearTimeout(timer);
}, [searchKeyword, setKeywords]);
// 合并错误状态
const combinedError = error || filesError || operationError;
if (loading) {
return (
@@ -220,10 +142,10 @@ function KnowledgeBaseDetail() {
);
}
if (error) {
if (combinedError) {
return (
<Box sx={{ p: 3 }}>
<Alert severity="error">{error}</Alert>
<Alert severity="error">{combinedError}</Alert>
</Box>
);
}
@@ -252,220 +174,59 @@ function KnowledgeBaseDetail() {
</Breadcrumbs>
{/* 知识库信息卡片 */}
<Card sx={{ mb: 3 }}>
<CardContent>
<Grid container spacing={3}>
<Grid size={{xs:12,md:8}}>
<Typography variant="h4" gutterBottom>
{knowledgeBase.name}
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 2 }}>
{knowledgeBase.description || '暂无描述'}
</Typography>
<Stack direction="row" spacing={2}>
<Chip label={`${knowledgeBase.doc_num} 个文件`} variant="outlined" />
<Chip label={`${knowledgeBase.chunk_num} 个分块`} variant="outlined" />
<Chip label={`${knowledgeBase.token_num} 个令牌`} variant="outlined" />
</Stack>
</Grid>
<Grid size={{xs:12,md:4}}>
<Stack spacing={1} alignItems="flex-end">
<Typography variant="body2" color="text.secondary">
: {knowledgeBase.create_date}
</Typography>
<Typography variant="body2" color="text.secondary">
: {knowledgeBase.update_date}
</Typography>
<Typography variant="body2" color="text.secondary">
: {knowledgeBase.language}
</Typography>
</Stack>
</Grid>
</Grid>
</CardContent>
</Card>
<KnowledgeInfoCard knowledgeBase={knowledgeBase} />
{/* 文件操作栏 */}
<Paper sx={{ p: 2, mb: 2 }}>
<Stack direction="row" spacing={2} alignItems="center" justifyContent="space-between">
<TextField
placeholder="搜索文件..."
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
sx={{ minWidth: 300 }}
size="small"
/>
{/* 文件列表组件 */}
<FileListComponent
files={files}
loading={filesLoading}
searchKeyword={searchKeyword}
onSearchChange={setSearchKeyword}
onReparse={(fileIds) => handleReparse(fileIds)}
onDelete={(fileIds) => {
console.log('删除文件:', fileIds);
<Stack direction="row" spacing={1}>
<Button
variant="contained"
startIcon={<UploadIcon />}
onClick={() => setUploadDialogOpen(true)}
>
</Button>
{selectedFiles.length > 0 && (
<>
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={() => handleReparse(selectedFiles)}
>
</Button>
<Button
variant="outlined"
color="error"
startIcon={<DeleteIcon />}
onClick={() => setDeleteDialogOpen(true)}
>
({selectedFiles.length})
</Button>
</>
)}
<IconButton onClick={() => fetchFileList()}>
<RefreshIcon />
</IconButton>
</Stack>
</Stack>
</Paper>
setRowSelectionModel({
type: 'include',
ids: new Set(fileIds)
});
setDeleteDialogOpen(true);
}}
onUpload={() => setUploadDialogOpen(true)}
onRefresh={() => {
refreshFiles();
fetchKnowledgeDetail();
}}
rowSelectionModel={rowSelectionModel}
onRowSelectionModelChange={(newModel) => {
console.log('新的选择模型:', newModel);
setRowSelectionModel(newModel);
}}
/>
{/* 文件列表 */}
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell padding="checkbox">
{/* 全选复选框可以在这里添加 */}
</TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{filesLoading ? (
<TableRow>
<TableCell colSpan={9} align="center">
<LinearProgress />
<Typography sx={{ mt: 1 }}>...</Typography>
</TableCell>
</TableRow>
) : filteredFiles.length === 0 ? (
<TableRow>
<TableCell colSpan={9} align="center">
<Typography color="text.secondary">
{searchKeyword ? '没有找到匹配的文件' : '暂无文件'}
</Typography>
</TableCell>
</TableRow>
) : (
filteredFiles.map((file) => (
<TableRow key={file.id} hover>
<TableCell padding="checkbox">
{/* 文件选择复选框 */}
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getFileIcon(file.type)}
<Typography variant="body2">{file.name}</Typography>
</Box>
</TableCell>
<TableCell>
<Chip label={file.type.toUpperCase()} size="small" variant="outlined" />
</TableCell>
<TableCell>{formatFileSize(file.size)}</TableCell>
<TableCell>{file.chunk_num}</TableCell>
<TableCell>{getStatusChip(file.status, file.progress)}</TableCell>
<TableCell>{getRunStatusChip(file.run, file.progress)}</TableCell>
<TableCell>{file.create_date}</TableCell>
<TableCell>
<IconButton
size="small"
onClick={(e) => setAnchorEl(e.currentTarget)}
>
<MoreVertIcon />
</IconButton>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
{/* 文件操作菜单 */}
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
>
<MenuItem onClick={() => setAnchorEl(null)}>
<RefreshIcon sx={{ mr: 1 }} />
</MenuItem>
<MenuItem onClick={() => setAnchorEl(null)}>
<SettingsIcon sx={{ mr: 1 }} />
</MenuItem>
<MenuItem onClick={() => setAnchorEl(null)} sx={{ color: 'error.main' }}>
<DeleteIcon sx={{ mr: 1 }} />
</MenuItem>
</Menu>
{/* 浮动操作按钮 */}
<FloatingActionButtons
onTestClick={() => setTestingDialogOpen(true)}
onConfigClick={() => setConfigDialogOpen(true)}
/>
{/* 上传文件对话框 */}
<Dialog open={uploadDialogOpen} onClose={() => setUploadDialogOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle></DialogTitle>
<DialogContent>
<Box
sx={{
border: '2px dashed #ccc',
borderRadius: 2,
p: 4,
textAlign: 'center',
cursor: 'pointer',
'&:hover': {
borderColor: 'primary.main',
backgroundColor: 'action.hover',
},
}}
>
<CloudUploadIcon sx={{ fontSize: 48, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" gutterBottom>
</Typography>
<Typography variant="body2" color="text.secondary">
PDF, DOCX, TXT, MD, PNG, JPG, MP4, WAV
</Typography>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setUploadDialogOpen(false)}></Button>
<Button variant="contained"></Button>
</DialogActions>
</Dialog>
<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>
{selectedFiles.length}
{rowSelectionModel.ids.size}
</Typography>
</DialogContent>
<DialogActions>
@@ -473,6 +234,88 @@ function KnowledgeBaseDetail() {
<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>
);
}

View File

@@ -13,6 +13,7 @@ import type {
IRenameTag,
ParserConfig,
} from '@/interfaces/database/knowledge';
import type { GridRowSelectionModel } from '@mui/x-data-grid';
// 知识库相关API服务
const knowledgeService = {
@@ -63,7 +64,7 @@ const knowledgeService = {
params?: IFetchKnowledgeListRequestParams,
body?: IFetchDocumentListRequestBody
) => {
return request.post(api.get_document_list, { data: body || {}, params });
return request.post(api.get_document_list, { data: body || {} }, { params });
},
// 创建文档
@@ -73,12 +74,20 @@ const knowledgeService = {
// 上传文档
uploadDocument: (data: FormData) => {
return post(api.document_upload, data);
// 设置请求头为 multipart/form-data
const headers = {
'Content-Type': 'multipart/form-data',
};
return post(api.document_upload, data, { headers });
},
// 上传并解析文档
uploadAndParseDocument: (data: FormData) => {
return post(api.upload_and_parse, data);
// 设置请求头为 multipart/form-data
const headers = {
'Content-Type': 'multipart/form-data',
};
return post(api.upload_and_parse, data, { headers });
},
// 解析文档
@@ -92,7 +101,7 @@ const knowledgeService = {
},
// 删除文档
removeDocument: (data: { doc_ids: string[] }) => {
removeDocument: (data: { doc_id: string | Array<string | number> }) => {
return post(api.document_rm, data);
},
@@ -102,12 +111,12 @@ const knowledgeService = {
},
// 更改文档状态
changeDocumentStatus: (data: { doc_ids: string[]; status: string }) => {
changeDocumentStatus: (data: { doc_id: string | Array<string | number>; status: string }) => {
return post(api.document_change_status, data);
},
// 运行文档处理
runDocument: (data: { doc_ids: string[] }) => {
runDocument: (data: { doc_id: string | Array<string | number>}) => {
return post(api.document_run, data);
},
@@ -127,7 +136,7 @@ const knowledgeService = {
},
// 获取文档信息
getDocumentInfos: (data: { doc_ids: string[] }) => {
getDocumentInfos: (data: { doc_id: string | Array<string | number> }) => {
return post(api.document_infos, data);
},

View File

@@ -1,4 +1,6 @@
import { createTheme } from '@mui/material/styles';
import { enUS as xGridEnUS } from '@mui/x-data-grid/locales';
import { enUS as coreEnUS } from '@mui/material/locale';
// Company branding colors extracted from web_prototype CSS
const brandColors = {
@@ -159,6 +161,9 @@ export const theme = createTheme({
},
},
},
});
},
xGridEnUS,
coreEnUS,
);
export default theme;

View File

@@ -85,14 +85,19 @@ const request: AxiosInstance = axios.create({
// 请求拦截器
request.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 转换数据格式
if (config.data) {
// 转换数据格式 - 跳过FormData对象
if (config.data && !(config.data instanceof FormData)) {
config.data = convertTheKeysOfTheObjectToSnake(config.data);
}
if (config.params) {
config.params = convertTheKeysOfTheObjectToSnake(config.params);
}
// 对于FormData删除默认的Content-Type让浏览器自动设置
if (config.data instanceof FormData) {
delete config.headers['Content-Type'];
}
// 添加授权头
const authorization = getAuthorization();
if (authorization && !config.headers?.skipToken) {