feat(knowledge): add knowledge base management with dialog system
- Implement knowledge base list, create, and detail pages - Add dialog provider and components for confirmation and alerts - Include knowledge card and grid view components - Enhance header with user menu and logout functionality - Implement knowledge operations hooks for CRUD operations
This commit is contained in:
@@ -29,7 +29,7 @@ import {
|
||||
CheckCircle as CheckCircleIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import KnowledgeGridView from '@/components/KnowledgeGridView';
|
||||
import KnowledgeGridView from '@/components/knowledge/KnowledgeGridView';
|
||||
import UserDataDebug from '@/components/UserDataDebug';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
|
||||
503
src/pages/knowledge/create.tsx
Normal file
503
src/pages/knowledge/create.tsx
Normal file
@@ -0,0 +1,503 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Card,
|
||||
CardContent,
|
||||
TextField,
|
||||
Button,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Alert,
|
||||
Grid,
|
||||
Divider,
|
||||
CircularProgress,
|
||||
Stepper,
|
||||
Step,
|
||||
StepLabel,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Slider,
|
||||
Chip,
|
||||
Stack,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
ArrowBack as ArrowBackIcon,
|
||||
Save as SaveIcon,
|
||||
Settings as SettingsIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
SkipNext as SkipNextIcon,
|
||||
} from '@mui/icons-material';
|
||||
import knowledgeService from '@/services/knowledge_service';
|
||||
|
||||
// 基础信息表单数据
|
||||
interface BasicFormData {
|
||||
name: string;
|
||||
description: string;
|
||||
permission: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
// 配置设置表单数据
|
||||
interface ConfigFormData {
|
||||
parser_id: string;
|
||||
embd_id: string;
|
||||
chunk_token_num: number;
|
||||
layout_recognize: string;
|
||||
delimiter: string;
|
||||
auto_keywords: number;
|
||||
auto_questions: number;
|
||||
html4excel: boolean;
|
||||
topn_tags: number;
|
||||
use_raptor: boolean;
|
||||
use_graphrag: boolean;
|
||||
graphrag_method: string;
|
||||
pagerank: number;
|
||||
}
|
||||
|
||||
const steps = ['基础信息', '配置设置'];
|
||||
|
||||
function KnowledgeBaseCreate() {
|
||||
const navigate = useNavigate();
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [success, setSuccess] = useState<string>('');
|
||||
const [createdKbId, setCreatedKbId] = useState<string>('');
|
||||
|
||||
// 基础信息表单
|
||||
const basicForm = useForm<BasicFormData>({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
description: '',
|
||||
permission: 'me',
|
||||
avatar: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// 配置设置表单
|
||||
const configForm = useForm<ConfigFormData>({
|
||||
defaultValues: {
|
||||
parser_id: 'naive',
|
||||
embd_id: 'text-embedding-v3@Tongyi-Qianwen',
|
||||
chunk_token_num: 512,
|
||||
layout_recognize: 'DeepDOC',
|
||||
delimiter: '\n',
|
||||
auto_keywords: 0,
|
||||
auto_questions: 0,
|
||||
html4excel: false,
|
||||
topn_tags: 3,
|
||||
use_raptor: false,
|
||||
use_graphrag: false,
|
||||
graphrag_method: 'light',
|
||||
pagerank: 0,
|
||||
},
|
||||
});
|
||||
|
||||
// 第一步:创建基础知识库
|
||||
const handleBasicSubmit = async (data: BasicFormData) => {
|
||||
setIsSubmitting(true);
|
||||
setError('');
|
||||
setSuccess('');
|
||||
|
||||
try {
|
||||
// 只发送基础字段到 create API
|
||||
const basicData = {
|
||||
name: data.name,
|
||||
avatar: data.avatar,
|
||||
description: data.description,
|
||||
permission: data.permission,
|
||||
};
|
||||
|
||||
const response = await knowledgeService.createKnowledge(basicData);
|
||||
|
||||
// 假设 API 返回包含 kb_id 的响应
|
||||
const kbId = response.data?.kb_id;
|
||||
setCreatedKbId(kbId);
|
||||
|
||||
setSuccess('知识库创建成功!您可以继续配置解析设置,或直接跳过。');
|
||||
setActiveStep(1); // 进入第二步
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('创建知识库失败:', err);
|
||||
setError(err.response?.data?.message || err.message || '创建知识库失败,请重试');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 第二步:更新配置设置
|
||||
const handleConfigSubmit = async (data: ConfigFormData) => {
|
||||
if (!createdKbId) {
|
||||
setError('未找到知识库ID,请重新创建');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
// 构建 update API 的数据结构
|
||||
const updateData:any = {
|
||||
kb_id: createdKbId,
|
||||
name: basicForm.getValues('name'),
|
||||
description: basicForm.getValues('description'),
|
||||
permission: basicForm.getValues('permission'),
|
||||
parser_id: data.parser_id,
|
||||
embd_id: data.embd_id,
|
||||
parser_config: {
|
||||
layout_recognize: data.layout_recognize,
|
||||
chunk_token_num: data.chunk_token_num,
|
||||
delimiter: data.delimiter,
|
||||
auto_keywords: data.auto_keywords,
|
||||
auto_questions: data.auto_questions,
|
||||
html4excel: data.html4excel,
|
||||
topn_tags: data.topn_tags,
|
||||
raptor: {
|
||||
use_raptor: data.use_raptor,
|
||||
},
|
||||
graphrag: {
|
||||
use_graphrag: data.use_graphrag,
|
||||
entity_types: ["organization", "person", "geo", "event", "category"],
|
||||
method: data.graphrag_method,
|
||||
},
|
||||
},
|
||||
pagerank: data.pagerank,
|
||||
};
|
||||
|
||||
await knowledgeService.updateKnowledge(updateData);
|
||||
|
||||
setSuccess('知识库配置更新成功!');
|
||||
|
||||
// 延迟跳转到知识库列表页面
|
||||
setTimeout(() => {
|
||||
navigate('/knowledge');
|
||||
}, 1500);
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('更新知识库配置失败:', err);
|
||||
setError(err.response?.data?.message || err.message || '更新配置失败,请重试');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 跳过配置设置
|
||||
const handleSkipConfig = () => {
|
||||
setSuccess('知识库创建完成!');
|
||||
setTimeout(() => {
|
||||
navigate('/knowledge');
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (activeStep === 0) {
|
||||
navigate('/knowledge');
|
||||
} else {
|
||||
setActiveStep(0);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3, maxWidth: 900, mx: 'auto' }}>
|
||||
{/* 页面标题 */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
|
||||
<Button
|
||||
startIcon={<ArrowBackIcon />}
|
||||
onClick={handleBack}
|
||||
sx={{ mr: 2 }}
|
||||
>
|
||||
{activeStep === 0 ? '返回' : '上一步'}
|
||||
</Button>
|
||||
<Typography variant="h4" component="h1" fontWeight={600}>
|
||||
创建知识库
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* 步骤指示器 */}
|
||||
<Stepper activeStep={activeStep} sx={{ mb: 4 }}>
|
||||
{steps.map((label) => (
|
||||
<Step key={label}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
|
||||
{/* 表单卡片 */}
|
||||
<Card>
|
||||
<CardContent sx={{ p: 4 }}>
|
||||
{/* 错误和成功提示 */}
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<Alert severity="success" sx={{ mb: 3 }}>
|
||||
{success}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* 第一步:基础信息 */}
|
||||
{activeStep === 0 && (
|
||||
<Box component="form" onSubmit={basicForm.handleSubmit(handleBasicSubmit)} noValidate>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
基础信息
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{/* 知识库名称 */}
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="知识库名称"
|
||||
placeholder="请输入知识库名称"
|
||||
{...basicForm.register('name', {
|
||||
required: '请输入知识库名称',
|
||||
minLength: {
|
||||
value: 2,
|
||||
message: '知识库名称至少需要2个字符',
|
||||
},
|
||||
maxLength: {
|
||||
value: 50,
|
||||
message: '知识库名称不能超过50个字符',
|
||||
},
|
||||
})}
|
||||
error={!!basicForm.formState.errors.name}
|
||||
helperText={basicForm.formState.errors.name?.message}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* 描述 */}
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
label="描述"
|
||||
placeholder="请输入知识库描述"
|
||||
{...basicForm.register('description', {
|
||||
maxLength: {
|
||||
value: 500,
|
||||
message: '描述不能超过500个字符',
|
||||
},
|
||||
})}
|
||||
error={!!basicForm.formState.errors.description}
|
||||
helperText={basicForm.formState.errors.description?.message}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* 权限设置 */}
|
||||
<Grid size={{xs:12, sm:6}}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>权限设置</InputLabel>
|
||||
<Select
|
||||
label="权限设置"
|
||||
{...basicForm.register('permission')}
|
||||
>
|
||||
<MenuItem value="me">仅自己</MenuItem>
|
||||
<MenuItem value="team">团队可见</MenuItem>
|
||||
<MenuItem value="public">公开</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<Grid size={{xs:12}}>
|
||||
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'flex-end', mt: 3 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => navigate('/knowledge')}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
startIcon={isSubmitting ? <CircularProgress size={20} /> : <SaveIcon />}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? '创建中...' : '创建知识库'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 第二步:配置设置 */}
|
||||
{activeStep === 1 && (
|
||||
<Box component="form" onSubmit={configForm.handleSubmit(handleConfigSubmit)} noValidate>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<CheckCircleIcon color="success" sx={{ mr: 1 }} />
|
||||
<Typography variant="h6">
|
||||
知识库已创建成功,现在可以配置解析设置
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
您可以现在配置这些设置,也可以稍后在知识库详情页面中修改
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{/* 解析器设置 */}
|
||||
<Grid size={{xs:12}}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
解析器设置
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{xs:12,sm:6}}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>解析器类型</InputLabel>
|
||||
<Select
|
||||
label="解析器类型"
|
||||
{...configForm.register('parser_id')}
|
||||
>
|
||||
<MenuItem value="naive">基础解析器</MenuItem>
|
||||
<MenuItem value="advanced">高级解析器</MenuItem>
|
||||
<MenuItem value="custom">自定义解析器</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{xs:12,sm:6}}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>嵌入模型</InputLabel>
|
||||
<Select
|
||||
label="嵌入模型"
|
||||
{...configForm.register('embd_id')}
|
||||
>
|
||||
<MenuItem value="text-embedding-v3@Tongyi-Qianwen">通义千问 v3</MenuItem>
|
||||
<MenuItem value="text-embedding-v2@Tongyi-Qianwen">通义千问 v2</MenuItem>
|
||||
<MenuItem value="openai-embedding">OpenAI Embedding</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
{/* 分块设置 */}
|
||||
<Grid size={{xs:12}}>
|
||||
<Typography variant="subtitle1" gutterBottom sx={{ mt: 2 }}>
|
||||
分块设置
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{xs:12,sm:6}}>
|
||||
<Typography gutterBottom>分块大小: {configForm.watch('chunk_token_num')}</Typography>
|
||||
<Slider
|
||||
{...configForm.register('chunk_token_num')}
|
||||
value={configForm.watch('chunk_token_num')}
|
||||
onChange={(_, value) => configForm.setValue('chunk_token_num', value as number)}
|
||||
min={128}
|
||||
max={2048}
|
||||
step={128}
|
||||
marks={[
|
||||
{ value: 128, label: '128' },
|
||||
{ value: 512, label: '512' },
|
||||
{ value: 1024, label: '1024' },
|
||||
{ value: 2048, label: '2048' },
|
||||
]}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{xs:12,sm:6}}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>布局识别</InputLabel>
|
||||
<Select
|
||||
label="布局识别"
|
||||
{...configForm.register('layout_recognize')}
|
||||
>
|
||||
<MenuItem value="DeepDOC">DeepDOC</MenuItem>
|
||||
<MenuItem value="OCR">OCR</MenuItem>
|
||||
<MenuItem value="None">无</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
{/* 高级功能 */}
|
||||
<Grid size={{xs:12}}>
|
||||
<Typography variant="subtitle1" gutterBottom sx={{ mt: 2 }}>
|
||||
高级功能
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{xs:12,sm:6}}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
{...configForm.register('use_raptor')}
|
||||
checked={configForm.watch('use_raptor')}
|
||||
onChange={(e) => configForm.setValue('use_raptor', e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="启用 Raptor"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{xs:12,sm:6}}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
{...configForm.register('use_graphrag')}
|
||||
checked={configForm.watch('use_graphrag')}
|
||||
onChange={(e) => configForm.setValue('use_graphrag', e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="启用 GraphRAG"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{configForm.watch('use_graphrag') && (
|
||||
<Grid size={{xs:12,sm:6}}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>GraphRAG 方法</InputLabel>
|
||||
<Select
|
||||
label="GraphRAG 方法"
|
||||
{...configForm.register('graphrag_method')}
|
||||
>
|
||||
<MenuItem value="light">轻量级</MenuItem>
|
||||
<MenuItem value="standard">标准</MenuItem>
|
||||
<MenuItem value="advanced">高级</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<Grid size={{xs:12}}>
|
||||
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'flex-end', mt: 3 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<SkipNextIcon />}
|
||||
onClick={handleSkipConfig}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
跳过配置
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
startIcon={isSubmitting ? <CircularProgress size={20} /> : <SettingsIcon />}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? '配置中...' : '完成配置'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default KnowledgeBaseCreate;
|
||||
480
src/pages/knowledge/detail.tsx
Normal file
480
src/pages/knowledge/detail.tsx
Normal file
@@ -0,0 +1,480 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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,
|
||||
Breadcrumbs,
|
||||
Link,
|
||||
} 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 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" />;
|
||||
}
|
||||
};
|
||||
|
||||
function KnowledgeBaseDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 状态管理
|
||||
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 [uploadDialogOpen, setUploadDialogOpen] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
// 获取知识库详情
|
||||
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 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 || '获取文件列表失败');
|
||||
// }
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || err.message || '获取文件列表失败');
|
||||
} finally {
|
||||
setFilesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除文件
|
||||
const handleDeleteFiles = async () => {
|
||||
if (selectedFiles.length === 0) return;
|
||||
|
||||
try {
|
||||
await knowledgeService.removeDocument({ doc_ids: selectedFiles });
|
||||
setSelectedFiles([]);
|
||||
setDeleteDialogOpen(false);
|
||||
fetchFileList(); // 刷新列表
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || err.message || '删除文件失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 重新解析文件
|
||||
const handleReparse = async (docIds: string[]) => {
|
||||
try {
|
||||
await knowledgeService.runDocument({ doc_ids: docIds });
|
||||
fetchFileList(); // 刷新列表
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || err.message || '重新解析失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化数据
|
||||
useEffect(() => {
|
||||
fetchKnowledgeDetail();
|
||||
// fetchFileList();
|
||||
}, [id]);
|
||||
|
||||
// 搜索文件
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
fetchFileList();
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchKeyword]);
|
||||
|
||||
// 过滤文件
|
||||
const filteredFiles = files.filter(file =>
|
||||
file.name.toLowerCase().includes(searchKeyword.toLowerCase())
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<LinearProgress />
|
||||
<Typography sx={{ mt: 2 }}>加载中...</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Alert severity="error">{error}</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>
|
||||
|
||||
{/* 知识库信息卡片 */}
|
||||
<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>
|
||||
|
||||
{/* 文件操作栏 */}
|
||||
<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"
|
||||
/>
|
||||
|
||||
<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>
|
||||
|
||||
{/* 文件列表 */}
|
||||
<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>
|
||||
|
||||
{/* 上传文件对话框 */}
|
||||
<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>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
|
||||
<DialogTitle>确认删除</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
确定要删除选中的 {selectedFiles.length} 个文件吗?此操作不可撤销。
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDeleteDialogOpen(false)}>取消</Button>
|
||||
<Button color="error" onClick={handleDeleteFiles}>删除</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default KnowledgeBaseDetail;
|
||||
@@ -19,13 +19,16 @@ import {
|
||||
Refresh as RefreshIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useKnowledgeList } from '@/hooks/knowledge_hooks';
|
||||
import { useKnowledgeList, useKnowledgeOperations } from '@/hooks/knowledge_hooks';
|
||||
import { useUserData } from '@/hooks/useUserData';
|
||||
import KnowledgeGridView from '@/components/KnowledgeGridView';
|
||||
import KnowledgeGridView from '@/components/knowledge/KnowledgeGridView';
|
||||
import type { IKnowledge } from '@/interfaces/database/knowledge';
|
||||
import { useDialog } from '@/hooks/useDialog';
|
||||
|
||||
const KnowledgeBaseList: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {deleteKnowledge} = useKnowledgeOperations();
|
||||
|
||||
// 搜索和筛选状态
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
@@ -78,22 +81,32 @@ const KnowledgeBaseList: React.FC = () => {
|
||||
navigate('/knowledge/create');
|
||||
}, [navigate]);
|
||||
|
||||
// 处理编辑知识库
|
||||
const handleEditKnowledge = useCallback((kb: IKnowledge) => {
|
||||
navigate(`/knowledge/${kb.id}/edit`);
|
||||
}, [navigate]);
|
||||
const dialog = useDialog();
|
||||
|
||||
// 处理删除知识库
|
||||
const handleDeleteKnowledge = useCallback(async (kb: IKnowledge) => {
|
||||
// 需要确认删除
|
||||
dialog.confirm({
|
||||
title: '确认删除',
|
||||
content: `是否确认删除知识库 ${kb.name}?`,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await deleteKnowledge(kb.id);
|
||||
// 删除成功后刷新列表
|
||||
refresh();
|
||||
} catch (err) {
|
||||
console.error('Failed to delete knowledge:', err);
|
||||
// 可以添加错误提示
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [deleteKnowledge, refresh, dialog]);
|
||||
|
||||
// 处理查看知识库详情
|
||||
const handleViewKnowledge = useCallback((kb: IKnowledge) => {
|
||||
navigate(`/knowledge/${kb.id}`);
|
||||
}, [navigate]);
|
||||
|
||||
// 处理删除知识库
|
||||
const handleDeleteKnowledge = useCallback((kb: IKnowledge) => {
|
||||
// TODO: 实现删除逻辑
|
||||
console.log('删除知识库:', kb.id);
|
||||
}, []);
|
||||
|
||||
// 根据团队筛选过滤知识库
|
||||
const filteredKnowledgeBases = useMemo(() => {
|
||||
if (!knowledgeBases) return [];
|
||||
@@ -226,9 +239,8 @@ const KnowledgeBaseList: React.FC = () => {
|
||||
<>
|
||||
<KnowledgeGridView
|
||||
knowledgeBases={currentPageData}
|
||||
onEdit={handleEditKnowledge}
|
||||
onDelete={handleDeleteKnowledge}
|
||||
onView={handleViewKnowledge}
|
||||
onDelete={handleDeleteKnowledge}
|
||||
loading={loading}
|
||||
searchTerm={searchTerm}
|
||||
teamFilter={teamFilter}
|
||||
Reference in New Issue
Block a user