feat(frontend): Day 21 knowledge base management frontend completed

This commit is contained in:
AI Clinical Dev Team
2025-10-11 12:48:26 +08:00
parent 5bacdc1768
commit 186ae55302
9 changed files with 1459 additions and 221 deletions

View File

@@ -1,6 +1,6 @@
import { useParams } from 'react-router-dom'
import { useState, useEffect } from 'react'
import { Card, Typography, Space, Alert, Spin, message } from 'antd'
import { Space, Alert, Spin, message } from 'antd'
import { RobotOutlined } from '@ant-design/icons'
import { agentApi, type AgentConfig } from '../api/agentApi'
import conversationApi, { type Conversation, type Message } from '../api/conversationApi'
@@ -9,8 +9,6 @@ import MessageInput from '../components/chat/MessageInput'
import ModelSelector, { type ModelType } from '../components/chat/ModelSelector'
import { useProjectStore } from '../stores/useProjectStore'
const { Title, Paragraph } = Typography
const AgentChatPage = () => {
const { agentId } = useParams()
const { currentProject } = useProjectStore()
@@ -71,10 +69,8 @@ const AgentChatPage = () => {
title: `${agent.name}的对话`,
})
if (response.success && response.data) {
setConversation(response.data)
setMessages([])
}
setConversation(response.data.data || null)
setMessages([])
} catch (err) {
console.error('Failed to create conversation:', err)
message.error('创建对话失败')

View File

@@ -1,225 +1,281 @@
import {
Card,
Button,
Space,
Table,
Tag,
Progress,
Alert,
} from 'antd'
import {
PlusOutlined,
FolderOutlined,
FileTextOutlined,
UploadOutlined,
DeleteOutlined,
SyncOutlined,
} from '@ant-design/icons'
import React, { useEffect, useState } from 'react';
import { Card, Tabs, Button, message } from 'antd';
import { ArrowLeftOutlined } from '@ant-design/icons';
import { useKnowledgeBaseStore } from '../stores/useKnowledgeBaseStore';
import KnowledgeBaseList from '../components/knowledge/KnowledgeBaseList';
import CreateKBDialog from '../components/knowledge/CreateKBDialog';
import EditKBDialog from '../components/knowledge/EditKBDialog';
import DocumentUpload from '../components/knowledge/DocumentUpload';
import DocumentList from '../components/knowledge/DocumentList';
import type { KnowledgeBase, Document } from '../api/knowledgeBaseApi';
const KnowledgePage = () => {
// 模拟知识库数据
const mockKnowledgeBases = [
{
id: '1',
name: '心血管疾病研究文献库',
fileCount: 15,
totalSize: '45MB',
createdAt: '2025-10-05',
status: 'ready',
},
]
const { TabPane } = Tabs;
// 模拟文档数据
const mockDocuments = [
{
id: '1',
name: '高血压治疗指南2024.pdf',
size: '3.2MB',
uploadedAt: '2025-10-05 14:30',
status: 'processed',
},
{
id: '2',
name: '心血管疾病流行病学研究.docx',
size: '1.8MB',
uploadedAt: '2025-10-05 15:20',
status: 'processing',
},
]
const KnowledgePage: React.FC = () => {
const {
knowledgeBases,
currentKb,
documents,
loading,
error,
fetchKnowledgeBases,
fetchKnowledgeBaseById,
createKnowledgeBase,
updateKnowledgeBase,
deleteKnowledgeBase,
fetchDocuments,
deleteDocument,
reprocessDocument,
setCurrentKb,
clearError,
} = useKnowledgeBaseStore();
const kbColumns = [
{
title: '知识库名称',
dataIndex: 'name',
key: 'name',
render: (text: string) => (
<Space>
<FolderOutlined style={{ color: '#1890ff' }} />
<span>{text}</span>
</Space>
),
},
{
title: '文档数量',
dataIndex: 'fileCount',
key: 'fileCount',
render: (count: number) => `${count} / 50`,
},
{
title: '总大小',
dataIndex: 'totalSize',
key: 'totalSize',
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Tag color={status === 'ready' ? 'green' : 'orange'}>
{status === 'ready' ? '就绪' : '处理中'}
</Tag>
),
},
{
title: '操作',
key: 'actions',
render: () => (
<Space>
<Button size="small" icon={<UploadOutlined />} disabled>
</Button>
<Button size="small" danger icon={<DeleteOutlined />} disabled>
</Button>
</Space>
),
},
]
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [editingKb, setEditingKb] = useState<KnowledgeBase | null>(null);
const docColumns = [
{
title: '文件名',
dataIndex: 'name',
key: 'name',
render: (text: string) => (
<Space>
<FileTextOutlined />
<span>{text}</span>
</Space>
),
},
{
title: '大小',
dataIndex: 'size',
key: 'size',
},
{
title: '上传时间',
dataIndex: 'uploadedAt',
key: 'uploadedAt',
},
{
title: '处理状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => {
if (status === 'processed') {
return <Tag color="green"></Tag>
}
return (
<Space>
<Tag color="processing" icon={<SyncOutlined spin />}>
</Tag>
<Progress percent={65} size="small" style={{ width: 100 }} />
</Space>
)
},
},
{
title: '操作',
key: 'actions',
render: () => (
<Button size="small" danger icon={<DeleteOutlined />} disabled>
</Button>
),
},
]
// 初始加载知识库列表
useEffect(() => {
fetchKnowledgeBases();
}, []);
// 显示错误提示
useEffect(() => {
if (error) {
message.error(error);
clearError();
}
}, [error]);
// 创建知识库
const handleCreate = async (name: string, description?: string) => {
await createKnowledgeBase(name, description);
};
// 编辑知识库
const handleEdit = (kb: KnowledgeBase) => {
setEditingKb(kb);
setEditDialogOpen(true);
};
// 更新知识库
const handleUpdate = async (id: string, name: string, description?: string) => {
await updateKnowledgeBase(id, name, description);
};
// 删除知识库
const handleDelete = async (kb: KnowledgeBase) => {
try {
await deleteKnowledgeBase(kb.id);
message.success('知识库删除成功');
// 如果删除的是当前打开的知识库,返回列表
if (currentKb?.id === kb.id) {
setCurrentKb(null);
}
} catch (error: any) {
message.error(error.message || '删除失败');
}
};
// 选择知识库,查看详情
const handleSelectKb = async (kb: KnowledgeBase) => {
setCurrentKb(kb);
await fetchKnowledgeBaseById(kb.id);
};
// 返回知识库列表
const handleBackToList = () => {
setCurrentKb(null);
};
// 上传成功后刷新文档列表
const handleUploadSuccess = async () => {
if (currentKb) {
await fetchDocuments(currentKb.id);
}
};
// 删除文档
const handleDeleteDocument = async (doc: Document) => {
try {
await deleteDocument(doc.id);
message.success('文档删除成功');
} catch (error: any) {
message.error(error.message || '删除失败');
}
};
// 重新处理文档
const handleReprocessDocument = async (doc: Document) => {
try {
await reprocessDocument(doc.id);
message.success('已开始重新处理');
} catch (error: any) {
message.error(error.message || '操作失败');
}
};
// 轮询文档状态每5秒
useEffect(() => {
if (!currentKb) return;
const hasProcessing = documents.some(doc =>
['uploading', 'parsing', 'indexing'].includes(doc.status)
);
if (!hasProcessing) return;
const interval = setInterval(() => {
fetchDocuments(currentKb.id);
}, 5000);
return () => clearInterval(interval);
}, [currentKb, documents]);
return (
<div style={{ padding: '24px' }}>
{/* 提示信息 */}
<Alert
message="知识库功能说明"
description="每个用户最多可创建3个知识库每个知识库最多上传50个文件支持PDF、DOCX格式。在对话时可以@知识库让AI基于您的文献进行回答。"
type="info"
showIcon
closable
style={{ marginBottom: 16 }}
/>
<div style={{ padding: 24, height: '100%', overflow: 'auto' }}>
{!currentKb ? (
// 知识库列表视图
<>
<KnowledgeBaseList
knowledgeBases={knowledgeBases}
loading={loading}
onCreateClick={() => setCreateDialogOpen(true)}
onEditClick={handleEdit}
onDeleteClick={handleDelete}
onSelectClick={handleSelectKb}
/>
<Alert
message="功能开发中"
description="知识库管理功能正在开发中,敬请期待..."
type="warning"
showIcon
icon={<SyncOutlined spin />}
style={{ marginBottom: 24 }}
/>
<CreateKBDialog
open={createDialogOpen}
onCancel={() => setCreateDialogOpen(false)}
onCreate={handleCreate}
/>
{/* 知识库列表 */}
<Card
title={
<Space>
<FolderOutlined />
<span> (1/3)</span>
</Space>
}
extra={
<Button type="primary" icon={<PlusOutlined />} disabled>
</Button>
}
style={{ marginBottom: 24 }}
>
<Table
columns={kbColumns}
dataSource={mockKnowledgeBases}
rowKey="id"
pagination={false}
/>
</Card>
<EditKBDialog
open={editDialogOpen}
knowledgeBase={editingKb}
onCancel={() => {
setEditDialogOpen(false);
setEditingKb(null);
}}
onUpdate={handleUpdate}
/>
</>
) : (
// 知识库详情视图
<div>
{/* 返回按钮和标题 */}
<div style={{ marginBottom: 24 }}>
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={handleBackToList}
style={{ marginBottom: 16 }}
>
</Button>
<Card style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h2 style={{ margin: 0, marginBottom: 8 }}>{currentKb.name}</h2>
<p style={{ margin: 0, color: '#8c8c8c' }}>
{currentKb.description || '暂无描述'}
</p>
</div>
<Button
onClick={() => handleEdit(currentKb)}
>
</Button>
</div>
</Card>
</div>
{/* 文档列表 */}
<Card
title={
<Space>
<FileTextOutlined />
<span></span>
</Space>
}
extra={
<Button icon={<UploadOutlined />} disabled>
</Button>
}
>
<Table
columns={docColumns}
dataSource={mockDocuments}
rowKey="id"
pagination={false}
/>
</Card>
{/* 文档管理标签页 */}
<Tabs defaultActiveKey="documents">
<TabPane tab={`文档管理 (${documents.length})`} key="documents">
<div style={{ marginBottom: 24 }}>
<DocumentUpload
kbId={currentKb.id}
onUploadSuccess={handleUploadSuccess}
disabled={loading}
maxDocuments={50}
currentDocumentCount={documents.length}
/>
</div>
<Card title="文档列表">
<DocumentList
documents={documents}
loading={loading}
onDelete={handleDeleteDocument}
onReprocess={handleReprocessDocument}
/>
</Card>
</TabPane>
<TabPane tab="统计信息" key="stats">
<Card>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 16 }}>
<div style={{ textAlign: 'center', padding: 16, background: '#f0f5ff', borderRadius: 8 }}>
<div style={{ fontSize: 32, fontWeight: 'bold', color: '#1890ff' }}>
{documents.length}
</div>
<div style={{ marginTop: 8, color: '#595959' }}></div>
</div>
<div style={{ textAlign: 'center', padding: 16, background: '#f6ffed', borderRadius: 8 }}>
<div style={{ fontSize: 32, fontWeight: 'bold', color: '#52c41a' }}>
{documents.filter(d => d.status === 'completed').length}
</div>
<div style={{ marginTop: 8, color: '#595959' }}></div>
</div>
<div style={{ textAlign: 'center', padding: 16, background: '#fffbe6', borderRadius: 8 }}>
<div style={{ fontSize: 32, fontWeight: 'bold', color: '#faad14' }}>
{documents.filter(d => ['uploading', 'parsing', 'indexing'].includes(d.status)).length}
</div>
<div style={{ marginTop: 8, color: '#595959' }}></div>
</div>
<div style={{ textAlign: 'center', padding: 16, background: '#fff1f0', borderRadius: 8 }}>
<div style={{ fontSize: 32, fontWeight: 'bold', color: '#ff4d4f' }}>
{documents.filter(d => d.status === 'error').length}
</div>
<div style={{ marginTop: 8, color: '#595959' }}></div>
</div>
<div style={{ textAlign: 'center', padding: 16, background: '#f0f0f0', borderRadius: 8 }}>
<div style={{ fontSize: 24, fontWeight: 'bold', color: '#262626' }}>
{documents.reduce((sum, d) => sum + (d.tokensCount || 0), 0).toLocaleString()}
</div>
<div style={{ marginTop: 8, color: '#595959' }}>Token数</div>
</div>
<div style={{ textAlign: 'center', padding: 16, background: '#f0f0f0', borderRadius: 8 }}>
<div style={{ fontSize: 24, fontWeight: 'bold', color: '#262626' }}>
{documents.reduce((sum, d) => sum + (d.segmentsCount || 0), 0).toLocaleString()}
</div>
<div style={{ marginTop: 8, color: '#595959' }}></div>
</div>
</div>
</Card>
</TabPane>
</Tabs>
<EditKBDialog
open={editDialogOpen}
knowledgeBase={currentKb}
onCancel={() => setEditDialogOpen(false)}
onUpdate={handleUpdate}
/>
</div>
)}
</div>
)
}
export default KnowledgePage
);
};
export default KnowledgePage;