agent / frontend /src /pages /teacher /KnowledgeBase.jsx
samlax12's picture
Upload 139 files
ad74240 verified
/**
* 教师端 - 知识库管理页面
* 对应原始 index.html 的 knowledge-base section
* 复用原始HTML的知识库创建和管理逻辑
*/
import { useState, useEffect } from 'react';
import { Card, Form, Input, Upload, Button, Row, Col, message, Progress, Space, Modal, List, Tag } from 'antd';
import {
UploadOutlined,
DeleteOutlined,
FileTextOutlined,
FilePdfOutlined,
FileWordOutlined,
FileImageOutlined,
PlusOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import { motion } from 'framer-motion';
import knowledgeService from '../../services/knowledgeService';
const { confirm } = Modal;
const KnowledgeBase = () => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [knowledgeBases, setKnowledgeBases] = useState([]);
const [uploadProgress, setUploadProgress] = useState(0);
const [uploading, setUploading] = useState(false);
const [fileList, setFileList] = useState([]);
useEffect(() => {
loadKnowledgeBases();
}, []);
// 加载知识库列表
const loadKnowledgeBases = async () => {
try {
setLoading(true);
const res = await knowledgeService.getKnowledgeBases();
setKnowledgeBases(res.data || []);
} catch (error) {
console.error('加载知识库失败:', error);
message.error('加载知识库失败');
} finally {
setLoading(false);
}
};
// 创建知识库
const handleCreateKnowledgeBase = async (values) => {
if (fileList.length === 0) {
message.warning('请选择要上传的文件');
return;
}
try {
setUploading(true);
// 后端只支持单文件上传,所以需要逐个上传
const firstFile = fileList[0];
const formData = new FormData();
formData.append('name', values.name);
formData.append('file', firstFile.originFileObj); // 注意:使用 'file' 不是 'files'
const res = await knowledgeService.createKnowledgeBase(formData);
if (res.success) {
message.success('知识库创建成功!');
// 如果有taskId,轮询进度
if (res.data?.task_id) {
pollProgress(res.data.task_id);
}
// 如果还有其他文件,需要逐个添加到知识库
if (fileList.length > 1) {
message.info(`正在上传剩余 ${fileList.length - 1} 个文件...`);
// 获取创建的知识库ID
const kbId = `rag_${values.name}`;
// 上传剩余文件
for (let i = 1; i < fileList.length; i++) {
const additionalFormData = new FormData();
additionalFormData.append('file', fileList[i].originFileObj);
try {
const addRes = await knowledgeService.addFileToKnowledgeBase(kbId, additionalFormData);
if (addRes.success && addRes.data?.task_id) {
pollProgress(addRes.data.task_id);
}
} catch (error) {
console.error(`上传文件 ${fileList[i].name} 失败:`, error);
message.error(`文件 ${fileList[i].name} 上传失败`);
}
}
}
form.resetFields();
setFileList([]);
setUploadProgress(0);
loadKnowledgeBases();
}
} catch (error) {
message.error('创建知识库失败');
console.error('创建知识库失败:', error);
} finally {
setUploading(false);
}
};
// 轮询文件处理进度
const pollProgress = async (taskId) => {
const maxAttempts = 60; // 最多轮询60次(约1分钟)
let attempts = 0;
const poll = async () => {
try {
const res = await knowledgeService.getProgress(taskId);
if (res.data?.status === 'completed') {
setUploadProgress(100);
message.success('文件处理完成');
loadKnowledgeBases();
return;
}
if (res.data?.status === 'failed') {
message.error('文件处理失败');
return;
}
if (res.data?.progress) {
setUploadProgress(res.data.progress);
}
attempts++;
if (attempts < maxAttempts) {
setTimeout(poll, 1000); // 每秒轮询一次
}
} catch (error) {
console.error('获取进度失败:', error);
}
};
poll();
};
// 删除知识库
const handleDeleteKnowledgeBase = (kbId, kbName) => {
confirm({
title: '确认删除',
icon: <ExclamationCircleOutlined />,
content: `确定要删除知识库 "${kbName}" 吗?此操作不可恢复。`,
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
await knowledgeService.deleteKnowledgeBase(kbId);
message.success('知识库已删除');
loadKnowledgeBases();
} catch (error) {
message.error('删除失败');
}
},
});
};
// 删除知识库中的文件
const handleDeleteFile = (indexId, fileName, kbName) => {
confirm({
title: '确认删除文件',
icon: <ExclamationCircleOutlined />,
content: `确定要从 "${kbName}" 中删除文件 "${fileName}" 吗?`,
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
await knowledgeService.deleteFile(indexId, fileName);
message.success('文件已删除');
loadKnowledgeBases();
} catch (error) {
message.error('删除文件失败');
}
},
});
};
// 添加文件到现有知识库
const handleAddFiles = async (kbId) => {
// TODO: 实现文件添加功能
message.info('添加文件功能开发中');
};
// 获取文件图标
const getFileIcon = (fileName) => {
if (!fileName || typeof fileName !== 'string') {
return <FileTextOutlined style={{ color: '#6b7280', fontSize: '20px' }} />;
}
const ext = fileName.split('.').pop().toLowerCase();
if (['txt', 'md'].includes(ext)) {
return <FileTextOutlined style={{ color: '#10b981', fontSize: '20px' }} />;
}
if (['pdf'].includes(ext)) {
return <FilePdfOutlined style={{ color: '#ef4444', fontSize: '20px' }} />;
}
if (['doc', 'docx'].includes(ext)) {
return <FileWordOutlined style={{ color: '#3b82f6', fontSize: '20px' }} />;
}
if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(ext)) {
return <FileImageOutlined style={{ color: '#f59e0b', fontSize: '20px' }} />;
}
return <FileTextOutlined style={{ color: '#6b7280', fontSize: '20px' }} />;
};
// 上传配置
const uploadProps = {
fileList,
onChange: ({ fileList: newFileList }) => setFileList(newFileList),
beforeUpload: (file) => {
const isValidType = [
'text/plain',
'text/markdown',
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'image/jpeg',
'image/png',
'image/gif',
'image/bmp',
].includes(file.type);
if (!isValidType) {
message.error('不支持的文件类型');
return Upload.LIST_IGNORE;
}
const isLt16M = file.size / 1024 / 1024 < 16;
if (!isLt16M) {
message.error('文件大小不能超过 16MB');
return Upload.LIST_IGNORE;
}
return false; // 阻止自动上传
},
multiple: true,
};
return (
<div>
<div style={{ marginBottom: '32px' }}>
<h1 style={{ fontSize: '28px', fontWeight: 600, color: '#1f2937', margin: 0 }}>
知识库管理
</h1>
<p style={{ fontSize: '14px', color: '#6b7280', marginTop: '8px' }}>
创建和管理您的教学资料知识库
</p>
</div>
<Row gutter={24}>
{/* 左侧:创建知识库 */}
<Col xs={24} lg={10}>
<Card
title={
<span style={{ fontSize: '18px', fontWeight: 600 }}>
<PlusOutlined style={{ marginRight: '8px', color: '#10b981' }} />
创建新知识库
</span>
}
style={{ border: '1px solid #e5e7eb', borderRadius: '12px' }}
>
<Form form={form} layout="vertical" onFinish={handleCreateKnowledgeBase}>
<Form.Item
label="知识库名称"
name="name"
rules={[{ required: true, message: '请输入知识库名称' }]}
>
<Input
placeholder="例如:Python编程资料"
size="large"
/>
</Form.Item>
<Form.Item label="上传文件">
<Upload {...uploadProps}>
<Button icon={<UploadOutlined />} size="large" block>
选择文件
</Button>
</Upload>
<div style={{ marginTop: '8px', fontSize: '12px', color: '#6b7280' }}>
支持格式:TXT, MD, PDF, DOC, DOCX, JPG, PNG, GIF, BMP
<br />
单个文件最大 16MB
</div>
</Form.Item>
{uploading && uploadProgress > 0 && (
<Form.Item>
<Progress
percent={uploadProgress}
status={uploadProgress === 100 ? 'success' : 'active'}
strokeColor="#10b981"
/>
</Form.Item>
)}
<Form.Item>
<Button
type="primary"
htmlType="submit"
size="large"
loading={uploading}
block
style={{ backgroundColor: '#10b981' }}
>
创建知识库
</Button>
</Form.Item>
</Form>
</Card>
</Col>
{/* 右侧:现有知识库列表 */}
<Col xs={24} lg={14}>
<Card
title={
<span style={{ fontSize: '18px', fontWeight: 600 }}>
现有知识库
</span>
}
style={{ border: '1px solid #e5e7eb', borderRadius: '12px' }}
loading={loading}
>
{knowledgeBases.length > 0 ? (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{knowledgeBases.map((kb) => (
<motion.div
key={kb.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
style={{
padding: '20px',
background: '#f8fafc',
borderRadius: '12px',
border: '1px solid #e5e7eb',
}}
>
{/* 知识库头部 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<div>
<h3 style={{ fontSize: '18px', fontWeight: 600, margin: 0, color: '#1f2937' }}>
{kb.name}
</h3>
<div style={{ fontSize: '12px', color: '#6b7280', marginTop: '4px' }}>
知识库ID: {kb.id}
</div>
</div>
<Space>
<Button
type="primary"
size="small"
icon={<PlusOutlined />}
onClick={() => handleAddFiles(kb.id)}
>
添加文件
</Button>
<Button
danger
size="small"
icon={<DeleteOutlined />}
onClick={() => handleDeleteKnowledgeBase(kb.id, kb.name)}
>
删除
</Button>
</Space>
</div>
{/* 文件列表 */}
{kb.files && kb.files.length > 0 ? (
<div
style={{
background: 'white',
borderRadius: '8px',
padding: '12px',
border: '1px solid #e5e7eb',
}}
>
<List
size="small"
dataSource={kb.files}
renderItem={(file) => (
<List.Item
actions={[
<Button
type="text"
danger
size="small"
icon={<DeleteOutlined />}
onClick={() => handleDeleteFile(kb.id, file.name, kb.name)}
>
删除
</Button>,
]}
>
<List.Item.Meta
avatar={getFileIcon(file.name)}
title={
<span style={{ fontSize: '14px', color: '#1f2937' }}>
{file.name}
</span>
}
description={
<span style={{ fontSize: '12px', color: '#6b7280' }}>
{file.size || '未知大小'}
</span>
}
/>
</List.Item>
)}
/>
</div>
) : (
<div
style={{
textAlign: 'center',
padding: '20px',
color: '#9ca3af',
background: 'white',
borderRadius: '8px',
border: '1px dashed #d1d5db',
}}
>
<FileTextOutlined style={{ fontSize: '32px', marginBottom: '8px' }} />
<div>暂无文件</div>
</div>
)}
{/* 知识库元数据 */}
<div style={{ marginTop: '12px', display: 'flex', gap: '12px' }}>
<Tag color="blue">{kb.fileCount || 0} 个文件</Tag>
<Tag color="green">索引: {kb.id}</Tag>
</div>
</motion.div>
))}
</Space>
) : (
<div
style={{
textAlign: 'center',
padding: '60px 20px',
color: '#9ca3af',
}}
>
<FileTextOutlined style={{ fontSize: '64px', marginBottom: '16px', opacity: 0.5 }} />
<div style={{ fontSize: '16px', marginBottom: '8px' }}>
还没有创建任何知识库
</div>
<div style={{ fontSize: '14px' }}>
在左侧创建您的第一个知识库
</div>
</div>
)}
</Card>
</Col>
</Row>
</div>
);
};
export default KnowledgeBase;