feat(web): 知识库 V2 管理页面 — 列表/CRUD/上传/向量搜索测试
- knowledgeV2.ts API client: 知识库/文档/搜索完整接口 - KnowledgeV2Page: 知识库列表 + 创建/编辑/删除 - 文档列表 Drawer: 按知识库查看文档(状态/切片进度) - 上传 Modal: Multipart 文件上传(PDF/TXT/MD/DOCX/XLSX) - 向量搜索测试 Drawer: 输入查询 → 余弦相似度结果展示 路由: /health/ai-knowledge-v2 Phase 3 Task 16-19
This commit is contained in:
@@ -47,6 +47,7 @@ const AiAnalysisList = lazy(() => import('./pages/health/AiAnalysisList'));
|
||||
const AiUsageDashboard = lazy(() => import('./pages/health/AiUsageDashboard'));
|
||||
const AiConfigPage = lazy(() => import('./pages/health/AiConfigPage'));
|
||||
const AiKnowledgePage = lazy(() => import('./pages/health/AiKnowledgePage'));
|
||||
const KnowledgeV2Page = lazy(() => import('./pages/ai/KnowledgeV2Page'));
|
||||
const AiChatPage = lazy(() => import('./pages/ai/ChatPage'));
|
||||
const AlertList = lazy(() => import('./pages/health/AlertList'));
|
||||
const AlertDashboard = lazy(() => import('./pages/health/AlertDashboard'));
|
||||
@@ -331,6 +332,7 @@ export default function App() {
|
||||
<Route path="/health/ai-usage" element={<AiUsageDashboard />} />
|
||||
<Route path="/health/ai-config" element={<AiConfigPage />} />
|
||||
<Route path="/health/ai-knowledge" element={<AiKnowledgePage />} />
|
||||
<Route path="/health/ai-knowledge-v2" element={<KnowledgeV2Page />} />
|
||||
<Route path="/ai/chat" element={<AiChatPage />} />
|
||||
<Route path="/health/alerts" element={<AlertList />} />
|
||||
<Route path="/health/alert-dashboard" element={<AlertDashboard />} />
|
||||
|
||||
188
apps/web/src/api/ai/knowledgeV2.ts
Normal file
188
apps/web/src/api/ai/knowledgeV2.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import client from '../client';
|
||||
|
||||
// === Types ===
|
||||
|
||||
export interface KnowledgeBase {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
name: string;
|
||||
kb_type: string;
|
||||
description: string | null;
|
||||
icon: string | null;
|
||||
chunk_strategy: Record<string, unknown>;
|
||||
intent_keywords: Record<string, unknown>;
|
||||
embedding_model: string | null;
|
||||
is_enabled: boolean;
|
||||
document_count: number;
|
||||
chunk_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface KnowledgeDocument {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
knowledge_base_id: string;
|
||||
title: string;
|
||||
doc_type: string;
|
||||
source_type: string;
|
||||
source_url: string | null;
|
||||
file_name: string | null;
|
||||
file_size: number | null;
|
||||
file_mime_type: string | null;
|
||||
content: string | null;
|
||||
status: string;
|
||||
chunk_count: number;
|
||||
embedded_count: number;
|
||||
error_message: string | null;
|
||||
processing_started_at: string | null;
|
||||
processing_completed_at: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface SearchHit {
|
||||
chunk_id: string;
|
||||
document_id: string;
|
||||
chunk_index: number;
|
||||
content: string;
|
||||
doc_title: string;
|
||||
similarity: number;
|
||||
metadata: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface CreateKnowledgeBaseReq {
|
||||
name: string;
|
||||
kb_type: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
chunk_strategy?: Record<string, unknown>;
|
||||
intent_keywords?: Record<string, unknown>;
|
||||
embedding_model?: string;
|
||||
is_enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateKnowledgeBaseReq {
|
||||
name?: string;
|
||||
kb_type?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
chunk_strategy?: Record<string, unknown>;
|
||||
intent_keywords?: Record<string, unknown>;
|
||||
embedding_model?: string;
|
||||
is_enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateDocumentReq {
|
||||
kb_id: string;
|
||||
title: string;
|
||||
doc_type?: string;
|
||||
source_type?: string;
|
||||
source_url?: string;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
// === API ===
|
||||
|
||||
export const knowledgeV2Api = {
|
||||
// Knowledge Bases
|
||||
listKnowledgeBases: async (params?: {
|
||||
kb_type?: string;
|
||||
is_enabled?: boolean;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}) => {
|
||||
const resp = await client.get('/ai/knowledge-bases', { params });
|
||||
return resp.data.data as {
|
||||
data: KnowledgeBase[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
};
|
||||
},
|
||||
|
||||
getKnowledgeBase: async (id: string) => {
|
||||
const resp = await client.get(`/ai/knowledge-bases/${id}`);
|
||||
return resp.data.data as KnowledgeBase;
|
||||
},
|
||||
|
||||
createKnowledgeBase: async (data: CreateKnowledgeBaseReq) => {
|
||||
const resp = await client.post('/ai/knowledge-bases', data);
|
||||
return resp.data.data as { id: string };
|
||||
},
|
||||
|
||||
updateKnowledgeBase: async (id: string, data: UpdateKnowledgeBaseReq) => {
|
||||
const resp = await client.put(`/ai/knowledge-bases/${id}`, data);
|
||||
return resp.data.data as { id: string };
|
||||
},
|
||||
|
||||
deleteKnowledgeBase: async (id: string) => {
|
||||
const resp = await client.delete(`/ai/knowledge-bases/${id}`);
|
||||
return resp.data.data as { id: string };
|
||||
},
|
||||
|
||||
// Documents
|
||||
listDocuments: async (
|
||||
kbId: string,
|
||||
params?: { status?: string; page?: number; page_size?: number },
|
||||
) => {
|
||||
const resp = await client.get(
|
||||
`/ai/knowledge-bases/${kbId}/documents`,
|
||||
{ params },
|
||||
);
|
||||
return resp.data.data as {
|
||||
data: KnowledgeDocument[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
};
|
||||
},
|
||||
|
||||
getDocument: async (id: string) => {
|
||||
const resp = await client.get(`/ai/documents/${id}`);
|
||||
return resp.data.data as KnowledgeDocument;
|
||||
},
|
||||
|
||||
createManualDocument: async (data: CreateDocumentReq) => {
|
||||
const resp = await client.post('/ai/documents/manual', data);
|
||||
return resp.data.data as { id: string };
|
||||
},
|
||||
|
||||
uploadDocument: async (
|
||||
kbId: string,
|
||||
file: File,
|
||||
title?: string,
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
formData.append('kb_id', kbId);
|
||||
formData.append('file', file);
|
||||
if (title) {
|
||||
formData.append('title', title);
|
||||
}
|
||||
const resp = await client.post('/ai/documents/upload', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
return resp.data.data as { id: string };
|
||||
},
|
||||
|
||||
deleteDocument: async (kbId: string, id: string) => {
|
||||
const resp = await client.delete(
|
||||
`/ai/knowledge-bases/${kbId}/documents/${id}`,
|
||||
);
|
||||
return resp.data.data as { id: string };
|
||||
},
|
||||
|
||||
// Hit Test
|
||||
hitTest: async (kbId: string, query: string, topK?: number) => {
|
||||
const resp = await client.post('/ai/documents/hit-test', {
|
||||
kb_id: kbId,
|
||||
query,
|
||||
top_k: topK,
|
||||
});
|
||||
return resp.data.data as {
|
||||
query: string;
|
||||
total: number;
|
||||
hits: SearchHit[];
|
||||
};
|
||||
},
|
||||
};
|
||||
566
apps/web/src/pages/ai/KnowledgeV2Page.tsx
Normal file
566
apps/web/src/pages/ai/KnowledgeV2Page.tsx
Normal file
@@ -0,0 +1,566 @@
|
||||
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<KnowledgeBase[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||
const [editKb, setEditKb] = useState<KnowledgeBase | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// Document drawer state
|
||||
const [docDrawerKb, setDocDrawerKb] = useState<KnowledgeBase | null>(null);
|
||||
const [docs, setDocs] = useState<KnowledgeDocument[]>([]);
|
||||
const [docsLoading, setDocsLoading] = useState(false);
|
||||
const [uploadModalOpen, setUploadModalOpen] = useState(false);
|
||||
const [uploadKbId, setUploadKbId] = useState<string>('');
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
|
||||
// Hit test state
|
||||
const [hitTestKb, setHitTestKb] = useState<KnowledgeBase | null>(null);
|
||||
const [hitTestQuery, setHitTestQuery] = useState('');
|
||||
const [hitResults, setHitResults] = useState<SearchHit[]>([]);
|
||||
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<string, { color: string; label: string }> = {
|
||||
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 <Tag color={info.color}>{info.label}</Tag>;
|
||||
};
|
||||
|
||||
const kbColumns = [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (name: string, record: KnowledgeBase) => (
|
||||
<Button type="link" onClick={() => loadDocuments(record)}>
|
||||
{name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
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) => (
|
||||
<Tag color={v ? 'green' : 'red'}>{v ? '启用' : '禁用'}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
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) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
size="small"
|
||||
icon={<UploadOutlined />}
|
||||
onClick={() => {
|
||||
setUploadKbId(record.id);
|
||||
setUploadModalOpen(true);
|
||||
}}
|
||||
>
|
||||
上传
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<SearchOutlined />}
|
||||
onClick={() => {
|
||||
setHitTestKb(record);
|
||||
setHitResults([]);
|
||||
setHitTestQuery('');
|
||||
}}
|
||||
>
|
||||
搜索测试
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setEditKb(record);
|
||||
form.setFieldsValue({
|
||||
name: record.name,
|
||||
kb_type: record.kb_type,
|
||||
description: record.description,
|
||||
is_enabled: record.is_enabled,
|
||||
});
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定删除此知识库?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
>
|
||||
<Button size="small" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const kbFormContent = (
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="知识库名称"
|
||||
rules={[{ required: true, message: '请输入知识库名称' }]}
|
||||
>
|
||||
<Input placeholder="例:高血压临床指南" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="kb_type"
|
||||
label="知识库类型"
|
||||
rules={[{ required: true, message: '请选择类型' }]}
|
||||
>
|
||||
<Select options={KB_TYPES} placeholder="选择类型" />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea rows={3} placeholder="知识库描述(可选)" />
|
||||
</Form.Item>
|
||||
<Form.Item name="is_enabled" label="启用" valuePropName="checked">
|
||||
<Switch defaultChecked />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
|
||||
const docColumns = [
|
||||
{
|
||||
title: '标题',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'doc_type',
|
||||
key: 'doc_type',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '来源',
|
||||
dataIndex: 'source_type',
|
||||
key: 'source_type',
|
||||
width: 80,
|
||||
render: (v: string) => <Tag>{v === 'upload' ? '上传' : '手动'}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 90,
|
||||
render: statusTag,
|
||||
},
|
||||
{
|
||||
title: '切片/嵌入',
|
||||
key: 'progress',
|
||||
width: 130,
|
||||
render: (_: unknown, record: KnowledgeDocument) => {
|
||||
if (record.chunk_count === 0) return '-';
|
||||
const pct = Math.round(
|
||||
(record.embedded_count / record.chunk_count) * 100,
|
||||
);
|
||||
return <Progress percent={pct} size="small" />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 70,
|
||||
render: (_: unknown, record: KnowledgeDocument) => (
|
||||
<Popconfirm
|
||||
title="确定删除此文档?"
|
||||
onConfirm={() =>
|
||||
handleDeleteDoc(record.knowledge_base_id, record.id)
|
||||
}
|
||||
>
|
||||
<Button size="small" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<DatabaseOutlined />
|
||||
知识库管理 V2
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
form.resetFields();
|
||||
setCreateModalOpen(true);
|
||||
}}
|
||||
>
|
||||
新建知识库
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={kbColumns}
|
||||
dataSource={kbs}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
total,
|
||||
pageSize: 20,
|
||||
onChange: setPage,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 创建知识库 Modal */}
|
||||
<Modal
|
||||
title="新建知识库"
|
||||
open={createModalOpen}
|
||||
onOk={handleCreate}
|
||||
onCancel={() => {
|
||||
setCreateModalOpen(false);
|
||||
form.resetFields();
|
||||
}}
|
||||
okText="创建"
|
||||
>
|
||||
{kbFormContent}
|
||||
</Modal>
|
||||
|
||||
{/* 编辑知识库 Modal */}
|
||||
<Modal
|
||||
title="编辑知识库"
|
||||
open={!!editKb}
|
||||
onOk={handleUpdate}
|
||||
onCancel={() => {
|
||||
setEditKb(null);
|
||||
form.resetFields();
|
||||
}}
|
||||
okText="保存"
|
||||
>
|
||||
{kbFormContent}
|
||||
</Modal>
|
||||
|
||||
{/* 文档列表 Drawer */}
|
||||
<Drawer
|
||||
title={
|
||||
docDrawerKb
|
||||
? `${docDrawerKb.name} — 文档列表`
|
||||
: '文档列表'
|
||||
}
|
||||
open={!!docDrawerKb}
|
||||
onClose={() => setDocDrawerKb(null)}
|
||||
width={720}
|
||||
>
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={docColumns}
|
||||
dataSource={docs}
|
||||
loading={docsLoading}
|
||||
pagination={false}
|
||||
size="small"
|
||||
locale={{ emptyText: <Empty description="暂无文档" /> }}
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
{/* 上传文档 Modal */}
|
||||
<Modal
|
||||
title="上传文档"
|
||||
open={uploadModalOpen}
|
||||
onOk={handleUpload}
|
||||
onCancel={() => {
|
||||
setUploadModalOpen(false);
|
||||
setFileList([]);
|
||||
}}
|
||||
okText="上传"
|
||||
>
|
||||
<Upload
|
||||
beforeUpload={() => false}
|
||||
maxCount={1}
|
||||
fileList={fileList}
|
||||
onChange={({ fileList: fl }) => setFileList(fl)}
|
||||
accept=".pdf,.txt,.md,.docx,.xlsx"
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>选择文件</Button>
|
||||
</Upload>
|
||||
<div style={{ marginTop: 8, color: '#999', fontSize: 12 }}>
|
||||
支持 PDF、TXT、Markdown、DOCX、XLSX,最大 20MB
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Hit Test Drawer */}
|
||||
<Drawer
|
||||
title={
|
||||
hitTestKb ? `${hitTestKb.name} — 向量搜索测试` : '搜索测试'
|
||||
}
|
||||
open={!!hitTestKb}
|
||||
onClose={() => {
|
||||
setHitTestKb(null);
|
||||
setHitResults([]);
|
||||
}}
|
||||
width={600}
|
||||
>
|
||||
<Space.Compact style={{ width: '100%', marginBottom: 16 }}>
|
||||
<Input
|
||||
placeholder="输入搜索文本..."
|
||||
value={hitTestQuery}
|
||||
onChange={(e) => setHitTestQuery(e.target.value)}
|
||||
onPressEnter={handleHitTest}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SearchOutlined />}
|
||||
loading={hitTestLoading}
|
||||
onClick={handleHitTest}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
|
||||
<List
|
||||
dataSource={hitResults}
|
||||
locale={{ emptyText: <Empty description="输入查询后点击搜索" /> }}
|
||||
renderItem={(item) => (
|
||||
<List.Item key={item.chunk_id}>
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
<FileTextOutlined style={{ fontSize: 20, color: '#1890ff' }} />
|
||||
}
|
||||
title={
|
||||
<Space>
|
||||
<span>{item.doc_title}</span>
|
||||
<Tag color="blue">切片 #{item.chunk_index}</Tag>
|
||||
<Tag color="green">
|
||||
相似度 {(item.similarity * 100).toFixed(1)}%
|
||||
</Tag>
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<div
|
||||
style={{
|
||||
maxHeight: 120,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
color: '#666',
|
||||
}}
|
||||
>
|
||||
{item.content}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user