| |
| |
| |
| |
| |
| 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); |
|
|
| const res = await knowledgeService.createKnowledgeBase(formData); |
|
|
| if (res.success) { |
| message.success('知识库创建成功!'); |
|
|
| |
| if (res.data?.task_id) { |
| pollProgress(res.data.task_id); |
| } |
|
|
| |
| if (fileList.length > 1) { |
| message.info(`正在上传剩余 ${fileList.length - 1} 个文件...`); |
| |
| 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; |
| 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) => { |
| |
| 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; |
|
|