import { useCallback, useEffect, useState } from 'react'; import { Card, Table, Button, Space, Modal, Form, Input, Select, Switch, message, Popconfirm, Tag, Upload, Progress, Drawer, List, Empty, } from 'antd'; import { PlusOutlined, DeleteOutlined, UploadOutlined, SearchOutlined, FileTextOutlined, DatabaseOutlined, } from '@ant-design/icons'; import type { UploadFile } from 'antd/es/upload/interface'; import { knowledgeV2Api, type KnowledgeBase, type KnowledgeDocument, type SearchHit, type CreateKnowledgeBaseReq, } from '../../api/ai/knowledgeV2'; const KB_TYPES = [ { label: '临床指南', value: 'clinical_guide' }, { label: '操作规程', value: 'sop' }, { label: 'FAQ', value: 'faq' }, { label: '产品知识', value: 'product' }, { label: '通用', value: 'general' }, ]; export default function KnowledgeV2Page() { const [kbs, setKbs] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); const [createModalOpen, setCreateModalOpen] = useState(false); const [editKb, setEditKb] = useState(null); const [form] = Form.useForm(); // Document drawer state const [docDrawerKb, setDocDrawerKb] = useState(null); const [docs, setDocs] = useState([]); const [docsLoading, setDocsLoading] = useState(false); const [uploadModalOpen, setUploadModalOpen] = useState(false); const [uploadKbId, setUploadKbId] = useState(''); const [fileList, setFileList] = useState([]); // Hit test state const [hitTestKb, setHitTestKb] = useState(null); const [hitTestQuery, setHitTestQuery] = useState(''); const [hitResults, setHitResults] = useState([]); const [hitTestLoading, setHitTestLoading] = useState(false); const loadKbs = useCallback(async () => { setLoading(true); try { const res = await knowledgeV2Api.listKnowledgeBases({ page, page_size: 20, }); setKbs(res.data); setTotal(res.total); } catch { message.error('加载知识库列表失败'); } finally { setLoading(false); } }, [page]); useEffect(() => { loadKbs(); }, [loadKbs]); const handleCreate = async () => { try { const values = await form.validateFields(); const req: CreateKnowledgeBaseReq = { name: values.name, kb_type: values.kb_type, description: values.description, is_enabled: values.is_enabled ?? true, }; await knowledgeV2Api.createKnowledgeBase(req); message.success('知识库创建成功'); setCreateModalOpen(false); form.resetFields(); loadKbs(); } catch { // validation error } }; const handleUpdate = async () => { if (!editKb) return; try { const values = await form.validateFields(); await knowledgeV2Api.updateKnowledgeBase(editKb.id, { name: values.name, kb_type: values.kb_type, description: values.description, is_enabled: values.is_enabled, }); message.success('知识库更新成功'); setEditKb(null); form.resetFields(); loadKbs(); } catch { // validation error } }; const handleDelete = async (id: string) => { try { await knowledgeV2Api.deleteKnowledgeBase(id); message.success('知识库已删除'); loadKbs(); } catch { message.error('删除失败'); } }; const loadDocuments = async (kb: KnowledgeBase) => { setDocDrawerKb(kb); setDocsLoading(true); try { const res = await knowledgeV2Api.listDocuments(kb.id, { page: 1, page_size: 50, }); setDocs(res.data); } catch { message.error('加载文档列表失败'); } finally { setDocsLoading(false); } }; const handleUpload = async () => { if (!uploadKbId || fileList.length === 0) return; try { const file = fileList[0].originFileObj; if (!file) return; await knowledgeV2Api.uploadDocument(uploadKbId, file); message.success('文档上传成功,正在处理...'); setUploadModalOpen(false); setFileList([]); if (docDrawerKb) { loadDocuments(docDrawerKb); } } catch { message.error('上传失败'); } }; const handleDeleteDoc = async (kbId: string, docId: string) => { try { await knowledgeV2Api.deleteDocument(kbId, docId); message.success('文档已删除'); if (docDrawerKb) { loadDocuments(docDrawerKb); } } catch { message.error('删除失败'); } }; const handleHitTest = async () => { if (!hitTestKb || !hitTestQuery.trim()) return; setHitTestLoading(true); try { const res = await knowledgeV2Api.hitTest(hitTestKb.id, hitTestQuery, 5); setHitResults(res.hits); } catch { message.error('搜索失败'); setHitResults([]); } finally { setHitTestLoading(false); } }; const statusTag = (status: string) => { const map: Record = { pending: { color: 'default', label: '待处理' }, processing: { color: 'processing', label: '处理中' }, completed: { color: 'success', label: '已完成' }, failed: { color: 'error', label: '失败' }, }; const info = map[status] || { color: 'default', label: status }; return {info.label}; }; const kbColumns = [ { title: '名称', dataIndex: 'name', key: 'name', render: (name: string, record: KnowledgeBase) => ( ), }, { title: '类型', dataIndex: 'kb_type', key: 'kb_type', render: (type: string) => { const found = KB_TYPES.find((t) => t.value === type); return found?.label || type; }, }, { title: '文档数', dataIndex: 'document_count', key: 'document_count', width: 90, }, { title: '切片数', dataIndex: 'chunk_count', key: 'chunk_count', width: 90, }, { title: '状态', dataIndex: 'is_enabled', key: 'is_enabled', width: 80, render: (v: boolean) => ( {v ? '启用' : '禁用'} ), }, { title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 170, render: (v: string) => new Date(v).toLocaleString('zh-CN'), }, { title: '操作', key: 'actions', width: 240, render: (_: unknown, record: KnowledgeBase) => ( handleDelete(record.id)} > }} renderItem={(item) => ( } title={ {item.doc_title} 切片 #{item.chunk_index} 相似度 {(item.similarity * 100).toFixed(1)}% } description={
{item.content}
} />
)} /> ); }